mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
BarChart: support stacking options (#37083)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
parent
40a87a7851
commit
8d06aaaf09
@ -20,7 +20,7 @@ export interface BarChartProps
|
|||||||
extends BarChartOptions,
|
extends BarChartOptions,
|
||||||
Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend' | 'theme'> {}
|
Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend' | 'theme'> {}
|
||||||
|
|
||||||
const propsToDiff: string[] = ['orientation', 'barWidth', 'groupWidth', 'showValue', 'text'];
|
const propsToDiff: string[] = ['orientation', 'barWidth', 'groupWidth', 'stacking', 'showValue', 'text'];
|
||||||
|
|
||||||
export const BarChart: React.FC<BarChartProps> = (props) => {
|
export const BarChart: React.FC<BarChartProps> = (props) => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { PanelProps, TimeRange, VizOrientation } from '@grafana/data';
|
import { PanelProps, TimeRange, VizOrientation } from '@grafana/data';
|
||||||
import { TooltipPlugin } from '@grafana/ui';
|
import { StackingMode, TooltipDisplayMode, TooltipPlugin } from '@grafana/ui';
|
||||||
import { BarChartOptions } from './types';
|
import { BarChartOptions } from './types';
|
||||||
import { BarChart } from './BarChart';
|
import { BarChart } from './BarChart';
|
||||||
import { prepareGraphableFrames } from './utils';
|
import { prepareGraphableFrames } from './utils';
|
||||||
@ -11,7 +11,10 @@ interface Props extends PanelProps<BarChartOptions> {}
|
|||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, width, height, timeZone }) => {
|
export const BarChartPanel: React.FunctionComponent<Props> = ({ 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(() => {
|
const orientation = useMemo(() => {
|
||||||
if (!options.orientation || options.orientation === VizOrientation.Auto) {
|
if (!options.orientation || options.orientation === VizOrientation.Auto) {
|
||||||
return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
|
return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
|
||||||
@ -20,6 +23,14 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
|||||||
return options.orientation;
|
return options.orientation;
|
||||||
}, [width, height, 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) {
|
if (!frames || warn) {
|
||||||
return (
|
return (
|
||||||
<div className="panel-empty">
|
<div className="panel-empty">
|
||||||
@ -40,7 +51,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
|||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
>
|
>
|
||||||
{(config, alignedFrame) => {
|
{(config, alignedFrame) => {
|
||||||
return <TooltipPlugin data={alignedFrame} config={config} mode={options.tooltip.mode} timeZone={timeZone} />;
|
return <TooltipPlugin data={alignedFrame} config={config} mode={tooltip.mode} timeZone={timeZone} />;
|
||||||
}}
|
}}
|
||||||
</BarChart>
|
</BarChart>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
import uPlot, { Axis } from 'uplot';
|
import uPlot, { Axis } from 'uplot';
|
||||||
import { pointWithin, Quadtree, Rect } from './quadtree';
|
import { pointWithin, Quadtree, Rect } from './quadtree';
|
||||||
import { distribute, SPACE_BETWEEN } from './distribute';
|
import { distribute, SPACE_BETWEEN } from './distribute';
|
||||||
import { BarValueVisibility, ScaleDirection, ScaleOrientation } from '@grafana/ui/src/components/uPlot/config';
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
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 groupDistr = SPACE_BETWEEN;
|
||||||
const barDistr = SPACE_BETWEEN;
|
const barDistr = SPACE_BETWEEN;
|
||||||
@ -33,6 +40,8 @@ export interface BarsOptions {
|
|||||||
groupWidth: number;
|
groupWidth: number;
|
||||||
barWidth: number;
|
barWidth: number;
|
||||||
showValue: BarValueVisibility;
|
showValue: BarValueVisibility;
|
||||||
|
stacking: StackingMode;
|
||||||
|
rawValue: (seriesIdx: number, valueIdx: number) => number | null;
|
||||||
formatValue: (seriesIdx: number, value: any) => string;
|
formatValue: (seriesIdx: number, value: any) => string;
|
||||||
text?: VizTextDisplayOptions;
|
text?: VizTextDisplayOptions;
|
||||||
onHover?: (seriesIdx: number, valueIdx: number) => void;
|
onHover?: (seriesIdx: number, valueIdx: number) => void;
|
||||||
@ -43,9 +52,10 @@ export interface BarsOptions {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
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 isXHorizontal = xOri === ScaleOrientation.Horizontal;
|
||||||
const hasAutoValueSize = !Boolean(opts.text?.valueSize);
|
const hasAutoValueSize = !Boolean(opts.text?.valueSize);
|
||||||
|
const isStacked = opts.stacking !== StackingMode.None;
|
||||||
|
|
||||||
let qt: Quadtree;
|
let qt: Quadtree;
|
||||||
let hovered: Rect | undefined = undefined;
|
let hovered: Rect | undefined = undefined;
|
||||||
@ -91,6 +101,22 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
|||||||
return out;
|
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<null | { offs: number[]; size: number[] }> = [];
|
let barsPctLayout: Array<null | { offs: number[]; size: number[] }> = [];
|
||||||
let barRects: Rect[] = [];
|
let barRects: Rect[] = [];
|
||||||
|
|
||||||
@ -151,7 +177,12 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
|||||||
s._paths = null;
|
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;
|
barRects.length = 0;
|
||||||
vSpace = hSpace = Infinity;
|
vSpace = hSpace = Infinity;
|
||||||
};
|
};
|
||||||
@ -166,7 +197,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
|||||||
let labelOffset = LABEL_OFFSET_MAX;
|
let labelOffset = LABEL_OFFSET_MAX;
|
||||||
|
|
||||||
barRects.forEach((r, i) => {
|
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)));
|
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;
|
let curAlign: CanvasTextAlign, curBaseline: CanvasTextBaseline;
|
||||||
|
|
||||||
barRects.forEach((r, i) => {
|
barRects.forEach((r, i) => {
|
||||||
let value = u.data[r.sidx][r.didx];
|
let value = rawValue(r.sidx, r.didx);
|
||||||
let text = texts[i];
|
let text = texts[i];
|
||||||
|
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
|
@ -86,6 +86,14 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
|||||||
},
|
},
|
||||||
defaultValue: BarValueVisibility.Auto,
|
defaultValue: BarValueVisibility.Auto,
|
||||||
})
|
})
|
||||||
|
.addRadio({
|
||||||
|
path: 'stacking',
|
||||||
|
name: 'Stacking',
|
||||||
|
settings: {
|
||||||
|
options: graphFieldOptions.stacking,
|
||||||
|
},
|
||||||
|
defaultValue: StackingMode.None,
|
||||||
|
})
|
||||||
.addSliderInput({
|
.addSliderInput({
|
||||||
path: 'groupWidth',
|
path: 'groupWidth',
|
||||||
name: 'Group width',
|
name: 'Group width',
|
||||||
|
@ -143,7 +143,7 @@ describe('BarChart utils', () => {
|
|||||||
|
|
||||||
describe('prepareGraphableFrames', () => {
|
describe('prepareGraphableFrames', () => {
|
||||||
it('will warn when there is no data in the response', () => {
|
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');
|
expect(result.warn).toEqual('No data in response');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ describe('BarChart utils', () => {
|
|||||||
{ name: 'value', values: [1, 2, 3, 4, 5] },
|
{ 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.warn).toEqual('Bar charts requires a string field');
|
||||||
expect(result.frames).toBeUndefined();
|
expect(result.frames).toBeUndefined();
|
||||||
});
|
});
|
||||||
@ -166,7 +166,7 @@ describe('BarChart utils', () => {
|
|||||||
{ name: 'value', type: FieldType.boolean, values: [true, true, true, true, true] },
|
{ 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.warn).toEqual('No numeric fields found');
|
||||||
expect(result.frames).toBeUndefined();
|
expect(result.frames).toBeUndefined();
|
||||||
});
|
});
|
||||||
@ -178,7 +178,7 @@ describe('BarChart utils', () => {
|
|||||||
{ name: 'value', values: [-10, NaN, 10, -Infinity, +Infinity] },
|
{ 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];
|
const field = result.frames![0].fields[1];
|
||||||
expect(field!.values.toArray()).toMatchInlineSnapshot(`
|
expect(field!.values.toArray()).toMatchInlineSnapshot(`
|
||||||
|
@ -17,9 +17,11 @@ import {
|
|||||||
ScaleDirection,
|
ScaleDirection,
|
||||||
ScaleDistribution,
|
ScaleDistribution,
|
||||||
ScaleOrientation,
|
ScaleOrientation,
|
||||||
|
StackingMode,
|
||||||
UPlotConfigBuilder,
|
UPlotConfigBuilder,
|
||||||
UPlotConfigPrepFn,
|
UPlotConfigPrepFn,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
import { collectStackingGroups } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||||
|
|
||||||
/** @alpha */
|
/** @alpha */
|
||||||
function getBarCharScaleOrientation(orientation: VizOrientation) {
|
function getBarCharScaleOrientation(orientation: VizOrientation) {
|
||||||
@ -47,6 +49,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
|||||||
showValue,
|
showValue,
|
||||||
groupWidth,
|
groupWidth,
|
||||||
barWidth,
|
barWidth,
|
||||||
|
stacking,
|
||||||
text,
|
text,
|
||||||
}) => {
|
}) => {
|
||||||
const builder = new UPlotConfigBuilder();
|
const builder = new UPlotConfigBuilder();
|
||||||
@ -69,6 +72,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
|||||||
xDir: vizOrientation.xDir,
|
xDir: vizOrientation.xDir,
|
||||||
groupWidth,
|
groupWidth,
|
||||||
barWidth,
|
barWidth,
|
||||||
|
stacking,
|
||||||
|
rawValue: (seriesIdx: number, valueIdx: number) => frame.fields[seriesIdx].values.get(valueIdx),
|
||||||
formatValue,
|
formatValue,
|
||||||
text,
|
text,
|
||||||
showValue,
|
showValue,
|
||||||
@ -106,6 +111,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
|||||||
|
|
||||||
let seriesIndex = 0;
|
let seriesIndex = 0;
|
||||||
|
|
||||||
|
const stackingGroups: Map<string, number[]> = new Map();
|
||||||
|
|
||||||
// iterate the y values
|
// iterate the y values
|
||||||
for (let i = 1; i < frame.fields.length; i++) {
|
for (let i = 1; i < frame.fields.length; i++) {
|
||||||
const field = frame.fields[i];
|
const field = frame.fields[i];
|
||||||
@ -173,6 +180,19 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
|||||||
theme,
|
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;
|
return builder;
|
||||||
@ -200,7 +220,10 @@ export function preparePlotFrame(data: DataFrame[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export function prepareGraphableFrames(series: DataFrame[]): { frames?: DataFrame[]; warn?: string } {
|
export function prepareGraphableFrames(
|
||||||
|
series: DataFrame[],
|
||||||
|
stacking: StackingMode
|
||||||
|
): { frames?: DataFrame[]; warn?: string } {
|
||||||
if (!series?.length) {
|
if (!series?.length) {
|
||||||
return { warn: 'No data in response' };
|
return { warn: 'No data in response' };
|
||||||
}
|
}
|
||||||
@ -226,6 +249,16 @@ export function prepareGraphableFrames(series: DataFrame[]): { frames?: DataFram
|
|||||||
if (field.type === FieldType.number) {
|
if (field.type === FieldType.number) {
|
||||||
let copy = {
|
let copy = {
|
||||||
...field,
|
...field,
|
||||||
|
config: {
|
||||||
|
...field.config,
|
||||||
|
custom: {
|
||||||
|
...field.config.custom,
|
||||||
|
stacking: {
|
||||||
|
group: '_',
|
||||||
|
mode: stacking,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
values: new ArrayVector(
|
values: new ArrayVector(
|
||||||
field.values.toArray().map((v) => {
|
field.values.toArray().map((v) => {
|
||||||
if (!(Number.isFinite(v) || v == null)) {
|
if (!(Number.isFinite(v) || v == null)) {
|
||||||
|
Loading…
Reference in New Issue
Block a user