From 75b73c98e225d08b065f131eeea115cc16d6736f Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Mon, 13 Apr 2026 19:28:57 +0800 Subject: [PATCH] fix: correct stack corner clipping with barMinHeight Stacked bar clip paths recomputed aggregate bounds from stack values even when barMinHeight had already rewritten the rendered segment geometry. Use the precomputed RECT coordinates for clip path bounds in that case and lock the regression with a chart unit test plus a Rush changefile. Constraint: barMinHeight rewrites rendered stack segment bounds before clipping Rejected: Disable stackCornerRadius when barMinHeight is set | drops intended styling Confidence: high Scope-risk: narrow Reversibility: clean Directive: When stack geometry is precomputed, derive clip bounds from RECT fields Tested: npm test -- --runInBand __tests__/unit/chart/bar.test.ts; npm run compile Not-tested: Browser rendering in the public playground for issue #4543 --- ...-stack-corner-radius_2026-04-13-11-27.json | 11 +++ .../vchart/__tests__/unit/chart/bar.test.ts | 67 +++++++++++++++++++ packages/vchart/src/series/bar/bar.ts | 21 ++++-- 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 common/changes/@visactor/vchart/fix-bar-min-height-stack-corner-radius_2026-04-13-11-27.json diff --git a/common/changes/@visactor/vchart/fix-bar-min-height-stack-corner-radius_2026-04-13-11-27.json b/common/changes/@visactor/vchart/fix-bar-min-height-stack-corner-radius_2026-04-13-11-27.json new file mode 100644 index 0000000000..bf29c3f6df --- /dev/null +++ b/common/changes/@visactor/vchart/fix-bar-min-height-stack-corner-radius_2026-04-13-11-27.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: correct stacked bar corner clipping when barMinHeight is applied (Issue #4543)", + "type": "patch", + "packageName": "@visactor/vchart" + } + ], + "packageName": "@visactor/vchart", + "email": "lixuef1313@163.com" +} diff --git a/packages/vchart/__tests__/unit/chart/bar.test.ts b/packages/vchart/__tests__/unit/chart/bar.test.ts index 26cb7817ee..11a6533772 100644 --- a/packages/vchart/__tests__/unit/chart/bar.test.ts +++ b/packages/vchart/__tests__/unit/chart/bar.test.ts @@ -148,4 +148,71 @@ describe('Bar chart test', () => { expect(series.fieldY2).toBe('__VCHART_STACK_START'); expect(series.fieldX2).toBeUndefined(); }); + + test('stackCornerRadius should build valid clip paths when barMinHeight is enabled', () => { + const stackSpec = { + type: 'bar', + data: { + values: [ + { type: 'Autocracies', year: '1930', value: 129 }, + { type: 'Autocracies', year: '1940', value: 133 }, + { type: 'Democracies', year: '1930', value: 22 }, + { type: 'Democracies', year: '1940', value: 13 }, + { type: 'Price', year: '1930', value: 1 }, + { type: 'Price', year: '1940', value: 1 } + ] + }, + barMaxWidth: 16, + barGapInGroup: 2, + barMinHeight: 2, + stackCornerRadius: [0, 2, 2, 0], + height: 500, + xField: 'year', + yField: 'value', + seriesField: 'type' + }; + const transformer = new BarChart.transformerConstructor({ + type: 'bar', + seriesType: 'bar', + getTheme: getTheme, + mode: 'desktop-browser' + }); + const info = transformer.initChartSpec(stackSpec as any); + chart = new BarChart( + stackSpec as any, + { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + eventDispatcher: new EventDispatcher({} as any, { addEventListener: () => {} } as any), + globalInstance: { + isAnimationEnable: () => true, + getContainer: () => ({}), + getTooltipHandlerByUser: (() => undefined) as () => undefined + }, + render: {} as any, + dataSet, + map: new Map(), + container: null, + mode: 'desktop-browser', + getCompiler: getTestCompiler, + globalScale: new GlobalScale([], { getAllSeries: () => [] as any[] } as any), + getTheme: getTheme, + getSpecInfo: () => info + } as any + ); + chart.created(transformer); + chart.init(); + + const series: BarSeries = chart.getAllSeries()[0] as BarSeries; + const barMark = series.getMarkInName('bar') as any; + const clipPaths = barMark._markConfig.clipPath(); + + expect(clipPaths.length).toBeGreaterThan(0); + clipPaths.forEach((path: any) => { + expect(Number.isFinite(path.attribute.x)).toBe(true); + expect(Number.isFinite(path.attribute.y)).toBe(true); + expect(Number.isFinite(path.attribute.y1)).toBe(true); + expect(Number.isFinite(path.attribute.width)).toBe(true); + }); + }); }); diff --git a/packages/vchart/src/series/bar/bar.ts b/packages/vchart/src/series/bar/bar.ts index c29a3f0846..05c600305a 100644 --- a/packages/vchart/src/series/bar/bar.ts +++ b/packages/vchart/src/series/bar/bar.ts @@ -494,14 +494,21 @@ export class BarSeries extends Cartes const xScale = this._xAxisHelper?.getScale?.(0); const yScale = this._yAxisHelper?.getScale?.(0); + const isVertical = this.direction === Direction.vertical; this._barMark.setMarkConfig({ clip: true, clipPath: () => { + const usePreCalculatedRect = !!this._shouldDoPreCalculate(); + if (usePreCalculatedRect) { + this._calculateStackRectPosition(isVertical); + } const rectPaths: any[] = []; this._forEachStackGroup(node => { let min = Infinity; let max = -Infinity; + let rectMin = Infinity; + let rectMax = -Infinity; let hasPercent = false; let minPercent = Infinity; let maxPercent = -Infinity; @@ -512,6 +519,12 @@ export class BarSeries extends Cartes const endPercent = datum[STACK_FIELD_END_PERCENT]; min = Math.min(min, start, end); max = Math.max(max, start, end); + if (usePreCalculatedRect) { + const rectStart = datum[isVertical ? RECT_Y : RECT_X]; + const rectEnd = datum[isVertical ? RECT_Y1 : RECT_X1]; + rectMin = Math.min(rectMin, rectStart, rectEnd); + rectMax = Math.max(rectMax, rectStart, rectEnd); + } if (isValid(startPercent) && isValid(endPercent)) { hasPercent = true; minPercent = Math.min(minPercent, startPercent, endPercent); @@ -532,14 +545,14 @@ export class BarSeries extends Cartes const rectAttr = this.direction === Direction.horizontal ? { - x: this._getBarXStart(mockDatum, xScale), - x1: this._getBarXEnd(mockDatum, xScale), + x: usePreCalculatedRect ? rectMin : this._getBarXStart(mockDatum, xScale), + x1: usePreCalculatedRect ? rectMax : this._getBarXEnd(mockDatum, xScale), y: this._getPosition(this.direction, mockDatum), height: this._getBarWidth(this._yAxisHelper) } : { - y: this._getBarYStart(mockDatum, yScale), - y1: this._getBarYEnd(mockDatum, yScale), + y: usePreCalculatedRect ? rectMin : this._getBarYStart(mockDatum, yScale), + y1: usePreCalculatedRect ? rectMax : this._getBarYEnd(mockDatum, yScale), x: this._getPosition(this.direction, mockDatum), width: this._getBarWidth(this._xAxisHelper) };