mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
BarChart: TooltipPlugin2 (#80920)
Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
parent
2ed8201f25
commit
2c3596854f
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
|
@ -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
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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',
|
||||
|
@ -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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user