diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index f59614acd53..6d6d05047d2 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, ReactNode } from 'react'; +import { useId, memo, HTMLAttributes, ReactNode, SVGProps } from 'react'; import { FieldDisplay } from '@grafana/data'; @@ -50,14 +50,13 @@ export const RadialArcPath = memo( }: RadialArcPathProps) => { const id = useId(); - const bgDivStyle: HTMLAttributes['style'] = { width: '100%', height: '100%' }; - if ('color' in rest) { - bgDivStyle.backgroundColor = rest.color; - } else { - bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape); - } + const isGradient = 'gradient' in rest; - const { radius, centerX, centerY, barWidth } = dimensions; + const { vizWidth, vizHeight, radius, centerX, centerY, barWidth } = dimensions; + const pad = Math.ceil(Math.max(2, barWidth / 2)); // pad to cover stroke caps and glow in Safari + const boxX = Math.round(centerX - radius - barWidth - pad); + const boxY = Math.round(centerY - radius - barWidth - pad); + const boxSize = Math.round((radius + barWidth) * 2 + pad * 2); const path = drawRadialArcPath(angle, arcLengthDeg, dimensions, roundedBars); @@ -72,9 +71,14 @@ export const RadialArcPath = memo( const dotRadius = endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2; + const bgDivStyle: HTMLAttributes['style'] = { width: boxSize, height: vizHeight, marginLeft: boxX }; + + const pathProps: SVGProps = {}; let barEndcapColors: [string, string] | undefined; let endpointMarks: ReactNode = null; - if ('gradient' in rest) { + if (isGradient) { + bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape); + if (endpointMarker && (rest.gradient?.length ?? 0) > 0) { switch (endpointMarker) { case 'point': @@ -115,25 +119,39 @@ export const RadialArcPath = memo( if (barEndcaps) { barEndcapColors = getBarEndcapColors(rest.gradient, fieldDisplay.display.percent); } + + pathProps.fill = 'none'; + pathProps.stroke = 'white'; + } else { + bgDivStyle.backgroundColor = rest.color; + + pathProps.fill = 'none'; + pathProps.stroke = rest.color; } + const pathEl = ( + + ); + return ( <> - {/* FIXME: optimize this by only using clippath + foreign obj for gradients */} - - - + {isGradient && ( + + + + {pathEl} + + + )} - -
- + {isGradient ? ( + +
+ + ) : ( + pathEl + )} {barEndcapColors?.[0] && } {barEndcapColors?.[1] && ( diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index 452dd394ab1..e60a3dfde31 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { useId } from 'react'; +import { useId, ReactNode } from 'react'; import { DisplayValueAlignmentFactors, FALLBACK_COLOR, FieldDisplay, GrafanaTheme2, TimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -107,14 +107,14 @@ export function RadialGauge(props: RadialGaugeProps) { const startAngle = shape === 'gauge' ? 250 : 0; const endAngle = shape === 'gauge' ? 110 : 360; - const defs: React.ReactNode[] = []; - const graphics: React.ReactNode[] = []; - let sparklineElement: React.ReactNode | null = null; + const defs: ReactNode[] = []; + const graphics: ReactNode[] = []; + let sparklineElement: ReactNode | null = null; for (let barIndex = 0; barIndex < values.length; barIndex++) { const displayValue = values[barIndex]; const { angle, angleRange } = getValueAngleForValue(displayValue, startAngle, endAngle); - const gradientStops = buildGradientColors(gradient, theme, displayValue); + const gradientStops = gradient ? buildGradientColors(theme, displayValue) : undefined; const color = displayValue.display.color ?? FALLBACK_COLOR; const dimensions = calculateDimensions( width, @@ -131,7 +131,9 @@ export function RadialGauge(props: RadialGaugeProps) { // 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 spotlightGradientRef = endpointMarker === 'glow' ? `url(#${spotlightGradientId})` : undefined; const glowFilterId = `glow-${gaugeId}`; + const glowFilterRef = glowBar ? `url(#${glowFilterId})` : undefined; if (endpointMarker === 'glow') { defs.push( @@ -154,7 +156,7 @@ export function RadialGauge(props: RadialGaugeProps) { fieldDisplay={displayValue} angleRange={angleRange} startAngle={startAngle} - glowFilter={`url(#${glowFilterId})`} + glowFilter={glowFilterRef} segmentCount={segmentCount} segmentSpacing={segmentSpacing} shape={shape} @@ -170,8 +172,8 @@ export function RadialGauge(props: RadialGaugeProps) { angleRange={angleRange} startAngle={startAngle} roundedBars={roundedBars} - glowFilter={`url(#${glowFilterId})`} - endpointMarkerGlowFilter={`url(#${spotlightGradientId})`} + glowFilter={glowFilterRef} + endpointMarkerGlowFilter={spotlightGradientRef} shape={shape} gradient={gradientStops} fieldDisplay={displayValue} @@ -183,7 +185,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) { @@ -234,7 +236,7 @@ export function RadialGauge(props: RadialGaugeProps) { endAngle={endAngle} angleRange={angleRange} roundedBars={roundedBars} - glowFilter={`url(#${glowFilterId})`} + glowFilter={glowFilterRef} shape={shape} gradient={gradientStops} /> @@ -260,7 +262,7 @@ export function RadialGauge(props: RadialGaugeProps) { const body = ( <> - {defs} + {defs.length > 0 && {defs}} {graphics} {sparklineElement} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx index 69ab16e450e..dc094b7261c 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx @@ -1,4 +1,3 @@ -import { css } from '@emotion/css'; import { memo } from 'react'; import { @@ -9,7 +8,6 @@ import { GrafanaTheme2, } from '@grafana/data'; -import { useStyles2 } from '../../themes/ThemeContext'; import { calculateFontSize } from '../../utils/measureText'; import { RadialShape, RadialTextMode, RadialGaugeDimensions } from './types'; @@ -50,7 +48,6 @@ export const RadialText = memo( valueManualFontSize, nameManualFontSize, }: RadialTextProps) => { - const styles = useStyles2(getStyles); const { centerX, centerY, radius, barWidth } = dimensions; if (textMode === 'none') { @@ -106,10 +103,9 @@ export const RadialText = memo( 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; + let yOffset = valueFontSize / 4; 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; @@ -126,15 +122,12 @@ export const RadialText = memo( y={valueY} fontSize={valueFontSize} fill={theme.colors.text.primary} - className={styles.text} textAnchor="middle" - dominantBaseline="middle" + dominantBaseline="text-bottom" > {displayValue.prefix ?? ''} {displayValue.text} - - {displayValue.suffix ?? ''} - + {displayValue.suffix ?? ''} )} {showName && ( @@ -143,7 +136,7 @@ export const RadialText = memo( x={centerX} y={nameY} textAnchor="middle" - dominantBaseline="middle" + dominantBaseline="text-bottom" fill={nameColor} > {displayValue.title} @@ -155,9 +148,3 @@ export const RadialText = memo( ); RadialText.displayName = 'RadialText'; - -const getStyles = (_theme: GrafanaTheme2) => ({ - text: css({ - verticalAlign: 'bottom', - }), -}); 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 index db4c1c40882..9d12b97a6c3 100644 --- a/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap +++ b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap @@ -1,17 +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 center x and y 1`] = `"M 150 120 A 80 80 0 1 1 149.98603736605492 120.00000121846968"`; -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 half arc 1`] = `"M 100 20 A 80 80 0 0 1 100 180"`; -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 bar width 1`] = `"M 100 20 A 80 80 0 0 1 100 180"`; -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 narrow radius 1`] = `"M 100 50 A 50 50 0 0 1 100 150"`; -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 quarter arc 1`] = `"M 100 20 A 80 80 0 0 1 180 100"`; -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 rounded bars 1`] = `"M 100 20 A 80 80 0 1 1 20 100.00000000000001"`; -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 three quarter arc 1`] = `"M 100 20 A 80 80 0 1 1 20 100.00000000000001"`; -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"`; +exports[`RadialGauge utils drawRadialArcPath should draw correct path for wide bar width 1`] = `"M 100 20 A 80 80 0 0 1 100 180"`; diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts index 321e95bb921..36a4c70d619 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts @@ -1,6 +1,6 @@ import { defaultsDeep } from 'lodash'; -import { createTheme, FALLBACK_COLOR, Field, FieldDisplay, FieldType, ThresholdsMode } from '@grafana/data'; +import { createTheme, Field, FieldDisplay, FieldType, ThresholdsMode } from '@grafana/data'; import { FieldColorModeId } from '@grafana/schema'; import { @@ -50,35 +50,9 @@ describe('RadialGauge color utils', () => { }, }); - it('should return the baseColor if gradient is false-y', () => { - expect( - buildGradientColors(false, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000') - ).toEqual([ - { color: '#FF0000', percent: 0 }, - { color: '#FF0000', percent: 1 }, - ]); - - expect( - buildGradientColors(undefined, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000') - ).toEqual([ - { color: '#FF0000', percent: 0 }, - { color: '#FF0000', percent: 1 }, - ]); - }); - - it('uses the fallback color if no baseColor is set', () => { - expect(buildGradientColors(false, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)))).toEqual( - [ - { color: FALLBACK_COLOR, percent: 0 }, - { color: FALLBACK_COLOR, percent: 1 }, - ] - ); - }); - it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => { expect( buildGradientColors( - true, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds), { view: { getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: '#444444' }))) }, @@ -89,14 +63,13 @@ describe('RadialGauge color utils', () => { it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => { expect( - buildGradientColors(true, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds)), '#FF0000') + buildGradientColors(createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds)), '#FF0000') ).toMatchSnapshot(); }); it('should return gradient colors for continuous color modes', () => { expect( buildGradientColors( - true, createTheme(), buildFieldDisplay(createField(FieldColorModeId.ContinuousCividis)), '#00FF00' @@ -107,7 +80,6 @@ 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( - true, createTheme({ colors: { mode } }), buildFieldDisplay(createField(FieldColorModeId.ContinuousBlues)) ) @@ -117,7 +89,6 @@ describe('RadialGauge color utils', () => { it.each(['dark', 'light'] as const)('should return gradient colors for fixed color mode in %s theme', (mode) => { expect( buildGradientColors( - true, createTheme({ colors: { mode } }), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#442299' diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts index 61160a9f826..3eb81c10899 100644 --- a/packages/grafana-ui/src/components/RadialGauge/colors.ts +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -7,18 +7,10 @@ import { GradientStop, RadialShape } from './types'; import { getFieldConfigMinMax, getFieldDisplayProcessor, getValuePercentageForValue } from './utils'; export function buildGradientColors( - gradient = false, theme: GrafanaTheme2, fieldDisplay: FieldDisplay, baseColor = fieldDisplay.display.color ?? FALLBACK_COLOR ): GradientStop[] { - if (!gradient) { - return [ - { color: baseColor, percent: 0 }, - { color: baseColor, percent: 1 }, - ]; - } - const colorMode = getFieldColorMode(fieldDisplay.field.color?.mode); // thresholds get special handling diff --git a/packages/grafana-ui/src/components/RadialGauge/effects.tsx b/packages/grafana-ui/src/components/RadialGauge/effects.tsx index 53a255d4a45..2d3ae3daf21 100644 --- a/packages/grafana-ui/src/components/RadialGauge/effects.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/effects.tsx @@ -2,14 +2,20 @@ import { colorManipulator, GrafanaTheme2 } from '@grafana/data'; import { RadialGaugeDimensions } from './types'; +// some utility transparent white colors for gradients +const TRANSPARENT_WHITE = '#ffffff00'; +const MOSTLY_TRANSPARENT_WHITE = '#ffffff88'; +const MOSTLY_OPAQUE_WHITE = '#ffffffbb'; +const OPAQUE_WHITE = '#ffffff'; + +const MIN_GLOW_SIZE = 0.75; +const GLOW_FACTOR = 0.08; + export interface GlowGradientProps { id: string; barWidth: number; } -const MIN_GLOW_SIZE = 0.75; -const GLOW_FACTOR = 0.08; - export function GlowGradient({ id, barWidth }: GlowGradientProps) { // 0.75 is the minimum glow size, and it scales with bar width const glowSize = MIN_GLOW_SIZE + barWidth * GLOW_FACTOR; @@ -27,16 +33,6 @@ export function GlowGradient({ id, barWidth }: GlowGradientProps) { const CENTER_GLOW_OPACITY = 0.25; -export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) { - const transparentColor = colorManipulator.alpha(color, CENTER_GLOW_OPACITY); - return ( - - - - - ); -} - export interface CenterGlowProps { dimensions: RadialGaugeDimensions; gaugeId: string; @@ -52,7 +48,7 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps - + @@ -62,19 +58,15 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps ); } -export function SpotlightGradient({ - id, - dimensions, - roundedBars, - angle, - theme, -}: { +interface SpotlightGradientProps { id: string; dimensions: RadialGaugeDimensions; angle: number; roundedBars: boolean; theme: GrafanaTheme2; -}) { +} + +export function SpotlightGradient({ id, dimensions, roundedBars, angle, theme }: SpotlightGradientProps) { if (theme.isLight) { return null; } @@ -88,9 +80,9 @@ export function SpotlightGradient({ return ( - - - {roundedBars && } + + + {roundedBars && } ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/types.ts b/packages/grafana-ui/src/components/RadialGauge/types.ts index cc233dd524c..111c7aef7d6 100644 --- a/packages/grafana-ui/src/components/RadialGauge/types.ts +++ b/packages/grafana-ui/src/components/RadialGauge/types.ts @@ -2,6 +2,8 @@ export type RadialTextMode = 'auto' | 'value_and_name' | 'value' | 'name' | 'non export type RadialShape = 'circle' | 'gauge'; export interface RadialGaugeDimensions { + vizHeight: number; + vizWidth: number; margin: number; radius: number; centerX: number; diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.test.ts b/packages/grafana-ui/src/components/RadialGauge/utils.test.ts index b9b2e4ad8f3..70ee54a6337 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.test.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.test.ts @@ -283,7 +283,9 @@ describe('RadialGauge utils', () => { }); describe('drawRadialArcPath', () => { - const defaultDims: RadialGaugeDimensions = Object.freeze({ + const defaultDims = Object.freeze({ + vizHeight: 220, + vizWidth: 220, centerX: 100, centerY: 100, radius: 80, @@ -297,7 +299,7 @@ describe('RadialGauge utils', () => { scaleLabelsSpacing: 0, scaleLabelsRadius: 0, gaugeBottomY: 0, - }); + }) satisfies RadialGaugeDimensions; it.each([ { description: 'quarter arc', startAngle: 0, endAngle: 90 }, @@ -324,11 +326,6 @@ describe('RadialGauge utils', () => { expect(drawRadialArcPath(0, 360, defaultDims)).toEqual(drawRadialArcPath(0, 359.99, defaultDims)); expect(drawRadialArcPath(0, 380, defaultDims)).toEqual(drawRadialArcPath(0, 380, defaultDims)); }); - - it('should return empty string if inner radius collapses to zero or below', () => { - const smallRadiusDims = { ...defaultDims, radius: 5, barWidth: 20 }; - expect(drawRadialArcPath(0, 180, smallRadiusDims)).toBe(''); - }); }); }); @@ -341,7 +338,9 @@ describe('RadialGauge utils', () => { describe('getOptimalSegmentCount', () => { it('should adjust segment count based on dimensions and spacing', () => { - const dimensions: RadialGaugeDimensions = { + const dimensions = { + vizHeight: 220, + vizWidth: 220, centerX: 100, centerY: 100, radius: 80, @@ -355,7 +354,7 @@ describe('RadialGauge utils', () => { scaleLabelsSpacing: 0, scaleLabelsRadius: 0, gaugeBottomY: 0, - }; + } satisfies RadialGaugeDimensions; 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 e26cf5eed2a..a18efc42699 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.ts @@ -155,6 +155,8 @@ export function calculateDimensions( } return { + vizWidth: width, + vizHeight: height, margin, gaugeBottomY: centerY + belowCenterY, radius: innerRadius, @@ -185,7 +187,7 @@ export function drawRadialArcPath( dimensions: RadialGaugeDimensions, roundedBars?: boolean ): string { - const { radius, centerX, centerY, barWidth } = dimensions; + const { radius, centerX, centerY } = dimensions; // For some reason a 100% full arc cannot be rendered if (endAngle >= 360) { @@ -197,66 +199,12 @@ export function drawRadialArcPath( const largeArc = endAngle > 180 ? 1 : 0; - const outerR = radius + barWidth / 2; - const innerR = Math.max(0, radius - barWidth / 2); - if (innerR <= 0) { - return ''; // cannot draw arc with 0 inner radius - } + 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); - // get points for both an inner and outer arc. we draw - // the arc entirely with a path's fill instead of using stroke - // so that it can be used as a clip-path. - const ox1 = centerX + outerR * Math.cos(startRadians); - const oy1 = centerY + outerR * Math.sin(startRadians); - const ox2 = centerX + outerR * Math.cos(endRadians); - const oy2 = centerY + outerR * Math.sin(endRadians); - - const ix1 = centerX + innerR * Math.cos(startRadians); - const iy1 = centerY + innerR * Math.sin(startRadians); - const ix2 = centerX + innerR * Math.cos(endRadians); - const iy2 = centerY + innerR * Math.sin(endRadians); - - // calculate the cap width in case we're drawing rounded bars - const capR = barWidth / 2; - - const pathParts = [ - // start at outer start - 'M', - ox1, - oy1, - // outer arc from start to end (clockwise) - 'A', - outerR, - outerR, - 0, - largeArc, - 1, - ox2, - oy2, - ]; - - if (roundedBars) { - // rounded end cap: small arc connecting outer end to inner end - pathParts.push('A', capR, capR, 0, 0, 1, ix2, iy2); - } else { - // straight line to inner end (square butt) - pathParts.push('L', ix2, iy2); - } - - // inner arc from end back to start (counter-clockwise) - pathParts.push('A', innerR, innerR, 0, largeArc, 0, ix1, iy1); - - if (roundedBars) { - // rounded start cap: small arc connecting inner start back to outer start - pathParts.push('A', capR, capR, 0, 0, 1, ox1, oy1); - } else { - // straight line back to outer start (square butt) - pathParts.push('L', ox1, oy1); - } - - pathParts.push('Z'); - - return pathParts.join(' '); + return ['M', x1, y1, 'A', radius, radius, 0, largeArc, 1, x2, y2].join(' '); } export function getAngleBetweenSegments(segmentSpacing: number, segmentCount: number, range: number) {