From 4dc4c285f1418cc2b39b37a23426075085feec14 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Fri, 15 Dec 2023 19:17:49 -0600 Subject: [PATCH] Heatmap: Update tooltip UX (#79429) --- .../VizTooltip/VizTooltipContent.tsx | 10 +- .../panel/heatmap/HeatmapHoverView.tsx | 112 ++++++++---------- .../heatmap/tooltip/tooltipUtils.test.ts | 8 +- .../plugins/panel/heatmap/tooltip/utils.ts | 15 ++- 4 files changed, 71 insertions(+), 74 deletions(-) diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx index f131472b71a..c7558bcf288 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx @@ -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) = ); })} - {customContent &&
{customContent}
} + {customContent?.map((content, i) => { + return ( +
+ {content} +
+ ); + })} ); }; diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx index 23a88663a3a..13bec6d02ef 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx @@ -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; @@ -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( + + ); + } + + // Color scale if (colorPalette && showColorScale) { - return ( + content.push( { - 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 ( - - ); - } - - return null; + return content; }; // @TODO remove this when adding annotations support @@ -299,11 +291,7 @@ const HeatmapHoverCell = ({ return (
- + {isPinned && }
diff --git a/public/app/plugins/panel/heatmap/tooltip/tooltipUtils.test.ts b/public/app/plugins/panel/heatmap/tooltip/tooltipUtils.test.ts index 4ef58686791..cbc0706c4a2 100644 --- a/public/app/plugins/panel/heatmap/tooltip/tooltipUtils.test.ts +++ b/public/app/plugins/panel/heatmap/tooltip/tooltipUtils.test.ts @@ -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); diff --git a/public/app/plugins/panel/heatmap/tooltip/utils.ts b/public/app/plugins/panel/heatmap/tooltip/utils.ts index 78b633ade0b..58114245d4e 100644 --- a/public/app/plugins/panel/heatmap/tooltip/utils.ts +++ b/public/app/plugins/panel/heatmap/tooltip/utils.ts @@ -27,16 +27,18 @@ const conversions: Record = { 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}`; };