From 33a27eaa4d2ab09eaa334c58ebe8e3231a84976e Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 11 Dec 2025 13:01:09 -0500 Subject: [PATCH 01/48] Gauge: Fit-and-finish tweaks to glows, text position, and sparkline size --- .../components/RadialGauge/RadialGauge.tsx | 12 +++-- .../RadialGauge/RadialSparkline.tsx | 29 +++++++---- .../src/components/RadialGauge/RadialText.tsx | 48 ++++++++++--------- .../src/components/RadialGauge/effects.tsx | 8 ++-- 4 files changed, 58 insertions(+), 39 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index 18147e0cac5..fadabf8ec72 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -106,6 +106,11 @@ export function RadialGauge(props: RadialGaugeProps) { const gaugeId = useId(); const styles = useStyles2(getStyles); + let effectiveTextMode = textMode; + if (effectiveTextMode === 'auto') { + effectiveTextMode = vizCount === 1 ? 'value' : 'value_and_name'; + } + const startAngle = shape === 'gauge' ? 250 : 0; const endAngle = shape === 'gauge' ? 110 : 360; @@ -188,7 +193,7 @@ export function RadialGauge(props: RadialGaugeProps) { // These elements are only added for first value / bar if (barIndex === 0) { if (glowBar) { - defs.push(); + defs.push(); } if (glowCenter) { @@ -198,14 +203,14 @@ export function RadialGauge(props: RadialGaugeProps) { graphics.push( ); @@ -254,6 +259,7 @@ export function RadialGauge(props: RadialGaugeProps) { theme={theme} color={color} shape={shape} + textMode={effectiveTextMode} /> ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx index 7a25fe3201a..a1011385cfa 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx @@ -1,11 +1,12 @@ import { css } from '@emotion/css'; +import { useMemo } from 'react'; import { FieldDisplay, GrafanaTheme2, FieldConfig } from '@grafana/data'; import { GraphFieldConfig, GraphGradientMode, LineInterpolation } from '@grafana/schema'; import { Sparkline } from '../Sparkline/Sparkline'; -import { RadialShape } from './RadialGauge'; +import { RadialShape, RadialTextMode } from './RadialGauge'; import { GaugeDimensions } from './utils'; interface RadialSparklineProps { @@ -14,19 +15,29 @@ interface RadialSparklineProps { theme: GrafanaTheme2; color?: string; shape?: RadialShape; + textMode: Exclude; } -export function RadialSparkline({ sparkline, dimensions, theme, color, shape }: RadialSparklineProps) { +export function RadialSparkline({ sparkline, dimensions, theme, color, shape, textMode }: RadialSparklineProps) { + const { radius, barWidth } = dimensions; + + const showName = textMode === 'name' || textMode === 'value_and_name'; + const height = showName ? radius / 4 : radius / 3; + const widthFactor = shape === 'gauge' ? 1.6 : 1.4; + const width = radius * widthFactor - barWidth; + const topPos = useMemo(() => { + if (!sparkline) { + return ''; + } + if (shape === 'gauge') { + return `${dimensions.gaugeBottomY - height}px`; + } + return `calc(50% + ${radius / 3.3}px)`; + }, [sparkline, dimensions.gaugeBottomY, height, radius, shape]); + if (!sparkline) { return null; } - const { radius, barWidth } = dimensions; - - const height = radius / 4; - const widthFactor = shape === 'gauge' ? 1.6 : 1.4; - const width = radius * widthFactor - barWidth; - const topPos = shape === 'gauge' ? `${dimensions.gaugeBottomY - height}px` : `calc(50% + ${radius / 2.8}px)`; - const styles = css({ position: 'absolute', top: topPos, diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx index d01a2d99570..530dcbe3a1a 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx @@ -1,6 +1,12 @@ import { css } from '@emotion/css'; -import { DisplayValue, DisplayValueAlignmentFactors, formattedValueToString, GrafanaTheme2 } from '@grafana/data'; +import { + DisplayValue, + DisplayValueAlignmentFactors, + FieldSparkline, + formattedValueToString, + GrafanaTheme2, +} from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; import { calculateFontSize } from '../../utils/measureText'; @@ -8,21 +14,13 @@ import { calculateFontSize } from '../../utils/measureText'; import { RadialShape, RadialTextMode } from './RadialGauge'; import { GaugeDimensions } from './utils'; -// function toCartesian(centerX: number, centerY: number, radius: number, angleInDegrees: number) { -// let radian = ((angleInDegrees - 90) * Math.PI) / 180.0; -// return { -// x: centerX + radius * Math.cos(radian), -// y: centerY + radius * Math.sin(radian), -// }; -// } - interface RadialTextProps { displayValue: DisplayValue; theme: GrafanaTheme2; dimensions: GaugeDimensions; - textMode: RadialTextMode; - vizCount: number; + textMode: Exclude; shape: RadialShape; + sparkline?: FieldSparkline; alignmentFactors?: DisplayValueAlignmentFactors; valueManualFontSize?: number; nameManualFontSize?: number; @@ -33,8 +31,8 @@ export function RadialText({ theme, dimensions, textMode, - vizCount, shape, + sparkline, alignmentFactors, valueManualFontSize, nameManualFontSize, @@ -46,10 +44,6 @@ export function RadialText({ return null; } - if (textMode === 'auto') { - textMode = vizCount === 1 ? 'value' : 'value_and_name'; - } - const nameToAlignTo = (alignmentFactors ? alignmentFactors.title : displayValue.title) ?? ''; const valueToAlignTo = formattedValueToString(alignmentFactors ? alignmentFactors : displayValue); @@ -59,7 +53,7 @@ export function RadialText({ // Not sure where this comes from but svg text is not using body line-height const lineHeight = 1.21; - const valueWidthToRadiusFactor = 0.85; + const valueWidthToRadiusFactor = 0.82; const nameToHeightFactor = 0.45; const largeRadiusScalingDecay = 0.86; @@ -104,12 +98,22 @@ export function RadialText({ const nameColor = showValue ? theme.colors.text.secondary : theme.colors.text.primary; const suffixShift = (valueFontSize - unitFontSize * 1.2) / 2; - // For gauge shape we shift text up a bit - const valueDy = shape === 'gauge' ? -valueFontSize * 0.3 : 0; - const nameDy = shape === 'gauge' ? -nameFontSize * 0.7 : 0; + // adjust the text up on gauges and when sparklines are present + let dy = 0; + if (shape === 'gauge') { + if (showName) { + dy -= nameFontSize * 0.2; + } + if (showValue) { + dy -= valueFontSize * 0.3; + } + } + if (sparkline) { + dy -= 8; + } return ( - + {showValue && ( {displayValue.prefix ?? ''} {displayValue.text} @@ -133,7 +136,6 @@ export function RadialText({ fontSize={nameFontSize} x={centerX} y={nameY} - dy={nameDy} textAnchor="middle" dominantBaseline="middle" fill={nameColor} diff --git a/packages/grafana-ui/src/components/RadialGauge/effects.tsx b/packages/grafana-ui/src/components/RadialGauge/effects.tsx index 551d9d91186..52c4c3621fa 100644 --- a/packages/grafana-ui/src/components/RadialGauge/effects.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/effects.tsx @@ -4,11 +4,11 @@ import { GaugeDimensions } from './utils'; export interface GlowGradientProps { id: string; - radius: number; + barWidth: number; } -export function GlowGradient({ id, radius }: GlowGradientProps) { - const glowSize = 0.02 * radius; +export function GlowGradient({ id, barWidth }: GlowGradientProps) { + const glowSize = 0.75 + barWidth * 0.08; return ( @@ -82,7 +82,7 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps <> - + From adeb6e42b7290aaf58df1843587b24c233ec017d Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 11 Dec 2025 17:24:02 -0500 Subject: [PATCH 02/48] adjust text height and positions a little more --- .../RadialGauge/RadialSparkline.tsx | 21 +++++++++---------- .../src/components/RadialGauge/RadialText.tsx | 5 ++--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx index a1011385cfa..c9da81f6cbf 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx @@ -1,4 +1,3 @@ -import { css } from '@emotion/css'; import { useMemo } from 'react'; import { FieldDisplay, GrafanaTheme2, FieldConfig } from '@grafana/data'; @@ -20,8 +19,8 @@ interface RadialSparklineProps { export function RadialSparkline({ sparkline, dimensions, theme, color, shape, textMode }: RadialSparklineProps) { const { radius, barWidth } = dimensions; - const showName = textMode === 'name' || textMode === 'value_and_name'; - const height = showName ? radius / 4 : radius / 3; + const showNameAndValue = textMode === 'value_and_name'; + const height = showNameAndValue ? radius / 4 : radius / 3; const widthFactor = shape === 'gauge' ? 1.6 : 1.4; const width = radius * widthFactor - barWidth; const topPos = useMemo(() => { @@ -31,18 +30,13 @@ export function RadialSparkline({ sparkline, dimensions, theme, color, shape, te if (shape === 'gauge') { return `${dimensions.gaugeBottomY - height}px`; } - return `calc(50% + ${radius / 3.3}px)`; - }, [sparkline, dimensions.gaugeBottomY, height, radius, shape]); + return `calc(50% + ${radius / (showNameAndValue ? 3.3 : 4)}px)`; + }, [sparkline, shape, radius, dimensions.gaugeBottomY, height, showNameAndValue]); if (!sparkline) { return null; } - const styles = css({ - position: 'absolute', - top: topPos, - }); - const config: FieldConfig = { color: { mode: 'fixed', @@ -56,7 +50,12 @@ export function RadialSparkline({ sparkline, dimensions, theme, color, shape, te }; return ( -
+
); diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx index 530dcbe3a1a..64e8ad28a0d 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx @@ -92,9 +92,8 @@ export function RadialText({ const valueHeight = valueFontSize * lineHeight; const nameHeight = nameFontSize * lineHeight; - const valueY = showName ? centerY - nameHeight / 2 : centerY; - const valueNameSpacing = valueHeight / 3.5; - const nameY = showValue ? valueY + valueHeight / 2 + valueNameSpacing : centerY; + 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; From 1e0456da139f7630185987bcd7f1465e46629be2 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 11 Dec 2025 17:39:45 -0500 Subject: [PATCH 03/48] cohesive no data handling --- public/app/plugins/panel/radialbar/RadialBarPanel.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx index fa064037157..bd1376229b2 100644 --- a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx +++ b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx @@ -3,10 +3,12 @@ import type { JSX } from 'react'; import { DisplayValueAlignmentFactors, FieldDisplay, + FieldType, getDisplayValueAlignmentFactors, getFieldDisplayValues, PanelProps, } from '@grafana/data'; +import { PanelDataErrorView } from '@grafana/runtime'; import { DataLinksContextMenu, Stack, VizRepeater, VizRepeaterRenderValueProps } from '@grafana/ui'; import { DataLinksContextMenuApi, RadialGauge } from '@grafana/ui/internal'; import { config } from 'app/core/config'; @@ -14,6 +16,7 @@ import { config } from 'app/core/config'; import { Options } from './panelcfg.gen'; export function RadialBarPanel({ + id, height, width, data, @@ -88,6 +91,10 @@ export function RadialBarPanel({ const minVizHeight = 60; const minVizWidth = 60; + if (getValues()[0]?.display?.text === 'No data') { + return ; + } + return ( Date: Thu, 11 Dec 2025 17:54:40 -0500 Subject: [PATCH 04/48] more tweaks --- .../panel-gauge/gauge_tests_new.json | 308 +++++++++++++----- .../RadialGauge/RadialSparkline.tsx | 31 +- .../src/components/RadialGauge/RadialText.tsx | 14 +- .../src/components/RadialGauge/effects.tsx | 1 + .../panel/radialbar/RadialBarPanel.tsx | 1 - 5 files changed, 233 insertions(+), 122 deletions(-) diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index f9ee5a8c4e3..967aae83c0e 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -18,7 +18,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 525, + "id": 1943, "links": [], "panels": [ { @@ -75,9 +75,9 @@ "effects": { "barGlow": false, "centerGlow": false, + "gradient": false, "rounded": true, - "spotlight": false, - "gradient": false + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -92,7 +92,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -152,9 +152,9 @@ "effects": { "barGlow": false, "centerGlow": true, + "gradient": false, "rounded": true, - "spotlight": false, - "gradient": false + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -169,7 +169,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -229,9 +229,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": false, "rounded": true, - "spotlight": false, - "gradient": false + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -246,7 +246,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -306,9 +306,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": false, "rounded": true, - "spotlight": true, - "gradient": false + "spotlight": true }, "orientation": "auto", "reduceOptions": { @@ -323,7 +323,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -383,9 +383,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": false, "rounded": true, - "spotlight": true, - "gradient": false + "spotlight": true }, "orientation": "auto", "reduceOptions": { @@ -400,7 +400,7 @@ "showThresholdMarkers": false, "sparkline": true }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -460,9 +460,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": false, "rounded": false, - "spotlight": true, - "gradient": false + "spotlight": true }, "orientation": "auto", "reduceOptions": { @@ -477,7 +477,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -537,9 +537,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": false, "rounded": false, - "spotlight": true, - "gradient": false + "spotlight": true }, "orientation": "auto", "reduceOptions": { @@ -554,7 +554,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -627,9 +627,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": false, "rounded": true, - "spotlight": true, - "gradient": false + "spotlight": true }, "orientation": "auto", "reduceOptions": { @@ -644,7 +644,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -704,9 +704,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": false, "rounded": true, - "spotlight": true, - "gradient": false + "spotlight": true }, "orientation": "auto", "reduceOptions": { @@ -721,7 +721,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -781,9 +781,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": false, "rounded": true, - "spotlight": true, - "gradient": false + "spotlight": true }, "orientation": "auto", "reduceOptions": { @@ -798,7 +798,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -858,9 +858,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": false, "rounded": true, - "spotlight": true, - "gradient": false + "spotlight": true }, "orientation": "auto", "reduceOptions": { @@ -875,7 +875,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -952,9 +952,9 @@ "effects": { "barGlow": false, "centerGlow": false, + "gradient": false, "rounded": false, - "spotlight": false, - "gradient": false + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -969,7 +969,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -1029,9 +1029,9 @@ "effects": { "barGlow": false, "centerGlow": false, + "gradient": false, "rounded": false, - "spotlight": false, - "gradient": false + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -1046,7 +1046,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -1106,9 +1106,9 @@ "effects": { "barGlow": false, "centerGlow": false, + "gradient": true, "rounded": false, - "spotlight": false, - "gradient": true + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -1123,7 +1123,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -1183,9 +1183,9 @@ "effects": { "barGlow": false, "centerGlow": false, + "gradient": false, "rounded": false, - "spotlight": false, - "gradient": false + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -1200,7 +1200,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -1260,9 +1260,9 @@ "effects": { "barGlow": false, "centerGlow": false, + "gradient": false, "rounded": false, - "spotlight": false, - "gradient": false + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -1277,7 +1277,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -1354,9 +1354,9 @@ "effects": { "barGlow": false, "centerGlow": false, + "gradient": true, "rounded": false, - "spotlight": false, - "gradient": true + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -1371,7 +1371,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -1435,9 +1435,9 @@ "effects": { "barGlow": false, "centerGlow": false, + "gradient": true, "rounded": false, - "spotlight": false, - "gradient": true + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -1452,7 +1452,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -1516,9 +1516,9 @@ "effects": { "barGlow": false, "centerGlow": false, + "gradient": true, "rounded": false, - "spotlight": false, - "gradient": true + "spotlight": false }, "orientation": "auto", "reduceOptions": { @@ -1533,7 +1533,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "alias": "1", @@ -1565,7 +1565,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1606,9 +1605,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": true, "rounded": true, - "spotlight": true, - "gradient": true + "spotlight": true }, "glow": "both", "orientation": "auto", @@ -1625,13 +1624,12 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 98, "min": 5, "noise": 22, @@ -1649,7 +1647,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1690,9 +1687,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": true, "rounded": true, - "spotlight": true, - "gradient": true + "spotlight": true }, "glow": "both", "orientation": "auto", @@ -1709,13 +1706,12 @@ "sparkline": true, "spotlight": true }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 98, "min": 5, "noise": 22, @@ -1746,7 +1742,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1788,9 +1783,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": true, "rounded": true, - "spotlight": true, - "gradient": true + "spotlight": true }, "glow": "both", "orientation": "auto", @@ -1807,13 +1802,12 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 8, "min": 1, "noise": 2, @@ -1831,7 +1825,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1873,9 +1866,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": true, "rounded": true, - "spotlight": true, - "gradient": true + "spotlight": true }, "glow": "both", "orientation": "auto", @@ -1892,13 +1885,12 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 12, "min": 1, "noise": 2, @@ -1916,7 +1908,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1957,9 +1948,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": true, "rounded": true, - "spotlight": true, - "gradient": true + "spotlight": true }, "glow": "both", "orientation": "auto", @@ -1976,13 +1967,12 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 100, "min": 10, "noise": 22, @@ -2000,7 +1990,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -2041,9 +2030,9 @@ "effects": { "barGlow": true, "centerGlow": true, + "gradient": true, "rounded": true, - "spotlight": true, - "gradient": true + "spotlight": true }, "glow": "both", "orientation": "auto", @@ -2060,13 +2049,12 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "13.0.0-pre", + "pluginVersion": "12.4.0-pre", "targets": [ { "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 100, "min": 10, "noise": 22, @@ -2079,6 +2067,147 @@ ], "title": "Backend", "type": "radialbar" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 66 + }, + "id": 35, + "panels": [], + "title": "Empty data", + "type": "row" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 67 + }, + "id": 36, + "options": { + "barWidthFactor": 0.5, + "effects": { + "barGlow": false, + "centerGlow": false, + "gradient": true, + "rounded": false, + "spotlight": false + }, + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "segmentCount": 1, + "segmentSpacing": 0.3, + "shape": "gauge", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sparkline": true + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 0 + } + ], + "title": "Numeric, no series", + "type": "gauge" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 67 + }, + "id": 37, + "options": { + "barWidthFactor": 0.5, + "effects": { + "barGlow": false, + "centerGlow": false, + "gradient": true, + "rounded": false, + "spotlight": false + }, + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "segmentCount": 1, + "segmentSpacing": 0.3, + "shape": "gauge", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sparkline": true + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "logs" + } + ], + "title": "Non-numeric", + "type": "gauge" } ], "preload": false, @@ -2094,6 +2223,5 @@ "timepicker": {}, "timezone": "browser", "title": "Panel tests - Gauge (new)", - "uid": "panel-tests-gauge-new", - "version": 6 + "version": 9 } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx index c9da81f6cbf..acb255a3f3e 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx @@ -1,5 +1,3 @@ -import { useMemo } from 'react'; - import { FieldDisplay, GrafanaTheme2, FieldConfig } from '@grafana/data'; import { GraphFieldConfig, GraphGradientMode, LineInterpolation } from '@grafana/schema'; @@ -19,24 +17,18 @@ interface RadialSparklineProps { export function RadialSparkline({ sparkline, dimensions, theme, color, shape, textMode }: RadialSparklineProps) { const { radius, barWidth } = dimensions; - const showNameAndValue = textMode === 'value_and_name'; - const height = showNameAndValue ? radius / 4 : radius / 3; - const widthFactor = shape === 'gauge' ? 1.6 : 1.4; - const width = radius * widthFactor - barWidth; - const topPos = useMemo(() => { - if (!sparkline) { - return ''; - } - if (shape === 'gauge') { - return `${dimensions.gaugeBottomY - height}px`; - } - return `calc(50% + ${radius / (showNameAndValue ? 3.3 : 4)}px)`; - }, [sparkline, shape, radius, dimensions.gaugeBottomY, height, showNameAndValue]); - if (!sparkline) { return null; } + const showNameAndValue = textMode === 'value_and_name'; + const height = radius / (showNameAndValue ? 4 : 3); + const width = radius * (shape === 'gauge' ? 1.6 : 1.4) - barWidth; + const topPos = + shape === 'gauge' + ? `${dimensions.gaugeBottomY - height}px` + : `calc(50% + ${radius / (showNameAndValue ? 3.3 : 4)}px)`; + const config: FieldConfig = { color: { mode: 'fixed', @@ -50,12 +42,7 @@ export function RadialSparkline({ sparkline, dimensions, theme, color, shape, te }; return ( -
+
); diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx index 64e8ad28a0d..51a1c64c842 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx @@ -98,21 +98,17 @@ export function RadialText({ const suffixShift = (valueFontSize - unitFontSize * 1.2) / 2; // adjust the text up on gauges and when sparklines are present - let dy = 0; + let yOffset = 0; if (shape === 'gauge') { - if (showName) { - dy -= nameFontSize * 0.2; - } - if (showValue) { - dy -= valueFontSize * 0.3; - } + // 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) { - dy -= 8; + yOffset -= 8; } return ( - + {showValue && ( Date: Thu, 11 Dec 2025 18:12:05 -0500 Subject: [PATCH 05/48] fix migration test --- .../v0alpha1.gauge_tests_new.v42.v1beta1.json | 2 +- .../panel-gauge/gauge_tests_new.v42.json | 153 ++++++++++++++++-- .../panel-gauge/gauge_tests_new.json | 57 +++---- 3 files changed, 171 insertions(+), 41 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json index 2eeb3040e6d..a1fcb2a85ac 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json @@ -2160,4 +2160,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json index c589f8b7400..9f8cf76c9f7 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json @@ -1603,7 +1603,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1671,7 +1670,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 98, "min": 5, "noise": 22, @@ -1689,7 +1687,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1757,7 +1754,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 98, "min": 5, "noise": 22, @@ -1788,7 +1784,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1857,7 +1852,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 8, "min": 1, "noise": 2, @@ -1875,7 +1869,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1944,7 +1937,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 12, "min": 1, "noise": 2, @@ -1962,7 +1954,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -2030,7 +2021,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 100, "min": 10, "noise": 22, @@ -2048,7 +2038,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -2116,7 +2105,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 100, "min": 10, "noise": 22, @@ -2129,6 +2117,147 @@ ], "title": "Backend", "type": "radialbar" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 66 + }, + "id": 35, + "panels": [], + "title": "Empty data", + "type": "row" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 67 + }, + "id": 36, + "options": { + "barWidthFactor": 0.5, + "effects": { + "barGlow": false, + "centerGlow": false, + "gradient": true, + "rounded": false, + "spotlight": false + }, + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "segmentCount": 1, + "segmentSpacing": 0.3, + "shape": "gauge", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sparkline": true + }, + "pluginVersion": "13.0.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 0 + } + ], + "title": "Numeric, no series", + "type": "gauge" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 67 + }, + "id": 37, + "options": { + "barWidthFactor": 0.5, + "effects": { + "barGlow": false, + "centerGlow": false, + "gradient": true, + "rounded": false, + "spotlight": false + }, + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "segmentCount": 1, + "segmentSpacing": 0.3, + "shape": "gauge", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sparkline": true + }, + "pluginVersion": "13.0.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "logs" + } + ], + "title": "Non-numeric", + "type": "gauge" } ], "preload": false, diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index 967aae83c0e..b3c47c9aa7a 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -18,7 +18,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 1943, + "id": 525, "links": [], "panels": [ { @@ -92,7 +92,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -169,7 +169,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -246,7 +246,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -323,7 +323,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -400,7 +400,7 @@ "showThresholdMarkers": false, "sparkline": true }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -477,7 +477,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -554,7 +554,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -644,7 +644,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -721,7 +721,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -798,7 +798,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -875,7 +875,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -969,7 +969,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -1046,7 +1046,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -1123,7 +1123,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -1200,7 +1200,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -1277,7 +1277,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -1371,7 +1371,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -1452,7 +1452,7 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -1533,7 +1533,7 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", @@ -1624,7 +1624,7 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { @@ -1706,7 +1706,7 @@ "sparkline": true, "spotlight": true }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { @@ -1802,7 +1802,7 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { @@ -1885,7 +1885,7 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { @@ -1967,7 +1967,7 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { @@ -2049,7 +2049,7 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { @@ -2133,7 +2133,7 @@ "showThresholdMarkers": true, "sparkline": true }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "refId": "A", @@ -2199,7 +2199,7 @@ "showThresholdMarkers": true, "sparkline": true }, - "pluginVersion": "12.4.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "refId": "A", @@ -2223,5 +2223,6 @@ "timepicker": {}, "timezone": "browser", "title": "Panel tests - Gauge (new)", + "uid": "panel-tests-gauge-new", "version": 9 } From 31e3a97ef2c75d8109681da9c96fafd62566a0f0 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 11 Dec 2025 18:18:50 -0500 Subject: [PATCH 06/48] Fix JSON formatting by adding missing newline From a5a929478470cf1b75245f6c0534207753523ff9 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 11 Dec 2025 18:20:06 -0500 Subject: [PATCH 07/48] remove new line --- .../panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json index a1fcb2a85ac..2eeb3040e6d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json @@ -2160,4 +2160,4 @@ "storedVersion": "v0alpha1" } } -} +} \ No newline at end of file From 5b229a80d3984e4005577da51267b8a04d46fbf0 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Fri, 12 Dec 2025 10:41:07 -0500 Subject: [PATCH 08/48] Gauge: Add guide dots for rounded bars to help with accuracy --- .../components/RadialGauge/RadialArcPath.tsx | 44 ++++++++++++++----- .../src/components/RadialGauge/RadialBar.tsx | 2 + .../RadialGauge/RadialColorDefs.tsx | 6 +++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index f54f3cd6f22..fcaf949bd79 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,14 +1,23 @@ import { GaugeDimensions, toRad } from './utils'; -export interface RadialArcPathProps { +export interface RadialArcPathPropsBase { startAngle: number; dimensions: GaugeDimensions; color: string; glowFilter?: string; arcLengthDeg: number; roundedBars?: boolean; + showGuideDots?: boolean; + guideDotColor?: string; } +interface RadialArcPathPropsWithGuideDot extends RadialArcPathPropsBase { + showGuideDots: true; + guideDotColor: string; +} + +type RadialArcPathProps = RadialArcPathPropsBase | RadialArcPathPropsWithGuideDot; + export function RadialArcPath({ startAngle: angle, dimensions, @@ -16,6 +25,8 @@ export function RadialArcPath({ glowFilter, arcLengthDeg, roundedBars, + showGuideDots, + guideDotColor, }: RadialArcPathProps) { const { radius, centerX, centerY, barWidth } = dimensions; @@ -35,18 +46,27 @@ export function RadialArcPath({ const largeArc = arcLengthDeg > 180 ? 1 : 0; const path = ['M', x1, y1, 'A', radius, radius, 0, largeArc, 1, x2, y2].join(' '); + const dotRadius = (barWidth / 2) * 0.4; return ( - + <> + + {showGuideDots && ( + <> + {arcLengthDeg > 5 && } + + + )} + ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index 2bacc3af3e8..2d9d0fbe80d 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -47,6 +47,8 @@ export function RadialBar({ color={colorDefs.getMainBarColor()} roundedBars={roundedBars} glowFilter={glowFilter} + showGuideDots={roundedBars} + guideDotColor={colorDefs.getGuideDotColor()} /> {spotlightStroke && angle > 8 && ( Date: Fri, 12 Dec 2025 11:13:10 -0500 Subject: [PATCH 09/48] 30% width --- .../grafana-ui/src/components/RadialGauge/RadialArcPath.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index fcaf949bd79..d9a79143c9b 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -46,7 +46,7 @@ export function RadialArcPath({ const largeArc = arcLengthDeg > 180 ? 1 : 0; const path = ['M', x1, y1, 'A', radius, radius, 0, largeArc, 1, x2, y2].join(' '); - const dotRadius = (barWidth / 2) * 0.4; + const dotRadius = (barWidth / 2) * 0.3; return ( <> From c9387d0f4427d2995842a6506f2e715ba9d593c7 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Fri, 12 Dec 2025 16:30:58 -0500 Subject: [PATCH 10/48] remove spotlight, starting to make gradients a bit more predictable --- .../v0alpha1.gauge_tests_new.v42.json | 108 ++++++------- .../v0alpha1.gauge_tests_old_to_new.v42.json | 3 +- ...a1.gauge_tests_old_to_new.v42.v1beta1.json | 7 +- ...1.gauge_tests_old_to_new.v42.v2alpha1.json | 7 +- ...a1.gauge_tests_old_to_new.v42.v2beta1.json | 7 +- .../panel-gauge/gauge_tests_new.v42.json | 103 ++++-------- .../gauge_tests_old_to_new.v42.json | 3 +- .../panel-gauge/gauge_tests_new.json | 153 +++++++----------- .../panel-gauge/gauge_tests_old_to_new.json | 3 +- .../src/themes/colorManipulator.test.ts | 46 ++++++ .../src/themes/colorManipulator.ts | 50 ++++++ .../panelcfg/x/NewGaugePanelCfg_types.gen.ts | 6 +- .../components/RadialGauge/RadialArcPath.tsx | 17 +- .../src/components/RadialGauge/RadialBar.tsx | 52 +----- .../RadialGauge/RadialColorDefs.tsx | 148 ++++++++++++----- .../RadialGauge/RadialGauge.story.tsx | 37 +---- .../components/RadialGauge/RadialGauge.tsx | 20 +-- .../src/components/RadialGauge/effects.tsx | 39 ----- .../plugins/panel/radialbar/EffectsEditor.tsx | 11 -- .../panel/radialbar/RadialBarPanel.tsx | 3 +- public/app/plugins/panel/radialbar/module.tsx | 38 +++-- .../app/plugins/panel/radialbar/panelcfg.cue | 3 +- .../plugins/panel/radialbar/panelcfg.gen.ts | 6 +- 23 files changed, 406 insertions(+), 464 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json index 7c7c479199d..091f705ef0b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json @@ -75,10 +75,10 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": true, - "spotlight": false, + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -154,10 +154,10 @@ "effects": { "barGlow": false, "centerGlow": true, - "rounded": true, - "spotlight": false, + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -233,10 +233,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": false, + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -312,10 +312,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -391,10 +391,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -470,10 +470,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -549,10 +548,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -641,10 +639,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -720,10 +718,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -799,10 +797,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -878,10 +876,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -974,10 +972,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1053,10 +1050,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1132,10 +1128,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1211,10 +1206,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1290,10 +1284,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1386,10 +1379,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1469,10 +1461,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1552,10 +1543,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1644,10 +1634,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1662,8 +1652,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1730,10 +1719,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1748,8 +1737,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1830,10 +1818,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1848,8 +1836,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1917,9 +1904,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "sparkline": false, - "spotlight": true, "gradient": true }, "glow": "both", @@ -1934,10 +1918,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 +1988,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2022,8 +2006,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2090,10 +2073,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2108,8 +2091,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json index a3de6df336a..885966627cd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json @@ -955,8 +955,6 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, "orientation": "auto", @@ -969,6 +967,7 @@ }, "segmentCount": 70, "segmentSpacing": 0.3, + "barShape": "flat", "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json index 959f0193ad6..f39e18c77ef 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json @@ -961,9 +961,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -976,6 +974,7 @@ "segmentCount": 70, "segmentSpacing": 0.3, "shape": "gauge", + "barShape": "flat", "showThresholdLabels": false, "showThresholdMarkers": true, "sparkline": false @@ -1175,4 +1174,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json index 66b29e88d13..c1134c2f9d1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json @@ -864,9 +864,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -879,6 +877,7 @@ "segmentCount": 70, "segmentSpacing": 0.3, "shape": "gauge", + "barShape": "flat", "showThresholdLabels": false, "showThresholdMarkers": true, "sparkline": false @@ -1620,4 +1619,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json index b870d0a91ad..82705e975c2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json @@ -901,9 +901,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -916,6 +914,7 @@ "segmentCount": 70, "segmentSpacing": 0.3, "shape": "gauge", + "barShape": "flat", "showThresholdLabels": false, "showThresholdMarkers": true, "sparkline": false @@ -1672,4 +1671,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json index 9f8cf76c9f7..c4cdddc4ac7 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json @@ -75,10 +75,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": true, - "spotlight": false, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -154,10 +153,9 @@ "effects": { "barGlow": false, "centerGlow": true, - "rounded": true, - "spotlight": false, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -233,10 +231,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": false, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -312,10 +309,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -391,10 +387,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -470,10 +465,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -549,10 +543,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -641,10 +634,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -720,10 +712,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -799,10 +790,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -878,10 +868,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -974,10 +963,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1053,10 +1041,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1132,10 +1119,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1211,10 +1197,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1290,10 +1275,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1386,10 +1370,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1469,10 +1452,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1552,10 +1534,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1643,10 +1624,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1661,8 +1641,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1727,10 +1706,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1745,8 +1723,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1825,10 +1802,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1843,8 +1819,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1910,10 +1885,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1928,8 +1902,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1994,10 +1967,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2012,8 +1984,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2078,10 +2049,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true, "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2096,8 +2066,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2166,10 +2135,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -2232,10 +2200,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json index a3de6df336a..446ba107f19 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json @@ -955,8 +955,6 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false }, "orientation": "auto", @@ -970,6 +968,7 @@ "segmentCount": 70, "segmentSpacing": 0.3, "shape": "gauge", + "barShape": "flat", "showThresholdLabels": false, "showThresholdMarkers": true, "sparkline": false diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index b3c47c9aa7a..530dcf8d63f 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -75,10 +75,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -152,10 +151,9 @@ "effects": { "barGlow": false, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -229,10 +227,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -306,10 +303,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -383,10 +379,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -460,10 +455,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -537,10 +531,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -627,10 +620,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -704,10 +696,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -781,10 +772,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -858,10 +848,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -952,10 +941,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1029,10 +1017,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1106,10 +1093,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1183,10 +1169,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1260,10 +1245,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1354,10 +1338,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1435,10 +1418,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1516,10 +1498,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1605,10 +1586,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1621,8 +1601,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1687,10 +1666,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1703,8 +1681,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1783,10 +1760,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1799,8 +1775,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1866,10 +1841,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1882,8 +1856,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1948,10 +1921,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1964,8 +1936,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2030,10 +2001,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -2046,8 +2016,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2116,10 +2085,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -2182,10 +2150,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json index b071ddff802..bb8d5309116 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json @@ -953,11 +953,10 @@ "sparkline": false, "showThresholdMarkers": true, "showThresholdLabels": false, + "barShape": "flat", "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false, "gradient": false } } diff --git a/packages/grafana-data/src/themes/colorManipulator.test.ts b/packages/grafana-data/src/themes/colorManipulator.test.ts index 5619ddc4928..b003e4baf63 100644 --- a/packages/grafana-data/src/themes/colorManipulator.test.ts +++ b/packages/grafana-data/src/themes/colorManipulator.test.ts @@ -12,6 +12,7 @@ import { lighten, asRgbString, onBackground, + colorAtGradientPercent, } from './colorManipulator'; describe('utils/colorManipulator', () => { @@ -436,4 +437,49 @@ describe('utils/colorManipulator', () => { expect(onBackground('rgba(0,0,255,1)', 'rgba(0,0,0,0.5)').toRgbString()).toBe('rgb(0, 0, 255)'); }); }); + + 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('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'); + }); + }); }); diff --git a/packages/grafana-data/src/themes/colorManipulator.ts b/packages/grafana-data/src/themes/colorManipulator.ts index eccb661b7d8..42bbf454b1a 100644 --- a/packages/grafana-data/src/themes/colorManipulator.ts +++ b/packages/grafana-data/src/themes/colorManipulator.ts @@ -392,6 +392,55 @@ export const onBackground = ( }); }; +/** + * 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: Array<{ color: string; percent: number }>, + 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 + const sorted = stops + .map((s) => ({ color: s.color, percent: clamp(s.percent, 0, 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 + let left = sorted[0]; + let right = sorted[sorted.length - 1]; + for (let i = 1; i < sorted.length; i++) { + if (percent <= sorted[i].percent) { + left = sorted[i - 1]; + right = sorted[i]; + break; + } + } + + const range = right.percent - left.percent; + const t = range === 0 ? 0 : (percent - left.percent) / range; // 0..1 + + // tinycolor.mix expects amount as percentage of the second color + const mixed = tinycolor.mix(left.color, right.color, t * 100); + + // return hex6 if opaque, hex8 if has alpha + return mixed; +} + interface DecomposeColor { type: string; values: any; @@ -414,4 +463,5 @@ export const colorManipulator = { darken, lighten, onBackground, + colorAtGradientPercent, }; diff --git a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts index aedc041cf71..3c9853535e8 100644 --- a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts @@ -16,19 +16,16 @@ export interface GaugePanelEffects { barGlow?: boolean; centerGlow?: boolean; gradient?: boolean; - rounded?: boolean; - spotlight?: boolean; } export const defaultGaugePanelEffects: Partial = { barGlow: false, centerGlow: false, gradient: true, - rounded: false, - spotlight: false, }; export interface Options extends common.SingleStatBaseOptions { + barShape: ('flat' | 'rounded'); barWidthFactor: number; effects: GaugePanelEffects; segmentCount: number; @@ -40,6 +37,7 @@ export interface Options extends common.SingleStatBaseOptions { } export const defaultOptions: Partial = { + barShape: 'flat', barWidthFactor: 0.5, effects: {}, segmentCount: 1, diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index d9a79143c9b..a6278294029 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -8,16 +8,20 @@ export interface RadialArcPathPropsBase { arcLengthDeg: number; roundedBars?: boolean; showGuideDots?: boolean; - guideDotColor?: string; + guideDotStartColor?: string; + guideDotEndColor?: string; } interface RadialArcPathPropsWithGuideDot extends RadialArcPathPropsBase { showGuideDots: true; - guideDotColor: string; + guideDotStartColor: string; + guideDotEndColor: string; } type RadialArcPathProps = RadialArcPathPropsBase | RadialArcPathPropsWithGuideDot; +const MAX_DOT_RADIUS = 8; + export function RadialArcPath({ startAngle: angle, dimensions, @@ -26,7 +30,8 @@ export function RadialArcPath({ arcLengthDeg, roundedBars, showGuideDots, - guideDotColor, + guideDotStartColor, + guideDotEndColor, }: RadialArcPathProps) { const { radius, centerX, centerY, barWidth } = dimensions; @@ -46,7 +51,7 @@ export function RadialArcPath({ const largeArc = arcLengthDeg > 180 ? 1 : 0; const path = ['M', x1, y1, 'A', radius, radius, 0, largeArc, 1, x2, y2].join(' '); - const dotRadius = (barWidth / 2) * 0.3; + const dotRadius = Math.min((barWidth / 2) * 0.4, MAX_DOT_RADIUS); return ( <> @@ -63,8 +68,8 @@ export function RadialArcPath({ /> {showGuideDots && ( <> - {arcLengthDeg > 5 && } - + {arcLengthDeg > 5 && } + )} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index 2d9d0fbe80d..c5552ad5bab 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -1,10 +1,8 @@ -import { GrafanaTheme2 } from '@grafana/data'; - import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; import { RadialColorDefs } from './RadialColorDefs'; -import { GaugeDimensions, toRad } from './utils'; +import { GaugeDimensions } from './utils'; export interface RadialBarProps { dimensions: GaugeDimensions; @@ -13,7 +11,6 @@ export interface RadialBarProps { angle: number; startAngle: number; roundedBars?: boolean; - spotlightStroke: string; glowFilter?: string; } export function RadialBar({ @@ -23,10 +20,10 @@ export function RadialBar({ angle, startAngle, roundedBars, - spotlightStroke, glowFilter, }: RadialBarProps) { const theme = useTheme2(); + const [startDotColor, endDotColor] = colorDefs.getGuideDotColors(); return ( <> @@ -48,52 +45,11 @@ export function RadialBar({ roundedBars={roundedBars} glowFilter={glowFilter} showGuideDots={roundedBars} - guideDotColor={colorDefs.getGuideDotColor()} + guideDotStartColor={startDotColor} + guideDotEndColor={endDotColor} /> - {spotlightStroke && angle > 8 && ( - - )} {colorDefs.getDefs()} ); } - -interface SpotlightEffectProps { - dimensions: GaugeDimensions; - angle: number; - glowFilter?: string; - spotlightStroke: string; - theme: GrafanaTheme2; - roundedBars?: boolean; -} - -function SpotlightSquareEffect({ dimensions, angle, glowFilter, spotlightStroke, roundedBars }: SpotlightEffectProps) { - const { radius, centerX, centerY, barWidth } = dimensions; - - const angleRadian = toRad(angle); - const x1 = centerX + radius * Math.cos(angleRadian - 0.2); - const y1 = centerY + radius * Math.sin(angleRadian - 0.2); - const x2 = centerX + radius * Math.cos(angleRadian); - const y2 = centerY + radius * Math.sin(angleRadian); - - const path = ['M', x1, y1, 'A', radius, radius, 0, 0, 1, x2, y2].join(' '); - - return ( - - ); -} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx index 9ce23cc9a38..abef48720b4 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx @@ -1,6 +1,13 @@ import tinycolor from 'tinycolor2'; -import { DisplayProcessor, FALLBACK_COLOR, FieldDisplay, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; +import { + colorManipulator, + DisplayProcessor, + FALLBACK_COLOR, + FieldDisplay, + getFieldColorMode, + GrafanaTheme2, +} from '@grafana/data'; import { RadialGradientMode, RadialShape } from './RadialGauge'; import { GaugeDimensions } from './utils'; @@ -15,6 +22,13 @@ export interface RadialColorDefsOptions { displayProcessor: DisplayProcessor; } +const CONTRAST_THRESHOLD_MAX = 7.5; +const getGuideDotColor = (color: string): string => { + const darkColor = '#111217'; // gray05 + const lightColor = '#fbfbfb'; // gray90 + return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor; +}; + export class RadialColorDefs { private colorToIds: Record = {}; private defs: React.ReactNode[] = []; @@ -48,17 +62,17 @@ export class RadialColorDefs { const colorMode = getFieldColorMode(colorModeId); const valuePercent = fieldDisplay.display.percent ?? 0; + const gradientStops = this.getGradient(); + const stops = gradientStops.map((stop, i) => ( + + )); + // Handle continusous color modes first // If it's a segment color we don't want to do continuous gradients if (colorMode.isContinuous && colorMode.getColors && !forSegment) { - const colors = colorMode.getColors(theme); - const count = colors.length; - this.defs.push( - - {colors.map((stopColor, i) => ( - - ))} + + {stops} ); @@ -68,21 +82,9 @@ export class RadialColorDefs { // 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( - - - - + + {stops} ); @@ -91,13 +93,11 @@ export class RadialColorDefs { // 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 + // this makes the point color math much more annoying so it's currently disabled. const transform = shape === 'circle' ? `rotate(${360 * valuePercent - 180} ${dimensions.centerX} ${dimensions.centerY})` @@ -114,31 +114,95 @@ export class RadialColorDefs { gradientUnits="userSpaceOnUse" gradientTransform={transform} > - {theme.isDark ? ( - <> - - - - ) : ( - <> - - - - )} + {stops} ); return returnColor; } - getMainBarColor(): string { - return this.getColor(this.options.fieldDisplay.display.color ?? FALLBACK_COLOR); + getFieldBaseColor(): string { + return this.options.fieldDisplay.display.color ?? FALLBACK_COLOR; } - getGuideDotColor(): string { - return this.getColor( - this.options.theme.colors.getContrastText(this.options.fieldDisplay.display.color ?? FALLBACK_COLOR) - ); + getMainBarColor(): string { + return this.getColor(this.getFieldBaseColor()); + } + + getGradient(): Array<{ color: string; percent: number }> { + const { gradient, fieldDisplay, theme } = this.options; + const baseColor = this.getFieldBaseColor(); + if (gradient === 'none') { + return [ + { color: baseColor, percent: 0 }, + { color: baseColor, percent: 1 }, + ]; + } + + const colorModeId = fieldDisplay.field.color?.mode; + const colorMode = getFieldColorMode(colorModeId); + + // Handle continusous color modes first + if (colorMode.isContinuous && colorMode.getColors) { + const colors = colorMode.getColors(theme); + return colors.map((color, idx) => ({ color, percent: idx / (colors.length - 1) })); + } else if (colorMode.isByValue) { + // 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 + 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 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 + // we set the highest contrast color second based on the theme. + const darkerColor = tinycolor(baseColor).spin(-20).darken(5); + const lighterColor = tinycolor(baseColor).saturate(20).spin(20).brighten(10); + return theme.isDark + ? [ + { color: darkerColor.darken(10).toString(), percent: 0 }, + { color: lighterColor.lighten(10).toString(), percent: 1 }, + ] + : [ + { color: lighterColor.lighten(10).toString(), percent: 0 }, + { color: darkerColor.toString(), percent: 1 }, + ]; + } + + getGuideDotColors(): [string, string] { + const { fieldDisplay } = this.options; + + const gradient = this.getGradient(); + let valuePercent = fieldDisplay.display.percent ?? 0; + + let startColor = gradient[0].color; + let endColor = gradient[gradient.length - 1].color; + + const colorMode = getFieldColorMode(fieldDisplay.field.color?.mode); + if (colorMode.isContinuous) { + } else if (colorMode.isByValue) { + } else { + } + + // if we have a percentageFilled, use it to get a the correct end color based on where the bar terminates + if (gradient.length >= 2) { + const endColorByPercentage = colorManipulator.colorAtGradientPercent(gradient, valuePercent); + endColor = + endColorByPercentage.getAlpha() === 1 + ? endColorByPercentage.toHexString() + : endColorByPercentage.toHex8String(); + } + + return [getGuideDotColor(startColor), getGuideDotColor(endColor)]; } getDefs(): React.ReactNode[] { diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx index c098beda886..79d8b9444e7 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx @@ -34,7 +34,6 @@ const meta: Meta = { }, args: { barWidthFactor: 0.2, - spotlight: false, glowBar: false, glowCenter: false, sparkline: false, @@ -56,7 +55,6 @@ const meta: Meta = { width: { control: { type: 'range', min: 50, max: 600 } }, height: { control: { type: 'range', min: 50, max: 600 } }, value: { control: { type: 'range', min: 0, max: 110 } }, - spotlight: { control: 'boolean' }, roundedBars: { control: 'boolean' }, sparkline: { control: 'boolean' }, thresholdsBar: { control: 'boolean' }, @@ -133,26 +131,10 @@ export const Examples: StoryFn = (args) => {
Effects
- - - - + + + +
Shape: Gauge & color scale
@@ -185,7 +167,6 @@ export const Examples: StoryFn = (args) => { shape="gauge" gradient="auto" sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.2} @@ -196,7 +177,6 @@ export const Examples: StoryFn = (args) => { shape="gauge" gradient="auto" sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.8} @@ -208,7 +188,6 @@ export const Examples: StoryFn = (args) => { width={250} gradient="auto" sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.2} @@ -220,7 +199,6 @@ export const Examples: StoryFn = (args) => { shape="gauge" gradient="auto" sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.8} @@ -260,7 +238,6 @@ export const Examples: StoryFn = (args) => { = (args) => { value={args.value ?? 80} width={250} colorScheme={FieldColorModeId.ContinuousGrYlRd} - spotlight shape="gauge" gradient="auto" glowBar={true} @@ -288,7 +264,6 @@ export const Examples: StoryFn = (args) => { gradient="auto" thresholdsBar={true} roundedBars={false} - spotlight glowCenter={true} barWidthFactor={0.7} /> @@ -347,7 +322,6 @@ export const Temp: StoryFn = (args) => { shape="gauge" roundedBars={false} barWidthFactor={0.8} - spotlight /> ); @@ -363,7 +337,6 @@ interface ExampleProps { max?: number; width?: number; height?: number; - spotlight?: boolean; glowBar?: boolean; glowCenter?: boolean; barWidthFactor?: number; @@ -390,7 +363,6 @@ export function RadialGaugeExample({ max = 100, width = 200, height = 200, - spotlight = false, glowBar = false, glowCenter = false, barWidthFactor = 0.4, @@ -480,7 +452,6 @@ export function RadialGaugeExample({ barWidthFactor={barWidthFactor} gradient={gradient} shape={shape} - spotlight={spotlight} glowBar={glowBar} glowCenter={glowCenter} textMode={textMode} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index fadabf8ec72..d20665872c7 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -21,7 +21,7 @@ import { RadialScaleLabels } from './RadialScaleLabels'; import { RadialSparkline } from './RadialSparkline'; import { RadialText } from './RadialText'; import { ThresholdsBar } from './ThresholdsBar'; -import { GlowGradient, MiddleCircleGlow, SpotlightGradient } from './effects'; +import { GlowGradient, MiddleCircleGlow } from './effects'; import { calculateDimensions, getValueAngleForValue } from './utils'; export interface RadialGaugeProps { @@ -40,8 +40,6 @@ 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; @@ -89,7 +87,6 @@ export function RadialGauge(props: RadialGaugeProps) { shape = 'circle', gradient = 'none', barWidthFactor = 0.4, - spotlight = false, glowBar = false, glowCenter = false, textMode = 'auto', @@ -135,7 +132,6 @@ export function RadialGauge(props: RadialGaugeProps) { ); const displayProcessor = getFieldDisplayProcessor(displayValue); - const spotlightGradientId = `spotlight-${barIndex}-${gaugeId}`; const glowFilterId = `glow-${gaugeId}`; const colorDefs = new RadialColorDefs({ gradient, @@ -147,19 +143,6 @@ export function RadialGauge(props: RadialGaugeProps) { displayProcessor, }); - if (spotlight && theme.isDark) { - defs.push( - - ); - } - if (segmentCount > 1) { graphics.push( ); diff --git a/packages/grafana-ui/src/components/RadialGauge/effects.tsx b/packages/grafana-ui/src/components/RadialGauge/effects.tsx index 354a68a25ba..09bbc46e349 100644 --- a/packages/grafana-ui/src/components/RadialGauge/effects.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/effects.tsx @@ -22,45 +22,6 @@ export function GlowGradient({ id, barWidth }: GlowGradientProps) { ); } -export function SpotlightGradient({ - id, - dimensions, - roundedBars, - angle, - theme, -}: { - id: string; - dimensions: GaugeDimensions; - angle: number; - roundedBars: boolean; - theme: GrafanaTheme2; -}) { - const angleRadian = ((angle - 90) * Math.PI) / 180; - - let x1 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian - 0.2); - let y1 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian - 0.2); - let x2 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian); - let y2 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian); - - if (theme.isLight) { - return ( - - - - - - ); - } - - return ( - - - - {roundedBars && } - - ); -} - export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) { return ( diff --git a/public/app/plugins/panel/radialbar/EffectsEditor.tsx b/public/app/plugins/panel/radialbar/EffectsEditor.tsx index d7a0f03cbc7..a3c26beca90 100644 --- a/public/app/plugins/panel/radialbar/EffectsEditor.tsx +++ b/public/app/plugins/panel/radialbar/EffectsEditor.tsx @@ -44,11 +44,6 @@ export function EffectsEditor(props: StandardEditorProps) { value={!!props.value?.gradient} onChange={(e) => props.onChange({ ...props.value, gradient: e.currentTarget.checked })} /> - props.onChange({ ...props.value, rounded: e.currentTarget.checked })} - /> ) { value={!!props.value?.centerGlow} onChange={(e) => props.onChange({ ...props.value, centerGlow: e.currentTarget.checked })} /> - props.onChange({ ...props.value, spotlight: e.currentTarget.checked })} - /> ); } diff --git a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx index 86235a3bf68..7c2a0cc9081 100644 --- a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx +++ b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx @@ -38,10 +38,9 @@ export function RadialBarPanel({ height={height} barWidthFactor={options.barWidthFactor} gradient={options.effects?.gradient ? 'auto' : 'none'} - spotlight={options.effects?.spotlight} 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} diff --git a/public/app/plugins/panel/radialbar/module.tsx b/public/app/plugins/panel/radialbar/module.tsx index 12aff4966f7..f7cd5fb043f 100644 --- a/public/app/plugins/panel/radialbar/module.tsx +++ b/public/app/plugins/panel/radialbar/module.tsx @@ -32,18 +32,6 @@ export const plugin = new PanelPlugin(RadialBarPanel) }, }); - builder.addSliderInput({ - path: 'barWidthFactor', - name: t('radialbar.config.bar-width', 'Bar width'), - category, - defaultValue: defaultOptions.barWidthFactor, - settings: { - min: 0.1, - max: 1, - step: 0.01, - }, - }); - builder.addSliderInput({ path: 'segmentCount', name: t('radialbar.config.segment-count', 'Segments'), @@ -69,6 +57,32 @@ export const plugin = new PanelPlugin(RadialBarPanel) }, }); + builder.addRadio({ + path: 'barShape', + name: t('radialbar.config.bar-shape', 'Bar Style'), + category, + defaultValue: defaultOptions.barShape, + settings: { + options: [ + { value: 'flat', label: t('radialbar.config.bar-shape-flat', 'Flat') }, + { value: 'rounded', label: t('radialbar.config.bar-shape-rounded', 'Rounded') }, + ], + }, + showIf: (options) => options.segmentCount === 1, + }); + + builder.addSliderInput({ + path: 'barWidthFactor', + name: t('radialbar.config.bar-width', 'Bar width'), + category, + defaultValue: defaultOptions.barWidthFactor, + settings: { + min: 0.1, + max: 1, + step: 0.01, + }, + }); + builder.addBooleanSwitch({ path: 'sparkline', name: t('radialbar.config.sparkline', 'Show sparkline'), diff --git a/public/app/plugins/panel/radialbar/panelcfg.cue b/public/app/plugins/panel/radialbar/panelcfg.cue index a37982dcfe1..89df6eddc26 100644 --- a/public/app/plugins/panel/radialbar/panelcfg.cue +++ b/public/app/plugins/panel/radialbar/panelcfg.cue @@ -27,8 +27,6 @@ composableKinds: PanelCfg: { schema: { GaugePanelEffects: { barGlow?: bool | *false - spotlight?: bool | *false - rounded?: bool | *false centerGlow?: bool | *false gradient?: bool | *true } @cuetsy(kind="interface") @@ -42,6 +40,7 @@ composableKinds: PanelCfg: { sparkline?: bool | *true shape: "circle" | *"gauge" barWidthFactor: number | *0.5 + barShape: "flat" | "rounded" | *"flat" effects: GaugePanelEffects | *{} } @cuetsy(kind="interface") } diff --git a/public/app/plugins/panel/radialbar/panelcfg.gen.ts b/public/app/plugins/panel/radialbar/panelcfg.gen.ts index 24915c62ef1..ddb7b522365 100644 --- a/public/app/plugins/panel/radialbar/panelcfg.gen.ts +++ b/public/app/plugins/panel/radialbar/panelcfg.gen.ts @@ -14,19 +14,16 @@ export interface GaugePanelEffects { barGlow?: boolean; centerGlow?: boolean; gradient?: boolean; - rounded?: boolean; - spotlight?: boolean; } export const defaultGaugePanelEffects: Partial = { barGlow: false, centerGlow: false, gradient: true, - rounded: false, - spotlight: false, }; export interface Options extends common.SingleStatBaseOptions { + barShape: ('flat' | 'rounded'); barWidthFactor: number; effects: GaugePanelEffects; segmentCount: number; @@ -38,6 +35,7 @@ export interface Options extends common.SingleStatBaseOptions { } export const defaultOptions: Partial = { + barShape: 'flat', barWidthFactor: 0.5, effects: {}, segmentCount: 1, From aa87720f4ac2b48e8b5ea80f1ff38c5196bdc476 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 10:55:39 -0500 Subject: [PATCH 11/48] fix segmented --- .../RadialGauge/RadialBarSegmented.tsx | 2 +- .../RadialGauge/RadialColorDefs.tsx | 33 +++++++++---------- .../components/RadialGauge/ThresholdsBar.tsx | 2 +- .../src/components/RadialGauge/effects.tsx | 2 -- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx index dbc93d2da7f..dd2ec0ac9bf 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx @@ -38,7 +38,7 @@ export function RadialBarSegmented({ for (let i = 0; i < segmentCountAdjusted; i++) { const angleValue = min + ((max - min) / segmentCountAdjusted) * i; - const angleColor = colorDefs.getSegmentColor(angleValue); + const angleColor = colorDefs.getSegmentColor(angleValue, i); const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01; const segmentColor = angleValue >= value ? theme.colors.action.hover : angleColor; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx index abef48720b4..5a8c14d529f 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx @@ -35,17 +35,20 @@ export class RadialColorDefs { constructor(private options: RadialColorDefsOptions) {} - getSegmentColor(forValue: number): string { + getSegmentColor(forValue: number, segmentIdx: number): string { const { displayProcessor } = this.options; const baseColor = displayProcessor(forValue).color ?? FALLBACK_COLOR; - - return this.getColor(baseColor, true); + return this.getColor(baseColor, segmentIdx); } - getColor(baseColor: string, forSegment?: boolean): string { - const { gradient, dimensions, gaugeId, fieldDisplay, shape, theme } = this.options; + getColor(baseColor: string, segmentIdx?: number): string { + const { gradient, dimensions, gaugeId, fieldDisplay, shape } = this.options; - const id = `value-color-${baseColor}-${gaugeId}`; + let id = `value-color-${baseColor}-${gaugeId}`; + const forSegment = segmentIdx !== undefined; + if (forSegment) { + id += `-segment-${segmentIdx}`; + } if (this.colorToIds[id]) { return this.colorToIds[id]; @@ -62,7 +65,7 @@ export class RadialColorDefs { const colorMode = getFieldColorMode(colorModeId); const valuePercent = fieldDisplay.display.percent ?? 0; - const gradientStops = this.getGradient(); + const gradientStops = this.getGradient(baseColor, forSegment); const stops = gradientStops.map((stop, i) => ( )); @@ -70,6 +73,10 @@ export class RadialColorDefs { // 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) { + // this linear gradient doesn't work well for the circular shape yoday for what we actually + // want for continuous color modes, which would be to have the radial bar fill from the top + // around the circle. But SVG doesn't support that kind of gradient on stroke paths out-of-the-box, + // we'd need to implement something like https://gist.github.com/mbostock/4163057 this.defs.push( {stops} @@ -97,7 +104,6 @@ export class RadialColorDefs { const y2 = shape === 'circle' ? dimensions.centerY + dimensions.radius : 0; // this makes it so the gradient is always brightest at the current value - // this makes the point color math much more annoying so it's currently disabled. const transform = shape === 'circle' ? `rotate(${360 * valuePercent - 180} ${dimensions.centerX} ${dimensions.centerY})` @@ -129,9 +135,8 @@ export class RadialColorDefs { return this.getColor(this.getFieldBaseColor()); } - getGradient(): Array<{ color: string; percent: number }> { + getGradient(baseColor = this.getFieldBaseColor(), forSegment?: boolean): Array<{ color: string; percent: number }> { const { gradient, fieldDisplay, theme } = this.options; - const baseColor = this.getFieldBaseColor(); if (gradient === 'none') { return [ { color: baseColor, percent: 0 }, @@ -143,7 +148,7 @@ export class RadialColorDefs { const colorMode = getFieldColorMode(colorModeId); // Handle continusous color modes first - if (colorMode.isContinuous && colorMode.getColors) { + if (colorMode.isContinuous && colorMode.getColors && !forSegment) { const colors = colorMode.getColors(theme); return colors.map((color, idx) => ({ color, percent: idx / (colors.length - 1) })); } else if (colorMode.isByValue) { @@ -187,12 +192,6 @@ export class RadialColorDefs { let startColor = gradient[0].color; let endColor = gradient[gradient.length - 1].color; - const colorMode = getFieldColorMode(fieldDisplay.field.color?.mode); - if (colorMode.isContinuous) { - } else if (colorMode.isByValue) { - } else { - } - // if we have a percentageFilled, use it to get a the correct end color based on where the bar terminates if (gradient.length >= 2) { const endColorByPercentage = colorManipulator.colorAtGradientPercent(gradient, valuePercent); diff --git a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx index 602038ccf10..8977bbb6642 100644 --- a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx @@ -58,7 +58,7 @@ export function ThresholdsBar({ dimensions={thresholdDimensions} roundedBars={roundedBars} glowFilter={glowFilter} - color={colorDefs.getColor(threshold.color, true)} + color={colorDefs.getColor(threshold.color, i)} /> ); diff --git a/packages/grafana-ui/src/components/RadialGauge/effects.tsx b/packages/grafana-ui/src/components/RadialGauge/effects.tsx index 09bbc46e349..759ae4241e8 100644 --- a/packages/grafana-ui/src/components/RadialGauge/effects.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/effects.tsx @@ -1,5 +1,3 @@ -import { GrafanaTheme2 } from '@grafana/data'; - import { GaugeDimensions } from './utils'; export interface GlowGradientProps { From 3f1a2833a84809cb80a1f52dfa175b06ab8589f4 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 12:18:34 -0500 Subject: [PATCH 12/48] update rotation of gauge color --- .../RadialGauge/RadialColorDefs.tsx | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx index 5a8c14d529f..9e42d7b1bc2 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx @@ -22,7 +22,7 @@ export interface RadialColorDefsOptions { displayProcessor: DisplayProcessor; } -const CONTRAST_THRESHOLD_MAX = 7.5; +const CONTRAST_THRESHOLD_MAX = 4.5; const getGuideDotColor = (color: string): string => { const darkColor = '#111217'; // gray05 const lightColor = '#fbfbfb'; // gray90 @@ -70,15 +70,16 @@ export class RadialColorDefs { )); + // circular gradients are a little awkward today. we don't exactly have the result we + // want for continuous color modes, which would be to have the radial bar fill from the top + // around the circle. But SVG doesn't support that kind of gradient on stroke paths out-of-the-box, + // we'd need to implement something like https://gist.github.com/mbostock/4163057 + // 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) { - // this linear gradient doesn't work well for the circular shape yoday for what we actually - // want for continuous color modes, which would be to have the radial bar fill from the top - // around the circle. But SVG doesn't support that kind of gradient on stroke paths out-of-the-box, - // we'd need to implement something like https://gist.github.com/mbostock/4163057 this.defs.push( - + {stops} ); @@ -89,12 +90,13 @@ export class RadialColorDefs { // 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 x2 = shape === 'circle' ? 0 : 1 / valuePercent; + const y2 = shape === 'circle' ? 1 : 0; this.defs.push( - + {stops} ); - return returnColor; } @@ -103,23 +105,8 @@ export class RadialColorDefs { const x2 = shape === 'circle' ? 0 : dimensions.centerX + dimensions.radius; const y2 = shape === 'circle' ? dimensions.centerY + dimensions.radius : 0; - // 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( - + {stops} ); @@ -184,11 +171,21 @@ export class RadialColorDefs { } getGuideDotColors(): [string, string] { - const { fieldDisplay } = this.options; + const { dimensions, fieldDisplay, shape } = this.options; const gradient = this.getGradient(); let valuePercent = fieldDisplay.display.percent ?? 0; + // the linear gradient used in circular gradients means that we want to use the + // y position of the edge of the bar to determine the color. If we ever address + // that shortcoming, we could delete this block. + if (shape === 'circle') { + const angleDeg = ((valuePercent - 0.25) % 1) * 360; + const angleRad = (angleDeg * Math.PI) / 180; + const yPos = dimensions.centerY + dimensions.radius * Math.sin(angleRad); + valuePercent = yPos / (dimensions.centerY * 2); + } + let startColor = gradient[0].color; let endColor = gradient[gradient.length - 1].color; From d52aea6d32d2089954937567f292db3351d53bee Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 12:37:43 -0500 Subject: [PATCH 13/48] update i18n and migration tests --- .../v0alpha1.gauge_tests_new.v42.v1beta1.json | 136 ++++++------------ ...v0alpha1.gauge_tests_new.v42.v2alpha1.json | 136 ++++++------------ .../v0alpha1.gauge_tests_new.v42.v2beta1.json | 135 ++++++----------- public/locales/en-US/grafana.json | 8 +- 4 files changed, 144 insertions(+), 271 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json index 2eeb3040e6d..4ae5e304bea 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json @@ -77,13 +77,12 @@ "id": 1, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -156,13 +155,12 @@ "id": 4, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -235,13 +233,12 @@ "id": 3, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -314,13 +311,12 @@ "id": 5, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -393,13 +389,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": { @@ -476,9 +471,7 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -555,9 +548,7 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -643,13 +634,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 +712,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 +790,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 +868,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": { @@ -980,9 +967,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1059,9 +1044,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1138,9 +1121,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1217,9 +1198,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1296,9 +1275,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1392,9 +1369,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1475,9 +1450,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1558,9 +1531,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1645,14 +1616,13 @@ "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 }, "glow": "both", "orientation": "auto", @@ -1668,8 +1638,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1731,14 +1700,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 +1722,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1831,14 +1798,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 +1820,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1918,15 +1883,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 +1905,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2005,14 +1967,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 +1989,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2091,14 +2051,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 +2073,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2160,4 +2118,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json index 4aecf4e0d9c..21fd7181fb6 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json @@ -73,13 +73,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -165,14 +164,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -188,8 +186,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "fieldConfig": { "defaults": { @@ -262,14 +259,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -285,8 +281,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -360,15 +355,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -384,8 +377,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -459,14 +451,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -482,8 +473,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -556,14 +546,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -579,8 +568,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -653,13 +641,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.1, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -745,13 +732,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.32, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -837,13 +823,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.57, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -929,13 +914,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.8, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1025,9 +1009,7 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1117,9 +1099,7 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1205,9 +1185,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1297,9 +1275,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1389,9 +1365,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1481,9 +1455,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1573,13 +1545,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": { @@ -1665,9 +1636,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1757,9 +1726,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1853,9 +1820,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1949,9 +1914,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -2045,13 +2008,12 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -2137,13 +2099,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": { @@ -2229,13 +2190,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,14 +2281,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", @@ -2344,8 +2303,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -2826,4 +2784,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json index 6b567f19b5e..90798acd192 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json @@ -77,13 +77,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -172,14 +171,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -195,8 +193,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "fieldConfig": { "defaults": { @@ -272,14 +269,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -295,8 +291,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -373,15 +368,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -397,8 +390,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -475,14 +467,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -498,8 +489,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -575,14 +565,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { "barGlow": true, "centerGlow": true, - "gradient": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", "orientation": "auto", @@ -598,8 +587,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -675,13 +663,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.1, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -770,13 +757,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.32, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -865,13 +851,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.57, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -960,13 +945,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.8, "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1059,9 +1043,7 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1154,9 +1136,7 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": false, - "spotlight": true + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1245,9 +1225,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1340,9 +1318,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1435,9 +1411,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1530,9 +1504,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1629,9 +1601,7 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1720,9 +1690,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false, - "rounded": false, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -1815,9 +1783,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -1914,9 +1880,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -2013,9 +1977,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": true, - "rounded": false, - "spotlight": false + "gradient": true }, "orientation": "auto", "reduceOptions": { @@ -2112,13 +2074,12 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "gradient": false, - "rounded": true, - "spotlight": false + "gradient": false }, "orientation": "auto", "reduceOptions": { @@ -2207,13 +2168,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": { @@ -2302,13 +2262,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,14 +2356,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", @@ -2420,8 +2378,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -2902,4 +2859,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 96a20fc1e3f..5eb9615cc3c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -12456,15 +12456,15 @@ }, "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" }, "segment-count": "Segments", "segment-spacing": "Segment spacing", From bbb770a325ed3ca7505afabb39f88158c4d9d263 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 12:41:43 -0500 Subject: [PATCH 14/48] fix spacing --- .../panel-gauge/v0alpha1.gauge_tests_new.v42.json | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json index 091f705ef0b..6b4367e0c3a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json @@ -75,7 +75,6 @@ "effects": { "barGlow": false, "centerGlow": false, - "gradient": false }, "barShape": "rounded", @@ -154,7 +153,6 @@ "effects": { "barGlow": false, "centerGlow": true, - "gradient": false }, "barShape": "rounded", @@ -233,7 +231,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false }, "barShape": "rounded", @@ -312,7 +309,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false }, "barShape": "rounded", @@ -391,7 +387,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false }, "barShape": "rounded", @@ -639,7 +634,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false }, "barShape": "rounded", @@ -718,7 +712,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false }, "barShape": "rounded", @@ -797,7 +790,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false }, "barShape": "rounded", @@ -876,7 +868,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": false }, "barShape": "rounded", @@ -1634,7 +1625,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true }, "barShape": "rounded", @@ -1719,7 +1709,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true }, "barShape": "rounded", @@ -1818,7 +1807,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true }, "barShape": "rounded", @@ -1988,7 +1976,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true }, "barShape": "rounded", @@ -2073,7 +2060,6 @@ "effects": { "barGlow": true, "centerGlow": true, - "gradient": true }, "barShape": "rounded", From f389a1aee266600f6fbe0920b779f5c9b77ceccb Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 13:06:43 -0500 Subject: [PATCH 15/48] more fixture updates --- .../v0alpha1.gauge_tests_old_to_new.v42.json | 1 - .../v0alpha1.gauge_tests_new.v42.v1beta1.json | 10 ++++++++++ .../v0alpha1.gauge_tests_new.v42.v2beta1.json | 11 +++++++++++ .../v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json | 1 - .../v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json | 1 - .../v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json | 1 - .../panel-gauge/gauge_tests_old_to_new.v42.json | 1 - .../panel-gauge/gauge_tests_old_to_new.json | 1 - 8 files changed, 21 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json index 885966627cd..dda0fbcd432 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json @@ -967,7 +967,6 @@ }, "segmentCount": 70, "segmentSpacing": 0.3, - "barShape": "flat", "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json index 4ae5e304bea..aebdc53aa18 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json @@ -467,6 +467,7 @@ "id": 22, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, @@ -544,6 +545,7 @@ "id": 23, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, @@ -963,6 +965,7 @@ "id": 25, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1040,6 +1043,7 @@ "id": 26, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, @@ -1117,6 +1121,7 @@ "id": 29, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, @@ -1194,6 +1199,7 @@ "id": 30, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1271,6 +1277,7 @@ "id": 28, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, @@ -1365,6 +1372,7 @@ "id": 32, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1446,6 +1454,7 @@ "id": 34, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1527,6 +1536,7 @@ "id": 33, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json index 90798acd192..6767812970e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json @@ -1039,6 +1039,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, @@ -1132,6 +1133,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, @@ -1221,6 +1223,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1314,6 +1317,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, @@ -1407,6 +1411,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, @@ -1500,6 +1505,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, @@ -1597,6 +1603,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.4, "effects": { "barGlow": true, @@ -1686,6 +1693,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1779,6 +1787,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1876,6 +1885,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1973,6 +1983,7 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json index f39e18c77ef..1d9f7e56513 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json @@ -974,7 +974,6 @@ "segmentCount": 70, "segmentSpacing": 0.3, "shape": "gauge", - "barShape": "flat", "showThresholdLabels": false, "showThresholdMarkers": true, "sparkline": false diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json index c1134c2f9d1..7b3f601b5cf 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json @@ -877,7 +877,6 @@ "segmentCount": 70, "segmentSpacing": 0.3, "shape": "gauge", - "barShape": "flat", "showThresholdLabels": false, "showThresholdMarkers": true, "sparkline": false diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json index 82705e975c2..534e7a1600c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json @@ -914,7 +914,6 @@ "segmentCount": 70, "segmentSpacing": 0.3, "shape": "gauge", - "barShape": "flat", "showThresholdLabels": false, "showThresholdMarkers": true, "sparkline": false diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json index 446ba107f19..dda0fbcd432 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json @@ -968,7 +968,6 @@ "segmentCount": 70, "segmentSpacing": 0.3, "shape": "gauge", - "barShape": "flat", "showThresholdLabels": false, "showThresholdMarkers": true, "sparkline": false diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json index bb8d5309116..bee1ece914e 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json @@ -953,7 +953,6 @@ "sparkline": false, "showThresholdMarkers": true, "showThresholdLabels": false, - "barShape": "flat", "effects": { "barGlow": false, "centerGlow": false, From 87521b03482cca332215ac280050defcd472ba92 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 17:38:26 -0500 Subject: [PATCH 16/48] wip: using clip-path and CSS for drawing the gauge --- .../components/RadialGauge/RadialArcPath.tsx | 139 ++++++++++++++---- .../src/components/RadialGauge/RadialBar.tsx | 2 +- .../RadialGauge/RadialColorDefs.tsx | 54 ++----- .../src/components/RadialGauge/colors.ts | 59 ++++++++ 4 files changed, 183 insertions(+), 71 deletions(-) create mode 100644 packages/grafana-ui/src/components/RadialGauge/colors.ts diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index a6278294029..b1a068c93a6 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,11 +1,18 @@ +import { useId } from 'react'; + +import { colorManipulator } from '@grafana/data'; + +import { RadialColorDefs } from './RadialColorDefs'; import { GaugeDimensions, toRad } from './utils'; export interface RadialArcPathPropsBase { startAngle: number; dimensions: GaugeDimensions; - color: string; - glowFilter?: string; + colorDef: RadialColorDefs; arcLengthDeg: number; + color?: string; + gradient?: string; + glowFilter?: string; roundedBars?: boolean; showGuideDots?: boolean; guideDotStartColor?: string; @@ -22,17 +29,17 @@ type RadialArcPathProps = RadialArcPathPropsBase | RadialArcPathPropsWithGuideDo const MAX_DOT_RADIUS = 8; -export function RadialArcPath({ - startAngle: angle, - dimensions, - color, - glowFilter, +function drawRadialArcPath({ + angle, arcLengthDeg, + dimensions, roundedBars, - showGuideDots, - guideDotStartColor, - guideDotEndColor, -}: RadialArcPathProps) { +}: { + angle: number; + dimensions: GaugeDimensions; + arcLengthDeg: number; + roundedBars?: boolean; +}): string { const { radius, centerX, centerY, barWidth } = dimensions; if (arcLengthDeg === 360) { @@ -43,29 +50,111 @@ export function RadialArcPath({ const startRadians = toRad(angle); const endRadians = toRad(angle + arcLengthDeg); + const largeArc = arcLengthDeg > 180 ? 1 : 0; + + const outerR = radius + barWidth / 2; + const innerR = Math.max(0, radius - barWidth / 2); + + 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); + + 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 + pathParts.push('L', ix2, iy2); + } + + if (innerR <= 0) { + // if inner radius collapsed to center, line to center and close + pathParts.push('L', centerX, centerY, 'Z'); + } else { + // 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 + pathParts.push('L', ox1, oy1); + } + + pathParts.push('Z'); + } + + return pathParts.join(' '); +} + +export function RadialArcPath({ + startAngle: angle, + dimensions, + color, + colorDef, + gradient, + arcLengthDeg, + roundedBars, + showGuideDots, + guideDotStartColor, + guideDotEndColor, +}: RadialArcPathProps) { + const id = useId(); + const { radius, centerX, centerY, barWidth } = dimensions; + + const startRadians = toRad(angle); + const endRadians = toRad(angle + arcLengthDeg); + const gradientList = colorDef?.getGradient(); + + const path = drawRadialArcPath({ angle, arcLengthDeg, dimensions, roundedBars }); 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(' '); const dotRadius = Math.min((barWidth / 2) * 0.4, MAX_DOT_RADIUS); return ( <> - + + + + +
+ + + {roundedBars && gradientList && ( + <> + + {/* this would actually need to be the color determined by the display */} + + + )} {showGuideDots && ( <> {arcLengthDeg > 5 && } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index c5552ad5bab..bb0dac60d45 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -41,7 +41,7 @@ export function RadialBar({ dimensions={dimensions} startAngle={startAngle} arcLengthDeg={angle} - color={colorDefs.getMainBarColor()} + gradient={colorDefs.getGradientDef()} roundedBars={roundedBars} glowFilter={glowFilter} showGuideDots={roundedBars} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx index 9e42d7b1bc2..97b24b99054 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx @@ -1,5 +1,3 @@ -import tinycolor from 'tinycolor2'; - import { colorManipulator, DisplayProcessor, @@ -10,6 +8,7 @@ import { } from '@grafana/data'; import { RadialGradientMode, RadialShape } from './RadialGauge'; +import { buildGradientColors } from './colors'; import { GaugeDimensions } from './utils'; export interface RadialColorDefsOptions { @@ -124,50 +123,15 @@ export class RadialColorDefs { getGradient(baseColor = this.getFieldBaseColor(), forSegment?: boolean): Array<{ color: string; percent: number }> { const { gradient, fieldDisplay, theme } = this.options; - if (gradient === 'none') { - return [ - { color: baseColor, percent: 0 }, - { color: baseColor, percent: 1 }, - ]; - } + return buildGradientColors(gradient, baseColor, theme, fieldDisplay.field.color?.mode, forSegment); + } - const colorModeId = fieldDisplay.field.color?.mode; - const colorMode = getFieldColorMode(colorModeId); - - // Handle continusous color modes first - if (colorMode.isContinuous && colorMode.getColors && !forSegment) { - const colors = colorMode.getColors(theme); - return colors.map((color, idx) => ({ color, percent: idx / (colors.length - 1) })); - } else if (colorMode.isByValue) { - // 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 - 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 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 - // we set the highest contrast color second based on the theme. - const darkerColor = tinycolor(baseColor).spin(-20).darken(5); - const lighterColor = tinycolor(baseColor).saturate(20).spin(20).brighten(10); - return theme.isDark - ? [ - { color: darkerColor.darken(10).toString(), percent: 0 }, - { color: lighterColor.lighten(10).toString(), percent: 1 }, - ] - : [ - { color: lighterColor.lighten(10).toString(), percent: 0 }, - { color: darkerColor.toString(), percent: 1 }, - ]; + getGradientDef(): string { + const gradientStops = this.getGradient(); + const colorStrings = gradientStops.map((stop) => `${stop.color} ${(stop.percent * 100).toFixed(2)}%`); + return this.options.shape === 'circle' + ? `conic-gradient(from -10deg, ${colorStrings.join(', ')})` + : 'linear-gradient(90deg, ' + colorStrings.join(', ') + ')'; } getGuideDotColors(): [string, string] { diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts new file mode 100644 index 00000000000..62a31470ed3 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -0,0 +1,59 @@ +import tinycolor from 'tinycolor2'; + +import { FieldColorModeId, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; + +import { RadialGradientMode } from './RadialGauge'; + +export function buildGradientColors( + gradientMode: RadialGradientMode, + baseColor: string, + theme: GrafanaTheme2, + colorModeId?: FieldColorModeId | string, + forSegment?: boolean +): Array<{ color: string; percent: number }> { + if (gradientMode === 'none') { + return [ + { color: baseColor, percent: 0 }, + { color: baseColor, percent: 1 }, + ]; + } + + const colorMode = getFieldColorMode(colorModeId); + + // TODO we need to return thresholded values here. those will have breakpoints + // which map to exact percentages, and we should show that correctly. + // Handle continuous color modes first + if (colorMode.isContinuous && colorMode.getColors && !forSegment) { + const colors = colorMode.getColors(theme); + return colors.map((color, idx) => ({ color, percent: idx / (colors.length - 1) })); + } else if (colorMode.isByValue) { + // 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 + 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 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 + // we set the highest contrast color second based on the theme. + const darkerColor = tinycolor(baseColor).spin(-20).darken(5); + const lighterColor = tinycolor(baseColor).saturate(20).spin(20).brighten(10); + return theme.isDark + ? [ + { color: darkerColor.darken(10).toString(), percent: 0 }, + { color: lighterColor.lighten(10).toString(), percent: 1 }, + ] + : [ + { color: lighterColor.lighten(10).toString(), percent: 0 }, + { color: darkerColor.toString(), percent: 1 }, + ]; +} From 23e6c3301baafe87b3bb66657f8808a681e48ce4 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 20:40:38 -0500 Subject: [PATCH 17/48] wip: overhaul color in gauge --- .../components/RadialGauge/RadialArcPath.tsx | 48 ++++++++++++------- .../src/components/RadialGauge/RadialBar.tsx | 7 +++ .../RadialGauge/RadialBarSegmented.tsx | 9 +++- .../RadialGauge/RadialColorDefs.tsx | 29 +++++------ .../components/RadialGauge/RadialGauge.tsx | 3 ++ .../components/RadialGauge/ThresholdsBar.tsx | 6 ++- .../src/components/RadialGauge/colors.ts | 37 +++++++++++--- 7 files changed, 95 insertions(+), 44 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index b1a068c93a6..abf142bc46c 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,15 +1,15 @@ import { useId } from 'react'; -import { colorManipulator } from '@grafana/data'; - import { RadialColorDefs } from './RadialColorDefs'; +import { RadialShape } from './RadialGauge'; import { GaugeDimensions, toRad } from './utils'; export interface RadialArcPathPropsBase { startAngle: number; dimensions: GaugeDimensions; - colorDef: RadialColorDefs; + colorDefs: RadialColorDefs; arcLengthDeg: number; + shape: RadialShape; color?: string; gradient?: string; glowFilter?: string; @@ -116,9 +116,10 @@ export function RadialArcPath({ startAngle: angle, dimensions, color, - colorDef, - gradient, + colorDefs, + shape, arcLengthDeg, + glowFilter, roundedBars, showGuideDots, guideDotStartColor, @@ -129,7 +130,7 @@ export function RadialArcPath({ const startRadians = toRad(angle); const endRadians = toRad(angle + arcLengthDeg); - const gradientList = colorDef?.getGradient(); + const [startColor, endColor] = colorDefs.getEndpointColors(); const path = drawRadialArcPath({ angle, arcLengthDeg, dimensions, roundedBars }); let x1 = centerX + radius * Math.cos(startRadians); @@ -144,19 +145,34 @@ export function RadialArcPath({ - -
- + + +
+ + - {roundedBars && gradientList && ( - <> - - {/* this would actually need to be the color determined by the display */} - - - )} {showGuideDots && ( <> + {shape === 'circle' && ( + <> + + + + )} + {arcLengthDeg > 5 && } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index bb0dac60d45..729f60ed630 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -2,6 +2,7 @@ import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; import { RadialColorDefs } from './RadialColorDefs'; +import { RadialShape } from './RadialGauge'; import { GaugeDimensions } from './utils'; export interface RadialBarProps { @@ -12,6 +13,7 @@ export interface RadialBarProps { startAngle: number; roundedBars?: boolean; glowFilter?: string; + shape: RadialShape; } export function RadialBar({ dimensions, @@ -21,6 +23,7 @@ export function RadialBar({ startAngle, roundedBars, glowFilter, + shape, }: RadialBarProps) { const theme = useTheme2(); const [startDotColor, endDotColor] = colorDefs.getGuideDotColors(); @@ -33,20 +36,24 @@ export function RadialBar({ startAngle={startAngle + angle} dimensions={dimensions} arcLengthDeg={angleRange - angle} + colorDefs={colorDefs} color={theme.colors.action.hover} roundedBars={roundedBars} + shape={shape} /> {/** The colored bar */} {colorDefs.getDefs()} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx index dd2ec0ac9bf..dbd668f3a00 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx @@ -4,6 +4,7 @@ import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; import { RadialColorDefs } from './RadialColorDefs'; +import { RadialShape } from './RadialGauge'; import { GaugeDimensions } from './utils'; export interface RadialBarSegmentedProps { @@ -15,6 +16,7 @@ export interface RadialBarSegmentedProps { glowFilter?: string; segmentCount: number; segmentSpacing: number; + shape: RadialShape; } export function RadialBarSegmented({ fieldDisplay, @@ -25,6 +27,7 @@ export function RadialBarSegmented({ segmentCount, segmentSpacing, colorDefs, + shape, }: RadialBarSegmentedProps) { const segments: React.ReactNode[] = []; const theme = useTheme2(); @@ -38,9 +41,9 @@ export function RadialBarSegmented({ for (let i = 0; i < segmentCountAdjusted; i++) { const angleValue = min + ((max - min) / segmentCountAdjusted) * i; - const angleColor = colorDefs.getSegmentColor(angleValue, i); + // const angleColor = colorDefs.getSegmentColor(angleValue, i); const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01; - const segmentColor = angleValue >= value ? theme.colors.action.hover : angleColor; + const segmentColor = angleValue >= value ? theme.colors.action.hover : undefined; segments.push( diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx index 97b24b99054..87a6a0d4ba8 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx @@ -122,35 +122,24 @@ export class RadialColorDefs { } getGradient(baseColor = this.getFieldBaseColor(), forSegment?: boolean): Array<{ color: string; percent: number }> { - const { gradient, fieldDisplay, theme } = this.options; - return buildGradientColors(gradient, baseColor, theme, fieldDisplay.field.color?.mode, forSegment); + const { displayProcessor, gradient, fieldDisplay, theme } = this.options; + return buildGradientColors(gradient, baseColor, theme, displayProcessor, fieldDisplay, forSegment); } getGradientDef(): string { const gradientStops = this.getGradient(); const colorStrings = gradientStops.map((stop) => `${stop.color} ${(stop.percent * 100).toFixed(2)}%`); return this.options.shape === 'circle' - ? `conic-gradient(from -10deg, ${colorStrings.join(', ')})` + ? `conic-gradient(from 0deg, ${colorStrings.join(', ')})` : 'linear-gradient(90deg, ' + colorStrings.join(', ') + ')'; } - getGuideDotColors(): [string, string] { - const { dimensions, fieldDisplay, shape } = this.options; + getEndpointColors(): [string, string] { + const { fieldDisplay } = this.options; const gradient = this.getGradient(); - let valuePercent = fieldDisplay.display.percent ?? 0; - - // the linear gradient used in circular gradients means that we want to use the - // y position of the edge of the bar to determine the color. If we ever address - // that shortcoming, we could delete this block. - if (shape === 'circle') { - const angleDeg = ((valuePercent - 0.25) % 1) * 360; - const angleRad = (angleDeg * Math.PI) / 180; - const yPos = dimensions.centerY + dimensions.radius * Math.sin(angleRad); - valuePercent = yPos / (dimensions.centerY * 2); - } - - let startColor = gradient[0].color; + const valuePercent = fieldDisplay.display.percent ?? 0; + const startColor = gradient[0].color; let endColor = gradient[gradient.length - 1].color; // if we have a percentageFilled, use it to get a the correct end color based on where the bar terminates @@ -161,7 +150,11 @@ export class RadialColorDefs { ? endColorByPercentage.toHexString() : endColorByPercentage.toHex8String(); } + return [startColor, endColor]; + } + getGuideDotColors(): [string, string] { + const [startColor, endColor] = this.getEndpointColors(); return [getGuideDotColor(startColor), getGuideDotColor(endColor)]; } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index d20665872c7..fc03f9845e3 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -155,6 +155,7 @@ export function RadialGauge(props: RadialGaugeProps) { segmentCount={segmentCount} segmentSpacing={segmentSpacing} colorDefs={colorDefs} + shape={shape} /> ); } else { @@ -168,6 +169,7 @@ export function RadialGauge(props: RadialGaugeProps) { startAngle={startAngle} roundedBars={roundedBars} glowFilter={`url(#${glowFilterId})`} + shape={shape} /> ); } @@ -228,6 +230,7 @@ export function RadialGauge(props: RadialGaugeProps) { roundedBars={roundedBars} glowFilter={`url(#${glowFilterId})`} colorDefs={colorDefs} + shape={shape} /> ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx index 8977bbb6642..15e5349e8a5 100644 --- a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx @@ -2,6 +2,7 @@ import { FieldDisplay, Threshold } from '@grafana/data'; import { RadialArcPath } from './RadialArcPath'; import { RadialColorDefs } from './RadialColorDefs'; +import { RadialShape } from './RadialGauge'; import { GaugeDimensions } from './utils'; export interface Props { @@ -9,6 +10,7 @@ export interface Props { angleRange: number; startAngle: number; endAngle: number; + shape: RadialShape; fieldDisplay: FieldDisplay; roundedBars?: boolean; glowFilter?: string; @@ -24,6 +26,7 @@ export function ThresholdsBar({ glowFilter, colorDefs, thresholds, + shape, }: Props) { const fieldConfig = fieldDisplay.field; const min = fieldConfig.min ?? 0; @@ -55,10 +58,11 @@ export function ThresholdsBar({ key={i} startAngle={currentStart} arcLengthDeg={lengthDeg} + colorDefs={colorDefs} + shape={shape} dimensions={thresholdDimensions} roundedBars={roundedBars} glowFilter={glowFilter} - color={colorDefs.getColor(threshold.color, i)} /> ); diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts index 62a31470ed3..99e23452898 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -1,6 +1,7 @@ import tinycolor from 'tinycolor2'; -import { FieldColorModeId, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; +import { DisplayProcessor, FieldDisplay, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; +import { FieldColorModeId } from '@grafana/schema'; import { RadialGradientMode } from './RadialGauge'; @@ -8,7 +9,8 @@ export function buildGradientColors( gradientMode: RadialGradientMode, baseColor: string, theme: GrafanaTheme2, - colorModeId?: FieldColorModeId | string, + displayProcessor: DisplayProcessor, + fieldDisplay: FieldDisplay, forSegment?: boolean ): Array<{ color: string; percent: number }> { if (gradientMode === 'none') { @@ -18,15 +20,36 @@ export function buildGradientColors( ]; } - const colorMode = getFieldColorMode(colorModeId); + const colorMode = getFieldColorMode(fieldDisplay.field.color?.mode); + + if (colorMode.id === FieldColorModeId.Thresholds) { + const thresholds = fieldDisplay.field.thresholds?.steps ?? []; + const min = fieldDisplay.field.min ?? 0; + const max = fieldDisplay.field.max ?? 100; + + 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: threshold.color, percent }); + } + } + + result.push({ color: displayProcessor(max).color ?? baseColor, percent: 1 }); + + return result; + } - // TODO we need to return thresholded values here. those will have breakpoints - // which map to exact percentages, and we should show that correctly. - // Handle continuous color modes first if (colorMode.isContinuous && colorMode.getColors && !forSegment) { + // Handle continuous color modes first const colors = colorMode.getColors(theme); return colors.map((color, idx) => ({ color, percent: idx / (colors.length - 1) })); - } else if (colorMode.isByValue) { + } + + if (colorMode.isByValue) { // 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 const darkerColor = tinycolor(baseColor).darken(5); From 59ec3cc8a9f279e6395374e051513e833014ecc7 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 22:29:53 -0500 Subject: [PATCH 18/48] wip: progress on everything --- .../components/RadialGauge/RadialArcPath.tsx | 3 +- .../src/components/RadialGauge/RadialBar.tsx | 2 - .../RadialGauge/RadialBarSegmented.tsx | 19 ++-- .../RadialGauge/RadialColorDefs.tsx | 100 +----------------- .../components/RadialGauge/RadialGauge.tsx | 2 + .../components/RadialGauge/ThresholdsBar.tsx | 15 +-- 6 files changed, 25 insertions(+), 116 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index abf142bc46c..7492bc27199 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,4 +1,4 @@ -import { useId } from 'react'; +import { useId, useEffect } from 'react'; import { RadialColorDefs } from './RadialColorDefs'; import { RadialShape } from './RadialGauge'; @@ -11,7 +11,6 @@ export interface RadialArcPathPropsBase { arcLengthDeg: number; shape: RadialShape; color?: string; - gradient?: string; glowFilter?: string; roundedBars?: boolean; showGuideDots?: boolean; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index 729f60ed630..f3d5d987d55 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -47,7 +47,6 @@ export function RadialBar({ startAngle={startAngle} arcLengthDeg={angle} colorDefs={colorDefs} - gradient={colorDefs.getGradientDef()} roundedBars={roundedBars} glowFilter={glowFilter} showGuideDots={roundedBars} @@ -56,7 +55,6 @@ export function RadialBar({ shape={shape} /> - {colorDefs.getDefs()} ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx index dbd668f3a00..e6858b6cec2 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx @@ -4,7 +4,7 @@ import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; import { RadialColorDefs } from './RadialColorDefs'; -import { RadialShape } from './RadialGauge'; +import { RadialGradientMode, RadialShape } from './RadialGauge'; import { GaugeDimensions } from './utils'; export interface RadialBarSegmentedProps { @@ -17,6 +17,7 @@ export interface RadialBarSegmentedProps { segmentCount: number; segmentSpacing: number; shape: RadialShape; + gradientMode: RadialGradientMode; } export function RadialBarSegmented({ fieldDisplay, @@ -28,6 +29,7 @@ export function RadialBarSegmented({ segmentSpacing, colorDefs, shape, + gradientMode, }: RadialBarSegmentedProps) { const segments: React.ReactNode[] = []; const theme = useTheme2(); @@ -41,9 +43,13 @@ export function RadialBarSegmented({ for (let i = 0; i < segmentCountAdjusted; i++) { const angleValue = min + ((max - min) / segmentCountAdjusted) * i; - // const angleColor = colorDefs.getSegmentColor(angleValue, i); const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01; - const segmentColor = angleValue >= value ? theme.colors.action.hover : undefined; + let segmentColor: string | undefined; + if (angleValue >= value) { + segmentColor = theme.colors.action.hover; + } else if (gradientMode === 'none') { + segmentColor = colorDefs.getSegmentColor(angleValue); + } segments.push( - {segments} - {colorDefs.getDefs()} - - ); + return {segments}; } export function getAngleBetweenSegments(segmentSpacing: number, segmentCount: number, range: number) { diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx index 87a6a0d4ba8..fabd4b8dd3f 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx @@ -1,11 +1,4 @@ -import { - colorManipulator, - DisplayProcessor, - FALLBACK_COLOR, - FieldDisplay, - getFieldColorMode, - GrafanaTheme2, -} from '@grafana/data'; +import { colorManipulator, DisplayProcessor, FALLBACK_COLOR, FieldDisplay, GrafanaTheme2 } from '@grafana/data'; import { RadialGradientMode, RadialShape } from './RadialGauge'; import { buildGradientColors } from './colors'; @@ -29,98 +22,17 @@ const getGuideDotColor = (color: string): string => { }; export class RadialColorDefs { - private colorToIds: Record = {}; - private defs: React.ReactNode[] = []; - constructor(private options: RadialColorDefsOptions) {} - getSegmentColor(forValue: number, segmentIdx: number): string { + getSegmentColor(forValue: number): string { const { displayProcessor } = this.options; - const baseColor = displayProcessor(forValue).color ?? FALLBACK_COLOR; - return this.getColor(baseColor, segmentIdx); - } - - getColor(baseColor: string, segmentIdx?: number): string { - const { gradient, dimensions, gaugeId, fieldDisplay, shape } = this.options; - - let id = `value-color-${baseColor}-${gaugeId}`; - const forSegment = segmentIdx !== undefined; - if (forSegment) { - id += `-segment-${segmentIdx}`; - } - - 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; - - const gradientStops = this.getGradient(baseColor, forSegment); - const stops = gradientStops.map((stop, i) => ( - - )); - - // circular gradients are a little awkward today. we don't exactly have the result we - // want for continuous color modes, which would be to have the radial bar fill from the top - // around the circle. But SVG doesn't support that kind of gradient on stroke paths out-of-the-box, - // we'd need to implement something like https://gist.github.com/mbostock/4163057 - - // 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) { - this.defs.push( - - {stops} - - ); - - 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 x2 = shape === 'circle' ? 0 : 1 / valuePercent; - const y2 = shape === 'circle' ? 1 : 0; - this.defs.push( - - {stops} - - ); - 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; - - this.defs.push( - - {stops} - - ); - - return returnColor; + return displayProcessor(forValue).color ?? FALLBACK_COLOR; } getFieldBaseColor(): string { return this.options.fieldDisplay.display.color ?? FALLBACK_COLOR; } - getMainBarColor(): string { - return this.getColor(this.getFieldBaseColor()); - } - getGradient(baseColor = this.getFieldBaseColor(), forSegment?: boolean): Array<{ color: string; percent: number }> { const { displayProcessor, gradient, fieldDisplay, theme } = this.options; return buildGradientColors(gradient, baseColor, theme, displayProcessor, fieldDisplay, forSegment); @@ -131,7 +43,7 @@ export class RadialColorDefs { const colorStrings = gradientStops.map((stop) => `${stop.color} ${(stop.percent * 100).toFixed(2)}%`); return this.options.shape === 'circle' ? `conic-gradient(from 0deg, ${colorStrings.join(', ')})` - : 'linear-gradient(90deg, ' + colorStrings.join(', ') + ')'; + : `linear-gradient(90deg, ${colorStrings.join(', ')})`; } getEndpointColors(): [string, string] { @@ -157,8 +69,4 @@ export class RadialColorDefs { const [startColor, endColor] = this.getEndpointColors(); return [getGuideDotColor(startColor), getGuideDotColor(endColor)]; } - - getDefs(): React.ReactNode[] { - return this.defs; - } } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index fc03f9845e3..97048b6674a 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -156,6 +156,7 @@ export function RadialGauge(props: RadialGaugeProps) { segmentSpacing={segmentSpacing} colorDefs={colorDefs} shape={shape} + gradientMode={gradient} /> ); } else { @@ -231,6 +232,7 @@ export function RadialGauge(props: RadialGaugeProps) { glowFilter={`url(#${glowFilterId})`} colorDefs={colorDefs} shape={shape} + gradientMode={gradient} /> ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx index 15e5349e8a5..bf9ebdcc46a 100644 --- a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx @@ -1,8 +1,10 @@ import { FieldDisplay, Threshold } from '@grafana/data'; +import { useTheme2 } from '../../themes/ThemeContext'; + import { RadialArcPath } from './RadialArcPath'; import { RadialColorDefs } from './RadialColorDefs'; -import { RadialShape } from './RadialGauge'; +import { RadialGradientMode, RadialShape } from './RadialGauge'; import { GaugeDimensions } from './utils'; export interface Props { @@ -16,6 +18,7 @@ export interface Props { glowFilter?: string; colorDefs: RadialColorDefs; thresholds: Threshold[]; + gradientMode: RadialGradientMode; } export function ThresholdsBar({ dimensions, @@ -27,7 +30,9 @@ export function ThresholdsBar({ colorDefs, thresholds, shape, + gradientMode, }: Props) { + const theme = useTheme2(); const fieldConfig = fieldDisplay.field; const min = fieldConfig.min ?? 0; const max = fieldConfig.max ?? 100; @@ -59,6 +64,7 @@ export function ThresholdsBar({ startAngle={currentStart} arcLengthDeg={lengthDeg} colorDefs={colorDefs} + color={gradientMode === 'none' ? threshold.color : undefined} shape={shape} dimensions={thresholdDimensions} roundedBars={roundedBars} @@ -69,10 +75,5 @@ export function ThresholdsBar({ currentStart += lengthDeg; } - return ( - <> - {paths} - {colorDefs.getDefs()} - - ); + return {paths}; } From b300bd8b85fa84b2bdc31b1852acccbb792b32ff Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 23:13:08 -0500 Subject: [PATCH 19/48] refactoring defs into utils --- packages/grafana-data/src/index.ts | 2 +- .../src/themes/colorManipulator.ts | 8 +- packages/grafana-data/src/themes/types.ts | 6 + .../components/RadialGauge/RadialArcPath.tsx | 180 ++++++------------ .../src/components/RadialGauge/RadialBar.tsx | 82 ++++---- .../RadialGauge/RadialBarSegmented.tsx | 54 +----- .../RadialGauge/RadialColorDefs.tsx | 72 ------- .../components/RadialGauge/RadialGauge.tsx | 18 +- .../components/RadialGauge/ThresholdsBar.tsx | 14 +- .../src/components/RadialGauge/colors.ts | 48 ++++- .../src/components/RadialGauge/utils.ts | 78 ++++++++ 11 files changed, 255 insertions(+), 307 deletions(-) delete mode 100644 packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 63c36639e25..0c6ff34517a 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -319,7 +319,7 @@ export { type MonacoLanguageRegistryItem, monacoLanguageRegistry } from './monac export { createTheme } from './themes/createTheme'; export { getThemeById, getBuiltInThemes, type ThemeRegistryItem } from './themes/registry'; export type { NewThemeOptions } from './themes/createTheme'; -export type { ThemeRichColor, GrafanaTheme2 } from './themes/types'; +export type { ThemeRichColor, GrafanaTheme2, GradientStop } from './themes/types'; export type { ThemeColors } from './themes/createColors'; export type { ThemeBreakpoints, ThemeBreakpointsKey } from './themes/breakpoints'; export type { ThemeShadows } from './themes/createShadows'; diff --git a/packages/grafana-data/src/themes/colorManipulator.ts b/packages/grafana-data/src/themes/colorManipulator.ts index 42bbf454b1a..844671d50da 100644 --- a/packages/grafana-data/src/themes/colorManipulator.ts +++ b/packages/grafana-data/src/themes/colorManipulator.ts @@ -4,6 +4,8 @@ import tinycolor from 'tinycolor2'; +import { GradientStop } from './types'; + /** * Returns a number whose value is limited to the given range. * @param value The value to be clamped @@ -393,16 +395,14 @@ export const onBackground = ( }; /** + * @alpha * 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: Array<{ color: string; percent: number }>, - percent: number -): tinycolor.Instance { +export function colorAtGradientPercent(stops: GradientStop[], percent: number): tinycolor.Instance { if (!stops || stops.length < 2) { throw new Error('colorAtGradientPercent requires at least two color stops'); } diff --git a/packages/grafana-data/src/themes/types.ts b/packages/grafana-data/src/themes/types.ts index f586937cf3c..93f34705522 100644 --- a/packages/grafana-data/src/themes/types.ts +++ b/packages/grafana-data/src/themes/types.ts @@ -59,3 +59,9 @@ export interface ThemeRichColor { export type DeepPartial = { [P in keyof T]?: DeepPartial; }; + +/** @alpha */ +export interface GradientStop { + color: string; + percent: number; +} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index 7492bc27199..a7c43aeb63c 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,21 +1,25 @@ -import { useId, useEffect } from 'react'; +import { useId, useMemo } from 'react'; -import { RadialColorDefs } from './RadialColorDefs'; -import { RadialShape } from './RadialGauge'; -import { GaugeDimensions, toRad } from './utils'; +import { DisplayProcessor, FieldDisplay } from '@grafana/data'; + +import { useTheme2 } from '../../themes/ThemeContext'; + +import { RadialGradientMode, RadialShape } from './RadialGauge'; +import { buildGradientColors, getEndpointColors, getGradientCss, getGuideDotColors } from './colors'; +import { drawRadialArcPath, GaugeDimensions, toRad } from './utils'; export interface RadialArcPathPropsBase { - startAngle: number; - dimensions: GaugeDimensions; - colorDefs: RadialColorDefs; arcLengthDeg: number; - shape: RadialShape; color?: string; + dimensions: GaugeDimensions; + displayProcessor: DisplayProcessor; + fieldDisplay: FieldDisplay; glowFilter?: string; + gradientMode: RadialGradientMode; roundedBars?: boolean; + shape: RadialShape; showGuideDots?: boolean; - guideDotStartColor?: string; - guideDotEndColor?: string; + startAngle: number; } interface RadialArcPathPropsWithGuideDot extends RadialArcPathPropsBase { @@ -28,110 +32,60 @@ type RadialArcPathProps = RadialArcPathPropsBase | RadialArcPathPropsWithGuideDo const MAX_DOT_RADIUS = 8; -function drawRadialArcPath({ - angle, - arcLengthDeg, - dimensions, - roundedBars, -}: { - angle: number; - dimensions: GaugeDimensions; - arcLengthDeg: number; - roundedBars?: boolean; -}): string { - const { radius, centerX, centerY, barWidth } = dimensions; - - if (arcLengthDeg === 360) { - // For some reason a 100% full arc cannot be rendered - arcLengthDeg = 359.99; - } - - const startRadians = toRad(angle); - const endRadians = toRad(angle + arcLengthDeg); - - const largeArc = arcLengthDeg > 180 ? 1 : 0; - - const outerR = radius + barWidth / 2; - const innerR = Math.max(0, radius - barWidth / 2); - - 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); - - 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 - pathParts.push('L', ix2, iy2); - } - - if (innerR <= 0) { - // if inner radius collapsed to center, line to center and close - pathParts.push('L', centerX, centerY, 'Z'); - } else { - // 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 - pathParts.push('L', ox1, oy1); - } - - pathParts.push('Z'); - } - - return pathParts.join(' '); -} - export function RadialArcPath({ - startAngle: angle, - dimensions, - color, - colorDefs, - shape, arcLengthDeg, + color, + dimensions, + displayProcessor, + fieldDisplay, glowFilter, + gradientMode, roundedBars, + shape, showGuideDots, - guideDotStartColor, - guideDotEndColor, + startAngle: angle, }: RadialArcPathProps) { + const theme = useTheme2(); const id = useId(); + const { radius, centerX, centerY, barWidth } = dimensions; + const gradientStops = useMemo(() => { + if (gradientMode === 'none') { + return []; + } + return buildGradientColors(gradientMode, theme, displayProcessor, fieldDisplay, fieldDisplay.display.color); + }, [gradientMode, fieldDisplay, theme, displayProcessor]); + + const { guideDotColors, endpointColors } = useMemo(() => { + if (!showGuideDots || shape !== 'circle' || gradientStops.length === 0) { + return { + guideDotStartColor: undefined, + guideDotEndColor: undefined, + }; + } + return { + guideDotColors: getGuideDotColors(gradientStops, fieldDisplay.display.percent ?? 0), + endpointColors: getEndpointColors(gradientStops, fieldDisplay.display.percent ?? 0), + }; + }, [showGuideDots, shape, fieldDisplay, gradientStops]); + + const bgDivStyle = useMemo(() => { + const baseStyles = { width: '100%', height: '100%' }; + if (color) { + return { backgroundColor: color, ...baseStyles }; + } + const gradientCss = getGradientCss(gradientStops, shape); + return { backgroundImage: gradientCss, ...baseStyles }; + }, [color, gradientStops, shape]); + const startRadians = toRad(angle); const endRadians = toRad(angle + arcLengthDeg); - const [startColor, endColor] = colorDefs.getEndpointColors(); - const path = drawRadialArcPath({ angle, arcLengthDeg, dimensions, roundedBars }); + const path = useMemo( + () => drawRadialArcPath(angle, arcLengthDeg, dimensions, roundedBars), + [angle, arcLengthDeg, dimensions, roundedBars] + ); let x1 = centerX + radius * Math.cos(startRadians); let y1 = centerY + radius * Math.sin(startRadians); let x2 = centerX + radius * Math.cos(endRadians); @@ -152,28 +106,16 @@ export function RadialArcPath({ height={(radius + barWidth) * 2} clipPath={`url(#${id})`} > -
+
{showGuideDots && ( <> - {shape === 'circle' && ( - <> - - - - )} - - {arcLengthDeg > 5 && } - + {endpointColors && } + {endpointColors && } + {guideDotColors && arcLengthDeg > 5 && } + {guideDotColors && } )} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index f3d5d987d55..786c506af00 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -1,60 +1,64 @@ +import { DisplayProcessor, FieldDisplay } from '@grafana/data'; + import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialColorDefs } from './RadialColorDefs'; -import { RadialShape } from './RadialGauge'; +import { RadialGradientMode, RadialShape } from './RadialGauge'; import { GaugeDimensions } from './utils'; export interface RadialBarProps { - dimensions: GaugeDimensions; - colorDefs: RadialColorDefs; - angleRange: number; angle: number; - startAngle: number; - roundedBars?: boolean; + angleRange: number; + dimensions: GaugeDimensions; + displayProcessor: DisplayProcessor; + fieldDisplay: FieldDisplay; glowFilter?: string; + gradientMode: RadialGradientMode; + roundedBars?: boolean; shape: RadialShape; + startAngle: number; } export function RadialBar({ - dimensions, - colorDefs, - angleRange, angle, - startAngle, - roundedBars, + angleRange, + dimensions, + displayProcessor, + fieldDisplay, glowFilter, + gradientMode, + roundedBars, shape, + startAngle, }: RadialBarProps) { const theme = useTheme2(); - const [startDotColor, endDotColor] = colorDefs.getGuideDotColors(); - return ( <> - - {/** Track */} - - {/** The colored bar */} - - + {/** Track */} + + {/** The colored bar */} + ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx index e6858b6cec2..bff45475181 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx @@ -1,16 +1,15 @@ -import { FieldDisplay } from '@grafana/data'; +import { DisplayProcessor, FALLBACK_COLOR, FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialColorDefs } from './RadialColorDefs'; import { RadialGradientMode, RadialShape } from './RadialGauge'; import { GaugeDimensions } from './utils'; export interface RadialBarSegmentedProps { fieldDisplay: FieldDisplay; + displayProcessor: DisplayProcessor; dimensions: GaugeDimensions; - colorDefs: RadialColorDefs; angleRange: number; startAngle: number; glowFilter?: string; @@ -21,13 +20,13 @@ export interface RadialBarSegmentedProps { } export function RadialBarSegmented({ fieldDisplay, + displayProcessor, dimensions, startAngle, angleRange, glowFilter, segmentCount, segmentSpacing, - colorDefs, shape, gradientMode, }: RadialBarSegmentedProps) { @@ -48,7 +47,7 @@ export function RadialBarSegmented({ if (angleValue >= value) { segmentColor = theme.colors.action.hover; } else if (gradientMode === 'none') { - segmentColor = colorDefs.getSegmentColor(angleValue); + segmentColor = displayProcessor(angleValue).color ?? FALLBACK_COLOR; } segments.push( @@ -58,9 +57,11 @@ export function RadialBarSegmented({ dimensions={dimensions} color={segmentColor} shape={shape} - colorDefs={colorDefs} glowFilter={glowFilter} arcLengthDeg={segmentArcLengthDeg} + gradientMode={gradientMode} + fieldDisplay={fieldDisplay} + displayProcessor={displayProcessor} /> ); } @@ -89,44 +90,3 @@ function getOptimalSegmentCount( 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 ( -// -// ); -// } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx deleted file mode 100644 index fabd4b8dd3f..00000000000 --- a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { colorManipulator, DisplayProcessor, FALLBACK_COLOR, FieldDisplay, GrafanaTheme2 } from '@grafana/data'; - -import { RadialGradientMode, RadialShape } from './RadialGauge'; -import { buildGradientColors } from './colors'; -import { GaugeDimensions } from './utils'; - -export interface RadialColorDefsOptions { - gradient: RadialGradientMode; - fieldDisplay: FieldDisplay; - theme: GrafanaTheme2; - dimensions: GaugeDimensions; - shape: RadialShape; - gaugeId: string; - displayProcessor: DisplayProcessor; -} - -const CONTRAST_THRESHOLD_MAX = 4.5; -const getGuideDotColor = (color: string): string => { - const darkColor = '#111217'; // gray05 - const lightColor = '#fbfbfb'; // gray90 - return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor; -}; - -export class RadialColorDefs { - constructor(private options: RadialColorDefsOptions) {} - - getSegmentColor(forValue: number): string { - const { displayProcessor } = this.options; - return displayProcessor(forValue).color ?? FALLBACK_COLOR; - } - - getFieldBaseColor(): string { - return this.options.fieldDisplay.display.color ?? FALLBACK_COLOR; - } - - getGradient(baseColor = this.getFieldBaseColor(), forSegment?: boolean): Array<{ color: string; percent: number }> { - const { displayProcessor, gradient, fieldDisplay, theme } = this.options; - return buildGradientColors(gradient, baseColor, theme, displayProcessor, fieldDisplay, forSegment); - } - - getGradientDef(): string { - const gradientStops = this.getGradient(); - const colorStrings = gradientStops.map((stop) => `${stop.color} ${(stop.percent * 100).toFixed(2)}%`); - return this.options.shape === 'circle' - ? `conic-gradient(from 0deg, ${colorStrings.join(', ')})` - : `linear-gradient(90deg, ${colorStrings.join(', ')})`; - } - - getEndpointColors(): [string, string] { - const { fieldDisplay } = this.options; - - const gradient = this.getGradient(); - const valuePercent = fieldDisplay.display.percent ?? 0; - const startColor = gradient[0].color; - let endColor = gradient[gradient.length - 1].color; - - // if we have a percentageFilled, use it to get a the correct end color based on where the bar terminates - if (gradient.length >= 2) { - const endColorByPercentage = colorManipulator.colorAtGradientPercent(gradient, valuePercent); - endColor = - endColorByPercentage.getAlpha() === 1 - ? endColorByPercentage.toHexString() - : endColorByPercentage.toHex8String(); - } - return [startColor, endColor]; - } - - getGuideDotColors(): [string, string] { - const [startColor, endColor] = this.getEndpointColors(); - return [getGuideDotColor(startColor), getGuideDotColor(endColor)]; - } -} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index 97048b6674a..0771a52f9d1 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -16,7 +16,6 @@ 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'; @@ -133,15 +132,6 @@ export function RadialGauge(props: RadialGaugeProps) { const displayProcessor = getFieldDisplayProcessor(displayValue); const glowFilterId = `glow-${gaugeId}`; - const colorDefs = new RadialColorDefs({ - gradient, - fieldDisplay: displayValue, - theme, - dimensions, - shape, - gaugeId, - displayProcessor, - }); if (segmentCount > 1) { graphics.push( @@ -154,9 +144,9 @@ export function RadialGauge(props: RadialGaugeProps) { glowFilter={`url(#${glowFilterId})`} segmentCount={segmentCount} segmentSpacing={segmentSpacing} - colorDefs={colorDefs} shape={shape} gradientMode={gradient} + displayProcessor={displayProcessor} /> ); } else { @@ -164,13 +154,15 @@ export function RadialGauge(props: RadialGaugeProps) { ); } @@ -230,9 +222,9 @@ export function RadialGauge(props: RadialGaugeProps) { angleRange={angleRange} roundedBars={roundedBars} glowFilter={`url(#${glowFilterId})`} - colorDefs={colorDefs} shape={shape} gradientMode={gradient} + displayProcessor={displayProcessor} /> ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx index bf9ebdcc46a..784e14aa397 100644 --- a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx @@ -1,9 +1,6 @@ -import { FieldDisplay, Threshold } from '@grafana/data'; - -import { useTheme2 } from '../../themes/ThemeContext'; +import { DisplayProcessor, FieldDisplay, Threshold } from '@grafana/data'; import { RadialArcPath } from './RadialArcPath'; -import { RadialColorDefs } from './RadialColorDefs'; import { RadialGradientMode, RadialShape } from './RadialGauge'; import { GaugeDimensions } from './utils'; @@ -16,9 +13,9 @@ export interface Props { fieldDisplay: FieldDisplay; roundedBars?: boolean; glowFilter?: string; - colorDefs: RadialColorDefs; thresholds: Threshold[]; gradientMode: RadialGradientMode; + displayProcessor: DisplayProcessor; } export function ThresholdsBar({ dimensions, @@ -27,12 +24,11 @@ export function ThresholdsBar({ angleRange, roundedBars, glowFilter, - colorDefs, thresholds, shape, gradientMode, + displayProcessor, }: Props) { - const theme = useTheme2(); const fieldConfig = fieldDisplay.field; const min = fieldConfig.min ?? 0; const max = fieldConfig.max ?? 100; @@ -63,12 +59,14 @@ export function ThresholdsBar({ key={i} startAngle={currentStart} arcLengthDeg={lengthDeg} - colorDefs={colorDefs} color={gradientMode === 'none' ? threshold.color : undefined} shape={shape} dimensions={thresholdDimensions} roundedBars={roundedBars} glowFilter={glowFilter} + gradientMode={gradientMode} + displayProcessor={displayProcessor} + fieldDisplay={fieldDisplay} /> ); diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts index 99e23452898..ed1ace25f5b 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -1,18 +1,26 @@ import tinycolor from 'tinycolor2'; -import { DisplayProcessor, FieldDisplay, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; +import { + colorManipulator, + DisplayProcessor, + FALLBACK_COLOR, + FieldDisplay, + getFieldColorMode, + GradientStop, + GrafanaTheme2, +} from '@grafana/data'; import { FieldColorModeId } from '@grafana/schema'; -import { RadialGradientMode } from './RadialGauge'; +import { RadialGradientMode, RadialShape } from './RadialGauge'; export function buildGradientColors( gradientMode: RadialGradientMode, - baseColor: string, theme: GrafanaTheme2, displayProcessor: DisplayProcessor, fieldDisplay: FieldDisplay, + baseColor = FALLBACK_COLOR, forSegment?: boolean -): Array<{ color: string; percent: number }> { +): GradientStop[] { if (gradientMode === 'none') { return [ { color: baseColor, percent: 0 }, @@ -80,3 +88,35 @@ export function buildGradientColors( { color: darkerColor.toString(), percent: 1 }, ]; } + +export function getEndpointColors(gradientStops: GradientStop[], percent = 0): [string, string] { + 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 = colorManipulator.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(', ')})`; +} + +const CONTRAST_THRESHOLD_MAX = 4.5; +const getGuideDotColor = (color: string): string => { + const darkColor = '#111217'; // gray05 + const lightColor = '#fbfbfb'; // gray90 + return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor; +}; + +export function getGuideDotColors(gradientStops: GradientStop[], percent = 0): [string, string] { + const [startColor, endColor] = getEndpointColors(gradientStops, percent); + return [getGuideDotColor(startColor), getGuideDotColor(endColor)]; +} diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.ts b/packages/grafana-ui/src/components/RadialGauge/utils.ts index 44f767d89b2..a0746ff54de 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.ts @@ -155,3 +155,81 @@ export function toCartesian(centerX: number, centerY: number, radius: number, an y: centerY + radius * Math.sin(radian), }; } + +export function drawRadialArcPath( + angle: number, + arcLengthDeg: number, + dimensions: GaugeDimensions, + roundedBars?: boolean +): string { + const { radius, centerX, centerY, barWidth } = dimensions; + + if (arcLengthDeg === 360) { + // For some reason a 100% full arc cannot be rendered + arcLengthDeg = 359.99; + } + + const startRadians = toRad(angle); + const endRadians = toRad(angle + arcLengthDeg); + + const largeArc = arcLengthDeg > 180 ? 1 : 0; + + const outerR = radius + barWidth / 2; + const innerR = Math.max(0, radius - barWidth / 2); + + 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); + + 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 + pathParts.push('L', ix2, iy2); + } + + if (innerR <= 0) { + // if inner radius collapsed to center, line to center and close + pathParts.push('L', centerX, centerY, 'Z'); + } else { + // 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 + pathParts.push('L', ox1, oy1); + } + + pathParts.push('Z'); + } + + return pathParts.join(' '); +} From 77138f640a8de8c72891e0b3b9ffc8ceac418356 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 23:23:30 -0500 Subject: [PATCH 20/48] its all working --- .../components/RadialGauge/RadialArcPath.tsx | 170 +++++++++--------- .../src/components/RadialGauge/colors.ts | 4 +- 2 files changed, 92 insertions(+), 82 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index a7c43aeb63c..65fe54ae285 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,4 +1,4 @@ -import { useId, useMemo } from 'react'; +import { useId, useMemo, memo } from 'react'; import { DisplayProcessor, FieldDisplay } from '@grafana/data'; @@ -30,94 +30,104 @@ interface RadialArcPathPropsWithGuideDot extends RadialArcPathPropsBase { type RadialArcPathProps = RadialArcPathPropsBase | RadialArcPathPropsWithGuideDot; +const DOT_RADIUS_FACTOR = 0.4; const MAX_DOT_RADIUS = 8; -export function RadialArcPath({ - arcLengthDeg, - color, - dimensions, - displayProcessor, - fieldDisplay, - glowFilter, - gradientMode, - roundedBars, - shape, - showGuideDots, - startAngle: angle, -}: RadialArcPathProps) { - const theme = useTheme2(); - const id = useId(); +export const RadialArcPath = memo( + ({ + arcLengthDeg, + color, + dimensions, + displayProcessor, + fieldDisplay, + glowFilter, + gradientMode, + roundedBars, + shape, + showGuideDots, + startAngle: angle, + }: RadialArcPathProps) => { + const theme = useTheme2(); + const id = useId(); - const { radius, centerX, centerY, barWidth } = dimensions; + const gradientStops = useMemo(() => { + if (gradientMode === 'none') { + return []; + } + return buildGradientColors(gradientMode, theme, displayProcessor, fieldDisplay, fieldDisplay.display.color); + }, [gradientMode, fieldDisplay, theme, displayProcessor]); - const gradientStops = useMemo(() => { - if (gradientMode === 'none') { - return []; - } - return buildGradientColors(gradientMode, theme, displayProcessor, fieldDisplay, fieldDisplay.display.color); - }, [gradientMode, fieldDisplay, theme, displayProcessor]); - - const { guideDotColors, endpointColors } = useMemo(() => { - if (!showGuideDots || shape !== 'circle' || gradientStops.length === 0) { + const { guideDotColors, endpointColors } = useMemo(() => { + if (!showGuideDots || gradientStops.length === 0) { + return { + guideDotStartColor: undefined, + guideDotEndColor: undefined, + }; + } return { - guideDotStartColor: undefined, - guideDotEndColor: undefined, + guideDotColors: getGuideDotColors(gradientStops, fieldDisplay.display.percent ?? 0), + endpointColors: + shape === 'circle' ? getEndpointColors(gradientStops, fieldDisplay.display.percent ?? 0) : undefined, }; - } - return { - guideDotColors: getGuideDotColors(gradientStops, fieldDisplay.display.percent ?? 0), - endpointColors: getEndpointColors(gradientStops, fieldDisplay.display.percent ?? 0), - }; - }, [showGuideDots, shape, fieldDisplay, gradientStops]); + }, [showGuideDots, fieldDisplay, gradientStops, shape]); - const bgDivStyle = useMemo(() => { - const baseStyles = { width: '100%', height: '100%' }; - if (color) { - return { backgroundColor: color, ...baseStyles }; - } - const gradientCss = getGradientCss(gradientStops, shape); - return { backgroundImage: gradientCss, ...baseStyles }; - }, [color, gradientStops, shape]); + const bgDivStyle = useMemo(() => { + const baseStyles = { width: '100%', height: '100%' }; + if (color) { + return { backgroundColor: color, ...baseStyles }; + } + const gradientCss = getGradientCss(gradientStops, shape); + return { backgroundImage: gradientCss, ...baseStyles }; + }, [color, gradientStops, shape]); - const startRadians = toRad(angle); - const endRadians = toRad(angle + arcLengthDeg); + const { radius, centerX, centerY, barWidth } = dimensions; - const path = useMemo( - () => drawRadialArcPath(angle, arcLengthDeg, dimensions, roundedBars), - [angle, arcLengthDeg, dimensions, roundedBars] - ); - 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 path = useMemo( + () => drawRadialArcPath(angle, arcLengthDeg, dimensions, roundedBars), + [angle, arcLengthDeg, dimensions, roundedBars] + ); - const dotRadius = Math.min((barWidth / 2) * 0.4, MAX_DOT_RADIUS); + const { x1, x2, y1, y2 } = useMemo(() => { + const startRadians = toRad(angle); + const endRadians = toRad(angle + arcLengthDeg); - return ( - <> - - - - - -
- - + 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); + return { x1, y1, x2, y2 }; + }, [angle, arcLengthDeg, centerX, centerY, radius]); - {showGuideDots && ( - <> - {endpointColors && } - {endpointColors && } - {guideDotColors && arcLengthDeg > 5 && } - {guideDotColors && } - - )} - - ); -} + const dotRadius = Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS); + + return ( + <> + + + + + +
+ + + + {showGuideDots && ( + <> + {endpointColors && } + {endpointColors && } + {guideDotColors && arcLengthDeg > 5 && } + {guideDotColors && } + + )} + + ); + } +); + +RadialArcPath.displayName = 'RadialArcPath'; diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts index ed1ace25f5b..e1d323b88a5 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -36,13 +36,13 @@ export function buildGradientColors( const max = fieldDisplay.field.max ?? 100; const result: Array<{ color: string; percent: number }> = [ - { color: displayProcessor(min).color ?? baseColor, percent: 0 }, + { color: displayProcessor(min).color ?? FALLBACK_COLOR, percent: 0 }, ]; for (const threshold of thresholds) { if (threshold.value > min && threshold.value < max) { const percent = (threshold.value - min) / (max - min); - result.push({ color: threshold.color, percent }); + result.push({ color: theme.visualization.getColorByName(threshold.color), percent }); } } From e3bced33a79a3eacc77ebebf237f0700c1dce4e5 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 23:25:39 -0500 Subject: [PATCH 21/48] fixme comment --- packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index 65fe54ae285..f105b821577 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -102,6 +102,7 @@ export const RadialArcPath = memo( return ( <> + {/* FIXME: optimize this by only using clippath + foreign obj for gradients */} From bf40fbe0646fbed27f96967f9725758d803fab74 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 23:34:11 -0500 Subject: [PATCH 22/48] fix backend migration tests --- .../panel-gauge/v0alpha1.gauge_tests_new.v42.json | 8 ++++---- .../v0alpha1.gauge_tests_new.v42.v2alpha1.json | 10 ++++++++++ .../v0alpha1.gauge_tests_new.v42.v2beta1.json | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json index 6b4367e0c3a..a50cd7e5941 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json @@ -71,13 +71,13 @@ "id": 1, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, "gradient": false }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -149,13 +149,13 @@ "id": 4, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, "gradient": false }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -227,13 +227,13 @@ "id": 3, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, "gradient": false }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -305,13 +305,13 @@ "id": 5, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, "gradient": false }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json index 21fd7181fb6..40b7e0a89e2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json @@ -1005,6 +1005,7 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, @@ -1095,6 +1096,7 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, @@ -1181,6 +1183,7 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1271,6 +1274,7 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, @@ -1361,6 +1365,7 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, @@ -1451,6 +1456,7 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, @@ -1632,6 +1638,7 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1722,6 +1729,7 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1816,6 +1824,7 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, @@ -1910,6 +1919,7 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json index 6767812970e..92fe497293b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json @@ -1603,7 +1603,7 @@ "version": "13.0.0-pre", "spec": { "options": { - "barShape": "flat", + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, From e21ca340be469f78c8f3b3a3a05f520a03d4c15b Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 23:37:30 -0500 Subject: [PATCH 23/48] remove any other mentions of spotlights --- .../v0alpha1.gauge_tests_new.v42.json | 78 --------------- .../v0alpha1.gauge_tests_new.v42.v1beta1.json | 78 --------------- ...v0alpha1.gauge_tests_new.v42.v2alpha1.json | 91 ------------------ .../v0alpha1.gauge_tests_new.v42.v2beta1.json | 94 ------------------- .../panel-gauge/gauge_tests_new.v42.json | 78 --------------- .../panel-gauge/gauge_tests_new.json | 76 --------------- .../plugins/panel/radialbar/suggestions.ts | 13 --- 7 files changed, 508 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json index a50cd7e5941..b6d6e826a6e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json @@ -268,84 +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": { - "barShape": "rounded", - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": 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" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json index aebdc53aa18..a7c479384a3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json @@ -274,84 +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": { - "barShape": "rounded", - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": 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" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json index 40b7e0a89e2..68a5ba40fe9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json @@ -2067,97 +2067,6 @@ } } }, - "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": { - "barShape": "rounded", - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "gradient": 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-8": { "kind": "Panel", "spec": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json index 92fe497293b..1f16b1924f7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json @@ -2134,100 +2134,6 @@ } } }, - "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": { - "barShape": "rounded", - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "gradient": 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-8": { "kind": "Panel", "spec": { diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json index 3f35e96c3cc..e8577b15a6e 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json @@ -268,84 +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, - "gradient": false - }, - "barShape": "rounded", - "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" diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index 530dcf8d63f..af45ba72e6e 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -262,82 +262,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 - }, - "barShape": "rounded", - "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" diff --git a/public/app/plugins/panel/radialbar/suggestions.ts b/public/app/plugins/panel/radialbar/suggestions.ts index 00896ae8458..eab5334ef40 100644 --- a/public/app/plugins/panel/radialbar/suggestions.ts +++ b/public/app/plugins/panel/radialbar/suggestions.ts @@ -18,19 +18,6 @@ const withDefaults = ( } }, }, - // styles: [{ - // name: t('gauge.suggestions.style.circular', 'Glowing'), - // options: { - // effects: { - // rounded: true, - // barGlow: true, - // centerGlow: true, - // spotlight: true, - // }, - // }, - // }, { - // name: t('gauge.suggestions.style.simple', 'Simple'), - // }] } satisfies VisualizationSuggestion); const MAX_GAUGES = 10; From ca1a431cc8635897a2b079dd58cc07aefacfd8fe Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 23:56:52 -0500 Subject: [PATCH 24/48] one more tweak --- .../src/components/RadialGauge/RadialArcPath.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index f105b821577..128566cec97 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -106,6 +106,7 @@ export const RadialArcPath = memo( +
+ {endpointColors && } + {endpointColors && } - {showGuideDots && ( - <> - {endpointColors && } - {endpointColors && } - {guideDotColors && arcLengthDeg > 5 && } - {guideDotColors && } - - )} + {guideDotColors && arcLengthDeg > 5 && } + {guideDotColors && } ); } From a8325b1e8754c26d8790ab6f69681aa40049f682 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 16 Dec 2025 00:15:01 -0500 Subject: [PATCH 25/48] update gdev --- .../panel-gauge/gauge_tests_new.json | 163 +++++++++--------- .../src/components/RadialGauge/effects.tsx | 11 +- public/locales/en-US/grafana.json | 6 +- 3 files changed, 91 insertions(+), 89 deletions(-) diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index af45ba72e6e..4d0cd7d5579 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -71,13 +71,13 @@ "id": 1, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, "gradient": false }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -147,13 +147,13 @@ "id": 4, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, "gradient": false }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -223,13 +223,13 @@ "id": 3, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, "gradient": false }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -293,19 +293,19 @@ "gridPos": { "h": 6, "w": 4, - "x": 16, + "x": 12, "y": 1 }, "id": 8, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, "gradient": false }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -369,19 +369,19 @@ "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -445,19 +445,19 @@ "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -496,7 +496,7 @@ "h": 1, "w": 24, "x": 0, - "y": 13 + "y": 7 }, "id": 17, "panels": [], @@ -533,20 +533,20 @@ }, "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 }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -609,20 +609,20 @@ }, "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 }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -685,20 +685,20 @@ }, "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 }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -761,20 +761,20 @@ }, "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 }, - "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -813,7 +813,7 @@ "h": 1, "w": 24, "x": 0, - "y": 20 + "y": 14 }, "id": 24, "panels": [], @@ -854,20 +854,20 @@ }, "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -930,20 +930,20 @@ }, "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1006,20 +1006,20 @@ }, "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1081,21 +1081,21 @@ "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1157,21 +1157,21 @@ "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1206,7 +1206,7 @@ "h": 1, "w": 24, "x": 0, - "y": 34 + "y": 21 }, "id": 31, "panels": [], @@ -1253,18 +1253,18 @@ "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1333,18 +1333,18 @@ "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1413,18 +1413,18 @@ "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1459,7 +1459,7 @@ "h": 1, "w": 24, "x": 0, - "y": 45 + "y": 32 }, "id": 6, "panels": [], @@ -1500,11 +1500,12 @@ "h": 6, "w": 24, "x": 0, - "y": 46 + "y": 33 }, "id": 9, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { @@ -1512,7 +1513,6 @@ "centerGlow": true, "gradient": true }, - "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1580,11 +1580,12 @@ "h": 6, "w": 24, "x": 0, - "y": 52 + "y": 39 }, "id": 11, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { @@ -1592,7 +1593,6 @@ "centerGlow": true, "gradient": true }, - "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1632,7 +1632,7 @@ "h": 1, "w": 24, "x": 0, - "y": 58 + "y": 45 }, "id": 12, "panels": [], @@ -1674,11 +1674,12 @@ "h": 7, "w": 4, "x": 0, - "y": 59 + "y": 46 }, "id": 13, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { @@ -1686,7 +1687,6 @@ "centerGlow": true, "gradient": true }, - "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1755,11 +1755,12 @@ "h": 7, "w": 5, "x": 4, - "y": 59 + "y": 46 }, "id": 14, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { @@ -1767,7 +1768,6 @@ "centerGlow": true, "gradient": true }, - "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1835,11 +1835,12 @@ "h": 7, "w": 5, "x": 9, - "y": 59 + "y": 46 }, "id": 15, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { @@ -1847,7 +1848,6 @@ "centerGlow": true, "gradient": true }, - "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1915,11 +1915,12 @@ "h": 7, "w": 6, "x": 14, - "y": 59 + "y": 46 }, "id": 16, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { @@ -1927,7 +1928,6 @@ "centerGlow": true, "gradient": true }, - "barShape": "rounded", "glow": "both", "orientation": "auto", "reduceOptions": { @@ -1967,7 +1967,7 @@ "h": 1, "w": 24, "x": 0, - "y": 66 + "y": 53 }, "id": 35, "panels": [], @@ -1998,20 +1998,20 @@ "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -2063,20 +2063,20 @@ "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 }, - "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -2115,5 +2115,6 @@ "timezone": "browser", "title": "Panel tests - Gauge (new)", "uid": "panel-tests-gauge-new", - "version": 9 + "version": 22, + "weekStart": "" } diff --git a/packages/grafana-ui/src/components/RadialGauge/effects.tsx b/packages/grafana-ui/src/components/RadialGauge/effects.tsx index 759ae4241e8..0f1163e21de 100644 --- a/packages/grafana-ui/src/components/RadialGauge/effects.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/effects.tsx @@ -5,9 +5,12 @@ export interface GlowGradientProps { 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 ( @@ -20,10 +23,12 @@ export function GlowGradient({ id, barWidth }: GlowGradientProps) { ); } +const CENTER_GLOW_OPACITY = 0.15; + export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) { return ( - + ); @@ -42,7 +47,7 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps <> - + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 5eb9615cc3c..a4ee917fe3c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -7955,11 +7955,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}}" }, From 942ed4b4e898ca781c27f6eac71d1dca4d9bcded Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 16 Dec 2025 11:15:19 -0500 Subject: [PATCH 26/48] add lots of tests and reorganize the code a bit --- packages/grafana-data/src/index.ts | 2 +- .../src/themes/colorManipulator.test.ts | 45 --- .../src/themes/colorManipulator.ts | 50 --- packages/grafana-data/src/themes/types.ts | 6 - .../components/RadialGauge/RadialArcPath.tsx | 16 +- .../src/components/RadialGauge/RadialBar.tsx | 13 +- .../RadialGauge/RadialBarSegmented.tsx | 128 ++++---- .../RadialGauge/RadialGauge.story.tsx | 3 +- .../components/RadialGauge/RadialGauge.tsx | 29 +- .../RadialGauge/RadialScaleLabels.tsx | 119 ++++---- .../RadialGauge/RadialSparkline.tsx | 22 +- .../src/components/RadialGauge/RadialText.tsx | 226 +++++++------- .../components/RadialGauge/ThresholdsBar.tsx | 35 +-- .../__snapshots__/utils.test.ts.snap | 17 ++ .../src/components/RadialGauge/colors.test.ts | 286 ++++++++++++++++++ .../src/components/RadialGauge/colors.ts | 110 +++++-- .../src/components/RadialGauge/effects.tsx | 8 +- .../src/components/RadialGauge/types.ts | 26 ++ .../src/components/RadialGauge/utils.test.ts | 199 +++++++++++- .../src/components/RadialGauge/utils.ts | 138 ++++++--- 20 files changed, 969 insertions(+), 509 deletions(-) create mode 100644 packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap create mode 100644 packages/grafana-ui/src/components/RadialGauge/colors.test.ts create mode 100644 packages/grafana-ui/src/components/RadialGauge/types.ts diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 0c6ff34517a..63c36639e25 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -319,7 +319,7 @@ export { type MonacoLanguageRegistryItem, monacoLanguageRegistry } from './monac export { createTheme } from './themes/createTheme'; export { getThemeById, getBuiltInThemes, type ThemeRegistryItem } from './themes/registry'; export type { NewThemeOptions } from './themes/createTheme'; -export type { ThemeRichColor, GrafanaTheme2, GradientStop } from './themes/types'; +export type { ThemeRichColor, GrafanaTheme2 } from './themes/types'; export type { ThemeColors } from './themes/createColors'; export type { ThemeBreakpoints, ThemeBreakpointsKey } from './themes/breakpoints'; export type { ThemeShadows } from './themes/createShadows'; diff --git a/packages/grafana-data/src/themes/colorManipulator.test.ts b/packages/grafana-data/src/themes/colorManipulator.test.ts index b003e4baf63..4407fc3ad8a 100644 --- a/packages/grafana-data/src/themes/colorManipulator.test.ts +++ b/packages/grafana-data/src/themes/colorManipulator.test.ts @@ -437,49 +437,4 @@ describe('utils/colorManipulator', () => { expect(onBackground('rgba(0,0,255,1)', 'rgba(0,0,0,0.5)').toRgbString()).toBe('rgb(0, 0, 255)'); }); }); - - 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('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'); - }); - }); }); diff --git a/packages/grafana-data/src/themes/colorManipulator.ts b/packages/grafana-data/src/themes/colorManipulator.ts index 844671d50da..eccb661b7d8 100644 --- a/packages/grafana-data/src/themes/colorManipulator.ts +++ b/packages/grafana-data/src/themes/colorManipulator.ts @@ -4,8 +4,6 @@ import tinycolor from 'tinycolor2'; -import { GradientStop } from './types'; - /** * Returns a number whose value is limited to the given range. * @param value The value to be clamped @@ -394,53 +392,6 @@ export const onBackground = ( }); }; -/** - * @alpha - * 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 - const sorted = stops - .map((s) => ({ color: s.color, percent: clamp(s.percent, 0, 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 - let left = sorted[0]; - let right = sorted[sorted.length - 1]; - for (let i = 1; i < sorted.length; i++) { - if (percent <= sorted[i].percent) { - left = sorted[i - 1]; - right = sorted[i]; - break; - } - } - - const range = right.percent - left.percent; - const t = range === 0 ? 0 : (percent - left.percent) / range; // 0..1 - - // tinycolor.mix expects amount as percentage of the second color - const mixed = tinycolor.mix(left.color, right.color, t * 100); - - // return hex6 if opaque, hex8 if has alpha - return mixed; -} - interface DecomposeColor { type: string; values: any; @@ -463,5 +414,4 @@ export const colorManipulator = { darken, lighten, onBackground, - colorAtGradientPercent, }; diff --git a/packages/grafana-data/src/themes/types.ts b/packages/grafana-data/src/themes/types.ts index 93f34705522..f586937cf3c 100644 --- a/packages/grafana-data/src/themes/types.ts +++ b/packages/grafana-data/src/themes/types.ts @@ -59,9 +59,3 @@ export interface ThemeRichColor { export type DeepPartial = { [P in keyof T]?: DeepPartial; }; - -/** @alpha */ -export interface GradientStop { - color: string; - percent: number; -} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index 128566cec97..b08085af48e 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,18 +1,17 @@ import { useId, useMemo, memo } from 'react'; -import { DisplayProcessor, FieldDisplay } from '@grafana/data'; +import { FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; -import { RadialGradientMode, RadialShape } from './RadialGauge'; import { buildGradientColors, getEndpointColors, getGradientCss, getGuideDotColors } from './colors'; -import { drawRadialArcPath, GaugeDimensions, toRad } from './utils'; +import { RadialGradientMode, RadialShape, RadialGaugeDimensions } from './types'; +import { drawRadialArcPath, toRad } from './utils'; export interface RadialArcPathPropsBase { arcLengthDeg: number; color?: string; - dimensions: GaugeDimensions; - displayProcessor: DisplayProcessor; + dimensions: RadialGaugeDimensions; fieldDisplay: FieldDisplay; glowFilter?: string; gradientMode: RadialGradientMode; @@ -38,7 +37,6 @@ export const RadialArcPath = memo( arcLengthDeg, color, dimensions, - displayProcessor, fieldDisplay, glowFilter, gradientMode, @@ -54,8 +52,8 @@ export const RadialArcPath = memo( if (gradientMode === 'none') { return []; } - return buildGradientColors(gradientMode, theme, displayProcessor, fieldDisplay, fieldDisplay.display.color); - }, [gradientMode, fieldDisplay, theme, displayProcessor]); + return buildGradientColors(gradientMode, theme, fieldDisplay, fieldDisplay.display.color); + }, [gradientMode, fieldDisplay, theme]); const { guideDotColors, endpointColors } = useMemo(() => { if (!showGuideDots || gradientStops.length === 0) { @@ -67,7 +65,7 @@ export const RadialArcPath = memo( return { guideDotColors: getGuideDotColors(gradientStops, fieldDisplay.display.percent ?? 0), endpointColors: - shape === 'circle' ? getEndpointColors(gradientStops, fieldDisplay.display.percent ?? 0) : undefined, + shape === 'circle' ? getEndpointColors(gradientStops, fieldDisplay.display.percent ?? 1) : undefined, }; }, [showGuideDots, fieldDisplay, gradientStops, shape]); diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index 786c506af00..cf2a50b55cb 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -1,16 +1,14 @@ -import { DisplayProcessor, FieldDisplay } from '@grafana/data'; +import { FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialGradientMode, RadialShape } from './RadialGauge'; -import { GaugeDimensions } from './utils'; +import { RadialGradientMode, RadialShape, RadialGaugeDimensions } from './types'; export interface RadialBarProps { angle: number; angleRange: number; - dimensions: GaugeDimensions; - displayProcessor: DisplayProcessor; + dimensions: RadialGaugeDimensions; fieldDisplay: FieldDisplay; glowFilter?: string; gradientMode: RadialGradientMode; @@ -22,7 +20,6 @@ export function RadialBar({ angle, angleRange, dimensions, - displayProcessor, fieldDisplay, glowFilter, gradientMode, @@ -36,10 +33,9 @@ export function RadialBar({ {/** Track */} { + const theme = useTheme2(); - for (let i = 0; i < segmentCountAdjusted; i++) { - const angleValue = min + ((max - min) / segmentCountAdjusted) * i; - const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01; - let segmentColor: string | undefined; - if (angleValue >= value) { - segmentColor = theme.colors.action.hover; - } else if (gradientMode === 'none') { - segmentColor = displayProcessor(angleValue).color ?? FALLBACK_COLOR; + 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; + + for (let i = 0; i < segmentCountAdjusted; i++) { + const angleValue = min + ((max - min) / segmentCountAdjusted) * i; + const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01; + let segmentColor: string | undefined; + if (angleValue >= value) { + segmentColor = theme.colors.action.hover; + } else if (gradientMode === 'none') { + segmentColor = getFieldDisplayProcessor(fieldDisplay)(angleValue).color ?? FALLBACK_COLOR; + } + + segments.push( + + ); } - segments.push( - - ); + return {segments}; } +); - return {segments}; -} - -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); -} +RadialBarSegmented.displayName = 'RadialBarSegmented'; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx index 79d8b9444e7..416594a2901 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx @@ -13,7 +13,8 @@ import { FieldColorModeId } from '@grafana/schema'; import { useTheme2 } from '../../themes/ThemeContext'; import { Stack } from '../Layout/Stack/Stack'; -import { RadialGauge, RadialGaugeProps, RadialGradientMode, RadialShape, RadialTextMode } from './RadialGauge'; +import { RadialGauge, RadialGaugeProps } from './RadialGauge'; +import { RadialGradientMode, RadialShape, RadialTextMode } from './types'; interface StoryProps extends RadialGaugeProps { value: number; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index 0771a52f9d1..7cf4ab3fa76 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -1,14 +1,7 @@ import { css, cx } from '@emotion/css'; -import { isNumber } from 'lodash'; import { useId } from 'react'; -import { - DisplayValueAlignmentFactors, - FieldDisplay, - getDisplayProcessor, - GrafanaTheme2, - TimeRange, -} from '@grafana/data'; +import { DisplayValueAlignmentFactors, FieldDisplay, GrafanaTheme2, TimeRange } from '@grafana/data'; import { t } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; @@ -21,6 +14,7 @@ import { RadialSparkline } from './RadialSparkline'; import { RadialText } from './RadialText'; import { ThresholdsBar } from './ThresholdsBar'; import { GlowGradient, MiddleCircleGlow } from './effects'; +import { RadialGradientMode, RadialShape, RadialTextMode } from './types'; import { calculateDimensions, getValueAngleForValue } from './utils'; export interface RadialGaugeProps { @@ -72,10 +66,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 */ @@ -130,7 +120,6 @@ export function RadialGauge(props: RadialGaugeProps) { showScaleLabels ); - const displayProcessor = getFieldDisplayProcessor(displayValue); const glowFilterId = `glow-${gaugeId}`; if (segmentCount > 1) { @@ -146,7 +135,6 @@ export function RadialGauge(props: RadialGaugeProps) { segmentSpacing={segmentSpacing} shape={shape} gradientMode={gradient} - displayProcessor={displayProcessor} /> ); } else { @@ -161,7 +149,6 @@ export function RadialGauge(props: RadialGaugeProps) { glowFilter={`url(#${glowFilterId})`} shape={shape} gradientMode={gradient} - displayProcessor={displayProcessor} fieldDisplay={displayValue} /> ); @@ -224,7 +211,6 @@ export function RadialGauge(props: RadialGaugeProps) { glowFilter={`url(#${glowFilterId})`} shape={shape} gradientMode={gradient} - displayProcessor={displayProcessor} /> ); } @@ -270,17 +256,6 @@ export function RadialGauge(props: RadialGaugeProps) { ); } -function getFieldDisplayProcessor(displayValue: FieldDisplay) { - if (displayValue.view && isNumber(displayValue.colIndex)) { - const dp = displayValue.view.getFieldDisplayProcessor(displayValue.colIndex); - if (dp) { - return dp; - } - } - - return getDisplayProcessor(); -} - function getStyles(theme: GrafanaTheme2) { return { vizWrapper: css({ diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx index 994b3b35eac..6588150602a 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx @@ -1,87 +1,84 @@ +import { memo } from 'react'; + import { FieldDisplay, GrafanaTheme2, Threshold } from '@grafana/data'; import { t } from '@grafana/i18n'; import { measureText } from '../../utils/measureText'; -import { GaugeDimensions, toCartesian } from './utils'; +import { RadialGaugeDimensions } from './types'; +import { getFieldConfigMinMax, toCartesian } from './utils'; interface RadialScaleLabelsProps { fieldDisplay: FieldDisplay; theme: GrafanaTheme2; thresholds: Threshold[]; - dimensions: GaugeDimensions; + dimensions: RadialGaugeDimensions; startAngle: number; endAngle: number; angleRange: number; } -export function RadialScaleLabels({ - fieldDisplay, - thresholds, - theme, - dimensions, - startAngle, - endAngle, - angleRange, -}: RadialScaleLabelsProps) { - const { centerX, centerY, scaleLabelsFontSize, scaleLabelsRadius } = dimensions; +const LINE_HEIGHT_FACTOR = 1.2; - const fieldConfig = fieldDisplay.field; - const min = fieldConfig.min ?? 0; - const max = fieldConfig.max ?? 100; +export const RadialScaleLabels = memo( + ({ fieldDisplay, thresholds, theme, dimensions, startAngle, endAngle, angleRange }: RadialScaleLabelsProps) => { + const { centerX, centerY, scaleLabelsFontSize, scaleLabelsRadius } = dimensions; + const [min, max] = getFieldConfigMinMax(fieldDisplay); - const fontSize = scaleLabelsFontSize; - const textLineHeight = scaleLabelsFontSize * 1.2; - const radius = scaleLabelsRadius - textLineHeight; + const fontSize = scaleLabelsFontSize; + const textLineHeight = scaleLabelsFontSize * LINE_HEIGHT_FACTOR; + const radius = scaleLabelsRadius - textLineHeight; - function getTextPosition(text: string, value: number, index: number) { - const isLast = index === thresholds.length - 1; - const isFirst = index === 0; + function getTextPosition(text: string, value: number, index: number) { + const isLast = index === thresholds.length - 1; + const isFirst = index === 0; - let valueDeg = ((value - min) / (max - min)) * angleRange; - let finalAngle = startAngle + valueDeg; + let valueDeg = ((value - min) / (max - min)) * angleRange; + let finalAngle = startAngle + valueDeg; - // Now adjust the final angle based on the label text width and the labels position on the arc - let measure = measureText(text, fontSize, theme.typography.fontWeightMedium); - let textWidthAngle = (measure.width / (2 * Math.PI * radius)) * angleRange; + // Now adjust the final angle based on the label text width and the labels position on the arc + let measure = measureText(text, fontSize, theme.typography.fontWeightMedium); + let textWidthAngle = (measure.width / (2 * Math.PI * radius)) * angleRange; - // the centering is different for gauge or circle shapes for some reason - finalAngle -= endAngle < 180 ? textWidthAngle : textWidthAngle / 2; + // the centering is different for gauge or circle shapes for some reason + finalAngle -= endAngle < 180 ? textWidthAngle : textWidthAngle / 2; - // For circle gauges we need to shift the first label more - if (isFirst) { - finalAngle += textWidthAngle; + // For circle gauges we need to shift the first label more + if (isFirst) { + finalAngle += textWidthAngle; + } + + // For circle gauges we need to shift the last label more + if (isLast && endAngle === 360) { + finalAngle -= textWidthAngle; + } + + const position = toCartesian(centerX, centerY, radius, finalAngle); + + return { ...position, transform: `rotate(${finalAngle}, ${position.x}, ${position.y})` }; } - // For circle gauges we need to shift the last label more - if (isLast && endAngle === 360) { - finalAngle -= textWidthAngle; - } - - const position = toCartesian(centerX, centerY, radius, finalAngle); - - return { ...position, transform: `rotate(${finalAngle}, ${position.x}, ${position.y})` }; + return ( + + {thresholds.map((threshold, index) => { + const labelPos = getTextPosition(String(threshold.value), threshold.value, index); + return ( + + {threshold.value} + + ); + })} + + ); } +); - return ( - - {thresholds.map((threshold, index) => { - const labelPos = getTextPosition(String(threshold.value), threshold.value, index); - - return ( - - {threshold.value} - - ); - })} - - ); -} +RadialScaleLabels.displayName = 'RadialScaleLabels'; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx index acb255a3f3e..4603426e8f9 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx @@ -3,17 +3,25 @@ import { GraphFieldConfig, GraphGradientMode, LineInterpolation } from '@grafana 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; + dimensions: RadialGaugeDimensions; theme: GrafanaTheme2; color?: string; shape?: RadialShape; textMode: Exclude; } + +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 RadialSparkline({ sparkline, dimensions, theme, color, shape, textMode }: RadialSparklineProps) { const { radius, barWidth } = dimensions; @@ -22,12 +30,12 @@ export function RadialSparkline({ sparkline, dimensions, theme, color, shape, te } const showNameAndValue = textMode === 'value_and_name'; - const height = radius / (showNameAndValue ? 4 : 3); - const width = radius * (shape === 'gauge' ? 1.6 : 1.4) - barWidth; + 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; const topPos = shape === 'gauge' - ? `${dimensions.gaugeBottomY - height}px` - : `calc(50% + ${radius / (showNameAndValue ? 3.3 : 4)}px)`; + ? 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 = { color: { diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx index 51a1c64c842..c1e9b0a616e 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import { memo } from 'react'; import { DisplayValue, @@ -11,13 +12,12 @@ import { import { useStyles2 } from '../../themes/ThemeContext'; import { calculateFontSize } from '../../utils/measureText'; -import { RadialShape, RadialTextMode } from './RadialGauge'; -import { GaugeDimensions } from './utils'; +import { RadialShape, RadialTextMode, RadialGaugeDimensions } from './types'; interface RadialTextProps { displayValue: DisplayValue; theme: GrafanaTheme2; - dimensions: GaugeDimensions; + dimensions: RadialGaugeDimensions; textMode: Exclude; shape: RadialShape; sparkline?: FieldSparkline; @@ -26,123 +26,131 @@ 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_UNIT_FONT_SIZE = 5; - 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 = + valueManualFontSize ?? + calculateFontSize( + valueToAlignTo, + maxTextWidth, + maxValueHeight, + LINE_HEIGHT_FACTOR, + undefined, + theme.typography.body.fontWeight + ); + + const nameFontSize = + nameManualFontSize ?? + calculateFontSize( + nameToAlignTo, + maxTextWidth, + maxNameHeight, + LINE_HEIGHT_FACTOR, + undefined, + theme.typography.body.fontWeight + ); + + 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 * (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 -= SPARKLINE_SPACING; + } + + return ( + + {showValue && ( + + {displayValue.prefix ?? ''} + {displayValue.text} + + {displayValue.suffix ?? ''} + + + )} + {showName && ( + + {displayValue.title} + + )} + ); - - const nameFontSize = - nameManualFontSize ?? - calculateFontSize( - nameToAlignTo, - maxTextWidth, - maxNameHeight, - lineHeight, - undefined, - theme.typography.body.fontWeight - ); - - const unitFontSize = Math.max(valueFontSize * 0.7, 5); - const valueHeight = valueFontSize * lineHeight; - const nameHeight = nameFontSize * lineHeight; - - 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; - - // 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; } +); - return ( - - {showValue && ( - - {displayValue.prefix ?? ''} - {displayValue.text} - - {displayValue.suffix ?? ''} - - - )} - {showName && ( - - {displayValue.title} - - )} - - ); -} +RadialText.displayName = 'RadialText'; -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = (_theme: GrafanaTheme2) => ({ text: css({ verticalAlign: 'bottom', }), diff --git a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx index 784e14aa397..5db86596dbe 100644 --- a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx @@ -1,11 +1,11 @@ -import { DisplayProcessor, FieldDisplay, Threshold } from '@grafana/data'; +import { FieldDisplay, Threshold } from '@grafana/data'; import { RadialArcPath } from './RadialArcPath'; -import { RadialGradientMode, RadialShape } from './RadialGauge'; -import { GaugeDimensions } from './utils'; +import { RadialGaugeDimensions, RadialGradientMode, RadialShape } from './types'; +import { getFieldConfigMinMax } from './utils'; -export interface Props { - dimensions: GaugeDimensions; +interface ThresholdsBarProps { + dimensions: RadialGaugeDimensions; angleRange: number; startAngle: number; endAngle: number; @@ -15,8 +15,8 @@ export interface Props { glowFilter?: string; thresholds: Threshold[]; gradientMode: RadialGradientMode; - displayProcessor: DisplayProcessor; } + export function ThresholdsBar({ dimensions, fieldDisplay, @@ -27,18 +27,15 @@ export function ThresholdsBar({ thresholds, shape, gradientMode, - displayProcessor, -}: Props) { - const fieldConfig = fieldDisplay.field; - const min = fieldConfig.min ?? 0; - const max = fieldConfig.max ?? 100; - +}: ThresholdsBarProps) { const thresholdDimensions = { ...dimensions, barWidth: dimensions.thresholdsBarWidth, radius: dimensions.thresholdsBarRadius, }; + const [min, max] = getFieldConfigMinMax(fieldDisplay); + let currentStart = startAngle; let paths: React.ReactNode[] = []; @@ -52,21 +49,21 @@ export function ThresholdsBar({ valueDeg = 0; } - let lengthDeg = valueDeg - currentStart + startAngle; + const lengthDeg = valueDeg - currentStart + startAngle; + const color = gradientMode === 'none' ? threshold.color : undefined; paths.push( ); diff --git a/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000000..db4c1c40882 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for center x and y 1`] = `"M 150 110 A 90 90 0 1 1 149.98429203681178 110.00000137077838 A 10 10 0 0 1 149.98778269529805 130.00000106616096 A 70 70 0 1 0 150 130 A 10 10 0 0 1 150 110 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for half arc 1`] = `"M 100 10 A 90 90 0 0 1 100 190 L 100 170 A 70 70 0 0 0 100 30 L 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow bar width 1`] = `"M 100 17.5 A 82.5 82.5 0 0 1 100 182.5 L 100 177.5 A 77.5 77.5 0 0 0 100 22.5 L 100 17.5 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow radius 1`] = `"M 100 40 A 60 60 0 0 1 100 160 L 100 140 A 40 40 0 0 0 100 60 L 100 40 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for quarter arc 1`] = `"M 100 10 A 90 90 0 0 1 190 100 L 170 100 A 70 70 0 0 0 100 30 L 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for rounded bars 1`] = `"M 100 10 A 90 90 0 1 1 10 100.00000000000001 A 10 10 0 0 1 30 100.00000000000001 A 70 70 0 1 0 100 30 A 10 10 0 0 1 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for three quarter arc 1`] = `"M 100 10 A 90 90 0 1 1 10 100.00000000000001 L 30 100.00000000000001 A 70 70 0 1 0 100 30 L 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for wide bar width 1`] = `"M 100 -5 A 105 105 0 0 1 100 205 L 100 155 A 55 55 0 0 0 100 45 L 100 -5 Z"`; diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts new file mode 100644 index 00000000000..f72f050e51a --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts @@ -0,0 +1,286 @@ +import { defaultsDeep } from 'lodash'; + +import { createTheme, FALLBACK_COLOR, Field, FieldDisplay, FieldType, ThresholdsMode } from '@grafana/data'; +import { FieldColorModeId } from '@grafana/schema'; + +import { + buildGradientColors, + colorAtGradientPercent, + getEndpointColors, + getGradientCss, + getGuideDotColors, +} from './colors'; + +export type DeepPartial = { + [P in keyof T]?: DeepPartial; +}; + +describe('RadialGauge color utils', () => { + describe('buildGradientColors', () => { + const createField = (colorMode: FieldColorModeId): Field => + ({ + type: FieldType.number, + name: 'Test Field', + config: { + color: { + mode: colorMode, + }, + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { value: -Infinity, color: 'green' }, + { value: 50, color: 'yellow' }, + { value: 80, color: 'red' }, + ], + }, + }, + values: [70, 40, 30, 90, 55], + }) satisfies Field; + + const buildFieldDisplay = (field: Field, part = {}): FieldDisplay => + defaultsDeep(part, { + field: field.config, + colIndex: 0, + view: { + getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: undefined }))), + }, + display: { + numeric: 75, + }, + }); + + it('should return the baseColor if gradientMode is none', () => { + expect( + buildGradientColors('none', 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('none', 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( + 'auto', + createTheme(), + buildFieldDisplay(createField(FieldColorModeId.Thresholds), { + view: { getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: '#444444' }))) }, + }) + ) + ).toEqual([ + { color: '#444444', percent: 0 }, + { color: '#FADE2A', percent: 0.5 }, + { color: '#F2495C', percent: 0.8 }, + { color: '#444444', percent: 1 }, + ]); + }); + + it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => { + expect( + buildGradientColors( + 'auto', + createTheme(), + buildFieldDisplay(createField(FieldColorModeId.Thresholds)), + '#FF0000' + ) + ).toEqual([ + { color: '#FF0000', percent: 0 }, + { color: '#FADE2A', percent: 0.5 }, + { color: '#F2495C', percent: 0.8 }, + { color: '#FF0000', percent: 1 }, + ]); + }); + + it('should return gradient colors for continuous color modes', () => { + expect( + buildGradientColors( + 'auto', + createTheme(), + buildFieldDisplay(createField(FieldColorModeId.ContinuousCividis)), + '#00FF00' + ) + ).toEqual([ + { + 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, + }, + ]); + }); + + it('should return gradient colors for by-value color modes', () => { + expect( + buildGradientColors('auto', createTheme(), buildFieldDisplay(createField(FieldColorModeId.ContinuousBlues))) + ).toEqual([ + { color: '#181b1f', percent: 0 }, + { color: '#1F60C4', percent: 1 }, + ]); + }); + + it('should return gradient colors for fixed color mode', () => { + expect( + buildGradientColors('auto', createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#442299') + ).toEqual([ + { color: '#14175a', percent: 0 }, + { color: '#a146da', percent: 1 }, + ]); + }); + }); + + 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('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('getEndpointColors', () => { + 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] = getEndpointColors(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] = getEndpointColors(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] = getEndpointColors(gradient); + expect(startColor).toBe('#ff0000'); + expect(endColor).toBe('#ff0000'); + }); + + it('should throw an error when no colors are provided', () => { + expect(() => { + getEndpointColors([]); + }).toThrow('getEndpointColors 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('getGuideDotColors', () => { + 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] = getGuideDotColors(gradient, 0.35); + expect(startDotColor).toBe('#fbfbfb'); + expect(endDotColor).toBe('#111217'); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts index e1d323b88a5..c390e57d09d 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -1,25 +1,16 @@ import tinycolor from 'tinycolor2'; -import { - colorManipulator, - DisplayProcessor, - FALLBACK_COLOR, - FieldDisplay, - getFieldColorMode, - GradientStop, - GrafanaTheme2, -} from '@grafana/data'; +import { colorManipulator, FALLBACK_COLOR, FieldDisplay, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; import { FieldColorModeId } from '@grafana/schema'; -import { RadialGradientMode, RadialShape } from './RadialGauge'; +import { GradientStop, RadialGradientMode, RadialShape } from './types'; +import { getFieldConfigMinMax, getFieldDisplayProcessor } from './utils'; export function buildGradientColors( gradientMode: RadialGradientMode, theme: GrafanaTheme2, - displayProcessor: DisplayProcessor, fieldDisplay: FieldDisplay, - baseColor = FALLBACK_COLOR, - forSegment?: boolean + baseColor = FALLBACK_COLOR ): GradientStop[] { if (gradientMode === 'none') { return [ @@ -30,13 +21,14 @@ export function buildGradientColors( 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 min = fieldDisplay.field.min ?? 0; - const max = fieldDisplay.field.max ?? 100; const result: Array<{ color: string; percent: number }> = [ - { color: displayProcessor(min).color ?? FALLBACK_COLOR, percent: 0 }, + { color: displayProcessor(min).color ?? baseColor, percent: 0 }, ]; for (const threshold of thresholds) { @@ -51,15 +43,15 @@ export function buildGradientColors( return result; } - if (colorMode.isContinuous && colorMode.getColors && !forSegment) { - // Handle continuous color modes first + // 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) { - // 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 const darkerColor = tinycolor(baseColor).darken(5); const lighterColor = tinycolor(baseColor).spin(20).lighten(10); @@ -73,8 +65,7 @@ export function buildGradientColors( ]; } - // 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 + // for fixed colors and other modes, we create a simple two-color gradient // we set the highest contrast color second based on the theme. const darkerColor = tinycolor(baseColor).spin(-20).darken(5); const lighterColor = tinycolor(baseColor).saturate(20).spin(20).brighten(10); @@ -89,13 +80,74 @@ export function buildGradientColors( ]; } -export function getEndpointColors(gradientStops: GradientStop[], percent = 0): [string, string] { +function clamp(value: number, min = 0, max = 1) { + if (process.env.NODE_ENV !== 'production') { + if (value < min || value > max) { + console.error(`The value provided ${value} is out of range [${min}, ${max}].`); + } + } + + return Math.min(Math.max(min, value), max); +} + +/** + * @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 + const sorted = stops + .map((s) => ({ color: s.color, percent: clamp(s.percent, 0, 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 + let left = sorted[0]; + let right = sorted[sorted.length - 1]; + for (let i = 1; i < sorted.length; i++) { + if (percent <= sorted[i].percent) { + left = sorted[i - 1]; + right = sorted[i]; + break; + } + } + + const range = right.percent - left.percent; + const t = range === 0 ? 0 : (percent - left.percent) / range; // 0..1 + + // tinycolor.mix expects amount as percentage of the second color + const mixed = tinycolor.mix(left.color, right.color, t * 100); + + // return hex6 if opaque, hex8 if has alpha + return mixed; +} + +export function getEndpointColors(gradientStops: GradientStop[], percent = 1): [string, string] { + if (gradientStops.length === 0) { + throw new Error('getEndpointColors 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 = colorManipulator.colorAtGradientPercent(gradientStops, percent); + const endColorByPercentage = colorAtGradientPercent(gradientStops, percent); endColor = endColorByPercentage.getAlpha() === 1 ? endColorByPercentage.toHexString() : endColorByPercentage.toHex8String(); } @@ -109,14 +161,18 @@ export function getGradientCss(gradientStops: GradientStop[], shape: RadialShape : `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 = '#111217'; // gray05 - const lightColor = '#fbfbfb'; // gray90 + const darkColor = GRAY_05; + const lightColor = GRAY_90; return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor; }; -export function getGuideDotColors(gradientStops: GradientStop[], percent = 0): [string, string] { +export function getGuideDotColors(gradientStops: GradientStop[], percent: number): [string, string] { const [startColor, endColor] = getEndpointColors(gradientStops, percent); return [getGuideDotColor(startColor), getGuideDotColor(endColor)]; } diff --git a/packages/grafana-ui/src/components/RadialGauge/effects.tsx b/packages/grafana-ui/src/components/RadialGauge/effects.tsx index 0f1163e21de..c8f7afb542f 100644 --- a/packages/grafana-ui/src/components/RadialGauge/effects.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/effects.tsx @@ -1,4 +1,4 @@ -import { GaugeDimensions } from './utils'; +import { RadialGaugeDimensions } from './types'; export interface GlowGradientProps { id: string; @@ -27,7 +27,7 @@ const CENTER_GLOW_OPACITY = 0.15; export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) { return ( - + @@ -35,7 +35,7 @@ export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: } export interface CenterGlowProps { - dimensions: GaugeDimensions; + dimensions: RadialGaugeDimensions; gaugeId: string; color?: string; } @@ -46,7 +46,7 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps return ( <> - + diff --git a/packages/grafana-ui/src/components/RadialGauge/types.ts b/packages/grafana-ui/src/components/RadialGauge/types.ts new file mode 100644 index 00000000000..67084a3fb63 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/types.ts @@ -0,0 +1,26 @@ +export type RadialGradientMode = 'none' | 'auto'; +export type RadialTextMode = 'auto' | 'value_and_name' | 'value' | 'name' | 'none'; +export type RadialShape = 'circle' | 'gauge'; + +export interface RadialGaugeDimensions { + margin: number; + radius: number; + centerX: number; + centerY: number; + barWidth: number; + endAngle?: number; + barIndex: number; + thresholdsBarRadius: number; + thresholdsBarWidth: number; + thresholdsBarSpacing: number; + scaleLabelsFontSize: number; + scaleLabelsSpacing: number; + scaleLabelsRadius: number; + gaugeBottomY: number; +} + +/** @alpha - perhaps this should go in @grafana/data */ +export interface GradientStop { + color: string; + percent: number; +} diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.test.ts b/packages/grafana-ui/src/components/RadialGauge/utils.test.ts index 5e3f34d62cd..c153faae173 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.test.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.test.ts @@ -1,24 +1,111 @@ -import { FieldDisplay } from '@grafana/data'; +import { DataFrameView, FieldDisplay } from '@grafana/data'; import type { RadialGaugeProps } from './RadialGauge'; -import { calculateDimensions, toRad, getValueAngleForValue } from './utils'; +import { RadialGaugeDimensions } from './types'; +import { + calculateDimensions, + toRad, + getValueAngleForValue, + drawRadialArcPath, + getFieldConfigMinMax, + getFieldDisplayProcessor, + getAngleBetweenSegments, + getOptimalSegmentCount, +} from './utils'; describe('RadialGauge utils', () => { - function calc(overrides: Partial = {}) { - return calculateDimensions( - overrides.width ?? 200, - overrides.height ?? 200, - overrides.shape === 'gauge' ? 110 : 360, - overrides.glowBar ?? false, - overrides.roundedBars ?? false, - overrides.barWidthFactor ?? 0.4, - overrides.barIndex ?? 0, - overrides.thresholdsBar ?? false, - overrides.showScaleLabels ?? false - ); - } + describe('getFieldDisplayProcessor', () => { + it('should return display processor from view when available', () => { + const mockProcessor = jest.fn(); + const mockView = { + getFieldDisplayProcessor: jest.fn().mockReturnValue(mockProcessor), + } as unknown as DataFrameView; + + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: {}, + view: mockView, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const dp = getFieldDisplayProcessor(fieldDisplay); + expect(dp).toBe(mockProcessor); + expect(mockView.getFieldDisplayProcessor).toHaveBeenCalledWith(0); + }); + + it('should return default display processor when view is not available', () => { + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: {}, + view: undefined, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const dp = getFieldDisplayProcessor(fieldDisplay); + expect(dp).toBeDefined(); + expect(typeof dp).toBe('function'); + }); + }); + + describe('getFieldConfigMinMax', () => { + it('should return min and max from field config when defined', () => { + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: { min: 10, max: 90 }, + view: undefined, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const [min, max] = getFieldConfigMinMax(fieldDisplay); + expect(min).toBe(10); + expect(max).toBe(90); + }); + + it('should return default min and max when not defined in field config', () => { + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: {}, + view: undefined, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const [min, max] = getFieldConfigMinMax(fieldDisplay); + expect(min).toBe(0); + expect(max).toBe(100); + }); + }); describe('calculateDimensions', () => { + function calc(overrides: Partial = {}) { + return calculateDimensions( + overrides.width ?? 200, + overrides.height ?? 200, + overrides.shape === 'gauge' ? 110 : 360, + overrides.glowBar ?? false, + overrides.roundedBars ?? false, + overrides.barWidthFactor ?? 0.4, + overrides.barIndex ?? 0, + overrides.thresholdsBar ?? false, + overrides.showScaleLabels ?? false + ); + } + it('should calculate basic dimensions for a square gauge', () => { const result = calc(); @@ -194,4 +281,86 @@ 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 throw an error if inner radius collapses to zero or below', () => { + const smallRadiusDims = { ...defaultDims, radius: 5, barWidth: 20 }; + expect(() => drawRadialArcPath(0, 180, smallRadiusDims)).toThrow( + 'Inner radius collapsed to zero or below, cannot draw radial arc path' + ); + }); + }); + }); + + describe('getAngleBetweenSegments', () => { + it('should calculate angle between segments based on spacing and count', () => { + expect(getAngleBetweenSegments(2, 10, 360)).toBe(48); + expect(getAngleBetweenSegments(5, 15, 180)).toBe(40); + }); + }); + + describe('getOptimalSegmentCount', () => { + it('should adjust segment count based on dimensions and spacing', () => { + const dimensions: RadialGaugeDimensions = { + centerX: 100, + centerY: 100, + radius: 80, + barWidth: 20, + margin: 0, + barIndex: 0, + thresholdsBarWidth: 0, + thresholdsBarSpacing: 0, + thresholdsBarRadius: 0, + scaleLabelsFontSize: 0, + scaleLabelsSpacing: 0, + scaleLabelsRadius: 0, + gaugeBottomY: 0, + }; + + expect(getOptimalSegmentCount(dimensions, 2, 10, 360)).toBe(8); + expect(getOptimalSegmentCount(dimensions, 1, 5, 360)).toBe(5); + }); + }); }); diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.ts b/packages/grafana-ui/src/components/RadialGauge/utils.ts index a0746ff54de..e682fbbd37d 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.ts @@ -1,11 +1,34 @@ -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 getValueAngleForValue( + fieldDisplay: FieldDisplay, + startAngle: number, + endAngle: number, + value = fieldDisplay.display.numeric +) { + const [min, max] = getFieldConfigMinMax(fieldDisplay); + const angleRange = (360 % (startAngle === 0 ? 1 : startAngle)) + endAngle; + + let angle = ((value - min) / (max - min)) * angleRange; if (angle > angleRange) { angle = angleRange; @@ -26,24 +49,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 +72,7 @@ export function calculateDimensions( barIndex: number, thresholdBar?: boolean, showScaleLabels?: boolean -): GaugeDimensions { +): RadialGaugeDimensions { const yMaxAngle = endAngle > 180 ? 180 : endAngle; let margin = 0; @@ -157,26 +175,32 @@ export function toCartesian(centerX: number, centerY: number, radius: number, an } export function drawRadialArcPath( - angle: number, - arcLengthDeg: number, - dimensions: GaugeDimensions, + startAngle: number, + endAngle: number, + dimensions: RadialGaugeDimensions, roundedBars?: boolean ): string { const { radius, centerX, centerY, barWidth } = dimensions; - if (arcLengthDeg === 360) { - // For some reason a 100% full arc cannot be rendered - arcLengthDeg = 359.99; + // For some reason a 100% full arc cannot be rendered + if (endAngle >= 360) { + endAngle = 359.99; } - const startRadians = toRad(angle); - const endRadians = toRad(angle + arcLengthDeg); + const startRadians = toRad(startAngle); + const endRadians = toRad(startAngle + endAngle); - const largeArc = arcLengthDeg > 180 ? 1 : 0; + const largeArc = endAngle > 180 ? 1 : 0; const outerR = radius + barWidth / 2; const innerR = Math.max(0, radius - barWidth / 2); + if (innerR <= 0) { + throw new Error('Inner radius collapsed to zero or below, cannot draw radial arc path'); + } + // 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); @@ -187,6 +211,7 @@ export function drawRadialArcPath( 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 = [ @@ -209,27 +234,44 @@ export function drawRadialArcPath( // 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 + // straight line to inner end (square butt) pathParts.push('L', ix2, iy2); } - if (innerR <= 0) { - // if inner radius collapsed to center, line to center and close - pathParts.push('L', centerX, centerY, 'Z'); + // 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 { - // 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 - pathParts.push('L', ox1, oy1); - } - - pathParts.push('Z'); + // 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); +} From 0b89c821ad354453eda7e539a0c345a98de33a5e Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 16 Dec 2025 12:31:17 -0500 Subject: [PATCH 27/48] fix dev dashboard fixture --- .../panel-gauge/gauge_tests_new.v42.json | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json index e8577b15a6e..67bab1878db 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json @@ -299,7 +299,7 @@ "gridPos": { "h": 6, "w": 4, - "x": 16, + "x": 12, "y": 1 }, "id": 8, @@ -377,8 +377,8 @@ "gridPos": { "h": 6, "w": 4, - "x": 0, - "y": 7 + "x": 16, + "y": 1 }, "id": 22, "maxDataPoints": 20, @@ -455,8 +455,8 @@ "gridPos": { "h": 6, "w": 4, - "x": 4, - "y": 7 + "x": 20, + "y": 1 }, "id": 23, "maxDataPoints": 20, @@ -508,7 +508,7 @@ "h": 1, "w": 24, "x": 0, - "y": 13 + "y": 7 }, "id": 17, "panels": [], @@ -545,9 +545,9 @@ }, "gridPos": { "h": 6, - "w": 5, + "w": 4, "x": 0, - "y": 14 + "y": 8 }, "id": 18, "maxDataPoints": 20, @@ -623,9 +623,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 5, - "y": 14 + "w": 4, + "x": 4, + "y": 8 }, "id": 19, "maxDataPoints": 20, @@ -701,9 +701,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 10, - "y": 14 + "w": 4, + "x": 8, + "y": 8 }, "id": 20, "maxDataPoints": 20, @@ -779,9 +779,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 15, - "y": 14 + "w": 4, + "x": 12, + "y": 8 }, "id": 21, "maxDataPoints": 20, @@ -833,7 +833,7 @@ "h": 1, "w": 24, "x": 0, - "y": 20 + "y": 14 }, "id": 24, "panels": [], @@ -874,9 +874,9 @@ }, "gridPos": { "h": 6, - "w": 6, + "w": 4, "x": 0, - "y": 21 + "y": 15 }, "id": 25, "maxDataPoints": 20, @@ -952,9 +952,9 @@ }, "gridPos": { "h": 6, - "w": 6, - "x": 6, - "y": 21 + "w": 4, + "x": 4, + "y": 15 }, "id": 26, "maxDataPoints": 20, @@ -1030,9 +1030,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 12, - "y": 21 + "w": 4, + "x": 8, + "y": 15 }, "id": 29, "maxDataPoints": 20, @@ -1107,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, @@ -1185,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, @@ -1236,7 +1236,7 @@ "h": 1, "w": 24, "x": 0, - "y": 34 + "y": 21 }, "id": 31, "panels": [], @@ -1283,7 +1283,7 @@ "h": 10, "w": 7, "x": 0, - "y": 35 + "y": 22 }, "id": 32, "maxDataPoints": 20, @@ -1365,7 +1365,7 @@ "h": 10, "w": 7, "x": 7, - "y": 35 + "y": 22 }, "id": 34, "maxDataPoints": 20, @@ -1447,7 +1447,7 @@ "h": 10, "w": 6, "x": 14, - "y": 35 + "y": 22 }, "id": 33, "maxDataPoints": 20, @@ -1495,7 +1495,7 @@ "h": 1, "w": 24, "x": 0, - "y": 45 + "y": 32 }, "id": 6, "panels": [], @@ -1536,7 +1536,7 @@ "h": 6, "w": 24, "x": 0, - "y": 46 + "y": 33 }, "id": 9, "maxDataPoints": 20, @@ -1618,7 +1618,7 @@ "h": 6, "w": 24, "x": 0, - "y": 52 + "y": 39 }, "id": 11, "maxDataPoints": 20, @@ -1672,7 +1672,7 @@ "h": 1, "w": 24, "x": 0, - "y": 58 + "y": 45 }, "id": 12, "panels": [], @@ -1714,7 +1714,7 @@ "h": 7, "w": 4, "x": 0, - "y": 59 + "y": 46 }, "id": 13, "maxDataPoints": 20, @@ -1797,7 +1797,7 @@ "h": 7, "w": 5, "x": 4, - "y": 59 + "y": 46 }, "id": 14, "maxDataPoints": 20, @@ -1879,7 +1879,7 @@ "h": 7, "w": 5, "x": 9, - "y": 59 + "y": 46 }, "id": 15, "maxDataPoints": 20, @@ -1961,7 +1961,7 @@ "h": 7, "w": 6, "x": 14, - "y": 59 + "y": 46 }, "id": 16, "maxDataPoints": 20, @@ -2015,7 +2015,7 @@ "h": 1, "w": 24, "x": 0, - "y": 66 + "y": 53 }, "id": 35, "panels": [], @@ -2046,10 +2046,10 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 6, + "h": 5, + "w": 12, "x": 0, - "y": 67 + "y": 54 }, "id": 36, "options": { @@ -2113,10 +2113,10 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 6, - "x": 6, - "y": 67 + "h": 5, + "w": 12, + "x": 12, + "y": 54 }, "id": 37, "options": { From 6042dfef89c9cc38b7e0cad31aa727d109ed14b4 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 16 Dec 2025 13:36:15 -0500 Subject: [PATCH 28/48] more cleanup, optimization --- .../components/RadialGauge/RadialArcPath.tsx | 63 +++++++---------- .../RadialGauge/RadialBarSegmented.tsx | 3 +- .../RadialGauge/RadialSparkline.tsx | 69 +++++++++++-------- .../src/components/RadialGauge/colors.ts | 2 +- 4 files changed, 65 insertions(+), 72 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index b08085af48e..d89ec94cc58 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,4 +1,4 @@ -import { useId, useMemo, memo } from 'react'; +import { useId, memo, HTMLAttributes } from 'react'; import { FieldDisplay } from '@grafana/data'; @@ -48,53 +48,36 @@ export const RadialArcPath = memo( const theme = useTheme2(); const id = useId(); - const gradientStops = useMemo(() => { - if (gradientMode === 'none') { - return []; - } - return buildGradientColors(gradientMode, theme, fieldDisplay, fieldDisplay.display.color); - }, [gradientMode, fieldDisplay, theme]); + const gradientStops = buildGradientColors(gradientMode, theme, fieldDisplay, fieldDisplay.display.color); - const { guideDotColors, endpointColors } = useMemo(() => { - if (!showGuideDots || gradientStops.length === 0) { - return { - guideDotStartColor: undefined, - guideDotEndColor: undefined, - }; - } - return { - guideDotColors: getGuideDotColors(gradientStops, fieldDisplay.display.percent ?? 0), - endpointColors: - shape === 'circle' ? getEndpointColors(gradientStops, fieldDisplay.display.percent ?? 1) : undefined, - }; - }, [showGuideDots, fieldDisplay, gradientStops, shape]); + let guideDotColors: [string, string] | undefined; + let endpointColors: [string, string] | undefined; - const bgDivStyle = useMemo(() => { - const baseStyles = { width: '100%', height: '100%' }; - if (color) { - return { backgroundColor: color, ...baseStyles }; + if (showGuideDots && gradientStops.length > 0) { + guideDotColors = getGuideDotColors(gradientStops, fieldDisplay.display.percent); + if (shape === 'circle') { + endpointColors = getEndpointColors(gradientStops, fieldDisplay.display.percent); } - const gradientCss = getGradientCss(gradientStops, shape); - return { backgroundImage: gradientCss, ...baseStyles }; - }, [color, gradientStops, shape]); + } + + const bgDivStyle: HTMLAttributes['style'] = { width: '100%', height: '100%' }; + if (color) { + bgDivStyle.backgroundColor = color; + } else { + bgDivStyle.backgroundImage = getGradientCss(gradientStops, shape); + } const { radius, centerX, centerY, barWidth } = dimensions; - const path = useMemo( - () => drawRadialArcPath(angle, arcLengthDeg, dimensions, roundedBars), - [angle, arcLengthDeg, dimensions, roundedBars] - ); + const path = drawRadialArcPath(angle, arcLengthDeg, dimensions, roundedBars); - const { x1, x2, y1, y2 } = useMemo(() => { - const startRadians = toRad(angle); - const endRadians = toRad(angle + arcLengthDeg); + 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); - return { x1, y1, x2, y2 }; - }, [angle, arcLengthDeg, centerX, centerY, radius]); + const x1 = centerX + radius * Math.cos(startRadians); + const y1 = centerY + radius * Math.sin(startRadians); + const x2 = centerX + radius * Math.cos(endRadians); + const y2 = centerY + radius * Math.sin(endRadians); const dotRadius = Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS); diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx index 522036377c2..d86baa5d1b5 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx @@ -45,6 +45,7 @@ export const RadialBarSegmented = memo( 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; @@ -53,7 +54,7 @@ export const RadialBarSegmented = memo( if (angleValue >= value) { segmentColor = theme.colors.action.hover; } else if (gradientMode === 'none') { - segmentColor = getFieldDisplayProcessor(fieldDisplay)(angleValue).color ?? FALLBACK_COLOR; + segmentColor = displayProcessor(angleValue).color ?? FALLBACK_COLOR; } segments.push( diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx index 4603426e8f9..67969fd3208 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx @@ -1,3 +1,5 @@ +import { memo, useMemo } from 'react'; + import { FieldDisplay, GrafanaTheme2, FieldConfig } from '@grafana/data'; import { GraphFieldConfig, GraphGradientMode, LineInterpolation } from '@grafana/schema'; @@ -22,36 +24,43 @@ const SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE = 4; const SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE_NAME_AND_VALUE = 3.3; const SPARKLINE_SPACING = 8; -export function RadialSparkline({ sparkline, dimensions, theme, color, shape, textMode }: RadialSparklineProps) { - const { radius, barWidth } = dimensions; +export const RadialSparkline = memo( + ({ sparkline, dimensions, theme, color, shape, textMode }: RadialSparklineProps) => { + const { radius, barWidth } = dimensions; - if (!sparkline) { - return null; + const showNameAndValue = textMode === 'value_and_name'; + 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; + const topPos = + shape === 'gauge' + ? dimensions.gaugeBottomY - height - SPARKLINE_SPACING + : `calc(50% + ${radius / (showNameAndValue ? SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE_NAME_AND_VALUE : SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE)}px)`; + + const config: FieldConfig = useMemo( + () => ({ + color: { + mode: 'fixed', + fixedColor: color ?? 'blue', + }, + custom: { + gradientMode: GraphGradientMode.Opacity, + fillOpacity: 40, + lineInterpolation: LineInterpolation.Smooth, + }, + }), + [color] + ); + + if (!sparkline) { + return null; + } + + return ( +
+ +
+ ); } +); - const showNameAndValue = textMode === 'value_and_name'; - const height = radius / (showNameAndValue ? SPARKLINE_HEIGHT_DIVISOR_NAME_AND_VALUE : SPARKLINE_HEIGHT_DIVISOR); - const width = radius * (shape === 'gauge' ? SPARKLINE_WIDTH_FACTOR_ARC : SPARKLINE_WIDTH_FACTOR_CIRCLE) - barWidth; - 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 = { - color: { - mode: 'fixed', - fixedColor: color ?? 'blue', - }, - custom: { - gradientMode: GraphGradientMode.Opacity, - fillOpacity: 40, - lineInterpolation: LineInterpolation.Smooth, - }, - }; - - return ( -
- -
- ); -} +RadialSparkline.displayName = 'RadialSparkline'; diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts index c390e57d09d..b15117f7b9f 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -172,7 +172,7 @@ const getGuideDotColor = (color: string): string => { return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor; }; -export function getGuideDotColors(gradientStops: GradientStop[], percent: number): [string, string] { +export function getGuideDotColors(gradientStops: GradientStop[], percent = 1): [string, string] { const [startColor, endColor] = getEndpointColors(gradientStops, percent); return [getGuideDotColor(startColor), getGuideDotColor(endColor)]; } From a8ff242a3fec3057821e80c25224e5c1d2aeb483 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 16 Dec 2025 14:06:43 -0500 Subject: [PATCH 29/48] fix a couple of bugs --- .../RadialGauge/RadialSparkline.tsx | 22 +++++++--- .../src/components/RadialGauge/RadialText.tsx | 44 +++++++++++-------- .../src/components/RadialGauge/utils.test.ts | 6 +-- .../src/components/RadialGauge/utils.ts | 2 +- 4 files changed, 44 insertions(+), 30 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx index 67969fd3208..2d6c45a14bf 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx @@ -8,12 +8,12 @@ import { Sparkline } from '../Sparkline/Sparkline'; import { RadialShape, RadialTextMode, RadialGaugeDimensions } from './types'; interface RadialSparklineProps { - sparkline: FieldDisplay['sparkline']; - dimensions: RadialGaugeDimensions; - theme: GrafanaTheme2; color?: string; - shape?: RadialShape; + dimensions: RadialGaugeDimensions; + shape: RadialShape; + sparkline: FieldDisplay['sparkline']; textMode: Exclude; + theme: GrafanaTheme2; } const SPARKLINE_HEIGHT_DIVISOR = 4; @@ -24,13 +24,23 @@ 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 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; + const { width, height } = getSparklineDimensions(radius, barWidth, showNameAndValue, shape); const topPos = shape === 'gauge' ? dimensions.gaugeBottomY - height - SPARKLINE_SPACING diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx index c1e9b0a616e..69ab16e450e 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx @@ -34,7 +34,9 @@ const MAX_TEXT_WIDTH_DIVISOR = 7; const MAX_NAME_HEIGHT_DIVISOR = 4; const VALUE_SPACE_PERCENTAGE = 0.7; const SPARKLINE_SPACING = 8; -const MIN_UNIT_FONT_SIZE = 5; +const MIN_VALUE_FONT_SIZE = 1; +const MIN_NAME_FONT_SIZE = 10; +const MIN_UNIT_FONT_SIZE = 6; export const RadialText = memo( ({ @@ -71,27 +73,31 @@ export const RadialText = memo( maxNameHeight = NAME_TO_HEIGHT_FACTOR * Math.pow(radius, LARGE_RADIUS_SCALING_DECAY); } - const valueFontSize = + const valueFontSize = Math.max( valueManualFontSize ?? - calculateFontSize( - valueToAlignTo, - maxTextWidth, - maxValueHeight, - LINE_HEIGHT_FACTOR, - undefined, - theme.typography.body.fontWeight - ); + calculateFontSize( + valueToAlignTo, + maxTextWidth, + maxValueHeight, + LINE_HEIGHT_FACTOR, + undefined, + theme.typography.body.fontWeight + ), + MIN_VALUE_FONT_SIZE + ); - const nameFontSize = + const nameFontSize = Math.max( nameManualFontSize ?? - calculateFontSize( - nameToAlignTo, - maxTextWidth, - maxNameHeight, - LINE_HEIGHT_FACTOR, - undefined, - theme.typography.body.fontWeight - ); + calculateFontSize( + nameToAlignTo, + maxTextWidth, + maxNameHeight, + LINE_HEIGHT_FACTOR, + undefined, + theme.typography.body.fontWeight + ), + MIN_NAME_FONT_SIZE + ); const unitFontSize = Math.max(valueFontSize * VALUE_SPACE_PERCENTAGE, MIN_UNIT_FONT_SIZE); const valueHeight = valueFontSize * LINE_HEIGHT_FACTOR; diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.test.ts b/packages/grafana-ui/src/components/RadialGauge/utils.test.ts index c153faae173..b9b2e4ad8f3 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.test.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.test.ts @@ -325,11 +325,9 @@ describe('RadialGauge utils', () => { expect(drawRadialArcPath(0, 380, defaultDims)).toEqual(drawRadialArcPath(0, 380, defaultDims)); }); - it('should throw an error if inner radius collapses to zero or below', () => { + 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)).toThrow( - 'Inner radius collapsed to zero or below, cannot draw radial arc path' - ); + expect(drawRadialArcPath(0, 180, smallRadiusDims)).toBe(''); }); }); }); diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.ts b/packages/grafana-ui/src/components/RadialGauge/utils.ts index e682fbbd37d..72030a09ad6 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.ts @@ -195,7 +195,7 @@ export function drawRadialArcPath( const outerR = radius + barWidth / 2; const innerR = Math.max(0, radius - barWidth / 2); if (innerR <= 0) { - throw new Error('Inner radius collapsed to zero or below, cannot draw radial arc path'); + return ''; // cannot draw arc with 0 inner radius } // get points for both an inner and outer arc. we draw From dfc5a07259ad0a1e2508b5f2563085d224860ac4 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 16 Dec 2025 14:14:31 -0500 Subject: [PATCH 30/48] fix bad import --- packages/grafana-data/src/themes/colorManipulator.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/grafana-data/src/themes/colorManipulator.test.ts b/packages/grafana-data/src/themes/colorManipulator.test.ts index 4407fc3ad8a..5619ddc4928 100644 --- a/packages/grafana-data/src/themes/colorManipulator.test.ts +++ b/packages/grafana-data/src/themes/colorManipulator.test.ts @@ -12,7 +12,6 @@ import { lighten, asRgbString, onBackground, - colorAtGradientPercent, } from './colorManipulator'; describe('utils/colorManipulator', () => { From 206bc6c4d921dce72060b17ed2033b64dc028936 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 16 Dec 2025 14:49:32 -0500 Subject: [PATCH 31/48] disable storybook test due to false positive --- .../RadialGauge/RadialGauge.story.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx index 416594a2901..acd25b91421 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx @@ -299,6 +299,24 @@ export const Examples: StoryFn = (args) => { Examples.parameters = { controls: { include: ['barWidthFactor', 'value'] }, + 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, + }, + ], + }, + }, }; export const MultiSeries: StoryFn = (args) => { From 9635f8ba4ee316284351259bfd8b6d0f1f5c1553 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 16 Dec 2025 15:28:40 -0500 Subject: [PATCH 32/48] a more sweeping disable of the color-contrast --- .../RadialGauge/RadialGauge.story.tsx | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx index acd25b91421..0bd1e4abf09 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx @@ -32,6 +32,24 @@ const meta: Meta = { controls: { exclude: ['theme', 'values', 'vizCount'], }, + a11y: { + config: { + rules: [ + { + id: 'scrollable-region-focusable', + selector: 'body', + enabled: false, + }, + // NOTE: this is necessary due to a false positive with the filered svg glow in one of the examples. + // The color-contrast in this component should be accessible! + { + id: 'color-contrast', + selector: 'text', + enabled: false, + }, + ], + }, + }, }, args: { barWidthFactor: 0.2, @@ -299,24 +317,6 @@ export const Examples: StoryFn = (args) => { Examples.parameters = { controls: { include: ['barWidthFactor', 'value'] }, - 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, - }, - ], - }, - }, }; export const MultiSeries: StoryFn = (args) => { From 81bff1a39e4635f706b10b1ec81e4ccfc44f41c1 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 16 Dec 2025 15:33:15 -0500 Subject: [PATCH 33/48] update backend tests --- .../v0alpha1.gauge_tests_new.v42.v2alpha1.json | 13 ------------- .../v0alpha1.gauge_tests_new.v42.v2beta1.json | 13 ------------- 2 files changed, 26 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json index 68a5ba40fe9..6ff78e8f271 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json @@ -2306,19 +2306,6 @@ } } }, - { - "kind": "GridLayoutItem", - "spec": { - "x": 12, - "y": 0, - "width": 4, - "height": 6, - "element": { - "kind": "ElementReference", - "name": "panel-5" - } - } - }, { "kind": "GridLayoutItem", "spec": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json index 1f16b1924f7..7b9a1b12422 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json @@ -2379,19 +2379,6 @@ } } }, - { - "kind": "GridLayoutItem", - "spec": { - "x": 12, - "y": 0, - "width": 4, - "height": 6, - "element": { - "kind": "ElementReference", - "name": "panel-5" - } - } - }, { "kind": "GridLayoutItem", "spec": { From 32a63c0cf0aa7a99193247fe65c6fd6cb93e1366 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 08:37:23 -0500 Subject: [PATCH 34/48] update gradient for fixed color --- .../src/components/RadialGauge/colors.test.ts | 63 ++----------------- .../src/components/RadialGauge/colors.ts | 23 +++---- 2 files changed, 17 insertions(+), 69 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts index f72f050e51a..5c18107ed5c 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts @@ -76,12 +76,7 @@ describe('RadialGauge color utils', () => { view: { getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: '#444444' }))) }, }) ) - ).toEqual([ - { color: '#444444', percent: 0 }, - { color: '#FADE2A', percent: 0.5 }, - { color: '#F2495C', percent: 0.8 }, - { color: '#444444', percent: 1 }, - ]); + ).toMatchSnapshot(); }); it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => { @@ -92,12 +87,7 @@ describe('RadialGauge color utils', () => { buildFieldDisplay(createField(FieldColorModeId.Thresholds)), '#FF0000' ) - ).toEqual([ - { color: '#FF0000', percent: 0 }, - { color: '#FADE2A', percent: 0.5 }, - { color: '#F2495C', percent: 0.8 }, - { color: '#FF0000', percent: 1 }, - ]); + ).toMatchSnapshot(); }); it('should return gradient colors for continuous color modes', () => { @@ -108,62 +98,19 @@ describe('RadialGauge color utils', () => { buildFieldDisplay(createField(FieldColorModeId.ContinuousCividis)), '#00FF00' ) - ).toEqual([ - { - 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, - }, - ]); + ).toMatchSnapshot(); }); it('should return gradient colors for by-value color modes', () => { expect( buildGradientColors('auto', createTheme(), buildFieldDisplay(createField(FieldColorModeId.ContinuousBlues))) - ).toEqual([ - { color: '#181b1f', percent: 0 }, - { color: '#1F60C4', percent: 1 }, - ]); + ).toMatchSnapshot(); }); it('should return gradient colors for fixed color mode', () => { expect( buildGradientColors('auto', createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#442299') - ).toEqual([ - { color: '#14175a', percent: 0 }, - { color: '#a146da', percent: 1 }, - ]); + ).toMatchSnapshot(); }); }); diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts index b15117f7b9f..58053d6cbe7 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -67,17 +67,18 @@ export function buildGradientColors( // for fixed colors and other modes, we create a simple two-color gradient // we set the highest contrast color second based on the theme. - const darkerColor = tinycolor(baseColor).spin(-20).darken(5); - const lighterColor = tinycolor(baseColor).saturate(20).spin(20).brighten(10); - return theme.isDark - ? [ - { color: darkerColor.darken(10).toString(), percent: 0 }, - { color: lighterColor.lighten(10).toString(), percent: 1 }, - ] - : [ - { color: lighterColor.lighten(10).toString(), percent: 0 }, - { color: darkerColor.toString(), percent: 1 }, - ]; + const darkerColor = tinycolor(baseColor).spin(5).darken(35).saturate(20); + const lighterColor = tinycolor(baseColor) + .spin(-5) + .brighten(theme.isDark ? 30 : 15) + .lighten(theme.isDark ? 35 : 15) + .desaturate(10); + return [ + { color: theme.isDark ? darkerColor.toString() : lighterColor.toString(), percent: 0 }, + { color: baseColor, percent: 0.45 }, + { color: baseColor, percent: 0.55 }, + { color: theme.isDark ? lighterColor.toString() : darkerColor.toString(), percent: 1 }, + ]; } function clamp(value: number, min = 0, max = 1) { From b815680a6b01013985bf03a3b09888af055327c9 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 08:41:56 -0500 Subject: [PATCH 35/48] test all dark/light theme variants --- .../__snapshots__/colors.test.ts.snap | 152 ++++++++++++++++++ .../src/components/RadialGauge/colors.test.ts | 17 +- 2 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap diff --git a/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap new file mode 100644 index 00000000000..3fabe922a8d --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap @@ -0,0 +1,152 @@ +// 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": "#030108", + "percent": 0, + }, + { + "color": "#442299", + "percent": 0.45, + }, + { + "color": "#442299", + "percent": 0.55, + }, + { + "color": "#ffffff", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for fixed color mode in light theme 1`] = ` +[ + { + "color": "#9689ca", + "percent": 0, + }, + { + "color": "#442299", + "percent": 0.45, + }, + { + "color": "#442299", + "percent": 0.55, + }, + { + "color": "#030108", + "percent": 1, + }, +] +`; diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts index 5c18107ed5c..bd70e13e3c0 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts @@ -101,15 +101,24 @@ describe('RadialGauge color utils', () => { ).toMatchSnapshot(); }); - it('should return gradient colors for by-value color modes', () => { + it.each(['dark', 'light'] as const)('should return gradient colors for by-value color mode in %s theme', (mode) => { expect( - buildGradientColors('auto', createTheme(), buildFieldDisplay(createField(FieldColorModeId.ContinuousBlues))) + buildGradientColors( + 'auto', + createTheme({ colors: { mode } }), + buildFieldDisplay(createField(FieldColorModeId.ContinuousBlues)) + ) ).toMatchSnapshot(); }); - it('should return gradient colors for fixed color mode', () => { + it.each(['dark', 'light'] as const)('should return gradient colors for fixed color mode in %s theme', (mode) => { expect( - buildGradientColors('auto', createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#442299') + buildGradientColors( + 'auto', + createTheme({ colors: { mode } }), + buildFieldDisplay(createField(FieldColorModeId.Fixed)), + '#442299' + ) ).toMatchSnapshot(); }); }); From 9c1df45589764861bcb4fe05c6a6bc300febb592 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 09:11:49 -0500 Subject: [PATCH 36/48] set opacity to 0.5 for dots --- .../src/components/RadialGauge/RadialArcPath.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index d89ec94cc58..2afd63a0777 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -29,6 +29,7 @@ interface RadialArcPathPropsWithGuideDot extends RadialArcPathPropsBase { type RadialArcPathProps = RadialArcPathPropsBase | RadialArcPathPropsWithGuideDot; +const DOT_OPACITY = 0.5; const DOT_RADIUS_FACTOR = 0.4; const MAX_DOT_RADIUS = 8; @@ -102,8 +103,10 @@ export const RadialArcPath = memo( {endpointColors && } - {guideDotColors && arcLengthDeg > 5 && } - {guideDotColors && } + {guideDotColors && arcLengthDeg > 5 && ( + + )} + {guideDotColors && } ); } From 2d72990c17d51a7d22f42ab3d8536310ffdb04b5 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 09:50:56 -0500 Subject: [PATCH 37/48] move min degrees for start dot render to a const --- .../grafana-ui/src/components/RadialGauge/RadialArcPath.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index 2afd63a0777..29841113167 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -29,6 +29,7 @@ interface RadialArcPathPropsWithGuideDot extends RadialArcPathPropsBase { type RadialArcPathProps = RadialArcPathPropsBase | RadialArcPathPropsWithGuideDot; +const DOT_START_MIN_ANGLE_DEG = 5; const DOT_OPACITY = 0.5; const DOT_RADIUS_FACTOR = 0.4; const MAX_DOT_RADIUS = 8; @@ -103,7 +104,7 @@ export const RadialArcPath = memo( {endpointColors && } - {guideDotColors && arcLengthDeg > 5 && ( + {guideDotColors && arcLengthDeg > DOT_START_MIN_ANGLE_DEG && ( )} {guideDotColors && } From 50fa37af535ec6c5693ff24bae47e43e15a9290e Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 14:36:01 -0500 Subject: [PATCH 38/48] change endpoint marks to be configurable --- .../panelcfg/x/NewGaugePanelCfg_types.gen.ts | 2 + .../components/RadialGauge/RadialArcPath.tsx | 106 +++++++++++++----- .../src/components/RadialGauge/RadialBar.tsx | 24 ++-- .../RadialGauge/RadialBarSegmented.tsx | 18 +-- .../RadialGauge/RadialGauge.story.tsx | 76 +++++-------- .../components/RadialGauge/RadialGauge.tsx | 37 ++++-- .../components/RadialGauge/ThresholdsBar.tsx | 10 +- .../__snapshots__/colors.test.ts.snap | 18 +-- .../src/components/RadialGauge/colors.test.ts | 58 +++++----- .../src/components/RadialGauge/colors.ts | 22 ++-- .../src/components/RadialGauge/effects.tsx | 35 ++++++ .../src/components/RadialGauge/types.ts | 1 - .../panel/radialbar/RadialBarPanel.tsx | 3 +- public/app/plugins/panel/radialbar/module.tsx | 16 +++ .../app/plugins/panel/radialbar/panelcfg.cue | 5 +- .../plugins/panel/radialbar/panelcfg.gen.ts | 2 + 16 files changed, 264 insertions(+), 169 deletions(-) diff --git a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts index 3c9853535e8..7c29eb7b463 100644 --- a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts @@ -28,6 +28,7 @@ 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,6 +41,7 @@ export const defaultOptions: Partial = { barShape: 'flat', barWidthFactor: 0.5, effects: {}, + endpointMarker: 'point', segmentCount: 1, segmentSpacing: 0.3, shape: 'gauge', diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index 29841113167..4a0eabb7866 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,11 +1,11 @@ -import { useId, memo, HTMLAttributes } from 'react'; +import { useId, memo, HTMLAttributes, ReactElement } from 'react'; import { FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; -import { buildGradientColors, getEndpointColors, getGradientCss, getGuideDotColors } from './colors'; -import { RadialGradientMode, RadialShape, RadialGaugeDimensions } from './types'; +import { buildGradientColors, getBarEndcapColors, getGradientCss, getEndpointMarkerColors } from './colors'; +import { RadialShape, RadialGaugeDimensions } from './types'; import { drawRadialArcPath, toRad } from './utils'; export interface RadialArcPathPropsBase { @@ -13,12 +13,13 @@ export interface RadialArcPathPropsBase { color?: string; dimensions: RadialGaugeDimensions; fieldDisplay: FieldDisplay; - glowFilter?: string; - gradientMode: RadialGradientMode; + gradient?: boolean; roundedBars?: boolean; shape: RadialShape; - showGuideDots?: boolean; + endpointMarker?: 'point' | 'glow'; startAngle: number; + glowFilter?: string; + endpointMarkerGlowFilter?: string; } interface RadialArcPathPropsWithGuideDot extends RadialArcPathPropsBase { @@ -40,27 +41,18 @@ export const RadialArcPath = memo( color, dimensions, fieldDisplay, - glowFilter, - gradientMode, + gradient, roundedBars, shape, - showGuideDots, + endpointMarker, startAngle: angle, + glowFilter, + endpointMarkerGlowFilter, }: RadialArcPathProps) => { const theme = useTheme2(); const id = useId(); - const gradientStops = buildGradientColors(gradientMode, theme, fieldDisplay, fieldDisplay.display.color); - - let guideDotColors: [string, string] | undefined; - let endpointColors: [string, string] | undefined; - - if (showGuideDots && gradientStops.length > 0) { - guideDotColors = getGuideDotColors(gradientStops, fieldDisplay.display.percent); - if (shape === 'circle') { - endpointColors = getEndpointColors(gradientStops, fieldDisplay.display.percent); - } - } + const gradientStops = buildGradientColors(gradient, theme, fieldDisplay, fieldDisplay.display.color); const bgDivStyle: HTMLAttributes['style'] = { width: '100%', height: '100%' }; if (color) { @@ -76,12 +68,65 @@ export const RadialArcPath = memo( const startRadians = toRad(angle); const endRadians = toRad(angle + arcLengthDeg); - const x1 = centerX + radius * Math.cos(startRadians); - const y1 = centerY + radius * Math.sin(startRadians); - const x2 = centerX + radius * Math.cos(endRadians); - const y2 = centerY + radius * Math.sin(endRadians); + 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 = Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS); + const dotRadius = + endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2; + + let barEndcapColors: [string | undefined, string | undefined] | undefined; + const endpointMarks: ReactElement[] = []; + if (endpointMarker && gradientStops.length > 0) { + switch (endpointMarker) { + case 'point': + const [pointColorStart, pointColorEnd] = getEndpointMarkerColors(gradientStops, fieldDisplay.display.percent); + if (arcLengthDeg > DOT_START_MIN_ANGLE_DEG) { + endpointMarks.push( + + ); + } + endpointMarks.push( + + ); + break; + case 'glow': + const xStartMark = centerX + radius * Math.cos(endRadians - 0.2); + const yStartMark = centerY + radius * Math.sin(endRadians - 0.2); + endpointMarks.push( + + ); + break; + default: + break; + } + + if (shape === 'circle') { + barEndcapColors = getBarEndcapColors(gradientStops, fieldDisplay.display.percent); + } + } return ( <> @@ -100,14 +145,13 @@ export const RadialArcPath = memo( >
- {endpointColors && } - {endpointColors && } + {barEndcapColors?.[0] && } + {barEndcapColors?.[1] && ( + + )} - {guideDotColors && arcLengthDeg > DOT_START_MIN_ANGLE_DEG && ( - - )} - {guideDotColors && } + {endpointMarks} ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index cf2a50b55cb..45107751f67 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -3,29 +3,33 @@ import { FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialGradientMode, RadialShape, RadialGaugeDimensions } from './types'; +import { RadialShape, RadialGaugeDimensions } from './types'; export interface RadialBarProps { angle: number; angleRange: number; dimensions: RadialGaugeDimensions; fieldDisplay: FieldDisplay; - glowFilter?: string; - gradientMode: RadialGradientMode; + gradient?: boolean; roundedBars?: boolean; + endpointMarker?: 'point' | 'glow'; shape: RadialShape; startAngle: number; + glowFilter?: string; + endpointMarkerGlowFilter?: string; } export function RadialBar({ angle, angleRange, dimensions, fieldDisplay, - glowFilter, - gradientMode, + gradient, roundedBars, + endpointMarker, shape, startAngle, + glowFilter, + endpointMarkerGlowFilter, }: RadialBarProps) { const theme = useTheme2(); return ( @@ -36,7 +40,6 @@ export function RadialBar({ fieldDisplay={fieldDisplay} color={theme.colors.action.hover} dimensions={dimensions} - gradientMode="none" roundedBars={roundedBars} shape={shape} startAngle={startAngle + angle} @@ -44,15 +47,16 @@ export function RadialBar({ {/** The colored bar */} ); diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx index d86baa5d1b5..5995d65dcbd 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx @@ -5,7 +5,7 @@ import { FALLBACK_COLOR, FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialGradientMode, RadialShape, RadialGaugeDimensions } from './types'; +import { RadialShape, RadialGaugeDimensions } from './types'; import { getAngleBetweenSegments, getFieldConfigMinMax, @@ -22,7 +22,7 @@ export interface RadialBarSegmentedProps { segmentCount: number; segmentSpacing: number; shape: RadialShape; - gradientMode: RadialGradientMode; + gradient?: boolean; } export const RadialBarSegmented = memo( @@ -32,10 +32,10 @@ export const RadialBarSegmented = memo( startAngle, angleRange, glowFilter, + gradient, segmentCount, segmentSpacing, shape, - gradientMode, }: RadialBarSegmentedProps) => { const theme = useTheme2(); @@ -53,21 +53,21 @@ export const RadialBarSegmented = memo( let segmentColor: string | undefined; if (angleValue >= value) { segmentColor = theme.colors.action.hover; - } else if (gradientMode === 'none') { + } else if (!gradient) { segmentColor = displayProcessor(angleValue).color ?? FALLBACK_COLOR; } segments.push( ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx index 0bd1e4abf09..e6d6da7366c 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx @@ -14,7 +14,7 @@ import { useTheme2 } from '../../themes/ThemeContext'; import { Stack } from '../Layout/Stack/Stack'; import { RadialGauge, RadialGaugeProps } from './RadialGauge'; -import { RadialGradientMode, RadialShape, RadialTextMode } from './types'; +import { RadialShape, RadialTextMode } from './types'; interface StoryProps extends RadialGaugeProps { value: number; @@ -60,7 +60,7 @@ const meta: Meta = { width: 200, height: 200, shape: 'circle', - gradient: 'none', + gradient: false, seriesCount: 1, segmentCount: 0, segmentSpacing: 0.2, @@ -77,7 +77,7 @@ const meta: Meta = { 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 } }, @@ -119,41 +119,17 @@ export const Examples: StoryFn = (args) => {
Bar width
- - - - + + + +
Effects
- - - - + + + +
Shape: Gauge & color scale
@@ -161,14 +137,14 @@ export const Examples: StoryFn = (args) => { value={40} shape="gauge" width={250} - gradient="auto" + gradient colorScheme={FieldColorModeId.ContinuousGrYlRd} glowCenter={true} barWidthFactor={0.6} /> = (args) => { value={args.value ?? 70} color="blue" shape="gauge" - gradient="auto" + gradient sparkline={true} glowBar={true} glowCenter={true} @@ -194,7 +170,7 @@ export const Examples: StoryFn = (args) => { value={args.value ?? 30} color="green" shape="gauge" - gradient="auto" + gradient sparkline={true} glowBar={true} glowCenter={true} @@ -205,7 +181,7 @@ export const Examples: StoryFn = (args) => { color="red" shape="gauge" width={250} - gradient="auto" + gradient sparkline={true} glowBar={true} glowCenter={true} @@ -216,7 +192,7 @@ export const Examples: StoryFn = (args) => { color="red" width={250} shape="gauge" - gradient="auto" + gradient sparkline={true} glowBar={true} glowCenter={true} @@ -228,7 +204,7 @@ export const Examples: StoryFn = (args) => { = (args) => { = (args) => { = (args) => { width={250} colorScheme={FieldColorModeId.ContinuousGrYlRd} shape="gauge" - gradient="auto" + gradient glowBar={true} glowCenter={true} segmentCount={40} @@ -280,7 +256,7 @@ export const Examples: StoryFn = (args) => { = (args) => { value={args.value ?? 70} width={250} colorScheme={FieldColorModeId.Thresholds} - gradient="auto" + gradient glowCenter={true} thresholdsBar={true} roundedBars={false} @@ -301,7 +277,7 @@ export const Examples: StoryFn = (args) => { value={args.value ?? 70} width={250} colorScheme={FieldColorModeId.Thresholds} - gradient="auto" + gradient glowCenter={true} thresholdsBar={true} roundedBars={false} @@ -347,7 +323,6 @@ export const Temp: StoryFn = (args) => { }; interface ExampleProps { - gradient?: RadialGradientMode; color?: string; seriesName?: string; value?: number; @@ -356,6 +331,7 @@ interface ExampleProps { max?: number; width?: number; height?: number; + gradient?: boolean; glowBar?: boolean; glowCenter?: boolean; barWidthFactor?: number; @@ -373,7 +349,6 @@ interface ExampleProps { } export function RadialGaugeExample({ - gradient = 'none', color, seriesName = 'Server A', value = 70, @@ -382,6 +357,7 @@ export function RadialGaugeExample({ max = 100, width = 200, height = 200, + gradient = false, glowBar = false, glowCenter = false, barWidthFactor = 0.4, diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index 7cf4ab3fa76..9cb6e55584f 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -13,8 +13,8 @@ import { RadialScaleLabels } from './RadialScaleLabels'; import { RadialSparkline } from './RadialSparkline'; import { RadialText } from './RadialText'; import { ThresholdsBar } from './ThresholdsBar'; -import { GlowGradient, MiddleCircleGlow } from './effects'; -import { RadialGradientMode, RadialShape, RadialTextMode } from './types'; +import { GlowGradient, MiddleCircleGlow, SpotlightGradient } from './effects'; +import { RadialShape, RadialTextMode } from './types'; import { calculateDimensions, getValueAngleForValue } from './utils'; export interface RadialGaugeProps { @@ -25,7 +25,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. @@ -37,6 +37,10 @@ export interface RadialGaugeProps { 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 @@ -74,7 +78,7 @@ export function RadialGauge(props: RadialGaugeProps) { width = 256, height = 256, shape = 'circle', - gradient = 'none', + gradient = false, barWidthFactor = 0.4, glowBar = false, glowCenter = false, @@ -85,6 +89,7 @@ export function RadialGauge(props: RadialGaugeProps) { roundedBars = true, thresholdsBar = false, showScaleLabels = false, + endpointMarker, onClick, values, } = props; @@ -120,8 +125,24 @@ export function RadialGauge(props: RadialGaugeProps) { showScaleLabels ); + // 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}`; + if (endpointMarker === 'glow') { + defs.push( + + ); + } + if (segmentCount > 1) { graphics.push( ); } else { @@ -147,9 +168,11 @@ export function RadialGauge(props: RadialGaugeProps) { startAngle={startAngle} roundedBars={roundedBars} glowFilter={`url(#${glowFilterId})`} + endpointMarkerGlowFilter={`url(#${spotlightGradientId})`} shape={shape} - gradientMode={gradient} + gradient={gradient} fieldDisplay={displayValue} + endpointMarker={endpointMarker} /> ); } @@ -210,7 +233,7 @@ export function RadialGauge(props: RadialGaugeProps) { roundedBars={roundedBars} glowFilter={`url(#${glowFilterId})`} shape={shape} - gradientMode={gradient} + gradient={gradient} /> ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx index 5db86596dbe..e9137d68ae1 100644 --- a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx @@ -1,7 +1,7 @@ import { FieldDisplay, Threshold } from '@grafana/data'; import { RadialArcPath } from './RadialArcPath'; -import { RadialGaugeDimensions, RadialGradientMode, RadialShape } from './types'; +import { RadialGaugeDimensions, RadialShape } from './types'; import { getFieldConfigMinMax } from './utils'; interface ThresholdsBarProps { @@ -14,7 +14,7 @@ interface ThresholdsBarProps { roundedBars?: boolean; glowFilter?: string; thresholds: Threshold[]; - gradientMode: RadialGradientMode; + gradient?: boolean; } export function ThresholdsBar({ @@ -26,7 +26,7 @@ export function ThresholdsBar({ glowFilter, thresholds, shape, - gradientMode, + gradient, }: ThresholdsBarProps) { const thresholdDimensions = { ...dimensions, @@ -50,7 +50,7 @@ export function ThresholdsBar({ } const lengthDeg = valueDeg - currentStart + startAngle; - const color = gradientMode === 'none' ? threshold.color : undefined; + const color = gradient ? undefined : threshold.color; paths.push( = { @@ -49,9 +49,16 @@ describe('RadialGauge color utils', () => { }, }); - it('should return the baseColor if gradientMode is none', () => { + it('should return the baseColor if gradient is false-y', () => { expect( - buildGradientColors('none', createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000') + 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 }, @@ -59,18 +66,18 @@ describe('RadialGauge color utils', () => { }); it('uses the fallback color if no baseColor is set', () => { - expect( - buildGradientColors('none', createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed))) - ).toEqual([ - { color: FALLBACK_COLOR, percent: 0 }, - { color: FALLBACK_COLOR, percent: 1 }, - ]); + 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( - 'auto', + true, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds), { view: { getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: '#444444' }))) }, @@ -81,19 +88,14 @@ describe('RadialGauge color utils', () => { it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => { expect( - buildGradientColors( - 'auto', - createTheme(), - buildFieldDisplay(createField(FieldColorModeId.Thresholds)), - '#FF0000' - ) + buildGradientColors(true, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds)), '#FF0000') ).toMatchSnapshot(); }); it('should return gradient colors for continuous color modes', () => { expect( buildGradientColors( - 'auto', + true, createTheme(), buildFieldDisplay(createField(FieldColorModeId.ContinuousCividis)), '#00FF00' @@ -104,7 +106,7 @@ describe('RadialGauge color utils', () => { it.each(['dark', 'light'] as const)('should return gradient colors for by-value color mode in %s theme', (mode) => { expect( buildGradientColors( - 'auto', + true, createTheme({ colors: { mode } }), buildFieldDisplay(createField(FieldColorModeId.ContinuousBlues)) ) @@ -114,7 +116,7 @@ describe('RadialGauge color utils', () => { it.each(['dark', 'light'] as const)('should return gradient colors for fixed color mode in %s theme', (mode) => { expect( buildGradientColors( - 'auto', + true, createTheme({ colors: { mode } }), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#442299' @@ -168,14 +170,14 @@ describe('RadialGauge color utils', () => { }); }); - describe('getEndpointColors', () => { + 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] = getEndpointColors(gradient); + const [startColor, endColor] = getBarEndcapColors(gradient); expect(startColor).toBe('#ff0000'); expect(endColor).toBe('#0000ff'); }); @@ -186,22 +188,22 @@ describe('RadialGauge color utils', () => { { color: '#00ff00', percent: 0.5 }, { color: '#0000ff', percent: 1 }, ]; - const [startColor, endColor] = getEndpointColors(gradient, 0.25); + 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] = getEndpointColors(gradient); + const [startColor, endColor] = getBarEndcapColors(gradient); expect(startColor).toBe('#ff0000'); expect(endColor).toBe('#ff0000'); }); it('should throw an error when no colors are provided', () => { expect(() => { - getEndpointColors([]); - }).toThrow('getEndpointColors requires at least one color stop'); + getBarEndcapColors([]); + }).toThrow('getBarEndcapColors requires at least one color stop'); }); }); @@ -227,14 +229,14 @@ describe('RadialGauge color utils', () => { }); }); - describe('getGuideDotColors', () => { + 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] = getGuideDotColors(gradient, 0.35); + const [startDotColor, endDotColor] = getEndpointMarkerColors(gradient, 0.35); expect(startDotColor).toBe('#fbfbfb'); expect(endDotColor).toBe('#111217'); }); diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts index 58053d6cbe7..4cf5d7ca84e 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -3,16 +3,16 @@ import tinycolor from 'tinycolor2'; import { colorManipulator, FALLBACK_COLOR, FieldDisplay, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; import { FieldColorModeId } from '@grafana/schema'; -import { GradientStop, RadialGradientMode, RadialShape } from './types'; +import { GradientStop, RadialShape } from './types'; import { getFieldConfigMinMax, getFieldDisplayProcessor } from './utils'; export function buildGradientColors( - gradientMode: RadialGradientMode, + gradient = false, theme: GrafanaTheme2, fieldDisplay: FieldDisplay, baseColor = FALLBACK_COLOR ): GradientStop[] { - if (gradientMode === 'none') { + if (!gradient) { return [ { color: baseColor, percent: 0 }, { color: baseColor, percent: 1 }, @@ -67,16 +67,14 @@ export function buildGradientColors( // for fixed colors and other modes, we create a simple two-color gradient // we set the highest contrast color second based on the theme. - const darkerColor = tinycolor(baseColor).spin(5).darken(35).saturate(20); + const darkerColor = tinycolor(baseColor).spin(5).darken(20).saturate(25); const lighterColor = tinycolor(baseColor) .spin(-5) .brighten(theme.isDark ? 30 : 15) - .lighten(theme.isDark ? 35 : 15) - .desaturate(10); + .lighten(theme.isDark ? 35 : 15); return [ { color: theme.isDark ? darkerColor.toString() : lighterColor.toString(), percent: 0 }, - { color: baseColor, percent: 0.45 }, - { color: baseColor, percent: 0.55 }, + { color: baseColor, percent: 0.33 }, { color: theme.isDark ? lighterColor.toString() : darkerColor.toString(), percent: 1 }, ]; } @@ -138,9 +136,9 @@ export function colorAtGradientPercent(stops: GradientStop[], percent: number): return mixed; } -export function getEndpointColors(gradientStops: GradientStop[], percent = 1): [string, string] { +export function getBarEndcapColors(gradientStops: GradientStop[], percent = 1): [string, string] { if (gradientStops.length === 0) { - throw new Error('getEndpointColors requires at least one color stop'); + throw new Error('getBarEndcapColors requires at least one color stop'); } const startColor = gradientStops[0].color; @@ -173,7 +171,7 @@ const getGuideDotColor = (color: string): string => { return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor; }; -export function getGuideDotColors(gradientStops: GradientStop[], percent = 1): [string, string] { - const [startColor, endColor] = getEndpointColors(gradientStops, percent); +export function getEndpointMarkerColors(gradientStops: GradientStop[], percent = 1): [string, string] { + const [startColor, endColor] = getBarEndcapColors(gradientStops, percent); return [getGuideDotColor(startColor), getGuideDotColor(endColor)]; } diff --git a/packages/grafana-ui/src/components/RadialGauge/effects.tsx b/packages/grafana-ui/src/components/RadialGauge/effects.tsx index c8f7afb542f..c48307f177e 100644 --- a/packages/grafana-ui/src/components/RadialGauge/effects.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/effects.tsx @@ -1,3 +1,5 @@ +import { GrafanaTheme2 } from '@grafana/data'; + import { RadialGaugeDimensions } from './types'; export interface GlowGradientProps { @@ -57,3 +59,36 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps ); } + +export function SpotlightGradient({ + id, + dimensions, + roundedBars, + angle, + theme, +}: { + id: string; + dimensions: RadialGaugeDimensions; + angle: number; + roundedBars: boolean; + theme: GrafanaTheme2; +}) { + if (theme.isLight) { + return null; + } + + const angleRadian = ((angle - 90) * Math.PI) / 180; + + let x1 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian - 0.2); + let y1 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian - 0.2); + let x2 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian); + let y2 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian); + + return ( + + + + {roundedBars && } + + ); +} diff --git a/packages/grafana-ui/src/components/RadialGauge/types.ts b/packages/grafana-ui/src/components/RadialGauge/types.ts index 67084a3fb63..cc233dd524c 100644 --- a/packages/grafana-ui/src/components/RadialGauge/types.ts +++ b/packages/grafana-ui/src/components/RadialGauge/types.ts @@ -1,4 +1,3 @@ -export type RadialGradientMode = 'none' | 'auto'; export type RadialTextMode = 'auto' | 'value_and_name' | 'value' | 'name' | 'none'; export type RadialShape = 'circle' | 'gauge'; diff --git a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx index 7c2a0cc9081..03232406463 100644 --- a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx +++ b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx @@ -37,7 +37,7 @@ export function RadialBarPanel({ width={width} height={height} barWidthFactor={options.barWidthFactor} - gradient={options.effects?.gradient ? 'auto' : 'none'} + gradient={options.effects?.gradient} glowBar={options.effects?.barGlow} glowCenter={options.effects?.centerGlow} roundedBars={options.barShape === 'rounded'} @@ -50,6 +50,7 @@ export function RadialBarPanel({ alignmentFactors={valueProps.alignmentFactors} valueManualFontSize={options.text?.valueSize} nameManualFontSize={options.text?.titleSize} + endpointMarker={options.endpointMarker !== 'none' ? options.endpointMarker : undefined} onClick={menuProps.openMenu} /> ); diff --git a/public/app/plugins/panel/radialbar/module.tsx b/public/app/plugins/panel/radialbar/module.tsx index f7cd5fb043f..e1719847632 100644 --- a/public/app/plugins/panel/radialbar/module.tsx +++ b/public/app/plugins/panel/radialbar/module.tsx @@ -71,6 +71,22 @@ export const plugin = new PanelPlugin(RadialBarPanel) 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.addSliderInput({ path: 'barWidthFactor', name: t('radialbar.config.bar-width', 'Bar width'), diff --git a/public/app/plugins/panel/radialbar/panelcfg.cue b/public/app/plugins/panel/radialbar/panelcfg.cue index 89df6eddc26..c959512c0b8 100644 --- a/public/app/plugins/panel/radialbar/panelcfg.cue +++ b/public/app/plugins/panel/radialbar/panelcfg.cue @@ -28,7 +28,7 @@ composableKinds: PanelCfg: { GaugePanelEffects: { barGlow?: bool | *false centerGlow?: bool | *false - gradient?: bool | *true + gradient?: bool | *true } @cuetsy(kind="interface") Options: { @@ -40,7 +40,8 @@ composableKinds: PanelCfg: { sparkline?: bool | *true shape: "circle" | *"gauge" barWidthFactor: number | *0.5 - barShape: "flat" | "rounded" | *"flat" + barShape: "flat" | "rounded" | *"flat" + endpointMarker?: "point" | "glow" | "none" | *"point" effects: GaugePanelEffects | *{} } @cuetsy(kind="interface") } diff --git a/public/app/plugins/panel/radialbar/panelcfg.gen.ts b/public/app/plugins/panel/radialbar/panelcfg.gen.ts index ddb7b522365..e050a044f77 100644 --- a/public/app/plugins/panel/radialbar/panelcfg.gen.ts +++ b/public/app/plugins/panel/radialbar/panelcfg.gen.ts @@ -26,6 +26,7 @@ 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,6 +39,7 @@ export const defaultOptions: Partial = { barShape: 'flat', barWidthFactor: 0.5, effects: {}, + endpointMarker: 'point', segmentCount: 1, segmentSpacing: 0.3, shape: 'gauge', From 7d2475454cd054a3e8e752cbd8efa697586606c6 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 14:39:02 -0500 Subject: [PATCH 39/48] update gdev and fixtures --- .../panel-gauge/v0alpha1.gauge_tests_new.v42.json | 3 ++- .../panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json | 1 + .../panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json | 1 + .../panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json | 1 + .../dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json | 3 ++- devenv/dev-dashboards/panel-gauge/gauge_tests_new.json | 1 + 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json index b6d6e826a6e..62648d7a4aa 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json @@ -1544,12 +1544,13 @@ "options": { "barWidth": 12, "barWidthFactor": 0.4, + "barShape": "rounded", "effects": { "barGlow": true, "centerGlow": true, "gradient": true }, - "barShape": "rounded", + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json index a7c479384a3..e04d448a5b8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json @@ -1556,6 +1556,7 @@ "centerGlow": true, "gradient": true }, + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json index 6ff78e8f271..0e6e3e13da5 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json @@ -2208,6 +2208,7 @@ "centerGlow": true, "gradient": true }, + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json index 7b9a1b12422..ad2b8ca0385 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json @@ -2281,6 +2281,7 @@ "centerGlow": true, "gradient": true }, + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json index 67bab1878db..cb130445efc 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json @@ -1543,12 +1543,13 @@ "options": { "barWidth": 12, "barWidthFactor": 0.4, + "barShape": "rounded", "effects": { "barGlow": true, "centerGlow": true, "gradient": true }, - "barShape": "rounded", + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index 4d0cd7d5579..65cc0b0a5ae 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -1513,6 +1513,7 @@ "centerGlow": true, "gradient": true }, + "endpointMarker": "glow", "glow": "both", "orientation": "auto", "reduceOptions": { From 46510e72f37f7cda6af924b8db7ed3d5ad42abbf Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 14:58:25 -0500 Subject: [PATCH 40/48] i18n --- public/locales/en-US/grafana.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a4ee917fe3c..9b4ebcccf9b 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -12462,6 +12462,11 @@ "gradient": "Gradient", "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", From 36bc5535f4d2f1ec34d0121ea198f1954b4f1e25 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 17:24:41 -0500 Subject: [PATCH 41/48] shore up testing a bit --- .../components/RadialGauge/RadialArcPath.tsx | 35 ++++++------------- .../RadialGauge/RadialGauge.story.tsx | 4 +++ .../RadialGauge/RadialGauge.test.tsx | 25 ++++++++++--- .../src/components/RadialGauge/utils.ts | 1 + 4 files changed, 36 insertions(+), 29 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index 4a0eabb7866..c648022d864 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,4 +1,4 @@ -import { useId, memo, HTMLAttributes, ReactElement } from 'react'; +import { useId, memo, HTMLAttributes, ReactNode } from 'react'; import { FieldDisplay } from '@grafana/data'; @@ -77,38 +77,25 @@ export const RadialArcPath = memo( endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2; let barEndcapColors: [string | undefined, string | undefined] | undefined; - const endpointMarks: ReactElement[] = []; + + let endpointMarks: ReactNode = null; if (endpointMarker && gradientStops.length > 0) { switch (endpointMarker) { case 'point': const [pointColorStart, pointColorEnd] = getEndpointMarkerColors(gradientStops, fieldDisplay.display.percent); - if (arcLengthDeg > DOT_START_MIN_ANGLE_DEG) { - endpointMarks.push( - - ); - } - endpointMarks.push( - + endpointMarks = ( + <> + {arcLengthDeg > DOT_START_MIN_ANGLE_DEG && ( + + )} + + ); break; case 'glow': const xStartMark = centerX + radius * Math.cos(endRadians - 0.2); const yStartMark = centerY + radius * Math.sin(endRadians - 0.2); - endpointMarks.push( + endpointMarks = ( = { 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: [ @@ -344,6 +345,7 @@ interface ExampleProps { roundedBars?: boolean; thresholdsBar?: boolean; colorScheme?: FieldColorModeId; + endpointMarker?: RadialGaugeProps['endpointMarker']; decimals?: number; showScaleLabels?: boolean; } @@ -370,6 +372,7 @@ export function RadialGaugeExample({ roundedBars = false, thresholdsBar = false, colorScheme = FieldColorModeId.Thresholds, + endpointMarker = 'glow', decimals = 0, showScaleLabels, }: ExampleProps) { @@ -456,6 +459,7 @@ export function RadialGaugeExample({ roundedBars={roundedBars} thresholdsBar={thresholdsBar} showScaleLabels={showScaleLabels} + endpointMarker={endpointMarker} /> ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx index e2fed36ec3f..783e3b764da 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx @@ -1,13 +1,28 @@ import { render, screen } from '@testing-library/react'; +import { ComponentProps } from 'react'; import { RadialGaugeExample } from './RadialGauge.story'; describe('RadialGauge', () => { - it('should render', () => { - render(); - - expect(screen.getByRole('img')).toBeInTheDocument(); - }); + it.each([ + { description: 'default', props: {} }, + { description: 'gauge shape', props: { shape: 'gauge' } }, + { description: 'with gradient', props: { gradient: true } }, + { description: 'with glow bar', props: { glowBar: true } }, + { description: 'with glow center', props: { glowCenter: true } }, + { description: 'with segments', props: { segmentCount: 5 } }, + { description: 'with rounded bars', props: { roundedBars: true } }, + { description: 'with endpoint marker glow', props: { roundedBars: true, endpointMarker: 'glow' } }, + { description: 'with endpoint marker point', props: { roundedBars: true, endpointMarker: 'point' } }, + { description: 'with thresholds bar', props: { thresholdsBar: true } }, + { description: 'with sparkline', props: { sparkline: true } }, + ] satisfies Array<{ description: string; props?: ComponentProps }>)( + 'should render $description without throwing', + ({ props }) => { + render(); + expect(screen.getByRole('img')).toBeInTheDocument(); + } + ); it('should render threshold labels', () => { render(); diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.ts b/packages/grafana-ui/src/components/RadialGauge/utils.ts index 72030a09ad6..e089f9b3267 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.ts @@ -115,6 +115,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; From 5a1ae834e07afdc3149068dfb7bf5733acec648a Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 17:26:43 -0500 Subject: [PATCH 42/48] remove period for consistency --- public/app/plugins/panel/radialbar/module.tsx | 2 +- public/locales/en-US/grafana.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/radialbar/module.tsx b/public/app/plugins/panel/radialbar/module.tsx index e1719847632..d9f1853ba7a 100644 --- a/public/app/plugins/panel/radialbar/module.tsx +++ b/public/app/plugins/panel/radialbar/module.tsx @@ -74,7 +74,7 @@ export const plugin = new PanelPlugin(RadialBarPanel) 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.'), + description: t('radialbar.config.endpoint-marker-description', 'Glow is only supported in dark mode'), category, defaultValue: defaultOptions.endpointMarker, settings: { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 9b4ebcccf9b..e95dfc9a464 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -12463,7 +12463,7 @@ "label": "Effects" }, "endpoint-marker": "Endpoint marker", - "endpoint-marker-description": "Glow is only supported in dark mode.", + "endpoint-marker-description": "Glow is only supported in dark mode", "endpoint-marker-glow": "Glow", "endpoint-marker-none": "None", "endpoint-marker-point": "Point", From 2c9b7ca135bbb03f116d0cfcedd68dcb482666d2 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 17:32:50 -0500 Subject: [PATCH 43/48] hide glow at small angles --- .../components/RadialGauge/RadialArcPath.tsx | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index c648022d864..59be60f27da 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -30,7 +30,7 @@ interface RadialArcPathPropsWithGuideDot extends RadialArcPathPropsBase { type RadialArcPathProps = RadialArcPathPropsBase | RadialArcPathPropsWithGuideDot; -const DOT_START_MIN_ANGLE_DEG = 5; +const ENDPOINT_MARKER_MIN_ANGLE = 10; const DOT_OPACITY = 0.5; const DOT_RADIUS_FACTOR = 0.4; const MAX_DOT_RADIUS = 8; @@ -85,7 +85,7 @@ export const RadialArcPath = memo( const [pointColorStart, pointColorEnd] = getEndpointMarkerColors(gradientStops, fieldDisplay.display.percent); endpointMarks = ( <> - {arcLengthDeg > DOT_START_MIN_ANGLE_DEG && ( + {arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && ( )} @@ -93,18 +93,20 @@ export const RadialArcPath = memo( ); break; case 'glow': - const xStartMark = centerX + radius * Math.cos(endRadians - 0.2); - const yStartMark = centerY + radius * Math.sin(endRadians - 0.2); - endpointMarks = ( - - ); + const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE); + const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle); + const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle); + endpointMarks = + arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE ? ( + + ) : null; break; default: break; From c7af2be682e295673d5ff50336f50a203685a99b Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 17 Dec 2025 17:42:14 -0500 Subject: [PATCH 44/48] more testing and cleanup --- .../src/components/RadialGauge/colors.test.ts | 13 +++++++++ .../src/components/RadialGauge/colors.ts | 29 +++++++++---------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts index b3206a267b8..9eb787ce150 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts @@ -151,6 +151,19 @@ describe('RadialGauge color utils', () => { 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 }, diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts index 4cf5d7ca84e..1f667804b7d 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -102,7 +102,7 @@ export function colorAtGradientPercent(stops: GradientStop[], percent: number): throw new Error('colorAtGradientPercent requires at least two color stops'); } - // normalize and sort stops by percent + // normalize and sort stops by percent. TODO: is this necessary? is gradientstops always sorted? const sorted = stops .map((s) => ({ color: s.color, percent: clamp(s.percent, 0, 1) })) .sort((a, b) => a.percent - b.percent); @@ -115,25 +115,24 @@ export function colorAtGradientPercent(stops: GradientStop[], percent: number): return tinycolor(sorted[sorted.length - 1].color); } - // find surrounding stops - let left = sorted[0]; - let right = sorted[sorted.length - 1]; - for (let i = 1; i < sorted.length; i++) { - if (percent <= sorted[i].percent) { - left = sorted[i - 1]; - right = sorted[i]; - break; + // 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 - - // tinycolor.mix expects amount as percentage of the second color - const mixed = tinycolor.mix(left.color, right.color, t * 100); - - // return hex6 if opaque, hex8 if has alpha - return mixed; + return tinycolor.mix(left.color, right.color, t * 100); } export function getBarEndcapColors(gradientStops: GradientStop[], percent = 1): [string, string] { From b123e24d8681dfcb14a2c989ae4940904f05a924 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 18 Dec 2025 11:52:34 -0500 Subject: [PATCH 45/48] addressing PR comments --- .../components/RadialGauge/RadialArcPath.tsx | 108 +++++++++--------- .../src/components/RadialGauge/RadialBar.tsx | 17 +-- .../RadialGauge/RadialBarSegmented.tsx | 17 +-- .../components/RadialGauge/RadialGauge.tsx | 12 +- .../components/RadialGauge/ThresholdsBar.tsx | 10 +- .../__snapshots__/colors.test.ts.snap | 16 +-- .../src/components/RadialGauge/colors.ts | 45 ++++---- .../src/components/RadialGauge/utils.ts | 8 +- public/app/plugins/panel/radialbar/module.tsx | 24 ++-- 9 files changed, 131 insertions(+), 126 deletions(-) diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index 59be60f27da..f59614acd53 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -2,18 +2,15 @@ import { useId, memo, HTMLAttributes, ReactNode } from 'react'; import { FieldDisplay } from '@grafana/data'; -import { useTheme2 } from '../../themes/ThemeContext'; - -import { buildGradientColors, getBarEndcapColors, getGradientCss, getEndpointMarkerColors } from './colors'; -import { RadialShape, RadialGaugeDimensions } from './types'; +import { getBarEndcapColors, getGradientCss, getEndpointMarkerColors } from './colors'; +import { RadialShape, RadialGaugeDimensions, GradientStop } from './types'; import { drawRadialArcPath, toRad } from './utils'; export interface RadialArcPathPropsBase { arcLengthDeg: number; - color?: string; + barEndcaps?: boolean; dimensions: RadialGaugeDimensions; fieldDisplay: FieldDisplay; - gradient?: boolean; roundedBars?: boolean; shape: RadialShape; endpointMarker?: 'point' | 'glow'; @@ -22,13 +19,15 @@ export interface RadialArcPathPropsBase { endpointMarkerGlowFilter?: string; } -interface RadialArcPathPropsWithGuideDot extends RadialArcPathPropsBase { - showGuideDots: true; - guideDotStartColor: string; - guideDotEndColor: string; +interface RadialArcPathPropsWithColor extends RadialArcPathPropsBase { + color: string; } -type RadialArcPathProps = RadialArcPathPropsBase | RadialArcPathPropsWithGuideDot; +interface RadialArcPathPropsWithGradient extends RadialArcPathPropsBase { + gradient: GradientStop[]; +} + +type RadialArcPathProps = RadialArcPathPropsWithColor | RadialArcPathPropsWithGradient; const ENDPOINT_MARKER_MIN_ANGLE = 10; const DOT_OPACITY = 0.5; @@ -38,27 +37,24 @@ const MAX_DOT_RADIUS = 8; export const RadialArcPath = memo( ({ arcLengthDeg, - color, dimensions, fieldDisplay, - gradient, roundedBars, shape, endpointMarker, + barEndcaps, startAngle: angle, glowFilter, endpointMarkerGlowFilter, + ...rest }: RadialArcPathProps) => { - const theme = useTheme2(); const id = useId(); - const gradientStops = buildGradientColors(gradient, theme, fieldDisplay, fieldDisplay.display.color); - const bgDivStyle: HTMLAttributes['style'] = { width: '100%', height: '100%' }; - if (color) { - bgDivStyle.backgroundColor = color; + if ('color' in rest) { + bgDivStyle.backgroundColor = rest.color; } else { - bgDivStyle.backgroundImage = getGradientCss(gradientStops, shape); + bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape); } const { radius, centerX, centerY, barWidth } = dimensions; @@ -76,44 +72,48 @@ export const RadialArcPath = memo( const dotRadius = endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2; - let barEndcapColors: [string | undefined, string | undefined] | undefined; - + let barEndcapColors: [string, string] | undefined; let endpointMarks: ReactNode = null; - if (endpointMarker && gradientStops.length > 0) { - switch (endpointMarker) { - case 'point': - const [pointColorStart, pointColorEnd] = getEndpointMarkerColors(gradientStops, fieldDisplay.display.percent); - endpointMarks = ( - <> - {arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && ( - - )} - - - ); - break; - case 'glow': - const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE); - const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle); - const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle); - endpointMarks = - arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE ? ( - - ) : null; - break; - default: - break; + if ('gradient' in rest) { + if (endpointMarker && (rest.gradient?.length ?? 0) > 0) { + switch (endpointMarker) { + case 'point': + const [pointColorStart, pointColorEnd] = getEndpointMarkerColors( + rest.gradient!, + fieldDisplay.display.percent + ); + endpointMarks = ( + <> + {arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && ( + + )} + + + ); + break; + case 'glow': + const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE); + const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle); + const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle); + endpointMarks = + arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE ? ( + + ) : null; + break; + default: + break; + } } - if (shape === 'circle') { - barEndcapColors = getBarEndcapColors(gradientStops, fieldDisplay.display.percent); + if (barEndcaps) { + barEndcapColors = getBarEndcapColors(rest.gradient, fieldDisplay.display.percent); } } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index 45107751f67..719ec52c625 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -1,16 +1,16 @@ -import { FieldDisplay } from '@grafana/data'; +import { FALLBACK_COLOR, FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialShape, RadialGaugeDimensions } from './types'; +import { RadialShape, RadialGaugeDimensions, GradientStop } from './types'; export interface RadialBarProps { angle: number; angleRange: number; dimensions: RadialGaugeDimensions; fieldDisplay: FieldDisplay; - gradient?: boolean; + gradient?: GradientStop[]; roundedBars?: boolean; endpointMarker?: 'point' | 'glow'; shape: RadialShape; @@ -32,6 +32,7 @@ export function RadialBar({ endpointMarkerGlowFilter, }: RadialBarProps) { const theme = useTheme2(); + const colorProps = gradient ? { gradient } : { color: fieldDisplay.display.color ?? FALLBACK_COLOR }; return ( <> {/** Track */} @@ -47,16 +48,16 @@ export function RadialBar({ {/** The colored bar */} ); diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx index 5995d65dcbd..b51cb4ce2f1 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx @@ -5,7 +5,7 @@ import { FALLBACK_COLOR, FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialShape, RadialGaugeDimensions } from './types'; +import { RadialShape, RadialGaugeDimensions, GradientStop } from './types'; import { getAngleBetweenSegments, getFieldConfigMinMax, @@ -22,7 +22,7 @@ export interface RadialBarSegmentedProps { segmentCount: number; segmentSpacing: number; shape: RadialShape; - gradient?: boolean; + gradient?: GradientStop[]; } export const RadialBarSegmented = memo( @@ -38,7 +38,6 @@ export const RadialBarSegmented = memo( shape, }: RadialBarSegmentedProps) => { const theme = useTheme2(); - const segments: React.ReactNode[] = []; const segmentCountAdjusted = getOptimalSegmentCount(dimensions, segmentSpacing, segmentCount, angleRange); const [min, max] = getFieldConfigMinMax(fieldDisplay); @@ -50,24 +49,20 @@ export const RadialBarSegmented = memo( for (let i = 0; i < segmentCountAdjusted; i++) { const angleValue = min + ((max - min) / segmentCountAdjusted) * i; const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01; - let segmentColor: string | undefined; - if (angleValue >= value) { - segmentColor = theme.colors.action.hover; - } else if (!gradient) { - segmentColor = displayProcessor(angleValue).color ?? FALLBACK_COLOR; - } + const segmentColor = + angleValue >= value ? theme.colors.border.medium : (displayProcessor(angleValue).color ?? FALLBACK_COLOR); + const colorProps = angleValue < value && gradient ? { gradient } : { color: segmentColor }; segments.push( ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index 9cb6e55584f..1251a364230 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -1,7 +1,7 @@ import { css, cx } from '@emotion/css'; import { useId } from 'react'; -import { DisplayValueAlignmentFactors, FieldDisplay, 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'; @@ -13,6 +13,7 @@ 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'; @@ -112,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, @@ -155,7 +157,7 @@ export function RadialGauge(props: RadialGaugeProps) { segmentCount={segmentCount} segmentSpacing={segmentSpacing} shape={shape} - gradient={gradient} + gradient={gradientStops} /> ); } else { @@ -170,7 +172,7 @@ export function RadialGauge(props: RadialGaugeProps) { glowFilter={`url(#${glowFilterId})`} endpointMarkerGlowFilter={`url(#${spotlightGradientId})`} shape={shape} - gradient={gradient} + gradient={gradientStops} fieldDisplay={displayValue} endpointMarker={endpointMarker} /> @@ -233,7 +235,7 @@ export function RadialGauge(props: RadialGaugeProps) { roundedBars={roundedBars} glowFilter={`url(#${glowFilterId})`} shape={shape} - gradient={gradient} + gradient={gradientStops} /> ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx index e9137d68ae1..cb2829934b9 100644 --- a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx @@ -1,7 +1,7 @@ import { FieldDisplay, Threshold } from '@grafana/data'; import { RadialArcPath } from './RadialArcPath'; -import { RadialGaugeDimensions, RadialShape } from './types'; +import { GradientStop, RadialGaugeDimensions, RadialShape } from './types'; import { getFieldConfigMinMax } from './utils'; interface ThresholdsBarProps { @@ -14,7 +14,7 @@ interface ThresholdsBarProps { roundedBars?: boolean; glowFilter?: string; thresholds: Threshold[]; - gradient?: boolean; + gradient?: GradientStop[]; } export function ThresholdsBar({ @@ -50,20 +50,20 @@ export function ThresholdsBar({ } const lengthDeg = valueDeg - currentStart + startAngle; - const color = gradient ? undefined : threshold.color; + const colorProps = gradient ? { gradient } : { color: threshold.color }; paths.push( ); diff --git a/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap index afabe6f9d2e..97b053c2d61 100644 --- a/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap +++ b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap @@ -112,15 +112,15 @@ exports[`RadialGauge color utils buildGradientColors should return gradient colo exports[`RadialGauge color utils buildGradientColors should return gradient colors for fixed color mode in dark theme 1`] = ` [ { - "color": "#210550", + "color": "#37237a", "percent": 0, }, { - "color": "#442299", - "percent": 0.33, + "color": "#a146da", + "percent": 0.75, }, { - "color": "#ffffff", + "color": "#a146da", "percent": 1, }, ] @@ -129,15 +129,15 @@ exports[`RadialGauge color utils buildGradientColors should return gradient colo exports[`RadialGauge color utils buildGradientColors should return gradient colors for fixed color mode in light theme 1`] = ` [ { - "color": "#9181d3", + "color": "#a146da", "percent": 0, }, { - "color": "#442299", - "percent": 0.33, + "color": "#3e2b9a", + "percent": 0.75, }, { - "color": "#210550", + "color": "#3e2b9a", "percent": 1, }, ] diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts index 1f667804b7d..a29a1efbbb3 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -4,13 +4,13 @@ import { colorManipulator, FALLBACK_COLOR, FieldDisplay, getFieldColorMode, Graf import { FieldColorModeId } from '@grafana/schema'; import { GradientStop, RadialShape } from './types'; -import { getFieldConfigMinMax, getFieldDisplayProcessor } from './utils'; +import { getFieldConfigMinMax, getFieldDisplayProcessor, getValuePercentageForValue } from './utils'; export function buildGradientColors( gradient = false, theme: GrafanaTheme2, fieldDisplay: FieldDisplay, - baseColor = FALLBACK_COLOR + baseColor = fieldDisplay.display.color ?? FALLBACK_COLOR ): GradientStop[] { if (!gradient) { return [ @@ -65,28 +65,31 @@ export function buildGradientColors( ]; } - // for fixed colors and other modes, we create a simple two-color gradient - // we set the highest contrast color second based on the theme. - const darkerColor = tinycolor(baseColor).spin(5).darken(20).saturate(25); - const lighterColor = tinycolor(baseColor) - .spin(-5) - .brighten(theme.isDark ? 30 : 15) - .lighten(theme.isDark ? 35 : 15); - return [ + // 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: baseColor, percent: 0.33 }, { color: theme.isDark ? lighterColor.toString() : darkerColor.toString(), percent: 1 }, ]; -} -function clamp(value: number, min = 0, max = 1) { - if (process.env.NODE_ENV !== 'production') { - if (value < min || value > max) { - console.error(`The value provided ${value} is out of range [${min}, ${max}].`); - } - } - - return Math.min(Math.max(min, value), max); + // 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 }, + ]; } /** @@ -104,7 +107,7 @@ export function colorAtGradientPercent(stops: GradientStop[], percent: number): // normalize and sort stops by percent. TODO: is this necessary? is gradientstops always sorted? const sorted = stops - .map((s) => ({ color: s.color, percent: clamp(s.percent, 0, 1) })) + .map((s) => ({ color: s.color, percent: Math.min(Math.max(0, s.percent), 1) })) .sort((a, b) => a.percent - b.percent); // percent outside range diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.ts b/packages/grafana-ui/src/components/RadialGauge/utils.ts index e089f9b3267..e26cf5eed2a 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.ts @@ -19,16 +19,20 @@ export function getFieldConfigMinMax(fieldDisplay: FieldDisplay) { return [min, max]; } +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 [min, max] = getFieldConfigMinMax(fieldDisplay); const angleRange = (360 % (startAngle === 0 ? 1 : startAngle)) + endAngle; - let angle = ((value - min) / (max - min)) * angleRange; + let angle = getValuePercentageForValue(fieldDisplay, value) * angleRange; if (angle > angleRange) { angle = angleRange; diff --git a/public/app/plugins/panel/radialbar/module.tsx b/public/app/plugins/panel/radialbar/module.tsx index d9f1853ba7a..3dfcb3e3d5f 100644 --- a/public/app/plugins/panel/radialbar/module.tsx +++ b/public/app/plugins/panel/radialbar/module.tsx @@ -32,6 +32,18 @@ export const plugin = new PanelPlugin(RadialBarPanel) }, }); + builder.addSliderInput({ + path: 'barWidthFactor', + name: t('radialbar.config.bar-width', 'Bar width'), + category, + defaultValue: defaultOptions.barWidthFactor, + settings: { + min: 0.1, + max: 1, + step: 0.01, + }, + }); + builder.addSliderInput({ path: 'segmentCount', name: t('radialbar.config.segment-count', 'Segments'), @@ -87,18 +99,6 @@ export const plugin = new PanelPlugin(RadialBarPanel) showIf: (options) => options.barShape === 'rounded' && options.segmentCount === 1, }); - builder.addSliderInput({ - path: 'barWidthFactor', - name: t('radialbar.config.bar-width', 'Bar width'), - category, - defaultValue: defaultOptions.barWidthFactor, - settings: { - min: 0.1, - max: 1, - step: 0.01, - }, - }); - builder.addBooleanSwitch({ path: 'sparkline', name: t('radialbar.config.sparkline', 'Show sparkline'), From 9deb30d88f3388e033fa18286d86f69626a8e259 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 18 Dec 2025 14:51:34 -0500 Subject: [PATCH 46/48] Sparkline: Add point annotations for some common calcs --- .../grafana-data/src/field/fieldDisplay.ts | 137 +++++++++++------- .../RadialGauge/RadialSparkline.tsx | 2 +- .../src/components/Sparkline/Sparkline.tsx | 10 +- .../src/components/Sparkline/utils.ts | 55 +++++-- 4 files changed, 129 insertions(+), 75 deletions(-) diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index 3d82f571926..3496f419395 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -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; } } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx index 2d6c45a14bf..4a52d5241d5 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx @@ -67,7 +67,7 @@ export const RadialSparkline = memo( return (
- +
); } diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx index c18b235e757..d1fb4f3b0e0 100644 --- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx +++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx @@ -14,18 +14,18 @@ export interface SparklineProps extends Themeable2 { height: number; config?: FieldConfig; sparkline: FieldSparkline; + showHighlights?: boolean; } -const SparklineFn: React.FC = memo((props) => { - const { sparkline, config: fieldConfig, theme, width, height } = props; - - const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, fieldConfig); +export const SparklineFn: React.FC = 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 ; }); diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts index be24eb6c4e8..8b347428167 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.ts @@ -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'; @@ -113,7 +115,7 @@ export function getYRange(alignedFrame: DataFrame): Range.MinMax { } // 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 +126,9 @@ const defaultConfig: GraphFieldConfig = { export const prepareSeries = ( sparkline: FieldSparkline, - fieldConfig?: FieldConfig + _theme: GrafanaTheme2, + fieldConfig?: FieldConfig, + _showHighlights?: boolean ): { frame: DataFrame; warning?: string } => { const frame = nullToValue(preparePlotFrame(sparkline, fieldConfig)); if (frame.fields.some((f) => f.values.length <= 1)) { @@ -136,16 +140,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 +235,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 +257,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, From b5655a0c20823909fd8fb914d5e192d8de7f03bc Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 18 Dec 2025 14:55:35 -0500 Subject: [PATCH 47/48] remove comment --- packages/grafana-ui/src/components/Sparkline/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts index 8b347428167..0c10dd35cca 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.ts @@ -114,7 +114,6 @@ export function getYRange(alignedFrame: DataFrame): Range.MinMax { return [roundedMin, roundedMax]; } -// TODO: #112977 enable highlight index const HIGHLIGHT_IDX_POINT_SIZE = 6; const defaultConfig: GraphFieldConfig = { From ee196f42d3e851f35d0297574d40f8eb3cea6953 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 18 Dec 2025 16:03:09 -0500 Subject: [PATCH 48/48] fix lint errors --- packages/grafana-ui/src/components/Sparkline/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts index 0c10dd35cca..c1402c4da2d 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.ts @@ -2,7 +2,7 @@ import { Range } from 'uplot'; import { applyNullInsertThreshold, - colorManipulator, + // colorManipulator, DataFrame, FieldConfig, FieldSparkline, @@ -23,7 +23,7 @@ import { VisibilityMode, ScaleDirection, ScaleOrientation, - FieldColorModeId, + // FieldColorModeId, } from '@grafana/schema'; import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';