GraphNG: stacking (#30749)

* First iteration

* Dev dash

* Re-use StackingMode type

* Fix ts and api issues

* Stacking work resurected

* Fix overrides

* Correct values in tooltip and updated test dashboard

* Update dev dashboard

* Apply correct bands for stacking

* Merge fix

* Update snapshot

* Revert go.sum

* Handle null values correctyl and make filleBelowTo and stacking mutual exclusive

* Snapshots update

* Graph->Time series stacking migration

* Review comments

* Indicate overrides in StandardEditorContext

* Change stacking UI editor, migrate stacking to object option

* Small refactor, fix for hiding series and dev dashboard
This commit is contained in:
Dominik Prokop
2021-04-15 13:00:01 +02:00
committed by GitHub
parent 04a8d5407e
commit 0cc620aea7
31 changed files with 2183 additions and 92 deletions

View File

@@ -10,6 +10,7 @@ export interface StandardEditorContext<TOptions> {
eventBus?: EventBus;
getSuggestions?: (scope?: VariableSuggestionsScope) => VariableSuggestion[];
options?: TOptions;
isOverride?: boolean;
}
export interface StandardEditorProps<TValue = any, TSettings = any, TOptions = any> {

View File

@@ -6,7 +6,8 @@ import { LegendDisplayMode } from '../VizLegend/models.gen';
import { prepDataForStorybook } from '../../utils/storybook/data';
import { useTheme } from '../../themes';
import { select } from '@storybook/addon-knobs';
import { BarChartOptions, BarStackingMode, BarValueVisibility } from './types';
import { BarChartOptions, BarValueVisibility } from './types';
import { StackingMode } from '../uPlot/config';
export default {
title: 'Visualizations/BarChart',
@@ -55,7 +56,7 @@ export const Basic: React.FC = () => {
const options: BarChartOptions = {
orientation: orientation,
legend: { displayMode: LegendDisplayMode.List, placement: legendPlacement, calcs: [] },
stacking: BarStackingMode.None,
stacking: StackingMode.None,
showValue: BarValueVisibility.Always,
barWidth: 0.97,
groupWidth: 0.7,

View File

@@ -56,6 +56,7 @@ UPlotConfigBuilder {
[Function],
],
},
"isStacking": false,
"scales": Array [
UPlotScaleBuilder {
"props": Object {
@@ -176,6 +177,7 @@ UPlotConfigBuilder {
[Function],
],
},
"isStacking": false,
"scales": Array [
UPlotScaleBuilder {
"props": Object {
@@ -296,6 +298,7 @@ UPlotConfigBuilder {
[Function],
],
},
"isStacking": false,
"scales": Array [
UPlotScaleBuilder {
"props": Object {
@@ -416,6 +419,7 @@ UPlotConfigBuilder {
[Function],
],
},
"isStacking": false,
"scales": Array [
UPlotScaleBuilder {
"props": Object {
@@ -536,6 +540,7 @@ UPlotConfigBuilder {
[Function],
],
},
"isStacking": false,
"scales": Array [
UPlotScaleBuilder {
"props": Object {
@@ -656,6 +661,7 @@ UPlotConfigBuilder {
[Function],
],
},
"isStacking": false,
"scales": Array [
UPlotScaleBuilder {
"props": Object {
@@ -776,6 +782,7 @@ UPlotConfigBuilder {
[Function],
],
},
"isStacking": false,
"scales": Array [
UPlotScaleBuilder {
"props": Object {
@@ -896,6 +903,7 @@ UPlotConfigBuilder {
[Function],
],
},
"isStacking": false,
"scales": Array [
UPlotScaleBuilder {
"props": Object {

View File

@@ -1,16 +1,7 @@
import { VizOrientation } from '@grafana/data';
import { AxisConfig, GraphGradientMode, HideableFieldConfig } from '../uPlot/config';
import { AxisConfig, GraphGradientMode, HideableFieldConfig, StackingMode } from '../uPlot/config';
import { VizLegendOptions } from '../VizLegend/models.gen';
/**
* @alpha
*/
export enum BarStackingMode {
None = 'none',
Standard = 'standard',
Percent = 'percent',
}
/**
* @alpha
*/
@@ -26,7 +17,7 @@ export enum BarValueVisibility {
export interface BarChartOptions {
orientation: VizOrientation;
legend: VizLegendOptions;
stacking: BarStackingMode;
stacking: StackingMode;
showValue: BarValueVisibility;
barWidth: number;
groupWidth: number;

View File

@@ -1,7 +1,7 @@
import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
import { FieldConfig, FieldType, GrafanaTheme, MutableDataFrame, VizOrientation } from '@grafana/data';
import { BarChartFieldConfig, BarChartOptions, BarStackingMode, BarValueVisibility } from './types';
import { GraphGradientMode } from '../uPlot/config';
import { BarChartFieldConfig, BarChartOptions, BarValueVisibility } from './types';
import { GraphGradientMode, StackingMode } from '../uPlot/config';
import { LegendDisplayMode } from '../VizLegend/models.gen';
function mockDataFrame() {
@@ -73,7 +73,7 @@ describe('GraphNG utils', () => {
placement: 'bottom',
calcs: [],
},
stacking: BarStackingMode.None,
stacking: StackingMode.None,
};
it.each([VizOrientation.Auto, VizOrientation.Horizontal, VizOrientation.Vertical])('orientation', (v) => {
@@ -94,7 +94,7 @@ describe('GraphNG utils', () => {
).toMatchSnapshot();
});
it.each([BarStackingMode.None, BarStackingMode.Percent, BarStackingMode.Standard])('stacking', (v) => {
it.each([StackingMode.None, StackingMode.Percent, StackingMode.Normal])('stacking', (v) => {
expect(
preparePlotConfigBuilder(frame!, { colors: { panelBg: '#000000' } } as GrafanaTheme, {
...config,

View File

@@ -64,6 +64,7 @@ export const Lines: Story<StoryProps> = ({ placement, unit, legendDisplayMode, .
placement: placement,
calcs: [],
}}
timeZone="browser"
/>
);
};

View File

@@ -31,10 +31,24 @@ UPlotConfigBuilder {
},
},
},
"bands": Array [],
"bands": Array [
Object {
"series": Array [
2,
1,
],
},
Object {
"series": Array [
4,
3,
],
},
],
"hasBottomAxis": true,
"hasLeftAxis": true,
"hooks": Object {},
"isStacking": true,
"scales": Array [
UPlotScaleBuilder {
"props": Object {
@@ -146,6 +160,135 @@ UPlotConfigBuilder {
"thresholds": undefined,
},
},
UPlotSeriesBuilder {
"props": Object {
"barAlignment": undefined,
"colorMode": Object {
"description": "Derive colors from thresholds",
"getCalculator": [Function],
"id": "thresholds",
"isByValue": true,
"name": "From thresholds",
},
"dataFrameFieldIndex": Object {
"fieldIndex": 2,
"frameIndex": 1,
},
"drawStyle": "line",
"fieldName": "Metric 3",
"fillOpacity": 0.1,
"gradientMode": "opacity",
"hideInLegend": undefined,
"lineColor": "#ff0000",
"lineInterpolation": "linear",
"lineStyle": Object {
"dash": Array [
1,
2,
],
"fill": "dash",
},
"lineWidth": 2,
"pointColor": "#808080",
"pointSize": undefined,
"scaleKey": "__fixed",
"show": true,
"showPoints": "always",
"spanNulls": false,
"theme": Object {
"colors": Object {
"panelBg": "#000000",
},
},
"thresholds": undefined,
},
},
UPlotSeriesBuilder {
"props": Object {
"barAlignment": -1,
"colorMode": Object {
"description": "Derive colors from thresholds",
"getCalculator": [Function],
"id": "thresholds",
"isByValue": true,
"name": "From thresholds",
},
"dataFrameFieldIndex": Object {
"fieldIndex": 3,
"frameIndex": 1,
},
"drawStyle": "bars",
"fieldName": "Metric 4",
"fillOpacity": 0.1,
"gradientMode": "hue",
"hideInLegend": undefined,
"lineColor": "#ff0000",
"lineInterpolation": "linear",
"lineStyle": Object {
"dash": Array [
1,
2,
],
"fill": "dash",
},
"lineWidth": 2,
"pointColor": "#808080",
"pointSize": undefined,
"scaleKey": "__fixed",
"show": true,
"showPoints": "always",
"spanNulls": false,
"theme": Object {
"colors": Object {
"panelBg": "#000000",
},
},
"thresholds": undefined,
},
},
UPlotSeriesBuilder {
"props": Object {
"barAlignment": -1,
"colorMode": Object {
"description": "Derive colors from thresholds",
"getCalculator": [Function],
"id": "thresholds",
"isByValue": true,
"name": "From thresholds",
},
"dataFrameFieldIndex": Object {
"fieldIndex": 4,
"frameIndex": 1,
},
"drawStyle": "bars",
"fieldName": "Metric 4",
"fillOpacity": 0.1,
"gradientMode": "hue",
"hideInLegend": undefined,
"lineColor": "#ff0000",
"lineInterpolation": "linear",
"lineStyle": Object {
"dash": Array [
1,
2,
],
"fill": "dash",
},
"lineWidth": 2,
"pointColor": "#808080",
"pointSize": undefined,
"scaleKey": "__fixed",
"show": true,
"showPoints": "always",
"spanNulls": false,
"theme": Object {
"colors": Object {
"panelBg": "#000000",
},
},
"thresholds": undefined,
},
},
],
"tz": "UTC",
"tzDate": [Function],

View File

@@ -37,5 +37,6 @@ export const useGraphNGContext = () => {
dimFields,
mapSeriesIndexToDataFrameFieldIndex,
getXAxisField,
alignedData: data,
};
};

View File

@@ -9,7 +9,15 @@ import {
GrafanaTheme,
MutableDataFrame,
} from '@grafana/data';
import { BarAlignment, DrawStyle, GraphFieldConfig, GraphGradientMode, LineInterpolation, PointVisibility } from '..';
import {
BarAlignment,
DrawStyle,
GraphFieldConfig,
GraphGradientMode,
LineInterpolation,
PointVisibility,
StackingMode,
} from '..';
function mockDataFrame() {
const df1 = new MutableDataFrame({
@@ -38,6 +46,10 @@ function mockDataFrame() {
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: PointVisibility.Always,
stacking: {
group: 'A',
mode: StackingMode.Normal,
},
},
};
@@ -58,6 +70,80 @@ function mockDataFrame() {
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: PointVisibility.Always,
stacking: {
group: 'A',
mode: StackingMode.Normal,
},
},
};
const f3Config: FieldConfig<GraphFieldConfig> = {
displayName: 'Metric 3',
decimals: 2,
custom: {
drawStyle: DrawStyle.Line,
gradientMode: GraphGradientMode.Opacity,
lineColor: '#ff0000',
lineWidth: 2,
lineInterpolation: LineInterpolation.Linear,
lineStyle: {
fill: 'dash',
dash: [1, 2],
},
spanNulls: false,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: PointVisibility.Always,
stacking: {
group: 'B',
mode: StackingMode.Normal,
},
},
};
const f4Config: FieldConfig<GraphFieldConfig> = {
displayName: 'Metric 4',
decimals: 2,
custom: {
drawStyle: DrawStyle.Bars,
gradientMode: GraphGradientMode.Hue,
lineColor: '#ff0000',
lineWidth: 2,
lineInterpolation: LineInterpolation.Linear,
lineStyle: {
fill: 'dash',
dash: [1, 2],
},
barAlignment: BarAlignment.Before,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: PointVisibility.Always,
stacking: {
group: 'B',
mode: StackingMode.Normal,
},
},
};
const f5Config: FieldConfig<GraphFieldConfig> = {
displayName: 'Metric 4',
decimals: 2,
custom: {
drawStyle: DrawStyle.Bars,
gradientMode: GraphGradientMode.Hue,
lineColor: '#ff0000',
lineWidth: 2,
lineInterpolation: LineInterpolation.Linear,
lineStyle: {
fill: 'dash',
dash: [1, 2],
},
barAlignment: BarAlignment.Before,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: PointVisibility.Always,
stacking: {
group: 'B',
mode: StackingMode.None,
},
},
};
@@ -72,6 +158,21 @@ function mockDataFrame() {
type: FieldType.number,
config: f2Config,
});
df2.addField({
name: 'metric3',
type: FieldType.number,
config: f3Config,
});
df2.addField({
name: 'metric4',
type: FieldType.number,
config: f4Config,
});
df2.addField({
name: 'metric5',
type: FieldType.number,
config: f5Config,
});
return preparePlotFrame([df1, df2], {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),

View File

@@ -26,6 +26,7 @@ import {
ScaleDirection,
ScaleOrientation,
} from '../uPlot/config';
import { collectStackingGroups } from '../uPlot/utils';
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
@@ -130,6 +131,8 @@ export function preparePlotConfigBuilder(
});
}
const stackingGroups: Map<string, number[]> = new Map();
let indexByName: Map<string, number> | undefined = undefined;
for (let i = 0; i < frame.fields.length; i++) {
@@ -178,6 +181,7 @@ export function preparePlotConfigBuilder(
const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
let { fillOpacity } = customConfig;
if (customConfig.fillBelowTo) {
if (!indexByName) {
indexByName = getNamesToFieldIndex(frame);
@@ -219,8 +223,20 @@ export function preparePlotConfigBuilder(
fieldName: getFieldDisplayName(field, frame),
hideInLegend: customConfig.hideFrom?.legend,
});
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;
}

View File

@@ -176,7 +176,6 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
render() {
const { data, configBuilder } = this.state;
const { width, height, sparkline } = this.props;
return (
<UPlotChart data={data} config={configBuilder} width={width} height={height} timeRange={sparkline.timeRange!} />
);

View File

@@ -228,7 +228,7 @@ export { GraphNG, FIXED_UNIT } from './GraphNG/GraphNG';
export { useGraphNGContext } from './GraphNG/hooks';
export { BarChart } from './BarChart/BarChart';
export { TimelineChart } from './Timeline/TimelineChart';
export { BarChartOptions, BarStackingMode, BarValueVisibility, BarChartFieldConfig } from './BarChart/types';
export { BarChartOptions, BarValueVisibility, BarChartFieldConfig } from './BarChart/types';
export { TimelineOptions, TimelineFieldConfig } from './Timeline/types';
export { GraphNGLegendEvent, GraphNGLegendEventMode } from './GraphNG/types';
export * from './NodeGraph';

View File

@@ -133,7 +133,6 @@ describe('UPlotChart', () => {
describe('config update', () => {
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} />
);

View File

@@ -175,6 +175,23 @@ export interface HideableFieldConfig {
hideFrom?: HideSeriesConfig;
}
/**
* @alpha
*/
export enum StackingMode {
None = 'none',
Normal = 'normal',
Percent = 'percent',
}
/**
* @alpha
*/
export interface StackingConfig {
mode?: StackingMode;
group?: string;
}
/**
* @alpha
*/
@@ -187,6 +204,7 @@ export interface GraphFieldConfig
HideableFieldConfig {
drawStyle?: DrawStyle;
gradientMode?: GraphGradientMode;
stacking?: StackingConfig;
}
/**
@@ -231,4 +249,9 @@ export const graphFieldOptions = {
{ label: 'Hue', value: GraphGradientMode.Hue },
// { label: 'Color scheme', value: GraphGradientMode.Scheme },
] as Array<SelectableValue<GraphGradientMode>>,
stacking: [
{ label: 'Off', value: StackingMode.None },
{ label: 'Normal', value: StackingMode.Normal },
] as Array<SelectableValue<StackingMode>>,
};

View File

@@ -350,7 +350,7 @@ describe('UPlotConfigBuilder', () => {
`);
});
it('Handles auto axis placement', () => {
it('handles auto axis placement', () => {
const builder = new UPlotConfigBuilder();
builder.addAxis({
@@ -370,7 +370,7 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getConfig().axes![1].grid!.show).toBe(false);
});
it('When fillColor is not set fill', () => {
it('when fillColor is not set fill', () => {
const builder = new UPlotConfigBuilder();
builder.addSeries({
drawStyle: DrawStyle.Line,
@@ -383,7 +383,7 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getConfig().series[1].fill).toBe(undefined);
});
it('When fillOpacity is set', () => {
it('when fillOpacity is set', () => {
const builder = new UPlotConfigBuilder();
builder.addSeries({
drawStyle: DrawStyle.Line,
@@ -397,7 +397,7 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getConfig().series[1].fill).toBe('rgba(255, 170, 187, 0.5)');
});
it('When fillColor is set ignore fillOpacity', () => {
it('when fillColor is set ignore fillOpacity', () => {
const builder = new UPlotConfigBuilder();
builder.addSeries({
drawStyle: DrawStyle.Line,
@@ -412,7 +412,7 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getConfig().series[1].fill).toBe('#FF0000');
});
it('When fillGradient mode is opacity', () => {
it('when fillGradient mode is opacity', () => {
const builder = new UPlotConfigBuilder();
builder.addSeries({
drawStyle: DrawStyle.Line,
@@ -486,4 +486,147 @@ describe('UPlotConfigBuilder', () => {
}
`);
});
describe('Stacking', () => {
it('allows stacking config', () => {
const builder = new UPlotConfigBuilder();
builder.setStacking();
builder.addSeries({
drawStyle: DrawStyle.Line,
scaleKey: 'scale-x',
fieldName: 'A-series',
fillOpacity: 50,
gradientMode: GraphGradientMode.Opacity,
showPoints: PointVisibility.Auto,
lineColor: '#0000ff',
lineWidth: 1,
spanNulls: false,
theme: darkTheme,
});
builder.addSeries({
drawStyle: DrawStyle.Line,
scaleKey: 'scale-x',
fieldName: 'B-series',
fillOpacity: 50,
gradientMode: GraphGradientMode.Opacity,
showPoints: PointVisibility.Auto,
pointSize: 5,
lineColor: '#00ff00',
lineWidth: 1,
spanNulls: false,
theme: darkTheme,
});
builder.addSeries({
drawStyle: DrawStyle.Line,
scaleKey: 'scale-x',
fieldName: 'C-series',
fillOpacity: 50,
gradientMode: GraphGradientMode.Opacity,
showPoints: PointVisibility.Auto,
pointSize: 5,
lineColor: '#ff0000',
lineWidth: 1,
spanNulls: false,
theme: darkTheme,
});
builder.addBand({
series: [3, 2],
fill: 'red',
});
builder.addBand({
series: [2, 1],
fill: 'blue',
});
expect(builder.getConfig()).toMatchInlineSnapshot(`
Object {
"axes": Array [],
"bands": Array [
Object {
"fill": "red",
"series": Array [
3,
2,
],
},
Object {
"fill": "blue",
"series": Array [
2,
1,
],
},
],
"cursor": Object {
"drag": Object {
"setScale": false,
},
"focus": Object {
"prox": 30,
},
"points": Object {
"fill": [Function],
"size": [Function],
"stroke": [Function],
"width": [Function],
},
},
"hooks": Object {},
"scales": Object {},
"select": undefined,
"series": Array [
Object {},
Object {
"fill": [Function],
"paths": [Function],
"points": Object {
"fill": undefined,
"size": undefined,
"stroke": undefined,
},
"pxAlign": undefined,
"scale": "scale-x",
"show": true,
"spanGaps": false,
"stroke": "#0000ff",
"width": 1,
},
Object {
"fill": [Function],
"paths": [Function],
"points": Object {
"fill": undefined,
"size": 5,
"stroke": undefined,
},
"pxAlign": undefined,
"scale": "scale-x",
"show": true,
"spanGaps": false,
"stroke": "#00ff00",
"width": 1,
},
Object {
"fill": [Function],
"paths": [Function],
"points": Object {
"fill": undefined,
"size": 5,
"stroke": undefined,
},
"pxAlign": undefined,
"scale": "scale-x",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
"width": 1,
},
],
"tzDate": [Function],
}
`);
});
});
});

View File

@@ -15,6 +15,7 @@ export class UPlotConfigBuilder {
private scales: UPlotScaleBuilder[] = [];
private bands: Band[] = [];
private cursor: Cursor | undefined;
private isStacking = false;
// uPlot types don't export the Select interface prior to 1.6.4
private select: Partial<BBox> | undefined;
private hasLeftAxis = false;
@@ -78,6 +79,9 @@ export class UPlotConfigBuilder {
this.select = select;
}
setStacking(enabled = true) {
this.isStacking = enabled;
}
addSeries(props: SeriesProps) {
this.series.push(new UPlotSeriesBuilder(props));
}
@@ -118,16 +122,22 @@ export class UPlotConfigBuilder {
config.tzDate = this.tzDate;
// When bands exist, only keep fill when defined
if (this.bands?.length) {
if (this.isStacking) {
// Let uPlot handle bands and fills
config.bands = this.bands;
const keepFill = new Set<number>();
for (const b of config.bands) {
keepFill.add(b.series[0]);
}
for (let i = 1; i < config.series.length; i++) {
if (!keepFill.has(i)) {
config.series[i].fill = undefined;
} else {
// When fillBelowTo option enabled, handle series bands fill manually
if (this.bands?.length) {
config.bands = this.bands;
const keepFill = new Set<number>();
for (const b of config.bands) {
keepFill.add(b.series[0]);
}
for (let i = 1; i < config.series.length; i++) {
if (!keepFill.has(i)) {
config.series[i].fill = undefined;
}
}
}
}

View File

@@ -58,10 +58,11 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({ mode = 'single', t
// when interacting with a point in single mode
if (mode === 'single' && originFieldIndex !== null) {
const field = otherProps.data[originFieldIndex.frameIndex].fields[originFieldIndex.fieldIndex];
const field = graphContext.alignedData.fields[focusedSeriesIdx!];
const plotSeries = plotContext.getSeries();
const fieldFmt = field.display || getDisplayProcessor({ field, timeZone });
const value = fieldFmt(plotContext.data[focusedSeriesIdx!][focusedPointIdx]);
const value = fieldFmt(field.values.get(focusedPointIdx));
tooltip = (
<SeriesTable
@@ -95,7 +96,9 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({ mode = 'single', t
continue;
}
const value = field.display!(plotContext.data[i][focusedPointIdx]);
// using aligned data value field here as it's indexes are in line with Plot data
const valueField = graphContext.alignedData.fields[i];
const value = valueField.display!(valueField.values.get(focusedPointIdx));
series.push({
// TODO: align with uPlot typings

View File

@@ -1,4 +1,6 @@
import { timeFormatToTemplate } from './utils';
import { preparePlotData, timeFormatToTemplate } from './utils';
import { FieldType, MutableDataFrame } from '@grafana/data';
import { StackingMode } from './config';
describe('timeFormatToTemplate', () => {
it.each`
@@ -13,3 +15,285 @@ describe('timeFormatToTemplate', () => {
expect(timeFormatToTemplate(format)).toEqual(expected);
});
});
describe('preparePlotData', () => {
const df = new MutableDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
{ name: 'a', values: [-10, 20, 10] },
{ name: 'b', values: [10, 10, 10] },
{ name: 'c', values: [20, 20, 20] },
],
});
it('creates array from DataFrame', () => {
expect(preparePlotData(df)).toMatchInlineSnapshot(`
Array [
Array [
9997,
9998,
9999,
],
Array [
-10,
20,
10,
],
Array [
10,
10,
10,
],
Array [
20,
20,
20,
],
]
`);
});
describe('stacking', () => {
it('none', () => {
const df = new MutableDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
{
name: 'a',
values: [-10, 20, 10],
config: { custom: { stacking: { mode: StackingMode.None } } },
},
{
name: 'b',
values: [10, 10, 10],
config: { custom: { stacking: { mode: StackingMode.None } } },
},
{
name: 'c',
values: [20, 20, 20],
config: { custom: { stacking: { mode: StackingMode.None } } },
},
],
});
expect(preparePlotData(df)).toMatchInlineSnapshot(`
Array [
Array [
9997,
9998,
9999,
],
Array [
-10,
20,
10,
],
Array [
10,
10,
10,
],
Array [
20,
20,
20,
],
]
`);
});
it('standard', () => {
const df = new MutableDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
{
name: 'a',
values: [-10, 20, 10],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
},
{
name: 'b',
values: [10, 10, 10],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
},
{
name: 'c',
values: [20, 20, 20],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
},
],
});
expect(preparePlotData(df)).toMatchInlineSnapshot(`
Array [
Array [
9997,
9998,
9999,
],
Array [
-10,
20,
10,
],
Array [
0,
30,
20,
],
Array [
20,
50,
40,
],
]
`);
});
it('standard with multiple groups', () => {
const df = new MutableDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
{
name: 'a',
values: [-10, 20, 10],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
},
{
name: 'b',
values: [10, 10, 10],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
},
{
name: 'c',
values: [20, 20, 20],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
},
{
name: 'd',
values: [1, 2, 3],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
},
{
name: 'e',
values: [1, 2, 3],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
},
{
name: 'f',
values: [1, 2, 3],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
},
],
});
expect(preparePlotData(df)).toMatchInlineSnapshot(`
Array [
Array [
9997,
9998,
9999,
],
Array [
-10,
20,
10,
],
Array [
0,
30,
20,
],
Array [
20,
50,
40,
],
Array [
1,
2,
3,
],
Array [
2,
4,
6,
],
Array [
3,
6,
9,
],
]
`);
});
it('standard with multiple groups and hidden fields', () => {
const df = new MutableDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
{
name: 'a',
values: [-10, 20, 10],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' }, hideFrom: { graph: true } } },
},
{
// Will ignore a series as stacking base as it's hidden from graph
name: 'b',
values: [10, 10, 10],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
},
{
name: 'd',
values: [1, 2, 3],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
},
{
name: 'e',
values: [1, 2, 3],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' }, hideFrom: { graph: true } } },
},
{
// Will ignore e series as stacking base as it's hidden from graph
name: 'f',
values: [1, 2, 3],
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
},
],
});
expect(preparePlotData(df)).toMatchInlineSnapshot(`
Array [
Array [
9997,
9998,
9999,
],
Array [
-10,
20,
10,
],
Array [
10,
10,
10,
],
Array [
1,
2,
3,
],
Array [
1,
2,
3,
],
Array [
2,
4,
6,
],
]
`);
});
});
});

View File

@@ -1,6 +1,7 @@
import { DataFrame, dateTime, FieldType } from '@grafana/data';
import { DataFrame, dateTime, Field, FieldType } from '@grafana/data';
import { AlignedData, Options } from 'uplot';
import { PlotPlugin, PlotProps } from './types';
import { StackingMode } from './config';
import { createLogger } from '../../utils/logger';
import { attachDebugger } from '../../utils';
@@ -33,8 +34,11 @@ export function buildPlotConfig(props: PlotProps, plugins: Record<string, PlotPl
}
/** @internal */
export function preparePlotData(frame: DataFrame, ignoreFieldTypes?: FieldType[]): AlignedData {
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];
@@ -46,20 +50,59 @@ export function preparePlotData(frame: DataFrame, ignoreFieldTypes?: FieldType[]
timestamps.push(dateTime(f.values.get(i)).valueOf());
}
result.push(timestamps);
seriesIndex++;
continue;
}
result.push(f.values.toArray());
seriesIndex++;
continue;
}
if (ignoreFieldTypes && ignoreFieldTypes.indexOf(f.type) > -1) {
continue;
}
collectStackingGroups(f, stackingGroups, seriesIndex);
result.push(f.values.toArray());
seriesIndex++;
}
// Stacking
if (stackingGroups.size !== 0) {
// array or stacking groups
for (const [_, seriesIdxs] of stackingGroups.entries()) {
const acc = Array(result[0].length).fill(0);
for (let j = 0; j < seriesIdxs.length; j++) {
const currentlyStacking = result[seriesIdxs[j]];
for (let k = 0; k < result[0].length; k++) {
const v = currentlyStacking[k];
acc[k] += v === null || v === undefined ? 0 : +v;
}
result[seriesIdxs[j]] = acc.slice();
}
}
return result as AlignedData;
}
return result as AlignedData;
}
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?.graph
) {
if (!groups.has(customConfig.stacking.group)) {
groups.set(customConfig.stacking.group, [seriesIdx]);
} else {
groups.set(customConfig.stacking.group, groups.get(customConfig.stacking.group)!.concat(seriesIdx));
}
}
}
// Dev helpers
/** @internal */