FlameGraph: Add option to align text left or right (#68893)

This commit is contained in:
Andrej Ocenas 2023-05-25 11:08:03 +02:00 committed by GitHub
parent 25b89babac
commit 55bba165b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 36 deletions

View File

@ -18,6 +18,8 @@ export const availableIconsIndex = {
'angle-left': true,
'angle-right': true,
'angle-up': true,
'align-left': true,
'align-right': true,
apps: true,
arrow: true,
'arrow-down': true,

View File

@ -46,6 +46,7 @@ describe('FlameGraph', () => {
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
selectedView={selectedView}
textAlign={'left'}
/>
);
};

View File

@ -25,7 +25,7 @@ import { CoreApp } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { PIXELS_PER_LEVEL } from '../../constants';
import { SelectedView, ContextMenuData } from '../types';
import { SelectedView, ContextMenuData, TextAlign } from '../types';
import FlameGraphContextMenu from './FlameGraphContextMenu';
import FlameGraphMetadata from './FlameGraphMetadata';
@ -48,6 +48,7 @@ type Props = {
setRangeMax: (range: number) => void;
selectedView: SelectedView;
style?: React.CSSProperties;
textAlign: TextAlign;
};
const FlameGraph = ({
@ -64,6 +65,7 @@ const FlameGraph = ({
setRangeMin,
setRangeMax,
selectedView,
textAlign,
}: Props) => {
const styles = useStyles2(getStyles);
const totalTicks = data.getValue(0);
@ -119,10 +121,21 @@ const FlameGraph = ({
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick);
for (const rect of dimensions) {
// Render each rectangle based on the computed dimensions
renderRect(ctx, rect, totalTicks, rangeMin, rangeMax, search, levelIndex, topLevelIndex, foundLabels);
renderRect(
ctx,
rect,
totalTicks,
rangeMin,
rangeMax,
search,
levelIndex,
topLevelIndex,
foundLabels,
textAlign
);
}
}
}, [data, levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, topLevelIndex, foundLabels]);
}, [data, levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, topLevelIndex, foundLabels, textAlign]);
useEffect(() => {
if (graphRef.current) {

View File

@ -8,6 +8,7 @@ import {
LABEL_THRESHOLD,
PIXELS_PER_LEVEL,
} from '../../constants';
import { TextAlign } from '../types';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
@ -80,7 +81,8 @@ export function renderRect(
query: string,
levelIndex: number,
topLevelIndex: number,
foundNames: Set<string>
foundNames: Set<string>,
textAlign: TextAlign
) {
if (rect.width < HIDE_THRESHOLD) {
return;
@ -110,18 +112,40 @@ export function renderRect(
ctx.fill();
if (!rect.collapsed && rect.width >= LABEL_THRESHOLD) {
ctx.save();
ctx.clip(); // so text does not overflow
ctx.fillStyle = '#222';
ctx.fillText(
`${name} (${rect.unitLabel})`,
Math.max(rect.x, 0) + BAR_TEXT_PADDING_LEFT,
rect.y + PIXELS_PER_LEVEL / 2
);
ctx.restore();
renderLabel(ctx, name, rect, textAlign);
}
}
// Renders a text inside the node rectangle. It allows setting alignment of the text left or right which takes effect
// when text is too long to fit in the rectangle.
function renderLabel(ctx: CanvasRenderingContext2D, name: string, rect: RectData, textAlign: TextAlign) {
ctx.save();
ctx.clip(); // so text does not overflow
ctx.fillStyle = '#222';
// We only measure name here instead of full label because of how we deal with the units and aligning later.
const measure = ctx.measureText(name);
const spaceForTextInRect = rect.width - BAR_TEXT_PADDING_LEFT;
let label = `${name} (${rect.unitLabel})`;
let labelX = Math.max(rect.x, 0) + BAR_TEXT_PADDING_LEFT;
// We use the desired alignment only if there is not enough space for the text, otherwise we keep left alignment as
// that will already show full text.
if (measure.width > spaceForTextInRect) {
ctx.textAlign = textAlign;
// If aligned to the right we don't want to take the space with the unit label as the assumption is user wants to
// mainly see the name. This also reflects how pyro/flamegraph works.
if (textAlign === 'right') {
label = name;
labelX = rect.x + rect.width - BAR_TEXT_PADDING_LEFT;
}
}
ctx.fillText(label, labelX, rect.y + PIXELS_PER_LEVEL / 2);
ctx.restore();
}
/**
* Returns the X position of the bar. totalTicks * rangeMin is to adjust for any current zoom. So if we zoom to a
* section of the graph we align and shift the X coordinates accordingly.

View File

@ -11,7 +11,7 @@ import FlameGraph from './FlameGraph/FlameGraph';
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './FlameGraph/dataTransform';
import FlameGraphHeader from './FlameGraphHeader';
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
import { SelectedView } from './types';
import { SelectedView, TextAlign } from './types';
type Props = {
data?: DataFrame;
@ -26,6 +26,8 @@ const FlameGraphContainer = (props: Props) => {
const [search, setSearch] = useState('');
const [selectedView, setSelectedView] = useState(SelectedView.Both);
const [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
const [textAlign, setTextAlign] = useState<TextAlign>('left');
const theme = useTheme2();
const [dataContainer, levels] = useMemo((): [FlameGraphDataContainer, LevelItem[][]] | [undefined, undefined] => {
@ -75,6 +77,8 @@ const FlameGraphContainer = (props: Props) => {
selectedView={selectedView}
setSelectedView={setSelectedView}
containerWidth={containerWidth}
textAlign={textAlign}
onTextAlignChange={setTextAlign}
/>
<div className={styles.body}>
@ -108,6 +112,7 @@ const FlameGraphContainer = (props: Props) => {
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
selectedView={selectedView}
textAlign={textAlign}
/>
)}
</div>
@ -125,7 +130,7 @@ function getStyles(theme: GrafanaTheme2) {
flex: '1 1 0',
flexDirection: 'column',
minHeight: 0,
gap: theme.spacing(2),
gap: theme.spacing(1),
}),
body: css({
display: 'flex',

View File

@ -25,6 +25,8 @@ describe('FlameGraphHeader', () => {
selectedView={selectedView}
setSelectedView={setSelectedView}
containerWidth={1600}
onTextAlignChange={jest.fn()}
textAlign={'left'}
/>
);
};

View File

@ -3,14 +3,14 @@ import React, { useEffect, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import usePrevious from 'react-use/lib/usePrevious';
import { GrafanaTheme2, CoreApp } from '@grafana/data';
import { CoreApp, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { Button, Input, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { config } from '../../../../core/config';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
import { SelectedView } from './types';
import { SelectedView, TextAlign } from './types';
type Props = {
app: CoreApp;
@ -23,6 +23,8 @@ type Props = {
selectedView: SelectedView;
setSelectedView: (view: SelectedView) => void;
containerWidth: number;
textAlign: TextAlign;
onTextAlignChange: (align: TextAlign) => void;
};
const FlameGraphHeader = ({
@ -36,19 +38,15 @@ const FlameGraphHeader = ({
selectedView,
setSelectedView,
containerWidth,
textAlign,
onTextAlignChange,
}: Props) => {
const styles = useStyles2((theme) => getStyles(theme, app));
let viewOptions: Array<{ value: SelectedView; label: string; description: string }> = [
{ value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' },
{ value: SelectedView.FlameGraph, label: 'Flame Graph', description: 'Only show flame graph' },
];
if (containerWidth >= MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH) {
viewOptions.push({
value: SelectedView.Both,
label: 'Both',
description: 'Show both the top table and flame graph',
function interaction(name: string, context: Record<string, string | number>) {
reportInteraction(`grafana_flamegraph_${name}`, {
app,
grafana_version: config.buildInfo.version,
...context,
});
}
@ -83,15 +81,23 @@ const FlameGraphHeader = ({
</div>
<div className={styles.rightContainer}>
<RadioButtonGroup<TextAlign>
size="sm"
disabled={selectedView === SelectedView.TopTable}
options={alignOptions}
value={textAlign}
onChange={(val) => {
interaction('text_align_selected', { align: val });
onTextAlignChange(val);
}}
className={styles.buttonSpacing}
/>
<RadioButtonGroup<SelectedView>
options={viewOptions}
size="sm"
options={getViewOptions(containerWidth)}
value={selectedView}
onChange={(view) => {
reportInteraction('grafana_flamegraph_view_selected', {
app,
grafana_version: config.buildInfo.version,
view,
});
interaction('view_selected', { view });
setSelectedView(view);
}}
/>
@ -100,6 +106,28 @@ const FlameGraphHeader = ({
);
};
const alignOptions: Array<SelectableValue<TextAlign>> = [
{ value: 'left', description: 'Align text left', icon: 'align-left' },
{ value: 'right', description: 'Align text right', icon: 'align-right' },
];
function getViewOptions(width: number): Array<SelectableValue<SelectedView>> {
let viewOptions: Array<{ value: SelectedView; label: string; description: string }> = [
{ value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' },
{ value: SelectedView.FlameGraph, label: 'Flame Graph', description: 'Only show flame graph' },
];
if (width >= MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH) {
viewOptions.push({
value: SelectedView.Both,
label: 'Both',
description: 'Show both the top table and flame graph',
});
}
return viewOptions;
}
function useSearchInput(
search: string,
setSearch: (search: string) => void
@ -133,9 +161,14 @@ const getStyles = (theme: GrafanaTheme2, app: CoreApp) => ({
width: 100%;
background: ${theme.colors.background.primary};
top: 0;
height: 50px;
z-index: ${theme.zIndex.navbarFixed};
${app === CoreApp.Explore ? 'position: sticky; margin-bottom: 8px; padding-top: 9px' : ''};
${app === CoreApp.Explore
? css`
position: sticky;
padding-bottom: ${theme.spacing(1)};
padding-top: ${theme.spacing(1)};
`
: ''};
`,
inputContainer: css`
float: left;
@ -147,6 +180,9 @@ const getStyles = (theme: GrafanaTheme2, app: CoreApp) => ({
rightContainer: css`
float: right;
`,
buttonSpacing: css`
margin-right: ${theme.spacing(1)};
`,
});
export default FlameGraphHeader;

View File

@ -44,3 +44,5 @@ export type TopTableValue = {
value: number;
unitValue: string;
};
export type TextAlign = 'left' | 'right';