diff --git a/__tests__/unit/shape/interval-color.spec.ts b/__tests__/unit/shape/interval-color.spec.ts new file mode 100644 index 0000000000..991bfa5136 --- /dev/null +++ b/__tests__/unit/shape/interval-color.spec.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { parseRadius } from '../../../src/shape/interval/color'; + +describe('parseRadius', () => { + describe('radius as a number', () => { + it('should apply the same radius to all four corners', () => { + expect(parseRadius(10)).toEqual([10, 10, 10, 10]); + }); + + it('should return all zeros for radius 0', () => { + expect(parseRadius(0)).toEqual([0, 0, 0, 0]); + }); + }); + + describe('radius as an array [TL, TR, BR, BL]', () => { + it('should expand 4-element radius array to corners', () => { + expect(parseRadius([10, 10, 4, 4])).toEqual([10, 10, 4, 4]); + }); + + it('should fallback missing array elements to 0', () => { + expect(parseRadius([10, 5])).toEqual([10, 5, 0, 0]); + expect(parseRadius([8])).toEqual([8, 0, 0, 0]); + expect(parseRadius([])).toEqual([0, 0, 0, 0]); + }); + + it('should fallback undefined elements to 0', () => { + expect(parseRadius([10, undefined, 4])).toEqual([10, 0, 4, 0]); + }); + }); + + describe('radius as undefined', () => { + it('should fallback to all zeros when radius is undefined', () => { + expect(parseRadius(undefined)).toEqual([0, 0, 0, 0]); + }); + }); + + describe('individual corner override with ?? pattern', () => { + it('should allow explicit corner radius to override parsed array values', () => { + const [tl, tr, br, bl] = parseRadius([10, 10, 4, 4]); + expect(20 ?? tl).toBe(20); // explicit override + expect(undefined ?? tr).toBe(10); // fallback to parsed value + expect(undefined ?? br).toBe(4); + expect(undefined ?? bl).toBe(4); + }); + }); +}); diff --git a/src/shape/interval/color.ts b/src/shape/interval/color.ts index ef10133e95..8c4d790dc2 100644 --- a/src/shape/interval/color.ts +++ b/src/shape/interval/color.ts @@ -6,6 +6,24 @@ import { sub } from '../../utils/vector'; import { clamp } from '../../utils/number'; import { applyStyle, getArcObject, reorder, toOpacityKey } from '../utils'; +/** + * Parse radius into a 4-element array [topLeft, topRight, bottomRight, bottomLeft]. + * - If radius is undefined, defaults to 0 for all corners. + * - If radius is a number, all corners use the same value. + * - If radius is an array, map to corners with 0 as fallback for missing elements. + */ +export function parseRadius( + radius: number | number[] | undefined, +): [number, number, number, number] { + if (radius === undefined) { + return [0, 0, 0, 0]; + } + if (Array.isArray(radius)) { + return [radius[0] ?? 0, radius[1] ?? 0, radius[2] ?? 0, radius[3] ?? 0]; + } + return [radius, radius, radius, radius]; +} + export type ColorOptions = { colorAttribute: 'fill' | 'stroke'; /** @@ -40,15 +58,26 @@ export function rect( insetTop = inset, insetRight = inset, insetBottom = inset, - radiusBottomLeft = radius, - radiusBottomRight = radius, - radiusTopLeft = radius, - radiusTopRight = radius, + radiusTopLeft: _radiusTopLeft, + radiusTopRight: _radiusTopRight, + radiusBottomRight: _radiusBottomRight, + radiusBottomLeft: _radiusBottomLeft, minWidth = -Infinity, maxWidth = Infinity, minHeight = -Infinity, ...rest } = style; + + const [ + defaultTopLeft, + defaultTopRight, + defaultBottomRight, + defaultBottomLeft, + ] = parseRadius(radius); + const radiusTopLeft = _radiusTopLeft ?? defaultTopLeft; + const radiusTopRight = _radiusTopRight ?? defaultTopRight; + const radiusBottomRight = _radiusBottomRight ?? defaultBottomRight; + const radiusBottomLeft = _radiusBottomLeft ?? defaultBottomLeft; if (!isPolar(coordinate) && !isHelix(coordinate)) { const tpShape = !!isTranspose(coordinate); @@ -97,7 +126,7 @@ export function rect( const center = coordinate.getCenter() as Vector2; const arcObject = getArcObject(coordinate, points, [y, y1]); const path = arc() - .cornerRadius(radius as number) + .cornerRadius(parseRadius(radius)[0]) .padAngle((inset * Math.PI) / 180); return select(document.createElement('path', {})) @@ -138,10 +167,10 @@ export const Color: SC = (options, context) => { const { stroke, radius = defaultRadius, - radiusTopLeft = radius, - radiusTopRight = radius, - radiusBottomRight = radius, - radiusBottomLeft = radius, + radiusTopLeft: _radiusTopLeft, + radiusTopRight: _radiusTopRight, + radiusBottomRight: _radiusBottomRight, + radiusBottomLeft: _radiusBottomLeft, innerRadius = 0, innerRadiusTopLeft = innerRadius, innerRadiusTopRight = innerRadius, @@ -160,6 +189,17 @@ export const Color: SC = (options, context) => { } = style; const { color = defaultColor, opacity } = value; + const [ + defaultTopLeft, + defaultTopRight, + defaultBottomRight, + defaultBottomLeft, + ] = parseRadius(radius); + const radiusTopLeft = _radiusTopLeft ?? defaultTopLeft; + const radiusTopRight = _radiusTopRight ?? defaultTopRight; + const radiusBottomRight = _radiusBottomRight ?? defaultBottomRight; + const radiusBottomLeft = _radiusBottomLeft ?? defaultBottomLeft; + // Extended style, which is not supported by native g shape, // should apply at first. const standardDirRadius = [ diff --git a/src/shape/interval/funnel.ts b/src/shape/interval/funnel.ts index fcdda36dfd..fd9b4a7f7b 100644 --- a/src/shape/interval/funnel.ts +++ b/src/shape/interval/funnel.ts @@ -5,6 +5,7 @@ import { ShapeComponent as SC, Vector2 } from '../../runtime'; import { select } from '../../utils/selection'; import { applyStyle, reorder } from '../utils'; import { createRoundedPath } from '../../utils/path'; +import { parseRadius } from './color'; export type FunnelOptions = { adjustPoints?: ( @@ -74,10 +75,10 @@ export const Funnel: SC = (options, context) => { const { adjustPoints = getFunnelPoints, radius, - radiusTopLeft = radius, - radiusTopRight = radius, - radiusBottomRight = radius, - radiusBottomLeft = radius, + radiusTopLeft: _radiusTopLeft, + radiusTopRight: _radiusTopRight, + radiusBottomRight: _radiusBottomRight, + radiusBottomLeft: _radiusBottomLeft, innerRadius = 0, innerRadiusTopLeft = innerRadius, innerRadiusTopRight = innerRadius, @@ -88,6 +89,18 @@ export const Funnel: SC = (options, context) => { ...style } = options; const { coordinate, document } = context; + + const [ + defaultTopLeft, + defaultTopRight, + defaultBottomRight, + defaultBottomLeft, + ] = parseRadius(radius); + const radiusTopLeft = _radiusTopLeft ?? defaultTopLeft; + const radiusTopRight = _radiusTopRight ?? defaultTopRight; + const radiusBottomRight = _radiusBottomRight ?? defaultBottomRight; + const radiusBottomLeft = _radiusBottomLeft ?? defaultBottomLeft; + return (points, value, defaults, point2d) => { const { index } = value; const { color: defaultColor, ...rest } = defaults;