Compare commits

...

58 Commits

Author SHA1 Message Date
Paul Marbach
6ff9d522bc Gauge: Update test dashboard to round two of the segment panels to whole numbers 2025-12-18 17:04:33 -05:00
Paul Marbach
9eb5be78f9 Merge branch 'fastfrwrd/sparkline-class-removal' into fastfrwrd/gauge-bug-bash 2025-12-18 16:55:26 -05:00
Paul Marbach
667be498d5 Merge branch 'fastfrwrd/112977-sparkline-annotations' into fastfrwrd/gauge-bug-bash 2025-12-18 16:05:50 -05:00
Paul Marbach
ee196f42d3 fix lint errors 2025-12-18 16:03:09 -05:00
Paul Marbach
7dd079b72e Gauge: Add guide dots for rounded bars to help with accuracy, update color logic for more consistent gradients 2025-12-18 16:02:14 -05:00
Paul Marbach
b5655a0c20 remove comment 2025-12-18 14:55:35 -05:00
Paul Marbach
9deb30d88f Sparkline: Add point annotations for some common calcs 2025-12-18 14:51:34 -05:00
Paul Marbach
4e6892bac8 Merge remote-tracking branch 'origin/main' into fastfrwrd/gauge-rounded-bars-accuracy 2025-12-18 14:46:59 -05:00
Paul Marbach
b123e24d86 addressing PR comments 2025-12-18 11:52:34 -05:00
Paul Marbach
c7af2be682 more testing and cleanup 2025-12-17 17:42:35 -05:00
Paul Marbach
2c9b7ca135 hide glow at small angles 2025-12-17 17:32:50 -05:00
Paul Marbach
5a1ae834e0 remove period for consistency 2025-12-17 17:26:43 -05:00
Paul Marbach
36bc5535f4 shore up testing a bit 2025-12-17 17:24:41 -05:00
Paul Marbach
46510e72f3 i18n 2025-12-17 14:58:25 -05:00
Paul Marbach
7d2475454c update gdev and fixtures 2025-12-17 14:39:02 -05:00
Paul Marbach
50fa37af53 change endpoint marks to be configurable 2025-12-17 14:36:01 -05:00
Paul Marbach
2d72990c17 move min degrees for start dot render to a const 2025-12-17 09:50:56 -05:00
Paul Marbach
9c1df45589 set opacity to 0.5 for dots 2025-12-17 09:49:29 -05:00
Paul Marbach
b815680a6b test all dark/light theme variants 2025-12-17 08:41:56 -05:00
Paul Marbach
32a63c0cf0 update gradient for fixed color 2025-12-17 08:37:23 -05:00
Paul Marbach
c4455f71da Sparkline: Restore to a function component 2025-12-16 15:36:08 -05:00
Paul Marbach
81bff1a39e update backend tests 2025-12-16 15:33:15 -05:00
Paul Marbach
9635f8ba4e a more sweeping disable of the color-contrast 2025-12-16 15:28:40 -05:00
Paul Marbach
206bc6c4d9 disable storybook test due to false positive 2025-12-16 14:54:04 -05:00
Paul Marbach
dfc5a07259 fix bad import 2025-12-16 14:14:31 -05:00
Paul Marbach
a8ff242a3f fix a couple of bugs 2025-12-16 14:06:43 -05:00
Paul Marbach
6042dfef89 more cleanup, optimization 2025-12-16 13:36:15 -05:00
Paul Marbach
0b89c821ad fix dev dashboard fixture 2025-12-16 12:31:17 -05:00
Paul Marbach
942ed4b4e8 add lots of tests and reorganize the code a bit 2025-12-16 11:15:19 -05:00
Paul Marbach
a8325b1e87 update gdev 2025-12-16 00:15:01 -05:00
Paul Marbach
ca1a431cc8 one more tweak 2025-12-15 23:56:52 -05:00
Paul Marbach
ed59edd88e Merge branch 'fastfrwrd/gauge-clip-path' into fastfrwrd/gauge-rounded-bars-accuracy 2025-12-15 23:46:36 -05:00
Paul Marbach
e21ca340be remove any other mentions of spotlights 2025-12-15 23:37:30 -05:00
Paul Marbach
bf40fbe064 fix backend migration tests 2025-12-15 23:34:11 -05:00
Paul Marbach
e3bced33a7 fixme comment 2025-12-15 23:25:39 -05:00
Paul Marbach
77138f640a its all working 2025-12-15 23:23:30 -05:00
Paul Marbach
b300bd8b85 refactoring defs into utils 2025-12-15 23:13:08 -05:00
Paul Marbach
59ec3cc8a9 wip: progress on everything 2025-12-15 22:29:53 -05:00
Paul Marbach
23e6c3301b wip: overhaul color in gauge 2025-12-15 20:40:38 -05:00
Paul Marbach
87521b0348 wip: using clip-path and CSS for drawing the gauge 2025-12-15 17:38:26 -05:00
Paul Marbach
0281ef1fed Merge remote-tracking branch 'origin/main' into fastfrwrd/gauge-rounded-bars-accuracy 2025-12-15 13:32:50 -05:00
Paul Marbach
f389a1aee2 more fixture updates 2025-12-15 13:06:43 -05:00
Paul Marbach
bbb770a325 fix spacing 2025-12-15 12:41:43 -05:00
Paul Marbach
d52aea6d32 update i18n and migration tests 2025-12-15 12:37:43 -05:00
Paul Marbach
3f1a2833a8 update rotation of gauge color 2025-12-15 12:18:34 -05:00
Paul Marbach
aa87720f4a fix segmented 2025-12-15 10:55:39 -05:00
Paul Marbach
0428d4d745 Merge remote-tracking branch 'origin/main' into fastfrwrd/gauge-rounded-bars-accuracy 2025-12-15 09:14:54 -05:00
Paul Marbach
c9387d0f44 remove spotlight, starting to make gradients a bit more predictable 2025-12-12 16:30:58 -05:00
Paul Marbach
1967134c45 30% width 2025-12-12 11:13:10 -05:00
Paul Marbach
1424016863 Merge remote-tracking branch 'origin/main' into fastfrwrd/gauge-rounded-bars-accuracy 2025-12-12 11:12:00 -05:00
Paul Marbach
5b229a80d3 Gauge: Add guide dots for rounded bars to help with accuracy 2025-12-12 10:41:07 -05:00
Paul Marbach
a5a9294784 remove new line 2025-12-11 18:20:06 -05:00
Paul Marbach
31e3a97ef2 Fix JSON formatting by adding missing newline 2025-12-11 18:18:50 -05:00
Paul Marbach
599b624fe9 fix migration test 2025-12-11 18:12:05 -05:00
Paul Marbach
76729fa866 more tweaks 2025-12-11 17:54:40 -05:00
Paul Marbach
1e0456da13 cohesive no data handling 2025-12-11 17:39:45 -05:00
Paul Marbach
adeb6e42b7 adjust text height and positions a little more 2025-12-11 17:24:02 -05:00
Paul Marbach
33a27eaa4d Gauge: Fit-and-finish tweaks to glows, text position, and sparkline size 2025-12-11 14:12:43 -05:00
42 changed files with 2270 additions and 2090 deletions

View File

@@ -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": [

View File

@@ -955,8 +955,6 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": false
},
"orientation": "auto",

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -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": [
@@ -1862,6 +1760,22 @@
"startValue": 0
}
],
"transformations": [
{
"id": "calculateField",
"options": {
"mode": "unary",
"reduce": {
"reducer": "sum"
},
"replaceFields": true,
"unary": {
"operator": "round",
"fieldName": "A-series"
}
}
}
],
"title": "Active gateways",
"type": "radialbar"
},
@@ -1900,7 +1814,7 @@
"h": 7,
"w": 5,
"x": 4,
"y": 59
"y": 46
},
"id": 14,
"maxDataPoints": 20,
@@ -1910,10 +1824,9 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true,
"rounded": true,
"spotlight": true
"gradient": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1928,8 +1841,7 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false,
"spotlight": true
"sparkline": false
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1947,6 +1859,22 @@
"startValue": 0
}
],
"transformations": [
{
"id": "calculateField",
"options": {
"mode": "unary",
"reduce": {
"reducer": "sum"
},
"replaceFields": true,
"unary": {
"operator": "round",
"fieldName": "A-series"
}
}
}
],
"title": "Active pods",
"type": "radialbar"
},
@@ -1984,7 +1912,7 @@
"h": 7,
"w": 5,
"x": 9,
"y": 59
"y": 46
},
"id": 15,
"maxDataPoints": 20,
@@ -1994,10 +1922,9 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true,
"rounded": true,
"spotlight": true
"gradient": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -2012,8 +1939,7 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false,
"spotlight": true
"sparkline": false
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -2068,7 +1994,7 @@
"h": 7,
"w": 6,
"x": 14,
"y": 59
"y": 46
},
"id": 16,
"maxDataPoints": 20,
@@ -2078,10 +2004,9 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true,
"rounded": true,
"spotlight": true
"gradient": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -2096,8 +2021,7 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false,
"spotlight": true
"sparkline": false
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -2124,7 +2048,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 66
"y": 53
},
"id": 35,
"panels": [],
@@ -2155,10 +2079,10 @@
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"h": 5,
"w": 12,
"x": 0,
"y": 67
"y": 54
},
"id": 36,
"options": {
@@ -2166,10 +2090,9 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true,
"rounded": false,
"spotlight": false
"gradient": true
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -2223,10 +2146,10 @@
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 6,
"y": 67
"h": 5,
"w": 12,
"x": 12,
"y": 54
},
"id": 37,
"options": {
@@ -2234,10 +2157,9 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true,
"rounded": false,
"spotlight": false
"gradient": true
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -2279,4 +2201,4 @@
"title": "Panel tests - Gauge (new)",
"uid": "panel-tests-gauge-new",
"weekStart": ""
}
}

View File

@@ -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": ""
}
}

View File

@@ -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": [
@@ -1818,6 +1718,22 @@
"startValue": 0
}
],
"transformations": [
{
"id": "calculateField",
"options": {
"mode": "unary",
"reduce": {
"reducer": "sum"
},
"replaceFields": true,
"unary": {
"operator": "round",
"fieldName": "A-series"
}
}
}
],
"title": "Active gateways",
"type": "radialbar"
},
@@ -1856,19 +1772,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 +1797,7 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false,
"spotlight": true
"sparkline": false
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1901,6 +1815,22 @@
"startValue": 0
}
],
"transformations": [
{
"id": "calculateField",
"options": {
"mode": "unary",
"reduce": {
"reducer": "sum"
},
"replaceFields": true,
"unary": {
"operator": "round",
"fieldName": "A-series"
}
}
}
],
"title": "Active pods",
"type": "radialbar"
},
@@ -1938,19 +1868,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 +1893,7 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false,
"spotlight": true
"sparkline": false
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -2020,19 +1948,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 +1973,7 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false,
"spotlight": true
"sparkline": false
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -2074,7 +2000,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 66
"y": 53
},
"id": 35,
"panels": [],
@@ -2105,20 +2031,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 +2096,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 +2148,6 @@
"timezone": "browser",
"title": "Panel tests - Gauge (new)",
"uid": "panel-tests-gauge-new",
"version": 9
"version": 22,
"weekStart": ""
}

View File

@@ -956,8 +956,6 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": false
}
}

View File

@@ -3,7 +3,7 @@ import { isEmpty } from 'lodash';
import { DataFrameView } from '../dataframe/DataFrameView';
import { getTimeField } from '../dataframe/processDataFrame';
import { GrafanaTheme2 } from '../themes/types';
import { reduceField, ReducerID } from '../transformations/fieldReducer';
import { isReducerID, reduceField, ReducerID } from '../transformations/fieldReducer';
import { getFieldMatcher } from '../transformations/matchers';
import { FieldMatcherID } from '../transformations/matchers/ids';
import { ScopedVars } from '../types/ScopedVars';
@@ -43,6 +43,7 @@ export interface FieldSparkline {
x?: Field; // if this does not exist, use the index
timeRange?: TimeRange; // Optionally force an absolute time
highlightIndex?: number;
highlightLine?: number;
}
export interface FieldDisplay {
@@ -72,6 +73,76 @@ export interface GetFieldDisplayValuesOptions {
export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
interface SparklineHighlightPoint {
type: 'point';
xIdx: number;
}
interface SparklineHighlightLine {
type: 'line';
y: number;
}
export function getSparklineHighlight(
sparkline: FieldSparkline,
calc: ReducerID
): SparklineHighlightPoint | SparklineHighlightLine | void {
switch (calc) {
case ReducerID.last:
return { type: 'point', xIdx: sparkline.y.values.length - 1 };
case ReducerID.first:
return { type: 'point', xIdx: 0 };
case ReducerID.lastNotNull: {
for (let k = sparkline.y.values.length - 1; k >= 0; k--) {
const v = sparkline.y.values[k];
if (v !== null && v !== undefined && !Number.isNaN(v)) {
return { type: 'point', xIdx: k };
}
}
return;
}
case ReducerID.firstNotNull: {
for (let k = 0; k < sparkline.y.values.length; k++) {
const v = sparkline.y.values[k];
if (v !== null && v !== undefined && !Number.isNaN(v)) {
return { type: 'point', xIdx: k };
}
}
return;
}
case ReducerID.min: {
let minIdx = -1;
let prevMin = Infinity;
for (let k = 0; k < sparkline.y.values.length; k++) {
const v = sparkline.y.values[k];
if (v !== null && v !== undefined && !Number.isNaN(v) && v < prevMin) {
prevMin = v;
minIdx = k;
}
}
return minIdx >= 0 ? { type: 'point', xIdx: minIdx } : undefined;
}
case ReducerID.max: {
let maxIdx = -1;
let prevMax = -Infinity;
for (let k = 0; k < sparkline.y.values.length; k++) {
const v = sparkline.y.values[k];
if (v !== null && v !== undefined && !Number.isNaN(v) && v > prevMax) {
prevMax = v;
maxIdx = k;
}
}
return maxIdx >= 0 ? { type: 'point', xIdx: maxIdx } : undefined;
}
case ReducerID.mean:
return { type: 'line', y: reduceField({ field: sparkline.y, reducers: [ReducerID.mean] }).mean };
case ReducerID.median:
return { type: 'line', y: reduceField({ field: sparkline.y, reducers: [ReducerID.median] }).median };
default:
return;
}
}
export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
const { replaceVariables, reduceOptions, timeZone, theme } = options;
const calcs = reduceOptions.calcs.length ? reduceOptions.calcs : [ReducerID.last];
@@ -190,62 +261,16 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
y: dataFrame.fields[i],
x: timeField,
};
let highlightIdx: number | undefined = (() => {
switch (calc) {
case ReducerID.last:
return sparkline.y.values.length - 1;
case ReducerID.first:
return 0;
// TODO: #112977 enable more reducers for highlight index
// case ReducerID.lastNotNull: {
// for (let k = sparkline.y.values.length - 1; k >= 0; k--) {
// const v = sparkline.y.values[k];
// if (v !== null && v !== undefined && !Number.isNaN(v)) {
// return k;
// }
// }
// return;
// }
// case ReducerID.firstNotNull: {
// for (let k = 0; k < sparkline.y.values.length; k++) {
// const v = sparkline.y.values[k];
// if (v !== null && v !== undefined && !Number.isNaN(v)) {
// return k;
// }
// }
// return;
// }
// case ReducerID.min: {
// let minIdx = -1;
// let prevMin = Infinity;
// for (let k = 0; k < sparkline.y.values.length; k++) {
// const v = sparkline.y.values[k];
// if (v !== null && v !== undefined && !Number.isNaN(v) && v < prevMin) {
// prevMin = v;
// minIdx = k;
// }
// }
// return minIdx >= 0 ? minIdx : undefined;
// }
// case ReducerID.max: {
// let maxIdx = -1;
// let prevMax = -Infinity;
// for (let k = 0; k < sparkline.y.values.length; k++) {
// const v = sparkline.y.values[k];
// if (v !== null && v !== undefined && !Number.isNaN(v) && v > prevMax) {
// prevMax = v;
// maxIdx = k;
// }
// }
// return maxIdx >= 0 ? maxIdx : undefined;
// }
default:
return;
if (isReducerID(calc)) {
const sparklineHighlight = getSparklineHighlight(sparkline, calc);
switch (sparklineHighlight?.type) {
case 'point':
sparkline.highlightIndex = sparklineHighlight.xIdx;
break;
case 'line':
sparkline.highlightLine = sparklineHighlight.y;
break;
}
})();
if (typeof highlightIdx === 'number') {
sparkline.highlightIndex = highlightIdx;
}
}

View File

@@ -16,21 +16,19 @@ export interface GaugePanelEffects {
barGlow?: boolean;
centerGlow?: boolean;
gradient?: boolean;
rounded?: boolean;
spotlight?: boolean;
}
export const defaultGaugePanelEffects: Partial<GaugePanelEffects> = {
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<Options> = {
barShape: 'flat',
barWidthFactor: 0.5,
effects: {},
endpointMarker: 'point',
segmentCount: 1,
segmentSpacing: 0.3,
shape: 'gauge',

View File

@@ -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<HTMLDivElement>['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 && (
<circle cx={xStart} cy={yStart} r={dotRadius} fill={pointColorStart} opacity={DOT_OPACITY} />
)}
<circle cx={xEnd} cy={yEnd} r={dotRadius} fill={pointColorEnd} opacity={DOT_OPACITY} />
</>
);
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 ? (
<path
d={['M', xStartMark, yStartMark, 'A', radius, radius, 0, 0, 1, xEnd, yEnd].join(' ')}
fill="none"
strokeWidth={barWidth}
stroke={endpointMarkerGlowFilter}
strokeLinecap={roundedBars ? 'round' : 'butt'}
filter={glowFilter}
/>
) : 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 */}
<clipPath id={id}>
<path d={path} />
</clipPath>
<g filter={glowFilter}>
<foreignObject
x={centerX - radius - barWidth}
y={centerY - radius - barWidth}
width={(radius + barWidth) * 2}
height={(radius + barWidth) * 2}
clipPath={`url(#${id})`}
>
<div style={bgDivStyle} />
</foreignObject>
{barEndcapColors?.[0] && <circle cx={xStart} cy={yStart} r={barWidth / 2} fill={barEndcapColors[0]} />}
{barEndcapColors?.[1] && (
<circle cx={xEnd} cy={yEnd} r={barWidth / 2} fill={barEndcapColors[1]} opacity={0.5} />
)}
</g>
{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 (
<path
d={path}
fill="none"
fillOpacity="1"
stroke={color}
strokeOpacity="1"
strokeWidth={barWidth}
filter={glowFilter}
strokeLinecap={roundedBars ? 'round' : 'butt'}
className="radial-arc-path"
/>
);
}
RadialArcPath.displayName = 'RadialArcPath';

View File

@@ -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 (
<>
<g>
{/** Track */}
<RadialArcPath
startAngle={startAngle + angle}
dimensions={dimensions}
arcLengthDeg={angleRange - angle}
color={theme.colors.action.hover}
roundedBars={roundedBars}
/>
{/** The colored bar */}
<RadialArcPath
dimensions={dimensions}
startAngle={startAngle}
arcLengthDeg={angle}
color={colorDefs.getMainBarColor()}
roundedBars={roundedBars}
glowFilter={glowFilter}
/>
{spotlightStroke && angle > 8 && (
<SpotlightSquareEffect
dimensions={dimensions}
angle={startAngle + angle}
glowFilter={glowFilter}
spotlightStroke={spotlightStroke}
theme={theme}
roundedBars={roundedBars}
/>
)}
</g>
<defs>{colorDefs.getDefs()}</defs>
{/** Track */}
<RadialArcPath
arcLengthDeg={angleRange - angle}
fieldDisplay={fieldDisplay}
color={theme.colors.action.hover}
dimensions={dimensions}
roundedBars={roundedBars}
shape={shape}
startAngle={startAngle + angle}
/>
{/** The colored bar */}
<RadialArcPath
arcLengthDeg={angle}
barEndcaps={shape === 'circle' && roundedBars}
dimensions={dimensions}
endpointMarker={roundedBars ? endpointMarker : undefined}
endpointMarkerGlowFilter={endpointMarkerGlowFilter}
fieldDisplay={fieldDisplay}
glowFilter={glowFilter}
roundedBars={roundedBars}
shape={shape}
startAngle={startAngle}
{...colorProps}
/>
</>
);
}
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 (
<path
d={path}
fill="none"
strokeWidth={barWidth}
stroke={spotlightStroke}
strokeLinecap={roundedBars ? 'round' : 'butt'}
filter={glowFilter}
/>
);
}

View File

@@ -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(
<RadialArcPath
key={i}
startAngle={segmentAngle}
dimensions={dimensions}
color={segmentColor}
glowFilter={glowFilter}
arcLengthDeg={segmentArcLengthDeg}
/>
);
segments.push(
<RadialArcPath
key={i}
arcLengthDeg={segmentArcLengthDeg}
dimensions={dimensions}
fieldDisplay={fieldDisplay}
glowFilter={glowFilter}
shape={shape}
startAngle={segmentAngle}
{...colorProps}
/>
);
}
return <g>{segments}</g>;
}
);
return (
<>
<g>{segments}</g>
<defs>{colorDefs.getDefs()}</defs>
</>
);
}
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 (
// <line
// x1={x1}
// y1={y1}
// x2={x2}
// y2={y2}
// fill="none"
// fillOpacity="0.85"
// stroke={color}
// strokeOpacity="1"
// strokeLinecap={roundedBars ? 'round' : 'butt'}
// strokeWidth={segmentWidth}
// strokeDasharray="0"
// filter={glow ? `url(#glow-${gaugeId})` : undefined}
// />
// );
// }
RadialBarSegmented.displayName = 'RadialBarSegmented';

View File

@@ -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<string, string> = {};
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(
<linearGradient x1="0" y1="0" x2={1 / valuePercent} y2="0" id={id}>
{colors.map((stopColor, i) => (
<stop key={i} offset={`${(i / (count - 1)).toFixed(2)}`} stopColor={stopColor} stopOpacity={1} />
))}
</linearGradient>
);
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(
<radialGradient
key={id}
id={id}
cx={dimensions.centerX}
cy={dimensions.centerY}
r={dimensions.radius + dimensions.barWidth / 2}
fr={dimensions.radius - dimensions.barWidth / 2}
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stopColor={tinycolor(baseColor).spin(20).lighten(10).toString()} stopOpacity={1} />
<stop offset="60%" stopColor={color1.toString()} stopOpacity={1} />
<stop offset="100%" stopColor={color1.toString()} stopOpacity={1} />
</radialGradient>
);
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(
<linearGradient
key={id}
id={id}
x1="0"
y1="0"
x2={x2}
y2={y2}
gradientUnits="userSpaceOnUse"
gradientTransform={transform}
>
{theme.isDark ? (
<>
<stop offset="0%" stopColor={color1.darken(10).toString()} stopOpacity={1} />
<stop offset="100%" stopColor={color2.lighten(10).toString()} stopOpacity={1} />
</>
) : (
<>
<stop offset="0%" stopColor={color2.lighten(10).toString()} stopOpacity={1} />
<stop offset="100%" stopColor={color1.toString()} stopOpacity={1} />
</>
)}
</linearGradient>
);
return returnColor;
}
getMainBarColor(): string {
return this.getColor(this.options.fieldDisplay.display.color ?? FALLBACK_COLOR);
}
getDefs(): React.ReactNode[] {
return this.defs;
}
}

View File

@@ -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<StoryProps> = {
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<StoryProps> = {
width: 200,
height: 200,
shape: 'circle',
gradient: 'none',
gradient: false,
seriesCount: 1,
segmentCount: 0,
segmentSpacing: 0.2,
@@ -56,14 +74,14 @@ const meta: Meta<StoryProps> = {
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<StoryProps> = (args) => {
<Stack direction={'column'} gap={3} wrap="wrap">
<div>Bar width</div>
<Stack direction="row" alignItems="center" gap={3} wrap="wrap">
<RadialGaugeExample
seriesName="0.1"
value={args.value ?? 30}
color="blue"
gradient="auto"
barWidthFactor={0.1}
/>
<RadialGaugeExample
seriesName="0.4"
value={args.value ?? 40}
color="green"
gradient="auto"
barWidthFactor={0.4}
/>
<RadialGaugeExample
seriesName="0.6"
value={args.value ?? 60}
color="red"
gradient="auto"
barWidthFactor={0.6}
/>
<RadialGaugeExample
seriesName="0.8"
value={args.value ?? 70}
color="purple"
gradient="auto"
barWidthFactor={0.8}
/>
<RadialGaugeExample seriesName="0.1" value={args.value ?? 30} color="blue" gradient barWidthFactor={0.1} />
<RadialGaugeExample seriesName="0.4" value={args.value ?? 40} color="green" gradient barWidthFactor={0.4} />
<RadialGaugeExample seriesName="0.6" value={args.value ?? 60} color="red" gradient barWidthFactor={0.6} />
<RadialGaugeExample seriesName="0.8" value={args.value ?? 70} color="purple" gradient barWidthFactor={0.8} />
</Stack>
<div>Effects</div>
<Stack direction="row" alignItems="center" gap={3} wrap="wrap">
<RadialGaugeExample value={args.value ?? 30} spotlight glowBar glowCenter color="blue" gradient="auto" />
<RadialGaugeExample value={args.value ?? 40} spotlight glowBar glowCenter color="green" gradient="auto" />
<RadialGaugeExample
value={args.value ?? 60}
spotlight
glowBar
glowCenter
color="red"
gradient="auto"
roundedBars
/>
<RadialGaugeExample
value={args.value ?? 70}
spotlight
glowBar
glowCenter
color="purple"
gradient="auto"
roundedBars
/>
<RadialGaugeExample value={args.value ?? 30} glowBar glowCenter color="blue" gradient />
<RadialGaugeExample value={args.value ?? 40} glowBar glowCenter color="green" gradient />
<RadialGaugeExample value={args.value ?? 60} glowBar glowCenter color="red" gradient roundedBars />
<RadialGaugeExample value={args.value ?? 70} glowBar glowCenter color="purple" gradient roundedBars />
</Stack>
<div>Shape: Gauge & color scale</div>
<Stack direction="row" alignItems="center" gap={3} wrap="wrap">
@@ -160,14 +138,14 @@ export const Examples: StoryFn<StoryProps> = (args) => {
value={40}
shape="gauge"
width={250}
gradient="auto"
gradient
colorScheme={FieldColorModeId.ContinuousGrYlRd}
glowCenter={true}
barWidthFactor={0.6}
/>
<RadialGaugeExample
colorScheme={FieldColorModeId.ContinuousGrYlRd}
gradient="auto"
gradient
width={250}
value={90}
barWidthFactor={0.6}
@@ -183,9 +161,8 @@ export const Examples: StoryFn<StoryProps> = (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<StoryProps> = (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<StoryProps> = (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<StoryProps> = (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<StoryProps> = (args) => {
<RadialGaugeExample
value={args.value ?? 70}
color="green"
gradient="auto"
gradient
glowCenter={true}
segmentCount={8}
segmentSpacing={0.1}
@@ -240,7 +214,7 @@ export const Examples: StoryFn<StoryProps> = (args) => {
<RadialGaugeExample
value={args.value ?? 30}
color="purple"
gradient="auto"
gradient
segmentCount={30}
glowCenter={true}
barWidthFactor={0.6}
@@ -248,7 +222,7 @@ export const Examples: StoryFn<StoryProps> = (args) => {
<RadialGaugeExample
value={args.value ?? 50}
color="red"
gradient="auto"
gradient
segmentCount={40}
glowCenter={true}
barWidthFactor={1}
@@ -260,7 +234,6 @@ export const Examples: StoryFn<StoryProps> = (args) => {
<RadialGaugeExample
value={args.value ?? 80}
colorScheme={FieldColorModeId.ContinuousGrYlRd}
spotlight
glowBar={true}
glowCenter={true}
segmentCount={20}
@@ -270,9 +243,8 @@ export const Examples: StoryFn<StoryProps> = (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<StoryProps> = (args) => {
<RadialGaugeExample
value={args.value ?? 70}
colorScheme={FieldColorModeId.Thresholds}
gradient="auto"
gradient
thresholdsBar={true}
roundedBars={false}
spotlight
glowCenter={true}
barWidthFactor={0.7}
/>
@@ -296,7 +267,7 @@ export const Examples: StoryFn<StoryProps> = (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<StoryProps> = (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<StoryProps> = (args) => {
shape="gauge"
roundedBars={false}
barWidthFactor={0.8}
spotlight
/>
</Stack>
);
};
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}
/>
);
}

View File

@@ -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(<RadialGaugeExample />);
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<typeof RadialGaugeExample> }>)(
'should render $description without throwing',
({ props }) => {
render(<RadialGaugeExample {...props} />);
expect(screen.getByRole('img')).toBeInTheDocument();
}
);
it('should render threshold labels', () => {
render(<RadialGaugeExample showScaleLabels={true} />);

View File

@@ -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(
<SpotlightGradient
key={spotlightGradientId}
@@ -171,7 +156,8 @@ export function RadialGauge(props: RadialGaugeProps) {
glowFilter={`url(#${glowFilterId})`}
segmentCount={segmentCount}
segmentSpacing={segmentSpacing}
colorDefs={colorDefs}
shape={shape}
gradient={gradientStops}
/>
);
} else {
@@ -179,13 +165,16 @@ export function RadialGauge(props: RadialGaugeProps) {
<RadialBar
key={`radial-bar-${barIndex}-${gaugeId}`}
dimensions={dimensions}
colorDefs={colorDefs}
angle={angle}
angleRange={angleRange}
startAngle={startAngle}
roundedBars={roundedBars}
spotlightStroke={`url(#${spotlightGradientId})`}
glowFilter={`url(#${glowFilterId})`}
endpointMarkerGlowFilter={`url(#${spotlightGradientId})`}
shape={shape}
gradient={gradientStops}
fieldDisplay={displayValue}
endpointMarker={endpointMarker}
/>
);
}
@@ -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({

View File

@@ -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 (
<g>
{thresholds.map((threshold, index) => {
const labelPos = getTextPosition(String(threshold.value), threshold.value, index);
return (
<text
key={index}
x={labelPos.x}
y={labelPos.y}
fontSize={fontSize}
fill={theme.colors.text.primary}
transform={labelPos.transform}
aria-label={t(`gauge.threshold`, 'Threshold {{value}}', { value: threshold.value })}
>
{threshold.value}
</text>
);
})}
</g>
);
}
);
return (
<g>
{thresholds.map((threshold, index) => {
const labelPos = getTextPosition(String(threshold.value), threshold.value, index);
return (
<text
key={index}
x={labelPos.x}
y={labelPos.y}
fontSize={fontSize}
fill={theme.colors.text.primary}
transform={labelPos.transform}
aria-label={t(`gauge.threshold`, 'Threshold {{value}}', { value: threshold.value })}
>
{threshold.value}
</text>
);
})}
</g>
);
}
RadialScaleLabels.displayName = 'RadialScaleLabels';

View File

@@ -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<RadialTextMode, 'auto'>;
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<GraphFieldConfig> = useMemo(
() => ({
color: {
mode: 'fixed',
fixedColor: color ?? 'blue',
},
custom: {
gradientMode: GraphGradientMode.Opacity,
fillOpacity: 40,
lineInterpolation: LineInterpolation.Smooth,
},
}),
[color]
);
if (!sparkline) {
return null;
}
return (
<div style={{ position: 'absolute', top: topPos }}>
<Sparkline height={height} width={width} sparkline={sparkline} theme={theme} config={config} showHighlights />
</div>
);
}
);
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<GraphFieldConfig> = {
color: {
mode: 'fixed',
fixedColor: color ?? 'blue',
},
custom: {
gradientMode: GraphGradientMode.Opacity,
fillOpacity: 40,
lineInterpolation: LineInterpolation.Smooth,
},
};
return (
<div style={{ position: 'absolute', top: topPos }}>
<Sparkline height={height} width={width} sparkline={sparkline} theme={theme} config={config} />
</div>
);
}
RadialSparkline.displayName = 'RadialSparkline';

View File

@@ -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<RadialTextMode, 'auto'>;
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 (
<g transform={`translate(0, ${yOffset})`}>
{showValue && (
<text
x={centerX}
y={valueY}
fontSize={valueFontSize}
fill={theme.colors.text.primary}
className={styles.text}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan fontSize={unitFontSize}>{displayValue.prefix ?? ''}</tspan>
<tspan>{displayValue.text}</tspan>
<tspan className={styles.text} fontSize={unitFontSize} dy={suffixShift}>
{displayValue.suffix ?? ''}
</tspan>
</text>
)}
{showName && (
<text
fontSize={nameFontSize}
x={centerX}
y={nameY}
textAnchor="middle"
dominantBaseline="middle"
fill={nameColor}
>
{displayValue.title}
</text>
)}
</g>
);
}
);
return (
<g transform={`translate(0, ${yOffset})`}>
{showValue && (
<text
x={centerX}
y={valueY}
fontSize={valueFontSize}
fill={theme.colors.text.primary}
className={styles.text}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan fontSize={unitFontSize}>{displayValue.prefix ?? ''}</tspan>
<tspan>{displayValue.text}</tspan>
<tspan className={styles.text} fontSize={unitFontSize} dy={suffixShift}>
{displayValue.suffix ?? ''}
</tspan>
</text>
)}
{showName && (
<text
fontSize={nameFontSize}
x={centerX}
y={nameY}
textAnchor="middle"
dominantBaseline="middle"
fill={nameColor}
>
{displayValue.title}
</text>
)}
</g>
);
}
RadialText.displayName = 'RadialText';
const getStyles = (theme: GrafanaTheme2) => ({
const getStyles = (_theme: GrafanaTheme2) => ({
text: css({
verticalAlign: 'bottom',
}),

View File

@@ -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(
<RadialArcPath
key={i}
startAngle={currentStart}
arcLengthDeg={lengthDeg}
barEndcaps={shape === 'circle' && roundedBars}
dimensions={thresholdDimensions}
roundedBars={roundedBars}
fieldDisplay={fieldDisplay}
glowFilter={glowFilter}
color={colorDefs.getColor(threshold.color, true)}
roundedBars={roundedBars}
shape={shape}
startAngle={currentStart}
{...colorProps}
/>
);
currentStart += lengthDeg;
}
return (
<>
<g>{paths}</g>
<defs>{colorDefs.getDefs()}</defs>
</>
);
return <g>{paths}</g>;
}

View File

@@ -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,
},
]
`;

View File

@@ -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"`;

View File

@@ -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<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
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');
});
});
});

View File

@@ -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)];
}

View File

@@ -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 (
<filter id={id} filterUnits="userSpaceOnUse">
@@ -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 (
<linearGradient x1={x1} y1={y1} x2={x2} y2={y2} id={id} gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor={'black'} stopOpacity={0.0} />
<stop offset="90%" stopColor={'black'} stopOpacity={0.0} />
<stop offset="91%" stopColor={'black'} stopOpacity={1} />
</linearGradient>
);
}
return (
<linearGradient x1={x1} y1={y1} x2={x2} y2={y2} id={id} gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor={'white'} stopOpacity={0.0} />
<stop offset="95%" stopColor={'white'} stopOpacity={0.5} />
{roundedBars && <stop offset="100%" stopColor={'white'} stopOpacity={roundedBars ? 0.7 : 1} />}
</linearGradient>
);
}
const CENTER_GLOW_OPACITY = 0.15;
export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) {
return (
<radialGradient id={`circle-glow-${gaugeId}`} r={'50%'} fr={'0%'}>
<stop offset="0%" stopColor={color} stopOpacity={0.2} />
<radialGradient id={`circle-glow-${gaugeId}`} r="50%" fr="0%">
<stop offset="0%" stopColor={color} stopOpacity={CENTER_GLOW_OPACITY} />
<stop offset="90%" stopColor={color} stopOpacity={0} />
</radialGradient>
);
}
export interface CenterGlowProps {
dimensions: GaugeDimensions;
dimensions: RadialGaugeDimensions;
gaugeId: string;
color?: string;
}
@@ -82,8 +48,8 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps
return (
<>
<defs>
<radialGradient id={gradientId} r={'50%'} fr={'0%'}>
<stop offset="0%" stopColor={color} stopOpacity={0.15} />
<radialGradient id={gradientId} r="50%" fr="0%">
<stop offset="0%" stopColor={color} stopOpacity={CENTER_GLOW_OPACITY} />
<stop offset="90%" stopColor={color} stopOpacity={0} />
</radialGradient>
</defs>
@@ -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 (
<linearGradient x1={x1} y1={y1} x2={x2} y2={y2} id={id} gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor={'white'} stopOpacity={0.0} />
<stop offset="95%" stopColor={'white'} stopOpacity={0.5} />
{roundedBars && <stop offset="100%" stopColor={'white'} stopOpacity={roundedBars ? 0.7 : 1} />}
</linearGradient>
);
}

View File

@@ -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;
}

View File

@@ -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<RadialGaugeProps & { barIndex: number }> = {}) {
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<RadialGaugeProps & { barIndex: number }> = {}) {
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);
});
});
});

View File

@@ -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);
}

View File

@@ -14,30 +14,20 @@ export interface SparklineProps extends Themeable2 {
height: number;
config?: FieldConfig<GraphFieldConfig>;
sparkline: FieldSparkline;
showHighlights?: boolean;
}
const SparklineFn: React.FC<SparklineProps> = memo((props) => {
const { sparkline, config: fieldConfig, theme, width, height } = props;
const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, fieldConfig);
export const Sparkline: React.FC<SparklineProps> = memo((props) => {
const { sparkline, config: fieldConfig, theme, width, height, showHighlights } = props;
const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, theme, fieldConfig, showHighlights);
if (warning) {
return null;
}
const data = preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame));
const configBuilder = prepareConfig(sparkline, alignedDataFrame, theme);
const configBuilder = prepareConfig(sparkline, alignedDataFrame, theme, showHighlights);
return <UPlotChart data={data} config={configBuilder} width={width} height={height} />;
});
SparklineFn.displayName = 'Sparkline';
// we converted to function component above, but some apps extend Sparkline, so we need
// to keep exporting a class component until those apps are all rolled out.
// see https://github.com/grafana/app-observability-plugin/pull/2079
// eslint-disable-next-line react-prefer-function-component/react-prefer-function-component
export class Sparkline extends React.PureComponent<SparklineProps> {
render() {
return <SparklineFn {...this.props} />;
}
}
Sparkline.displayName = 'Sparkline';

View File

@@ -2,6 +2,7 @@ import { Range } from 'uplot';
import {
applyNullInsertThreshold,
// colorManipulator,
DataFrame,
FieldConfig,
FieldSparkline,
@@ -22,6 +23,7 @@ import {
VisibilityMode,
ScaleDirection,
ScaleOrientation,
// FieldColorModeId,
} from '@grafana/schema';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
@@ -112,8 +114,7 @@ export function getYRange(alignedFrame: DataFrame): Range.MinMax {
return [roundedMin, roundedMax];
}
// TODO: #112977 enable highlight index
// const HIGHLIGHT_IDX_POINT_SIZE = 6;
const HIGHLIGHT_IDX_POINT_SIZE = 6;
const defaultConfig: GraphFieldConfig = {
drawStyle: GraphDrawStyle.Line,
@@ -124,7 +125,9 @@ const defaultConfig: GraphFieldConfig = {
export const prepareSeries = (
sparkline: FieldSparkline,
fieldConfig?: FieldConfig<GraphFieldConfig>
_theme: GrafanaTheme2,
fieldConfig?: FieldConfig<GraphFieldConfig>,
_showHighlights?: boolean
): { frame: DataFrame; warning?: string } => {
const frame = nullToValue(preparePlotFrame(sparkline, fieldConfig));
if (frame.fields.some((f) => f.values.length <= 1)) {
@@ -136,16 +139,41 @@ export const prepareSeries = (
frame,
};
}
// TODO:rgb(24, 24, 24) will address this.
// if (showHighlights && typeof sparkline.highlightLine === 'number') {
// const highlightY = sparkline.highlightLine;
// const colorMode = getFieldColorModeForField(sparkline.y);
// const seriesColor = colorMode.getCalculator(sparkline.y, theme)(highlightY, 0);
// frame.fields.push({
// name: 'highlightLine',
// type: FieldType.number,
// values: new Array(frame.length).fill(highlightY),
// config: {
// color: {
// mode: FieldColorModeId.Fixed,
// fixedColor: colorManipulator.lighten(seriesColor, 0.5),
// },
// custom: {
// lineStyle: {
// fill: 'dash',
// dash: [5, 2],
// },
// },
// },
// state: {},
// });
// }
return { frame };
};
export const prepareConfig = (
sparkline: FieldSparkline,
dataFrame: DataFrame,
theme: GrafanaTheme2
theme: GrafanaTheme2,
showHighlights?: boolean
): UPlotConfigBuilder => {
const builder = new UPlotConfigBuilder();
// const rangePad = HIGHLIGHT_IDX_POINT_SIZE / 2;
const rangePad = HIGHLIGHT_IDX_POINT_SIZE / 2;
builder.setCursor({
show: false,
@@ -206,13 +234,14 @@ export const prepareConfig = (
const colorMode = getFieldColorModeForField(field);
const seriesColor = colorMode.getCalculator(field, theme)(0, 0);
// TODO: #112977 enable highlight index and adjust padding accordingly
// const hasHighlightIndex = typeof sparkline.highlightIndex === 'number';
// if (hasHighlightIndex) {
// builder.setPadding([rangePad, rangePad, rangePad, rangePad]);
// }
const hasHighlightIndex = showHighlights && typeof sparkline.highlightIndex === 'number';
if (hasHighlightIndex) {
builder.setPadding([rangePad, rangePad, rangePad, rangePad]);
}
const pointsMode =
customConfig.drawStyle === GraphDrawStyle.Points // || hasHighlightIndex
customConfig.drawStyle === GraphDrawStyle.Points || hasHighlightIndex
? VisibilityMode.Always
: customConfig.showPoints;
@@ -227,9 +256,8 @@ export const prepareConfig = (
lineWidth: customConfig.lineWidth,
lineInterpolation: customConfig.lineInterpolation,
showPoints: pointsMode,
// TODO: #112977 enable highlight index
pointSize: /* hasHighlightIndex ? HIGHLIGHT_IDX_POINT_SIZE : */ customConfig.pointSize,
// pointsFilter: hasHighlightIndex ? [sparkline.highlightIndex!] : undefined,
pointSize: hasHighlightIndex ? HIGHLIGHT_IDX_POINT_SIZE : customConfig.pointSize,
pointsFilter: hasHighlightIndex ? [sparkline.highlightIndex!] : undefined,
fillOpacity: customConfig.fillOpacity,
fillColor: customConfig.fillColor,
lineStyle: customConfig.lineStyle,

View File

@@ -44,11 +44,6 @@ export function EffectsEditor(props: StandardEditorProps<GaugePanelEffects>) {
value={!!props.value?.gradient}
onChange={(e) => props.onChange({ ...props.value, gradient: e.currentTarget.checked })}
/>
<EffectsEditorInput
label={t('radialbar.config.effects.rounded-bars', 'Rounded bars')}
value={!!props.value?.rounded}
onChange={(e) => props.onChange({ ...props.value, rounded: e.currentTarget.checked })}
/>
<EffectsEditorInput
label={t('radialbar.config.effects.bar-glow', 'Bar glow')}
value={!!props.value?.barGlow}
@@ -59,12 +54,6 @@ export function EffectsEditor(props: StandardEditorProps<GaugePanelEffects>) {
value={!!props.value?.centerGlow}
onChange={(e) => props.onChange({ ...props.value, centerGlow: e.currentTarget.checked })}
/>
<EffectsEditorInput
label={t('radialbar.config.effects.spotlight', 'Spotlight')}
tooltip={t('radialbar.config.effects.spotlight-tooltip', 'Only visible in dark themes')}
value={!!props.value?.spotlight}
onChange={(e) => props.onChange({ ...props.value, spotlight: e.currentTarget.checked })}
/>
</Grid>
);
}

View File

@@ -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}
/>
);

View File

@@ -69,6 +69,36 @@ export const plugin = new PanelPlugin<Options>(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'),

View File

@@ -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")
}

View File

@@ -14,21 +14,19 @@ export interface GaugePanelEffects {
barGlow?: boolean;
centerGlow?: boolean;
gradient?: boolean;
rounded?: boolean;
spotlight?: boolean;
}
export const defaultGaugePanelEffects: Partial<GaugePanelEffects> = {
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<Options> = {
barShape: 'flat',
barWidthFactor: 0.5,
effects: {},
endpointMarker: 'point',
segmentCount: 1,
segmentSpacing: 0.3,
shape: 'gauge',

View File

@@ -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<Options, GraphFieldConfig>);
const MAX_GAUGES = 10;

View File

@@ -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",