From 0cc620aea7417c3d594aab29d4d86c115d5fad36 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 15 Apr 2021 13:00:01 +0200 Subject: [PATCH] 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 --- .../panel-graph/graph-ng-stacking.json | 896 ++++++++++++++++++ .../standardFieldConfigEditorRegistry.ts | 1 + .../components/BarChart/BarChart.story.tsx | 5 +- .../BarChart/__snapshots__/utils.test.ts.snap | 8 + .../src/components/BarChart/types.ts | 13 +- .../src/components/BarChart/utils.test.ts | 8 +- .../src/components/GraphNG/GraphNG.story.tsx | 1 + .../GraphNG/__snapshots__/utils.test.ts.snap | 145 ++- .../src/components/GraphNG/hooks.ts | 1 + .../src/components/GraphNG/utils.test.ts | 103 +- .../src/components/GraphNG/utils.ts | 16 + .../src/components/Sparkline/Sparkline.tsx | 1 - packages/grafana-ui/src/components/index.ts | 2 +- .../src/components/uPlot/Plot.test.tsx | 1 - .../grafana-ui/src/components/uPlot/config.ts | 23 + .../uPlot/config/UPlotConfigBuilder.test.ts | 153 ++- .../uPlot/config/UPlotConfigBuilder.ts | 28 +- .../uPlot/plugins/TooltipPlugin.tsx | 9 +- .../src/components/uPlot/utils.test.ts | 286 +++++- .../grafana-ui/src/components/uPlot/utils.ts | 47 +- .../PanelEditor/getFieldOverrideElements.tsx | 16 +- public/app/features/sandbox/TestStuffPage.tsx | 2 +- public/app/plugins/panel/barchart/module.tsx | 23 +- .../panel/timeseries/StackingEditor.tsx | 51 + .../__snapshots__/migrations.test.ts.snap | 149 ++- public/app/plugins/panel/timeseries/config.ts | 40 +- .../panel/timeseries/migrations.test.ts | 216 +++++ .../plugins/panel/timeseries/migrations.ts | 23 +- .../app/plugins/panel/timeseries/module.tsx | 1 + public/app/plugins/panel/timeseries/types.ts | 5 - .../plugins/panel/xychart/XYChartPanel.tsx | 2 +- 31 files changed, 2183 insertions(+), 92 deletions(-) create mode 100644 devenv/dev-dashboards/panel-graph/graph-ng-stacking.json create mode 100644 public/app/plugins/panel/timeseries/StackingEditor.tsx diff --git a/devenv/dev-dashboards/panel-graph/graph-ng-stacking.json b/devenv/dev-dashboards/panel-graph/graph-ng-stacking.json new file mode 100644 index 00000000000..0876d2d3fe4 --- /dev/null +++ b/devenv/dev-dashboards/panel-graph/graph-ng-stacking.json @@ -0,0 +1,896 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 15, 14, 30, 30, 30, 30, 30, 30, 30, -15, 30, 30, 30, 15, 0" + }, + { + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 20, 10, 10, 10, 10, 10" + }, + { + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 20, 10, 10, 10, 10, 10" + }, + { + "hide": false, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5" + }, + { + "hide": false, + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Data not stacked", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 8, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 15, 14, 30, 30, 30, 30, 30, 30, 30, -15, 30, 30, 30, 15, 0" + }, + { + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 20, 10, 10, 10, 10, 10" + }, + { + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 20, 10, 10, 10, 10, 10" + }, + { + "hide": false, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5" + }, + { + "hide": false, + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Normal stacking (series order 1)", + "type": "timeseries" + }, + { + "datasource": null, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 8, + "y": 8 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "hide": false, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5" + }, + { + "hide": false, + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5" + }, + { + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 20, 10, 10, 10, 10, 10" + }, + { + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 20, 10, 10, 10, 10, 10" + }, + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 15, 14, 30, 30, 30, 30, 30, 30, 30, -15, 30, 30, 30, 15, 0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Normal stacking (series order 2)", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "109,117,122,104,105,117,119,121,117,121,122,129,119,113,113,121,108,108,100,103,113,110,null,null,null,null,87,83,91,85,81,69,76,61,63,74,76,68,55,61,48,39,54,44,37,30,22,33,29,21,22,43,47,33,47,28,29,31,32,35,37,25,-5,-14,-7,-14,-7,-18,-18,-18,-16,-41,-22,-30,-27,-30,-47,-49,-47,-42,-55,-34,-27,-22,-23,-34,-23,-32,-36,-47,-33,-32,-18,-23,-21,-33,-39,-21,-18,-27,-5" + }, + { + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "253,248,247,282,212,275,306,276,253,243,276,261,313,233,231,315,302,288,251,300,263,227,232,271,258,212,225,234,244,200,249,205,231,173,263,246,190,184,198,233,246,232,228,201,212,163,194,155,177,179,130,160,245,184,225,221,217,229,165,172,227,212,181,133,140,128,111,177,177,159,92,100,92,154,161,124,83,99,104,76,117,87,151,154,77,71,164,68,94,105,107,74,130,156,91,103,97,98,96,76,117" + }, + { + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "408,436,368,422,383,454,459,380,360,400,378,419,468,394,343,457,411,422,448,419,431,389,414,381,415,357,419,398,352,398,379,400,337,299,369,416,329,303,333,362,438,383,350,337,348,289,362,298,355,285,294,319,445,308,375,387,337,406,323,372,342,389,311,254,270,248,271,309,374,323,255,289,223,280,331,237,247,229,293,230,239,242,271,343,255,185,296,217,236,268,257,273,323,346,205,285,277,265,277,253,219" + }, + { + "hide": false, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "556,581,482,552,550,589,575,565,471,575,565,605,663,544,471,641,567,535,630,535,623,579,582,559,611,509,521,563,513,544,523,558,450,486,478,584,440,432,473,480,555,510,451,476,490,470,555,418,463,402,403,476,591,431,560,581,532,579,427,501,452,551,479,405,397,376,458,439,490,508,389,488,360,413,509,351,434,392,430,426,375,414,401,459,435,327,491,406,397,406,384,423,439,538,338,467,421,406,477,428,371" + } + ], + "title": "Bars, data not stacked", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 8, + "y": 16 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "stacking": "none", + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "109,117,122,104,105,117,119,121,117,121,122,129,119,113,113,121,108,108,100,103,113,110,null,null,null,null,87,83,91,85,81,69,76,61,63,74,76,68,55,61,48,39,54,44,37,30,22,33,29,21,22,43,47,33,47,28,29,31,32,35,37,25,-5,-14,-7,-14,-7,-18,-18,-18,-16,-41,-22,-30,-27,-30,-47,-49,-47,-42,-55,-34,-27,-22,-23,-34,-23,-32,-36,-47,-33,-32,-18,-23,-21,-33,-39,-21,-18,-27,-5" + }, + { + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "253,248,247,282,212,275,306,276,253,243,276,261,313,233,231,315,302,288,251,300,263,227,232,271,258,212,225,234,244,200,249,205,231,173,263,246,190,184,198,233,246,232,228,201,212,163,194,155,177,179,130,160,245,184,225,221,217,229,165,172,227,212,181,133,140,128,111,177,177,159,92,100,92,154,161,124,83,99,104,76,117,87,151,154,77,71,164,68,94,105,107,74,130,156,91,103,97,98,96,76,117" + }, + { + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "408,436,368,422,383,454,459,380,360,400,378,419,468,394,343,457,411,422,448,419,431,389,414,381,415,357,419,398,352,398,379,400,337,299,369,416,329,303,333,362,438,383,350,337,348,289,362,298,355,285,294,319,445,308,375,387,337,406,323,372,342,389,311,254,270,248,271,309,374,323,255,289,223,280,331,237,247,229,293,230,239,242,271,343,255,185,296,217,236,268,257,273,323,346,205,285,277,265,277,253,219" + }, + { + "hide": false, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "556,581,482,552,550,589,575,565,471,575,565,605,663,544,471,641,567,535,630,535,623,579,582,559,611,509,521,563,513,544,523,558,450,486,478,584,440,432,473,480,555,510,451,476,490,470,555,418,463,402,403,476,591,431,560,581,532,579,427,501,452,551,479,405,397,376,458,439,490,508,389,488,360,413,509,351,434,392,430,426,375,414,401,459,435,327,491,406,397,406,384,423,439,538,338,467,421,406,477,428,371" + } + ], + "title": "Bars, normal stacking (series order 1)", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 8, + "y": 24 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "stacking": "none", + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "hide": false, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "556,581,482,552,550,589,575,565,471,575,565,605,663,544,471,641,567,535,630,535,623,579,582,559,611,509,521,563,513,544,523,558,450,486,478,584,440,432,473,480,555,510,451,476,490,470,555,418,463,402,403,476,591,431,560,581,532,579,427,501,452,551,479,405,397,376,458,439,490,508,389,488,360,413,509,351,434,392,430,426,375,414,401,459,435,327,491,406,397,406,384,423,439,538,338,467,421,406,477,428,371" + }, + { + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "408,436,368,422,383,454,459,380,360,400,378,419,468,394,343,457,411,422,448,419,431,389,414,381,415,357,419,398,352,398,379,400,337,299,369,416,329,303,333,362,438,383,350,337,348,289,362,298,355,285,294,319,445,308,375,387,337,406,323,372,342,389,311,254,270,248,271,309,374,323,255,289,223,280,331,237,247,229,293,230,239,242,271,343,255,185,296,217,236,268,257,273,323,346,205,285,277,265,277,253,219" + }, + { + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "253,248,247,282,212,275,306,276,253,243,276,261,313,233,231,315,302,288,251,300,263,227,232,271,258,212,225,234,244,200,249,205,231,173,263,246,190,184,198,233,246,232,228,201,212,163,194,155,177,179,130,160,245,184,225,221,217,229,165,172,227,212,181,133,140,128,111,177,177,159,92,100,92,154,161,124,83,99,104,76,117,87,151,154,77,71,164,68,94,105,107,74,130,156,91,103,97,98,96,76,117" + }, + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "109,117,122,104,105,117,119,121,117,121,122,129,119,113,113,121,108,108,100,103,113,110,null,null,null,null,87,83,91,85,81,69,76,61,63,74,76,68,55,61,48,39,54,44,37,30,22,33,29,21,22,43,47,33,47,28,29,31,32,35,37,25,-5,-14,-7,-14,-7,-18,-18,-18,-16,-41,-22,-30,-27,-30,-47,-49,-47,-42,-55,-34,-27,-22,-23,-34,-23,-32,-36,-47,-33,-32,-18,-23,-21,-33,-39,-21,-18,-27,-5" + } + ], + "title": "Bars, normal stacking (series order 2)", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 32 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "stacking": "none", + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,6,7,8,7,6" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "5,5,5,5,5,5" + }, + { + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "5,5,5,5,5,5" + }, + { + "hide": false, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "5,5,5,5,5,5" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Stacking groups, data not stacked", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "custom.stacking", + "value": { + "group": "A", + "mode": "normal" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "B-series" + }, + "properties": [ + { + "id": "custom.stacking", + "value": { + "group": "A", + "mode": "normal" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "C-series" + }, + "properties": [ + { + "id": "custom.stacking", + "value": { + "group": "B", + "mode": "normal" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "D-series" + }, + "properties": [ + { + "id": "custom.stacking", + "value": { + "group": "B", + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 8, + "y": 32 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "stacking": "none", + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,6,7,8,7,6" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "5,5,5,5,5,5" + }, + { + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "5,5,5,5,5,5" + }, + { + "hide": false, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "5,5,5,5,5,5" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Stacking groups, data stacked, two stacking groups", + "type": "timeseries" + } + ], + "schemaVersion": 27, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Panel Tests - TimeSeries - stacking", + "uid": "Ei86FP_Mx", + "version": 6 +} diff --git a/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts b/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts index 74db315bd1e..31a5c2480c6 100644 --- a/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts +++ b/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts @@ -10,6 +10,7 @@ export interface StandardEditorContext { eventBus?: EventBus; getSuggestions?: (scope?: VariableSuggestionsScope) => VariableSuggestion[]; options?: TOptions; + isOverride?: boolean; } export interface StandardEditorProps { diff --git a/packages/grafana-ui/src/components/BarChart/BarChart.story.tsx b/packages/grafana-ui/src/components/BarChart/BarChart.story.tsx index b50f46b8206..e3b2a3524eb 100644 --- a/packages/grafana-ui/src/components/BarChart/BarChart.story.tsx +++ b/packages/grafana-ui/src/components/BarChart/BarChart.story.tsx @@ -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, diff --git a/packages/grafana-ui/src/components/BarChart/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/BarChart/__snapshots__/utils.test.ts.snap index 6517f87290b..c2198ef3437 100644 --- a/packages/grafana-ui/src/components/BarChart/__snapshots__/utils.test.ts.snap +++ b/packages/grafana-ui/src/components/BarChart/__snapshots__/utils.test.ts.snap @@ -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 { diff --git a/packages/grafana-ui/src/components/BarChart/types.ts b/packages/grafana-ui/src/components/BarChart/types.ts index 2e1b3c33fab..46ef7a9f656 100644 --- a/packages/grafana-ui/src/components/BarChart/types.ts +++ b/packages/grafana-ui/src/components/BarChart/types.ts @@ -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; diff --git a/packages/grafana-ui/src/components/BarChart/utils.test.ts b/packages/grafana-ui/src/components/BarChart/utils.test.ts index 1b905290955..be48c3bd599 100644 --- a/packages/grafana-ui/src/components/BarChart/utils.test.ts +++ b/packages/grafana-ui/src/components/BarChart/utils.test.ts @@ -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, diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.story.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.story.tsx index e1eb1ae2d71..5141cf6a9db 100644 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.story.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.story.tsx @@ -64,6 +64,7 @@ export const Lines: Story = ({ placement, unit, legendDisplayMode, . placement: placement, calcs: [], }} + timeZone="browser" /> ); }; diff --git a/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap index f4b5b597520..8d65298a51a 100644 --- a/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap +++ b/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap @@ -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], diff --git a/packages/grafana-ui/src/components/GraphNG/hooks.ts b/packages/grafana-ui/src/components/GraphNG/hooks.ts index 3d35a95e797..1a8db1a0449 100644 --- a/packages/grafana-ui/src/components/GraphNG/hooks.ts +++ b/packages/grafana-ui/src/components/GraphNG/hooks.ts @@ -37,5 +37,6 @@ export const useGraphNGContext = () => { dimFields, mapSeriesIndexToDataFrameFieldIndex, getXAxisField, + alignedData: data, }; }; diff --git a/packages/grafana-ui/src/components/GraphNG/utils.test.ts b/packages/grafana-ui/src/components/GraphNG/utils.test.ts index 8881a00601b..5e48b60693e 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.test.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.test.ts @@ -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 = { + 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 = { + 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 = { + 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({}), diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index 6076d95a309..498fd69b5f6 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -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 = new Map(); + let indexByName: Map | 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; } diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx index a0c0326173f..0f56730b32c 100755 --- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx +++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx @@ -176,7 +176,6 @@ export class Sparkline extends PureComponent { render() { const { data, configBuilder } = this.state; const { width, height, sparkline } = this.props; - return ( ); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 3b742578208..cf737c56ad2 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/packages/grafana-ui/src/components/uPlot/Plot.test.tsx b/packages/grafana-ui/src/components/uPlot/Plot.test.tsx index 999d1fd0d56..8bc5d4eb10f 100644 --- a/packages/grafana-ui/src/components/uPlot/Plot.test.tsx +++ b/packages/grafana-ui/src/components/uPlot/Plot.test.tsx @@ -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( ); diff --git a/packages/grafana-ui/src/components/uPlot/config.ts b/packages/grafana-ui/src/components/uPlot/config.ts index 86798a44072..bc3429d8140 100644 --- a/packages/grafana-ui/src/components/uPlot/config.ts +++ b/packages/grafana-ui/src/components/uPlot/config.ts @@ -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>, + + stacking: [ + { label: 'Off', value: StackingMode.None }, + { label: 'Normal', value: StackingMode.Normal }, + ] as Array>, }; diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts index 6209487e89e..ae2bdb3dde9 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts @@ -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], + } + `); + }); + }); }); diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index 77bd3dfb271..68bf86c1e7e 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -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 | 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(); - 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(); + 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; + } } } } diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx index a8003bbff85..d5acfb48a4a 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx @@ -58,10 +58,11 @@ export const TooltipPlugin: React.FC = ({ 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 = ( = ({ 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 diff --git a/packages/grafana-ui/src/components/uPlot/utils.test.ts b/packages/grafana-ui/src/components/uPlot/utils.test.ts index 79fd38a3807..97a3fcec9f7 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.test.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.test.ts @@ -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, + ], + ] + `); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/uPlot/utils.ts b/packages/grafana-ui/src/components/uPlot/utils.ts index 1a353414611..d1ff8fbbf0f 100755 --- a/packages/grafana-ui/src/components/uPlot/utils.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.ts @@ -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 = 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, 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 */ diff --git a/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx b/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx index 481b9325e71..b4d592c9d6c 100644 --- a/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx @@ -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) => { + const onDynamicConfigValueAdd = (o: ConfigOverrideRule, value: SelectableValue) => { 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, diff --git a/public/app/features/sandbox/TestStuffPage.tsx b/public/app/features/sandbox/TestStuffPage.tsx index a0bbaa99244..c89ef9c5f56 100644 --- a/public/app/features/sandbox/TestStuffPage.tsx +++ b/public/app/features/sandbox/TestStuffPage.tsx @@ -62,7 +62,7 @@ export const TestStuffPage: FC = () => { timeRange={data.timeRange} timeZone="browser" /> -
+
)} diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index 72269956cad..4e37eeeeba3 100755 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -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(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(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; diff --git a/public/app/plugins/panel/timeseries/StackingEditor.tsx b/public/app/plugins/panel/timeseries/StackingEditor.tsx new file mode 100644 index 00000000000..57c3a8d4452 --- /dev/null +++ b/public/app/plugins/panel/timeseries/StackingEditor.tsx @@ -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> = ({ + value, + context, + onChange, + item, +}) => { + return ( + + { + onChange({ + ...value, + mode: v, + }); + }} + /> + {context.isOverride && value?.mode && value?.mode !== StackingMode.None && ( + + + + } + defaultValue={value?.group} + onChange={(v) => { + onChange({ + ...value, + group: v.currentTarget.value.trim(), + }); + }} + /> + )} + + ); +}; diff --git a/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap b/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap index 776563cdf09..7ec3040596a 100644 --- a/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap +++ b/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap @@ -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", diff --git a/public/app/plugins/panel/timeseries/config.ts b/public/app/plugins/panel/timeseries/config.ts index ca870d8dd03..f18a1e5d113 100644 --- a/public/app/plugins/panel/timeseries/config.ts +++ b/public/app/plugins/panel/timeseries/config.ts @@ -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 { - const categoryStyles = ['Graph styles']; +const categoryStyles = ['Graph styles']; +export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOptionsArgs { 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(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, + }); +} diff --git a/public/app/plugins/panel/timeseries/migrations.test.ts b/public/app/plugins/panel/timeseries/migrations.test.ts index 6fbd9d26248..de496597a97 100644 --- a/public/app/plugins/panel/timeseries/migrations.test.ts +++ b/public/app/plugins/panel/timeseries/migrations.test.ts @@ -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, +}; diff --git a/public/app/plugins/panel/timeseries/migrations.ts b/public/app/plugins/panel/timeseries/migrations.ts index 4870604c0a9..fe431bf6180 100644 --- a/public/app/plugins/panel/timeseries/migrations.ts +++ b/public/app/plugins/panel/timeseries/migrations.ts @@ -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', diff --git a/public/app/plugins/panel/timeseries/module.tsx b/public/app/plugins/panel/timeseries/module.tsx index 23066060217..7ab311cbd9e 100644 --- a/public/app/plugins/panel/timeseries/module.tsx +++ b/public/app/plugins/panel/timeseries/module.tsx @@ -23,5 +23,6 @@ export const plugin = new PanelPlugin(TimeSeriesPanel ], }, }); + addLegendOptions(builder); }); diff --git a/public/app/plugins/panel/timeseries/types.ts b/public/app/plugins/panel/timeseries/types.ts index f75e2afc64a..1058e544380 100644 --- a/public/app/plugins/panel/timeseries/types.ts +++ b/public/app/plugins/panel/timeseries/types.ts @@ -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; } diff --git a/public/app/plugins/panel/xychart/XYChartPanel.tsx b/public/app/plugins/panel/xychart/XYChartPanel.tsx index 64df31371f5..0dc835f1a69 100644 --- a/public/app/plugins/panel/xychart/XYChartPanel.tsx +++ b/public/app/plugins/panel/xychart/XYChartPanel.tsx @@ -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';