diff --git a/public/app/plugins/panel/barchart/BarChart.tsx b/public/app/plugins/panel/barchart/BarChart.tsx index 737a60881f8..2a862b763d3 100644 --- a/public/app/plugins/panel/barchart/BarChart.tsx +++ b/public/app/plugins/panel/barchart/BarChart.tsx @@ -20,7 +20,7 @@ export interface BarChartProps extends BarChartOptions, Omit {} -const propsToDiff: string[] = ['orientation', 'barWidth', 'groupWidth', 'showValue', 'text']; +const propsToDiff: string[] = ['orientation', 'barWidth', 'groupWidth', 'stacking', 'showValue', 'text']; export const BarChart: React.FC = (props) => { const theme = useTheme2(); diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index 0a4f9ef9869..5e489cec2a0 100755 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { PanelProps, TimeRange, VizOrientation } from '@grafana/data'; -import { TooltipPlugin } from '@grafana/ui'; +import { StackingMode, TooltipDisplayMode, TooltipPlugin } from '@grafana/ui'; import { BarChartOptions } from './types'; import { BarChart } from './BarChart'; import { prepareGraphableFrames } from './utils'; @@ -11,7 +11,10 @@ interface Props extends PanelProps {} * @alpha */ export const BarChartPanel: React.FunctionComponent = ({ data, options, width, height, timeZone }) => { - const { frames, warn } = useMemo(() => prepareGraphableFrames(data?.series), [data]); + const { frames, warn } = useMemo(() => prepareGraphableFrames(data?.series, options.stacking), [ + data, + options.stacking, + ]); const orientation = useMemo(() => { if (!options.orientation || options.orientation === VizOrientation.Auto) { return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical; @@ -20,6 +23,14 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w return options.orientation; }, [width, height, options.orientation]); + // Force 'multi' tooltip setting or stacking mode + const tooltip = useMemo(() => { + if (options.stacking === StackingMode.Normal || options.stacking === StackingMode.Percent) { + return { ...options.tooltip, mode: TooltipDisplayMode.Multi }; + } + return options.tooltip; + }, [options.tooltip, options.stacking]); + if (!frames || warn) { return (
@@ -40,7 +51,7 @@ export const BarChartPanel: React.FunctionComponent = ({ data, options, w orientation={orientation} > {(config, alignedFrame) => { - return ; + return ; }} ); diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index b272aad10b4..70db4071150 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -1,9 +1,16 @@ import uPlot, { Axis } from 'uplot'; import { pointWithin, Quadtree, Rect } from './quadtree'; import { distribute, SPACE_BETWEEN } from './distribute'; -import { BarValueVisibility, ScaleDirection, ScaleOrientation } from '@grafana/ui/src/components/uPlot/config'; import { GrafanaTheme2 } from '@grafana/data'; -import { calculateFontSize, PlotTooltipInterpolator, VizTextDisplayOptions } from '@grafana/ui'; +import { + calculateFontSize, + PlotTooltipInterpolator, + VizTextDisplayOptions, + StackingMode, + BarValueVisibility, + ScaleDirection, + ScaleOrientation, +} from '@grafana/ui'; const groupDistr = SPACE_BETWEEN; const barDistr = SPACE_BETWEEN; @@ -33,6 +40,8 @@ export interface BarsOptions { groupWidth: number; barWidth: number; showValue: BarValueVisibility; + stacking: StackingMode; + rawValue: (seriesIdx: number, valueIdx: number) => number | null; formatValue: (seriesIdx: number, value: any) => string; text?: VizTextDisplayOptions; onHover?: (seriesIdx: number, valueIdx: number) => void; @@ -43,9 +52,10 @@ export interface BarsOptions { * @internal */ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { - const { xOri, xDir: dir, groupWidth, barWidth, formatValue, showValue } = opts; + const { xOri, xDir: dir, groupWidth, barWidth, rawValue, formatValue, showValue } = opts; const isXHorizontal = xOri === ScaleOrientation.Horizontal; const hasAutoValueSize = !Boolean(opts.text?.valueSize); + const isStacked = opts.stacking !== StackingMode.None; let qt: Quadtree; let hovered: Rect | undefined = undefined; @@ -91,6 +101,22 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { return out; }; + let distrOne = (groupCount: number, barCount: number) => { + let out = Array.from({ length: barCount }, () => ({ + offs: Array(groupCount).fill(0), + size: Array(groupCount).fill(0), + })); + + distribute(groupCount, groupWidth, groupDistr, null, (groupIdx, groupOffPct, groupDimPct) => { + distribute(barCount, barWidth, barDistr, null, (barIdx, barOffPct, barDimPct) => { + out[barIdx].offs[groupIdx] = groupOffPct; + out[barIdx].size[groupIdx] = groupDimPct; + }); + }); + + return out; + }; + let barsPctLayout: Array = []; let barRects: Rect[] = []; @@ -151,7 +177,12 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { s._paths = null; }); - barsPctLayout = ([null] as any).concat(distrTwo(u.data[0].length, u.data.length - 1)); + if (isStacked) { + //barsPctLayout = [null as any].concat(distrOne(u.data.length - 1, u.data[0].length)); + barsPctLayout = [null as any].concat(distrOne(u.data[0].length, u.data.length - 1)); + } else { + barsPctLayout = [null as any].concat(distrTwo(u.data[0].length, u.data.length - 1)); + } barRects.length = 0; vSpace = hSpace = Infinity; }; @@ -166,7 +197,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { let labelOffset = LABEL_OFFSET_MAX; barRects.forEach((r, i) => { - texts[i] = formatValue(r.sidx, u.data[r.sidx][r.didx]); + texts[i] = formatValue(r.sidx, rawValue(r.sidx, r.didx)); labelOffset = Math.min(labelOffset, Math.round(LABEL_OFFSET_FACTOR * (isXHorizontal ? r.w : r.h))); }); @@ -201,7 +232,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { let curAlign: CanvasTextAlign, curBaseline: CanvasTextBaseline; barRects.forEach((r, i) => { - let value = u.data[r.sidx][r.didx]; + let value = rawValue(r.sidx, r.didx); let text = texts[i]; if (value != null) { diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index 074d69a046d..69e876842ff 100755 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -86,6 +86,14 @@ export const plugin = new PanelPlugin(BarC }, defaultValue: BarValueVisibility.Auto, }) + .addRadio({ + path: 'stacking', + name: 'Stacking', + settings: { + options: graphFieldOptions.stacking, + }, + defaultValue: StackingMode.None, + }) .addSliderInput({ path: 'groupWidth', name: 'Group width', diff --git a/public/app/plugins/panel/barchart/utils.test.ts b/public/app/plugins/panel/barchart/utils.test.ts index 68eda8f55bc..b1d1096e7fb 100644 --- a/public/app/plugins/panel/barchart/utils.test.ts +++ b/public/app/plugins/panel/barchart/utils.test.ts @@ -143,7 +143,7 @@ describe('BarChart utils', () => { describe('prepareGraphableFrames', () => { it('will warn when there is no data in the response', () => { - const result = prepareGraphableFrames([]); + const result = prepareGraphableFrames([], StackingMode.None); expect(result.warn).toEqual('No data in response'); }); @@ -154,7 +154,7 @@ describe('BarChart utils', () => { { name: 'value', values: [1, 2, 3, 4, 5] }, ], }); - const result = prepareGraphableFrames([df]); + const result = prepareGraphableFrames([df], StackingMode.None); expect(result.warn).toEqual('Bar charts requires a string field'); expect(result.frames).toBeUndefined(); }); @@ -166,7 +166,7 @@ describe('BarChart utils', () => { { name: 'value', type: FieldType.boolean, values: [true, true, true, true, true] }, ], }); - const result = prepareGraphableFrames([df]); + const result = prepareGraphableFrames([df], StackingMode.None); expect(result.warn).toEqual('No numeric fields found'); expect(result.frames).toBeUndefined(); }); @@ -178,7 +178,7 @@ describe('BarChart utils', () => { { name: 'value', values: [-10, NaN, 10, -Infinity, +Infinity] }, ], }); - const result = prepareGraphableFrames([df]); + const result = prepareGraphableFrames([df], StackingMode.None); const field = result.frames![0].fields[1]; expect(field!.values.toArray()).toMatchInlineSnapshot(` diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index 070e2e50469..af07f1eac1f 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -17,9 +17,11 @@ import { ScaleDirection, ScaleDistribution, ScaleOrientation, + StackingMode, UPlotConfigBuilder, UPlotConfigPrepFn, } from '@grafana/ui'; +import { collectStackingGroups } from '../../../../../packages/grafana-ui/src/components/uPlot/utils'; /** @alpha */ function getBarCharScaleOrientation(orientation: VizOrientation) { @@ -47,6 +49,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ showValue, groupWidth, barWidth, + stacking, text, }) => { const builder = new UPlotConfigBuilder(); @@ -69,6 +72,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ xDir: vizOrientation.xDir, groupWidth, barWidth, + stacking, + rawValue: (seriesIdx: number, valueIdx: number) => frame.fields[seriesIdx].values.get(valueIdx), formatValue, text, showValue, @@ -106,6 +111,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ let seriesIndex = 0; + const stackingGroups: Map = new Map(); + // iterate the y values for (let i = 1; i < frame.fields.length; i++) { const field = frame.fields[i]; @@ -173,6 +180,19 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ theme, }); } + + collectStackingGroups(field, stackingGroups, seriesIndex); + } + + if (stackingGroups.size !== 0) { + builder.setStacking(true); + for (const [_, seriesIdxs] of stackingGroups.entries()) { + for (let j = seriesIdxs.length - 1; j > 0; j--) { + builder.addBand({ + series: [seriesIdxs[j], seriesIdxs[j - 1]], + }); + } + } } return builder; @@ -200,7 +220,10 @@ export function preparePlotFrame(data: DataFrame[]) { } /** @internal */ -export function prepareGraphableFrames(series: DataFrame[]): { frames?: DataFrame[]; warn?: string } { +export function prepareGraphableFrames( + series: DataFrame[], + stacking: StackingMode +): { frames?: DataFrame[]; warn?: string } { if (!series?.length) { return { warn: 'No data in response' }; } @@ -226,6 +249,16 @@ export function prepareGraphableFrames(series: DataFrame[]): { frames?: DataFram if (field.type === FieldType.number) { let copy = { ...field, + config: { + ...field.config, + custom: { + ...field.config.custom, + stacking: { + group: '_', + mode: stacking, + }, + }, + }, values: new ArrayVector( field.values.toArray().map((v) => { if (!(Number.isFinite(v) || v == null)) {