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

@@ -6,6 +6,7 @@ import {
isSystemOverride as isSystemOverrideGuard,
VariableSuggestionsScope,
DynamicConfigValue,
ConfigOverrideRule,
} from '@grafana/data';
import { Container, fieldMatchersUI, ValuePicker } from '@grafana/ui';
import { OptionPaneRenderProps } from './types';
@@ -51,6 +52,7 @@ export function getFieldOverrideCategories(props: OptionPaneRenderProps): Option
const context = {
data,
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
isOverride: true,
};
/**
@@ -88,7 +90,7 @@ export function getFieldOverrideCategories(props: OptionPaneRenderProps): Option
onOverrideChange(idx, override);
};
const onDynamicConfigValueAdd = (value: SelectableValue<string>) => {
const onDynamicConfigValueAdd = (o: ConfigOverrideRule, value: SelectableValue<string>) => {
const registryItem = registry.get(value.value!);
const propertyConfig: DynamicConfigValue = {
id: registryItem.id,
@@ -96,12 +98,12 @@ export function getFieldOverrideCategories(props: OptionPaneRenderProps): Option
};
if (override.properties) {
override.properties.push(propertyConfig);
o.properties.push(propertyConfig);
} else {
override.properties = [propertyConfig];
o.properties = [propertyConfig];
}
onOverrideChange(idx, override);
onOverrideChange(idx, o);
};
/**
@@ -185,7 +187,7 @@ export function getFieldOverrideCategories(props: OptionPaneRenderProps): Option
icon="plus"
menuPlacement="auto"
options={configPropertiesOptions}
onChange={onDynamicConfigValueAdd}
onChange={(v) => onDynamicConfigValueAdd(override, v)}
/>
);
},
@@ -241,8 +243,8 @@ function getOverrideProperties(registry: FieldConfigOptionsRegistry) {
.filter((o) => !o.hideFromOverrides)
.map((item) => {
let label = item.name;
if (item.category && item.category.length > 1) {
label = [...item.category!.slice(1), item.name].join(' > ');
if (item.category) {
label = [...item.category, item.name].join(' > ');
}
return {
label,

View File

@@ -62,7 +62,7 @@ export const TestStuffPage: FC = () => {
timeRange={data.timeRange}
timeZone="browser"
/>
<hr></hr>
<hr />
<Table data={data.series[0]} width={1200} height={300} />
</div>
)}

View File

@@ -7,13 +7,7 @@ import {
VizOrientation,
} from '@grafana/data';
import { BarChartPanel } from './BarChartPanel';
import {
BarChartFieldConfig,
BarChartOptions,
BarStackingMode,
BarValueVisibility,
graphFieldOptions,
} from '@grafana/ui';
import { BarChartFieldConfig, BarChartOptions, StackingMode, BarValueVisibility, graphFieldOptions } from '@grafana/ui';
import { addAxisConfig, addHideFrom, addLegendOptions } from '../timeseries/config';
import { defaultBarChartFieldConfig } from '@grafana/ui/src/components/BarChart/types';
@@ -80,19 +74,6 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
},
defaultValue: VizOrientation.Auto,
})
.addRadio({
path: 'stacking',
name: 'Stacking',
settings: {
options: [
{ value: BarStackingMode.None, label: 'None' },
{ value: BarStackingMode.Standard, label: 'Standard' },
{ value: BarStackingMode.Percent, label: 'Percent' },
],
},
defaultValue: BarStackingMode.None,
showIf: () => false, // <<< Hide from the UI for now
})
.addRadio({
path: 'showValue',
name: 'Show values',
@@ -115,7 +96,7 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
step: 0.01,
},
showIf: (c, data) => {
if (c.stacking && c.stacking !== BarStackingMode.None) {
if (c.stacking && c.stacking !== StackingMode.None) {
return false;
}
return countNumberFields(data) !== 1;

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { FieldOverrideEditorProps } from '@grafana/data';
import {
HorizontalGroup,
IconButton,
Input,
RadioButtonGroup,
StackingConfig,
StackingMode,
Tooltip,
} from '@grafana/ui';
export const StackingEditor: React.FC<FieldOverrideEditorProps<StackingConfig, any>> = ({
value,
context,
onChange,
item,
}) => {
return (
<HorizontalGroup>
<RadioButtonGroup
value={value?.mode || StackingMode.None}
options={item.settings.options}
onChange={(v) => {
onChange({
...value,
mode: v,
});
}}
/>
{context.isOverride && value?.mode && value?.mode !== StackingMode.None && (
<Input
type="text"
placeholder="Group"
suffix={
<Tooltip content="Name of the stacking group" placement="top">
<IconButton name="question-circle" />
</Tooltip>
}
defaultValue={value?.group}
onChange={(v) => {
onChange({
...value,
group: v.currentTarget.value.trim(),
});
}}
/>
)}
</HorizontalGroup>
);
};

View File

@@ -35,7 +35,6 @@ Object {
],
},
"options": Object {
"graph": Object {},
"legend": Object {
"calcs": Array [
"mean",
@@ -68,7 +67,6 @@ Object {
"overrides": Array [],
},
"options": Object {
"graph": Object {},
"legend": Object {
"calcs": Array [],
"displayMode": "list",
@@ -81,6 +79,150 @@ Object {
}
`;
exports[`Graph Migrations stacking groups 1`] = `
Object {
"fieldConfig": Object {
"defaults": Object {
"custom": Object {
"axisPlacement": "auto",
"drawStyle": "line",
"fillOpacity": 50,
"lineInterpolation": "stepAfter",
"lineWidth": 5,
"showPoints": "never",
"spanNulls": true,
"stacking": Object {
"group": "A",
"mode": "normal",
},
},
"nullValueMode": "null",
"unit": "short",
},
"overrides": Array [
Object {
"matcher": Object {
"id": "byName",
"options": "A-series",
},
"properties": Array [
Object {
"id": "color",
"value": Object {
"fixedColor": "red",
"mode": "fixed",
},
},
],
},
Object {
"matcher": Object {
"id": "byName",
"options": "A-series",
},
"properties": Array [
Object {
"id": "custom.stacking",
"value": Object {
"group": "A",
"mode": "normal",
},
},
],
},
Object {
"matcher": Object {
"id": "byName",
"options": "B-series",
},
"properties": Array [
Object {
"id": "custom.stacking",
"value": Object {
"group": "A",
"mode": "normal",
},
},
],
},
],
},
"options": Object {
"legend": Object {
"calcs": Array [
"mean",
"lastNotNull",
"max",
"min",
"sum",
],
"displayMode": "table",
"placement": "bottom",
},
"tooltipOptions": Object {
"mode": "single",
},
},
}
`;
exports[`Graph Migrations stacking simple 1`] = `
Object {
"fieldConfig": Object {
"defaults": Object {
"custom": Object {
"axisPlacement": "auto",
"drawStyle": "line",
"fillOpacity": 50,
"lineInterpolation": "stepAfter",
"lineWidth": 5,
"showPoints": "never",
"spanNulls": true,
"stacking": Object {
"group": "A",
"mode": "normal",
},
},
"nullValueMode": "null",
"unit": "short",
},
"overrides": Array [
Object {
"matcher": Object {
"id": "byName",
"options": "A-series",
},
"properties": Array [
Object {
"id": "color",
"value": Object {
"fixedColor": "red",
"mode": "fixed",
},
},
],
},
],
},
"options": Object {
"legend": Object {
"calcs": Array [
"mean",
"lastNotNull",
"max",
"min",
"sum",
],
"displayMode": "table",
"placement": "bottom",
},
"tooltipOptions": Object {
"mode": "single",
},
},
}
`;
exports[`Graph Migrations stairscase 1`] = `
Object {
"fieldConfig": Object {
@@ -102,7 +244,6 @@ Object {
"overrides": Array [],
},
"options": Object {
"graph": Object {},
"legend": Object {
"calcs": Array [
"mean",
@@ -156,7 +297,6 @@ Object {
],
},
"options": Object {
"graph": Object {},
"legend": Object {
"calcs": Array [],
"displayMode": "list",
@@ -237,7 +377,6 @@ Object {
],
},
"options": Object {
"graph": Object {},
"legend": Object {
"calcs": Array [],
"displayMode": "list",

View File

@@ -11,20 +11,22 @@ import {
stringOverrideProcessor,
} from '@grafana/data';
import {
AxisConfig,
AxisPlacement,
BarAlignment,
DrawStyle,
GraphFieldConfig,
graphFieldOptions,
GraphGradientMode,
HideableFieldConfig,
LegendDisplayMode,
LineInterpolation,
LineStyle,
PointVisibility,
ScaleDistribution,
ScaleDistributionConfig,
GraphGradientMode,
LegendDisplayMode,
AxisConfig,
HideableFieldConfig,
StackingConfig,
StackingMode,
} from '@grafana/ui';
import { SeriesConfigEditor } from './HideSeriesConfigEditor';
import { ScaleDistributionEditor } from './ScaleDistributionEditor';
@@ -32,6 +34,7 @@ import { LineStyleEditor } from './LineStyleEditor';
import { FillBellowToEditor } from './FillBelowToEditor';
import { OptionsWithLegend } from './types';
import { SpanNullsEditor } from './SpanNullsEditor';
import { StackingEditor } from './StackingEditor';
export const defaultGraphConfig: GraphFieldConfig = {
drawStyle: DrawStyle.Line,
@@ -40,11 +43,15 @@ export const defaultGraphConfig: GraphFieldConfig = {
fillOpacity: 0,
gradientMode: GraphGradientMode.None,
barAlignment: BarAlignment.Center,
stacking: {
mode: StackingMode.None,
group: 'A',
},
};
export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOptionsArgs<GraphFieldConfig> {
const categoryStyles = ['Graph styles'];
const categoryStyles = ['Graph styles'];
export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOptionsArgs<GraphFieldConfig> {
return {
standardOptions: {
[FieldConfigProperty.Color]: {
@@ -180,6 +187,7 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
showIf: (c) => c.showPoints !== PointVisibility.Never || c.drawStyle === DrawStyle.Points,
});
addStackingConfig(builder, cfg.stacking);
addAxisConfig(builder, cfg);
addHideFrom(builder);
},
@@ -319,3 +327,23 @@ export function addLegendOptions<T extends OptionsWithLegend>(builder: PanelOpti
showIf: (currentConfig) => currentConfig.legend.displayMode !== LegendDisplayMode.Hidden,
});
}
export function addStackingConfig(
builder: FieldConfigEditorBuilder<{ stacking: StackingConfig }>,
defaultConfig?: StackingConfig
) {
builder.addCustomEditor({
id: 'stacking',
path: 'stacking',
name: 'Stack series',
category: categoryStyles,
defaultValue: defaultConfig,
editor: StackingEditor,
override: StackingEditor,
settings: {
options: graphFieldOptions.stacking,
},
process: identityOverrideProcessor,
shouldApply: (f) => f.type === FieldType.number,
});
}

View File

@@ -48,6 +48,25 @@ describe('Graph Migrations', () => {
panel.options = graphPanelChangedHandler(panel, 'graph', old);
expect(panel).toMatchSnapshot();
});
describe('stacking', () => {
test('simple', () => {
const old: any = {
angular: stacking,
};
const panel = {} as PanelModel;
panel.options = graphPanelChangedHandler(panel, 'graph', old);
expect(panel).toMatchSnapshot();
});
test('groups', () => {
const old: any = {
angular: stackingGroups,
};
const panel = {} as PanelModel;
panel.options = graphPanelChangedHandler(panel, 'graph', old);
expect(panel).toMatchSnapshot();
});
});
});
const stairscase = {
@@ -409,3 +428,200 @@ const legend = {
timeShift: null,
datasource: null,
};
const stacking = {
aliasColors: {
'A-series': 'red',
},
dashLength: 10,
fieldConfig: {
defaults: {
custom: {},
},
overrides: [],
},
fill: 5,
gridPos: {
h: 9,
w: 12,
x: 0,
y: 0,
},
id: 2,
legend: {
avg: true,
current: true,
max: false,
min: false,
show: true,
total: true,
values: true,
alignAsTable: true,
},
lines: true,
linewidth: 5,
maxDataPoints: 20,
nullPointMode: 'null',
options: {
alertThreshold: true,
},
pluginVersion: '7.4.0-pre',
pointradius: 2,
renderer: 'flot',
seriesOverrides: [],
spaceLength: 10,
steppedLine: true,
thresholds: [],
timeRegions: [],
title: 'Panel Title',
tooltip: {
shared: true,
sort: 0,
value_type: 'individual',
},
type: 'graph',
xaxis: {
buckets: null,
mode: 'time',
name: null,
show: true,
values: [],
},
yaxes: [
{
$$hashKey: 'object:38',
format: 'short',
label: null,
logBase: 1,
max: null,
min: null,
show: true,
},
{
$$hashKey: 'object:39',
format: 'short',
label: null,
logBase: 1,
max: null,
min: null,
show: true,
},
],
yaxis: {
align: false,
alignLevel: null,
},
bars: false,
dashes: false,
fillGradient: 0,
hiddenSeries: false,
percentage: false,
points: false,
stack: true,
timeFrom: null,
timeShift: null,
datasource: null,
};
const stackingGroups = {
aliasColors: {
'A-series': 'red',
},
dashLength: 10,
fieldConfig: {
defaults: {
custom: {},
},
overrides: [],
},
fill: 5,
gridPos: {
h: 9,
w: 12,
x: 0,
y: 0,
},
id: 2,
legend: {
avg: true,
current: true,
max: false,
min: false,
show: true,
total: true,
values: true,
alignAsTable: true,
},
lines: true,
linewidth: 5,
maxDataPoints: 20,
nullPointMode: 'null',
options: {
alertThreshold: true,
},
pluginVersion: '7.4.0-pre',
pointradius: 2,
renderer: 'flot',
seriesOverrides: [
{
alias: 'A-series',
stack: 'A',
},
{
alias: 'B-series',
stack: 'A',
},
],
spaceLength: 10,
steppedLine: true,
thresholds: [],
timeRegions: [],
title: 'Panel Title',
tooltip: {
shared: true,
sort: 0,
value_type: 'individual',
},
type: 'graph',
xaxis: {
buckets: null,
mode: 'time',
name: null,
show: true,
values: [],
},
yaxes: [
{
$$hashKey: 'object:38',
format: 'short',
label: null,
logBase: 1,
max: null,
min: null,
show: true,
},
{
$$hashKey: 'object:39',
format: 'short',
label: null,
logBase: 1,
max: null,
min: null,
show: true,
},
],
yaxis: {
align: false,
alignLevel: null,
},
bars: false,
dashes: false,
fillGradient: 0,
hiddenSeries: false,
percentage: false,
points: false,
stack: true,
timeFrom: null,
timeShift: null,
datasource: null,
};

View File

@@ -10,19 +10,22 @@ import {
NullValueMode,
PanelModel,
} from '@grafana/data';
import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui';
import {
GraphGradientMode,
AxisPlacement,
DrawStyle,
GraphFieldConfig,
GraphGradientMode,
LegendDisplayMode,
LineInterpolation,
LineStyle,
PointVisibility,
} from '@grafana/ui/src/components/uPlot/config';
StackingMode,
} from '@grafana/ui';
import { Options } from './types';
import omitBy from 'lodash/omitBy';
import isNil from 'lodash/isNil';
import { isNumber, isString } from 'lodash';
import { defaultGraphConfig } from './config';
/**
* This is called when the panel changes from another panel
@@ -210,6 +213,12 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
break;
}
break;
case 'stack':
rule.properties.push({
id: 'custom.stacking',
value: { mode: StackingMode.Normal, group: v },
});
break;
default:
console.log('Ignore override migration:', seriesOverride.alias, p, v);
}
@@ -265,11 +274,17 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
graph.fillOpacity = 100; // bars were always
}
if (angular.stack) {
graph.stacking = {
mode: StackingMode.Normal,
group: defaultGraphConfig.stacking!.group,
};
}
y1.custom = omitBy(graph, isNil);
y1.nullValueMode = angular.nullPointMode as NullValueMode;
const options: Options = {
graph: {},
legend: {
displayMode: LegendDisplayMode.List,
placement: 'bottom',

View File

@@ -23,5 +23,6 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(TimeSeriesPanel
],
},
});
addLegendOptions(builder);
});

View File

@@ -1,14 +1,9 @@
import { VizLegendOptions, GraphTooltipOptions } from '@grafana/ui';
export interface GraphOptions {
// nothing for now
}
export interface OptionsWithLegend {
legend: VizLegendOptions;
}
export interface Options extends OptionsWithLegend {
graph: GraphOptions;
tooltipOptions: GraphTooltipOptions;
}

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from 'react';
import { Button, TooltipPlugin, GraphNG, GraphNGLegendEvent } from '@grafana/ui';
import { Button, GraphNG, GraphNGLegendEvent, TooltipPlugin } from '@grafana/ui';
import { PanelProps } from '@grafana/data';
import { Options } from './types';
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory';