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'),