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];
// 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

View File

@ -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 (
<GraphNG
theme={theme}
@ -321,7 +328,33 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ
height={height}
>
{(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({
config,
onUPlotClick,

View File

@ -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],

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 { 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<Rect | null> = 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<HTMLDivElement>('.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

View File

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

View File

@ -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;
}
/**

View File

@ -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<BarChartOptionsEX> = ({
@ -82,6 +86,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
legend,
timeZone,
fullHighlight,
hoverMulti,
}) => {
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:'),
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<BarChartOptionsEX> = ({
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',

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