mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -64,6 +64,7 @@ export const Lines: Story<StoryProps> = ({ placement, unit, legendDisplayMode, .
|
||||
placement: placement,
|
||||
calcs: [],
|
||||
}}
|
||||
timeZone="browser"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -37,5 +37,6 @@ export const useGraphNGContext = () => {
|
||||
dimFields,
|
||||
mapSeriesIndexToDataFrameFieldIndex,
|
||||
getXAxisField,
|
||||
alignedData: data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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({}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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!} />
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
|
||||
@@ -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>>,
|
||||
};
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user