From 1ec04243dabcce54c3e3f21b4c10247e5cf83428 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Fri, 5 Jan 2024 13:11:24 -0600 Subject: [PATCH] Heatmap: All tooltip mode selector (#79956) Co-authored-by: Adela Almasan Co-authored-by: nmarrs --- .../heatmap/panelcfg/schema-reference.md | 12 +- .../panelcfg/x/HeatmapPanelCfg_types.gen.ts | 6 +- .../uPlot/plugins/TooltipPlugin2.tsx | 13 +- .../panel/heatmap/HeatmapHoverView.tsx | 181 +++++++++++++----- .../plugins/panel/heatmap/HeatmapPanel.tsx | 8 +- .../plugins/panel/heatmap/migrations.test.ts | 4 +- .../app/plugins/panel/heatmap/migrations.ts | 17 +- public/app/plugins/panel/heatmap/module.tsx | 20 +- public/app/plugins/panel/heatmap/panelcfg.cue | 6 +- .../app/plugins/panel/heatmap/panelcfg.gen.ts | 6 +- 10 files changed, 198 insertions(+), 75 deletions(-) diff --git a/docs/sources/developers/kinds/composable/heatmap/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/heatmap/panelcfg/schema-reference.md index 4427b6c99e9..87b7fbfdd21 100644 --- a/docs/sources/developers/kinds/composable/heatmap/panelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/heatmap/panelcfg/schema-reference.md @@ -126,7 +126,7 @@ Controls tooltip options | Property | Type | Required | Default | Description | |------------------|---------|----------|---------|----------------------------------------------------------------| -| `show` | boolean | **Yes** | | Controls if the tooltip is shown | +| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | | `showColorScale` | boolean | No | | Controls if the tooltip shows a color scale in header | | `yHistogram` | boolean | No | | Controls if the tooltip shows a histogram of the y-axis values | @@ -138,7 +138,7 @@ Controls tooltip options | `exemplars` | [ExemplarConfig](#exemplarconfig) | **Yes** | | Controls exemplar options | | `legend` | [HeatmapLegend](#heatmaplegend) | **Yes** | | Controls legend options | | `showValue` | string | **Yes** | | | *{
layout: ui.HeatmapCellLayout & "auto" // TODO: fix after remove when https://github.com/grafana/cuetsy/issues/74 is fixed
}
Controls the display of the value in the cell | -| `tooltip` | [HeatmapTooltip](#heatmaptooltip) | **Yes** | | Controls tooltip options | +| `tooltip` | [object](#tooltip) | **Yes** | `map[mode:single showColorScale:false yHistogram:false]` | Controls tooltip options | | `yAxis` | [YAxisConfig](#yaxisconfig) | **Yes** | | Configuration options for the yAxis | | `calculate` | boolean | No | `false` | Controls if the heatmap should be calculated from data | | `calculation` | [HeatmapCalculationOptions](#heatmapcalculationoptions) | No | | | @@ -237,4 +237,12 @@ Filters values between a given range |----------|-----------------------------------|----------|---------|-------------| | `object` | Possible types are: [](#), [](#). | | | +### Tooltip + +Controls tooltip options + +| Property | Type | Required | Default | Description | +|----------|-----------------------------------|----------|---------|-------------| +| `object` | Possible types are: [](#), [](#). | | | + diff --git a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts index 4ed9a2f5450..ab654c3d887 100644 --- a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts @@ -130,9 +130,9 @@ export interface FilterValueRange { */ export interface HeatmapTooltip { /** - * Controls if the tooltip is shown + * Controls how the tooltip is shown */ - show: boolean; + mode: ui.TooltipDisplayMode; /** * Controls if the tooltip shows a color scale in header */ @@ -266,7 +266,7 @@ export const defaultOptions: Partial = { }, showValue: ui.VisibilityMode.Auto, tooltip: { - show: true, + mode: ui.TooltipDisplayMode.Single, yHistogram: false, showColorScale: false, }, diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index 76cd2eca5e3..c5968dafdbc 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -134,6 +134,7 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false, winHeight = htmlEl.clientHeight - 5; }); + let seriesIdxs: Array = plot?.cursor.idxs!.slice()!; let closestSeriesIdx: number | null = null; let pendingRender = false; @@ -192,9 +193,7 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false, style: _style, isPinned: _isPinned, isHovering: _isHovering, - contents: _isHovering - ? renderRef.current(_plot!, _plot!.cursor.idxs!, closestSeriesIdx, _isPinned, dismiss) - : null, + contents: _isHovering ? renderRef.current(_plot!, seriesIdxs, closestSeriesIdx, _isPinned, dismiss) : null, dismiss, }; @@ -324,12 +323,12 @@ export const TooltipPlugin2 = ({ config, hoverMode, render, clientZoom = false, // fires on data value hovers/unhovers (before setSeries) config.addHook('setLegend', (u) => { - let hoveredSeriesIdx = _plot!.cursor.idxs!.findIndex((v, i) => i > 0 && v != null); + seriesIdxs = _plot?.cursor!.idxs!.slice()!; + + let hoveredSeriesIdx = seriesIdxs.findIndex((v, i) => i > 0 && v != null); let _isHoveringNow = hoveredSeriesIdx !== -1; - // in mode: 2 uPlot won't fire the proximity-based setSeries (below) - // so we set closestSeriesIdx here instead - // TODO: setSeries only fires for TimeSeries & Trend...not state timeline or statsus history + // setSeries may not fire if focus.prox is not set, so we set closestSeriesIdx here instead if (hoverMode === TooltipHoverMode.xyOne) { closestSeriesIdx = hoveredSeriesIdx; } diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx index f5ea9eee339..2eff6a8a4a6 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx @@ -16,7 +16,7 @@ import { ScopedVars, } from '@grafana/data'; import { HeatmapCellLayout } from '@grafana/schema'; -import { useStyles2 } from '@grafana/ui'; +import { TooltipDisplayMode, 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'; @@ -31,6 +31,7 @@ import { renderHistogram } from './renderHistogram'; import { formatMilliseconds, getFieldFromData, getHoverCellColor, getSparseCellMinMax } from './tooltip/utils'; interface Props { + mode: TooltipDisplayMode; dataIdxs: Array; seriesIdx: number | null | undefined; dataRef: React.MutableRefObject; @@ -65,11 +66,10 @@ const HeatmapHoverCell = ({ showHistogram, isPinned, canAnnotate, - panelData, showColorScale = false, scopedVars, replaceVars, - dismiss, + mode, }: Props) => { const index = dataIdxs[1]!; const data = dataRef.current; @@ -102,8 +102,6 @@ const HeatmapHoverCell = ({ const meta = readHeatmapRowsCustomMeta(data.heatmap); const yDisp = yField?.display ? (v: string) => formattedValueToString(yField.display!(v)) : (v: string) => `${v}`; - const yValueIdx = index % data.yBucketCount! ?? 0; - let interval = xField?.config.interval; let yBucketMin: string; @@ -114,9 +112,15 @@ const HeatmapHoverCell = ({ let nonNumericOrdinalDisplay: string | undefined = undefined; - if (isSparse) { - ({ xBucketMin, xBucketMax, yBucketMin, yBucketMax } = getSparseCellMinMax(data!, index)); - } else { + let contentLabelValue: LabelValue[] = []; + + const getYValueIndex = (idx: number) => { + return idx % data.yBucketCount! ?? 0; + }; + + let yValueIdx = getYValueIndex(index); + + const getData = (idx: number = index) => { if (meta.yOrdinalDisplay) { const yMinIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx - 1 : yValueIdx; const yMaxIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx : yValueIdx + 1; @@ -154,15 +158,130 @@ const HeatmapHoverCell = ({ } if (data.xLayout === HeatmapCellLayout.le) { - xBucketMax = xVals[index]; + xBucketMax = xVals[idx]; xBucketMin = xBucketMax - data.xBucketSize!; } else { - xBucketMin = xVals[index]; + xBucketMin = xVals[idx]; xBucketMax = xBucketMin + data.xBucketSize!; } + }; + + if (isSparse) { + ({ xBucketMin, xBucketMax, yBucketMin, yBucketMax } = getSparseCellMinMax(data!, index)); + } else { + getData(); } - const count = countVals?.[index]; + const { cellColor, colorPalette } = getHoverCellColor(data, index); + + const getDisplayData = (fromIdx: number, toIdx: number) => { + let vals = []; + for (let idx = fromIdx; idx <= toIdx; idx++) { + if (!countVals?.[idx]) { + continue; + } + + const color = getHoverCellColor(data, idx).cellColor; + count = getCountValue(idx); + + if (isSparse) { + ({ xBucketMin, xBucketMax, yBucketMin, yBucketMax } = getSparseCellMinMax(data!, idx)); + } else { + yValueIdx = getYValueIndex(idx); + getData(idx); + } + + const { label, value } = getContentLabels()[0]; + + vals.push({ + label, + value, + color: color ?? '#FFF', + isActive: index === idx, + }); + } + + return vals; + }; + + const getContentLabels = (): LabelValue[] => { + const isMulti = mode === TooltipDisplayMode.Multi && !isPinned; + + if (nonNumericOrdinalDisplay) { + return isMulti + ? [{ label: `Name ${nonNumericOrdinalDisplay}`, value: data.display!(count) }] + : [{ label: 'Name', value: nonNumericOrdinalDisplay }]; + } + + switch (data.yLayout) { + case HeatmapCellLayout.unknown: + return isMulti + ? [{ label: yDisp(yBucketMin), value: data.display!(count) }] + : [{ label: '', value: yDisp(yBucketMin) }]; + } + + return isMulti + ? [ + { + label: `Bucket ${yDisp(yBucketMin)}` + '-' + `${yDisp(yBucketMax)}`, + value: data.display!(count), + }, + ] + : [ + { + label: 'Bucket', + value: `${yDisp(yBucketMin)}` + '-' + `${yDisp(yBucketMax)}`, + }, + ]; + }; + + const getCountValue = (idx: number) => { + return countVals?.[idx]; + }; + + let count = getCountValue(index); + + if (mode === TooltipDisplayMode.Single || isPinned) { + const fromToInt: LabelValue[] = interval ? [{ label: 'Duration', value: formatMilliseconds(interval) }] : []; + + contentLabelValue = [ + { + label: getFieldDisplayName(countField, data.heatmap), + value: data.display!(count), + color: cellColor ?? '#FFF', + colorPlacement: ColorPlacement.trailing, + colorIndicator: ColorIndicator.value, + }, + ...getContentLabels(), + ...fromToInt, + ]; + } + + if (mode === TooltipDisplayMode.Multi && !isPinned) { + let xVal = xField.values[index]; + let fromIdx = index; + let toIdx = index; + + while (xField.values[fromIdx - 1] === xVal) { + fromIdx--; + } + + while (xField.values[toIdx + 1] === xVal) { + toIdx++; + } + + const vals: LabelValue[] = getDisplayData(fromIdx, toIdx); + vals.forEach((val) => { + contentLabelValue.push({ + label: val.label, + value: val.value, + color: val.color ?? '#FFF', + colorIndicator: ColorIndicator.value, + colorPlacement: ColorPlacement.trailing, + isActive: val.isActive, + }); + }); + } const visibleFields = data.heatmap?.fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip)); const links: Array> = []; @@ -203,7 +322,7 @@ const HeatmapHoverCell = ({ useEffect( () => { - if (showHistogram && xVals != null && countVals != null) { + if (showHistogram && xVals != null && countVals != null && mode === TooltipDisplayMode.Single) { renderHistogram(can, histCanWidth, histCanHeight, xVals, countVals, index, data.yBucketCount!); } }, @@ -211,26 +330,6 @@ const HeatmapHoverCell = ({ [index] ); - const { cellColor, colorPalette } = getHoverCellColor(data, index); - - const getContentLabels = (): 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)}`, - }, - ]; - }; - const getHeaderLabel = (): LabelValue => { return { label: '', @@ -239,23 +338,15 @@ const HeatmapHoverCell = ({ }; 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, - ]; + return contentLabelValue; }; const getCustomContent = () => { let content: ReactElement[] = []; + if (mode !== TooltipDisplayMode.Single) { + return content; + } + // Histogram if (showHistogram) { content.push( diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index 5e4e545925c..a3592699709 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -18,6 +18,7 @@ import { Portal, ScaleDistribution, TooltipPlugin2, + TooltipDisplayMode, ZoomPlugin, UPlotChart, usePanelContext, @@ -167,7 +168,7 @@ export const HeatmapPanel = ({ theme, eventBus, onhover: !showNewVizTooltips ? onhover : null, - onclick: !showNewVizTooltips && options.tooltip.show ? onclick : null, + onclick: !showNewVizTooltips && options.tooltip.mode !== TooltipDisplayMode.None ? onclick : null, isToolTipOpen, timeZone, getTimeRange: () => timeRangeRef.current, @@ -232,7 +233,7 @@ export const HeatmapPanel = ({ {/*children ? children(config, alignedFrame) : null*/} {!showNewVizTooltips && } - {showNewVizTooltips && options.tooltip.show && ( + {showNewVizTooltips && options.tooltip.mode !== TooltipDisplayMode.None && ( { return ( {!showNewVizTooltips && ( - {hover && options.tooltip.show && ( + {hover && options.tooltip.mode !== TooltipDisplayMode.None && ( { }, "showValue": "never", "tooltip": { - "show": true, + "mode": "single", "yHistogram": true, }, "yAxis": { @@ -131,7 +131,7 @@ describe('Heatmap Migrations', () => { }, "showValue": "never", "tooltip": { - "show": false, + "mode": "none", "yHistogram": false, }, "yAxis": { diff --git a/public/app/plugins/panel/heatmap/migrations.ts b/public/app/plugins/panel/heatmap/migrations.ts index 5445ab13745..325159e5758 100644 --- a/public/app/plugins/panel/heatmap/migrations.ts +++ b/public/app/plugins/panel/heatmap/migrations.ts @@ -7,6 +7,7 @@ import { HeatmapCalculationMode, HeatmapCalculationOptions, } from '@grafana/schema'; +import { TooltipDisplayMode } from '@grafana/ui'; import { colorSchemes } from './palettes'; import { Options, defaultOptions, HeatmapColorMode } from './types'; @@ -17,6 +18,20 @@ export const heatmapMigrationHandler = (panel: PanelModel): Partial => if (Object.keys(panel.options ?? {}).length === 0) { return heatmapChangedHandler(panel, 'heatmap', { angular: panel }, panel.fieldConfig); } + + // multi tooltip mode in 10.3+ + let showTooltip = panel.options?.tooltip?.show; + if (showTooltip !== undefined) { + if (showTooltip === true) { + panel.options.tooltip.mode = TooltipDisplayMode.Single; + } else if (showTooltip === false) { + panel.options.tooltip.mode = TooltipDisplayMode.None; + } + + // Remove old tooltip option + delete panel.options.tooltip?.show; + } + return panel.options; }; @@ -111,7 +126,7 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS }, showValue: VisibilityMode.Never, tooltip: { - show: Boolean(angular.tooltip?.show), + mode: Boolean(angular.tooltip?.show) ? TooltipDisplayMode.Single : TooltipDisplayMode.None, yHistogram: Boolean(angular.tooltip?.showHistogram), }, exemplars: { diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index da2d05509f6..30d7adfa451 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -9,6 +9,7 @@ import { ScaleDistributionConfig, HeatmapCellLayout, } from '@grafana/schema'; +import { TooltipDisplayMode } from '@grafana/ui'; import { addHideFrom, ScaleDistributionEditor } from '@grafana/ui/src/options/builder'; import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; import { addHeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/editor/helper'; @@ -391,11 +392,18 @@ export const plugin = new PanelPlugin(HeatmapPanel) category = ['Tooltip']; - builder.addBooleanSwitch({ - path: 'tooltip.show', - name: 'Show tooltip', - defaultValue: defaultOptions.tooltip.show, + builder.addRadio({ + path: 'tooltip.mode', + name: 'Tooltip mode', category, + defaultValue: TooltipDisplayMode.Single, + settings: { + options: [ + { value: TooltipDisplayMode.Single, label: 'Single' }, + { value: TooltipDisplayMode.Multi, label: 'All' }, + { value: TooltipDisplayMode.None, label: 'Hidden' }, + ], + }, }); builder.addBooleanSwitch({ @@ -403,7 +411,7 @@ export const plugin = new PanelPlugin(HeatmapPanel) name: 'Show histogram (Y axis)', defaultValue: defaultOptions.tooltip.yHistogram, category, - showIf: (opts) => opts.tooltip.show, + showIf: (opts) => opts.tooltip.mode !== TooltipDisplayMode.None, }); builder.addBooleanSwitch({ @@ -411,7 +419,7 @@ export const plugin = new PanelPlugin(HeatmapPanel) name: 'Show color scale', defaultValue: defaultOptions.tooltip.showColorScale, category, - showIf: (opts) => opts.tooltip.show && config.featureToggles.newVizTooltips, + showIf: (opts) => opts.tooltip.mode !== TooltipDisplayMode.None && config.featureToggles.newVizTooltips, }); category = ['Legend']; diff --git a/public/app/plugins/panel/heatmap/panelcfg.cue b/public/app/plugins/panel/heatmap/panelcfg.cue index 687eefdb41e..13f690f82de 100644 --- a/public/app/plugins/panel/heatmap/panelcfg.cue +++ b/public/app/plugins/panel/heatmap/panelcfg.cue @@ -78,8 +78,8 @@ composableKinds: PanelCfg: lineage: { } @cuetsy(kind="interface") // Controls tooltip options HeatmapTooltip: { - // Controls if the tooltip is shown - show: bool + // Controls how the tooltip is shown + mode: ui.TooltipDisplayMode // Controls if the tooltip shows a histogram of the y-axis values yHistogram?: bool // Controls if the tooltip shows a color scale in header @@ -145,7 +145,7 @@ composableKinds: PanelCfg: lineage: { } // Controls tooltip options tooltip: HeatmapTooltip | *{ - show: true + mode: ui.TooltipDisplayMode & (*"single" | _) yHistogram: false showColorScale: false } diff --git a/public/app/plugins/panel/heatmap/panelcfg.gen.ts b/public/app/plugins/panel/heatmap/panelcfg.gen.ts index 7699ec9e120..b5589b134e3 100644 --- a/public/app/plugins/panel/heatmap/panelcfg.gen.ts +++ b/public/app/plugins/panel/heatmap/panelcfg.gen.ts @@ -127,9 +127,9 @@ export interface FilterValueRange { */ export interface HeatmapTooltip { /** - * Controls if the tooltip is shown + * Controls how the tooltip is shown */ - show: boolean; + mode: ui.TooltipDisplayMode; /** * Controls if the tooltip shows a color scale in header */ @@ -263,7 +263,7 @@ export const defaultOptions: Partial = { }, showValue: ui.VisibilityMode.Auto, tooltip: { - show: true, + mode: ui.TooltipDisplayMode.Single, yHistogram: false, showColorScale: false, },