From bf2c45db01f6329f99f0a0632d54224d526d27ba Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 11 May 2021 19:24:23 +0200 Subject: [PATCH] BarChartPanel: Adds support for Tooltip in BarChartPanel (#33938) * Adds support for Tooltip in BarChartPanel * Revert some formatting * Remove BarChart story --- .../components/BarChart/BarChart.story.tsx | 66 -------- .../src/components/BarChart/BarChart.tsx | 136 +++++------------ .../BarChart/__snapshots__/utils.test.ts.snap | 96 ++---------- .../src/components/BarChart/bars.ts | 143 ++++++------------ .../src/components/BarChart/types.ts | 5 +- .../src/components/BarChart/utils.test.ts | 38 ++++- .../src/components/BarChart/utils.ts | 94 ++++++------ .../src/components/GraphNG/GraphNG.tsx | 10 +- .../uPlot/config/UPlotConfigBuilder.ts | 11 +- .../uPlot/plugins/TooltipPlugin.tsx | 58 ++++--- .../grafana-ui/src/components/uPlot/types.ts | 6 + .../plugins/panel/barchart/BarChartPanel.tsx | 15 +- public/app/plugins/panel/barchart/module.tsx | 2 + 13 files changed, 263 insertions(+), 417 deletions(-) delete mode 100644 packages/grafana-ui/src/components/BarChart/BarChart.story.tsx diff --git a/packages/grafana-ui/src/components/BarChart/BarChart.story.tsx b/packages/grafana-ui/src/components/BarChart/BarChart.story.tsx deleted file mode 100644 index 4deded449cb..00000000000 --- a/packages/grafana-ui/src/components/BarChart/BarChart.story.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { toDataFrame, FieldType, VizOrientation } from '@grafana/data'; -import React from 'react'; -import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; -import { BarChart } from './BarChart'; -import { LegendDisplayMode } from '../VizLegend/models.gen'; -import { prepDataForStorybook } from '../../utils/storybook/data'; -import { useTheme2 } from '../../themes'; -import { select } from '@storybook/addon-knobs'; -import { BarChartOptions, BarValueVisibility } from './types'; -import { StackingMode } from '../uPlot/config'; - -export default { - title: 'Visualizations/BarChart', - component: BarChart, - decorators: [withCenteredStory], - parameters: { - docs: {}, - }, -}; - -const getKnobs = () => { - return { - legendPlacement: select( - 'Legend placement', - { - bottom: 'bottom', - right: 'right', - }, - 'bottom' - ), - orientation: select( - 'Bar orientation', - { - vertical: VizOrientation.Vertical, - horizontal: VizOrientation.Horizontal, - }, - VizOrientation.Vertical - ), - }; -}; - -export const Basic: React.FC = () => { - const { legendPlacement, orientation } = getKnobs(); - - const theme = useTheme2(); - const frame = toDataFrame({ - fields: [ - { name: 'x', type: FieldType.string, values: ['group 1', 'group 2'] }, - { name: 'a', type: FieldType.number, values: [10, 20] }, - { name: 'b', type: FieldType.number, values: [30, 10] }, - ], - }); - - const data = prepDataForStorybook([frame], theme); - - const options: BarChartOptions = { - orientation: orientation, - legend: { displayMode: LegendDisplayMode.List, placement: legendPlacement, calcs: [] }, - stacking: StackingMode.None, - showValue: BarValueVisibility.Always, - barWidth: 0.97, - groupWidth: 0.7, - }; - - return ; -}; diff --git a/packages/grafana-ui/src/components/BarChart/BarChart.tsx b/packages/grafana-ui/src/components/BarChart/BarChart.tsx index 15ab4a7c1ba..ac216ef273b 100644 --- a/packages/grafana-ui/src/components/BarChart/BarChart.tsx +++ b/packages/grafana-ui/src/components/BarChart/BarChart.tsx @@ -1,100 +1,52 @@ import React from 'react'; -import { AlignedData } from 'uplot'; import { DataFrame, TimeRange } from '@grafana/data'; -import { VizLayout } from '../VizLayout/VizLayout'; -import { Themeable2 } from '../../types'; -import { UPlotChart } from '../uPlot/Plot'; -import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; -import { GraphNGLegendEvent } from '../GraphNG/types'; import { BarChartOptions } from './types'; import { withTheme2 } from '../../themes/ThemeContext'; import { preparePlotConfigBuilder, preparePlotFrame } from './utils'; -import { pluginLog, preparePlotData } from '../uPlot/utils'; import { LegendDisplayMode } from '../VizLegend/models.gen'; import { PlotLegend } from '../uPlot/PlotLegend'; +import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG'; +import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; /** * @alpha */ -export interface BarChartProps extends Themeable2, BarChartOptions { - height: number; - width: number; - data: DataFrame[]; - structureRev?: number; // a number that will change when the data[] structure changes - onLegendClick?: (event: GraphNGLegendEvent) => void; -} +export interface BarChartProps + extends BarChartOptions, + Omit {} -interface BarChartState { - data: AlignedData; - alignedDataFrame: DataFrame; - config?: UPlotConfigBuilder; -} +const propsToDiff: string[] = ['orientation', 'barWidth', 'groupWidth', 'showValue']; -class UnthemedBarChart extends React.Component { - constructor(props: BarChartProps) { - super(props); - const alignedDataFrame = preparePlotFrame(props.data); - if (!alignedDataFrame) { - return; - } - const data = preparePlotData(alignedDataFrame); - const config = preparePlotConfigBuilder(alignedDataFrame, this.props.theme, this.props); - this.state = { - alignedDataFrame, - data, - config, - }; - } +class UnthemedBarChart extends React.Component { + prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => { + const { eventBus } = this.context; + const { theme, timeZone, orientation, barWidth, showValue, groupWidth, stacking, legend, tooltip } = this.props; + return preparePlotConfigBuilder({ + frame: alignedFrame, + getTimeRange, + theme, + timeZone, + eventBus, + orientation, + barWidth, + showValue, + groupWidth, + stacking, + legend, + tooltip, + }); + }; - componentDidUpdate(prevProps: BarChartProps) { - const { data, orientation, groupWidth, barWidth, showValue, structureRev } = this.props; - const { alignedDataFrame } = this.state; - let shouldConfigUpdate = false; - let stateUpdate = {} as BarChartState; - - if ( - this.state.config === undefined || - orientation !== prevProps.orientation || - groupWidth !== prevProps.groupWidth || - barWidth !== prevProps.barWidth || - showValue !== prevProps.showValue - ) { - shouldConfigUpdate = true; - } - - if (data !== prevProps.data || shouldConfigUpdate) { - const hasStructureChanged = structureRev !== prevProps.structureRev || !structureRev; - const alignedData = preparePlotFrame(data); - - if (!alignedData) { - return; - } - stateUpdate = { - alignedDataFrame: alignedData, - data: preparePlotData(alignedData), - }; - if (shouldConfigUpdate || hasStructureChanged) { - pluginLog('BarChart', false, 'updating config'); - const builder = preparePlotConfigBuilder(alignedDataFrame, this.props.theme, this.props); - stateUpdate = { ...stateUpdate, config: builder }; - } - } - - if (Object.keys(stateUpdate).length > 0) { - this.setState(stateUpdate); - } - } - - renderLegend() { - const { legend, onLegendClick, data } = this.props; - const { config } = this.state; + renderLegend = (config: UPlotConfigBuilder) => { + const { legend, onLegendClick, frames } = this.props; if (!config || legend.displayMode === LegendDisplayMode.Hidden) { return; } + return ( { {...legend} /> ); - } + }; render() { - const { width, height } = this.props; - const { config, data } = this.state; - - if (!config) { - return null; - } - return ( - - {(vizWidth: number, vizHeight: number) => ( - - )} - + ); } } export const BarChart = withTheme2(UnthemedBarChart); -BarChart.displayName = 'GraphNG'; +BarChart.displayName = 'BarChart'; diff --git a/packages/grafana-ui/src/components/BarChart/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/BarChart/__snapshots__/utils.test.ts.snap index c8bc16d13e0..92fc1b0ac35 100644 --- a/packages/grafana-ui/src/components/BarChart/__snapshots__/utils.test.ts.snap +++ b/packages/grafana-ui/src/components/BarChart/__snapshots__/utils.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`GraphNG utils preparePlotConfigBuilder orientation 1`] = ` +exports[`BarChart utils preparePlotConfigBuilder orientation 1`] = ` Object { "axes": Array [ Object { @@ -61,13 +61,10 @@ Object { }, "points": Object { "fill": [Function], - "show": false, "size": [Function], "stroke": [Function], "width": [Function], }, - "x": false, - "y": false, }, "hooks": Object { "drawClear": Array [ @@ -76,9 +73,6 @@ Object { "init": Array [ [Function], ], - "setCursor": Array [ - [Function], - ], }, "scales": Object { "m/s": Object { @@ -100,9 +94,7 @@ Object { "time": false, }, }, - "select": Object { - "show": false, - }, + "select": undefined, "series": Array [ Object {}, Object { @@ -126,7 +118,7 @@ Object { } `; -exports[`GraphNG utils preparePlotConfigBuilder orientation 2`] = ` +exports[`BarChart utils preparePlotConfigBuilder orientation 2`] = ` Object { "axes": Array [ Object { @@ -187,13 +179,10 @@ Object { }, "points": Object { "fill": [Function], - "show": false, "size": [Function], "stroke": [Function], "width": [Function], }, - "x": false, - "y": false, }, "hooks": Object { "drawClear": Array [ @@ -202,9 +191,6 @@ Object { "init": Array [ [Function], ], - "setCursor": Array [ - [Function], - ], }, "scales": Object { "m/s": Object { @@ -226,9 +212,7 @@ Object { "time": false, }, }, - "select": Object { - "show": false, - }, + "select": undefined, "series": Array [ Object {}, Object { @@ -252,7 +236,7 @@ Object { } `; -exports[`GraphNG utils preparePlotConfigBuilder orientation 3`] = ` +exports[`BarChart utils preparePlotConfigBuilder orientation 3`] = ` Object { "axes": Array [ Object { @@ -313,13 +297,10 @@ Object { }, "points": Object { "fill": [Function], - "show": false, "size": [Function], "stroke": [Function], "width": [Function], }, - "x": false, - "y": false, }, "hooks": Object { "drawClear": Array [ @@ -328,9 +309,6 @@ Object { "init": Array [ [Function], ], - "setCursor": Array [ - [Function], - ], }, "scales": Object { "m/s": Object { @@ -352,9 +330,7 @@ Object { "time": false, }, }, - "select": Object { - "show": false, - }, + "select": undefined, "series": Array [ Object {}, Object { @@ -378,7 +354,7 @@ Object { } `; -exports[`GraphNG utils preparePlotConfigBuilder stacking 1`] = ` +exports[`BarChart utils preparePlotConfigBuilder stacking 1`] = ` Object { "axes": Array [ Object { @@ -439,13 +415,10 @@ Object { }, "points": Object { "fill": [Function], - "show": false, "size": [Function], "stroke": [Function], "width": [Function], }, - "x": false, - "y": false, }, "hooks": Object { "drawClear": Array [ @@ -454,9 +427,6 @@ Object { "init": Array [ [Function], ], - "setCursor": Array [ - [Function], - ], }, "scales": Object { "m/s": Object { @@ -478,9 +448,7 @@ Object { "time": false, }, }, - "select": Object { - "show": false, - }, + "select": undefined, "series": Array [ Object {}, Object { @@ -504,7 +472,7 @@ Object { } `; -exports[`GraphNG utils preparePlotConfigBuilder stacking 2`] = ` +exports[`BarChart utils preparePlotConfigBuilder stacking 2`] = ` Object { "axes": Array [ Object { @@ -565,13 +533,10 @@ Object { }, "points": Object { "fill": [Function], - "show": false, "size": [Function], "stroke": [Function], "width": [Function], }, - "x": false, - "y": false, }, "hooks": Object { "drawClear": Array [ @@ -580,9 +545,6 @@ Object { "init": Array [ [Function], ], - "setCursor": Array [ - [Function], - ], }, "scales": Object { "m/s": Object { @@ -604,9 +566,7 @@ Object { "time": false, }, }, - "select": Object { - "show": false, - }, + "select": undefined, "series": Array [ Object {}, Object { @@ -630,7 +590,7 @@ Object { } `; -exports[`GraphNG utils preparePlotConfigBuilder stacking 3`] = ` +exports[`BarChart utils preparePlotConfigBuilder stacking 3`] = ` Object { "axes": Array [ Object { @@ -691,13 +651,10 @@ Object { }, "points": Object { "fill": [Function], - "show": false, "size": [Function], "stroke": [Function], "width": [Function], }, - "x": false, - "y": false, }, "hooks": Object { "drawClear": Array [ @@ -706,9 +663,6 @@ Object { "init": Array [ [Function], ], - "setCursor": Array [ - [Function], - ], }, "scales": Object { "m/s": Object { @@ -730,9 +684,7 @@ Object { "time": false, }, }, - "select": Object { - "show": false, - }, + "select": undefined, "series": Array [ Object {}, Object { @@ -756,7 +708,7 @@ Object { } `; -exports[`GraphNG utils preparePlotConfigBuilder value visibility 1`] = ` +exports[`BarChart utils preparePlotConfigBuilder value visibility 1`] = ` Object { "axes": Array [ Object { @@ -817,13 +769,10 @@ Object { }, "points": Object { "fill": [Function], - "show": false, "size": [Function], "stroke": [Function], "width": [Function], }, - "x": false, - "y": false, }, "hooks": Object { "drawClear": Array [ @@ -832,9 +781,6 @@ Object { "init": Array [ [Function], ], - "setCursor": Array [ - [Function], - ], }, "scales": Object { "m/s": Object { @@ -856,9 +802,7 @@ Object { "time": false, }, }, - "select": Object { - "show": false, - }, + "select": undefined, "series": Array [ Object {}, Object { @@ -882,7 +826,7 @@ Object { } `; -exports[`GraphNG utils preparePlotConfigBuilder value visibility 2`] = ` +exports[`BarChart utils preparePlotConfigBuilder value visibility 2`] = ` Object { "axes": Array [ Object { @@ -943,13 +887,10 @@ Object { }, "points": Object { "fill": [Function], - "show": false, "size": [Function], "stroke": [Function], "width": [Function], }, - "x": false, - "y": false, }, "hooks": Object { "drawClear": Array [ @@ -958,9 +899,6 @@ Object { "init": Array [ [Function], ], - "setCursor": Array [ - [Function], - ], }, "scales": Object { "m/s": Object { @@ -982,9 +920,7 @@ Object { "time": false, }, }, - "select": Object { - "show": false, - }, + "select": undefined, "series": Array [ Object {}, Object { diff --git a/packages/grafana-ui/src/components/BarChart/bars.ts b/packages/grafana-ui/src/components/BarChart/bars.ts index 8babb0a1be8..cbb790ece82 100644 --- a/packages/grafana-ui/src/components/BarChart/bars.ts +++ b/packages/grafana-ui/src/components/BarChart/bars.ts @@ -1,13 +1,12 @@ -import uPlot, { Axis, Series, Cursor, Select } from 'uplot'; +import uPlot, { Axis, Series } from 'uplot'; import { Quadtree, Rect, pointWithin } from './quadtree'; import { distribute, SPACE_BETWEEN } from './distribute'; - -const pxRatio = devicePixelRatio; +import { TooltipInterpolator } from '../uPlot/types'; +import { ScaleDirection, ScaleOrientation } from '../uPlot/config'; const groupDistr = SPACE_BETWEEN; const barDistr = SPACE_BETWEEN; - -const font = Math.round(10 * pxRatio) + 'px Arial'; +const font = Math.round(10 * devicePixelRatio) + 'px Arial'; type WalkTwoCb = null | ((idx: number, offPx: number, dimPx: number) => void); @@ -41,8 +40,8 @@ function walkTwo( * @internal */ export interface BarsOptions { - xOri: 1 | 0; - xDir: 1 | -1; + xOri: ScaleOrientation; + xDir: ScaleDirection; groupWidth: number; barWidth: number; formatValue?: (seriesIdx: number, value: any) => string; @@ -54,11 +53,11 @@ export interface BarsOptions { * @internal */ export function getConfig(opts: BarsOptions) { - const { xOri: ori, xDir: dir, groupWidth, barWidth, formatValue, onHover, onLeave } = opts; + const { xOri: ori, xDir: dir, groupWidth, barWidth, formatValue } = opts; let qt: Quadtree; - const drawBars: Series.PathBuilder = (u, sidx, i0, i1) => { + const drawBars: Series.PathBuilder = (u, sidx) => { return uPlot.orient( u, sidx, @@ -112,29 +111,14 @@ export function getConfig(opts: BarsOptions) { const drawPoints: Series.Points.Show = formatValue == null ? false - : (u, sidx, i0, i1) => { + : (u, sidx) => { u.ctx.font = font; u.ctx.fillStyle = 'white'; uPlot.orient( u, sidx, - ( - series, - dataX, - dataY, - scaleX, - scaleY, - valToPosX, - valToPosY, - xOff, - yOff, - xDim, - yDim, - moveTo, - lineTo, - rect - ) => { + (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => { let numGroups = dataX.length; let barsPerGroup = u.series.length - 1; @@ -148,13 +132,11 @@ export function getConfig(opts: BarsOptions) { if (dataY[ix] != null) { let yPos = valToPosY(dataY[ix]!, scaleY, yDim, yOff); - /* eslint-disable no-multi-spaces */ let x = ori === 0 ? Math.round(lft + barWid / 2) : Math.round(yPos); let y = ori === 0 ? Math.round(yPos) : Math.round(lft + barWid / 2); u.ctx.textAlign = ori === 0 ? 'center' : dataY[ix]! >= 0 ? 'left' : 'right'; u.ctx.textBaseline = ori === 1 ? 'middle' : dataY[ix]! >= 0 ? 'bottom' : 'top'; - /* eslint-enable */ u.ctx.fillText(formatValue(sidx, dataY[ix]), x, y); } @@ -165,23 +147,15 @@ export function getConfig(opts: BarsOptions) { return false; }; - /* - const yRange: Scale.Range = (u, dataMin, dataMax) => { - // @ts-ignore - let [min, max] = uPlot.rangeNum(0, dataMax, 0.05, true); - return [0, max]; - }; - */ - - const xSplits: Axis.Splits = (u: uPlot, axisIdx: number) => { + const xSplits: Axis.Splits = (u: uPlot) => { const dim = ori === 0 ? u.bbox.width : u.bbox.height; const _dir = dir * (ori === 0 ? 1 : -1); let splits: number[] = []; distribute(u.data[0].length, groupWidth, groupDistr, null, (di, lftPct, widPct) => { - let groupLftPx = (dim * lftPct) / pxRatio; - let groupWidPx = (dim * widPct) / pxRatio; + let groupLftPx = (dim * lftPct) / devicePixelRatio; + let groupWidPx = (dim * widPct) / devicePixelRatio; let groupCenterPx = groupLftPx + groupWidPx / 2; @@ -191,7 +165,6 @@ export function getConfig(opts: BarsOptions) { return _dir === 1 ? splits : splits.reverse(); }; - // @ts-ignore const xValues: Axis.Values = (u) => u.data[0]; let hovered: Rect | null = null; @@ -201,21 +174,6 @@ export function getConfig(opts: BarsOptions) { barMark.style.position = 'absolute'; barMark.style.background = 'rgba(255,255,255,0.4)'; - // hide crosshair cursor & hover points - const cursor: Cursor = { - x: false, - y: false, - points: { - show: false, - }, - }; - - // disable selection - // uPlot types do not export the Select interface prior to 1.6.4 - const select: Partial