BarChart: TooltipPlugin2 (#80920)

Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
Leon Sorokin 2024-02-26 19:18:40 -06:00 committed by GitHub
parent 2ed8201f25
commit 2c3596854f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 107 additions and 36 deletions

View File

@ -118,8 +118,12 @@ export const getContentItems = (
const v = fields[i].values[dataIdx]; const v = fields[i].values[dataIdx];
// no value -> zero? if (v == null && field.config.noValue == null) {
continue;
}
const display = field.display!(v); // super expensive :( const display = field.display!(v); // super expensive :(
// sort NaN and non-numeric to bottom (regardless of sort order) // sort NaN and non-numeric to bottom (regardless of sort order)
const numeric = !Number.isNaN(display.numeric) const numeric = !Number.isNaN(display.numeric)
? display.numeric ? display.numeric

View File

@ -12,7 +12,7 @@ import {
TimeRange, TimeRange,
VizOrientation, VizOrientation,
} from '@grafana/data'; } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime'; import { PanelDataErrorView, config } from '@grafana/runtime';
import { SortOrder } from '@grafana/schema'; import { SortOrder } from '@grafana/schema';
import { import {
GraphGradientMode, GraphGradientMode,
@ -28,13 +28,17 @@ import {
VizLayout, VizLayout,
VizLegend, VizLegend,
VizTooltipContainer, VizTooltipContainer,
TooltipPlugin2,
} from '@grafana/ui'; } from '@grafana/ui';
import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; 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 { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { GraphNG, GraphNGProps, PropDiffFn } from 'app/core/components/GraphNG/GraphNG'; import { GraphNG, GraphNGProps, PropDiffFn } from 'app/core/components/GraphNG/GraphNG';
import { getFieldLegendItem } from 'app/core/components/TimelineChart/utils'; import { getFieldLegendItem } from 'app/core/components/TimelineChart/utils';
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip';
import { Options } from './panelcfg.gen'; import { Options } from './panelcfg.gen';
import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils'; import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils';
@ -302,9 +306,12 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ
fillOpacity, fillOpacity,
allFrames: info.viz, allFrames: info.viz,
fullHighlight, fullHighlight,
hoverMulti: tooltip.mode === TooltipDisplayMode.Multi,
}); });
}; };
const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips);
return ( return (
<GraphNG <GraphNG
theme={theme} theme={theme}
@ -321,7 +328,33 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ
height={height} height={height}
> >
{(config) => { {(config) => {
if (oldConfig.current !== config) { if (showNewVizTooltips && options.tooltip.mode !== TooltipDisplayMode.None) {
return (
<TooltipPlugin2
config={config}
hoverMode={
options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll
}
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2) => {
return (
<TimeSeriesTooltip
frames={info.viz}
seriesFrame={info.aligned}
dataIdxs={dataIdxs}
seriesIdx={seriesIdx}
mode={options.tooltip.mode}
sortOrder={options.tooltip.sort}
isPinned={isPinned}
/>
);
}}
maxWidth={options.tooltip.maxWidth}
maxHeight={options.tooltip.maxHeight}
/>
);
}
if (!showNewVizTooltips && oldConfig.current !== config) {
oldConfig.current = addTooltipSupport({ oldConfig.current = addTooltipSupport({
config, config,
onUPlotClick, onUPlotClick,

View File

@ -68,7 +68,8 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 1`] = `
"y": false, "y": false,
}, },
"focus": { "focus": {
"prox": 30, "dist": [Function],
"prox": 1000,
}, },
"points": { "points": {
"bbox": [Function], "bbox": [Function],
@ -223,7 +224,8 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 2`] = `
"y": false, "y": false,
}, },
"focus": { "focus": {
"prox": 30, "dist": [Function],
"prox": 1000,
}, },
"points": { "points": {
"bbox": [Function], "bbox": [Function],
@ -378,7 +380,8 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 3`] = `
"y": false, "y": false,
}, },
"focus": { "focus": {
"prox": 30, "dist": [Function],
"prox": 1000,
}, },
"points": { "points": {
"bbox": [Function], "bbox": [Function],
@ -533,7 +536,8 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 1`] = `
"y": false, "y": false,
}, },
"focus": { "focus": {
"prox": 30, "dist": [Function],
"prox": 1000,
}, },
"points": { "points": {
"bbox": [Function], "bbox": [Function],
@ -688,7 +692,8 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 2`] = `
"y": false, "y": false,
}, },
"focus": { "focus": {
"prox": 30, "dist": [Function],
"prox": 1000,
}, },
"points": { "points": {
"bbox": [Function], "bbox": [Function],
@ -843,7 +848,8 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 3`] = `
"y": false, "y": false,
}, },
"focus": { "focus": {
"prox": 30, "dist": [Function],
"prox": 1000,
}, },
"points": { "points": {
"bbox": [Function], "bbox": [Function],
@ -998,7 +1004,8 @@ exports[`BarChart utils preparePlotConfigBuilder value visibility 1`] = `
"y": false, "y": false,
}, },
"focus": { "focus": {
"prox": 30, "dist": [Function],
"prox": 1000,
}, },
"points": { "points": {
"bbox": [Function], "bbox": [Function],
@ -1153,7 +1160,8 @@ exports[`BarChart utils preparePlotConfigBuilder value visibility 2`] = `
"y": false, "y": false,
}, },
"focus": { "focus": {
"prox": 30, "dist": [Function],
"prox": 1000,
}, },
"points": { "points": {
"bbox": [Function], "bbox": [Function],

View File

@ -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 { StackingGroup, preparePlotData2 } from '@grafana/ui/src/components/uPlot/utils';
import { distribute, SPACE_BETWEEN } from './distribute'; 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 groupDistr = SPACE_BETWEEN;
const barDistr = SPACE_BETWEEN; const barDistr = SPACE_BETWEEN;
@ -56,6 +56,7 @@ export interface BarsOptions {
text?: VizTextDisplayOptions; text?: VizTextDisplayOptions;
onHover?: (seriesIdx: number, valueIdx: number) => void; onHover?: (seriesIdx: number, valueIdx: number) => void;
onLeave?: (seriesIdx: number, valueIdx: number) => void; onLeave?: (seriesIdx: number, valueIdx: number) => void;
hoverMulti?: boolean;
legend?: VizLegendOptions; legend?: VizLegendOptions;
xSpacing?: number; xSpacing?: number;
xTimeAuto?: boolean; xTimeAuto?: boolean;
@ -128,6 +129,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
fillOpacity = 1, fillOpacity = 1,
showValue, showValue,
xSpacing = 0, xSpacing = 0,
hoverMulti = false,
} = opts; } = opts;
const isXHorizontal = xOri === ScaleOrientation.Horizontal; const isXHorizontal = xOri === ScaleOrientation.Horizontal;
const hasAutoValueSize = !Boolean(opts.text?.valueSize); const hasAutoValueSize = !Boolean(opts.text?.valueSize);
@ -141,6 +143,8 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
} }
let qt: Quadtree; let qt: Quadtree;
const numSeries = 30; // !!
const hovered: Array<Rect | null> = Array(numSeries).fill(null);
let hRect: Rect | null; let hRect: Rect | null;
// for distr: 2 scales, the splits array should contain indices into data[0] rather than values // 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 }; 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) { if (opts.xOri === ScaleOrientation.Horizontal) {
barRect.y = 0; barRect.y = 0;
barRect.h = u.bbox.height; barRect.h = u.bbox.height;
@ -443,8 +447,6 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
}); });
const init = (u: uPlot) => { const init = (u: uPlot) => {
let over = u.over;
over.style.overflow = 'hidden';
u.root.querySelectorAll<HTMLDivElement>('.u-cursor-pt').forEach((el) => { u.root.querySelectorAll<HTMLDivElement>('.u-cursor-pt').forEach((el) => {
el.style.borderRadius = '0'; el.style.borderRadius = '0';
@ -462,7 +464,8 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
y: false, y: false,
}, },
dataIdx: (u, seriesIdx) => { dataIdx: (u, seriesIdx) => {
if (seriesIdx === 1) { if (seriesIdx === 0) {
hovered.fill(null);
hRect = null; hRect = null;
let cx = u.cursor.left! * uPlot.pxRatio; 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) => { qt.get(cx, cy, 1, 1, (o) => {
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) { 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: { points: {
fill: 'rgba(255,255,255,0.4)', fill: 'rgba(255,255,255,0.4)',
bbox: (u, seriesIdx) => { bbox: (u, seriesIdx) => {
let isHovered = hRect && seriesIdx === hRect.sidx; let hRect2 = hovered[seriesIdx];
let isHovered = hRect2 != null;
return { return {
left: isHovered ? hRect!.x / uPlot.pxRatio : -10, left: isHovered ? hRect2!.x / uPlot.pxRatio : -10,
top: isHovered ? hRect!.y / uPlot.pxRatio : -10, top: isHovered ? hRect2!.y / uPlot.pxRatio : -10,
width: isHovered ? hRect!.w / uPlot.pxRatio : 0, width: isHovered ? hRect2!.w / uPlot.pxRatio : 0,
height: isHovered ? hRect!.h / uPlot.pxRatio : 0, height: isHovered ? hRect2!.h / uPlot.pxRatio : 0,
}; };
}, },
}, },
focus: {
prox: 1e3,
dist: (u, seriesIdx) => (hRect?.sidx === seriesIdx ? 0 : Infinity),
},
}; };
// Build bars // Build bars

View File

@ -225,6 +225,7 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(BarChartPanel)
path: 'fullHighlight', path: 'fullHighlight',
name: 'Highlight full area on hover', name: 'Highlight full area on hover',
defaultValue: defaultOptions.fullHighlight, defaultValue: defaultOptions.fullHighlight,
showIf: (c) => c.stacking === StackingMode.None,
}); });
builder.addFieldNamePicker({ builder.addFieldNamePicker({

View File

@ -14,24 +14,20 @@ export function pointWithin(px: number, py: number, rlft: number, rtop: number,
/** /**
* @internal * @internal
*/ */
export function findRect(qt: Quadtree, sidx: number, didx: number): Rect | undefined { export function findRects(qt: Quadtree, sidx?: number, didx?: number) {
let out: Rect | undefined; let rects: Rect[] = [];
if (qt.o.length) { 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++) { for (let i = 0; i < qt.q.length; i++) {
out = findRect(qt.q[i], sidx, didx); rects.push(...findRects(qt.q[i], sidx, didx));
if (out) {
break;
}
} }
} }
return out; return rects;
} }
/** /**

View File

@ -17,6 +17,7 @@ import {
getFieldDisplayName, getFieldDisplayName,
} from '@grafana/data'; } from '@grafana/data';
import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames';
import { config as runtimeConfig } from '@grafana/runtime';
import { import {
AxisColorMode, AxisColorMode,
AxisPlacement, 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 { getStackingGroups } from '@grafana/ui/src/components/uPlot/utils';
import { findField } from 'app/features/dimensions'; import { findField } from 'app/features/dimensions';
import { setClassicPaletteIdxs } from '../timeseries/utils';
import { BarsOptions, getConfig } from './bars'; import { BarsOptions, getConfig } from './bars';
import { FieldConfig, Options, defaultFieldConfig } from './panelcfg.gen'; import { FieldConfig, Options, defaultFieldConfig } from './panelcfg.gen';
import { BarChartDisplayValues, BarChartDisplayWarning } from './types'; import { BarChartDisplayValues, BarChartDisplayWarning } from './types';
@ -60,6 +63,7 @@ export interface BarChartOptionsEX extends Options {
getColor?: (seriesIdx: number, valueIdx: number, value: unknown) => string | null; getColor?: (seriesIdx: number, valueIdx: number, value: unknown) => string | null;
timeZone?: TimeZone; timeZone?: TimeZone;
fillOpacity?: number; fillOpacity?: number;
hoverMulti?: boolean;
} }
export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
@ -82,6 +86,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
legend, legend,
timeZone, timeZone,
fullHighlight, fullHighlight,
hoverMulti,
}) => { }) => {
const builder = new UPlotConfigBuilder(); const builder = new UPlotConfigBuilder();
@ -122,6 +127,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
xTimeAuto: frame.fields[0]?.type === FieldType.time && !frame.fields[0].config.unit?.startsWith('time:'), 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), negY: frame.fields.map((f) => f.config.custom?.transform === GraphTransform.NegativeY),
fullHighlight, fullHighlight,
hoverMulti,
}; };
const config = getConfig(opts, theme); const config = getConfig(opts, theme);
@ -132,7 +138,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
builder.addHook('drawClear', config.drawClear); builder.addHook('drawClear', config.drawClear);
builder.addHook('draw', config.draw); builder.addHook('draw', config.draw);
builder.setTooltipInterpolator(config.interpolateTooltip); const showNewVizTooltips = Boolean(runtimeConfig.featureToggles.newVizTooltips);
!showNewVizTooltips && builder.setTooltipInterpolator(config.interpolateTooltip);
if (xTickLabelRotation !== 0) { if (xTickLabelRotation !== 0) {
// these are the amount of space we already have available between plot edge and first label // 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],
series[0].fields.findIndex((f) => f.type === FieldType.time) series[0].fields.findIndex((f) => f.type === FieldType.time)
) )
: outerJoinDataFrames({ frames: series }); : outerJoinDataFrames({ frames: series, keepDisplayNames: true });
if (!frame) { if (!frame) {
return { warn: 'Unable to join data' }; 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) { if (!fields.length) {
return { return {
warn: 'No numeric fields found', warn: 'No numeric fields found',

View File

@ -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; let seriesIndex = 0;
frames.forEach((frame) => { frames.forEach((frame) => {
frame.fields.forEach((field, fieldIdx) => { frame.fields.forEach((field, fieldIdx) => {