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