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 c0f8481a7f5..af5eb1b7e3c 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
@@ -29,6 +29,7 @@ export interface Options extends common.SingleStatBaseOptions {
barWidthFactor: number;
effects: GaugePanelEffects;
endpointMarker?: ('point' | 'glow' | 'none');
+ neutral?: number;
segmentCount: number;
segmentSpacing: number;
shape: ('circle' | 'gauge');
diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx
index 719ec52c625..392b0a4981c 100644
--- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx
+++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx
@@ -1,4 +1,6 @@
-import { FALLBACK_COLOR, FieldDisplay } from '@grafana/data';
+import { useMemo } from 'react';
+
+import { colorManipulator, FALLBACK_COLOR, FieldDisplay } from '@grafana/data';
import { useTheme2 } from '../../themes/ThemeContext';
@@ -6,7 +8,6 @@ import { RadialArcPath } from './RadialArcPath';
import { RadialShape, RadialGaugeDimensions, GradientStop } from './types';
export interface RadialBarProps {
- angle: number;
angleRange: number;
dimensions: RadialGaugeDimensions;
fieldDisplay: FieldDisplay;
@@ -15,11 +16,12 @@ export interface RadialBarProps {
endpointMarker?: 'point' | 'glow';
shape: RadialShape;
startAngle: number;
+ startValueAngle: number;
+ endValueAngle: number;
glowFilter?: string;
endpointMarkerGlowFilter?: string;
}
export function RadialBar({
- angle,
angleRange,
dimensions,
fieldDisplay,
@@ -28,26 +30,45 @@ export function RadialBar({
endpointMarker,
shape,
startAngle,
+ startValueAngle,
+ endValueAngle,
glowFilter,
endpointMarkerGlowFilter,
}: RadialBarProps) {
const theme = useTheme2();
const colorProps = gradient ? { gradient } : { color: fieldDisplay.display.color ?? FALLBACK_COLOR };
+ const trackColor = useMemo(
+ () => colorManipulator.onBackground(theme.colors.action.hover, theme.colors.background.primary).toHexString(),
+ [theme]
+ );
+
return (
<>
- {/** Track */}
+ {/** Track before value */}
+ {startValueAngle !== 0 && (
+
+ )}
+ {/** Track after value */}
{/** The colored bar */}
>
diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx
index b51cb4ce2f1..61d10044802 100644
--- a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx
+++ b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx
@@ -11,6 +11,7 @@ import {
getFieldConfigMinMax,
getFieldDisplayProcessor,
getOptimalSegmentCount,
+ getValuePercentageForValue,
} from './utils';
export interface RadialBarSegmentedProps {
@@ -18,6 +19,8 @@ export interface RadialBarSegmentedProps {
dimensions: RadialGaugeDimensions;
angleRange: number;
startAngle: number;
+ startValueAngle: number;
+ endValueAngle: number;
glowFilter?: string;
segmentCount: number;
segmentSpacing: number;
@@ -36,22 +39,24 @@ export const RadialBarSegmented = memo(
segmentCount,
segmentSpacing,
shape,
+ startValueAngle,
+ endValueAngle,
}: RadialBarSegmentedProps) => {
const theme = useTheme2();
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;
const displayProcessor = getFieldDisplayProcessor(fieldDisplay);
for (let i = 0; i < segmentCountAdjusted; i++) {
- const angleValue = min + ((max - min) / segmentCountAdjusted) * i;
- const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01;
- const segmentColor =
- angleValue >= value ? theme.colors.border.medium : (displayProcessor(angleValue).color ?? FALLBACK_COLOR);
- const colorProps = angleValue < value && gradient ? { gradient } : { color: segmentColor };
+ const value = min + ((max - min) / segmentCountAdjusted) * i;
+ const segmentAngle = getValuePercentageForValue(fieldDisplay, value) * angleRange;
+ const isTrack = segmentAngle < startValueAngle || segmentAngle > startValueAngle + endValueAngle;
+ const segmentStartAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01;
+ const segmentColor = isTrack ? theme.colors.border.medium : (displayProcessor(value).color ?? FALLBACK_COLOR);
+ const colorProps = !isTrack && 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 e60a3dfde31..7fc3ae4a383 100644
--- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx
+++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx
@@ -17,7 +17,7 @@ import { ThresholdsBar } from './ThresholdsBar';
import { buildGradientColors } from './colors';
import { GlowGradient, MiddleCircleGlow, SpotlightGradient } from './effects';
import { RadialShape, RadialTextMode } from './types';
-import { calculateDimensions, getValueAngleForValue } from './utils';
+import { calculateDimensions, getFieldConfigMinMax, getValueAngleForValue } from './utils';
export interface RadialGaugeProps {
values: FieldDisplay[];
@@ -67,6 +67,11 @@ export interface RadialGaugeProps {
/** Specify which text should be visible */
textMode?: RadialTextMode;
showScaleLabels?: boolean;
+ /**
+ * If set, the gauge will use the neutral value instead of the min value as the starting point for a gauge.
+ * this is most useful when you need to show positive and negative values on a gauge.
+ */
+ neutral?: number;
/** For data links */
onClick?: React.MouseEventHandler;
timeRange?: TimeRange;
@@ -91,6 +96,7 @@ export function RadialGauge(props: RadialGaugeProps) {
roundedBars = true,
thresholdsBar = false,
showScaleLabels = false,
+ neutral: rawNeutral,
endpointMarker,
onClick,
values,
@@ -113,7 +119,15 @@ 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 [min, max] = getFieldConfigMinMax(displayValue);
+ const neutral = typeof rawNeutral === 'number' ? Math.min(Math.max(min, rawNeutral), max) : undefined;
+ const { startValueAngle, endValueAngle, angleRange } = getValueAngleForValue(
+ displayValue,
+ startAngle,
+ endAngle,
+ neutral
+ );
+
const gradientStops = gradient ? buildGradientColors(theme, displayValue) : undefined;
const color = displayValue.display.color ?? FALLBACK_COLOR;
const dimensions = calculateDimensions(
@@ -140,7 +154,7 @@ export function RadialGauge(props: RadialGaugeProps) {
{
const fieldDisplay = createFieldDisplay(50, 0, 100);
const result = getValueAngleForValue(fieldDisplay, 0, 360);
- expect(result.angle).toBe(180); // 50% of 360°
+ expect(result.startValueAngle).toBe(0);
+ expect(result.endValueAngle).toBe(180); // 50% of 360°
expect(result.angleRange).toBe(360);
});
@@ -241,7 +242,8 @@ describe('RadialGauge utils', () => {
const fieldDisplay = createFieldDisplay(50, 0, 100);
const result = getValueAngleForValue(fieldDisplay, 90, 270);
- expect(result.angle).toBe(135); // 50% of 360° range
+ expect(result.startValueAngle).toBe(0);
+ expect(result.endValueAngle).toBe(135); // 50% of 360° range
expect(result.angleRange).toBe(270);
});
@@ -249,28 +251,28 @@ describe('RadialGauge utils', () => {
const fieldDisplay = createFieldDisplay(150, 0, 100); // value exceeds max
const result = getValueAngleForValue(fieldDisplay, 0, 360);
- expect(result.angle).toBe(360); // clamped to angleRange
+ expect(result.endValueAngle).toBe(360); // clamped to angleRange
});
it('should handle minimum values', () => {
const fieldDisplay = createFieldDisplay(0, 0, 100);
const result = getValueAngleForValue(fieldDisplay, 0, 360);
- expect(result.angle).toBe(0);
+ expect(result.endValueAngle).toBe(0);
});
it('should handle maximum values', () => {
const fieldDisplay = createFieldDisplay(100, 0, 100);
const result = getValueAngleForValue(fieldDisplay, 0, 360);
- expect(result.angle).toBe(360);
+ expect(result.endValueAngle).toBe(360);
});
it('should handle values lower than min', () => {
const fieldDisplay = createFieldDisplay(-50, 0, 100);
const result = getValueAngleForValue(fieldDisplay, 240, 120);
- expect(result.angle).toBe(0);
+ expect(result.endValueAngle).toBe(0);
});
it('should handle values higher than max', () => {
@@ -278,8 +280,10 @@ describe('RadialGauge utils', () => {
const result = getValueAngleForValue(fieldDisplay, 240, 120);
// Expect the angle to be clamped to the maximum range
- expect(result.angle).toBe(240);
+ expect(result.endValueAngle).toBe(240);
});
+
+ // TODO add tests for neutral handling once implemented
});
describe('drawRadialArcPath', () => {
diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.ts b/packages/grafana-ui/src/components/RadialGauge/utils.ts
index a18efc42699..b2cef2b21ba 100644
--- a/packages/grafana-ui/src/components/RadialGauge/utils.ts
+++ b/packages/grafana-ui/src/components/RadialGauge/utils.ts
@@ -28,19 +28,30 @@ export function getValueAngleForValue(
fieldDisplay: FieldDisplay,
startAngle: number,
endAngle: number,
- value = fieldDisplay.display.numeric
+ neutral?: number
) {
const angleRange = (360 % (startAngle === 0 ? 1 : startAngle)) + endAngle;
+ const value = fieldDisplay.display.numeric;
- let angle = getValuePercentageForValue(fieldDisplay, value) * angleRange;
+ const valueAngle = getValuePercentageForValue(fieldDisplay, value) * angleRange;
- if (angle > angleRange) {
- angle = angleRange;
- } else if (angle < 0) {
- angle = 0;
+ let endValueAngle = valueAngle;
+
+ let startValueAngle = 0;
+ if (typeof neutral === 'number') {
+ const neutralAngle = getValuePercentageForValue(fieldDisplay, neutral) * angleRange;
+ if (neutralAngle <= valueAngle) {
+ startValueAngle = neutralAngle;
+ endValueAngle = valueAngle - neutralAngle;
+ } else {
+ startValueAngle = valueAngle;
+ endValueAngle = neutralAngle - valueAngle;
+ }
}
- return { angleRange, angle };
+ const clampedEndValueAngle = Math.min(Math.max(endValueAngle, 0), angleRange);
+
+ return { angleRange, startValueAngle, endValueAngle: clampedEndValueAngle };
}
/**
diff --git a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx
index 42c70511977..979dcbe3db4 100644
--- a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx
+++ b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx
@@ -33,26 +33,27 @@ export function RadialBarPanel({
return (
);
}
diff --git a/public/app/plugins/panel/radialbar/module.tsx b/public/app/plugins/panel/radialbar/module.tsx
index f7afa0ef2c9..93c40c4f158 100644
--- a/public/app/plugins/panel/radialbar/module.tsx
+++ b/public/app/plugins/panel/radialbar/module.tsx
@@ -115,6 +115,17 @@ export const plugin = new PanelPlugin(RadialBarPanel)
defaultValue: defaultOptions.textMode,
});
+ builder.addNumberInput({
+ path: 'neutral',
+ name: t('radialbar.config.neutral.title', 'Neutral value'),
+ description: t('radialbar.config.neutral.description', 'Leave empty to use Min as neutral point'),
+ category,
+ settings: {
+ placeholder: t('radialbar.config.neutral.placeholder', 'none'),
+ step: 1,
+ },
+ });
+
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 6e5fd7eec21..5f95de9428d 100644
--- a/public/app/plugins/panel/radialbar/panelcfg.cue
+++ b/public/app/plugins/panel/radialbar/panelcfg.cue
@@ -42,7 +42,8 @@ composableKinds: PanelCfg: {
barWidthFactor: number | *0.5
barShape: "flat" | "rounded" | *"flat"
endpointMarker?: "point" | "glow" | "none" | *"point"
- textMode?: "auto" | "value_and_name" | "value" | "name" | "none" | *"auto"
+ textMode?: "auto" | "value_and_name" | "value" | "name" | "none" | *"auto"
+ neutral?: number
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 594747136ef..c51c71d23c9 100644
--- a/public/app/plugins/panel/radialbar/panelcfg.gen.ts
+++ b/public/app/plugins/panel/radialbar/panelcfg.gen.ts
@@ -27,6 +27,7 @@ export interface Options extends common.SingleStatBaseOptions {
barWidthFactor: number;
effects: GaugePanelEffects;
endpointMarker?: ('point' | 'glow' | 'none');
+ neutral?: number;
segmentCount: number;
segmentSpacing: number;
shape: ('circle' | 'gauge');
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 2174ff0adbc..6b6d360ea07 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -12528,6 +12528,11 @@
"endpoint-marker-glow": "Glow",
"endpoint-marker-none": "None",
"endpoint-marker-point": "Point",
+ "neutral": {
+ "description": "Leave empty to use Min as neutral point",
+ "placeholder": "none",
+ "title": "Neutral value"
+ },
"segment-count": "Segments",
"segment-spacing": "Segment spacing",
"shape": "Style",