mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 10:20:29 -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,
|
||||
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) => {
|
||||
const theme = useTheme2();
|
||||
|
@ -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<BarChartOptions> {}
|
||||
* @alpha
|
||||
*/
|
||||
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(() => {
|
||||
if (!options.orientation || options.orientation === VizOrientation.Auto) {
|
||||
return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
|
||||
@ -20,6 +23,14 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ 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 (
|
||||
<div className="panel-empty">
|
||||
@ -40,7 +51,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
||||
orientation={orientation}
|
||||
>
|
||||
{(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>
|
||||
);
|
||||
|
@ -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<null | { offs: number[]; size: number[] }> = [];
|
||||
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) {
|
||||
|
@ -86,6 +86,14 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
||||
},
|
||||
defaultValue: BarValueVisibility.Auto,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'stacking',
|
||||
name: 'Stacking',
|
||||
settings: {
|
||||
options: graphFieldOptions.stacking,
|
||||
},
|
||||
defaultValue: StackingMode.None,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'groupWidth',
|
||||
name: 'Group width',
|
||||
|
@ -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(`
|
||||
|
@ -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<BarChartOptions> = ({
|
||||
showValue,
|
||||
groupWidth,
|
||||
barWidth,
|
||||
stacking,
|
||||
text,
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
@ -69,6 +72,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
||||
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<BarChartOptions> = ({
|
||||
|
||||
let seriesIndex = 0;
|
||||
|
||||
const stackingGroups: Map<string, number[]> = 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<BarChartOptions> = ({
|
||||
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)) {
|
||||
|
Loading…
Reference in New Issue
Block a user