Heatmap: Update tooltip UX (#79429)

This commit is contained in:
Adela Almasan
2023-12-15 19:17:49 -06:00
committed by GitHub
parent b166bdc3fc
commit 4dc4c285f1
4 changed files with 71 additions and 74 deletions

View File

@ -10,7 +10,7 @@ import { LabelValue } from './types';
interface Props {
contentLabelValue: LabelValue[];
customContent?: ReactElement | null;
customContent?: ReactElement[];
}
export const VizTooltipContent = ({ contentLabelValue, customContent }: Props) => {
@ -35,7 +35,13 @@ export const VizTooltipContent = ({ contentLabelValue, customContent }: Props) =
);
})}
</div>
{customContent && <div className={styles.customContentPadding}>{customContent}</div>}
{customContent?.map((content, i) => {
return (
<div key={i} className={styles.customContentPadding}>
{content}
</div>
);
})}
</div>
);
};

View File

@ -20,7 +20,7 @@ import { useStyles2 } from '@grafana/ui';
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter';
import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader';
import { ColorIndicator, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
import { ColorIndicator, ColorPlacement, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
@ -28,7 +28,7 @@ import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverVi
import { HeatmapData } from './fields';
import { renderHistogram } from './renderHistogram';
import { getSparseCellMinMax, formatMilliseconds, getFieldFromData, getHoverCellColor } from './tooltip/utils';
import { getSparseCellMinMax, getFieldFromData, getHoverCellColor, formatMilliseconds } from './tooltip/utils';
interface Props {
dataIdxs: Array<number | null>;
@ -213,37 +213,64 @@ const HeatmapHoverCell = ({
const { cellColor, colorPalette } = getHoverCellColor(data, index);
const getLabelValue = (): LabelValue[] => {
const getContentLabels = (): LabelValue[] => {
if (nonNumericOrdinalDisplay) {
return [{ label: 'Name', value: nonNumericOrdinalDisplay }];
}
switch (data.yLayout) {
case HeatmapCellLayout.unknown:
return [{ label: '', value: yDisp(yBucketMin) }];
}
return [
{
label: getFieldDisplayName(countField, data.heatmap),
value: data.display!(count),
color: cellColor ?? '#FFF',
colorIndicator: ColorIndicator.value,
label: 'Bucket',
value: `${yDisp(yBucketMin)}` + '-' + `${yDisp(yBucketMax)}`,
},
];
};
const getHeaderLabel = (): LabelValue => {
if (nonNumericOrdinalDisplay) {
return { label: 'Name', value: nonNumericOrdinalDisplay };
}
switch (data.yLayout) {
case HeatmapCellLayout.unknown:
return { label: '', value: yDisp(yBucketMin) };
}
return {
label: 'Bucket',
value: `${yDisp(yBucketMin)}` + '-' + `${yDisp(yBucketMax)}`,
label: '',
value: xDisp(xBucketMax)!,
};
};
// Color scale
const getCustomValueDisplay = (): ReactElement | null => {
const getContentLabelValue = (): LabelValue[] => {
const fromToInt: LabelValue[] = interval ? [{ label: 'Duration', value: formatMilliseconds(interval) }] : [];
return [
{
label: getFieldDisplayName(countField, data.heatmap),
value: data.display!(count),
color: cellColor ?? '#FFF',
colorPlacement: ColorPlacement.trailing,
colorIndicator: ColorIndicator.value,
},
...getContentLabels(),
...fromToInt,
];
};
const getCustomContent = () => {
let content: ReactElement[] = [];
// Histogram
if (showHistogram) {
content.push(
<canvas
width={histCanWidth}
height={histCanHeight}
ref={can}
style={{ width: histCssWidth + 'px', height: histCssHeight + 'px' }}
/>
);
}
// Color scale
if (colorPalette && showColorScale) {
return (
content.push(
<ColorScale
colorPalette={colorPalette}
min={data.heatmapColors?.minValue!}
@ -254,42 +281,7 @@ const HeatmapHoverCell = ({
);
}
return null;
};
const getContentLabelValue = (): LabelValue[] => {
let fromToInt = [
{
label: 'From',
value: xDisp(xBucketMin)!,
},
];
if (data.xLayout !== HeatmapCellLayout.unknown) {
fromToInt.push({ label: 'To', value: xDisp(xBucketMax)! });
if (interval) {
const formattedString = formatMilliseconds(interval);
fromToInt.push({ label: 'Interval', value: formattedString });
}
}
return fromToInt;
};
const getCustomContent = (): ReactElement | null => {
if (showHistogram) {
return (
<canvas
width={histCanWidth}
height={histCanHeight}
ref={can}
style={{ width: histCssWidth + 'px', height: histCssHeight + 'px' }}
/>
);
}
return null;
return content;
};
// @TODO remove this when adding annotations support
@ -299,11 +291,7 @@ const HeatmapHoverCell = ({
return (
<div className={styles.wrapper}>
<VizTooltipHeader
headerLabel={getHeaderLabel()}
keyValuePairs={getLabelValue()}
customValueDisplay={getCustomValueDisplay()}
/>
<VizTooltipHeader headerLabel={getHeaderLabel()} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} customContent={getCustomContent()} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={canAnnotate} />}
</div>

View File

@ -4,19 +4,19 @@ describe('heatmap tooltip utils', () => {
it('converts ms to appropriate unit', async () => {
let msToFormat = 10;
let formatted = formatMilliseconds(msToFormat);
expect(formatted).toBe('10 milliseconds');
expect(formatted).toBe('10 ms');
msToFormat = 1000;
formatted = formatMilliseconds(msToFormat);
expect(formatted).toBe('1 second');
expect(formatted).toBe('1 s');
msToFormat = 1000 * 120;
formatted = formatMilliseconds(msToFormat);
expect(formatted).toBe('2 minutes');
expect(formatted).toBe('2 m');
msToFormat = 1000 * 60 * 60;
formatted = formatMilliseconds(msToFormat);
expect(formatted).toBe('1 hour');
expect(formatted).toBe('1 h');
msToFormat = 1000 * 60 * 60 * 24;
formatted = formatMilliseconds(msToFormat);

View File

@ -27,16 +27,18 @@ const conversions: Record<string, number> = {
month: 1000 * 60 * 60 * 24 * 30,
week: 1000 * 60 * 60 * 24 * 7,
day: 1000 * 60 * 60 * 24,
hour: 1000 * 60 * 60,
minute: 1000 * 60,
second: 1000,
millisecond: 1,
h: 1000 * 60 * 60,
m: 1000 * 60,
s: 1000,
ms: 1,
};
const noPluralize = new Set(['ms', 's', 'm', 'h']);
// @TODO: display "~ 1 year/month"?
export const formatMilliseconds = (milliseconds: number) => {
let value = 1;
let unit = 'millisecond';
let unit = 'ms';
for (unit in conversions) {
if (milliseconds >= conversions[unit]) {
@ -45,7 +47,8 @@ export const formatMilliseconds = (milliseconds: number) => {
}
}
const unitString = value === 1 ? unit : unit + 's';
const plural = value !== 1 && !noPluralize.has(unit);
const unitString = plural ? unit + 's' : unit;
return `${value} ${unitString}`;
};