From 704d723a301aece2e02b546691145b1a392cbc00 Mon Sep 17 00:00:00 2001 From: biubiukam Date: Sat, 29 Aug 2026 03:35:11 +0800 Subject: [PATCH 1/2] fix: prevent rich text labels from inheriting line dash styles --- .../fix-issue-4595-richtext-line-dash_2026-08-28.json | 11 +++++++++++ packages/vchart/__tests__/unit/mark/text.test.ts | 2 +- packages/vchart/src/mark/text.ts | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 common/changes/@visactor/vchart/fix-issue-4595-richtext-line-dash_2026-08-28.json diff --git a/common/changes/@visactor/vchart/fix-issue-4595-richtext-line-dash_2026-08-28.json b/common/changes/@visactor/vchart/fix-issue-4595-richtext-line-dash_2026-08-28.json new file mode 100644 index 0000000000..dc32b61fc2 --- /dev/null +++ b/common/changes/@visactor/vchart/fix-issue-4595-richtext-line-dash_2026-08-28.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "fix: prevent rich text labels from inheriting line dash styles (Issue #4595)", + "type": "patch" + } + ], + "packageName": "@visactor/vchart", + "email": "biukam.w@gmail.com" +} diff --git a/packages/vchart/__tests__/unit/mark/text.test.ts b/packages/vchart/__tests__/unit/mark/text.test.ts index 2a763a5210..3f423d7d8a 100644 --- a/packages/vchart/__tests__/unit/mark/text.test.ts +++ b/packages/vchart/__tests__/unit/mark/text.test.ts @@ -30,6 +30,6 @@ test('rule mark initial style', () => { expect(stroke).toEqual(undefined); expect(strokeOpacity).toEqual(undefined); expect(strokeWidth).toEqual(0); - expect(lineDash).toEqual(undefined); + expect(lineDash).toEqual([]); expect(cursor).toEqual(undefined); }); diff --git a/packages/vchart/src/mark/text.ts b/packages/vchart/src/mark/text.ts index 0130c9afa5..daf8784dbe 100644 --- a/packages/vchart/src/mark/text.ts +++ b/packages/vchart/src/mark/text.ts @@ -29,6 +29,7 @@ export class TextMark extends BaseMark implements ITextMa angle: 0, textAlign: 'center', lineWidth: 0, + lineDash: [], textConfig: [] }; return defaultStyle; From cfbd61c1ab918ed988755a604a0bf48271d70f89 Mon Sep 17 00:00:00 2001 From: biubiukam Date: Mon, 31 Aug 2026 22:42:50 +0800 Subject: [PATCH 2/2] test: cover rich text line dash rendering regression --- .../vchart/__tests__/unit/mark/text.test.ts | 92 ++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/vchart/__tests__/unit/mark/text.test.ts b/packages/vchart/__tests__/unit/mark/text.test.ts index 3f423d7d8a..8a310a3c92 100644 --- a/packages/vchart/__tests__/unit/mark/text.test.ts +++ b/packages/vchart/__tests__/unit/mark/text.test.ts @@ -1,8 +1,10 @@ import { markContext as ctx } from '../../util/context'; import { TextMark } from '../../../src/mark/text'; import { LayoutZIndex } from '../../../src/constant/layout'; +import { default as VChart } from '../../../src'; +import { createCanvas, removeDom } from '../../util/dom'; -test('rule mark initial style', () => { +test('text mark initial style', () => { const textMark = new TextMark('rule0', ctx); textMark.created(); const visible = textMark.getAttribute('visible', {}); @@ -33,3 +35,91 @@ test('rule mark initial style', () => { expect(lineDash).toEqual([]); expect(cursor).toEqual(undefined); }); + +describe('rich text line dash rendering', () => { + let canvasDom: HTMLCanvasElement; + let chart: VChart; + let setLineDashSpy: jest.SpyInstance; + let strokeTextSpy: jest.SpyInstance; + + beforeEach(() => { + canvasDom = createCanvas(); + canvasDom.width = 500; + canvasDom.height = 500; + }); + + afterEach(() => { + setLineDashSpy?.mockRestore(); + strokeTextSpy?.mockRestore(); + chart?.release(); + removeDom(canvasDom); + }); + + test('resets line dash before drawing a rich text label', () => { + setLineDashSpy = jest.spyOn(CanvasRenderingContext2D.prototype, 'setLineDash'); + strokeTextSpy = jest.spyOn(CanvasRenderingContext2D.prototype, 'strokeText'); + + chart = new VChart( + { + type: 'scatter', + data: [{ id: 'data1', values: [{ x: 1, y: 1, size: 50 }] }], + xField: 'x', + yField: 'y', + sizeField: 'size', + point: { + style: { + stroke: '#ff0000', + lineWidth: 4, + lineDash: [10, 10] + } + }, + label: { + visible: true, + position: 'top', + offset: 10, + formatMethod: () => ({ + type: 'rich', + text: [ + { + text: 'label', + fontSize: 30, + fill: '#000', + stroke: '#00ff00', + lineWidth: 4 + } + ] + }) + }, + animation: false + } as any, + { + renderCanvas: canvasDom, + animation: false + } + ); + + chart.renderSync(); + + const richTextGraphic = chart.getStage().getElementsByType('richtext')[0] as any; + const lineDashCalls = setLineDashSpy.mock.calls.map(([lineDash]) => lineDash); + const pointLineDashIndex = lineDashCalls.findIndex( + lineDash => Array.isArray(lineDash) && lineDash[0] === 10 && lineDash[1] === 10 + ); + const labelStrokeTextIndex = strokeTextSpy.mock.calls.findIndex(([text]) => text === 'label'); + const labelStrokeTextInvocationOrder = strokeTextSpy.mock.invocationCallOrder[labelStrokeTextIndex]; + const lineDashCallsBeforeLabel = setLineDashSpy.mock.calls + .map(([lineDash], index) => ({ + lineDash, + invocationOrder: setLineDashSpy.mock.invocationCallOrder[index] + })) + .filter(({ invocationOrder }) => invocationOrder < labelStrokeTextInvocationOrder) + .sort((a, b) => a.invocationOrder - b.invocationOrder); + const lastLineDashBeforeLabel = lineDashCallsBeforeLabel[lineDashCallsBeforeLabel.length - 1]; + + expect(richTextGraphic.attribute.lineDash).toEqual([]); + expect(pointLineDashIndex).toBeGreaterThanOrEqual(0); + expect(labelStrokeTextIndex).toBeGreaterThanOrEqual(0); + expect(setLineDashSpy.mock.invocationCallOrder[pointLineDashIndex]).toBeLessThan(labelStrokeTextInvocationOrder); + expect(lastLineDashBeforeLabel?.lineDash).toEqual([]); + }); +});