mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 15:45:43 -06:00
TimeSeries & BarChart: refactor stacking (#47373)
Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com> Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
3456793e0f
commit
dfdfe3f428
2957
devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json
Normal file
2957
devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -82,7 +82,7 @@ function sameProps(prevProps: any, nextProps: any, propsToDiff: Array<string | P
|
||||
*/
|
||||
export interface GraphNGState {
|
||||
alignedFrame: DataFrame;
|
||||
alignedData: AlignedData;
|
||||
alignedData?: AlignedData;
|
||||
config?: UPlotConfigBuilder;
|
||||
}
|
||||
|
||||
@ -98,7 +98,9 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
|
||||
constructor(props: GraphNGProps) {
|
||||
super(props);
|
||||
this.state = this.prepState(props);
|
||||
let state = this.prepState(props);
|
||||
state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData;
|
||||
this.state = state;
|
||||
this.plotInstance = React.createRef();
|
||||
}
|
||||
|
||||
@ -131,7 +133,6 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
|
||||
state = {
|
||||
alignedFrame,
|
||||
alignedData: config!.prepData!([alignedFrame]) as AlignedData,
|
||||
config,
|
||||
};
|
||||
|
||||
@ -229,12 +230,13 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
|
||||
if (shouldReconfig) {
|
||||
newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange);
|
||||
newState.alignedData = newState.config.prepData!([newState.alignedFrame]) as AlignedData;
|
||||
pluginLog('GraphNG', false, 'config recreated', newState.config);
|
||||
}
|
||||
}
|
||||
|
||||
newState && this.setState(newState);
|
||||
newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData;
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,7 +257,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
{(vizWidth: number, vizHeight: number) => (
|
||||
<UPlotChart
|
||||
config={config}
|
||||
data={alignedData}
|
||||
data={alignedData!}
|
||||
width={vizWidth}
|
||||
height={vizHeight}
|
||||
timeRange={timeRange}
|
||||
|
@ -58,22 +58,6 @@ Object {
|
||||
"values": [Function],
|
||||
},
|
||||
],
|
||||
"bands": Array [
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
2,
|
||||
1,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
4,
|
||||
3,
|
||||
],
|
||||
},
|
||||
],
|
||||
"cursor": Object {
|
||||
"dataIdx": [Function],
|
||||
"drag": Object {
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||
import { UPlotChart } from '../uPlot/Plot';
|
||||
import { Themeable2 } from '../../types';
|
||||
import { preparePlotData } from '../uPlot/utils';
|
||||
import { preparePlotData2, getStackingGroups } from '../uPlot/utils';
|
||||
import { preparePlotFrame } from './utils';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@ -51,7 +51,7 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
|
||||
const alignedDataFrame = preparePlotFrame(props.sparkline, props.config);
|
||||
|
||||
this.state = {
|
||||
data: preparePlotData([alignedDataFrame]),
|
||||
data: preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame)),
|
||||
alignedDataFrame,
|
||||
configBuilder: this.prepareConfig(alignedDataFrame),
|
||||
};
|
||||
@ -65,7 +65,7 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
|
||||
|
||||
return {
|
||||
...state,
|
||||
data: preparePlotData([frame]),
|
||||
data: preparePlotData2(frame, getStackingGroups(frame)),
|
||||
alignedDataFrame: frame,
|
||||
};
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
|
||||
|
||||
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||
const { eventBus, sync } = this.context as PanelContext;
|
||||
const { theme, timeZone, legend, renderers, tweakAxis, tweakScale } = this.props;
|
||||
const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props;
|
||||
|
||||
return preparePlotConfigBuilder({
|
||||
frame: alignedFrame,
|
||||
@ -29,7 +29,6 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
|
||||
eventBus,
|
||||
sync,
|
||||
allFrames,
|
||||
legend,
|
||||
renderers,
|
||||
tweakScale,
|
||||
tweakAxis,
|
||||
|
@ -23,10 +23,9 @@ import {
|
||||
VisibilityMode,
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
VizLegendOptions,
|
||||
StackingMode,
|
||||
} from '@grafana/schema';
|
||||
import { collectStackingGroups, INTERNAL_NEGATIVE_Y_PREFIX, orderIdsByCalcs, preparePlotData } from '../uPlot/utils';
|
||||
import { getStackingGroups, preparePlotData2 } from '../uPlot/utils';
|
||||
import uPlot from 'uplot';
|
||||
import { buildScaleKey } from '../GraphNG/utils';
|
||||
|
||||
@ -40,7 +39,6 @@ const defaultConfig: GraphFieldConfig = {
|
||||
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
sync?: () => DashboardCursorSync;
|
||||
legend?: VizLegendOptions;
|
||||
}> = ({
|
||||
frame,
|
||||
theme,
|
||||
@ -50,13 +48,12 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
sync,
|
||||
allFrames,
|
||||
renderers,
|
||||
legend,
|
||||
tweakScale = (opts) => opts,
|
||||
tweakAxis = (opts) => opts,
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder(timeZone);
|
||||
|
||||
builder.setPrepData((prepData) => preparePlotData(prepData, undefined, legend));
|
||||
builder.setPrepData((frames) => preparePlotData2(frames[0], builder.getStackingGroups()));
|
||||
|
||||
// X is the first field in the aligned frame
|
||||
const xField = frame.fields[0];
|
||||
@ -122,8 +119,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
let customRenderedFields =
|
||||
renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? [];
|
||||
|
||||
const stackingGroups: Map<string, number[]> = new Map();
|
||||
|
||||
let indexByName: Map<string, number> | undefined;
|
||||
|
||||
for (let i = 1; i < frame.fields.length; i++) {
|
||||
@ -328,20 +323,11 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
});
|
||||
}
|
||||
}
|
||||
collectStackingGroups(field, stackingGroups, seriesIndex);
|
||||
}
|
||||
|
||||
if (stackingGroups.size !== 0) {
|
||||
for (const [group, seriesIds] of stackingGroups.entries()) {
|
||||
const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame });
|
||||
for (let j = seriesIdxs.length - 1; j > 0; j--) {
|
||||
builder.addBand({
|
||||
series: [seriesIdxs[j], seriesIdxs[j - 1]],
|
||||
dir: group.startsWith(INTERNAL_NEGATIVE_Y_PREFIX) ? 1 : -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let stackingGroups = getStackingGroups(frame);
|
||||
|
||||
builder.setStackingGroups(stackingGroups);
|
||||
|
||||
// hook up custom/composite renderers
|
||||
renderers?.forEach((r) => {
|
||||
|
@ -6,7 +6,7 @@ import { GraphFieldConfig, GraphDrawStyle } from '@grafana/schema';
|
||||
import uPlot from 'uplot';
|
||||
import createMockRaf from 'mock-raf';
|
||||
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
|
||||
import { preparePlotData } from './utils';
|
||||
import { preparePlotData2, getStackingGroups } from './utils';
|
||||
import { SeriesProps } from './config/UPlotSeriesBuilder';
|
||||
|
||||
const mockRaf = createMockRaf();
|
||||
@ -55,7 +55,7 @@ const mockData = () => {
|
||||
|
||||
const config = new UPlotConfigBuilder();
|
||||
config.addSeries({} as SeriesProps);
|
||||
return { data: [data], timeRange, config };
|
||||
return { data: data, timeRange, config };
|
||||
};
|
||||
|
||||
describe('UPlotChart', () => {
|
||||
@ -75,7 +75,7 @@ describe('UPlotChart', () => {
|
||||
|
||||
const { unmount } = render(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // mock
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // mock
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
@ -94,7 +94,7 @@ describe('UPlotChart', () => {
|
||||
|
||||
const { rerender } = render(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // mock
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // mock
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
@ -104,11 +104,11 @@ describe('UPlotChart', () => {
|
||||
|
||||
expect(uPlot).toBeCalledTimes(1);
|
||||
|
||||
data[0].fields[1].values.set(0, 1);
|
||||
data.fields[1].values.set(0, 1);
|
||||
|
||||
rerender(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // changed
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // changed
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
@ -124,7 +124,13 @@ describe('UPlotChart', () => {
|
||||
it('skips uPlot intialization for width and height equal 0', async () => {
|
||||
const { data, timeRange, config } = mockData();
|
||||
const { queryAllByTestId } = render(
|
||||
<UPlotChart data={preparePlotData(data)} config={config} timeRange={timeRange} width={0} height={0} />
|
||||
<UPlotChart
|
||||
data={preparePlotData2(data, getStackingGroups(data))}
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={0}
|
||||
height={0}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(queryAllByTestId('uplot-main-div')).toHaveLength(1);
|
||||
@ -136,7 +142,7 @@ describe('UPlotChart', () => {
|
||||
|
||||
const { rerender } = render(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // frame
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // frame
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
@ -150,7 +156,13 @@ describe('UPlotChart', () => {
|
||||
nextConfig.addSeries({} as SeriesProps);
|
||||
|
||||
rerender(
|
||||
<UPlotChart data={preparePlotData(data)} config={nextConfig} timeRange={timeRange} width={100} height={100} />
|
||||
<UPlotChart
|
||||
data={preparePlotData2(data, getStackingGroups(data))}
|
||||
config={nextConfig}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(destroyMock).toBeCalledTimes(1);
|
||||
@ -162,7 +174,7 @@ describe('UPlotChart', () => {
|
||||
|
||||
const { rerender } = render(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // frame
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // frame
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
@ -173,7 +185,7 @@ describe('UPlotChart', () => {
|
||||
// we wait 1 frame for plugins initialisation logic to finish
|
||||
rerender(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // frame
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // frame
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={200}
|
||||
|
@ -15,7 +15,7 @@ import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
|
||||
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
|
||||
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
|
||||
import { AxisPlacement } from '@grafana/schema';
|
||||
import { pluginLog } from '../utils';
|
||||
import { getStackingBands, pluginLog, StackingGroup } from '../utils';
|
||||
import { getThresholdsDrawHook, UPlotThresholdOptions } from './UPlotThresholds';
|
||||
|
||||
const cursorDefaults: Cursor = {
|
||||
@ -33,12 +33,14 @@ const cursorDefaults: Cursor = {
|
||||
};
|
||||
|
||||
type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
|
||||
type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData;
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
private series: UPlotSeriesBuilder[] = [];
|
||||
private axes: Record<string, UPlotAxisBuilder> = {};
|
||||
private scales: UPlotScaleBuilder[] = [];
|
||||
private bands: Band[] = [];
|
||||
private stackingGroups: StackingGroup[] = [];
|
||||
private cursor: Cursor | undefined;
|
||||
private select: uPlot.Select | undefined;
|
||||
private hasLeftAxis = false;
|
||||
@ -143,6 +145,14 @@ export class UPlotConfigBuilder {
|
||||
this.bands.push(band);
|
||||
}
|
||||
|
||||
setStackingGroups(groups: StackingGroup[]) {
|
||||
this.stackingGroups = groups;
|
||||
}
|
||||
|
||||
getStackingGroups() {
|
||||
return this.stackingGroups;
|
||||
}
|
||||
|
||||
setTooltipInterpolator(interpolator: PlotTooltipInterpolator) {
|
||||
this.tooltipInterpolator = interpolator;
|
||||
}
|
||||
@ -151,10 +161,10 @@ export class UPlotConfigBuilder {
|
||||
return this.tooltipInterpolator;
|
||||
}
|
||||
|
||||
setPrepData(prepData: PrepData) {
|
||||
setPrepData(prepData: PreDataStacked) {
|
||||
this.prepData = (frames) => {
|
||||
this.frames = frames;
|
||||
return prepData(frames);
|
||||
return prepData(frames, this.getStackingGroups());
|
||||
};
|
||||
}
|
||||
|
||||
@ -221,6 +231,14 @@ export class UPlotConfigBuilder {
|
||||
config.tzDate = this.tzDate;
|
||||
config.padding = this.padding;
|
||||
|
||||
if (this.stackingGroups.length) {
|
||||
this.stackingGroups.forEach((group) => {
|
||||
getStackingBands(group).forEach((band) => {
|
||||
this.addBand(band);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.bands.length) {
|
||||
config.bands = this.bands;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { orderIdsByCalcs, preparePlotData, timeFormatToTemplate } from './utils';
|
||||
import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils';
|
||||
import { FieldType, MutableDataFrame } from '@grafana/data';
|
||||
import { GraphTransform, StackingMode } from '@grafana/schema';
|
||||
import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema';
|
||||
import Units from 'ol/proj/Units';
|
||||
|
||||
describe('timeFormatToTemplate', () => {
|
||||
it.each`
|
||||
@ -16,7 +17,7 @@ describe('timeFormatToTemplate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('preparePlotData', () => {
|
||||
describe('preparePlotData2', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
@ -27,7 +28,7 @@ describe('preparePlotData', () => {
|
||||
});
|
||||
|
||||
it('creates array from DataFrame', () => {
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@ -63,7 +64,7 @@ describe('preparePlotData', () => {
|
||||
{ name: 'c', values: [20, 20, 20], config: { custom: { transform: GraphTransform.NegativeY } } },
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@ -104,7 +105,7 @@ describe('preparePlotData', () => {
|
||||
{ name: 'i', values: [20, undefined, 20, 20], config: { custom: { transform: GraphTransform.NegativeY } } },
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@ -178,7 +179,7 @@ describe('preparePlotData', () => {
|
||||
{ name: 'c', values: [20, 20, 20] },
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@ -226,7 +227,7 @@ describe('preparePlotData', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@ -273,7 +274,7 @@ describe('preparePlotData', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@ -329,7 +330,7 @@ describe('preparePlotData', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@ -397,7 +398,7 @@ describe('preparePlotData', () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@ -472,7 +473,7 @@ describe('preparePlotData', () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@ -507,239 +508,330 @@ describe('preparePlotData', () => {
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('ignores nullish-only stacks', () => {
|
||||
test('single stacking group', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-10, null, 10],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [10, null, null],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [20, undefined, 20],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
-10,
|
||||
null,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
0,
|
||||
null,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
null,
|
||||
30,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
test('multiple stacking groups', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-10, undefined, 10],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [10, undefined, 10],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [20, undefined, 20],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
values: [1, 2, null],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
|
||||
},
|
||||
{
|
||||
name: 'e',
|
||||
values: [1, 2, null],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
|
||||
},
|
||||
{
|
||||
name: 'f',
|
||||
values: [1, 2, null],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
-10,
|
||||
null,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
0,
|
||||
null,
|
||||
20,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
null,
|
||||
40,
|
||||
],
|
||||
Array [
|
||||
1,
|
||||
2,
|
||||
null,
|
||||
],
|
||||
Array [
|
||||
2,
|
||||
4,
|
||||
null,
|
||||
],
|
||||
Array [
|
||||
3,
|
||||
6,
|
||||
null,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
describe('with legend sorted', () => {
|
||||
it('should affect when single group', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-10, 20, 10],
|
||||
state: { calcs: { max: 20 } },
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [10, 10, 10],
|
||||
state: { calcs: { max: 10 } },
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [20, 20, 20],
|
||||
state: { calcs: { max: 20 } },
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df], undefined, { sortBy: 'Max', sortDesc: false } as any)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
0,
|
||||
30,
|
||||
20,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
50,
|
||||
40,
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(preparePlotData([df], undefined, { sortBy: 'Max', sortDesc: true } as any)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
-10,
|
||||
20,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
50,
|
||||
40,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
40,
|
||||
30,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('orderIdsByCalcs', () => {
|
||||
const ids = [1, 2, 3, 4];
|
||||
const frame = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{ name: 'a', values: [-10, 20, 10], state: { calcs: { min: -10 } } },
|
||||
{ name: 'b', values: [20, 20, 20], state: { calcs: { min: 20 } } },
|
||||
{ name: 'c', values: [10, 10, 10], state: { calcs: { min: 10 } } },
|
||||
{ name: 'd', values: [30, 30, 30] },
|
||||
],
|
||||
describe('auto stacking groups', () => {
|
||||
test('split on stacking mode', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Percent } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [4, 5, 6],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
1,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
2,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ legend: undefined },
|
||||
{ legend: { sortBy: 'Min' } },
|
||||
{ legend: { sortDesc: false } },
|
||||
{ legend: {} },
|
||||
{ sortBy: 'Mik', sortDesc: true },
|
||||
])('should return without ordering if legend option is %o', (legend: any) => {
|
||||
const result = orderIdsByCalcs({ ids, frame, legend });
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
test('split pos/neg', () => {
|
||||
// since we expect most series to be Pos, we try to bail early when scanning all values
|
||||
// as soon as we find a value >= 0, it's assumed Pos, else Neg
|
||||
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-1, null, -3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [0, 0, 0],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
1,
|
||||
3,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
2,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should order the ids based on the frame stat', () => {
|
||||
const resultDesc = orderIdsByCalcs({ ids, frame, legend: { sortBy: 'Min', sortDesc: true } as any });
|
||||
expect(resultDesc).toEqual([4, 2, 3, 1]);
|
||||
const resultAsc = orderIdsByCalcs({ ids, frame, legend: { sortBy: 'Min', sortDesc: false } as any });
|
||||
expect(resultAsc).toEqual([1, 3, 2, 4]);
|
||||
test('split pos/neg with NegY', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-1, null, -3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal }, transform: GraphTransform.NegativeY } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [0, 0, 0],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
1,
|
||||
2,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('split on drawStyle, lineInterpolation, barAlignment', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Bars,
|
||||
barAlignment: BarAlignment.After,
|
||||
stacking: { mode: StackingMode.Normal },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Bars,
|
||||
barAlignment: BarAlignment.Before,
|
||||
stacking: { mode: StackingMode.Normal },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Line,
|
||||
lineInterpolation: LineInterpolation.Linear,
|
||||
stacking: { mode: StackingMode.Normal },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Line,
|
||||
lineInterpolation: LineInterpolation.Smooth,
|
||||
stacking: { mode: StackingMode.Normal },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'e',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { drawStyle: GraphDrawStyle.Points, stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
1,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
2,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
4,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
5,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('split on axis & units (scaleKey)', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } }, unit: Units.FEET },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } }, unit: Units.DEGREES },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
1,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
2,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('split on explicit stacking group & mode & pos/neg w/NegY', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'A' } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'A' } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Percent, group: 'A' } } },
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'B' } } },
|
||||
},
|
||||
{
|
||||
name: 'e',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Percent, group: 'B' } } },
|
||||
},
|
||||
{
|
||||
name: 'e',
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: { stacking: { mode: StackingMode.Percent, group: 'B' }, transform: GraphTransform.NegativeY },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
1,
|
||||
2,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
4,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
5,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
6,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { DataFrame, ensureTimeField, Field, FieldType } from '@grafana/data';
|
||||
import { GraphFieldConfig, GraphTransform, StackingMode, VizLegendOptions } from '@grafana/schema';
|
||||
import { orderBy } from 'lodash';
|
||||
import { DataFrame, ensureTimeField, FieldType } from '@grafana/data';
|
||||
import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema';
|
||||
import uPlot, { AlignedData, Options, PaddingSide } from 'uplot';
|
||||
import { attachDebugger } from '../../utils';
|
||||
import { createLogger } from '../../utils/logger';
|
||||
import { buildScaleKey } from '../GraphNG/utils';
|
||||
|
||||
const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g;
|
||||
export const INTERNAL_NEGATIVE_Y_PREFIX = '__internalNegY';
|
||||
|
||||
export function timeFormatToTemplate(f: string) {
|
||||
return f.replace(ALLOWED_FORMAT_STRINGS_REGEX, (match) => `{${match}}`);
|
||||
@ -41,115 +40,225 @@ interface StackMeta {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function preparePlotData(
|
||||
frames: DataFrame[],
|
||||
onStackMeta?: (meta: StackMeta) => void,
|
||||
legend?: VizLegendOptions
|
||||
): AlignedData {
|
||||
const frame = frames[0];
|
||||
const result: any[] = [];
|
||||
const stackingGroups: Map<string, number[]> = new Map();
|
||||
let seriesIndex = 0;
|
||||
|
||||
for (let i = 0; i < frame.fields.length; i++) {
|
||||
const f = frame.fields[i];
|
||||
|
||||
if (f.type === FieldType.time) {
|
||||
result.push(ensureTimeField(f).values.toArray());
|
||||
seriesIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
collectStackingGroups(f, stackingGroups, seriesIndex);
|
||||
const customConfig: GraphFieldConfig = f.config.custom || {};
|
||||
|
||||
const values = f.values.toArray();
|
||||
|
||||
if (customConfig.transform === GraphTransform.NegativeY) {
|
||||
result.push(values.map((v) => (v == null ? v : v * -1)));
|
||||
} else if (customConfig.transform === GraphTransform.Constant) {
|
||||
result.push(new Array(values.length).fill(values[0]));
|
||||
} else {
|
||||
result.push(values);
|
||||
}
|
||||
seriesIndex++;
|
||||
}
|
||||
|
||||
// Stacking
|
||||
if (stackingGroups.size !== 0) {
|
||||
const byPct = frame.fields[1].config.custom?.stacking?.mode === StackingMode.Percent;
|
||||
const dataLength = result[0].length;
|
||||
const alignedTotals = Array(stackingGroups.size);
|
||||
alignedTotals[0] = null;
|
||||
|
||||
// array or stacking groups
|
||||
for (const [_, seriesIds] of stackingGroups.entries()) {
|
||||
const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame });
|
||||
const noValueStack = Array(dataLength).fill(true);
|
||||
const groupTotals = byPct ? Array(dataLength).fill(0) : null;
|
||||
|
||||
if (byPct) {
|
||||
for (let j = 0; j < seriesIdxs.length; j++) {
|
||||
const currentlyStacking = result[seriesIdxs[j]];
|
||||
|
||||
for (let k = 0; k < dataLength; k++) {
|
||||
const v = currentlyStacking[k];
|
||||
groupTotals![k] += v == null ? 0 : +v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const acc = Array(dataLength).fill(0);
|
||||
|
||||
for (let j = 0; j < seriesIdxs.length; j++) {
|
||||
let seriesIdx = seriesIdxs[j];
|
||||
|
||||
alignedTotals[seriesIdx] = groupTotals;
|
||||
|
||||
const currentlyStacking = result[seriesIdx];
|
||||
|
||||
for (let k = 0; k < dataLength; k++) {
|
||||
const v = currentlyStacking[k];
|
||||
if (v != null && noValueStack[k]) {
|
||||
noValueStack[k] = false;
|
||||
}
|
||||
acc[k] += v == null ? 0 : v / (byPct ? groupTotals![k] : 1);
|
||||
}
|
||||
|
||||
result[seriesIdx] = acc.slice().map((v, i) => (noValueStack[i] ? null : v));
|
||||
}
|
||||
}
|
||||
|
||||
onStackMeta &&
|
||||
onStackMeta({
|
||||
totals: alignedTotals as AlignedData,
|
||||
});
|
||||
}
|
||||
|
||||
return result as AlignedData;
|
||||
export interface StackingGroup {
|
||||
series: number[];
|
||||
dir: StackDirection;
|
||||
}
|
||||
|
||||
export function collectStackingGroups(f: Field, groups: Map<string, number[]>, seriesIdx: number) {
|
||||
const customConfig = f.config.custom;
|
||||
if (!customConfig) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
customConfig.stacking?.mode !== StackingMode.None &&
|
||||
customConfig.stacking?.group &&
|
||||
!customConfig.hideFrom?.viz
|
||||
) {
|
||||
const group =
|
||||
customConfig.transform === GraphTransform.NegativeY
|
||||
? `${INTERNAL_NEGATIVE_Y_PREFIX}-${customConfig.stacking.group}`
|
||||
: customConfig.stacking.group;
|
||||
/** @internal */
|
||||
const enum StackDirection {
|
||||
Pos = 1,
|
||||
Neg = -1,
|
||||
}
|
||||
|
||||
if (!groups.has(group)) {
|
||||
groups.set(group, [seriesIdx]);
|
||||
} else {
|
||||
groups.set(group, groups.get(group)!.concat(seriesIdx));
|
||||
// generates bands between adjacent group series
|
||||
/** @internal */
|
||||
export function getStackingBands(group: StackingGroup) {
|
||||
let bands: uPlot.Band[] = [];
|
||||
let { series, dir } = group;
|
||||
let lastIdx = series.length - 1;
|
||||
|
||||
let rSeries = series.slice().reverse();
|
||||
|
||||
rSeries.forEach((si, i) => {
|
||||
if (i !== lastIdx) {
|
||||
let nextIdx = rSeries[i + 1];
|
||||
bands.push({
|
||||
series: [si, nextIdx],
|
||||
// fill direction is inverted from stack direction
|
||||
dir: (-1 * dir) as 1 | -1,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return bands;
|
||||
}
|
||||
|
||||
// expects an AlignedFrame
|
||||
/** @internal */
|
||||
export function getStackingGroups(frame: DataFrame) {
|
||||
let groups: Map<string, StackingGroup> = new Map();
|
||||
|
||||
frame.fields.forEach(({ config, values }, i) => {
|
||||
// skip x or time field
|
||||
if (i === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { custom } = config;
|
||||
|
||||
if (custom == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: currently all AlignedFrame fields end up in uplot series & data, even custom.hideFrom?.viz
|
||||
// ideally hideFrom.viz fields would be excluded so we can remove this
|
||||
if (custom.hideFrom?.viz) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { stacking } = custom;
|
||||
|
||||
if (stacking == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { mode: stackingMode, group: stackingGroup } = stacking;
|
||||
|
||||
// not stacking
|
||||
if (stackingMode === StackingMode.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
// will this be stacked up or down after any transforms applied
|
||||
let vals = values.toArray();
|
||||
let transform = custom.transform;
|
||||
let stackDir =
|
||||
transform === GraphTransform.Constant
|
||||
? vals[0] > 0
|
||||
? StackDirection.Pos
|
||||
: StackDirection.Neg
|
||||
: transform === GraphTransform.NegativeY
|
||||
? vals.some((v) => v > 0)
|
||||
? StackDirection.Neg
|
||||
: StackDirection.Pos
|
||||
: vals.some((v) => v > 0)
|
||||
? StackDirection.Pos
|
||||
: StackDirection.Neg;
|
||||
|
||||
let drawStyle = custom.drawStyle as GraphDrawStyle;
|
||||
let drawStyle2 =
|
||||
drawStyle === GraphDrawStyle.Bars
|
||||
? (custom.barAlignment as BarAlignment)
|
||||
: drawStyle === GraphDrawStyle.Line
|
||||
? (custom.lineInterpolation as LineInterpolation)
|
||||
: null;
|
||||
|
||||
let stackKey = `${stackDir}|${stackingMode}|${stackingGroup}|${buildScaleKey(config)}|${drawStyle}|${drawStyle2}`;
|
||||
|
||||
let group = groups.get(stackKey);
|
||||
|
||||
if (group == null) {
|
||||
group = {
|
||||
series: [],
|
||||
dir: stackDir,
|
||||
};
|
||||
|
||||
groups.set(stackKey, group);
|
||||
}
|
||||
|
||||
group.series.push(i);
|
||||
});
|
||||
|
||||
return [...groups.values()];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function preparePlotData2(
|
||||
frame: DataFrame,
|
||||
stackingGroups: StackingGroup[],
|
||||
onStackMeta?: (meta: StackMeta) => void
|
||||
) {
|
||||
let data = Array(frame.fields.length) as AlignedData;
|
||||
|
||||
let dataLen = frame.length;
|
||||
let zeroArr = Array(dataLen).fill(0);
|
||||
let accums = Array.from({ length: stackingGroups.length }, () => zeroArr.slice());
|
||||
|
||||
frame.fields.forEach((field, i) => {
|
||||
let vals = field.values.toArray();
|
||||
|
||||
if (i === 0) {
|
||||
if (field.type === FieldType.time) {
|
||||
data[i] = ensureTimeField(field).values.toArray();
|
||||
} else {
|
||||
data[i] = vals;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let { custom } = field.config;
|
||||
|
||||
if (!custom || custom.hideFrom?.viz) {
|
||||
data[i] = vals;
|
||||
return;
|
||||
}
|
||||
|
||||
// apply transforms
|
||||
if (custom.transform === GraphTransform.Constant) {
|
||||
vals = Array(vals.length).fill(vals[0]);
|
||||
} else {
|
||||
vals = vals.slice();
|
||||
|
||||
if (custom.transform === GraphTransform.NegativeY) {
|
||||
for (let i = 0; i < vals.length; i++) {
|
||||
if (vals[i] != null) {
|
||||
vals[i] *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stackingMode = custom.stacking?.mode;
|
||||
|
||||
if (!stackingMode || stackingMode === StackingMode.None) {
|
||||
data[i] = vals;
|
||||
} else {
|
||||
let stackIdx = stackingGroups.findIndex((group) => group.series.indexOf(i) > -1);
|
||||
|
||||
let accum = accums[stackIdx];
|
||||
let stacked = (data[i] = Array(dataLen));
|
||||
|
||||
for (let i = 0; i < dataLen; i++) {
|
||||
let v = vals[i];
|
||||
|
||||
if (v != null) {
|
||||
stacked[i] = accum[i] += v;
|
||||
} else {
|
||||
stacked[i] = v; // we may want to coerce to 0 here
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (onStackMeta) {
|
||||
let accumsBySeriesIdx = data.map((vals, i) => {
|
||||
let stackIdx = stackingGroups.findIndex((group) => group.series.indexOf(i) > -1);
|
||||
return stackIdx !== -1 ? accums[stackIdx] : vals;
|
||||
});
|
||||
|
||||
onStackMeta({
|
||||
totals: accumsBySeriesIdx as AlignedData,
|
||||
});
|
||||
}
|
||||
|
||||
// re-compute by percent
|
||||
frame.fields.forEach((field, i) => {
|
||||
if (i === 0 || field.config.custom?.hideFrom?.viz) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stackingMode = field.config.custom?.stacking?.mode;
|
||||
|
||||
if (stackingMode === StackingMode.Percent) {
|
||||
let stackIdx = stackingGroups.findIndex((group) => group.series.indexOf(i) > -1);
|
||||
let accum = accums[stackIdx];
|
||||
let group = stackingGroups[stackIdx];
|
||||
|
||||
let stacked = data[i];
|
||||
|
||||
for (let i = 0; i < dataLen; i++) {
|
||||
let v = stacked[i];
|
||||
|
||||
if (v != null) {
|
||||
// v / accum will always be pos, so properly (re)sign by group stacking dir
|
||||
stacked[i] = group.dir * (v / accum[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,23 +324,3 @@ export const pluginLogger = createLogger('uPlot');
|
||||
export const pluginLog = pluginLogger.logger;
|
||||
// pluginLogger.enable();
|
||||
attachDebugger('graphng', undefined, pluginLogger);
|
||||
|
||||
type OrderIdsByCalcsOptions = {
|
||||
legend?: VizLegendOptions;
|
||||
ids: number[];
|
||||
frame: DataFrame;
|
||||
};
|
||||
export function orderIdsByCalcs({ legend, ids, frame }: OrderIdsByCalcsOptions) {
|
||||
if (!legend?.sortBy || legend.sortDesc == null) {
|
||||
return ids;
|
||||
}
|
||||
const orderedIds = orderBy<number>(
|
||||
ids,
|
||||
(id) => {
|
||||
return frame.fields[id].state?.calcs?.[legend.sortBy!.toLowerCase()];
|
||||
},
|
||||
legend.sortDesc ? 'desc' : 'asc'
|
||||
);
|
||||
|
||||
return orderedIds;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
VizTextDisplayOptions,
|
||||
VizLegendOptions,
|
||||
} from '@grafana/schema';
|
||||
import { preparePlotData } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||
import { preparePlotData2, StackingGroup } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||
import { formatTime } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
|
||||
|
||||
@ -564,16 +564,11 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
|
||||
let alignedTotals: AlignedData | null = null;
|
||||
|
||||
function prepData(frames: DataFrame[]) {
|
||||
function prepData(frames: DataFrame[], stackingGroups: StackingGroup[]) {
|
||||
alignedTotals = null;
|
||||
|
||||
return preparePlotData(
|
||||
frames,
|
||||
({ totals }) => {
|
||||
alignedTotals = totals;
|
||||
},
|
||||
opts.legend
|
||||
);
|
||||
return preparePlotData2(frames[0], stackingGroups, ({ totals }) => {
|
||||
alignedTotals = totals;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -25,9 +25,9 @@ import {
|
||||
StackingMode,
|
||||
VizLegendOptions,
|
||||
} from '@grafana/schema';
|
||||
import { collectStackingGroups, orderIdsByCalcs } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||
import { orderBy } from 'lodash';
|
||||
import { findField } from 'app/features/dimensions';
|
||||
import { getStackingGroups } from '@grafana/ui/src/components/uPlot/utils';
|
||||
|
||||
function getBarCharScaleOrientation(orientation: VizOrientation) {
|
||||
if (orientation === VizOrientation.Vertical) {
|
||||
@ -156,7 +156,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
|
||||
|
||||
let seriesIndex = 0;
|
||||
const legendOrdered = isLegendOrdered(legend);
|
||||
const stackingGroups: Map<string, number[]> = new Map();
|
||||
|
||||
// iterate the y values
|
||||
for (let i = 1; i < frame.fields.length; i++) {
|
||||
@ -237,20 +236,11 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
|
||||
grid: { show: customConfig.axisGridShow },
|
||||
});
|
||||
}
|
||||
|
||||
collectStackingGroups(field, stackingGroups, seriesIndex);
|
||||
}
|
||||
|
||||
if (stackingGroups.size !== 0) {
|
||||
for (const [_, seriesIds] of stackingGroups.entries()) {
|
||||
const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame });
|
||||
for (let j = seriesIdxs.length - 1; j > 0; j--) {
|
||||
builder.addBand({
|
||||
series: [seriesIdxs[j], seriesIdxs[j - 1]],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let stackingGroups = getStackingGroups(frame);
|
||||
|
||||
builder.setStackingGroups(stackingGroups);
|
||||
|
||||
return builder;
|
||||
};
|
||||
|
@ -31,7 +31,7 @@ import { getConfig, TimelineCoreOptions } from './timeline';
|
||||
import { VizLegendOptions, AxisPlacement, ScaleDirection, ScaleOrientation } from '@grafana/schema';
|
||||
import { TimelineFieldConfig, TimelineOptions } from './types';
|
||||
import { PlotTooltipInterpolator } from '@grafana/ui/src/components/uPlot/types';
|
||||
import { preparePlotData } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||
import { preparePlotData2, getStackingGroups } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
const defaultConfig: TimelineFieldConfig = {
|
||||
@ -153,7 +153,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
|
||||
|
||||
builder.setTooltipInterpolator(interpolateTooltip);
|
||||
|
||||
builder.setPrepData(preparePlotData);
|
||||
builder.setPrepData((frames) => preparePlotData2(frames[0], getStackingGroups(frames[0])));
|
||||
|
||||
builder.setCursor(coreConfig.cursor);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user