From 7dd079b72e38e9852c36409439dc5d143c2e6e09 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 18 Dec 2025 16:02:14 -0500 Subject: [PATCH] Gauge: Add guide dots for rounded bars to help with accuracy, update color logic for more consistent gradients --- .../v0alpha1.gauge_tests_new.v42.json | 171 ++------- .../v0alpha1.gauge_tests_old_to_new.v42.json | 2 - .../v0alpha1.gauge_tests_new.v42.v1beta1.json | 221 +++--------- ...v0alpha1.gauge_tests_new.v42.v2alpha1.json | 247 +++---------- .../v0alpha1.gauge_tests_new.v42.v2beta1.json | 250 +++---------- ...a1.gauge_tests_old_to_new.v42.v1beta1.json | 6 +- ...1.gauge_tests_old_to_new.v42.v2alpha1.json | 6 +- ...a1.gauge_tests_old_to_new.v42.v2beta1.json | 6 +- .../panel-gauge/gauge_tests_new.v42.json | 336 ++++++------------ .../gauge_tests_old_to_new.v42.json | 6 +- .../panel-gauge/gauge_tests_new.json | 335 ++++++----------- .../panel-gauge/gauge_tests_old_to_new.json | 2 - .../panelcfg/x/NewGaugePanelCfg_types.gen.ts | 8 +- .../components/RadialGauge/RadialArcPath.tsx | 185 +++++++--- .../src/components/RadialGauge/RadialBar.tsx | 119 +++---- .../RadialGauge/RadialBarSegmented.tsx | 164 +++------ .../RadialGauge/RadialColorDefs.tsx | 141 -------- .../RadialGauge/RadialGauge.story.tsx | 128 +++---- .../RadialGauge/RadialGauge.test.tsx | 25 +- .../components/RadialGauge/RadialGauge.tsx | 69 ++-- .../RadialGauge/RadialScaleLabels.tsx | 119 +++---- .../RadialGauge/RadialSparkline.tsx | 99 ++++-- .../src/components/RadialGauge/RadialText.tsx | 222 ++++++------ .../components/RadialGauge/ThresholdsBar.tsx | 42 +-- .../__snapshots__/colors.test.ts.snap | 144 ++++++++ .../__snapshots__/utils.test.ts.snap | 17 + .../src/components/RadialGauge/colors.test.ts | 257 ++++++++++++++ .../src/components/RadialGauge/colors.ts | 179 ++++++++++ .../src/components/RadialGauge/effects.tsx | 89 +++-- .../src/components/RadialGauge/types.ts | 25 ++ .../src/components/RadialGauge/utils.test.ts | 197 +++++++++- .../src/components/RadialGauge/utils.ts | 171 +++++++-- .../plugins/panel/radialbar/EffectsEditor.tsx | 11 - .../panel/radialbar/RadialBarPanel.tsx | 6 +- public/app/plugins/panel/radialbar/module.tsx | 30 ++ .../app/plugins/panel/radialbar/panelcfg.cue | 6 +- .../plugins/panel/radialbar/panelcfg.gen.ts | 8 +- .../plugins/panel/radialbar/suggestions.ts | 13 - public/locales/en-US/grafana.json | 19 +- 39 files changed, 2077 insertions(+), 2004 deletions(-) delete mode 100644 packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx create mode 100644 packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap create mode 100644 packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap create mode 100644 packages/grafana-ui/src/components/RadialGauge/colors.test.ts create mode 100644 packages/grafana-ui/src/components/RadialGauge/colors.ts create mode 100644 packages/grafana-ui/src/components/RadialGauge/types.ts diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json index 7c7c479199d..62648d7a4aa 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json @@ -71,12 +71,11 @@ "id": 1, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "rounded": true, - "spotlight": false, "gradient": false }, "orientation": "auto", @@ -150,12 +149,11 @@ "id": 4, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "rounded": true, - "spotlight": false, "gradient": false }, "orientation": "auto", @@ -229,12 +227,11 @@ "id": 3, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": false, "gradient": false }, "orientation": "auto", @@ -271,85 +268,6 @@ "title": "Center and bar glow", "type": "radialbar" }, - { - "datasource": { - "type": "grafana-testdata-datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 4, - "x": 12, - "y": 1 - }, - "id": 5, - "maxDataPoints": 20, - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "rounded": true, - "spotlight": true, - "gradient": false - }, - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "pluginVersion": "13.0.0-pre", - "targets": [ - { - "alias": "1", - "datasource": { - "type": "grafana-testdata-datasource" - }, - "max": 100, - "min": 1, - "noise": 22, - "refId": "A", - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - ], - "title": "Spotlight", - "type": "radialbar" - }, { "datasource": { "type": "grafana-testdata-datasource" @@ -391,10 +309,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -470,10 +387,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -549,10 +465,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -641,10 +556,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -720,10 +634,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -799,10 +712,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -878,10 +790,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -974,10 +885,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1053,10 +963,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1132,10 +1041,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1211,10 +1119,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1290,10 +1197,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1386,10 +1292,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1469,10 +1374,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1552,10 +1456,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1641,13 +1544,13 @@ "options": { "barWidth": 12, "barWidthFactor": 0.4, + "barShape": "rounded", "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1662,8 +1565,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1730,10 +1632,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1748,8 +1649,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1830,10 +1730,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1848,8 +1747,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1917,9 +1815,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "sparkline": false, - "spotlight": true, "gradient": true }, "glow": "both", @@ -1934,10 +1829,10 @@ "segmentCount": 12, "segmentSpacing": 0.3, "shape": "circle", + "barShape": "rounded", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2004,10 +1899,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2022,8 +1916,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2090,10 +1983,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2108,8 +2000,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json index a3de6df336a..dda0fbcd432 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json @@ -955,8 +955,6 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, "orientation": "auto", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json index 2eeb3040e6d..e04d448a5b8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json @@ -77,13 +77,12 @@ "id": 1, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -156,13 +155,12 @@ "id": 4, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -235,13 +233,12 @@ "id": 3, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -277,85 +274,6 @@ "title": "Center and bar glow", "type": "radialbar" }, - { - "datasource": { - "type": "grafana-testdata-datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 4, - "x": 12, - "y": 1 - }, - "id": 5, - "maxDataPoints": 20, - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true - }, - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "pluginVersion": "13.0.0-pre", - "targets": [ - { - "alias": "1", - "datasource": { - "type": "grafana-testdata-datasource" - }, - "max": 100, - "min": 1, - "noise": 22, - "refId": "A", - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - ], - "title": "Spotlight", - "type": "radialbar" - }, { "datasource": { "type": "grafana-testdata-datasource" @@ -393,13 +311,12 @@ "id": 8, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -472,13 +389,12 @@ "id": 22, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -551,13 +467,12 @@ "id": 23, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -643,13 +558,12 @@ "id": 18, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.1, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -722,13 +636,12 @@ "id": 19, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.32, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -801,13 +714,12 @@ "id": 20, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.57, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -880,13 +792,12 @@ "id": 21, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.8, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -976,13 +887,12 @@ "id": 25, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1055,13 +965,12 @@ "id": 26, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1134,13 +1043,12 @@ "id": 29, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1213,13 +1121,12 @@ "id": 30, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1292,13 +1199,12 @@ "id": 28, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1388,13 +1294,12 @@ "id": 32, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1471,13 +1376,12 @@ "id": 34, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1554,13 +1458,12 @@ "id": 33, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1645,15 +1548,15 @@ "id": 9, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1668,8 +1571,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1731,14 +1633,13 @@ "id": 11, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -1754,8 +1655,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1831,14 +1731,13 @@ "id": 13, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -1854,8 +1753,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1918,15 +1816,13 @@ "id": 14, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -1942,8 +1838,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2005,14 +1900,13 @@ "id": 15, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -2028,8 +1922,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2091,14 +1984,13 @@ "id": 16, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -2114,8 +2006,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2160,4 +2051,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json index 4aecf4e0d9c..0e6e3e13da5 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json @@ -73,13 +73,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -165,14 +164,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -188,8 +186,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "fieldConfig": { "defaults": { @@ -262,14 +259,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -285,8 +281,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -360,15 +355,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -384,8 +377,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -459,14 +451,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -482,8 +473,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -556,14 +546,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -579,8 +568,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -653,13 +641,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.1, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -745,13 +732,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.32, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -837,13 +823,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.57, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -929,13 +914,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.8, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1021,13 +1005,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1113,13 +1096,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1201,13 +1183,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1293,13 +1274,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1385,13 +1365,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1477,13 +1456,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1573,13 +1551,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1661,13 +1638,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1753,13 +1729,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1849,13 +1824,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1945,13 +1919,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -2045,105 +2018,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false - }, - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "fieldConfig": { - "defaults": { - "min": 0, - "max": 100, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "value": 0, - "color": "green" - }, - { - "value": 80, - "color": "red" - } - ] - }, - "color": { - "mode": "thresholds" - } - }, - "overrides": [] - } - } - } - } - }, - "panel-5": { - "kind": "Panel", - "spec": { - "id": 5, - "title": "Spotlight", - "description": "", - "links": [], - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "grafana-testdata-datasource", - "spec": { - "alias": "1", - "max": 100, - "min": 1, - "noise": 22, - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - }, - "refId": "A", - "hidden": false - } - } - ], - "transformations": [], - "queryOptions": { - "maxDataPoints": 20 - } - } - }, - "vizConfig": { - "kind": "radialbar", - "spec": { - "pluginVersion": "13.0.0-pre", - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -2229,13 +2109,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -2321,15 +2200,15 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2344,8 +2223,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -2429,19 +2307,6 @@ } } }, - { - "kind": "GridLayoutItem", - "spec": { - "x": 12, - "y": 0, - "width": 4, - "height": 6, - "element": { - "kind": "ElementReference", - "name": "panel-5" - } - } - }, { "kind": "GridLayoutItem", "spec": { @@ -2826,4 +2691,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json index 6b567f19b5e..ad2b8ca0385 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json @@ -77,13 +77,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -172,14 +171,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -195,8 +193,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "fieldConfig": { "defaults": { @@ -272,14 +269,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -295,8 +291,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -373,15 +368,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -397,8 +390,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -475,14 +467,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -498,8 +489,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -575,14 +565,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -598,8 +587,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -675,13 +663,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.1, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -770,13 +757,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.32, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -865,13 +851,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.57, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -960,13 +945,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.8, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1055,13 +1039,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1150,13 +1133,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1241,13 +1223,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1336,13 +1317,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1431,13 +1411,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1526,13 +1505,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1625,13 +1603,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1716,13 +1693,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1811,13 +1787,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1910,13 +1885,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -2009,13 +1983,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -2112,108 +2085,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false - }, - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "fieldConfig": { - "defaults": { - "min": 0, - "max": 100, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "value": 0, - "color": "green" - }, - { - "value": 80, - "color": "red" - } - ] - }, - "color": { - "mode": "thresholds" - } - }, - "overrides": [] - } - } - } - } - }, - "panel-5": { - "kind": "Panel", - "spec": { - "id": 5, - "title": "Spotlight", - "description": "", - "links": [], - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "grafana-testdata-datasource", - "version": "v0", - "spec": { - "alias": "1", - "max": 100, - "min": 1, - "noise": 22, - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - }, - "refId": "A", - "hidden": false - } - } - ], - "transformations": [], - "queryOptions": { - "maxDataPoints": 20 - } - } - }, - "vizConfig": { - "kind": "VizConfig", - "group": "radialbar", - "version": "13.0.0-pre", - "spec": { - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -2302,13 +2179,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -2397,15 +2273,15 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2420,8 +2296,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -2505,19 +2380,6 @@ } } }, - { - "kind": "GridLayoutItem", - "spec": { - "x": 12, - "y": 0, - "width": 4, - "height": 6, - "element": { - "kind": "ElementReference", - "name": "panel-5" - } - } - }, { "kind": "GridLayoutItem", "spec": { @@ -2902,4 +2764,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json index 959f0193ad6..1d9f7e56513 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json @@ -961,9 +961,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1175,4 +1173,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json index 66b29e88d13..7b3f601b5cf 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json @@ -864,9 +864,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1620,4 +1618,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json index b870d0a91ad..534e7a1600c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json @@ -901,9 +901,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1672,4 +1670,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json index a89d8744f39..cb130445efc 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json @@ -75,10 +75,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -154,10 +153,9 @@ "effects": { "barGlow": false, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -233,10 +231,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -305,85 +302,6 @@ "x": 12, "y": 1 }, - "id": 5, - "maxDataPoints": 20, - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true - }, - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "pluginVersion": "13.0.0-pre", - "targets": [ - { - "alias": "1", - "datasource": { - "type": "grafana-testdata-datasource" - }, - "max": 100, - "min": 1, - "noise": 22, - "refId": "A", - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - ], - "title": "Spotlight", - "type": "radialbar" - }, - { - "datasource": { - "type": "grafana-testdata-datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 4, - "x": 16, - "y": 1 - }, "id": 8, "maxDataPoints": 20, "options": { @@ -391,10 +309,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -460,8 +377,8 @@ "gridPos": { "h": 6, "w": 4, - "x": 0, - "y": 7 + "x": 16, + "y": 1 }, "id": 22, "maxDataPoints": 20, @@ -470,10 +387,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -539,8 +455,8 @@ "gridPos": { "h": 6, "w": 4, - "x": 4, - "y": 7 + "x": 20, + "y": 1 }, "id": 23, "maxDataPoints": 20, @@ -549,10 +465,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -593,7 +508,7 @@ "h": 1, "w": 24, "x": 0, - "y": 13 + "y": 7 }, "id": 17, "panels": [], @@ -630,9 +545,9 @@ }, "gridPos": { "h": 6, - "w": 5, + "w": 4, "x": 0, - "y": 14 + "y": 8 }, "id": 18, "maxDataPoints": 20, @@ -641,10 +556,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -709,9 +623,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 5, - "y": 14 + "w": 4, + "x": 4, + "y": 8 }, "id": 19, "maxDataPoints": 20, @@ -720,10 +634,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -788,9 +701,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 10, - "y": 14 + "w": 4, + "x": 8, + "y": 8 }, "id": 20, "maxDataPoints": 20, @@ -799,10 +712,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -867,9 +779,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 15, - "y": 14 + "w": 4, + "x": 12, + "y": 8 }, "id": 21, "maxDataPoints": 20, @@ -878,10 +790,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -922,7 +833,7 @@ "h": 1, "w": 24, "x": 0, - "y": 20 + "y": 14 }, "id": 24, "panels": [], @@ -963,9 +874,9 @@ }, "gridPos": { "h": 6, - "w": 6, + "w": 4, "x": 0, - "y": 21 + "y": 15 }, "id": 25, "maxDataPoints": 20, @@ -974,10 +885,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1042,9 +952,9 @@ }, "gridPos": { "h": 6, - "w": 6, - "x": 6, - "y": 21 + "w": 4, + "x": 4, + "y": 15 }, "id": 26, "maxDataPoints": 20, @@ -1053,10 +963,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1121,9 +1030,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 12, - "y": 21 + "w": 4, + "x": 8, + "y": 15 }, "id": 29, "maxDataPoints": 20, @@ -1132,10 +1041,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1199,10 +1107,10 @@ "overrides": [] }, "gridPos": { - "h": 7, - "w": 6, - "x": 0, - "y": 27 + "h": 6, + "w": 4, + "x": 12, + "y": 15 }, "id": 30, "maxDataPoints": 20, @@ -1211,10 +1119,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1278,10 +1185,10 @@ "overrides": [] }, "gridPos": { - "h": 7, - "w": 6, - "x": 6, - "y": 27 + "h": 6, + "w": 4, + "x": 16, + "y": 15 }, "id": 28, "maxDataPoints": 20, @@ -1290,10 +1197,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1330,7 +1236,7 @@ "h": 1, "w": 24, "x": 0, - "y": 34 + "y": 21 }, "id": 31, "panels": [], @@ -1377,7 +1283,7 @@ "h": 10, "w": 7, "x": 0, - "y": 35 + "y": 22 }, "id": 32, "maxDataPoints": 20, @@ -1386,10 +1292,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1460,7 +1365,7 @@ "h": 10, "w": 7, "x": 7, - "y": 35 + "y": 22 }, "id": 34, "maxDataPoints": 20, @@ -1469,10 +1374,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1543,7 +1447,7 @@ "h": 10, "w": 6, "x": 14, - "y": 35 + "y": 22 }, "id": 33, "maxDataPoints": 20, @@ -1552,10 +1456,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1592,7 +1495,7 @@ "h": 1, "w": 24, "x": 0, - "y": 45 + "y": 32 }, "id": 6, "panels": [], @@ -1633,20 +1536,20 @@ "h": 6, "w": 24, "x": 0, - "y": 46 + "y": 33 }, "id": 9, "maxDataPoints": 20, "options": { "barWidth": 12, "barWidthFactor": 0.4, + "barShape": "rounded", "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1661,8 +1564,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1717,7 +1619,7 @@ "h": 6, "w": 24, "x": 0, - "y": 52 + "y": 39 }, "id": 11, "maxDataPoints": 20, @@ -1727,10 +1629,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1745,8 +1646,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1773,7 +1673,7 @@ "h": 1, "w": 24, "x": 0, - "y": 58 + "y": 45 }, "id": 12, "panels": [], @@ -1815,7 +1715,7 @@ "h": 7, "w": 4, "x": 0, - "y": 59 + "y": 46 }, "id": 13, "maxDataPoints": 20, @@ -1825,10 +1725,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1843,8 +1742,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1900,7 +1798,7 @@ "h": 7, "w": 5, "x": 4, - "y": 59 + "y": 46 }, "id": 14, "maxDataPoints": 20, @@ -1910,10 +1808,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1928,8 +1825,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1984,7 +1880,7 @@ "h": 7, "w": 5, "x": 9, - "y": 59 + "y": 46 }, "id": 15, "maxDataPoints": 20, @@ -1994,10 +1890,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2012,8 +1907,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2068,7 +1962,7 @@ "h": 7, "w": 6, "x": 14, - "y": 59 + "y": 46 }, "id": 16, "maxDataPoints": 20, @@ -2078,10 +1972,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2096,8 +1989,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2124,7 +2016,7 @@ "h": 1, "w": 24, "x": 0, - "y": 66 + "y": 53 }, "id": 35, "panels": [], @@ -2155,10 +2047,10 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 6, + "h": 5, + "w": 12, "x": 0, - "y": 67 + "y": 54 }, "id": 36, "options": { @@ -2166,10 +2058,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2223,10 +2114,10 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 6, - "x": 6, - "y": 67 + "h": 5, + "w": 12, + "x": 12, + "y": 54 }, "id": 37, "options": { @@ -2234,10 +2125,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2279,4 +2169,4 @@ "title": "Panel tests - Gauge (new)", "uid": "panel-tests-gauge-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json index 4a5ac97a6b5..dda0fbcd432 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json @@ -955,9 +955,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1162,4 +1160,4 @@ "title": "Panel tests - Old gauge to new", "uid": "panel-tests-old-gauge-to-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index b3c47c9aa7a..65cc0b0a5ae 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -71,13 +71,12 @@ "id": 1, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -148,13 +147,12 @@ "id": 4, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -225,13 +223,12 @@ "id": 3, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -299,93 +296,15 @@ "x": 12, "y": 1 }, - "id": 5, - "maxDataPoints": 20, - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true - }, - "orientation": "auto", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "pluginVersion": "13.0.0-pre", - "targets": [ - { - "alias": "1", - "datasource": { - "type": "grafana-testdata-datasource" - }, - "max": 100, - "min": 1, - "noise": 22, - "refId": "A", - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - ], - "title": "Spotlight", - "type": "radialbar" - }, - { - "datasource": { - "type": "grafana-testdata-datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 4, - "x": 16, - "y": 1 - }, "id": 8, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -450,19 +369,18 @@ "gridPos": { "h": 6, "w": 4, - "x": 0, - "y": 7 + "x": 16, + "y": 1 }, "id": 22, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -527,19 +445,18 @@ "gridPos": { "h": 6, "w": 4, - "x": 4, - "y": 7 + "x": 20, + "y": 1 }, "id": 23, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -579,7 +496,7 @@ "h": 1, "w": 24, "x": 0, - "y": 13 + "y": 7 }, "id": 17, "panels": [], @@ -616,20 +533,19 @@ }, "gridPos": { "h": 6, - "w": 5, + "w": 4, "x": 0, - "y": 14 + "y": 8 }, "id": 18, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.1, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -693,20 +609,19 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 5, - "y": 14 + "w": 4, + "x": 4, + "y": 8 }, "id": 19, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.32, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -770,20 +685,19 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 10, - "y": 14 + "w": 4, + "x": 8, + "y": 8 }, "id": 20, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.57, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -847,20 +761,19 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 15, - "y": 14 + "w": 4, + "x": 12, + "y": 8 }, "id": 21, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.8, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -900,7 +813,7 @@ "h": 1, "w": 24, "x": 0, - "y": 20 + "y": 14 }, "id": 24, "panels": [], @@ -941,20 +854,19 @@ }, "gridPos": { "h": 6, - "w": 6, + "w": 4, "x": 0, - "y": 21 + "y": 15 }, "id": 25, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1018,20 +930,19 @@ }, "gridPos": { "h": 6, - "w": 6, - "x": 6, - "y": 21 + "w": 4, + "x": 4, + "y": 15 }, "id": 26, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1095,20 +1006,19 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 12, - "y": 21 + "w": 4, + "x": 8, + "y": 15 }, "id": 29, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1171,21 +1081,20 @@ "overrides": [] }, "gridPos": { - "h": 7, - "w": 6, - "x": 0, - "y": 27 + "h": 6, + "w": 4, + "x": 12, + "y": 15 }, "id": 30, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1248,21 +1157,20 @@ "overrides": [] }, "gridPos": { - "h": 7, - "w": 6, - "x": 6, - "y": 27 + "h": 6, + "w": 4, + "x": 16, + "y": 15 }, "id": 28, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1298,7 +1206,7 @@ "h": 1, "w": 24, "x": 0, - "y": 34 + "y": 21 }, "id": 31, "panels": [], @@ -1345,18 +1253,17 @@ "h": 10, "w": 7, "x": 0, - "y": 35 + "y": 22 }, "id": 32, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1426,18 +1333,17 @@ "h": 10, "w": 7, "x": 7, - "y": 35 + "y": 22 }, "id": 34, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1507,18 +1413,17 @@ "h": 10, "w": 6, "x": 14, - "y": 35 + "y": 22 }, "id": 33, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1554,7 +1459,7 @@ "h": 1, "w": 24, "x": 0, - "y": 45 + "y": 32 }, "id": 6, "panels": [], @@ -1595,20 +1500,20 @@ "h": 6, "w": 24, "x": 0, - "y": 46 + "y": 33 }, "id": 9, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1621,8 +1526,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1677,19 +1581,18 @@ "h": 6, "w": 24, "x": 0, - "y": 52 + "y": 39 }, "id": 11, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -1703,8 +1606,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1731,7 +1633,7 @@ "h": 1, "w": 24, "x": 0, - "y": 58 + "y": 45 }, "id": 12, "panels": [], @@ -1773,19 +1675,18 @@ "h": 7, "w": 4, "x": 0, - "y": 59 + "y": 46 }, "id": 13, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -1799,8 +1700,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1856,19 +1756,18 @@ "h": 7, "w": 5, "x": 4, - "y": 59 + "y": 46 }, "id": 14, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -1882,8 +1781,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1938,19 +1836,18 @@ "h": 7, "w": 5, "x": 9, - "y": 59 + "y": 46 }, "id": 15, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -1964,8 +1861,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2020,19 +1916,18 @@ "h": 7, "w": 6, "x": 14, - "y": 59 + "y": 46 }, "id": 16, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -2046,8 +1941,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2074,7 +1968,7 @@ "h": 1, "w": 24, "x": 0, - "y": 66 + "y": 53 }, "id": 35, "panels": [], @@ -2105,20 +1999,19 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 6, + "h": 5, + "w": 12, "x": 0, - "y": 67 + "y": 54 }, "id": 36, "options": { + "barShape": "flat", "barWidthFactor": 0.5, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -2171,20 +2064,19 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 6, - "x": 6, - "y": 67 + "h": 5, + "w": 12, + "x": 12, + "y": 54 }, "id": 37, "options": { + "barShape": "flat", "barWidthFactor": 0.5, "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -2224,5 +2116,6 @@ "timezone": "browser", "title": "Panel tests - Gauge (new)", "uid": "panel-tests-gauge-new", - "version": 9 + "version": 22, + "weekStart": "" } diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json index b071ddff802..bee1ece914e 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json @@ -956,8 +956,6 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false } } diff --git a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts index aedc041cf71..7c29eb7b463 100644 --- a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts @@ -16,21 +16,19 @@ export interface GaugePanelEffects { barGlow?: boolean; centerGlow?: boolean; gradient?: boolean; - rounded?: boolean; - spotlight?: boolean; } export const defaultGaugePanelEffects: Partial = { barGlow: false, centerGlow: false, gradient: true, - rounded: false, - spotlight: false, }; export interface Options extends common.SingleStatBaseOptions { + barShape: ('flat' | 'rounded'); barWidthFactor: number; effects: GaugePanelEffects; + endpointMarker?: ('point' | 'glow' | 'none'); segmentCount: number; segmentSpacing: number; shape: ('circle' | 'gauge'); @@ -40,8 +38,10 @@ export interface Options extends common.SingleStatBaseOptions { } export const defaultOptions: Partial = { + barShape: 'flat', barWidthFactor: 0.5, effects: {}, + endpointMarker: 'point', segmentCount: 1, segmentSpacing: 0.3, shape: 'gauge', diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index f54f3cd6f22..f59614acd53 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,52 +1,149 @@ -import { GaugeDimensions, toRad } from './utils'; +import { useId, memo, HTMLAttributes, ReactNode } from 'react'; -export interface RadialArcPathProps { - startAngle: number; - dimensions: GaugeDimensions; - color: string; - glowFilter?: string; +import { FieldDisplay } from '@grafana/data'; + +import { getBarEndcapColors, getGradientCss, getEndpointMarkerColors } from './colors'; +import { RadialShape, RadialGaugeDimensions, GradientStop } from './types'; +import { drawRadialArcPath, toRad } from './utils'; + +export interface RadialArcPathPropsBase { arcLengthDeg: number; + barEndcaps?: boolean; + dimensions: RadialGaugeDimensions; + fieldDisplay: FieldDisplay; roundedBars?: boolean; + shape: RadialShape; + endpointMarker?: 'point' | 'glow'; + startAngle: number; + glowFilter?: string; + endpointMarkerGlowFilter?: string; } -export function RadialArcPath({ - startAngle: angle, - dimensions, - color, - glowFilter, - arcLengthDeg, - roundedBars, -}: RadialArcPathProps) { - const { radius, centerX, centerY, barWidth } = dimensions; +interface RadialArcPathPropsWithColor extends RadialArcPathPropsBase { + color: string; +} - if (arcLengthDeg === 360) { - // For some reason a 100% full arc cannot be rendered - arcLengthDeg = 359.99; +interface RadialArcPathPropsWithGradient extends RadialArcPathPropsBase { + gradient: GradientStop[]; +} + +type RadialArcPathProps = RadialArcPathPropsWithColor | RadialArcPathPropsWithGradient; + +const ENDPOINT_MARKER_MIN_ANGLE = 10; +const DOT_OPACITY = 0.5; +const DOT_RADIUS_FACTOR = 0.4; +const MAX_DOT_RADIUS = 8; + +export const RadialArcPath = memo( + ({ + arcLengthDeg, + dimensions, + fieldDisplay, + roundedBars, + shape, + endpointMarker, + barEndcaps, + startAngle: angle, + glowFilter, + endpointMarkerGlowFilter, + ...rest + }: RadialArcPathProps) => { + const id = useId(); + + const bgDivStyle: HTMLAttributes['style'] = { width: '100%', height: '100%' }; + if ('color' in rest) { + bgDivStyle.backgroundColor = rest.color; + } else { + bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape); + } + + const { radius, centerX, centerY, barWidth } = dimensions; + + const path = drawRadialArcPath(angle, arcLengthDeg, dimensions, roundedBars); + + const startRadians = toRad(angle); + const endRadians = toRad(angle + arcLengthDeg); + + const xStart = centerX + radius * Math.cos(startRadians); + const yStart = centerY + radius * Math.sin(startRadians); + const xEnd = centerX + radius * Math.cos(endRadians); + const yEnd = centerY + radius * Math.sin(endRadians); + + const dotRadius = + endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2; + + let barEndcapColors: [string, string] | undefined; + let endpointMarks: ReactNode = null; + if ('gradient' in rest) { + if (endpointMarker && (rest.gradient?.length ?? 0) > 0) { + switch (endpointMarker) { + case 'point': + const [pointColorStart, pointColorEnd] = getEndpointMarkerColors( + rest.gradient!, + fieldDisplay.display.percent + ); + endpointMarks = ( + <> + {arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && ( + + )} + + + ); + break; + case 'glow': + const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE); + const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle); + const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle); + endpointMarks = + arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE ? ( + + ) : null; + break; + default: + break; + } + } + + if (barEndcaps) { + barEndcapColors = getBarEndcapColors(rest.gradient, fieldDisplay.display.percent); + } + } + + return ( + <> + {/* FIXME: optimize this by only using clippath + foreign obj for gradients */} + + + + + + +
+ + {barEndcapColors?.[0] && } + {barEndcapColors?.[1] && ( + + )} + + + {endpointMarks} + + ); } +); - const startRadians = toRad(angle); - const endRadians = toRad(angle + arcLengthDeg); - - let x1 = centerX + radius * Math.cos(startRadians); - let y1 = centerY + radius * Math.sin(startRadians); - let x2 = centerX + radius * Math.cos(endRadians); - let y2 = centerY + radius * Math.sin(endRadians); - - const largeArc = arcLengthDeg > 180 ? 1 : 0; - - const path = ['M', x1, y1, 'A', radius, radius, 0, largeArc, 1, x2, y2].join(' '); - - return ( - - ); -} +RadialArcPath.displayName = 'RadialArcPath'; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index 2bacc3af3e8..719ec52c625 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -1,97 +1,64 @@ -import { GrafanaTheme2 } from '@grafana/data'; +import { FALLBACK_COLOR, FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialColorDefs } from './RadialColorDefs'; -import { GaugeDimensions, toRad } from './utils'; +import { RadialShape, RadialGaugeDimensions, GradientStop } from './types'; export interface RadialBarProps { - dimensions: GaugeDimensions; - colorDefs: RadialColorDefs; - angleRange: number; angle: number; - startAngle: number; + angleRange: number; + dimensions: RadialGaugeDimensions; + fieldDisplay: FieldDisplay; + gradient?: GradientStop[]; roundedBars?: boolean; - spotlightStroke: string; + endpointMarker?: 'point' | 'glow'; + shape: RadialShape; + startAngle: number; glowFilter?: string; + endpointMarkerGlowFilter?: string; } export function RadialBar({ - dimensions, - colorDefs, - angleRange, angle, - startAngle, + angleRange, + dimensions, + fieldDisplay, + gradient, roundedBars, - spotlightStroke, + endpointMarker, + shape, + startAngle, glowFilter, + endpointMarkerGlowFilter, }: RadialBarProps) { const theme = useTheme2(); - + const colorProps = gradient ? { gradient } : { color: fieldDisplay.display.color ?? FALLBACK_COLOR }; return ( <> - - {/** Track */} - - {/** The colored bar */} - - {spotlightStroke && angle > 8 && ( - - )} - - {colorDefs.getDefs()} + {/** Track */} + + {/** The colored bar */} + ); } - -interface SpotlightEffectProps { - dimensions: GaugeDimensions; - angle: number; - glowFilter?: string; - spotlightStroke: string; - theme: GrafanaTheme2; - roundedBars?: boolean; -} - -function SpotlightSquareEffect({ dimensions, angle, glowFilter, spotlightStroke, roundedBars }: SpotlightEffectProps) { - const { radius, centerX, centerY, barWidth } = dimensions; - - const angleRadian = toRad(angle); - const x1 = centerX + radius * Math.cos(angleRadian - 0.2); - const y1 = centerY + radius * Math.sin(angleRadian - 0.2); - const x2 = centerX + radius * Math.cos(angleRadian); - const y2 = centerY + radius * Math.sin(angleRadian); - - const path = ['M', x1, y1, 'A', radius, radius, 0, 0, 1, x2, y2].join(' '); - - return ( - - ); -} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx index dbc93d2da7f..b51cb4ce2f1 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx @@ -1,126 +1,74 @@ -import { FieldDisplay } from '@grafana/data'; +import { memo } from 'react'; + +import { FALLBACK_COLOR, FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialColorDefs } from './RadialColorDefs'; -import { GaugeDimensions } from './utils'; +import { RadialShape, RadialGaugeDimensions, GradientStop } from './types'; +import { + getAngleBetweenSegments, + getFieldConfigMinMax, + getFieldDisplayProcessor, + getOptimalSegmentCount, +} from './utils'; export interface RadialBarSegmentedProps { fieldDisplay: FieldDisplay; - dimensions: GaugeDimensions; - colorDefs: RadialColorDefs; + dimensions: RadialGaugeDimensions; angleRange: number; startAngle: number; glowFilter?: string; segmentCount: number; segmentSpacing: number; + shape: RadialShape; + gradient?: GradientStop[]; } -export function RadialBarSegmented({ - fieldDisplay, - dimensions, - startAngle, - angleRange, - glowFilter, - segmentCount, - segmentSpacing, - colorDefs, -}: RadialBarSegmentedProps) { - const segments: React.ReactNode[] = []; - const theme = useTheme2(); - const segmentCountAdjusted = getOptimalSegmentCount(dimensions, segmentSpacing, segmentCount, angleRange); - const min = fieldDisplay.field.min ?? 0; - const max = fieldDisplay.field.max ?? 100; - const value = fieldDisplay.display.numeric; - const angleBetweenSegments = getAngleBetweenSegments(segmentSpacing, segmentCount, angleRange); - const segmentArcLengthDeg = angleRange / segmentCountAdjusted - angleBetweenSegments; +export const RadialBarSegmented = memo( + ({ + fieldDisplay, + dimensions, + startAngle, + angleRange, + glowFilter, + gradient, + segmentCount, + segmentSpacing, + shape, + }: RadialBarSegmentedProps) => { + const theme = useTheme2(); + const segments: React.ReactNode[] = []; + const segmentCountAdjusted = getOptimalSegmentCount(dimensions, segmentSpacing, segmentCount, angleRange); + const [min, max] = getFieldConfigMinMax(fieldDisplay); + const value = fieldDisplay.display.numeric; + const angleBetweenSegments = getAngleBetweenSegments(segmentSpacing, segmentCount, angleRange); + const segmentArcLengthDeg = angleRange / segmentCountAdjusted - angleBetweenSegments; + const displayProcessor = getFieldDisplayProcessor(fieldDisplay); - for (let i = 0; i < segmentCountAdjusted; i++) { - const angleValue = min + ((max - min) / segmentCountAdjusted) * i; - const angleColor = colorDefs.getSegmentColor(angleValue); - const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01; - const segmentColor = angleValue >= value ? theme.colors.action.hover : angleColor; + for (let i = 0; i < segmentCountAdjusted; i++) { + const angleValue = min + ((max - min) / segmentCountAdjusted) * i; + const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01; + const segmentColor = + angleValue >= value ? theme.colors.border.medium : (displayProcessor(angleValue).color ?? FALLBACK_COLOR); + const colorProps = angleValue < value && gradient ? { gradient } : { color: segmentColor }; - segments.push( - - ); + segments.push( + + ); + } + + return {segments}; } +); - return ( - <> - {segments} - {colorDefs.getDefs()} - - ); -} - -export function getAngleBetweenSegments(segmentSpacing: number, segmentCount: number, range: number) { - // Max spacing is 8 degrees between segments - // Changing this constant could be considered a breaking change - const maxAngleBetweenSegments = Math.max(range / 1.5 / segmentCount, 2); - return segmentSpacing * maxAngleBetweenSegments; -} - -function getOptimalSegmentCount( - dimensions: GaugeDimensions, - segmentSpacing: number, - segmentCount: number, - range: number -) { - const angleBetweenSegments = getAngleBetweenSegments(segmentSpacing, segmentCount, range); - - const innerRadius = dimensions.radius - dimensions.barWidth / 2; - const circumference = Math.PI * innerRadius * 2 * (range / 360); - const maxSegments = Math.floor(circumference / (angleBetweenSegments + 3)); - - return Math.min(maxSegments, segmentCount); -} - -// export function RadialSegmentLine({ -// gaugeId, -// center, -// angle, -// size, -// color, -// barWidth, -// roundedBars, -// glow, -// margin, -// segmentWidth, -// }: RadialSegmentProps) { -// const arcSize = size - barWidth; -// const radius = arcSize / 2 - margin; - -// const angleRad = (Math.PI * (angle - 90)) / 180; -// const lineLength = radius - barWidth; - -// const x1 = center + radius * Math.cos(angleRad); -// const y1 = center + radius * Math.sin(angleRad); -// const x2 = center + lineLength * Math.cos(angleRad); -// const y2 = center + lineLength * Math.sin(angleRad); - -// return ( -// -// ); -// } +RadialBarSegmented.displayName = 'RadialBarSegmented'; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx deleted file mode 100644 index 6bcf4876880..00000000000 --- a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import tinycolor from 'tinycolor2'; - -import { DisplayProcessor, FALLBACK_COLOR, FieldDisplay, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; - -import { RadialGradientMode, RadialShape } from './RadialGauge'; -import { GaugeDimensions } from './utils'; - -export interface RadialColorDefsOptions { - gradient: RadialGradientMode; - fieldDisplay: FieldDisplay; - theme: GrafanaTheme2; - dimensions: GaugeDimensions; - shape: RadialShape; - gaugeId: string; - displayProcessor: DisplayProcessor; -} - -export class RadialColorDefs { - private colorToIds: Record = {}; - private defs: React.ReactNode[] = []; - - constructor(private options: RadialColorDefsOptions) {} - - getSegmentColor(forValue: number): string { - const { displayProcessor } = this.options; - const baseColor = displayProcessor(forValue).color ?? FALLBACK_COLOR; - - return this.getColor(baseColor, true); - } - - getColor(baseColor: string, forSegment?: boolean): string { - const { gradient, dimensions, gaugeId, fieldDisplay, shape, theme } = this.options; - - const id = `value-color-${baseColor}-${gaugeId}`; - - if (this.colorToIds[id]) { - return this.colorToIds[id]; - } - - // If no gradient, just return the base color - if (gradient === 'none') { - this.colorToIds[id] = baseColor; - return baseColor; - } - - const returnColor = (this.colorToIds[id] = `url(#${id})`); - const colorModeId = fieldDisplay.field.color?.mode; - const colorMode = getFieldColorMode(colorModeId); - const valuePercent = fieldDisplay.display.percent ?? 0; - - // Handle continusous color modes first - // If it's a segment color we don't want to do continuous gradients - if (colorMode.isContinuous && colorMode.getColors && !forSegment) { - const colors = colorMode.getColors(theme); - const count = colors.length; - - this.defs.push( - - {colors.map((stopColor, i) => ( - - ))} - - ); - - return returnColor; - } - - // For value based colors we want to stay more true to the specific color - // So a radial gradient that adds a bit of light and shade works best - if (colorMode.isByValue) { - const color1 = tinycolor(baseColor).darken(5); - - this.defs.push( - - - - - - ); - - return returnColor; - } - - // For fixed / palette based color scales we can create a more fun - // hue and light based linear gradient that we rotate/move with the value - - const x2 = shape === 'circle' ? 0 : dimensions.centerX + dimensions.radius; - const y2 = shape === 'circle' ? dimensions.centerY + dimensions.radius : 0; - const color1 = tinycolor(baseColor).spin(-20).darken(5); - const color2 = tinycolor(baseColor).saturate(20).spin(20).brighten(10); - - // this makes it so the gradient is always brightest at the current value - const transform = - shape === 'circle' - ? `rotate(${360 * valuePercent - 180} ${dimensions.centerX} ${dimensions.centerY})` - : `translate(-${dimensions.radius * 2 * (1 - valuePercent)}, 0)`; - - this.defs.push( - - {theme.isDark ? ( - <> - - - - ) : ( - <> - - - - )} - - ); - - return returnColor; - } - - getMainBarColor(): string { - return this.getColor(this.options.fieldDisplay.display.color ?? FALLBACK_COLOR); - } - - getDefs(): React.ReactNode[] { - return this.defs; - } -} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx index c098beda886..b0574ef3f87 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx @@ -13,7 +13,8 @@ import { FieldColorModeId } from '@grafana/schema'; import { useTheme2 } from '../../themes/ThemeContext'; import { Stack } from '../Layout/Stack/Stack'; -import { RadialGauge, RadialGaugeProps, RadialGradientMode, RadialShape, RadialTextMode } from './RadialGauge'; +import { RadialGauge, RadialGaugeProps } from './RadialGauge'; +import { RadialShape, RadialTextMode } from './types'; interface StoryProps extends RadialGaugeProps { value: number; @@ -31,10 +32,27 @@ const meta: Meta = { controls: { exclude: ['theme', 'values', 'vizCount'], }, + a11y: { + config: { + rules: [ + { + id: 'scrollable-region-focusable', + selector: 'body', + enabled: false, + }, + // NOTE: this is necessary due to a false positive with the filered svg glow in one of the examples. + // The color-contrast in this component should be accessible! + { + id: 'color-contrast', + selector: 'text', + enabled: false, + }, + ], + }, + }, }, args: { barWidthFactor: 0.2, - spotlight: false, glowBar: false, glowCenter: false, sparkline: false, @@ -42,7 +60,7 @@ const meta: Meta = { width: 200, height: 200, shape: 'circle', - gradient: 'none', + gradient: false, seriesCount: 1, segmentCount: 0, segmentSpacing: 0.2, @@ -56,14 +74,14 @@ const meta: Meta = { width: { control: { type: 'range', min: 50, max: 600 } }, height: { control: { type: 'range', min: 50, max: 600 } }, value: { control: { type: 'range', min: 0, max: 110 } }, - spotlight: { control: 'boolean' }, roundedBars: { control: 'boolean' }, sparkline: { control: 'boolean' }, thresholdsBar: { control: 'boolean' }, - gradient: { control: { type: 'radio' } }, + gradient: { control: { type: 'boolean' } }, seriesCount: { control: { type: 'range', min: 1, max: 20 } }, segmentCount: { control: { type: 'range', min: 0, max: 100 } }, segmentSpacing: { control: { type: 'range', min: 0, max: 1, step: 0.01 } }, + endpointMarker: { control: { type: 'select' }, options: ['none', 'point', 'glow'] }, colorScheme: { control: { type: 'select' }, options: [ @@ -102,57 +120,17 @@ export const Examples: StoryFn = (args) => {
Bar width
- - - - + + + +
Effects
- - - - + + + +
Shape: Gauge & color scale
@@ -160,14 +138,14 @@ export const Examples: StoryFn = (args) => { value={40} shape="gauge" width={250} - gradient="auto" + gradient colorScheme={FieldColorModeId.ContinuousGrYlRd} glowCenter={true} barWidthFactor={0.6} /> = (args) => { value={args.value ?? 70} color="blue" shape="gauge" - gradient="auto" + gradient sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.2} @@ -194,9 +171,8 @@ export const Examples: StoryFn = (args) => { value={args.value ?? 30} color="green" shape="gauge" - gradient="auto" + gradient sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.8} @@ -206,9 +182,8 @@ export const Examples: StoryFn = (args) => { color="red" shape="gauge" width={250} - gradient="auto" + gradient sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.2} @@ -218,9 +193,8 @@ export const Examples: StoryFn = (args) => { color="red" width={250} shape="gauge" - gradient="auto" + gradient sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.8} @@ -231,7 +205,7 @@ export const Examples: StoryFn = (args) => { = (args) => { = (args) => { = (args) => { = (args) => { value={args.value ?? 80} width={250} colorScheme={FieldColorModeId.ContinuousGrYlRd} - spotlight shape="gauge" - gradient="auto" + gradient glowBar={true} glowCenter={true} segmentCount={40} @@ -285,10 +257,9 @@ export const Examples: StoryFn = (args) => { @@ -296,7 +267,7 @@ export const Examples: StoryFn = (args) => { value={args.value ?? 70} width={250} colorScheme={FieldColorModeId.Thresholds} - gradient="auto" + gradient glowCenter={true} thresholdsBar={true} roundedBars={false} @@ -307,7 +278,7 @@ export const Examples: StoryFn = (args) => { value={args.value ?? 70} width={250} colorScheme={FieldColorModeId.Thresholds} - gradient="auto" + gradient glowCenter={true} thresholdsBar={true} roundedBars={false} @@ -347,14 +318,12 @@ export const Temp: StoryFn = (args) => { shape="gauge" roundedBars={false} barWidthFactor={0.8} - spotlight /> ); }; interface ExampleProps { - gradient?: RadialGradientMode; color?: string; seriesName?: string; value?: number; @@ -363,7 +332,7 @@ interface ExampleProps { max?: number; width?: number; height?: number; - spotlight?: boolean; + gradient?: boolean; glowBar?: boolean; glowCenter?: boolean; barWidthFactor?: number; @@ -376,12 +345,12 @@ interface ExampleProps { roundedBars?: boolean; thresholdsBar?: boolean; colorScheme?: FieldColorModeId; + endpointMarker?: RadialGaugeProps['endpointMarker']; decimals?: number; showScaleLabels?: boolean; } export function RadialGaugeExample({ - gradient = 'none', color, seriesName = 'Server A', value = 70, @@ -390,7 +359,7 @@ export function RadialGaugeExample({ max = 100, width = 200, height = 200, - spotlight = false, + gradient = false, glowBar = false, glowCenter = false, barWidthFactor = 0.4, @@ -403,6 +372,7 @@ export function RadialGaugeExample({ roundedBars = false, thresholdsBar = false, colorScheme = FieldColorModeId.Thresholds, + endpointMarker = 'glow', decimals = 0, showScaleLabels, }: ExampleProps) { @@ -480,7 +450,6 @@ export function RadialGaugeExample({ barWidthFactor={barWidthFactor} gradient={gradient} shape={shape} - spotlight={spotlight} glowBar={glowBar} glowCenter={glowCenter} textMode={textMode} @@ -490,6 +459,7 @@ export function RadialGaugeExample({ roundedBars={roundedBars} thresholdsBar={thresholdsBar} showScaleLabels={showScaleLabels} + endpointMarker={endpointMarker} /> ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx index e2fed36ec3f..783e3b764da 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx @@ -1,13 +1,28 @@ import { render, screen } from '@testing-library/react'; +import { ComponentProps } from 'react'; import { RadialGaugeExample } from './RadialGauge.story'; describe('RadialGauge', () => { - it('should render', () => { - render(); - - expect(screen.getByRole('img')).toBeInTheDocument(); - }); + it.each([ + { description: 'default', props: {} }, + { description: 'gauge shape', props: { shape: 'gauge' } }, + { description: 'with gradient', props: { gradient: true } }, + { description: 'with glow bar', props: { glowBar: true } }, + { description: 'with glow center', props: { glowCenter: true } }, + { description: 'with segments', props: { segmentCount: 5 } }, + { description: 'with rounded bars', props: { roundedBars: true } }, + { description: 'with endpoint marker glow', props: { roundedBars: true, endpointMarker: 'glow' } }, + { description: 'with endpoint marker point', props: { roundedBars: true, endpointMarker: 'point' } }, + { description: 'with thresholds bar', props: { thresholdsBar: true } }, + { description: 'with sparkline', props: { sparkline: true } }, + ] satisfies Array<{ description: string; props?: ComponentProps }>)( + 'should render $description without throwing', + ({ props }) => { + render(); + expect(screen.getByRole('img')).toBeInTheDocument(); + } + ); it('should render threshold labels', () => { render(); diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index fadabf8ec72..1251a364230 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -1,14 +1,7 @@ import { css, cx } from '@emotion/css'; -import { isNumber } from 'lodash'; import { useId } from 'react'; -import { - DisplayValueAlignmentFactors, - FieldDisplay, - getDisplayProcessor, - GrafanaTheme2, - TimeRange, -} from '@grafana/data'; +import { DisplayValueAlignmentFactors, FALLBACK_COLOR, FieldDisplay, GrafanaTheme2, TimeRange } from '@grafana/data'; import { t } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; @@ -16,12 +9,13 @@ import { getFormattedThresholds } from '../Gauge/utils'; import { RadialBar } from './RadialBar'; import { RadialBarSegmented } from './RadialBarSegmented'; -import { RadialColorDefs } from './RadialColorDefs'; import { RadialScaleLabels } from './RadialScaleLabels'; import { RadialSparkline } from './RadialSparkline'; import { RadialText } from './RadialText'; import { ThresholdsBar } from './ThresholdsBar'; +import { buildGradientColors } from './colors'; import { GlowGradient, MiddleCircleGlow, SpotlightGradient } from './effects'; +import { RadialShape, RadialTextMode } from './types'; import { calculateDimensions, getValueAngleForValue } from './utils'; export interface RadialGaugeProps { @@ -32,7 +26,7 @@ export interface RadialGaugeProps { * Circle or gauge (partial circle) */ shape?: RadialShape; - gradient?: RadialGradientMode; + gradient?: boolean; /** * Bar width is always relative to size of the gauge. * But this gives you control over the width relative to size. @@ -40,12 +34,14 @@ export interface RadialGaugeProps { * Defaults to 0.4 **/ barWidthFactor?: number; - /** Adds a white spotlight for the end position */ - spotlight?: boolean; glowBar?: boolean; glowCenter?: boolean; roundedBars?: boolean; thresholdsBar?: boolean; + /** + * Specify if an endpoint marker should be shown at the end of the bar + */ + endpointMarker?: 'point' | 'glow'; /** * Number of segments depends on size of gauge but this * factor 1-10 gives you relative control @@ -75,10 +71,6 @@ export interface RadialGaugeProps { timeRange?: TimeRange; } -export type RadialGradientMode = 'none' | 'auto'; -export type RadialTextMode = 'auto' | 'value_and_name' | 'value' | 'name' | 'none'; -export type RadialShape = 'circle' | 'gauge'; - /** * https://developers.grafana.com/ui/latest/index.html?path=/docs/plugins-radialgauge--docs */ @@ -87,9 +79,8 @@ export function RadialGauge(props: RadialGaugeProps) { width = 256, height = 256, shape = 'circle', - gradient = 'none', + gradient = false, barWidthFactor = 0.4, - spotlight = false, glowBar = false, glowCenter = false, textMode = 'auto', @@ -99,6 +90,7 @@ export function RadialGauge(props: RadialGaugeProps) { roundedBars = true, thresholdsBar = false, showScaleLabels = false, + endpointMarker, onClick, values, } = props; @@ -121,7 +113,8 @@ export function RadialGauge(props: RadialGaugeProps) { for (let barIndex = 0; barIndex < values.length; barIndex++) { const displayValue = values[barIndex]; const { angle, angleRange } = getValueAngleForValue(displayValue, startAngle, endAngle); - const color = displayValue.display.color ?? 'gray'; + const gradientStops = buildGradientColors(gradient, theme, displayValue); + const color = displayValue.display.color ?? FALLBACK_COLOR; const dimensions = calculateDimensions( width, height, @@ -134,20 +127,12 @@ export function RadialGauge(props: RadialGaugeProps) { showScaleLabels ); - const displayProcessor = getFieldDisplayProcessor(displayValue); + // FIXME: I want to move the ids for these filters into a context which the children + // can reference via a hook, rather than passing them down as props const spotlightGradientId = `spotlight-${barIndex}-${gaugeId}`; const glowFilterId = `glow-${gaugeId}`; - const colorDefs = new RadialColorDefs({ - gradient, - fieldDisplay: displayValue, - theme, - dimensions, - shape, - gaugeId, - displayProcessor, - }); - if (spotlight && theme.isDark) { + if (endpointMarker === 'glow') { defs.push( ); } else { @@ -179,13 +165,16 @@ export function RadialGauge(props: RadialGaugeProps) { ); } @@ -245,7 +234,8 @@ export function RadialGauge(props: RadialGaugeProps) { angleRange={angleRange} roundedBars={roundedBars} glowFilter={`url(#${glowFilterId})`} - colorDefs={colorDefs} + shape={shape} + gradient={gradientStops} /> ); } @@ -291,17 +281,6 @@ export function RadialGauge(props: RadialGaugeProps) { ); } -function getFieldDisplayProcessor(displayValue: FieldDisplay) { - if (displayValue.view && isNumber(displayValue.colIndex)) { - const dp = displayValue.view.getFieldDisplayProcessor(displayValue.colIndex); - if (dp) { - return dp; - } - } - - return getDisplayProcessor(); -} - function getStyles(theme: GrafanaTheme2) { return { vizWrapper: css({ diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx index 994b3b35eac..6588150602a 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx @@ -1,87 +1,84 @@ +import { memo } from 'react'; + import { FieldDisplay, GrafanaTheme2, Threshold } from '@grafana/data'; import { t } from '@grafana/i18n'; import { measureText } from '../../utils/measureText'; -import { GaugeDimensions, toCartesian } from './utils'; +import { RadialGaugeDimensions } from './types'; +import { getFieldConfigMinMax, toCartesian } from './utils'; interface RadialScaleLabelsProps { fieldDisplay: FieldDisplay; theme: GrafanaTheme2; thresholds: Threshold[]; - dimensions: GaugeDimensions; + dimensions: RadialGaugeDimensions; startAngle: number; endAngle: number; angleRange: number; } -export function RadialScaleLabels({ - fieldDisplay, - thresholds, - theme, - dimensions, - startAngle, - endAngle, - angleRange, -}: RadialScaleLabelsProps) { - const { centerX, centerY, scaleLabelsFontSize, scaleLabelsRadius } = dimensions; +const LINE_HEIGHT_FACTOR = 1.2; - const fieldConfig = fieldDisplay.field; - const min = fieldConfig.min ?? 0; - const max = fieldConfig.max ?? 100; +export const RadialScaleLabels = memo( + ({ fieldDisplay, thresholds, theme, dimensions, startAngle, endAngle, angleRange }: RadialScaleLabelsProps) => { + const { centerX, centerY, scaleLabelsFontSize, scaleLabelsRadius } = dimensions; + const [min, max] = getFieldConfigMinMax(fieldDisplay); - const fontSize = scaleLabelsFontSize; - const textLineHeight = scaleLabelsFontSize * 1.2; - const radius = scaleLabelsRadius - textLineHeight; + const fontSize = scaleLabelsFontSize; + const textLineHeight = scaleLabelsFontSize * LINE_HEIGHT_FACTOR; + const radius = scaleLabelsRadius - textLineHeight; - function getTextPosition(text: string, value: number, index: number) { - const isLast = index === thresholds.length - 1; - const isFirst = index === 0; + function getTextPosition(text: string, value: number, index: number) { + const isLast = index === thresholds.length - 1; + const isFirst = index === 0; - let valueDeg = ((value - min) / (max - min)) * angleRange; - let finalAngle = startAngle + valueDeg; + let valueDeg = ((value - min) / (max - min)) * angleRange; + let finalAngle = startAngle + valueDeg; - // Now adjust the final angle based on the label text width and the labels position on the arc - let measure = measureText(text, fontSize, theme.typography.fontWeightMedium); - let textWidthAngle = (measure.width / (2 * Math.PI * radius)) * angleRange; + // Now adjust the final angle based on the label text width and the labels position on the arc + let measure = measureText(text, fontSize, theme.typography.fontWeightMedium); + let textWidthAngle = (measure.width / (2 * Math.PI * radius)) * angleRange; - // the centering is different for gauge or circle shapes for some reason - finalAngle -= endAngle < 180 ? textWidthAngle : textWidthAngle / 2; + // the centering is different for gauge or circle shapes for some reason + finalAngle -= endAngle < 180 ? textWidthAngle : textWidthAngle / 2; - // For circle gauges we need to shift the first label more - if (isFirst) { - finalAngle += textWidthAngle; + // For circle gauges we need to shift the first label more + if (isFirst) { + finalAngle += textWidthAngle; + } + + // For circle gauges we need to shift the last label more + if (isLast && endAngle === 360) { + finalAngle -= textWidthAngle; + } + + const position = toCartesian(centerX, centerY, radius, finalAngle); + + return { ...position, transform: `rotate(${finalAngle}, ${position.x}, ${position.y})` }; } - // For circle gauges we need to shift the last label more - if (isLast && endAngle === 360) { - finalAngle -= textWidthAngle; - } - - const position = toCartesian(centerX, centerY, radius, finalAngle); - - return { ...position, transform: `rotate(${finalAngle}, ${position.x}, ${position.y})` }; + return ( + + {thresholds.map((threshold, index) => { + const labelPos = getTextPosition(String(threshold.value), threshold.value, index); + return ( + + {threshold.value} + + ); + })} + + ); } +); - return ( - - {thresholds.map((threshold, index) => { - const labelPos = getTextPosition(String(threshold.value), threshold.value, index); - - return ( - - {threshold.value} - - ); - })} - - ); -} +RadialScaleLabels.displayName = 'RadialScaleLabels'; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx index acb255a3f3e..2d6c45a14bf 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx @@ -1,49 +1,76 @@ +import { memo, useMemo } from 'react'; + import { FieldDisplay, GrafanaTheme2, FieldConfig } from '@grafana/data'; import { GraphFieldConfig, GraphGradientMode, LineInterpolation } from '@grafana/schema'; import { Sparkline } from '../Sparkline/Sparkline'; -import { RadialShape, RadialTextMode } from './RadialGauge'; -import { GaugeDimensions } from './utils'; +import { RadialShape, RadialTextMode, RadialGaugeDimensions } from './types'; interface RadialSparklineProps { - sparkline: FieldDisplay['sparkline']; - dimensions: GaugeDimensions; - theme: GrafanaTheme2; color?: string; - shape?: RadialShape; + dimensions: RadialGaugeDimensions; + shape: RadialShape; + sparkline: FieldDisplay['sparkline']; textMode: Exclude; + theme: GrafanaTheme2; } -export function RadialSparkline({ sparkline, dimensions, theme, color, shape, textMode }: RadialSparklineProps) { - const { radius, barWidth } = dimensions; - if (!sparkline) { - return null; +const SPARKLINE_HEIGHT_DIVISOR = 4; +const SPARKLINE_HEIGHT_DIVISOR_NAME_AND_VALUE = 4; +const SPARKLINE_WIDTH_FACTOR_ARC = 1.4; +const SPARKLINE_WIDTH_FACTOR_CIRCLE = 1.6; +const SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE = 4; +const SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE_NAME_AND_VALUE = 3.3; +const SPARKLINE_SPACING = 8; + +export function getSparklineDimensions( + radius: number, + barWidth: number, + showNameAndValue: boolean, + shape: RadialShape +): { width: number; height: number } { + const height = radius / (showNameAndValue ? SPARKLINE_HEIGHT_DIVISOR_NAME_AND_VALUE : SPARKLINE_HEIGHT_DIVISOR); + const width = radius * (shape === 'gauge' ? SPARKLINE_WIDTH_FACTOR_ARC : SPARKLINE_WIDTH_FACTOR_CIRCLE) - barWidth; + return { width, height }; +} + +export const RadialSparkline = memo( + ({ sparkline, dimensions, theme, color, shape, textMode }: RadialSparklineProps) => { + const { radius, barWidth } = dimensions; + + const showNameAndValue = textMode === 'value_and_name'; + const { width, height } = getSparklineDimensions(radius, barWidth, showNameAndValue, shape); + const topPos = + shape === 'gauge' + ? dimensions.gaugeBottomY - height - SPARKLINE_SPACING + : `calc(50% + ${radius / (showNameAndValue ? SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE_NAME_AND_VALUE : SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE)}px)`; + + const config: FieldConfig = useMemo( + () => ({ + color: { + mode: 'fixed', + fixedColor: color ?? 'blue', + }, + custom: { + gradientMode: GraphGradientMode.Opacity, + fillOpacity: 40, + lineInterpolation: LineInterpolation.Smooth, + }, + }), + [color] + ); + + if (!sparkline) { + return null; + } + + return ( +
+ +
+ ); } +); - const showNameAndValue = textMode === 'value_and_name'; - const height = radius / (showNameAndValue ? 4 : 3); - const width = radius * (shape === 'gauge' ? 1.6 : 1.4) - barWidth; - const topPos = - shape === 'gauge' - ? `${dimensions.gaugeBottomY - height}px` - : `calc(50% + ${radius / (showNameAndValue ? 3.3 : 4)}px)`; - - const config: FieldConfig = { - color: { - mode: 'fixed', - fixedColor: color ?? 'blue', - }, - custom: { - gradientMode: GraphGradientMode.Opacity, - fillOpacity: 40, - lineInterpolation: LineInterpolation.Smooth, - }, - }; - - return ( -
- -
- ); -} +RadialSparkline.displayName = 'RadialSparkline'; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx index 51a1c64c842..69ab16e450e 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import { memo } from 'react'; import { DisplayValue, @@ -11,13 +12,12 @@ import { import { useStyles2 } from '../../themes/ThemeContext'; import { calculateFontSize } from '../../utils/measureText'; -import { RadialShape, RadialTextMode } from './RadialGauge'; -import { GaugeDimensions } from './utils'; +import { RadialShape, RadialTextMode, RadialGaugeDimensions } from './types'; interface RadialTextProps { displayValue: DisplayValue; theme: GrafanaTheme2; - dimensions: GaugeDimensions; + dimensions: RadialGaugeDimensions; textMode: Exclude; shape: RadialShape; sparkline?: FieldSparkline; @@ -26,123 +26,137 @@ interface RadialTextProps { nameManualFontSize?: number; } -export function RadialText({ - displayValue, - theme, - dimensions, - textMode, - shape, - sparkline, - alignmentFactors, - valueManualFontSize, - nameManualFontSize, -}: RadialTextProps) { - const styles = useStyles2(getStyles); - const { centerX, centerY, radius, barWidth } = dimensions; +const LINE_HEIGHT_FACTOR = 1.21; +const VALUE_WIDTH_TO_RADIUS_FACTOR = 0.82; +const NAME_TO_HEIGHT_FACTOR = 0.45; +const LARGE_RADIUS_SCALING_DECAY = 0.86; +const MAX_TEXT_WIDTH_DIVISOR = 7; +const MAX_NAME_HEIGHT_DIVISOR = 4; +const VALUE_SPACE_PERCENTAGE = 0.7; +const SPARKLINE_SPACING = 8; +const MIN_VALUE_FONT_SIZE = 1; +const MIN_NAME_FONT_SIZE = 10; +const MIN_UNIT_FONT_SIZE = 6; - if (textMode === 'none') { - return null; - } +export const RadialText = memo( + ({ + displayValue, + theme, + dimensions, + textMode, + shape, + sparkline, + alignmentFactors, + valueManualFontSize, + nameManualFontSize, + }: RadialTextProps) => { + const styles = useStyles2(getStyles); + const { centerX, centerY, radius, barWidth } = dimensions; - const nameToAlignTo = (alignmentFactors ? alignmentFactors.title : displayValue.title) ?? ''; - const valueToAlignTo = formattedValueToString(alignmentFactors ? alignmentFactors : displayValue); + if (textMode === 'none') { + return null; + } - const showValue = textMode === 'value' || textMode === 'value_and_name'; - const showName = textMode === 'name' || textMode === 'value_and_name'; - const maxTextWidth = radius * 2 - barWidth - radius / 7; + const nameToAlignTo = (alignmentFactors ? alignmentFactors.title : displayValue.title) ?? ''; + const valueToAlignTo = formattedValueToString(alignmentFactors ? alignmentFactors : displayValue); - // Not sure where this comes from but svg text is not using body line-height - const lineHeight = 1.21; - const valueWidthToRadiusFactor = 0.82; - const nameToHeightFactor = 0.45; - const largeRadiusScalingDecay = 0.86; + const showValue = textMode === 'value' || textMode === 'value_and_name'; + const showName = textMode === 'name' || textMode === 'value_and_name'; + const maxTextWidth = radius * 2 - barWidth - radius / MAX_TEXT_WIDTH_DIVISOR; - // This pow 0.92 factor is to create a decay so the font size does not become rediculously large for very large panels - let maxValueHeight = valueWidthToRadiusFactor * Math.pow(radius, largeRadiusScalingDecay); - let maxNameHeight = radius / 4; + // This pow 0.92 factor is to create a decay so the font size does not become rediculously large for very large panels + let maxValueHeight = VALUE_WIDTH_TO_RADIUS_FACTOR * Math.pow(radius, LARGE_RADIUS_SCALING_DECAY); + let maxNameHeight = radius / MAX_NAME_HEIGHT_DIVISOR; - if (showValue && showName) { - maxValueHeight = valueWidthToRadiusFactor * Math.pow(radius, largeRadiusScalingDecay); - maxNameHeight = nameToHeightFactor * Math.pow(radius, largeRadiusScalingDecay); - } + if (showValue && showName) { + maxValueHeight = VALUE_WIDTH_TO_RADIUS_FACTOR * Math.pow(radius, LARGE_RADIUS_SCALING_DECAY); + maxNameHeight = NAME_TO_HEIGHT_FACTOR * Math.pow(radius, LARGE_RADIUS_SCALING_DECAY); + } - const valueFontSize = - valueManualFontSize ?? - calculateFontSize( - valueToAlignTo, - maxTextWidth, - maxValueHeight, - lineHeight, - undefined, - theme.typography.body.fontWeight + const valueFontSize = Math.max( + valueManualFontSize ?? + calculateFontSize( + valueToAlignTo, + maxTextWidth, + maxValueHeight, + LINE_HEIGHT_FACTOR, + undefined, + theme.typography.body.fontWeight + ), + MIN_VALUE_FONT_SIZE ); - const nameFontSize = - nameManualFontSize ?? - calculateFontSize( - nameToAlignTo, - maxTextWidth, - maxNameHeight, - lineHeight, - undefined, - theme.typography.body.fontWeight + const nameFontSize = Math.max( + nameManualFontSize ?? + calculateFontSize( + nameToAlignTo, + maxTextWidth, + maxNameHeight, + LINE_HEIGHT_FACTOR, + undefined, + theme.typography.body.fontWeight + ), + MIN_NAME_FONT_SIZE ); - const unitFontSize = Math.max(valueFontSize * 0.7, 5); - const valueHeight = valueFontSize * lineHeight; - const nameHeight = nameFontSize * lineHeight; + const unitFontSize = Math.max(valueFontSize * VALUE_SPACE_PERCENTAGE, MIN_UNIT_FONT_SIZE); + const valueHeight = valueFontSize * LINE_HEIGHT_FACTOR; + const nameHeight = nameFontSize * LINE_HEIGHT_FACTOR; - const valueY = showName ? centerY - nameHeight * 0.3 : centerY; - const nameY = showValue ? valueY + valueHeight * 0.7 : centerY; - const nameColor = showValue ? theme.colors.text.secondary : theme.colors.text.primary; - const suffixShift = (valueFontSize - unitFontSize * 1.2) / 2; + const valueY = showName ? centerY - nameHeight * (1 - VALUE_SPACE_PERCENTAGE) : centerY; + const nameY = showValue ? valueY + valueHeight * VALUE_SPACE_PERCENTAGE : centerY; + const nameColor = showValue ? theme.colors.text.secondary : theme.colors.text.primary; + const suffixShift = (valueFontSize - unitFontSize * LINE_HEIGHT_FACTOR) / 2; - // adjust the text up on gauges and when sparklines are present - let yOffset = 0; - if (shape === 'gauge') { - // we render from the center of the gauge, so move up by half of half of the total height - yOffset -= (valueHeight + nameHeight) / 4; - } - if (sparkline) { - yOffset -= 8; + // adjust the text up on gauges and when sparklines are present + let yOffset = 0; + if (shape === 'gauge') { + // we render from the center of the gauge, so move up by half of half of the total height + yOffset -= (valueHeight + nameHeight) / 4; + } + if (sparkline) { + yOffset -= SPARKLINE_SPACING; + } + + return ( + + {showValue && ( + + {displayValue.prefix ?? ''} + {displayValue.text} + + {displayValue.suffix ?? ''} + + + )} + {showName && ( + + {displayValue.title} + + )} + + ); } +); - return ( - - {showValue && ( - - {displayValue.prefix ?? ''} - {displayValue.text} - - {displayValue.suffix ?? ''} - - - )} - {showName && ( - - {displayValue.title} - - )} - - ); -} +RadialText.displayName = 'RadialText'; -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = (_theme: GrafanaTheme2) => ({ text: css({ verticalAlign: 'bottom', }), diff --git a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx index 602038ccf10..cb2829934b9 100644 --- a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx @@ -1,20 +1,22 @@ import { FieldDisplay, Threshold } from '@grafana/data'; import { RadialArcPath } from './RadialArcPath'; -import { RadialColorDefs } from './RadialColorDefs'; -import { GaugeDimensions } from './utils'; +import { GradientStop, RadialGaugeDimensions, RadialShape } from './types'; +import { getFieldConfigMinMax } from './utils'; -export interface Props { - dimensions: GaugeDimensions; +interface ThresholdsBarProps { + dimensions: RadialGaugeDimensions; angleRange: number; startAngle: number; endAngle: number; + shape: RadialShape; fieldDisplay: FieldDisplay; roundedBars?: boolean; glowFilter?: string; - colorDefs: RadialColorDefs; thresholds: Threshold[]; + gradient?: GradientStop[]; } + export function ThresholdsBar({ dimensions, fieldDisplay, @@ -22,19 +24,18 @@ export function ThresholdsBar({ angleRange, roundedBars, glowFilter, - colorDefs, thresholds, -}: Props) { - const fieldConfig = fieldDisplay.field; - const min = fieldConfig.min ?? 0; - const max = fieldConfig.max ?? 100; - + shape, + gradient, +}: ThresholdsBarProps) { const thresholdDimensions = { ...dimensions, barWidth: dimensions.thresholdsBarWidth, radius: dimensions.thresholdsBarRadius, }; + const [min, max] = getFieldConfigMinMax(fieldDisplay); + let currentStart = startAngle; let paths: React.ReactNode[] = []; @@ -48,27 +49,26 @@ export function ThresholdsBar({ valueDeg = 0; } - let lengthDeg = valueDeg - currentStart + startAngle; + const lengthDeg = valueDeg - currentStart + startAngle; + const colorProps = gradient ? { gradient } : { color: threshold.color }; paths.push( ); currentStart += lengthDeg; } - return ( - <> - {paths} - {colorDefs.getDefs()} - - ); + return {paths}; } diff --git a/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap new file mode 100644 index 00000000000..97b053c2d61 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap @@ -0,0 +1,144 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RadialGauge color utils buildGradientColors should map threshold colors correctly (with baseColor if displayProcessor does not return colors) 1`] = ` +[ + { + "color": "#444444", + "percent": 0, + }, + { + "color": "#FADE2A", + "percent": 0.5, + }, + { + "color": "#F2495C", + "percent": 0.8, + }, + { + "color": "#444444", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should map threshold colors correctly (with baseColor if displayProcessor does not return colors) 2`] = ` +[ + { + "color": "#FF0000", + "percent": 0, + }, + { + "color": "#FADE2A", + "percent": 0.5, + }, + { + "color": "#F2495C", + "percent": 0.8, + }, + { + "color": "#FF0000", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for by-value color mode in dark theme 1`] = ` +[ + { + "color": "#181b1f", + "percent": 0, + }, + { + "color": "#1F60C4", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for by-value color mode in light theme 1`] = ` +[ + { + "color": "#ffffff", + "percent": 0, + }, + { + "color": "#1250B0", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for continuous color modes 1`] = ` +[ + { + "color": "rgb(0, 32, 81)", + "percent": 0, + }, + { + "color": "rgb(17, 54, 108)", + "percent": 0.125, + }, + { + "color": "rgb(60, 77, 110)", + "percent": 0.25, + }, + { + "color": "rgb(98, 100, 111)", + "percent": 0.375, + }, + { + "color": "rgb(127, 124, 117)", + "percent": 0.5, + }, + { + "color": "rgb(154, 148, 120)", + "percent": 0.625, + }, + { + "color": "rgb(187, 175, 113)", + "percent": 0.75, + }, + { + "color": "rgb(226, 203, 92)", + "percent": 0.875, + }, + { + "color": "rgb(253, 234, 69)", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for fixed color mode in dark theme 1`] = ` +[ + { + "color": "#37237a", + "percent": 0, + }, + { + "color": "#a146da", + "percent": 0.75, + }, + { + "color": "#a146da", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for fixed color mode in light theme 1`] = ` +[ + { + "color": "#a146da", + "percent": 0, + }, + { + "color": "#3e2b9a", + "percent": 0.75, + }, + { + "color": "#3e2b9a", + "percent": 1, + }, +] +`; diff --git a/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000000..db4c1c40882 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for center x and y 1`] = `"M 150 110 A 90 90 0 1 1 149.98429203681178 110.00000137077838 A 10 10 0 0 1 149.98778269529805 130.00000106616096 A 70 70 0 1 0 150 130 A 10 10 0 0 1 150 110 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for half arc 1`] = `"M 100 10 A 90 90 0 0 1 100 190 L 100 170 A 70 70 0 0 0 100 30 L 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow bar width 1`] = `"M 100 17.5 A 82.5 82.5 0 0 1 100 182.5 L 100 177.5 A 77.5 77.5 0 0 0 100 22.5 L 100 17.5 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow radius 1`] = `"M 100 40 A 60 60 0 0 1 100 160 L 100 140 A 40 40 0 0 0 100 60 L 100 40 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for quarter arc 1`] = `"M 100 10 A 90 90 0 0 1 190 100 L 170 100 A 70 70 0 0 0 100 30 L 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for rounded bars 1`] = `"M 100 10 A 90 90 0 1 1 10 100.00000000000001 A 10 10 0 0 1 30 100.00000000000001 A 70 70 0 1 0 100 30 A 10 10 0 0 1 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for three quarter arc 1`] = `"M 100 10 A 90 90 0 1 1 10 100.00000000000001 L 30 100.00000000000001 A 70 70 0 1 0 100 30 L 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for wide bar width 1`] = `"M 100 -5 A 105 105 0 0 1 100 205 L 100 155 A 55 55 0 0 0 100 45 L 100 -5 Z"`; diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts new file mode 100644 index 00000000000..9eb787ce150 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts @@ -0,0 +1,257 @@ +import { defaultsDeep } from 'lodash'; + +import { createTheme, FALLBACK_COLOR, Field, FieldDisplay, FieldType, ThresholdsMode } from '@grafana/data'; +import { FieldColorModeId } from '@grafana/schema'; + +import { + buildGradientColors, + colorAtGradientPercent, + getBarEndcapColors, + getEndpointMarkerColors, + getGradientCss, +} from './colors'; + +export type DeepPartial = { + [P in keyof T]?: DeepPartial; +}; + +describe('RadialGauge color utils', () => { + describe('buildGradientColors', () => { + const createField = (colorMode: FieldColorModeId): Field => + ({ + type: FieldType.number, + name: 'Test Field', + config: { + color: { + mode: colorMode, + }, + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { value: -Infinity, color: 'green' }, + { value: 50, color: 'yellow' }, + { value: 80, color: 'red' }, + ], + }, + }, + values: [70, 40, 30, 90, 55], + }) satisfies Field; + + const buildFieldDisplay = (field: Field, part = {}): FieldDisplay => + defaultsDeep(part, { + field: field.config, + colIndex: 0, + view: { + getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: undefined }))), + }, + display: { + numeric: 75, + }, + }); + + it('should return the baseColor if gradient is false-y', () => { + expect( + buildGradientColors(false, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000') + ).toEqual([ + { color: '#FF0000', percent: 0 }, + { color: '#FF0000', percent: 1 }, + ]); + + expect( + buildGradientColors(undefined, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000') + ).toEqual([ + { color: '#FF0000', percent: 0 }, + { color: '#FF0000', percent: 1 }, + ]); + }); + + it('uses the fallback color if no baseColor is set', () => { + expect(buildGradientColors(false, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)))).toEqual( + [ + { color: FALLBACK_COLOR, percent: 0 }, + { color: FALLBACK_COLOR, percent: 1 }, + ] + ); + }); + + it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => { + expect( + buildGradientColors( + true, + createTheme(), + buildFieldDisplay(createField(FieldColorModeId.Thresholds), { + view: { getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: '#444444' }))) }, + }) + ) + ).toMatchSnapshot(); + }); + + it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => { + expect( + buildGradientColors(true, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds)), '#FF0000') + ).toMatchSnapshot(); + }); + + it('should return gradient colors for continuous color modes', () => { + expect( + buildGradientColors( + true, + createTheme(), + buildFieldDisplay(createField(FieldColorModeId.ContinuousCividis)), + '#00FF00' + ) + ).toMatchSnapshot(); + }); + + it.each(['dark', 'light'] as const)('should return gradient colors for by-value color mode in %s theme', (mode) => { + expect( + buildGradientColors( + true, + createTheme({ colors: { mode } }), + buildFieldDisplay(createField(FieldColorModeId.ContinuousBlues)) + ) + ).toMatchSnapshot(); + }); + + it.each(['dark', 'light'] as const)('should return gradient colors for fixed color mode in %s theme', (mode) => { + expect( + buildGradientColors( + true, + createTheme({ colors: { mode } }), + buildFieldDisplay(createField(FieldColorModeId.Fixed)), + '#442299' + ) + ).toMatchSnapshot(); + }); + }); + + describe('colorAtGradientPercent', () => { + it('should calculate the color at a given percent in a gradient of two colors', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#0000ff', percent: 1 }, + ]; + expect(colorAtGradientPercent(gradient, 0).toHexString()).toBe('#ff0000'); + expect(colorAtGradientPercent(gradient, 0.25).toHexString()).toBe('#bf0040'); + expect(colorAtGradientPercent(gradient, 0.5).toHexString()).toBe('#800080'); + expect(colorAtGradientPercent(gradient, 0.75).toHexString()).toBe('#4000bf'); + expect(colorAtGradientPercent(gradient, 1).toHexString()).toBe('#0000ff'); + }); + + it('should calculate the color at a given percent in a gradient of multiple colors', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + expect(colorAtGradientPercent(gradient, 0).toHexString()).toBe('#ff0000'); + expect(colorAtGradientPercent(gradient, 0.25).toHexString()).toBe('#808000'); + expect(colorAtGradientPercent(gradient, 0.5).toHexString()).toBe('#00ff00'); + expect(colorAtGradientPercent(gradient, 0.75).toHexString()).toBe('#008080'); + expect(colorAtGradientPercent(gradient, 1).toHexString()).toBe('#0000ff'); + }); + + it('will still work if unsorted', () => { + const gradient = [ + { color: '#0000ff', percent: 1 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#ff0000', percent: 0 }, + ]; + expect(colorAtGradientPercent(gradient, 0).toHexString()).toBe('#ff0000'); + expect(colorAtGradientPercent(gradient, 0.25).toHexString()).toBe('#808000'); + expect(colorAtGradientPercent(gradient, 0.5).toHexString()).toBe('#00ff00'); + expect(colorAtGradientPercent(gradient, 0.75).toHexString()).toBe('#008080'); + expect(colorAtGradientPercent(gradient, 1).toHexString()).toBe('#0000ff'); + }); + + it('should not throw an error when percent is outside 0-1 range', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#0000ff', percent: 1 }, + ]; + expect(colorAtGradientPercent(gradient, -0.5).toHexString()).toBe('#ff0000'); + expect(colorAtGradientPercent(gradient, 1.5).toHexString()).toBe('#0000ff'); + }); + + it('should throw an error when less than two stops are provided', () => { + expect(() => { + colorAtGradientPercent([], 0.5); + }).toThrow('colorAtGradientPercent requires at least two color stops'); + expect(() => { + colorAtGradientPercent([{ color: '#ff0000', percent: 0 }], 0.5); + }).toThrow('colorAtGradientPercent requires at least two color stops'); + }); + }); + + describe('getBarEndcapColors', () => { + it('should return the first and last colors in the gradient', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + const [startColor, endColor] = getBarEndcapColors(gradient); + expect(startColor).toBe('#ff0000'); + expect(endColor).toBe('#0000ff'); + }); + + it('should return the correct end color based on percent', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + const [startColor, endColor] = getBarEndcapColors(gradient, 0.25); + expect(startColor).toBe('#ff0000'); + expect(endColor).toBe('#808000'); + }); + + it('should handle gradients with only one colors', () => { + const gradient = [{ color: '#ff0000', percent: 0 }]; + const [startColor, endColor] = getBarEndcapColors(gradient); + expect(startColor).toBe('#ff0000'); + expect(endColor).toBe('#ff0000'); + }); + + it('should throw an error when no colors are provided', () => { + expect(() => { + getBarEndcapColors([]); + }).toThrow('getBarEndcapColors requires at least one color stop'); + }); + }); + + describe('getGradientCss', () => { + it('should return conic-gradient CSS for circle shape', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + const css = getGradientCss(gradient, 'circle'); + expect(css).toBe('conic-gradient(from 0deg, #ff0000 0.00%, #00ff00 50.00%, #0000ff 100.00%)'); + }); + + it('should return linear-gradient CSS for arc shape', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + const css = getGradientCss(gradient, 'gauge'); + expect(css).toBe('linear-gradient(90deg, #ff0000 0.00%, #00ff00 50.00%, #0000ff 100.00%)'); + }); + }); + + describe('getEndpointMarkerColors', () => { + it('should return contrasting guide dot colors based on the gradient endpoints and percent', () => { + const gradient = [ + { color: '#000000', percent: 0 }, + { color: '#ffffff', percent: 0.5 }, + { color: '#ffffff', percent: 1 }, + ]; + const [startDotColor, endDotColor] = getEndpointMarkerColors(gradient, 0.35); + expect(startDotColor).toBe('#fbfbfb'); + expect(endDotColor).toBe('#111217'); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts new file mode 100644 index 00000000000..a29a1efbbb3 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -0,0 +1,179 @@ +import tinycolor from 'tinycolor2'; + +import { colorManipulator, FALLBACK_COLOR, FieldDisplay, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; +import { FieldColorModeId } from '@grafana/schema'; + +import { GradientStop, RadialShape } from './types'; +import { getFieldConfigMinMax, getFieldDisplayProcessor, getValuePercentageForValue } from './utils'; + +export function buildGradientColors( + gradient = false, + theme: GrafanaTheme2, + fieldDisplay: FieldDisplay, + baseColor = fieldDisplay.display.color ?? FALLBACK_COLOR +): GradientStop[] { + if (!gradient) { + return [ + { color: baseColor, percent: 0 }, + { color: baseColor, percent: 1 }, + ]; + } + + const colorMode = getFieldColorMode(fieldDisplay.field.color?.mode); + + // thresholds get special handling + if (colorMode.id === FieldColorModeId.Thresholds) { + const displayProcessor = getFieldDisplayProcessor(fieldDisplay); + const [min, max] = getFieldConfigMinMax(fieldDisplay); + const thresholds = fieldDisplay.field.thresholds?.steps ?? []; + + const result: Array<{ color: string; percent: number }> = [ + { color: displayProcessor(min).color ?? baseColor, percent: 0 }, + ]; + + for (const threshold of thresholds) { + if (threshold.value > min && threshold.value < max) { + const percent = (threshold.value - min) / (max - min); + result.push({ color: theme.visualization.getColorByName(threshold.color), percent }); + } + } + + result.push({ color: displayProcessor(max).color ?? baseColor, percent: 1 }); + + return result; + } + + // Handle continuous color modes before other by-value modes + if (colorMode.isContinuous && colorMode.getColors) { + const colors = colorMode.getColors(theme); + return colors.map((color, idx) => ({ color, percent: idx / (colors.length - 1) })); + } + + // For value-based colors, we want to stay more true to the specific color, + // so a radial gradient that adds a bit of light and shade works best + if (colorMode.isByValue) { + const darkerColor = tinycolor(baseColor).darken(5); + const lighterColor = tinycolor(baseColor).spin(20).lighten(10); + + const color1 = theme.isDark ? lighterColor : darkerColor; + const color2 = theme.isDark ? darkerColor : lighterColor; + + return [ + { color: color1.toString(), percent: 0 }, + { color: color2.toString(), percent: 0.6 }, + { color: color2.toString(), percent: 1 }, + ]; + } + + // For fixed / palette based color scales we can create a more hue and light + // based linear gradient that we rotate with the value + const darkerColor = tinycolor(baseColor) + .spin(-20) + .darken(theme.isDark ? 15 : 5); + const lighterColor = tinycolor(baseColor).saturate(20).spin(20).brighten(10).lighten(10); + + const underlyingGradient = [ + { color: theme.isDark ? darkerColor.toString() : lighterColor.toString(), percent: 0 }, + { color: theme.isDark ? lighterColor.toString() : darkerColor.toString(), percent: 1 }, + ]; + + // rotate the gradient so that the highest contrasting point is the value, depending on theme. + const valuePercent = getValuePercentageForValue(fieldDisplay); + const startColor = theme.isDark + ? colorAtGradientPercent(underlyingGradient, 1 - valuePercent).toHexString() + : underlyingGradient[0].color; + const endColor = theme.isDark + ? underlyingGradient[1].color + : colorAtGradientPercent(underlyingGradient, valuePercent).toHexString(); + return [ + { color: startColor, percent: 0 }, + { color: endColor, percent: valuePercent }, + { color: endColor, percent: 1 }, + ]; +} + +/** + * @alpha - perhaps this should go in colorManipulator.ts + * Given color stops (each with a color and percentage 0..1) returns the color at a given percentage. + * Uses tinycolor.mix for interpolation. + * @params stops - array of color stops (percentages 0..1) + * @params percent - percentage 0..1 + * @returns color at the given percentage + */ +export function colorAtGradientPercent(stops: GradientStop[], percent: number): tinycolor.Instance { + if (!stops || stops.length < 2) { + throw new Error('colorAtGradientPercent requires at least two color stops'); + } + + // normalize and sort stops by percent. TODO: is this necessary? is gradientstops always sorted? + const sorted = stops + .map((s) => ({ color: s.color, percent: Math.min(Math.max(0, s.percent), 1) })) + .sort((a, b) => a.percent - b.percent); + + // percent outside range + if (percent <= sorted[0].percent) { + return tinycolor(sorted[0].color); + } + if (percent >= sorted[sorted.length - 1].percent) { + return tinycolor(sorted[sorted.length - 1].color); + } + + // find surrounding stops using binary search + let lo = 0; + let hi = sorted.length - 1; + while (lo + 1 < hi) { + const mid = (lo + hi) >> 1; + if (percent <= sorted[mid].percent) { + hi = mid; + } else { + lo = mid; + } + } + + const left = sorted[lo]; + const right = sorted[hi]; + + const range = right.percent - left.percent; + const t = range === 0 ? 0 : (percent - left.percent) / range; // 0..1 + return tinycolor.mix(left.color, right.color, t * 100); +} + +export function getBarEndcapColors(gradientStops: GradientStop[], percent = 1): [string, string] { + if (gradientStops.length === 0) { + throw new Error('getBarEndcapColors requires at least one color stop'); + } + + const startColor = gradientStops[0].color; + let endColor = gradientStops[gradientStops.length - 1].color; + + // if we have a percentageFilled, use it to get a the correct end color based on where the bar terminates + if (gradientStops.length >= 2) { + const endColorByPercentage = colorAtGradientPercent(gradientStops, percent); + endColor = + endColorByPercentage.getAlpha() === 1 ? endColorByPercentage.toHexString() : endColorByPercentage.toHex8String(); + } + return [startColor, endColor]; +} + +export function getGradientCss(gradientStops: GradientStop[], shape: RadialShape): string { + const colorStrings = gradientStops.map((stop) => `${stop.color} ${(stop.percent * 100).toFixed(2)}%`); + return shape === 'circle' + ? `conic-gradient(from 0deg, ${colorStrings.join(', ')})` + : `linear-gradient(90deg, ${colorStrings.join(', ')})`; +} + +// the theme does not make the full palette available to us, and we +// don't want transparent colors which our grays usually have. +const GRAY_05 = '#111217'; +const GRAY_90 = '#fbfbfb'; +const CONTRAST_THRESHOLD_MAX = 4.5; +const getGuideDotColor = (color: string): string => { + const darkColor = GRAY_05; + const lightColor = GRAY_90; + return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor; +}; + +export function getEndpointMarkerColors(gradientStops: GradientStop[], percent = 1): [string, string] { + const [startColor, endColor] = getBarEndcapColors(gradientStops, percent); + return [getGuideDotColor(startColor), getGuideDotColor(endColor)]; +} diff --git a/packages/grafana-ui/src/components/RadialGauge/effects.tsx b/packages/grafana-ui/src/components/RadialGauge/effects.tsx index 354a68a25ba..c48307f177e 100644 --- a/packages/grafana-ui/src/components/RadialGauge/effects.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/effects.tsx @@ -1,15 +1,18 @@ import { GrafanaTheme2 } from '@grafana/data'; -import { GaugeDimensions } from './utils'; +import { RadialGaugeDimensions } from './types'; export interface GlowGradientProps { id: string; barWidth: number; } +const MIN_GLOW_SIZE = 0.75; +const GLOW_FACTOR = 0.08; + export function GlowGradient({ id, barWidth }: GlowGradientProps) { // 0.75 is the minimum glow size, and it scales with bar width - const glowSize = 0.75 + barWidth * 0.08; + const glowSize = MIN_GLOW_SIZE + barWidth * GLOW_FACTOR; return ( @@ -22,56 +25,19 @@ export function GlowGradient({ id, barWidth }: GlowGradientProps) { ); } -export function SpotlightGradient({ - id, - dimensions, - roundedBars, - angle, - theme, -}: { - id: string; - dimensions: GaugeDimensions; - angle: number; - roundedBars: boolean; - theme: GrafanaTheme2; -}) { - const angleRadian = ((angle - 90) * Math.PI) / 180; - - let x1 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian - 0.2); - let y1 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian - 0.2); - let x2 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian); - let y2 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian); - - if (theme.isLight) { - return ( - - - - - - ); - } - - return ( - - - - {roundedBars && } - - ); -} +const CENTER_GLOW_OPACITY = 0.15; export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) { return ( - - + + ); } export interface CenterGlowProps { - dimensions: GaugeDimensions; + dimensions: RadialGaugeDimensions; gaugeId: string; color?: string; } @@ -82,8 +48,8 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps return ( <> - - + + @@ -93,3 +59,36 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps ); } + +export function SpotlightGradient({ + id, + dimensions, + roundedBars, + angle, + theme, +}: { + id: string; + dimensions: RadialGaugeDimensions; + angle: number; + roundedBars: boolean; + theme: GrafanaTheme2; +}) { + if (theme.isLight) { + return null; + } + + const angleRadian = ((angle - 90) * Math.PI) / 180; + + let x1 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian - 0.2); + let y1 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian - 0.2); + let x2 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian); + let y2 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian); + + return ( + + + + {roundedBars && } + + ); +} diff --git a/packages/grafana-ui/src/components/RadialGauge/types.ts b/packages/grafana-ui/src/components/RadialGauge/types.ts new file mode 100644 index 00000000000..cc233dd524c --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/types.ts @@ -0,0 +1,25 @@ +export type RadialTextMode = 'auto' | 'value_and_name' | 'value' | 'name' | 'none'; +export type RadialShape = 'circle' | 'gauge'; + +export interface RadialGaugeDimensions { + margin: number; + radius: number; + centerX: number; + centerY: number; + barWidth: number; + endAngle?: number; + barIndex: number; + thresholdsBarRadius: number; + thresholdsBarWidth: number; + thresholdsBarSpacing: number; + scaleLabelsFontSize: number; + scaleLabelsSpacing: number; + scaleLabelsRadius: number; + gaugeBottomY: number; +} + +/** @alpha - perhaps this should go in @grafana/data */ +export interface GradientStop { + color: string; + percent: number; +} diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.test.ts b/packages/grafana-ui/src/components/RadialGauge/utils.test.ts index 5e3f34d62cd..b9b2e4ad8f3 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.test.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.test.ts @@ -1,24 +1,111 @@ -import { FieldDisplay } from '@grafana/data'; +import { DataFrameView, FieldDisplay } from '@grafana/data'; import type { RadialGaugeProps } from './RadialGauge'; -import { calculateDimensions, toRad, getValueAngleForValue } from './utils'; +import { RadialGaugeDimensions } from './types'; +import { + calculateDimensions, + toRad, + getValueAngleForValue, + drawRadialArcPath, + getFieldConfigMinMax, + getFieldDisplayProcessor, + getAngleBetweenSegments, + getOptimalSegmentCount, +} from './utils'; describe('RadialGauge utils', () => { - function calc(overrides: Partial = {}) { - return calculateDimensions( - overrides.width ?? 200, - overrides.height ?? 200, - overrides.shape === 'gauge' ? 110 : 360, - overrides.glowBar ?? false, - overrides.roundedBars ?? false, - overrides.barWidthFactor ?? 0.4, - overrides.barIndex ?? 0, - overrides.thresholdsBar ?? false, - overrides.showScaleLabels ?? false - ); - } + describe('getFieldDisplayProcessor', () => { + it('should return display processor from view when available', () => { + const mockProcessor = jest.fn(); + const mockView = { + getFieldDisplayProcessor: jest.fn().mockReturnValue(mockProcessor), + } as unknown as DataFrameView; + + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: {}, + view: mockView, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const dp = getFieldDisplayProcessor(fieldDisplay); + expect(dp).toBe(mockProcessor); + expect(mockView.getFieldDisplayProcessor).toHaveBeenCalledWith(0); + }); + + it('should return default display processor when view is not available', () => { + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: {}, + view: undefined, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const dp = getFieldDisplayProcessor(fieldDisplay); + expect(dp).toBeDefined(); + expect(typeof dp).toBe('function'); + }); + }); + + describe('getFieldConfigMinMax', () => { + it('should return min and max from field config when defined', () => { + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: { min: 10, max: 90 }, + view: undefined, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const [min, max] = getFieldConfigMinMax(fieldDisplay); + expect(min).toBe(10); + expect(max).toBe(90); + }); + + it('should return default min and max when not defined in field config', () => { + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: {}, + view: undefined, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const [min, max] = getFieldConfigMinMax(fieldDisplay); + expect(min).toBe(0); + expect(max).toBe(100); + }); + }); describe('calculateDimensions', () => { + function calc(overrides: Partial = {}) { + return calculateDimensions( + overrides.width ?? 200, + overrides.height ?? 200, + overrides.shape === 'gauge' ? 110 : 360, + overrides.glowBar ?? false, + overrides.roundedBars ?? false, + overrides.barWidthFactor ?? 0.4, + overrides.barIndex ?? 0, + overrides.thresholdsBar ?? false, + overrides.showScaleLabels ?? false + ); + } + it('should calculate basic dimensions for a square gauge', () => { const result = calc(); @@ -194,4 +281,84 @@ describe('RadialGauge utils', () => { expect(result.angle).toBe(240); }); }); + + describe('drawRadialArcPath', () => { + const defaultDims: RadialGaugeDimensions = Object.freeze({ + centerX: 100, + centerY: 100, + radius: 80, + barWidth: 20, + margin: 0, + barIndex: 0, + thresholdsBarWidth: 0, + thresholdsBarSpacing: 0, + thresholdsBarRadius: 0, + scaleLabelsFontSize: 0, + scaleLabelsSpacing: 0, + scaleLabelsRadius: 0, + gaugeBottomY: 0, + }); + + it.each([ + { description: 'quarter arc', startAngle: 0, endAngle: 90 }, + { description: 'half arc', startAngle: 0, endAngle: 180 }, + { description: 'three quarter arc', startAngle: 0, endAngle: 270 }, + { description: 'rounded bars', startAngle: 0, endAngle: 270, roundedBars: true }, + { description: 'wide bar width', startAngle: 0, endAngle: 180, dimensions: { barWidth: 50 } }, + { description: 'narrow bar width', startAngle: 0, endAngle: 180, dimensions: { barWidth: 5 } }, + { description: 'narrow radius', startAngle: 0, endAngle: 180, dimensions: { radius: 50 } }, + { + description: 'center x and y', + startAngle: 0, + endAngle: 360, + roundedBars: true, + dimensions: { centerX: 150, centerY: 200 }, + }, + ])(`should draw correct path for $description`, ({ startAngle, endAngle, dimensions, roundedBars }) => { + const path = drawRadialArcPath(startAngle, endAngle, { ...defaultDims, ...dimensions }, roundedBars); + expect(path).toMatchSnapshot(); + }); + + describe('edge cases', () => { + it('should adjust 360deg or greater arcs to avoid SVG rendering issues', () => { + expect(drawRadialArcPath(0, 360, defaultDims)).toEqual(drawRadialArcPath(0, 359.99, defaultDims)); + expect(drawRadialArcPath(0, 380, defaultDims)).toEqual(drawRadialArcPath(0, 380, defaultDims)); + }); + + it('should return empty string if inner radius collapses to zero or below', () => { + const smallRadiusDims = { ...defaultDims, radius: 5, barWidth: 20 }; + expect(drawRadialArcPath(0, 180, smallRadiusDims)).toBe(''); + }); + }); + }); + + describe('getAngleBetweenSegments', () => { + it('should calculate angle between segments based on spacing and count', () => { + expect(getAngleBetweenSegments(2, 10, 360)).toBe(48); + expect(getAngleBetweenSegments(5, 15, 180)).toBe(40); + }); + }); + + describe('getOptimalSegmentCount', () => { + it('should adjust segment count based on dimensions and spacing', () => { + const dimensions: RadialGaugeDimensions = { + centerX: 100, + centerY: 100, + radius: 80, + barWidth: 20, + margin: 0, + barIndex: 0, + thresholdsBarWidth: 0, + thresholdsBarSpacing: 0, + thresholdsBarRadius: 0, + scaleLabelsFontSize: 0, + scaleLabelsSpacing: 0, + scaleLabelsRadius: 0, + gaugeBottomY: 0, + }; + + expect(getOptimalSegmentCount(dimensions, 2, 10, 360)).toBe(8); + expect(getOptimalSegmentCount(dimensions, 1, 5, 360)).toBe(5); + }); + }); }); diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.ts b/packages/grafana-ui/src/components/RadialGauge/utils.ts index 44f767d89b2..e26cf5eed2a 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.ts @@ -1,11 +1,38 @@ -import { FieldDisplay } from '@grafana/data'; +import { FieldDisplay, getDisplayProcessor } from '@grafana/data'; -export function getValueAngleForValue(fieldDisplay: FieldDisplay, startAngle: number, endAngle: number) { - const angleRange = (360 % (startAngle === 0 ? 1 : startAngle)) + endAngle; +import { RadialGaugeDimensions } from './types'; + +export function getFieldDisplayProcessor(displayValue: FieldDisplay) { + if (displayValue.view && displayValue.colIndex != null) { + const dp = displayValue.view.getFieldDisplayProcessor(displayValue.colIndex); + if (dp) { + return dp; + } + } + + return getDisplayProcessor(); +} + +export function getFieldConfigMinMax(fieldDisplay: FieldDisplay) { const min = fieldDisplay.field.min ?? 0; const max = fieldDisplay.field.max ?? 100; + return [min, max]; +} - let angle = ((fieldDisplay.display.numeric - min) / (max - min)) * angleRange; +export function getValuePercentageForValue(fieldDisplay: FieldDisplay, value = fieldDisplay.display.numeric) { + const [min, max] = getFieldConfigMinMax(fieldDisplay); + return (value - min) / (max - min); +} + +export function getValueAngleForValue( + fieldDisplay: FieldDisplay, + startAngle: number, + endAngle: number, + value = fieldDisplay.display.numeric +) { + const angleRange = (360 % (startAngle === 0 ? 1 : startAngle)) + endAngle; + + let angle = getValuePercentageForValue(fieldDisplay, value) * angleRange; if (angle > angleRange) { angle = angleRange; @@ -26,24 +53,19 @@ export function toRad(angle: number) { return ((angle - 90) * Math.PI) / 180; } -export interface GaugeDimensions { - margin: number; - radius: number; - centerX: number; - centerY: number; - barWidth: number; - endAngle?: number; - barIndex: number; - thresholdsBarRadius: number; - thresholdsBarWidth: number; - thresholdsBarSpacing: number; - showScaleLabels?: boolean; - scaleLabelsFontSize: number; - scaleLabelsSpacing: number; - scaleLabelsRadius: number; - gaugeBottomY: number; -} - +/** + * returns the calculated dimensions for the radial gauge + * @param width + * @param height + * @param endAngle + * @param glow + * @param roundedBars + * @param barWidthFactor + * @param barIndex + * @param thresholdBar + * @param showScaleLabels + * @returns {RadialGaugeDimensions} + */ export function calculateDimensions( width: number, height: number, @@ -54,7 +76,7 @@ export function calculateDimensions( barIndex: number, thresholdBar?: boolean, showScaleLabels?: boolean -): GaugeDimensions { +): RadialGaugeDimensions { const yMaxAngle = endAngle > 180 ? 180 : endAngle; let margin = 0; @@ -97,6 +119,7 @@ export function calculateDimensions( maxRadiusW -= labelsSize; maxRadiusH -= labelsSize; + // FIXME: needs coverage // For gauges the max label needs a bit more vertical space so that it does not get clipped if (maxRadiusIsLimitedByHeight && endAngle < 180) { const amount = outerRadius * 0.07; @@ -155,3 +178,105 @@ export function toCartesian(centerX: number, centerY: number, radius: number, an y: centerY + radius * Math.sin(radian), }; } + +export function drawRadialArcPath( + startAngle: number, + endAngle: number, + dimensions: RadialGaugeDimensions, + roundedBars?: boolean +): string { + const { radius, centerX, centerY, barWidth } = dimensions; + + // For some reason a 100% full arc cannot be rendered + if (endAngle >= 360) { + endAngle = 359.99; + } + + const startRadians = toRad(startAngle); + const endRadians = toRad(startAngle + endAngle); + + const largeArc = endAngle > 180 ? 1 : 0; + + const outerR = radius + barWidth / 2; + const innerR = Math.max(0, radius - barWidth / 2); + if (innerR <= 0) { + return ''; // cannot draw arc with 0 inner radius + } + + // get points for both an inner and outer arc. we draw + // the arc entirely with a path's fill instead of using stroke + // so that it can be used as a clip-path. + const ox1 = centerX + outerR * Math.cos(startRadians); + const oy1 = centerY + outerR * Math.sin(startRadians); + const ox2 = centerX + outerR * Math.cos(endRadians); + const oy2 = centerY + outerR * Math.sin(endRadians); + + const ix1 = centerX + innerR * Math.cos(startRadians); + const iy1 = centerY + innerR * Math.sin(startRadians); + const ix2 = centerX + innerR * Math.cos(endRadians); + const iy2 = centerY + innerR * Math.sin(endRadians); + + // calculate the cap width in case we're drawing rounded bars + const capR = barWidth / 2; + + const pathParts = [ + // start at outer start + 'M', + ox1, + oy1, + // outer arc from start to end (clockwise) + 'A', + outerR, + outerR, + 0, + largeArc, + 1, + ox2, + oy2, + ]; + + if (roundedBars) { + // rounded end cap: small arc connecting outer end to inner end + pathParts.push('A', capR, capR, 0, 0, 1, ix2, iy2); + } else { + // straight line to inner end (square butt) + pathParts.push('L', ix2, iy2); + } + + // inner arc from end back to start (counter-clockwise) + pathParts.push('A', innerR, innerR, 0, largeArc, 0, ix1, iy1); + + if (roundedBars) { + // rounded start cap: small arc connecting inner start back to outer start + pathParts.push('A', capR, capR, 0, 0, 1, ox1, oy1); + } else { + // straight line back to outer start (square butt) + pathParts.push('L', ox1, oy1); + } + + pathParts.push('Z'); + + return pathParts.join(' '); +} + +export function getAngleBetweenSegments(segmentSpacing: number, segmentCount: number, range: number) { + // Max spacing is 8 degrees between segments + // Changing this constant could be considered a breaking change + const maxAngleBetweenSegments = Math.max(range / 1.5 / segmentCount, 2); + return segmentSpacing * maxAngleBetweenSegments; +} + +export function getOptimalSegmentCount( + dimensions: RadialGaugeDimensions, + segmentSpacing: number, + segmentCount: number, + range: number +) { + const angleBetweenSegments = getAngleBetweenSegments(segmentSpacing, segmentCount, range); + + const innerRadius = dimensions.radius - dimensions.barWidth / 2; + const circumference = Math.PI * innerRadius * 2 * (range / 360); + const maxSegments = Math.floor(circumference / (angleBetweenSegments + 3)); + + return Math.min(maxSegments, segmentCount); +} diff --git a/public/app/plugins/panel/radialbar/EffectsEditor.tsx b/public/app/plugins/panel/radialbar/EffectsEditor.tsx index d7a0f03cbc7..a3c26beca90 100644 --- a/public/app/plugins/panel/radialbar/EffectsEditor.tsx +++ b/public/app/plugins/panel/radialbar/EffectsEditor.tsx @@ -44,11 +44,6 @@ export function EffectsEditor(props: StandardEditorProps) { value={!!props.value?.gradient} onChange={(e) => props.onChange({ ...props.value, gradient: e.currentTarget.checked })} /> - props.onChange({ ...props.value, rounded: e.currentTarget.checked })} - /> ) { value={!!props.value?.centerGlow} onChange={(e) => props.onChange({ ...props.value, centerGlow: e.currentTarget.checked })} /> - props.onChange({ ...props.value, spotlight: e.currentTarget.checked })} - /> ); } diff --git a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx index 86235a3bf68..03232406463 100644 --- a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx +++ b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx @@ -37,11 +37,10 @@ export function RadialBarPanel({ width={width} height={height} barWidthFactor={options.barWidthFactor} - gradient={options.effects?.gradient ? 'auto' : 'none'} - spotlight={options.effects?.spotlight} + gradient={options.effects?.gradient} glowBar={options.effects?.barGlow} glowCenter={options.effects?.centerGlow} - roundedBars={options.effects?.rounded} + roundedBars={options.barShape === 'rounded'} vizCount={valueProps.count} shape={options.shape} segmentCount={options.segmentCount} @@ -51,6 +50,7 @@ export function RadialBarPanel({ alignmentFactors={valueProps.alignmentFactors} valueManualFontSize={options.text?.valueSize} nameManualFontSize={options.text?.titleSize} + endpointMarker={options.endpointMarker !== 'none' ? options.endpointMarker : undefined} onClick={menuProps.openMenu} /> ); diff --git a/public/app/plugins/panel/radialbar/module.tsx b/public/app/plugins/panel/radialbar/module.tsx index 12aff4966f7..3dfcb3e3d5f 100644 --- a/public/app/plugins/panel/radialbar/module.tsx +++ b/public/app/plugins/panel/radialbar/module.tsx @@ -69,6 +69,36 @@ export const plugin = new PanelPlugin(RadialBarPanel) }, }); + builder.addRadio({ + path: 'barShape', + name: t('radialbar.config.bar-shape', 'Bar Style'), + category, + defaultValue: defaultOptions.barShape, + settings: { + options: [ + { value: 'flat', label: t('radialbar.config.bar-shape-flat', 'Flat') }, + { value: 'rounded', label: t('radialbar.config.bar-shape-rounded', 'Rounded') }, + ], + }, + showIf: (options) => options.segmentCount === 1, + }); + + builder.addRadio({ + path: 'endpointMarker', + name: t('radialbar.config.endpoint-marker', 'Endpoint marker'), + description: t('radialbar.config.endpoint-marker-description', 'Glow is only supported in dark mode'), + category, + defaultValue: defaultOptions.endpointMarker, + settings: { + options: [ + { value: 'point', label: t('radialbar.config.endpoint-marker-point', 'Point') }, + { value: 'glow', label: t('radialbar.config.endpoint-marker-glow', 'Glow') }, + { value: 'none', label: t('radialbar.config.endpoint-marker-none', 'None') }, + ], + }, + showIf: (options) => options.barShape === 'rounded' && options.segmentCount === 1, + }); + builder.addBooleanSwitch({ path: 'sparkline', name: t('radialbar.config.sparkline', 'Show sparkline'), diff --git a/public/app/plugins/panel/radialbar/panelcfg.cue b/public/app/plugins/panel/radialbar/panelcfg.cue index a37982dcfe1..c959512c0b8 100644 --- a/public/app/plugins/panel/radialbar/panelcfg.cue +++ b/public/app/plugins/panel/radialbar/panelcfg.cue @@ -27,10 +27,8 @@ composableKinds: PanelCfg: { schema: { GaugePanelEffects: { barGlow?: bool | *false - spotlight?: bool | *false - rounded?: bool | *false centerGlow?: bool | *false - gradient?: bool | *true + gradient?: bool | *true } @cuetsy(kind="interface") Options: { @@ -42,6 +40,8 @@ composableKinds: PanelCfg: { sparkline?: bool | *true shape: "circle" | *"gauge" barWidthFactor: number | *0.5 + barShape: "flat" | "rounded" | *"flat" + endpointMarker?: "point" | "glow" | "none" | *"point" effects: GaugePanelEffects | *{} } @cuetsy(kind="interface") } diff --git a/public/app/plugins/panel/radialbar/panelcfg.gen.ts b/public/app/plugins/panel/radialbar/panelcfg.gen.ts index 24915c62ef1..e050a044f77 100644 --- a/public/app/plugins/panel/radialbar/panelcfg.gen.ts +++ b/public/app/plugins/panel/radialbar/panelcfg.gen.ts @@ -14,21 +14,19 @@ export interface GaugePanelEffects { barGlow?: boolean; centerGlow?: boolean; gradient?: boolean; - rounded?: boolean; - spotlight?: boolean; } export const defaultGaugePanelEffects: Partial = { barGlow: false, centerGlow: false, gradient: true, - rounded: false, - spotlight: false, }; export interface Options extends common.SingleStatBaseOptions { + barShape: ('flat' | 'rounded'); barWidthFactor: number; effects: GaugePanelEffects; + endpointMarker?: ('point' | 'glow' | 'none'); segmentCount: number; segmentSpacing: number; shape: ('circle' | 'gauge'); @@ -38,8 +36,10 @@ export interface Options extends common.SingleStatBaseOptions { } export const defaultOptions: Partial = { + barShape: 'flat', barWidthFactor: 0.5, effects: {}, + endpointMarker: 'point', segmentCount: 1, segmentSpacing: 0.3, shape: 'gauge', diff --git a/public/app/plugins/panel/radialbar/suggestions.ts b/public/app/plugins/panel/radialbar/suggestions.ts index 00896ae8458..eab5334ef40 100644 --- a/public/app/plugins/panel/radialbar/suggestions.ts +++ b/public/app/plugins/panel/radialbar/suggestions.ts @@ -18,19 +18,6 @@ const withDefaults = ( } }, }, - // styles: [{ - // name: t('gauge.suggestions.style.circular', 'Glowing'), - // options: { - // effects: { - // rounded: true, - // barGlow: true, - // centerGlow: true, - // spotlight: true, - // }, - // }, - // }, { - // name: t('gauge.suggestions.style.simple', 'Simple'), - // }] } satisfies VisualizationSuggestion); const MAX_GAUGES = 10; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6949fa04bb3..45d3ed1f04b 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -7931,11 +7931,7 @@ "suggestions": { "arc": "Gauge", "circular": "Circular gauge", - "no-thresholds": "Gauge - no thresholds", - "style": { - "circular": "Glowing", - "simple": "Simple" - } + "no-thresholds": "Gauge - no thresholds" }, "threshold": "Threshold {{value}}" }, @@ -12431,16 +12427,21 @@ }, "radialbar": { "config": { + "bar-shape": "Bar Style", + "bar-shape-flat": "Flat", + "bar-shape-rounded": "Rounded", "bar-width": "Bar width", "effects": { "bar-glow": "Bar glow", "center-glow": "Center glow", "gradient": "Gradient", - "label": "Effects", - "rounded-bars": "Rounded bars", - "spotlight": "Spotlight", - "spotlight-tooltip": "Only visible in dark themes" + "label": "Effects" }, + "endpoint-marker": "Endpoint marker", + "endpoint-marker-description": "Glow is only supported in dark mode", + "endpoint-marker-glow": "Glow", + "endpoint-marker-none": "None", + "endpoint-marker-point": "Point", "segment-count": "Segments", "segment-spacing": "Segment spacing", "shape": "Style",