mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 03:13:49 +08:00
Heatmap: Update tooltip UX (#79429)
This commit is contained in:
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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}`;
|
||||
};
|
||||
|
Reference in New Issue
Block a user