From 77e7120ceeb3f44bc9ac5f95c89ffcf62db9685f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A6=8F=E6=99=8B?= Date: Thu, 18 Jun 2026 11:35:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20style.radius?= =?UTF-8?q?=20=E6=95=B0=E7=BB=84=E8=A1=A8=E8=BE=BE=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/shape/interval-color.spec.ts | 40 +++++++++++++++ src/shape/interval/color.ts | 54 +++++++++++++++++---- src/shape/interval/funnel.ts | 21 ++++++-- 3 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 __tests__/unit/shape/interval-color.spec.ts diff --git a/__tests__/unit/shape/interval-color.spec.ts b/__tests__/unit/shape/interval-color.spec.ts new file mode 100644 index 0000000000..19f6e06108 --- /dev/null +++ b/__tests__/unit/shape/interval-color.spec.ts @@ -0,0 +1,40 @@ +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('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..1ee9d0f99e 100644 --- a/src/shape/interval/color.ts +++ b/src/shape/interval/color.ts @@ -6,6 +6,20 @@ 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 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[], +): [number, number, number, number] { + 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 +54,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 +122,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(Array.isArray(radius) ? radius[0] : (radius as number)) .padAngle((inset * Math.PI) / 180); return select(document.createElement('path', {})) @@ -138,10 +163,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 +185,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; From 8c66407eb87cff6c03c9f587a094af66a5773c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A6=8F=E6=99=8B?= Date: Thu, 18 Jun 2026 11:51:50 +0800 Subject: [PATCH 2/2] fix: conversation --- __tests__/unit/shape/interval-color.spec.ts | 6 ++++++ src/shape/interval/color.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/__tests__/unit/shape/interval-color.spec.ts b/__tests__/unit/shape/interval-color.spec.ts index 19f6e06108..991bfa5136 100644 --- a/__tests__/unit/shape/interval-color.spec.ts +++ b/__tests__/unit/shape/interval-color.spec.ts @@ -28,6 +28,12 @@ describe('parseRadius', () => { }); }); + 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]); diff --git a/src/shape/interval/color.ts b/src/shape/interval/color.ts index 1ee9d0f99e..8c4d790dc2 100644 --- a/src/shape/interval/color.ts +++ b/src/shape/interval/color.ts @@ -8,12 +8,16 @@ 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[], + 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]; } @@ -122,7 +126,7 @@ export function rect( const center = coordinate.getCenter() as Vector2; const arcObject = getArcObject(coordinate, points, [y, y1]); const path = arc() - .cornerRadius(Array.isArray(radius) ? radius[0] : (radius as number)) + .cornerRadius(parseRadius(radius)[0]) .padAngle((inset * Math.PI) / 180); return select(document.createElement('path', {}))