From 2c3596854f21e20d5cc12eea4f2c78462c6221fb Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 26 Feb 2024 19:18:40 -0600 Subject: [PATCH] BarChart: TooltipPlugin2 (#80920) Co-authored-by: Adela Almasan --- .../src/components/VizTooltip/utils.ts | 6 ++- .../plugins/panel/barchart/BarChartPanel.tsx | 37 +++++++++++++++++- .../barchart/__snapshots__/utils.test.ts.snap | 24 ++++++++---- public/app/plugins/panel/barchart/bars.ts | 38 +++++++++++++------ public/app/plugins/panel/barchart/module.tsx | 1 + public/app/plugins/panel/barchart/quadtree.ts | 16 +++----- public/app/plugins/panel/barchart/utils.ts | 19 +++++++++- public/app/plugins/panel/timeseries/utils.ts | 2 +- 8 files changed, 107 insertions(+), 36 deletions(-) diff --git a/packages/grafana-ui/src/components/VizTooltip/utils.ts b/packages/grafana-ui/src/components/VizTooltip/utils.ts index 24d81a57c40..7bbc0795b69 100644 --- a/packages/grafana-ui/src/components/VizTooltip/utils.ts +++ b/packages/grafana-ui/src/components/VizTooltip/utils.ts @@ -118,8 +118,12 @@ export const getContentItems = ( const v = fields[i].values[dataIdx]; - // no value -> zero? + if (v == null && field.config.noValue == null) { + continue; + } + const display = field.display!(v); // super expensive :( + // sort NaN and non-numeric to bottom (regardless of sort order) const numeric = !Number.isNaN(display.numeric) ? display.numeric diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index 27d7a366e17..b5d2b1ad4b7 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -12,7 +12,7 @@ import { TimeRange, VizOrientation, } from '@grafana/data'; -import { PanelDataErrorView } from '@grafana/runtime'; +import { PanelDataErrorView, config } from '@grafana/runtime'; import { SortOrder } from '@grafana/schema'; import { GraphGradientMode, @@ -28,13 +28,17 @@ import { VizLayout, VizLegend, VizTooltipContainer, + TooltipPlugin2, } from '@grafana/ui'; import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; +import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { GraphNG, GraphNGProps, PropDiffFn } from 'app/core/components/GraphNG/GraphNG'; import { getFieldLegendItem } from 'app/core/components/TimelineChart/utils'; import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; +import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; + import { Options } from './panelcfg.gen'; import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils'; @@ -302,9 +306,12 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ fillOpacity, allFrames: info.viz, fullHighlight, + hoverMulti: tooltip.mode === TooltipDisplayMode.Multi, }); }; + const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); + return ( {(config) => { - if (oldConfig.current !== config) { + if (showNewVizTooltips && options.tooltip.mode !== TooltipDisplayMode.None) { + return ( + { + return ( + + ); + }} + maxWidth={options.tooltip.maxWidth} + maxHeight={options.tooltip.maxHeight} + /> + ); + } + + if (!showNewVizTooltips && oldConfig.current !== config) { oldConfig.current = addTooltipSupport({ config, onUPlotClick, diff --git a/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap b/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap index e8513d91a0d..460a05d35ca 100644 --- a/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap +++ b/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap @@ -68,7 +68,8 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 1`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -223,7 +224,8 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 2`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -378,7 +380,8 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 3`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -533,7 +536,8 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 1`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -688,7 +692,8 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 2`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -843,7 +848,8 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 3`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -998,7 +1004,8 @@ exports[`BarChart utils preparePlotConfigBuilder value visibility 1`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -1153,7 +1160,8 @@ exports[`BarChart utils preparePlotConfigBuilder value visibility 2`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index 9cc562781f0..541d5c3f307 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -15,7 +15,7 @@ import { formatTime } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBui import { StackingGroup, preparePlotData2 } from '@grafana/ui/src/components/uPlot/utils'; import { distribute, SPACE_BETWEEN } from './distribute'; -import { intersects, pointWithin, Quadtree, Rect } from './quadtree'; +import { findRects, intersects, pointWithin, Quadtree, Rect } from './quadtree'; const groupDistr = SPACE_BETWEEN; const barDistr = SPACE_BETWEEN; @@ -56,6 +56,7 @@ export interface BarsOptions { text?: VizTextDisplayOptions; onHover?: (seriesIdx: number, valueIdx: number) => void; onLeave?: (seriesIdx: number, valueIdx: number) => void; + hoverMulti?: boolean; legend?: VizLegendOptions; xSpacing?: number; xTimeAuto?: boolean; @@ -128,6 +129,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { fillOpacity = 1, showValue, xSpacing = 0, + hoverMulti = false, } = opts; const isXHorizontal = xOri === ScaleOrientation.Horizontal; const hasAutoValueSize = !Boolean(opts.text?.valueSize); @@ -141,6 +143,8 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { } let qt: Quadtree; + const numSeries = 30; // !! + const hovered: Array = Array(numSeries).fill(null); let hRect: Rect | null; // for distr: 2 scales, the splits array should contain indices into data[0] rather than values @@ -324,7 +328,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { let barRect = { x: lft, y: top, w: wid, h: hgt, sidx: seriesIdx, didx: dataIdx }; - if (opts.fullHighlight) { + if (!isStacked && opts.fullHighlight) { if (opts.xOri === ScaleOrientation.Horizontal) { barRect.y = 0; barRect.h = u.bbox.height; @@ -443,8 +447,6 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { }); const init = (u: uPlot) => { - let over = u.over; - over.style.overflow = 'hidden'; u.root.querySelectorAll('.u-cursor-pt').forEach((el) => { el.style.borderRadius = '0'; @@ -462,7 +464,8 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { y: false, }, dataIdx: (u, seriesIdx) => { - if (seriesIdx === 1) { + if (seriesIdx === 0) { + hovered.fill(null); hRect = null; let cx = u.cursor.left! * uPlot.pxRatio; @@ -470,26 +473,37 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { qt.get(cx, cy, 1, 1, (o) => { if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) { - hRect = o; + hRect = hovered[0] = o; + hovered[hRect.sidx] = hRect; + + hoverMulti && + findRects(qt, undefined, hRect.didx).forEach((r) => { + hovered[r.sidx] = r; + }); } }); } - return hRect && seriesIdx === hRect.sidx ? hRect.didx : null; + return hovered[seriesIdx]?.didx; }, points: { fill: 'rgba(255,255,255,0.4)', bbox: (u, seriesIdx) => { - let isHovered = hRect && seriesIdx === hRect.sidx; + let hRect2 = hovered[seriesIdx]; + let isHovered = hRect2 != null; return { - left: isHovered ? hRect!.x / uPlot.pxRatio : -10, - top: isHovered ? hRect!.y / uPlot.pxRatio : -10, - width: isHovered ? hRect!.w / uPlot.pxRatio : 0, - height: isHovered ? hRect!.h / uPlot.pxRatio : 0, + left: isHovered ? hRect2!.x / uPlot.pxRatio : -10, + top: isHovered ? hRect2!.y / uPlot.pxRatio : -10, + width: isHovered ? hRect2!.w / uPlot.pxRatio : 0, + height: isHovered ? hRect2!.h / uPlot.pxRatio : 0, }; }, }, + focus: { + prox: 1e3, + dist: (u, seriesIdx) => (hRect?.sidx === seriesIdx ? 0 : Infinity), + }, }; // Build bars diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index eb61ddc0c5d..21e23f6f60d 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -225,6 +225,7 @@ export const plugin = new PanelPlugin(BarChartPanel) path: 'fullHighlight', name: 'Highlight full area on hover', defaultValue: defaultOptions.fullHighlight, + showIf: (c) => c.stacking === StackingMode.None, }); builder.addFieldNamePicker({ diff --git a/public/app/plugins/panel/barchart/quadtree.ts b/public/app/plugins/panel/barchart/quadtree.ts index be6be6b4d74..26850a9a8e8 100644 --- a/public/app/plugins/panel/barchart/quadtree.ts +++ b/public/app/plugins/panel/barchart/quadtree.ts @@ -14,24 +14,20 @@ export function pointWithin(px: number, py: number, rlft: number, rtop: number, /** * @internal */ -export function findRect(qt: Quadtree, sidx: number, didx: number): Rect | undefined { - let out: Rect | undefined; +export function findRects(qt: Quadtree, sidx?: number, didx?: number) { + let rects: Rect[] = []; if (qt.o.length) { - out = qt.o.find((rect) => rect.sidx === sidx && rect.didx === didx); + rects.push(...qt.o.filter((rect) => (sidx == null || rect.sidx === sidx) && (didx == null || rect.didx === didx))); } - if (out == null && qt.q) { + if (qt.q) { for (let i = 0; i < qt.q.length; i++) { - out = findRect(qt.q[i], sidx, didx); - - if (out) { - break; - } + rects.push(...findRects(qt.q[i], sidx, didx)); } } - return out; + return rects; } /** diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index 62f342ce087..e170ead77bd 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -17,6 +17,7 @@ import { getFieldDisplayName, } from '@grafana/data'; import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames'; +import { config as runtimeConfig } from '@grafana/runtime'; import { AxisColorMode, AxisPlacement, @@ -33,6 +34,8 @@ import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuil import { getStackingGroups } from '@grafana/ui/src/components/uPlot/utils'; import { findField } from 'app/features/dimensions'; +import { setClassicPaletteIdxs } from '../timeseries/utils'; + import { BarsOptions, getConfig } from './bars'; import { FieldConfig, Options, defaultFieldConfig } from './panelcfg.gen'; import { BarChartDisplayValues, BarChartDisplayWarning } from './types'; @@ -60,6 +63,7 @@ export interface BarChartOptionsEX extends Options { getColor?: (seriesIdx: number, valueIdx: number, value: unknown) => string | null; timeZone?: TimeZone; fillOpacity?: number; + hoverMulti?: boolean; } export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ @@ -82,6 +86,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ legend, timeZone, fullHighlight, + hoverMulti, }) => { const builder = new UPlotConfigBuilder(); @@ -122,6 +127,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ xTimeAuto: frame.fields[0]?.type === FieldType.time && !frame.fields[0].config.unit?.startsWith('time:'), negY: frame.fields.map((f) => f.config.custom?.transform === GraphTransform.NegativeY), fullHighlight, + hoverMulti, }; const config = getConfig(opts, theme); @@ -132,7 +138,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ builder.addHook('drawClear', config.drawClear); builder.addHook('draw', config.draw); - builder.setTooltipInterpolator(config.interpolateTooltip); + const showNewVizTooltips = Boolean(runtimeConfig.featureToggles.newVizTooltips); + !showNewVizTooltips && builder.setTooltipInterpolator(config.interpolateTooltip); if (xTickLabelRotation !== 0) { // these are the amount of space we already have available between plot edge and first label @@ -389,7 +396,8 @@ export function prepareBarChartDisplayValues( series[0], series[0].fields.findIndex((f) => f.type === FieldType.time) ) - : outerJoinDataFrames({ frames: series }); + : outerJoinDataFrames({ frames: series, keepDisplayNames: true }); + if (!frame) { return { warn: 'Unable to join data' }; } @@ -478,6 +486,13 @@ export function prepareBarChartDisplayValues( }; } + // if both string and time fields exist, remove unused leftover time field + if (frame.fields[0].type === FieldType.time && frame.fields[0] !== firstField) { + frame.fields.shift(); + } + + setClassicPaletteIdxs([frame], theme, 0); + if (!fields.length) { return { warn: 'No numeric fields found', diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index 73d74ec00c9..a7d24f820e5 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -242,7 +242,7 @@ const matchEnumColorToSeriesColor = (frames: DataFrame[], theme: GrafanaTheme2) } }; -const setClassicPaletteIdxs = (frames: DataFrame[], theme: GrafanaTheme2, skipFieldIdx?: number) => { +export const setClassicPaletteIdxs = (frames: DataFrame[], theme: GrafanaTheme2, skipFieldIdx?: number) => { let seriesIndex = 0; frames.forEach((frame) => { frame.fields.forEach((field, fieldIdx) => {