Refactorings
CodeQL checks / Detect whether code changed (push) Waiting to run Details
CodeQL checks / Analyze (actions) (push) Blocked by required conditions Details
CodeQL checks / Analyze (go) (push) Blocked by required conditions Details
CodeQL checks / Analyze (javascript) (push) Blocked by required conditions Details

This commit is contained in:
Torkel Ödegaard 2025-10-07 15:29:37 +02:00
parent b2447fa3d9
commit 73e3122621
7 changed files with 215 additions and 206 deletions

View File

@ -7,15 +7,14 @@ import { GaugeDimensions } from './utils';
interface GradientDefProps {
fieldDisplay: FieldDisplay;
index: number;
id: string;
theme: GrafanaTheme2;
gaugeId: string;
gradient: RadialGradientMode;
dimensions: GaugeDimensions;
shape: RadialShape;
}
export function GradientDef({ fieldDisplay, index, theme, gaugeId, gradient, dimensions, shape }: GradientDefProps) {
export function GradientDef({ fieldDisplay, id, theme, gradient, dimensions, shape }: GradientDefProps) {
const colorModeId = fieldDisplay.field.color?.mode;
const valuePercent = fieldDisplay.display.percent ?? 0;
const colorMode = getFieldColorMode(colorModeId);
@ -39,7 +38,7 @@ export function GradientDef({ fieldDisplay, index, theme, gaugeId, gradient, dim
y1="0"
x2={x2}
y2={y2}
id={getGradientId(gaugeId, index)}
id={id}
gradientUnits="userSpaceOnUse"
gradientTransform={transform}
>
@ -59,7 +58,7 @@ export function GradientDef({ fieldDisplay, index, theme, gaugeId, gradient, dim
y1="0"
x2={x2}
y2={y2}
id={getGradientId(gaugeId, index)}
id={id}
gradientUnits="userSpaceOnUse"
gradientTransform={transform}
>
@ -83,7 +82,7 @@ export function GradientDef({ fieldDisplay, index, theme, gaugeId, gradient, dim
const count = colors.length;
return (
<linearGradient x1="0" y1="1" x2={1 / valuePercent} y2="1" id={getGradientId(gaugeId, index)}>
<linearGradient x1="0" y1="1" x2={1 / valuePercent} y2="1" id={id}>
{colors.map((stopColor, i) => (
<stop key={i} offset={`${(i / (count - 1)).toFixed(2)}`} stopColor={stopColor} stopOpacity={1} />
))}
@ -91,7 +90,7 @@ export function GradientDef({ fieldDisplay, index, theme, gaugeId, gradient, dim
);
} else {
return (
<linearGradient x1="0" y1="1" x2={0} y2="1" id={getGradientId(gaugeId, index)}>
<linearGradient x1="0" y1="1" x2={0} y2="1" id={id}>
<stop stopColor={fieldDisplay.display.color ?? 'gray'} stopOpacity={1} />
</linearGradient>
);
@ -117,7 +116,3 @@ export function GradientDef({ fieldDisplay, index, theme, gaugeId, gradient, dim
return null;
}
export function getGradientId(gaugeId: string, index: number) {
return `radial-gauge-${gaugeId}-${index}`;
}

View File

@ -1,42 +1,38 @@
import { FieldDisplay, GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { useTheme2 } from '../../themes/ThemeContext';
import { GaugeDimensions, getValueAngleForValue } from './utils';
import { GaugeDimensions } from './utils';
export interface RadialBarProps {
gaugeId: string;
dimensions: GaugeDimensions;
fieldDisplay: FieldDisplay;
angleRange: number;
angle: number;
startAngle: number;
endAngle: number;
color: string;
roundedBars?: boolean;
spotlight?: boolean;
glow?: boolean;
spotlightStroke: string;
glowFilter?: string;
}
export function RadialBar({
dimensions,
fieldDisplay,
gaugeId,
angleRange,
angle,
startAngle,
endAngle,
color,
roundedBars,
spotlight,
glow,
spotlightStroke,
glowFilter,
}: RadialBarProps) {
const theme = useTheme2();
const { range, angle } = getValueAngleForValue(fieldDisplay, startAngle, endAngle);
const trackStart = startAngle + angle;
const trackLength = range - angle;
const trackLength = angleRange - angle;
return (
<>
<g>
{/** Track */}
<RadialArcPath
gaugeId={gaugeId}
angle={trackLength}
dimensions={dimensions}
startAngle={trackStart}
@ -46,41 +42,38 @@ export function RadialBar({
/>
{/** The colored bar */}
<RadialArcPath
gaugeId={gaugeId}
angle={angle}
dimensions={dimensions}
startAngle={startAngle}
color={color}
roundedBars={roundedBars}
spotlight={spotlight}
glow={glow}
spotlightStroke={spotlightStroke}
glowFilter={glowFilter}
theme={theme}
/>
</>
</g>
);
}
export interface RadialArcPathProps {
gaugeId: string;
angle: number;
startAngle: number;
dimensions: GaugeDimensions;
color: string;
roundedBars?: boolean;
spotlight?: boolean;
glow?: boolean;
spotlightStroke?: string;
glowFilter?: string;
theme: GrafanaTheme2;
}
export function RadialArcPath({
gaugeId,
startAngle,
dimensions,
angle,
color,
roundedBars,
spotlight,
glow,
spotlightStroke,
glowFilter,
theme,
}: RadialArcPathProps) {
let { radius, centerX, centerY, barWidth } = dimensions;
@ -114,17 +107,17 @@ export function RadialArcPath({
strokeLinecap={roundedBars ? 'round' : 'butt'}
strokeWidth={barWidth}
strokeDasharray="0"
filter={glow ? `url(#glow-${gaugeId})` : undefined}
filter={glowFilter}
/>
{spotlight && angle > 8 && (
{spotlightStroke && angle > 8 && (
<SpotlightSquareEffect
radius={radius}
centerX={centerX}
centerY={centerY}
angleRadian={endRadians}
barWidth={barWidth}
glow={glow}
gaugeId={gaugeId}
glowFilter={glowFilter}
spotlightStroke={spotlightStroke}
theme={theme}
roundedBars={roundedBars}
/>
@ -139,8 +132,8 @@ interface SpotlightEffectProps {
centerY: number;
angleRadian: number;
barWidth: number;
glow?: boolean;
gaugeId: string;
glowFilter?: string;
spotlightStroke: string;
theme: GrafanaTheme2;
roundedBars?: boolean;
}
@ -151,8 +144,8 @@ function SpotlightSquareEffect({
centerY,
angleRadian,
barWidth,
glow,
gaugeId,
glowFilter,
spotlightStroke,
roundedBars,
}: SpotlightEffectProps) {
let x1 = centerX + radius * Math.cos(angleRadian - 0.2);
@ -167,9 +160,9 @@ function SpotlightSquareEffect({
d={path}
fill="none"
strokeWidth={barWidth}
stroke={`url(#spotlight-${gaugeId})`}
stroke={spotlightStroke}
strokeLinecap={roundedBars ? 'round' : 'butt'}
filter={glow ? `url(#glow-${gaugeId})` : undefined}
filter={glowFilter}
/>
);
}

View File

@ -3,30 +3,27 @@ import { DisplayProcessor, FieldDisplay } from '@grafana/data';
import { useTheme2 } from '../../themes/ThemeContext';
import { RadialGradientMode } from './RadialGauge';
import { GaugeDimensions, getValueAngleForValue } from './utils';
import { GaugeDimensions } from './utils';
export interface RadialBarSegmentedProps {
gaugeId: string;
fieldDisplay: FieldDisplay;
dimensions: GaugeDimensions;
angleRange: number;
startAngle: number;
endAngle: number;
color: string;
spotlight?: boolean;
glow?: boolean;
glowFilter?: string;
segmentCount: number;
segmentSpacing: number;
displayProcessor: DisplayProcessor;
gradient: RadialGradientMode;
}
export function RadialBarSegmented({
gaugeId,
fieldDisplay,
dimensions,
startAngle,
endAngle,
angleRange,
color,
glow,
glowFilter,
segmentCount,
segmentSpacing,
displayProcessor,
@ -35,8 +32,7 @@ export function RadialBarSegmented({
const segments: React.ReactNode[] = [];
const theme = useTheme2();
const { range } = getValueAngleForValue(fieldDisplay, startAngle, endAngle);
const segmentCountAdjusted = getOptimalSegmentCount(dimensions, segmentSpacing, segmentCount, range);
const segmentCountAdjusted = getOptimalSegmentCount(dimensions, segmentSpacing, segmentCount, angleRange);
const min = fieldDisplay.field.min ?? 0;
const max = fieldDisplay.field.max ?? 100;
@ -54,41 +50,38 @@ export function RadialBarSegmented({
for (let i = 0; i < segmentCountAdjusted; i++) {
const angleValue = ((max - min) / segmentCountAdjusted) * i;
const angleColor = getColorForValue(angleValue);
const segmentAngle = startAngle + (range / segmentCountAdjusted) * i + 0.01;
const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01;
const segmentColor = angleValue > value ? theme.colors.action.hover : angleColor;
segments.push(
<RadialSegmentArcPath
gaugeId={gaugeId}
angle={segmentAngle}
dimensions={dimensions}
color={segmentColor}
glow={glow}
glowFilter={glowFilter}
segmentSpacing={segmentSpacing}
arcLengthDeg={range / segmentCountAdjusted}
arcLengthDeg={angleRange / segmentCountAdjusted}
/>
);
}
return segments;
return <g>{segments}</g>;
}
export interface RadialSegmentProps {
gaugeId: string;
angle: number;
dimensions: GaugeDimensions;
color: string;
glow?: boolean;
glowFilter?: string;
segmentSpacing: number;
arcLengthDeg: number;
}
export function RadialSegmentArcPath({
gaugeId,
angle,
dimensions,
color,
glow,
glowFilter,
segmentSpacing,
arcLengthDeg,
}: RadialSegmentProps) {
@ -121,7 +114,7 @@ export function RadialSegmentArcPath({
strokeLinecap={'butt'}
strokeWidth={barWidth}
strokeDasharray="0"
filter={glow ? `url(#glow-${gaugeId})` : undefined}
filter={glowFilter}
/>
);
}

View File

@ -325,6 +325,10 @@ function RadialBarExample({
}: ExampleProps) {
const theme = useTheme2();
if (gradient === 'scheme' && colorMode === FieldColorModeId.Fixed) {
colorMode = FieldColorModeId.ContinuousGrYlRd;
}
const frame = toDataFrame({
name: 'TestData',
length: 18,
@ -400,7 +404,7 @@ function getExtraSeries(seriesCount: number, colorMode: FieldColorModeId, theme:
const fields: Field[] = [];
const colors = ['blue', 'green', 'purple', 'orange', 'yellow'];
for (let i = 0; i < seriesCount; i++) {
for (let i = 1; i < seriesCount; i++) {
fields.push({
name: `Series ${i + 1}`,
type: FieldType.number,

View File

@ -2,17 +2,17 @@ import { css } from '@emotion/css';
import { isNumber } from 'lodash';
import { useId } from 'react';
import { DisplayValue, FieldDisplay, getDisplayProcessor, GrafanaTheme2 } from '@grafana/data';
import { FieldDisplay, getDisplayProcessor, GrafanaTheme2 } from '@grafana/data';
import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
import { GradientDef, getGradientId } from './GradientDef';
import { GradientDef } from './GradientDef';
import { RadialBar } from './RadialBar';
import { RadialBarSegmented } from './RadialBarSegmented';
import { RadialSparkline } from './RadialSparkline';
import { RadialText } from './RadialText';
import { CenterGlowGradient, GlowGradient, SpotlightGradient } from './effects';
import { calculateDimensions, GaugeDimensions, getValueAngleForValue } from './utils';
import { GlowGradient, MiddleCircleGlow, SpotlightGradient } from './effects';
import { calculateDimensions, getValueAngleForValue } from './utils';
export interface RadialGaugeProps {
values: FieldDisplay[];
@ -81,136 +81,130 @@ export function RadialGauge(props: RadialGaugeProps) {
const startAngle = shape === 'gauge' ? 250 : 0;
const endAngle = shape === 'gauge' ? 110 : 360;
const dimensions = calculateDimensions(width, height, endAngle, glowBar, spotlight, roundedBars, barWidthFactor);
console.log('dimensions', dimensions);
const defs: React.ReactNode[] = [];
const graphics: React.ReactNode[] = [];
let sparklineElement: React.ReactNode | null = null;
const primaryValue = values[0];
const color = primaryValue.display.color ?? theme.colors.primary.main;
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 dimensions = calculateDimensions(width, height, endAngle, glowBar, roundedBars, barWidthFactor, barIndex);
const { angle } = getValueAngleForValue(primaryValue, startAngle, endAngle);
let displayProcessor = getDisplayProcessor();
if (displayValue.view && isNumber(displayValue.colIndex)) {
displayProcessor = displayValue.view.getFieldDisplayProcessor(displayValue.colIndex) ?? displayProcessor;
}
const spotlightGradientId = `spotlight-${barIndex}-${gaugeId}`;
const glowFilterId = `glow-${gaugeId}`;
const colorGradientId = `bar-color-${barIndex}-${gaugeId}`;
const barColor = gradient !== 'none' ? `url(#${colorGradientId})` : color;
defs.push(
<GradientDef
key={`gradient-${barIndex}`}
fieldDisplay={displayValue}
id={colorGradientId}
theme={theme}
gradient={gradient}
dimensions={dimensions}
shape={shape}
/>
);
if (spotlight) {
defs.push(
<SpotlightGradient
key={spotlightGradientId}
id={spotlightGradientId}
angle={angle + startAngle}
dimensions={dimensions}
roundedBars={roundedBars}
theme={theme}
/>
);
}
if (segmentCount > 1) {
graphics.push(
<RadialBarSegmented
dimensions={dimensions}
fieldDisplay={displayValue}
angleRange={angleRange}
startAngle={startAngle}
color={barColor}
glowFilter={`url(#${glowFilterId})`}
segmentCount={segmentCount}
segmentSpacing={segmentSpacing}
displayProcessor={displayProcessor}
gradient={gradient}
/>
);
} else {
graphics.push(
<RadialBar
dimensions={dimensions}
key={barIndex}
angle={angle}
angleRange={angleRange}
startAngle={startAngle}
color={barColor}
roundedBars={roundedBars}
spotlightStroke={`url(#${spotlightGradientId})`}
glowFilter={`url(#${glowFilterId})`}
/>
);
}
// These elements are only added for first value / bar
if (barIndex === 0) {
if (glowBar) {
defs.push(<GlowGradient id={glowFilterId} radius={dimensions.radius} />);
}
if (glowCenter) {
graphics.push(<MiddleCircleGlow gaugeId={gaugeId} color={color} dimensions={dimensions} />);
}
graphics.push(
<RadialText
vizCount={vizCount}
textMode={textMode}
displayValue={displayValue.display}
dimensions={dimensions}
theme={theme}
shape={shape}
/>
);
if (displayValue.sparkline) {
sparklineElement = (
<RadialSparkline
sparkline={displayValue.sparkline}
dimensions={dimensions}
theme={theme}
color={color}
shape={shape}
/>
);
}
}
}
return (
<div className={styles.vizWrapper} style={{ width, height }}>
<svg width={width} height={height}>
<defs>
{values.map((displayValue, barIndex) => (
<GradientDef
key={barIndex}
fieldDisplay={displayValue}
index={barIndex}
theme={theme}
gaugeId={gaugeId}
gradient={gradient}
dimensions={dimensions}
shape={shape}
/>
))}
{spotlight && (
<SpotlightGradient
angle={angle + startAngle}
gaugeId={gaugeId}
dimensions={dimensions}
roundedBars={roundedBars}
theme={theme}
/>
)}
{glowBar && <GlowGradient gaugeId={gaugeId} radius={dimensions.radius} />}
{glowCenter && <CenterGlowGradient gaugeId={gaugeId} color={color} />}
</defs>
<g>
{values.map((displayValue, barIndex) => {
const barColor = getColorForBar(displayValue.display, barIndex, gradient, gaugeId);
//const barSize = dimensions.radius - (barWidth * 2 + 8) * barIndex;
let displayProcessor = getDisplayProcessor();
if (displayValue.view && isNumber(displayValue.colIndex)) {
displayProcessor = displayValue.view.getFieldDisplayProcessor(displayValue.colIndex) ?? displayProcessor;
}
if (segmentCount > 1) {
return (
<RadialBarSegmented
dimensions={dimensions}
key={barIndex}
gaugeId={gaugeId}
fieldDisplay={displayValue}
startAngle={startAngle}
endAngle={endAngle}
color={barColor}
spotlight={spotlight}
glow={glowBar}
segmentCount={segmentCount}
segmentSpacing={segmentSpacing}
displayProcessor={displayProcessor}
gradient={gradient}
/>
);
}
return (
<RadialBar
dimensions={dimensions}
key={barIndex}
gaugeId={gaugeId}
fieldDisplay={displayValue}
startAngle={startAngle}
endAngle={endAngle}
color={barColor}
roundedBars={roundedBars}
spotlight={spotlight}
glow={glowBar}
/>
);
})}
</g>
<g>
{glowCenter && <MiddleCircle fill={`url(#circle-glow-${gaugeId})`} dimensions={dimensions} />}
{primaryValue && (
<RadialText
vizCount={vizCount}
textMode={textMode}
displayValue={primaryValue.display}
dimensions={dimensions}
theme={theme}
shape={shape}
/>
)}
</g>
<defs>{defs}</defs>
{graphics}
</svg>
{primaryValue && primaryValue.sparkline && (
<RadialSparkline
sparkline={primaryValue.sparkline}
dimensions={dimensions}
theme={theme}
color={color}
shape={shape}
/>
)}
{sparklineElement}
</div>
);
}
function getColorForBar(displayValue: DisplayValue, barIndex: number, gradient: RadialGradientMode, gaugeId: string) {
if (gradient === 'none') {
return displayValue.color ?? 'gray';
}
return `url(#${getGradientId(gaugeId, barIndex)})`;
}
export interface MiddleCircleProps {
dimensions: GaugeDimensions;
fill?: string;
className?: string;
}
export function MiddleCircle({ dimensions, fill, className }: MiddleCircleProps) {
return (
<circle cx={dimensions.centerX} cy={dimensions.centerY} r={dimensions.radius} fill={fill} className={className} />
);
}
function getStyles(theme: GrafanaTheme2) {
return {
vizWrapper: css({

View File

@ -3,15 +3,15 @@ import { GrafanaTheme2 } from '@grafana/data';
import { GaugeDimensions } from './utils';
export interface GlowGradientProps {
gaugeId: string;
id: string;
radius: number;
}
export function GlowGradient({ gaugeId, radius }: GlowGradientProps) {
export function GlowGradient({ id, radius }: GlowGradientProps) {
const glowSize = 0.04 * radius;
return (
<filter id={`glow-${gaugeId}`} filterUnits="userSpaceOnUse">
<filter id={id} filterUnits="userSpaceOnUse">
<feGaussianBlur stdDeviation={glowSize} />
<feComponentTransfer>
<feFuncA type="linear" slope="1" />
@ -22,13 +22,13 @@ export function GlowGradient({ gaugeId, radius }: GlowGradientProps) {
}
export function SpotlightGradient({
gaugeId,
id,
dimensions,
roundedBars,
angle,
theme,
}: {
gaugeId: string;
id: string;
dimensions: GaugeDimensions;
angle: number;
roundedBars: boolean;
@ -44,7 +44,7 @@ export function SpotlightGradient({
const color = theme.colors.text.maxContrast;
return (
<linearGradient x1={x1} y1={y1} x2={x2} y2={y2} id={`spotlight-${gaugeId}`} gradientUnits="userSpaceOnUse">
<linearGradient x1={x1} y1={y1} x2={x2} y2={y2} id={id} gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor={color} stopOpacity={0.0} />
<stop offset="95%" stopColor={color} stopOpacity={0.5} />
{roundedBars && <stop offset="100%" stopColor={color} stopOpacity={roundedBars ? 0.7 : 1} />}
@ -60,3 +60,27 @@ export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color:
</radialGradient>
);
}
export interface CenterGlowProps {
dimensions: GaugeDimensions;
gaugeId: string;
color?: string;
}
export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps) {
const gradientId = `circle-glow-${gaugeId}`;
return (
<>
<defs>
<radialGradient id={gradientId} r={'50%'} fr={'0%'}>
<stop offset="0%" stopColor={color} stopOpacity={0.2} />
<stop offset="90%" stopColor={color} stopOpacity={0} />
</radialGradient>
</defs>
<g>
<circle cx={dimensions.centerX} cy={dimensions.centerY} r={dimensions.radius} fill={`url(#${gradientId})`} />
</g>
</>
);
}

View File

@ -1,17 +1,17 @@
import { FieldDisplay } from '@grafana/data';
export function getValueAngleForValue(fieldDisplay: FieldDisplay, startAngle: number, endAngle: number) {
const range = (360 % (startAngle === 0 ? 1 : startAngle)) + endAngle;
const angleRange = (360 % (startAngle === 0 ? 1 : startAngle)) + endAngle;
const min = fieldDisplay.field.min ?? 0;
const max = fieldDisplay.field.max ?? 100;
let angle = ((fieldDisplay.display.numeric - min) / (max - min)) * range;
let angle = ((fieldDisplay.display.numeric - min) / (max - min)) * angleRange;
if (angle > range) {
angle = range;
if (angle > angleRange) {
angle = angleRange;
}
return { range, angle };
return { angleRange, angle };
}
/**
@ -31,6 +31,7 @@ export interface GaugeDimensions {
centerY: number;
barWidth: number;
endAngle?: number;
barIndex: number;
}
export function calculateDimensions(
@ -38,9 +39,9 @@ export function calculateDimensions(
height: number,
endAngle: number,
glow: boolean,
spotlight: boolean,
roundedBars: boolean,
barWidthFactor: number
barWidthFactor: number,
barIndex: number
): GaugeDimensions {
const yMaxAngle = endAngle > 180 ? 180 : endAngle;
let margin = 0;
@ -67,9 +68,14 @@ export function calculateDimensions(
outerRadius -= (margin * 2) / (1 + heightRatioV);
}
const innerRadius = outerRadius - barWidth / 2;
let innerRadius = outerRadius - barWidth / 2;
const centerX = width / 2;
const centerY = outerRadius + margin;
return { margin, radius: innerRadius, centerX, centerY, barWidth };
if (barIndex > 0) {
innerRadius = innerRadius - (barWidth + 4) * barIndex;
}
return { margin, radius: innerRadius, centerX, centerY, barWidth, barIndex };
}