From b300bd8b85fa84b2bdc31b1852acccbb792b32ff Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Dec 2025 23:13:08 -0500 Subject: [PATCH] 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(' '); +}