wip: using clip-path and CSS for drawing the gauge
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
fillOpacity="1"
|
||||
stroke={color}
|
||||
strokeOpacity="1"
|
||||
strokeWidth={barWidth}
|
||||
filter={glowFilter}
|
||||
strokeLinecap={roundedBars ? 'round' : 'butt'}
|
||||
className="radial-arc-path"
|
||||
/>
|
||||
<clipPath id={id}>
|
||||
<path d={path} />
|
||||
</clipPath>
|
||||
<foreignObject x="0" y="0" width={centerX * 2} height={centerY * 2} clipPath={`url(#${id})`}>
|
||||
<div style={{ width: '100%', height: '100%', backgroundImage: gradient, backgroundColor: color }} />
|
||||
</foreignObject>
|
||||
|
||||
{roundedBars && gradientList && (
|
||||
<>
|
||||
<circle cx={x1} cy={y1} r={dotRadius} fill={gradientList[0].color} />
|
||||
{/* this would actually need to be the color determined by the display */}
|
||||
<circle cx={x2} cy={y2} r={dotRadius} fill={gradientList[gradientList.length - 1].color} />
|
||||
</>
|
||||
)}
|
||||
{showGuideDots && (
|
||||
<>
|
||||
{arcLengthDeg > 5 && <circle cx={x1} cy={y1} r={dotRadius} fill={guideDotStartColor} />}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user