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
@@ -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
}
@@ -10,6 +10,7 @@ export interface StandardEditorContext<TOptions> {
eventBus?: EventBus; eventBus?: EventBus;
getSuggestions?: (scope?: VariableSuggestionsScope) => VariableSuggestion[]; getSuggestions?: (scope?: VariableSuggestionsScope) => VariableSuggestion[];
options?: TOptions; options?: TOptions;
isOverride?: boolean;
} }
export interface StandardEditorProps<TValue = any, TSettings = any, TOptions = any> { 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 { prepDataForStorybook } from '../../utils/storybook/data';
import { useTheme } from '../../themes'; import { useTheme } from '../../themes';
import { select } from '@storybook/addon-knobs'; import { select } from '@storybook/addon-knobs';
import { BarChartOptions, BarStackingMode, BarValueVisibility } from './types'; import { BarChartOptions, BarValueVisibility } from './types';
import { StackingMode } from '../uPlot/config';
export default { export default {
title: 'Visualizations/BarChart', title: 'Visualizations/BarChart',
@@ -55,7 +56,7 @@ export const Basic: React.FC = () => {
const options: BarChartOptions = { const options: BarChartOptions = {
orientation: orientation, orientation: orientation,
legend: { displayMode: LegendDisplayMode.List, placement: legendPlacement, calcs: [] }, legend: { displayMode: LegendDisplayMode.List, placement: legendPlacement, calcs: [] },
stacking: BarStackingMode.None, stacking: StackingMode.None,
showValue: BarValueVisibility.Always, showValue: BarValueVisibility.Always,
barWidth: 0.97, barWidth: 0.97,
groupWidth: 0.7, groupWidth: 0.7,
@@ -56,6 +56,7 @@ UPlotConfigBuilder {
[Function], [Function],
], ],
}, },
"isStacking": false,
"scales": Array [ "scales": Array [
UPlotScaleBuilder { UPlotScaleBuilder {
"props": Object { "props": Object {
@@ -176,6 +177,7 @@ UPlotConfigBuilder {
[Function], [Function],
], ],
}, },
"isStacking": false,
"scales": Array [ "scales": Array [
UPlotScaleBuilder { UPlotScaleBuilder {
"props": Object { "props": Object {
@@ -296,6 +298,7 @@ UPlotConfigBuilder {
[Function], [Function],
], ],
}, },
"isStacking": false,
"scales": Array [ "scales": Array [
UPlotScaleBuilder { UPlotScaleBuilder {
"props": Object { "props": Object {
@@ -416,6 +419,7 @@ UPlotConfigBuilder {
[Function], [Function],
], ],
}, },
"isStacking": false,
"scales": Array [ "scales": Array [
UPlotScaleBuilder { UPlotScaleBuilder {
"props": Object { "props": Object {
@@ -536,6 +540,7 @@ UPlotConfigBuilder {
[Function], [Function],
], ],
}, },
"isStacking": false,
"scales": Array [ "scales": Array [
UPlotScaleBuilder { UPlotScaleBuilder {
"props": Object { "props": Object {
@@ -656,6 +661,7 @@ UPlotConfigBuilder {
[Function], [Function],
], ],
}, },
"isStacking": false,
"scales": Array [ "scales": Array [
UPlotScaleBuilder { UPlotScaleBuilder {
"props": Object { "props": Object {
@@ -776,6 +782,7 @@ UPlotConfigBuilder {
[Function], [Function],
], ],
}, },
"isStacking": false,
"scales": Array [ "scales": Array [
UPlotScaleBuilder { UPlotScaleBuilder {
"props": Object { "props": Object {
@@ -896,6 +903,7 @@ UPlotConfigBuilder {
[Function], [Function],
], ],
}, },
"isStacking": false,
"scales": Array [ "scales": Array [
UPlotScaleBuilder { UPlotScaleBuilder {
"props": Object { "props": Object {
@@ -1,16 +1,7 @@
import { VizOrientation } from '@grafana/data'; 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'; import { VizLegendOptions } from '../VizLegend/models.gen';
/**
* @alpha
*/
export enum BarStackingMode {
None = 'none',
Standard = 'standard',
Percent = 'percent',
}
/** /**
* @alpha * @alpha
*/ */
@@ -26,7 +17,7 @@ export enum BarValueVisibility {
export interface BarChartOptions { export interface BarChartOptions {
orientation: VizOrientation; orientation: VizOrientation;
legend: VizLegendOptions; legend: VizLegendOptions;
stacking: BarStackingMode; stacking: StackingMode;
showValue: BarValueVisibility; showValue: BarValueVisibility;
barWidth: number; barWidth: number;
groupWidth: number; groupWidth: number;
@@ -1,7 +1,7 @@
import { preparePlotConfigBuilder, preparePlotFrame } from './utils'; import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
import { FieldConfig, FieldType, GrafanaTheme, MutableDataFrame, VizOrientation } from '@grafana/data'; import { FieldConfig, FieldType, GrafanaTheme, MutableDataFrame, VizOrientation } from '@grafana/data';
import { BarChartFieldConfig, BarChartOptions, BarStackingMode, BarValueVisibility } from './types'; import { BarChartFieldConfig, BarChartOptions, BarValueVisibility } from './types';
import { GraphGradientMode } from '../uPlot/config'; import { GraphGradientMode, StackingMode } from '../uPlot/config';
import { LegendDisplayMode } from '../VizLegend/models.gen'; import { LegendDisplayMode } from '../VizLegend/models.gen';
function mockDataFrame() { function mockDataFrame() {
@@ -73,7 +73,7 @@ describe('GraphNG utils', () => {
placement: 'bottom', placement: 'bottom',
calcs: [], calcs: [],
}, },
stacking: BarStackingMode.None, stacking: StackingMode.None,
}; };
it.each([VizOrientation.Auto, VizOrientation.Horizontal, VizOrientation.Vertical])('orientation', (v) => { it.each([VizOrientation.Auto, VizOrientation.Horizontal, VizOrientation.Vertical])('orientation', (v) => {
@@ -94,7 +94,7 @@ describe('GraphNG utils', () => {
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
it.each([BarStackingMode.None, BarStackingMode.Percent, BarStackingMode.Standard])('stacking', (v) => { it.each([StackingMode.None, StackingMode.Percent, StackingMode.Normal])('stacking', (v) => {
expect( expect(
preparePlotConfigBuilder(frame!, { colors: { panelBg: '#000000' } } as GrafanaTheme, { preparePlotConfigBuilder(frame!, { colors: { panelBg: '#000000' } } as GrafanaTheme, {
...config, ...config,
@@ -64,6 +64,7 @@ export const Lines: Story<StoryProps> = ({ placement, unit, legendDisplayMode, .
placement: placement, placement: placement,
calcs: [], calcs: [],
}} }}
timeZone="browser"
/> />
); );
}; };
@@ -31,10 +31,24 @@ UPlotConfigBuilder {
}, },
}, },
}, },
"bands": Array [], "bands": Array [
Object {
"series": Array [
2,
1,
],
},
Object {
"series": Array [
4,
3,
],
},
],
"hasBottomAxis": true, "hasBottomAxis": true,
"hasLeftAxis": true, "hasLeftAxis": true,
"hooks": Object {}, "hooks": Object {},
"isStacking": true,
"scales": Array [ "scales": Array [
UPlotScaleBuilder { UPlotScaleBuilder {
"props": Object { "props": Object {
@@ -146,6 +160,135 @@ UPlotConfigBuilder {
"thresholds": undefined, "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", "tz": "UTC",
"tzDate": [Function], "tzDate": [Function],
@@ -37,5 +37,6 @@ export const useGraphNGContext = () => {
dimFields, dimFields,
mapSeriesIndexToDataFrameFieldIndex, mapSeriesIndexToDataFrameFieldIndex,
getXAxisField, getXAxisField,
alignedData: data,
}; };
}; };
@@ -9,7 +9,15 @@ import {
GrafanaTheme, GrafanaTheme,
MutableDataFrame, MutableDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { BarAlignment, DrawStyle, GraphFieldConfig, GraphGradientMode, LineInterpolation, PointVisibility } from '..'; import {
BarAlignment,
DrawStyle,
GraphFieldConfig,
GraphGradientMode,
LineInterpolation,
PointVisibility,
StackingMode,
} from '..';
function mockDataFrame() { function mockDataFrame() {
const df1 = new MutableDataFrame({ const df1 = new MutableDataFrame({
@@ -38,6 +46,10 @@ function mockDataFrame() {
fillColor: '#ff0000', fillColor: '#ff0000',
fillOpacity: 0.1, fillOpacity: 0.1,
showPoints: PointVisibility.Always, showPoints: PointVisibility.Always,
stacking: {
group: 'A',
mode: StackingMode.Normal,
},
}, },
}; };
@@ -58,6 +70,80 @@ function mockDataFrame() {
fillColor: '#ff0000', fillColor: '#ff0000',
fillOpacity: 0.1, fillOpacity: 0.1,
showPoints: PointVisibility.Always, 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, type: FieldType.number,
config: f2Config, 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], { return preparePlotFrame([df1, df2], {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
@@ -26,6 +26,7 @@ import {
ScaleDirection, ScaleDirection,
ScaleOrientation, ScaleOrientation,
} from '../uPlot/config'; } from '../uPlot/config';
import { collectStackingGroups } from '../uPlot/utils';
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); 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; let indexByName: Map<string, number> | undefined = undefined;
for (let i = 0; i < frame.fields.length; i++) { 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; const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
let { fillOpacity } = customConfig; let { fillOpacity } = customConfig;
if (customConfig.fillBelowTo) { if (customConfig.fillBelowTo) {
if (!indexByName) { if (!indexByName) {
indexByName = getNamesToFieldIndex(frame); indexByName = getNamesToFieldIndex(frame);
@@ -219,8 +223,20 @@ export function preparePlotConfigBuilder(
fieldName: getFieldDisplayName(field, frame), fieldName: getFieldDisplayName(field, frame),
hideInLegend: customConfig.hideFrom?.legend, 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; return builder;
} }
@@ -176,7 +176,6 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
render() { render() {
const { data, configBuilder } = this.state; const { data, configBuilder } = this.state;
const { width, height, sparkline } = this.props; const { width, height, sparkline } = this.props;
return ( return (
<UPlotChart data={data} config={configBuilder} width={width} height={height} timeRange={sparkline.timeRange!} /> <UPlotChart data={data} config={configBuilder} width={width} height={height} timeRange={sparkline.timeRange!} />
); );
+1 -1
View File
@@ -228,7 +228,7 @@ export { GraphNG, FIXED_UNIT } from './GraphNG/GraphNG';
export { useGraphNGContext } from './GraphNG/hooks'; export { useGraphNGContext } from './GraphNG/hooks';
export { BarChart } from './BarChart/BarChart'; export { BarChart } from './BarChart/BarChart';
export { TimelineChart } from './Timeline/TimelineChart'; 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 { TimelineOptions, TimelineFieldConfig } from './Timeline/types';
export { GraphNGLegendEvent, GraphNGLegendEventMode } from './GraphNG/types'; export { GraphNGLegendEvent, GraphNGLegendEventMode } from './GraphNG/types';
export * from './NodeGraph'; export * from './NodeGraph';
@@ -133,7 +133,6 @@ describe('UPlotChart', () => {
describe('config update', () => { describe('config update', () => {
it('skips uPlot intialization for width and height equal 0', async () => { it('skips uPlot intialization for width and height equal 0', async () => {
const { data, timeRange, config } = mockData(); const { data, timeRange, config } = mockData();
const { queryAllByTestId } = render( const { queryAllByTestId } = render(
<UPlotChart data={preparePlotData(data)} config={config} timeRange={timeRange} width={0} height={0} /> <UPlotChart data={preparePlotData(data)} config={config} timeRange={timeRange} width={0} height={0} />
); );
@@ -175,6 +175,23 @@ export interface HideableFieldConfig {
hideFrom?: HideSeriesConfig; hideFrom?: HideSeriesConfig;
} }
/**
* @alpha
*/
export enum StackingMode {
None = 'none',
Normal = 'normal',
Percent = 'percent',
}
/**
* @alpha
*/
export interface StackingConfig {
mode?: StackingMode;
group?: string;
}
/** /**
* @alpha * @alpha
*/ */
@@ -187,6 +204,7 @@ export interface GraphFieldConfig
HideableFieldConfig { HideableFieldConfig {
drawStyle?: DrawStyle; drawStyle?: DrawStyle;
gradientMode?: GraphGradientMode; gradientMode?: GraphGradientMode;
stacking?: StackingConfig;
} }
/** /**
@@ -231,4 +249,9 @@ export const graphFieldOptions = {
{ label: 'Hue', value: GraphGradientMode.Hue }, { label: 'Hue', value: GraphGradientMode.Hue },
// { label: 'Color scheme', value: GraphGradientMode.Scheme }, // { label: 'Color scheme', value: GraphGradientMode.Scheme },
] as Array<SelectableValue<GraphGradientMode>>, ] 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(); const builder = new UPlotConfigBuilder();
builder.addAxis({ builder.addAxis({
@@ -370,7 +370,7 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getConfig().axes![1].grid!.show).toBe(false); 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(); const builder = new UPlotConfigBuilder();
builder.addSeries({ builder.addSeries({
drawStyle: DrawStyle.Line, drawStyle: DrawStyle.Line,
@@ -383,7 +383,7 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getConfig().series[1].fill).toBe(undefined); expect(builder.getConfig().series[1].fill).toBe(undefined);
}); });
it('When fillOpacity is set', () => { it('when fillOpacity is set', () => {
const builder = new UPlotConfigBuilder(); const builder = new UPlotConfigBuilder();
builder.addSeries({ builder.addSeries({
drawStyle: DrawStyle.Line, drawStyle: DrawStyle.Line,
@@ -397,7 +397,7 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getConfig().series[1].fill).toBe('rgba(255, 170, 187, 0.5)'); 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(); const builder = new UPlotConfigBuilder();
builder.addSeries({ builder.addSeries({
drawStyle: DrawStyle.Line, drawStyle: DrawStyle.Line,
@@ -412,7 +412,7 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getConfig().series[1].fill).toBe('#FF0000'); expect(builder.getConfig().series[1].fill).toBe('#FF0000');
}); });
it('When fillGradient mode is opacity', () => { it('when fillGradient mode is opacity', () => {
const builder = new UPlotConfigBuilder(); const builder = new UPlotConfigBuilder();
builder.addSeries({ builder.addSeries({
drawStyle: DrawStyle.Line, 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 scales: UPlotScaleBuilder[] = [];
private bands: Band[] = []; private bands: Band[] = [];
private cursor: Cursor | undefined; private cursor: Cursor | undefined;
private isStacking = false;
// uPlot types don't export the Select interface prior to 1.6.4 // uPlot types don't export the Select interface prior to 1.6.4
private select: Partial<BBox> | undefined; private select: Partial<BBox> | undefined;
private hasLeftAxis = false; private hasLeftAxis = false;
@@ -78,6 +79,9 @@ export class UPlotConfigBuilder {
this.select = select; this.select = select;
} }
setStacking(enabled = true) {
this.isStacking = enabled;
}
addSeries(props: SeriesProps) { addSeries(props: SeriesProps) {
this.series.push(new UPlotSeriesBuilder(props)); this.series.push(new UPlotSeriesBuilder(props));
} }
@@ -118,16 +122,22 @@ export class UPlotConfigBuilder {
config.tzDate = this.tzDate; config.tzDate = this.tzDate;
// When bands exist, only keep fill when defined if (this.isStacking) {
if (this.bands?.length) { // Let uPlot handle bands and fills
config.bands = this.bands; config.bands = this.bands;
const keepFill = new Set<number>(); } else {
for (const b of config.bands) { // When fillBelowTo option enabled, handle series bands fill manually
keepFill.add(b.series[0]); if (this.bands?.length) {
} config.bands = this.bands;
for (let i = 1; i < config.series.length; i++) { const keepFill = new Set<number>();
if (!keepFill.has(i)) { for (const b of config.bands) {
config.series[i].fill = undefined; 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 // when interacting with a point in single mode
if (mode === 'single' && originFieldIndex !== null) { 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 plotSeries = plotContext.getSeries();
const fieldFmt = field.display || getDisplayProcessor({ field, timeZone }); const fieldFmt = field.display || getDisplayProcessor({ field, timeZone });
const value = fieldFmt(plotContext.data[focusedSeriesIdx!][focusedPointIdx]);
const value = fieldFmt(field.values.get(focusedPointIdx));
tooltip = ( tooltip = (
<SeriesTable <SeriesTable
@@ -95,7 +96,9 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({ mode = 'single', t
continue; 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({ series.push({
// TODO: align with uPlot typings // 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', () => { describe('timeFormatToTemplate', () => {
it.each` it.each`
@@ -13,3 +15,285 @@ describe('timeFormatToTemplate', () => {
expect(timeFormatToTemplate(format)).toEqual(expected); 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 { AlignedData, Options } from 'uplot';
import { PlotPlugin, PlotProps } from './types'; import { PlotPlugin, PlotProps } from './types';
import { StackingMode } from './config';
import { createLogger } from '../../utils/logger'; import { createLogger } from '../../utils/logger';
import { attachDebugger } from '../../utils'; import { attachDebugger } from '../../utils';
@@ -33,8 +34,11 @@ export function buildPlotConfig(props: PlotProps, plugins: Record<string, PlotPl
} }
/** @internal */ /** @internal */
export function preparePlotData(frame: DataFrame, ignoreFieldTypes?: FieldType[]): AlignedData { export function preparePlotData(frame: DataFrame, ignoreFieldTypes?: FieldType[]): AlignedData {
const result: any[] = []; const result: any[] = [];
const stackingGroups: Map<string, number[]> = new Map();
let seriesIndex = 0;
for (let i = 0; i < frame.fields.length; i++) { for (let i = 0; i < frame.fields.length; i++) {
const f = frame.fields[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()); timestamps.push(dateTime(f.values.get(i)).valueOf());
} }
result.push(timestamps); result.push(timestamps);
seriesIndex++;
continue; continue;
} }
result.push(f.values.toArray()); result.push(f.values.toArray());
seriesIndex++;
continue; continue;
} }
if (ignoreFieldTypes && ignoreFieldTypes.indexOf(f.type) > -1) { if (ignoreFieldTypes && ignoreFieldTypes.indexOf(f.type) > -1) {
continue; continue;
} }
collectStackingGroups(f, stackingGroups, seriesIndex);
result.push(f.values.toArray()); 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; 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 // Dev helpers
/** @internal */ /** @internal */
@@ -6,6 +6,7 @@ import {
isSystemOverride as isSystemOverrideGuard, isSystemOverride as isSystemOverrideGuard,
VariableSuggestionsScope, VariableSuggestionsScope,
DynamicConfigValue, DynamicConfigValue,
ConfigOverrideRule,
} from '@grafana/data'; } from '@grafana/data';
import { Container, fieldMatchersUI, ValuePicker } from '@grafana/ui'; import { Container, fieldMatchersUI, ValuePicker } from '@grafana/ui';
import { OptionPaneRenderProps } from './types'; import { OptionPaneRenderProps } from './types';
@@ -51,6 +52,7 @@ export function getFieldOverrideCategories(props: OptionPaneRenderProps): Option
const context = { const context = {
data, data,
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope), getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
isOverride: true,
}; };
/** /**
@@ -88,7 +90,7 @@ export function getFieldOverrideCategories(props: OptionPaneRenderProps): Option
onOverrideChange(idx, override); onOverrideChange(idx, override);
}; };
const onDynamicConfigValueAdd = (value: SelectableValue<string>) => { const onDynamicConfigValueAdd = (o: ConfigOverrideRule, value: SelectableValue<string>) => {
const registryItem = registry.get(value.value!); const registryItem = registry.get(value.value!);
const propertyConfig: DynamicConfigValue = { const propertyConfig: DynamicConfigValue = {
id: registryItem.id, id: registryItem.id,
@@ -96,12 +98,12 @@ export function getFieldOverrideCategories(props: OptionPaneRenderProps): Option
}; };
if (override.properties) { if (override.properties) {
override.properties.push(propertyConfig); o.properties.push(propertyConfig);
} else { } 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" icon="plus"
menuPlacement="auto" menuPlacement="auto"
options={configPropertiesOptions} options={configPropertiesOptions}
onChange={onDynamicConfigValueAdd} onChange={(v) => onDynamicConfigValueAdd(override, v)}
/> />
); );
}, },
@@ -241,8 +243,8 @@ function getOverrideProperties(registry: FieldConfigOptionsRegistry) {
.filter((o) => !o.hideFromOverrides) .filter((o) => !o.hideFromOverrides)
.map((item) => { .map((item) => {
let label = item.name; let label = item.name;
if (item.category && item.category.length > 1) { if (item.category) {
label = [...item.category!.slice(1), item.name].join(' > '); label = [...item.category, item.name].join(' > ');
} }
return { return {
label, label,
@@ -62,7 +62,7 @@ export const TestStuffPage: FC = () => {
timeRange={data.timeRange} timeRange={data.timeRange}
timeZone="browser" timeZone="browser"
/> />
<hr></hr> <hr />
<Table data={data.series[0]} width={1200} height={300} /> <Table data={data.series[0]} width={1200} height={300} />
</div> </div>
)} )}
+2 -21
View File
@@ -7,13 +7,7 @@ import {
VizOrientation, VizOrientation,
} from '@grafana/data'; } from '@grafana/data';
import { BarChartPanel } from './BarChartPanel'; import { BarChartPanel } from './BarChartPanel';
import { import { BarChartFieldConfig, BarChartOptions, StackingMode, BarValueVisibility, graphFieldOptions } from '@grafana/ui';
BarChartFieldConfig,
BarChartOptions,
BarStackingMode,
BarValueVisibility,
graphFieldOptions,
} from '@grafana/ui';
import { addAxisConfig, addHideFrom, addLegendOptions } from '../timeseries/config'; import { addAxisConfig, addHideFrom, addLegendOptions } from '../timeseries/config';
import { defaultBarChartFieldConfig } from '@grafana/ui/src/components/BarChart/types'; import { defaultBarChartFieldConfig } from '@grafana/ui/src/components/BarChart/types';
@@ -80,19 +74,6 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
}, },
defaultValue: VizOrientation.Auto, 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({ .addRadio({
path: 'showValue', path: 'showValue',
name: 'Show values', name: 'Show values',
@@ -115,7 +96,7 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
step: 0.01, step: 0.01,
}, },
showIf: (c, data) => { showIf: (c, data) => {
if (c.stacking && c.stacking !== BarStackingMode.None) { if (c.stacking && c.stacking !== StackingMode.None) {
return false; return false;
} }
return countNumberFields(data) !== 1; return countNumberFields(data) !== 1;
@@ -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>
);
};
@@ -35,7 +35,6 @@ Object {
], ],
}, },
"options": Object { "options": Object {
"graph": Object {},
"legend": Object { "legend": Object {
"calcs": Array [ "calcs": Array [
"mean", "mean",
@@ -68,7 +67,6 @@ Object {
"overrides": Array [], "overrides": Array [],
}, },
"options": Object { "options": Object {
"graph": Object {},
"legend": Object { "legend": Object {
"calcs": Array [], "calcs": Array [],
"displayMode": "list", "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`] = ` exports[`Graph Migrations stairscase 1`] = `
Object { Object {
"fieldConfig": Object { "fieldConfig": Object {
@@ -102,7 +244,6 @@ Object {
"overrides": Array [], "overrides": Array [],
}, },
"options": Object { "options": Object {
"graph": Object {},
"legend": Object { "legend": Object {
"calcs": Array [ "calcs": Array [
"mean", "mean",
@@ -156,7 +297,6 @@ Object {
], ],
}, },
"options": Object { "options": Object {
"graph": Object {},
"legend": Object { "legend": Object {
"calcs": Array [], "calcs": Array [],
"displayMode": "list", "displayMode": "list",
@@ -237,7 +377,6 @@ Object {
], ],
}, },
"options": Object { "options": Object {
"graph": Object {},
"legend": Object { "legend": Object {
"calcs": Array [], "calcs": Array [],
"displayMode": "list", "displayMode": "list",
+34 -6
View File
@@ -11,20 +11,22 @@ import {
stringOverrideProcessor, stringOverrideProcessor,
} from '@grafana/data'; } from '@grafana/data';
import { import {
AxisConfig,
AxisPlacement, AxisPlacement,
BarAlignment, BarAlignment,
DrawStyle, DrawStyle,
GraphFieldConfig, GraphFieldConfig,
graphFieldOptions, graphFieldOptions,
GraphGradientMode,
HideableFieldConfig,
LegendDisplayMode,
LineInterpolation, LineInterpolation,
LineStyle, LineStyle,
PointVisibility, PointVisibility,
ScaleDistribution, ScaleDistribution,
ScaleDistributionConfig, ScaleDistributionConfig,
GraphGradientMode, StackingConfig,
LegendDisplayMode, StackingMode,
AxisConfig,
HideableFieldConfig,
} from '@grafana/ui'; } from '@grafana/ui';
import { SeriesConfigEditor } from './HideSeriesConfigEditor'; import { SeriesConfigEditor } from './HideSeriesConfigEditor';
import { ScaleDistributionEditor } from './ScaleDistributionEditor'; import { ScaleDistributionEditor } from './ScaleDistributionEditor';
@@ -32,6 +34,7 @@ import { LineStyleEditor } from './LineStyleEditor';
import { FillBellowToEditor } from './FillBelowToEditor'; import { FillBellowToEditor } from './FillBelowToEditor';
import { OptionsWithLegend } from './types'; import { OptionsWithLegend } from './types';
import { SpanNullsEditor } from './SpanNullsEditor'; import { SpanNullsEditor } from './SpanNullsEditor';
import { StackingEditor } from './StackingEditor';
export const defaultGraphConfig: GraphFieldConfig = { export const defaultGraphConfig: GraphFieldConfig = {
drawStyle: DrawStyle.Line, drawStyle: DrawStyle.Line,
@@ -40,11 +43,15 @@ export const defaultGraphConfig: GraphFieldConfig = {
fillOpacity: 0, fillOpacity: 0,
gradientMode: GraphGradientMode.None, gradientMode: GraphGradientMode.None,
barAlignment: BarAlignment.Center, 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 { return {
standardOptions: { standardOptions: {
[FieldConfigProperty.Color]: { [FieldConfigProperty.Color]: {
@@ -180,6 +187,7 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
showIf: (c) => c.showPoints !== PointVisibility.Never || c.drawStyle === DrawStyle.Points, showIf: (c) => c.showPoints !== PointVisibility.Never || c.drawStyle === DrawStyle.Points,
}); });
addStackingConfig(builder, cfg.stacking);
addAxisConfig(builder, cfg); addAxisConfig(builder, cfg);
addHideFrom(builder); addHideFrom(builder);
}, },
@@ -319,3 +327,23 @@ export function addLegendOptions<T extends OptionsWithLegend>(builder: PanelOpti
showIf: (currentConfig) => currentConfig.legend.displayMode !== LegendDisplayMode.Hidden, 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,
});
}
@@ -48,6 +48,25 @@ describe('Graph Migrations', () => {
panel.options = graphPanelChangedHandler(panel, 'graph', old); panel.options = graphPanelChangedHandler(panel, 'graph', old);
expect(panel).toMatchSnapshot(); 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 = { const stairscase = {
@@ -409,3 +428,200 @@ const legend = {
timeShift: null, timeShift: null,
datasource: 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,
};
@@ -10,19 +10,22 @@ import {
NullValueMode, NullValueMode,
PanelModel, PanelModel,
} from '@grafana/data'; } from '@grafana/data';
import { GraphFieldConfig, LegendDisplayMode } from '@grafana/ui';
import { import {
GraphGradientMode,
AxisPlacement, AxisPlacement,
DrawStyle, DrawStyle,
GraphFieldConfig,
GraphGradientMode,
LegendDisplayMode,
LineInterpolation, LineInterpolation,
LineStyle, LineStyle,
PointVisibility, PointVisibility,
} from '@grafana/ui/src/components/uPlot/config'; StackingMode,
} from '@grafana/ui';
import { Options } from './types'; import { Options } from './types';
import omitBy from 'lodash/omitBy'; import omitBy from 'lodash/omitBy';
import isNil from 'lodash/isNil'; import isNil from 'lodash/isNil';
import { isNumber, isString } from 'lodash'; import { isNumber, isString } from 'lodash';
import { defaultGraphConfig } from './config';
/** /**
* This is called when the panel changes from another panel * This is called when the panel changes from another panel
@@ -210,6 +213,12 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
break; break;
} }
break; break;
case 'stack':
rule.properties.push({
id: 'custom.stacking',
value: { mode: StackingMode.Normal, group: v },
});
break;
default: default:
console.log('Ignore override migration:', seriesOverride.alias, p, v); 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 graph.fillOpacity = 100; // bars were always
} }
if (angular.stack) {
graph.stacking = {
mode: StackingMode.Normal,
group: defaultGraphConfig.stacking!.group,
};
}
y1.custom = omitBy(graph, isNil); y1.custom = omitBy(graph, isNil);
y1.nullValueMode = angular.nullPointMode as NullValueMode; y1.nullValueMode = angular.nullPointMode as NullValueMode;
const options: Options = { const options: Options = {
graph: {},
legend: { legend: {
displayMode: LegendDisplayMode.List, displayMode: LegendDisplayMode.List,
placement: 'bottom', placement: 'bottom',
@@ -23,5 +23,6 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(TimeSeriesPanel
], ],
}, },
}); });
addLegendOptions(builder); addLegendOptions(builder);
}); });
@@ -1,14 +1,9 @@
import { VizLegendOptions, GraphTooltipOptions } from '@grafana/ui'; import { VizLegendOptions, GraphTooltipOptions } from '@grafana/ui';
export interface GraphOptions {
// nothing for now
}
export interface OptionsWithLegend { export interface OptionsWithLegend {
legend: VizLegendOptions; legend: VizLegendOptions;
} }
export interface Options extends OptionsWithLegend { export interface Options extends OptionsWithLegend {
graph: GraphOptions;
tooltipOptions: GraphTooltipOptions; tooltipOptions: GraphTooltipOptions;
} }
@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from 'react'; 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 { PanelProps } from '@grafana/data';
import { Options } from './types'; import { Options } from './types';
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory'; import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory';