From 6f66026cf9cb70794e95dff7d66473111a85ae54 Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Mon, 27 Apr 2026 14:06:54 +0800 Subject: [PATCH 1/8] chore: unblock VChart validation on alpha.2 Use the published VRender alpha.2 packages for the VChart validation branch. Keep the minimal app-aware stage path and external ownership fixes in review scope. Add regression coverage for VRender patches and final attrs. Constraint: Published npm alpha is the validation source Constraint: option.stage and option.app remain external-owned Rejected: Local linked VRender build | final evidence must use npm alpha Rejected: Broad env/bootstrap migration | this branch keeps stage changes minimal Confidence: high Scope-risk: moderate Directive: Do not downgrade @visactor/vrender* or bypass VRender state patches Tested: rush install --check-only Tested: rush test --only @visactor/vchart Tested: rush build --to @visactor/vchart Tested: eslint touched VChart files Tested: browser strict smoke for basicBar/basicLine/textHeavy hover/select Not-tested: full miniApp/wx/tt/harmony/lynx app factory migration --- common/config/rush/pnpm-lock.yaml | 116 ++++----- docs/package.json | 6 +- packages/openinula-vchart/package.json | 6 +- packages/react-vchart/package.json | 4 +- packages/vchart-extension/package.json | 8 +- .../__tests__/unit/core/vchart-event.test.ts | 91 +++++++ .../vchart/__tests__/unit/core/vchart.test.ts | 241 +++++++++++++++++- packages/vchart/package.json | 8 +- packages/vchart/src/compile/compiler.ts | 36 ++- .../vchart/src/compile/interface/compiler.ts | 10 +- packages/vchart/src/compile/stage-app.ts | 51 ++++ tools/story-player/package.json | 8 +- 12 files changed, 494 insertions(+), 91 deletions(-) create mode 100644 packages/vchart/src/compile/stage-app.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 3c7d870779..546b5efda2 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -37,11 +37,11 @@ importers: specifier: 1.2.4-alpha.5 version: 1.2.4-alpha.5 '@visactor/vrender': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-kits': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vtable': specifier: 1.19.0-alpha.0 version: 1.19.0-alpha.0 @@ -203,11 +203,11 @@ importers: specifier: workspace:2.0.22 version: link:../vchart '@visactor/vrender-core': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-kits': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -294,11 +294,11 @@ importers: specifier: workspace:2.0.22 version: link:../vchart-extension '@visactor/vrender-core': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-kits': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -529,17 +529,17 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender-animate': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-components': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-core': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-kits': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vscale': specifier: ~1.0.23 version: 1.0.23 @@ -692,17 +692,17 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender-animate': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-components': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-core': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-kits': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -1260,14 +1260,14 @@ importers: specifier: workspace:2.0.22 version: link:../../packages/vchart '@visactor/vrender': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-core': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-kits': - specifier: 1.0.45 - version: 1.0.45 + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -3073,29 +3073,29 @@ packages: '@visactor/vrender-animate@1.0.0-alpha.18': resolution: {integrity: sha512-9kTtvp1ef+1t+AtUiza6A7qBQP7SmvOu3/ILGrqs/HGdZVj1XGjbYvD/X/zwKJ3LEb7gGV5fa8x95e4czTvRSA==} - '@visactor/vrender-animate@1.0.45': - resolution: {integrity: sha512-6v7LRpr+zugxR8JH3RCnodYxrrzHkSp4GaBTNwXuJ7bwThi/YzXTvmBz2pfQNmUG/rRze8Bm7vqneHVdXuFhng==} + '@visactor/vrender-animate@1.1.0-alpha.2': + resolution: {integrity: sha512-jlegJK92jVvT/PF16nMeRjlN68IoSgvJz/tKgsZVveJCt5276LSm2K5vpIjXNeaEKvmUnKes+45sZZX6kWNC/A==} '@visactor/vrender-components@1.0.0-alpha.18': resolution: {integrity: sha512-7Euq+ZfswL74n2pgkaqZSsPxoSa5SPIGyXatN1eUrdzM2Z0kX6U0RcJg01fctvRs4op6WhcecRLqGvnHcBeb9Q==} - '@visactor/vrender-components@1.0.45': - resolution: {integrity: sha512-rYsG/rncT5FgYYWqv09f6LkZEAk/IS45IEYWBD0SCgKguYnpEebYDmDba1muJGn+rRv/vGlqg1pYqXLw8mEU5w==} + '@visactor/vrender-components@1.1.0-alpha.2': + resolution: {integrity: sha512-nmBTBXEPvDRYbvXtOCLZ89+9WK1A+wLsE4RbuKv2omjA9D+XYRE7QZsW3O1g1/XBD8JqDVDiz+tk2Pp11IBBMw==} '@visactor/vrender-core@1.0.0-alpha.18': resolution: {integrity: sha512-0ihtNvCyNkOsWPFgRqowHzq0IcQgS2Wl/nPpKbVtxWKveenwlhA+ZKoQvam6VJyBY7jeNe1pROy0mJMDyVAJQw==} - '@visactor/vrender-core@1.0.45': - resolution: {integrity: sha512-kvYAsKGZ+dXbhOQzjjbMkRzI815KgaHoWOW7iyVorXTldQBTsxLtFIi/J3VfYqGdM3apUgsFoy8uvjS5DepPUA==} + '@visactor/vrender-core@1.1.0-alpha.2': + resolution: {integrity: sha512-f9BTOK3Rv5yiTxMJMPLV2VYIgwcsakJ/VXCOp88hbWgy/or2pvPW4/42sOLALDMCjSnpd2zGhV2R2vy2+nSD2w==} '@visactor/vrender-kits@1.0.0-alpha.18': resolution: {integrity: sha512-Tvolkq+4G8qiPFZo0Aj8M//Yr6jR2h8FNkFEyWM9gbQbEiTkjpmHAJOYnoSsaPtPrcMSlG4EhJSFDk6ymANHVg==} - '@visactor/vrender-kits@1.0.45': - resolution: {integrity: sha512-iaeRitht8IqNvJwdKmcPKAYL0a4+f8xhK2bWUumSQjNbZJQx9UzGuDp73cIdNXzCNoCiJZElpsOiIGu3kymqiw==} + '@visactor/vrender-kits@1.1.0-alpha.2': + resolution: {integrity: sha512-fXG3SB/PDcmA2oK6FqHTBvb8438BDeeCkpIvzt1WI/lpghzD6wT7Dd3HhY8hxonArDUUqAsorZyFeQFNPezUTQ==} - '@visactor/vrender@1.0.45': - resolution: {integrity: sha512-mHIB9euxbgpotNLe8cn8IThTJbXgrn/B1Nx/PCqp/OeBEcNihmy5X9ODJVgUhrF3dY1ZX0EeE2zrEE0mE/+eHQ==} + '@visactor/vrender@1.1.0-alpha.2': + resolution: {integrity: sha512-MNW2rTJLgCVa0T4TTavQ+fMJp2PkvkZwxWAwdzdNlFAIX9WriF24L+Xqwu8nvqsJiUz2BEUbgYP+7sTZcn7krA==} '@visactor/vscale@0.18.18': resolution: {integrity: sha512-iRG4kv+5Fv4KX3AxEfV95XU3I6OmF0QizyAhqHxKa7L1MaT+MRvDDk5zHWf1E8gialLbL2xDe3GnT6g/4u5jhA==} @@ -15026,9 +15026,9 @@ snapshots: '@visactor/vrender-core': 1.0.0-alpha.18 '@visactor/vutils': 1.0.4 - '@visactor/vrender-animate@1.0.45': + '@visactor/vrender-animate@1.1.0-alpha.2': dependencies: - '@visactor/vrender-core': 1.0.45 + '@visactor/vrender-core': 1.1.0-alpha.2 '@visactor/vutils': 1.0.23 '@visactor/vrender-components@1.0.0-alpha.18': @@ -15039,11 +15039,11 @@ snapshots: '@visactor/vscale': 1.0.4 '@visactor/vutils': 1.0.4 - '@visactor/vrender-components@1.0.45': + '@visactor/vrender-components@1.1.0-alpha.2': dependencies: - '@visactor/vrender-animate': 1.0.45 - '@visactor/vrender-core': 1.0.45 - '@visactor/vrender-kits': 1.0.45 + '@visactor/vrender-animate': 1.1.0-alpha.2 + '@visactor/vrender-core': 1.1.0-alpha.2 + '@visactor/vrender-kits': 1.1.0-alpha.2 '@visactor/vscale': 1.0.23 '@visactor/vutils': 1.0.23 @@ -15052,7 +15052,7 @@ snapshots: '@visactor/vutils': 1.0.4 color-convert: 2.0.1 - '@visactor/vrender-core@1.0.45': + '@visactor/vrender-core@1.1.0-alpha.2': dependencies: '@visactor/vutils': 1.0.23 color-convert: 2.0.1 @@ -15066,21 +15066,21 @@ snapshots: lottie-web: 5.13.0 roughjs: 4.5.2 - '@visactor/vrender-kits@1.0.45': + '@visactor/vrender-kits@1.1.0-alpha.2': dependencies: '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.0.45 + '@visactor/vrender-core': 1.1.0-alpha.2 '@visactor/vutils': 1.0.23 gifuct-js: 2.1.2 lottie-web: 5.13.0 roughjs: 4.6.6 - '@visactor/vrender@1.0.45': + '@visactor/vrender@1.1.0-alpha.2': dependencies: - '@visactor/vrender-animate': 1.0.45 - '@visactor/vrender-components': 1.0.45 - '@visactor/vrender-core': 1.0.45 - '@visactor/vrender-kits': 1.0.45 + '@visactor/vrender-animate': 1.1.0-alpha.2 + '@visactor/vrender-components': 1.1.0-alpha.2 + '@visactor/vrender-core': 1.1.0-alpha.2 + '@visactor/vrender-kits': 1.1.0-alpha.2 '@visactor/vscale@0.18.18': dependencies: diff --git a/docs/package.json b/docs/package.json index 5f958df927..916924de51 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,8 +19,8 @@ "@visactor/vchart-theme": "~1.6.6", "@visactor/vmind": "1.2.4-alpha.5", "@visactor/vutils": "~1.0.23", - "@visactor/vrender": "1.0.45", - "@visactor/vrender-kits": "1.0.45", + "@visactor/vrender": "1.1.0-alpha.2", + "@visactor/vrender-kits": "1.1.0-alpha.2", "@visactor/vtable": "1.19.0-alpha.0", "@visactor/vtable-editors": "1.19.0-alpha.0", "@visactor/vtable-gantt": "1.19.0-alpha.0", @@ -58,4 +58,4 @@ "react-device-detect": "^2.2.2", "minimist": "1.2.8" } -} \ No newline at end of file +} diff --git a/packages/openinula-vchart/package.json b/packages/openinula-vchart/package.json index 2f7629c7f8..bcc4a86dae 100644 --- a/packages/openinula-vchart/package.json +++ b/packages/openinula-vchart/package.json @@ -30,8 +30,8 @@ "dependencies": { "@visactor/vchart": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "1.0.45", - "@visactor/vrender-kits": "1.0.45", + "@visactor/vrender-core": "1.1.0-alpha.2", + "@visactor/vrender-kits": "1.1.0-alpha.2", "react-is": "^18.2.0" }, "devDependencies": { @@ -78,4 +78,4 @@ "access": "public", "registry": "https://registry.npmjs.org/" } -} \ No newline at end of file +} diff --git a/packages/react-vchart/package.json b/packages/react-vchart/package.json index 16de6da94c..3aa5be4964 100644 --- a/packages/react-vchart/package.json +++ b/packages/react-vchart/package.json @@ -36,8 +36,8 @@ "@visactor/vchart": "workspace:2.0.22", "@visactor/vchart-extension": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "1.0.45", - "@visactor/vrender-kits": "1.0.45", + "@visactor/vrender-core": "1.1.0-alpha.2", + "@visactor/vrender-kits": "1.1.0-alpha.2", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/vchart-extension/package.json b/packages/vchart-extension/package.json index cda702106a..25789045fa 100644 --- a/packages/vchart-extension/package.json +++ b/packages/vchart-extension/package.json @@ -26,10 +26,10 @@ "start": "ts-node __tests__/runtime/browser/scripts/initVite.ts && vite serve __tests__/runtime/browser" }, "dependencies": { - "@visactor/vrender-core": "1.0.45", - "@visactor/vrender-kits": "1.0.45", - "@visactor/vrender-components": "1.0.45", - "@visactor/vrender-animate": "1.0.45", + "@visactor/vrender-core": "1.1.0-alpha.2", + "@visactor/vrender-kits": "1.1.0-alpha.2", + "@visactor/vrender-components": "1.1.0-alpha.2", + "@visactor/vrender-animate": "1.1.0-alpha.2", "@visactor/vchart": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", "@visactor/vdataset": "~1.0.23", diff --git a/packages/vchart/__tests__/unit/core/vchart-event.test.ts b/packages/vchart/__tests__/unit/core/vchart-event.test.ts index 39ec0a0efe..2896595ed3 100644 --- a/packages/vchart/__tests__/unit/core/vchart-event.test.ts +++ b/packages/vchart/__tests__/unit/core/vchart-event.test.ts @@ -310,6 +310,97 @@ describe('vchart event test', () => { } }); + it('should apply interaction state style through vrender state patch', () => { + const stateContainer = createDiv(); + const stateDom = createDiv(stateContainer); + + const chartWithState = new VChart( + { + type: 'bar', + width: 400, + height: 300, + data: [ + { + id: 'data', + values: [ + { x: 'Mon', y: 10 }, + { x: 'Tue', y: 12 } + ] + } + ], + xField: 'x', + yField: 'y', + hover: { enable: true, trigger: 'pointerover', triggerOff: 'pointerout' }, + select: { enable: true, trigger: 'click', mode: 'single' }, + bar: { + state: { + hover: { + fillOpacity: 0.31 + }, + selected: { + stroke: '#111827', + lineWidth: 4 + } + } + } + } as IBarChartSpec, + { + dom: stateDom, + animation: false + } + ); + + chartWithState.renderSync(); + + try { + const chart = chartWithState.getChart() as IChart; + const barSeries = chart.getAllSeries()[0]; + const barMark = barSeries.getMarks().find((mark: IMark) => mark.name === 'bar'); + expect(barMark).toBeDefined(); + if (!barMark) { + throw new Error('Expected bar mark to exist'); + } + + const barGraphic = barMark.getGraphics()[0] as IMarkGraphic & { + attribute: Record; + resolvedStatePatch?: Record; + stateProxy?: (stateName: string, states: string[]) => Record; + }; + expect(barGraphic).toBeDefined(); + expect(typeof barGraphic.stateProxy).toBe('function'); + + const hoverPatch = barGraphic.stateProxy?.('hover', ['hover']); + expect(hoverPatch?.fillOpacity).toBe(0.31); + + chart.getEvent().emit('pointerover', { item: barGraphic } as unknown as BaseEventParams); + + expect(barGraphic.hasState('hover')).toBe(true); + expect(barGraphic.resolvedStatePatch?.fillOpacity).toBe(0.31); + expect(barGraphic.attribute.fillOpacity).toBe(0.31); + + chart.getEvent().emit('pointerout', { item: barGraphic } as unknown as BaseEventParams); + + expect(barGraphic.hasState('hover')).toBe(false); + expect(barGraphic.resolvedStatePatch).toBeUndefined(); + expect(barGraphic.attribute.fillOpacity).not.toBe(0.31); + + const selectedPatch = barGraphic.stateProxy?.('selected', ['selected']); + expect(selectedPatch?.stroke).toBe('#111827'); + expect(selectedPatch?.lineWidth).toBe(4); + + chart.getEvent().emit('click', { item: barGraphic } as unknown as BaseEventParams); + + expect(barGraphic.hasState('selected')).toBe(true); + expect(barGraphic.resolvedStatePatch?.stroke).toBe('#111827'); + expect(barGraphic.resolvedStatePatch?.lineWidth).toBe(4); + expect(barGraphic.attribute.stroke).toBe('#111827'); + expect(barGraphic.attribute.lineWidth).toBe(4); + } finally { + chartWithState.release(); + removeDom(stateContainer); + } + }); + it('should merge only identical interaction triggers in common chart', () => { const commonContainer = createDiv(); const commonDom = createDiv(commonContainer); diff --git a/packages/vchart/__tests__/unit/core/vchart.test.ts b/packages/vchart/__tests__/unit/core/vchart.test.ts index c31a5dbdc1..03a09b6b4e 100644 --- a/packages/vchart/__tests__/unit/core/vchart.test.ts +++ b/packages/vchart/__tests__/unit/core/vchart.test.ts @@ -1,6 +1,13 @@ -import type { Group, IArc, Text } from '@visactor/vrender-core'; +import { createBrowserApp, Stage, type Group, type IApp, type IArc, type Text } from '@visactor/vrender-core'; +import { + installBrowserEnvToApp, + installBrowserPickersToApp, + installDefaultGraphicsToApp +} from '@visactor/vrender-kits'; import type { IBarChartSpec } from '../../../src'; import { default as VChart } from '../../../src'; +import { getDefaultVRenderApp } from '../../../src/compile/stage-app'; +import { registerBrowserEnv } from '../../../src/env'; import { createDiv, createCanvas, removeDom } from '../../util/dom'; import type { ICommonChartSpec } from '../../../src/chart/common'; import type { IAreaSeriesSpec } from '../../../src/series/area/interface'; @@ -508,7 +515,7 @@ describe('VChart', () => { expect(value1).toBe(mark.attribute.x); const value2 = vchart.convertValueToPosition(0, { axisId: 'left' }); - expect(value2).toBe(394); + expect(value2).toBe(vchart.getChart()!.getAllSeries()[0].getRegion().getLayoutRect().height); }); it('should convert correctly in funnel chart', () => { @@ -759,4 +766,234 @@ describe('VChart', () => { expect(axis.getScale().domain()).toEqual([-52, 30]); }); }); + + describe('external stage ownership', () => { + let canvasDom: HTMLCanvasElement; + let charts: VChart[]; + let externalStage: Stage | undefined; + let rawExternalStageRelease: (() => void) | undefined; + let externalApp: IApp | undefined; + let rawExternalAppRelease: (() => void) | undefined; + + const spec = (valueOffset: number = 0): IBarChartSpec => ({ + type: 'bar', + width: 200, + height: 150, + data: [ + { + id: 'data', + values: [ + { x: 'A', y: 12 + valueOffset }, + { x: 'B', y: 18 + valueOffset } + ] + } + ], + xField: 'x', + yField: 'y', + animation: false + }); + + const createExternalStage = () => { + registerBrowserEnv(); + externalStage = new Stage({ + width: 200, + height: 150, + canvas: canvasDom, + autoRender: true, + disableDirtyBounds: true + }); + rawExternalStageRelease = externalStage.release.bind(externalStage); + return externalStage; + }; + + const createExternalApp = () => { + externalApp = createBrowserApp(); + installBrowserEnvToApp(externalApp); + installDefaultGraphicsToApp(externalApp); + installBrowserPickersToApp(externalApp); + rawExternalAppRelease = externalApp.release.bind(externalApp); + return externalApp; + }; + + beforeEach(() => { + canvasDom = createCanvas(); + canvasDom.width = 200; + canvasDom.height = 150; + charts = []; + }); + + afterEach(() => { + charts.forEach(chart => { + if (!(chart as any)._isReleased) { + chart.release(); + } + }); + if (externalStage && (externalStage as any).releaseStatus !== 'released') { + rawExternalStageRelease?.(); + } + if (externalApp && !externalApp.released) { + rawExternalAppRelease?.(); + } + removeDom(canvasDom); + externalStage = undefined; + rawExternalStageRelease = undefined; + externalApp = undefined; + rawExternalAppRelease = undefined; + }); + + it('should not release an externally owned stage and should remove chart-owned root group', () => { + const stage = createExternalStage(); + const releaseStage = jest.fn(() => rawExternalStageRelease?.()); + stage.release = releaseStage as any; + + const chart = new VChart(spec(), { + stage: stage as any, + animation: false + }); + charts.push(chart); + chart.renderSync(); + + const rootGroup = stage.defaultLayer.find(node => node.name === 'root', false); + expect(rootGroup).toBeDefined(); + + chart.release(); + + expect(releaseStage).not.toHaveBeenCalled(); + expect(stage.window).toBeDefined(); + expect(stage.defaultLayer).toBeDefined(); + expect(stage.defaultLayer.find(node => node === rootGroup, false)).toBeFalsy(); + }); + + it('should allow a second VChart instance to reuse the same external stage', () => { + const stage = createExternalStage(); + const releaseStage = jest.fn(() => rawExternalStageRelease?.()); + stage.release = releaseStage as any; + + const first = new VChart(spec(), { + stage: stage as any, + animation: false + }); + charts.push(first); + first.renderSync(); + first.updateSpecSync(spec(3)); + first.release(); + + expect(releaseStage).not.toHaveBeenCalled(); + + const second = new VChart(spec(6), { + stage: stage as any, + animation: false + }); + charts.push(second); + second.renderSync(); + second.release(); + + expect(releaseStage).not.toHaveBeenCalled(); + }); + + it('should still release an internally created stage', () => { + const chart = new VChart(spec(), { + renderCanvas: canvasDom, + animation: false + }); + charts.push(chart); + chart.renderSync(); + + const stage = chart.getStage() as unknown as Stage; + const rawRelease = stage.release.bind(stage); + const releaseStage = jest.fn(() => rawRelease()); + stage.release = releaseStage as any; + + chart.release(); + + expect(releaseStage).toHaveBeenCalledTimes(1); + }); + + it('should use an external app to create an internally owned stage without releasing the app', () => { + const app = createExternalApp(); + const createStage = jest.spyOn(app, 'createStage'); + const releaseApp = jest.fn(() => rawExternalAppRelease?.()); + app.release = releaseApp as any; + + const chart = new VChart(spec(), { + app, + renderCanvas: canvasDom, + animation: false + } as any); + charts.push(chart); + chart.renderSync(); + + expect(createStage).toHaveBeenCalledTimes(1); + + const stage = chart.getStage() as unknown as Stage; + const rawRelease = stage.release.bind(stage); + const releaseStage = jest.fn(() => rawRelease()); + stage.release = releaseStage as any; + + chart.release(); + + expect(releaseStage).toHaveBeenCalledTimes(1); + expect(releaseApp).not.toHaveBeenCalled(); + }); + + it('should reuse the fallback app while keeping internally created stages isolated', () => { + const fallbackApp = getDefaultVRenderApp('desktop-browser'); + const createStage = jest.spyOn(fallbackApp, 'createStage'); + const secondCanvasDom = createCanvas(); + secondCanvasDom.width = 200; + secondCanvasDom.height = 150; + + const first = new VChart(spec(), { + renderCanvas: canvasDom, + animation: false + }); + charts.push(first); + first.renderSync(); + + const second = new VChart(spec(6), { + renderCanvas: secondCanvasDom, + animation: false + }); + charts.push(second); + second.renderSync(); + + const firstStage = first.getStage() as unknown as Stage; + const secondStage = second.getStage() as unknown as Stage; + const rawSecondStageRelease = secondStage.release.bind(secondStage); + const releaseSecondStage = jest.fn(() => rawSecondStageRelease()); + secondStage.release = releaseSecondStage as any; + + expect(getDefaultVRenderApp('desktop-browser')).toBe(fallbackApp); + expect(createStage).toHaveBeenCalledTimes(2); + expect(firstStage).not.toBe(secondStage); + + first.release(); + + expect(releaseSecondStage).not.toHaveBeenCalled(); + expect(secondStage.window).toBeDefined(); + expect(secondStage.defaultLayer).toBeDefined(); + + second.release(); + + expect(releaseSecondStage).toHaveBeenCalledTimes(1); + removeDom(secondCanvasDom); + }); + + it('should keep the default dom render path working with fallback app stage creation', () => { + const dom = createDiv(); + const chart = new VChart(spec(), { + dom, + animation: false + }); + charts.push(chart); + + chart.renderSync(); + chart.updateSpecSync(spec(9)); + + expect(chart.getStage()).toBeDefined(); + + chart.release(); + removeDom(dom); + }); + }); }); diff --git a/packages/vchart/package.json b/packages/vchart/package.json index ef9bf65d99..477dd24a59 100644 --- a/packages/vchart/package.json +++ b/packages/vchart/package.json @@ -126,10 +126,10 @@ "@visactor/vdataset": "~1.0.23", "@visactor/vscale": "~1.0.23", "@visactor/vlayouts": "~1.0.23", - "@visactor/vrender-core": "1.0.45", - "@visactor/vrender-kits": "1.0.45", - "@visactor/vrender-components": "1.0.45", - "@visactor/vrender-animate": "1.0.45", + "@visactor/vrender-core": "1.1.0-alpha.2", + "@visactor/vrender-kits": "1.1.0-alpha.2", + "@visactor/vrender-components": "1.1.0-alpha.2", + "@visactor/vrender-animate": "1.1.0-alpha.2", "@visactor/vutils-extension": "workspace:2.0.22" }, "publishConfig": { diff --git a/packages/vchart/src/compile/compiler.ts b/packages/vchart/src/compile/compiler.ts index 881086fac9..9a4d257c86 100644 --- a/packages/vchart/src/compile/compiler.ts +++ b/packages/vchart/src/compile/compiler.ts @@ -9,7 +9,7 @@ import type { IBoundsLike } from '@visactor/vutils'; import { array, isArray, isObject, isValid } from '@visactor/vutils'; import type { EventSourceType } from '../event/interface'; import type { IChart } from '../chart/interface'; -import { createGroup, Stage, vglobal, waitForAllSubLayers } from '@visactor/vrender-core'; +import { createGroup, vglobal, waitForAllSubLayers } from '@visactor/vrender-core'; import type { IColor, IEventTarget, IGroup, IStage } from '@visactor/vrender-core'; import type { IMorphConfig } from '../animation/spec'; import type { IVChart, IVChartRenderOption } from '../core/interface'; @@ -23,6 +23,7 @@ import { log } from '../util/debug'; import type { MarkAnimationSpec, TypeAnimationConfig } from '../animation/interface'; import { AnimationStateEnum } from '../animation/interface'; import { BuiltIn_DISAPPEAR_ANIMATE_NAME } from '../constant/animate'; +import { createStageFromApp, resolveVRenderApp } from './stage-app'; type EventListener = { type: string; @@ -47,6 +48,8 @@ export class Compiler implements ICompiler { protected _stage: IStage; + private _isExternalStage: boolean = false; + protected _stateAnimationConfig: Partial; get stateAnimationConfig() { return this._stateAnimationConfig; @@ -127,9 +130,13 @@ export class Compiler implements ICompiler { background } = this._option; vglobal.setEnv(toRenderMode(mode), modeParams ?? {}); - this._stage = - this._option.stage ?? - (new Stage({ + const externalStage = this._option.stage; + this._isExternalStage = !!externalStage; + this._stage = externalStage; + + if (!this._stage) { + const app = resolveVRenderApp(this._option.app, mode); + this._stage = createStageFromApp(app, { background, width: this._width, height: this._height, @@ -158,7 +165,8 @@ export class Compiler implements ICompiler { ReactDOM: this._option.ReactDOM, autoRefresh: isValid(autoRefreshDpr) ? autoRefreshDpr : !isValid(dpr), ...(this._option.renderHooks ?? {}) - }) as unknown as IStage); + }) as unknown as IStage; + } this._stage.enableIncrementalAutoRender(); @@ -726,17 +734,27 @@ export class Compiler implements ICompiler { } release(): void { + const stage = this._stage; + const rootGroup = this._rootGroup; + const shouldReleaseStage = !!stage && !this._isExternalStage; + this.clearNextRender(); this.releaseEvent(); - this._option = this._container = null as any; // vgrammar release this.releaseGrammar(true); - if (this._stage !== this._option?.stage) { - // don't release the stage created by outside - this._stage.release(); + if (stage) { + if (shouldReleaseStage) { + stage.release(); + } else if (rootGroup) { + stage.defaultLayer.removeChild(rootGroup); + rootGroup.release(); + } } this._stage = null; + this._rootGroup = null; + this._isExternalStage = false; + this._option = this._container = null as any; this.isInited = false; this._compileChart = null; diff --git a/packages/vchart/src/compile/interface/compiler.ts b/packages/vchart/src/compile/interface/compiler.ts index 9d5df61fc5..6380d1b1fe 100644 --- a/packages/vchart/src/compile/interface/compiler.ts +++ b/packages/vchart/src/compile/interface/compiler.ts @@ -1,4 +1,4 @@ -import type { IColor, IStageParams, IStage, ILayer, IOption3D, ITicker } from '@visactor/vrender-core'; +import type { IApp, IColor, IStageParams, IStage, ILayer, IOption3D, ITicker } from '@visactor/vrender-core'; import type { IPerformanceHook, RenderMode } from '../../typings/spec/common'; import type { IBoundsLike } from '@visactor/vutils'; import type { StringOrNumber } from '../../typings'; @@ -114,9 +114,15 @@ export interface IRenderOption { */ canvasControled?: boolean; /** - * 外部传入的 VRender stage + * 外部传入的 VRender stage。普通用户仍应优先使用 dom/renderCanvas; + * 显式传入 stage 时,stage ownership 属于外部,VChart release 不会释放该 stage。 */ stage?: IStage; + /** + * 外部传入的 VRender app。VChart 可使用它创建内部 stage,但 app ownership 属于外部, + * VChart release 只释放自己创建的 stage,不释放该 app。 + */ + app?: IApp; /** * 外部传入的 VRender layer */ diff --git a/packages/vchart/src/compile/stage-app.ts b/packages/vchart/src/compile/stage-app.ts new file mode 100644 index 0000000000..4cee1db3fc --- /dev/null +++ b/packages/vchart/src/compile/stage-app.ts @@ -0,0 +1,51 @@ +import { createBrowserApp, createNodeApp, type IApp, type IStage, type IStageParams } from '@visactor/vrender-core'; +import { + installBrowserEnvToApp, + installBrowserPickersToApp, + installDefaultGraphicsToApp, + installNodeEnvToApp, + installNodePickersToApp +} from '@visactor/vrender-kits'; +import { RenderModeEnum, type RenderMode } from '../typings/spec/common'; + +type VRenderAppEnv = 'browser' | 'node'; + +const defaultVRenderApps = new Map(); + +const getVRenderAppEnv = (mode?: RenderMode): VRenderAppEnv => + mode === RenderModeEnum.node || mode === RenderModeEnum.worker ? 'node' : 'browser'; + +// Default apps are an internal reuse detail; ordinary VChart users should keep using dom/renderCanvas. +const createDefaultVRenderApp = (env: VRenderAppEnv): IApp => { + const app = env === 'node' ? createNodeApp() : createBrowserApp(); + + if (env === 'node') { + installNodeEnvToApp(app); + installDefaultGraphicsToApp(app); + installNodePickersToApp(app); + } else { + installBrowserEnvToApp(app); + installDefaultGraphicsToApp(app); + installBrowserPickersToApp(app); + } + + return app; +}; + +export const getDefaultVRenderApp = (mode?: RenderMode): IApp => { + const env = getVRenderAppEnv(mode); + const app = defaultVRenderApps.get(env); + + if (app && !app.released) { + return app; + } + + const nextApp = createDefaultVRenderApp(env); + defaultVRenderApps.set(env, nextApp); + + return nextApp; +}; + +export const resolveVRenderApp = (app: IApp | undefined, mode?: RenderMode): IApp => app ?? getDefaultVRenderApp(mode); + +export const createStageFromApp = (app: IApp, params: Partial): IStage => app.createStage(params); diff --git a/tools/story-player/package.json b/tools/story-player/package.json index 3cc90f174c..6339cca92c 100644 --- a/tools/story-player/package.json +++ b/tools/story-player/package.json @@ -56,10 +56,10 @@ "vite": "3.2.6" }, "dependencies": { - "@visactor/vrender-core": "1.0.45", - "@visactor/vrender-kits": "1.0.45", + "@visactor/vrender-core": "1.1.0-alpha.2", + "@visactor/vrender-kits": "1.1.0-alpha.2", "@visactor/vchart": "workspace:2.0.22", - "@visactor/vrender": "1.0.45", + "@visactor/vrender": "1.1.0-alpha.2", "@visactor/vutils": "~1.0.23" } -} \ No newline at end of file +} From ea7a0007c054cf864b585e3bb10adf582e838144 Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Mon, 27 Apr 2026 15:49:00 +0800 Subject: [PATCH 2/8] chore: align VChart with VRender alpha app and state Use root app factories for VChart-created stages. Publish ordinary mark state styles as shared VRender state definitions. Keep external stage and app ownership outside VChart release. Constraint: Do not modify VRender or use local VRender2 links Constraint: Keep this as a minimal VChart migration Rejected: Manual app bootstrap | root VRender app creators are public API Rejected: stateProxy normal path | VRender now owns resolvedStatePatch Confidence: high Scope-risk: moderate Directive: Do not use direct addState/removeState for VChart state changes Tested: install --check-only Tested: test --only @visactor/vchart Tested: build --to @visactor/vchart Tested: eslint touched VChart files Not-tested: Browser smoke lacked a local browser driver or harness --- common/config/rush/pnpm-lock.yaml | 3 + .../__tests__/unit/core/vchart-event.test.ts | 220 +++++++++++++++++- .../vchart/__tests__/unit/core/vchart.test.ts | 47 +++- packages/vchart/package.json | 1 + packages/vchart/src/compile/compiler.ts | 76 +++--- packages/vchart/src/compile/stage-app.ts | 88 ++++--- packages/vchart/src/component/brush/brush.ts | 18 +- .../vchart/src/interaction/interaction.ts | 19 +- .../triggers/element-active-by-legend.ts | 3 +- .../interaction/triggers/element-active.ts | 5 +- .../triggers/element-highlight-by-group.ts | 3 +- .../triggers/element-highlight-by-legend.ts | 3 +- .../triggers/element-highlight-by-name.ts | 3 +- .../interaction/triggers/element-highlight.ts | 3 +- .../interaction/triggers/element-select.ts | 5 +- packages/vchart/src/mark/base/base-mark.ts | 88 ++++--- packages/vchart/src/mark/utils/glyph.ts | 5 +- packages/vchart/src/series/sankey/sankey.ts | 71 +++--- packages/vchart/src/util/graphic-state.ts | 32 +++ 19 files changed, 523 insertions(+), 170 deletions(-) create mode 100644 packages/vchart/src/util/graphic-state.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 546b5efda2..6862a761e8 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -528,6 +528,9 @@ importers: '@visactor/vlayouts': specifier: ~1.0.23 version: 1.0.23 + '@visactor/vrender': + specifier: 1.1.0-alpha.2 + version: 1.1.0-alpha.2 '@visactor/vrender-animate': specifier: 1.1.0-alpha.2 version: 1.1.0-alpha.2 diff --git a/packages/vchart/__tests__/unit/core/vchart-event.test.ts b/packages/vchart/__tests__/unit/core/vchart-event.test.ts index 2896595ed3..2483e9eb15 100644 --- a/packages/vchart/__tests__/unit/core/vchart-event.test.ts +++ b/packages/vchart/__tests__/unit/core/vchart-event.test.ts @@ -3,6 +3,7 @@ import VChart, { type BaseEventParams, type IBarChartSpec, type IChart, + type ILineChartSpec, type ICommonChartSpec, type IMark, type IMarkGraphic, @@ -10,6 +11,22 @@ import VChart, { } from '../../../src'; import { createDiv, removeDom } from '../../util/dom'; +type StateGraphic = IMarkGraphic & { + attribute: Record; + resolvedStatePatch?: Record; + stateProxy?: (stateName: string, states: string[]) => Record; +}; + +type SharedStateDefinitions = Record< + string, + { + resolver?: (context: { graphic: IMarkGraphic }) => Record | undefined; + } +>; + +const getSharedStateDefinitions = (mark: IMark) => + (mark.getProduct() as unknown as { sharedStateDefinitions?: SharedStateDefinitions }).sharedStateDefinitions; + describe('vchart event test', () => { let container: HTMLElement; let dom: HTMLElement; @@ -361,15 +378,14 @@ describe('vchart event test', () => { throw new Error('Expected bar mark to exist'); } - const barGraphic = barMark.getGraphics()[0] as IMarkGraphic & { - attribute: Record; - resolvedStatePatch?: Record; - stateProxy?: (stateName: string, states: string[]) => Record; - }; + const barGraphic = barMark.getGraphics()[0] as StateGraphic; expect(barGraphic).toBeDefined(); - expect(typeof barGraphic.stateProxy).toBe('function'); + const sharedStateDefinitions = getSharedStateDefinitions(barMark); + expect(typeof sharedStateDefinitions?.hover?.resolver).toBe('function'); + expect(typeof sharedStateDefinitions?.selected?.resolver).toBe('function'); + expect(barGraphic.stateProxy).toBeFalsy(); - const hoverPatch = barGraphic.stateProxy?.('hover', ['hover']); + const hoverPatch = sharedStateDefinitions.hover.resolver({ graphic: barGraphic }); expect(hoverPatch?.fillOpacity).toBe(0.31); chart.getEvent().emit('pointerover', { item: barGraphic } as unknown as BaseEventParams); @@ -384,7 +400,7 @@ describe('vchart event test', () => { expect(barGraphic.resolvedStatePatch).toBeUndefined(); expect(barGraphic.attribute.fillOpacity).not.toBe(0.31); - const selectedPatch = barGraphic.stateProxy?.('selected', ['selected']); + const selectedPatch = sharedStateDefinitions.selected.resolver({ graphic: barGraphic }); expect(selectedPatch?.stroke).toBe('#111827'); expect(selectedPatch?.lineWidth).toBe(4); @@ -395,6 +411,194 @@ describe('vchart event test', () => { expect(barGraphic.resolvedStatePatch?.lineWidth).toBe(4); expect(barGraphic.attribute.stroke).toBe('#111827'); expect(barGraphic.attribute.lineWidth).toBe(4); + + chartWithState.updateSpecSync({ + ...(chartWithState.getSpec() as IBarChartSpec), + data: [ + { + id: 'data', + values: [ + { x: 'Mon', y: 14 }, + { x: 'Tue', y: 16 } + ] + } + ] + }); + + barGraphic.useStates([]); + + expect(barGraphic.resolvedStatePatch).toBeUndefined(); + expect(barGraphic.attribute.stroke).not.toBe('#111827'); + expect(barGraphic.attribute.lineWidth).not.toBe(4); + } finally { + chartWithState.release(); + removeDom(stateContainer); + } + }); + + it('should apply line interaction state style through shared vrender state definitions', () => { + const stateContainer = createDiv(); + const stateDom = createDiv(stateContainer); + + const chartWithState = new VChart( + { + type: 'line', + width: 400, + height: 300, + data: [ + { + id: 'data', + values: [ + { x: 'Mon', y: 10 }, + { x: 'Tue', y: 12 } + ] + } + ], + xField: 'x', + yField: 'y', + hover: { enable: true, trigger: 'pointerover', triggerOff: 'pointerout' }, + select: { enable: true, trigger: 'click', mode: 'single' }, + line: { + state: { + hover: { + stroke: '#ea580c' + }, + selected: { + lineWidth: 5 + } + } + } + } as ILineChartSpec, + { + dom: stateDom, + animation: false + } + ); + + chartWithState.renderSync(); + + try { + const chart = chartWithState.getChart() as IChart; + const lineSeries = chart.getAllSeries()[0]; + const lineMark = lineSeries.getMarks().find((mark: IMark) => mark.name === 'line'); + expect(lineMark).toBeDefined(); + if (!lineMark) { + throw new Error('Expected line mark to exist'); + } + + const lineGraphic = lineMark.getGraphics()[0] as StateGraphic; + const sharedStateDefinitions = getSharedStateDefinitions(lineMark); + expect(typeof sharedStateDefinitions?.hover?.resolver).toBe('function'); + expect(typeof sharedStateDefinitions?.selected?.resolver).toBe('function'); + expect(lineGraphic.stateProxy).toBeFalsy(); + + expect(sharedStateDefinitions.hover.resolver({ graphic: lineGraphic })?.stroke).toBe('#ea580c'); + chart.getEvent().emit('pointerover', { item: lineGraphic } as unknown as BaseEventParams); + expect(lineGraphic.hasState('hover')).toBe(true); + expect(lineGraphic.resolvedStatePatch?.stroke).toBe('#ea580c'); + expect(lineGraphic.attribute.stroke).toBe('#ea580c'); + + chart.getEvent().emit('pointerout', { item: lineGraphic } as unknown as BaseEventParams); + expect(lineGraphic.hasState('hover')).toBe(false); + expect(lineGraphic.resolvedStatePatch).toBeUndefined(); + expect(lineGraphic.attribute.stroke).not.toBe('#ea580c'); + + expect(sharedStateDefinitions.selected.resolver({ graphic: lineGraphic })?.lineWidth).toBe(5); + chart.getEvent().emit('click', { item: lineGraphic } as unknown as BaseEventParams); + expect(lineGraphic.hasState('selected')).toBe(true); + expect(lineGraphic.resolvedStatePatch?.lineWidth).toBe(5); + expect(lineGraphic.attribute.lineWidth).toBe(5); + + lineGraphic.useStates([]); + expect(lineGraphic.resolvedStatePatch).toBeUndefined(); + } finally { + chartWithState.release(); + removeDom(stateContainer); + } + }); + + it('should apply text custom-mark interaction state style through shared vrender state definitions', () => { + const stateContainer = createDiv(); + const stateDom = createDiv(stateContainer); + + const chartWithState = new VChart( + { + type: 'bar', + width: 400, + height: 300, + data: [ + { + id: 'data', + values: [{ x: 'Mon', y: 10 }] + } + ], + xField: 'x', + yField: 'y', + hover: { enable: true, trigger: 'pointerover', triggerOff: 'pointerout' }, + select: { enable: true, trigger: 'click', mode: 'single' }, + customMark: [ + { + type: 'text', + name: 'stateText', + style: { + x: 40, + y: 40, + text: 'state', + fill: '#334155', + fontSize: 14 + }, + state: { + hover: { + fill: '#dc2626' + }, + selected: { + fontSize: 24 + } + } + } + ] + } as IBarChartSpec, + { + dom: stateDom, + animation: false + } + ); + + chartWithState.renderSync(); + + try { + const chart = chartWithState.getChart() as IChart; + const textMark = chart.getAllMarks().find((mark: IMark) => mark.name === 'stateText'); + expect(textMark).toBeDefined(); + if (!textMark) { + throw new Error('Expected text custom mark to exist'); + } + + const textGraphic = textMark.getGraphics()[0] as StateGraphic; + const sharedStateDefinitions = getSharedStateDefinitions(textMark); + expect(typeof sharedStateDefinitions?.hover?.resolver).toBe('function'); + expect(typeof sharedStateDefinitions?.selected?.resolver).toBe('function'); + expect(textGraphic.stateProxy).toBeFalsy(); + + expect(sharedStateDefinitions.hover.resolver({ graphic: textGraphic })?.fill).toBe('#dc2626'); + textGraphic.useStates(['hover']); + expect(textGraphic.hasState('hover')).toBe(true); + expect(textGraphic.resolvedStatePatch?.fill).toBe('#dc2626'); + expect(textGraphic.attribute.fill).toBe('#dc2626'); + + textGraphic.useStates([]); + expect(textGraphic.hasState('hover')).toBe(false); + expect(textGraphic.resolvedStatePatch).toBeUndefined(); + expect(textGraphic.attribute.fill).not.toBe('#dc2626'); + + expect(sharedStateDefinitions.selected.resolver({ graphic: textGraphic })?.fontSize).toBe(24); + textGraphic.useStates(['selected']); + expect(textGraphic.hasState('selected')).toBe(true); + expect(textGraphic.resolvedStatePatch?.fontSize).toBe(24); + expect(textGraphic.attribute.fontSize).toBe(24); + + textGraphic.useStates([]); + expect(textGraphic.resolvedStatePatch).toBeUndefined(); } finally { chartWithState.release(); removeDom(stateContainer); diff --git a/packages/vchart/__tests__/unit/core/vchart.test.ts b/packages/vchart/__tests__/unit/core/vchart.test.ts index 03a09b6b4e..d07f21e45e 100644 --- a/packages/vchart/__tests__/unit/core/vchart.test.ts +++ b/packages/vchart/__tests__/unit/core/vchart.test.ts @@ -1,9 +1,5 @@ -import { createBrowserApp, Stage, type Group, type IApp, type IArc, type Text } from '@visactor/vrender-core'; -import { - installBrowserEnvToApp, - installBrowserPickersToApp, - installDefaultGraphicsToApp -} from '@visactor/vrender-kits'; +import { Stage, type Group, type IApp, type IArc, type Text } from '@visactor/vrender-core'; +import { createBrowserVRenderApp } from '@visactor/vrender'; import type { IBarChartSpec } from '../../../src'; import { default as VChart } from '../../../src'; import { getDefaultVRenderApp } from '../../../src/compile/stage-app'; @@ -807,10 +803,7 @@ describe('VChart', () => { }; const createExternalApp = () => { - externalApp = createBrowserApp(); - installBrowserEnvToApp(externalApp); - installDefaultGraphicsToApp(externalApp); - installBrowserPickersToApp(externalApp); + externalApp = createBrowserVRenderApp(); rawExternalAppRelease = externalApp.release.bind(externalApp); return externalApp; }; @@ -979,6 +972,40 @@ describe('VChart', () => { removeDom(secondCanvasDom); }); + it('should release the fallback app after the last internally owned stage is released', () => { + const fallbackApp = getDefaultVRenderApp('desktop-browser'); + const rawFallbackAppRelease = fallbackApp.release.bind(fallbackApp); + const releaseFallbackApp = jest.fn(() => rawFallbackAppRelease()); + fallbackApp.release = releaseFallbackApp as any; + const secondCanvasDom = createCanvas(); + secondCanvasDom.width = 200; + secondCanvasDom.height = 150; + + const first = new VChart(spec(), { + renderCanvas: canvasDom, + animation: false + }); + charts.push(first); + first.renderSync(); + + const second = new VChart(spec(6), { + renderCanvas: secondCanvasDom, + animation: false + }); + charts.push(second); + second.renderSync(); + + first.release(); + + expect(releaseFallbackApp).not.toHaveBeenCalled(); + + second.release(); + + expect(releaseFallbackApp).toHaveBeenCalledTimes(1); + expect(getDefaultVRenderApp('desktop-browser')).not.toBe(fallbackApp); + removeDom(secondCanvasDom); + }); + it('should keep the default dom render path working with fallback app stage creation', () => { const dom = createDiv(); const chart = new VChart(spec(), { diff --git a/packages/vchart/package.json b/packages/vchart/package.json index 477dd24a59..b684b8e7c0 100644 --- a/packages/vchart/package.json +++ b/packages/vchart/package.json @@ -126,6 +126,7 @@ "@visactor/vdataset": "~1.0.23", "@visactor/vscale": "~1.0.23", "@visactor/vlayouts": "~1.0.23", + "@visactor/vrender": "1.1.0-alpha.2", "@visactor/vrender-core": "1.1.0-alpha.2", "@visactor/vrender-kits": "1.1.0-alpha.2", "@visactor/vrender-components": "1.1.0-alpha.2", diff --git a/packages/vchart/src/compile/compiler.ts b/packages/vchart/src/compile/compiler.ts index 9a4d257c86..54dbf1afeb 100644 --- a/packages/vchart/src/compile/compiler.ts +++ b/packages/vchart/src/compile/compiler.ts @@ -50,6 +50,8 @@ export class Compiler implements ICompiler { private _isExternalStage: boolean = false; + private _releaseVRenderAppRef?: () => void; + protected _stateAnimationConfig: Partial; get stateAnimationConfig() { return this._stateAnimationConfig; @@ -132,40 +134,49 @@ export class Compiler implements ICompiler { vglobal.setEnv(toRenderMode(mode), modeParams ?? {}); const externalStage = this._option.stage; this._isExternalStage = !!externalStage; + this._releaseVRenderAppRef = undefined; this._stage = externalStage; if (!this._stage) { - const app = resolveVRenderApp(this._option.app, mode); - this._stage = createStageFromApp(app, { - background, - width: this._width, - height: this._height, - container: this._container.dom ?? null, - canvas: this._container.canvas ?? null, - dpr, - viewBox: this._option.viewBox, - canvasControled: this._option.canvasControled, - beforeRender: (stage: IStage) => { - this._compileChart?.onBeforeRender(); - this._option.beforeRender?.(stage); - }, - afterRender: this._option.afterRender, - disableDirtyBounds: true, - autoRender: true, - ticker: this._option.ticker, - pluginList: this._option.pluginList, - enableHtmlAttribute: this._option.enableHtmlAttribute, - optimize: this._option.optimize, - supportsTouchEvents: this._option.supportsTouchEvents, - supportsPointerEvents: this._option.supportsPointerEvents, - event: { - clickInterval: clickInterval, - autoPreventDefault: autoPreventDefault - }, - ReactDOM: this._option.ReactDOM, - autoRefresh: isValid(autoRefreshDpr) ? autoRefreshDpr : !isValid(dpr), - ...(this._option.renderHooks ?? {}) - }) as unknown as IStage; + const resolvedApp = resolveVRenderApp(this._option.app, mode); + this._releaseVRenderAppRef = resolvedApp.releaseAppRef; + + try { + this._stage = createStageFromApp(resolvedApp.app, { + background, + width: this._width, + height: this._height, + container: this._container.dom ?? null, + canvas: this._container.canvas ?? null, + dpr, + viewBox: this._option.viewBox, + canvasControled: this._option.canvasControled, + beforeRender: (stage: IStage) => { + this._compileChart?.onBeforeRender(); + this._option.beforeRender?.(stage); + }, + afterRender: this._option.afterRender, + disableDirtyBounds: true, + autoRender: true, + ticker: this._option.ticker, + pluginList: this._option.pluginList, + enableHtmlAttribute: this._option.enableHtmlAttribute, + optimize: this._option.optimize, + supportsTouchEvents: this._option.supportsTouchEvents, + supportsPointerEvents: this._option.supportsPointerEvents, + event: { + clickInterval: clickInterval, + autoPreventDefault: autoPreventDefault + }, + ReactDOM: this._option.ReactDOM, + autoRefresh: isValid(autoRefreshDpr) ? autoRefreshDpr : !isValid(dpr), + ...(this._option.renderHooks ?? {}) + }) as unknown as IStage; + } catch (error) { + this._releaseVRenderAppRef?.(); + this._releaseVRenderAppRef = undefined; + throw error; + } } this._stage.enableIncrementalAutoRender(); @@ -737,6 +748,7 @@ export class Compiler implements ICompiler { const stage = this._stage; const rootGroup = this._rootGroup; const shouldReleaseStage = !!stage && !this._isExternalStage; + const releaseVRenderAppRef = this._releaseVRenderAppRef; this.clearNextRender(); this.releaseEvent(); @@ -751,9 +763,11 @@ export class Compiler implements ICompiler { rootGroup.release(); } } + releaseVRenderAppRef?.(); this._stage = null; this._rootGroup = null; this._isExternalStage = false; + this._releaseVRenderAppRef = undefined; this._option = this._container = null as any; this.isInited = false; diff --git a/packages/vchart/src/compile/stage-app.ts b/packages/vchart/src/compile/stage-app.ts index 4cee1db3fc..9f6ecb6853 100644 --- a/packages/vchart/src/compile/stage-app.ts +++ b/packages/vchart/src/compile/stage-app.ts @@ -1,51 +1,79 @@ -import { createBrowserApp, createNodeApp, type IApp, type IStage, type IStageParams } from '@visactor/vrender-core'; -import { - installBrowserEnvToApp, - installBrowserPickersToApp, - installDefaultGraphicsToApp, - installNodeEnvToApp, - installNodePickersToApp -} from '@visactor/vrender-kits'; +import { createBrowserVRenderApp, createNodeVRenderApp } from '@visactor/vrender'; +import type { IApp, IStage, IStageParams } from '@visactor/vrender-core'; import { RenderModeEnum, type RenderMode } from '../typings/spec/common'; type VRenderAppEnv = 'browser' | 'node'; -const defaultVRenderApps = new Map(); +type DefaultVRenderAppRecord = { + app: IApp; + refCount: number; +}; + +export type ResolvedVRenderApp = { + app: IApp; + releaseAppRef?: () => void; +}; + +const defaultVRenderApps = new Map(); const getVRenderAppEnv = (mode?: RenderMode): VRenderAppEnv => mode === RenderModeEnum.node || mode === RenderModeEnum.worker ? 'node' : 'browser'; // Default apps are an internal reuse detail; ordinary VChart users should keep using dom/renderCanvas. -const createDefaultVRenderApp = (env: VRenderAppEnv): IApp => { - const app = env === 'node' ? createNodeApp() : createBrowserApp(); - - if (env === 'node') { - installNodeEnvToApp(app); - installDefaultGraphicsToApp(app); - installNodePickersToApp(app); - } else { - installBrowserEnvToApp(app); - installDefaultGraphicsToApp(app); - installBrowserPickersToApp(app); +const createDefaultVRenderApp = (env: VRenderAppEnv): IApp => + env === 'node' ? createNodeVRenderApp() : createBrowserVRenderApp(); + +const getDefaultVRenderAppRecord = (mode?: RenderMode): DefaultVRenderAppRecord => { + const env = getVRenderAppEnv(mode); + const record = defaultVRenderApps.get(env); + + if (record && !record.app.released) { + return record; } - return app; + const nextRecord = { + app: createDefaultVRenderApp(env), + refCount: 0 + }; + defaultVRenderApps.set(env, nextRecord); + + return nextRecord; }; -export const getDefaultVRenderApp = (mode?: RenderMode): IApp => { +export const getDefaultVRenderApp = (mode?: RenderMode): IApp => getDefaultVRenderAppRecord(mode).app; + +const retainDefaultVRenderApp = (mode?: RenderMode): ResolvedVRenderApp => { const env = getVRenderAppEnv(mode); - const app = defaultVRenderApps.get(env); + const record = getDefaultVRenderAppRecord(mode); + let released = false; - if (app && !app.released) { - return app; - } + record.refCount += 1; - const nextApp = createDefaultVRenderApp(env); - defaultVRenderApps.set(env, nextApp); + return { + app: record.app, + releaseAppRef: () => { + if (released) { + return; + } + released = true; + record.refCount -= 1; - return nextApp; + if (record.refCount <= 0) { + defaultVRenderApps.delete(env); + if (!record.app.released) { + record.app.release(); + } + } + } + }; }; -export const resolveVRenderApp = (app: IApp | undefined, mode?: RenderMode): IApp => app ?? getDefaultVRenderApp(mode); +export const resolveVRenderApp = (app: IApp | undefined, mode?: RenderMode): ResolvedVRenderApp => { + if (app) { + return { app }; + } + + return retainDefaultVRenderApp(mode); +}; export const createStageFromApp = (app: IApp, params: Partial): IStage => app.createStage(params); diff --git a/packages/vchart/src/component/brush/brush.ts b/packages/vchart/src/component/brush/brush.ts index d130fc57d1..686a974702 100644 --- a/packages/vchart/src/component/brush/brush.ts +++ b/packages/vchart/src/component/brush/brush.ts @@ -25,6 +25,7 @@ import { brush } from '../../theme/builtin/common/component/brush'; import { isReverse, statePointToData } from '../data-zoom/util'; import type { CartesianAxis } from '../axis/cartesian'; import type { IRenderOption } from '../../compile/interface'; +import { addGraphicState, removeGraphicState } from '../../util/graphic-state'; const IN_BRUSH_STATE = 'inBrush'; const OUT_BRUSH_STATE = 'outOfBrush'; @@ -499,15 +500,15 @@ export class Brush extends BaseComponent i // now: 不在当前brush中 const isBrushContainItem = this._isBrushContainItem(operateMask.globalAABBBounds, pointsCoord, graphicItem); if (this._outOfBrushElementsMap?.[elementKey] && isBrushContainItem) { - graphicItem.addState(IN_BRUSH_STATE, true); + addGraphicState(graphicItem, IN_BRUSH_STATE, true); if (!this._inBrushElementsMap[operateMask?.name]) { this._inBrushElementsMap[operateMask?.name] = {}; } this._inBrushElementsMap[operateMask?.name][elementKey] = graphicItem; delete this._outOfBrushElementsMap[elementKey]; } else if (this._inBrushElementsMap?.[operateMask?.name]?.[elementKey] && !isBrushContainItem) { - graphicItem.removeState(IN_BRUSH_STATE); - graphicItem.addState(OUT_BRUSH_STATE, true); + removeGraphicState(graphicItem, IN_BRUSH_STATE); + addGraphicState(graphicItem, OUT_BRUSH_STATE, true); this._outOfBrushElementsMap[elementKey] = graphicItem; delete this._inBrushElementsMap[operateMask.name][elementKey]; } @@ -577,7 +578,7 @@ export class Brush extends BaseComponent i this._linkedOutOfBrushElementsMap?.[elementKey] && this._isBrushContainItem(operateMask.globalAABBBounds, pointsCoord, graphicItem) ) { - graphicItem.addState(IN_BRUSH_STATE, true); + addGraphicState(graphicItem, IN_BRUSH_STATE, true); if (!this._linkedInBrushElementsMap[operateMask?.name]) { this._linkedInBrushElementsMap[operateMask?.name] = {}; } @@ -587,8 +588,8 @@ export class Brush extends BaseComponent i this._linkedInBrushElementsMap?.[operateMask?.name]?.[elementKey] && !this._isBrushContainItem(operateMask.globalAABBBounds, pointsCoord, graphicItem) ) { - graphicItem.removeState(IN_BRUSH_STATE); - graphicItem.addState(OUT_BRUSH_STATE, true); + removeGraphicState(graphicItem, IN_BRUSH_STATE); + addGraphicState(graphicItem, OUT_BRUSH_STATE, true); this._linkedOutOfBrushElementsMap[elementKey] = graphicItem; } }); @@ -643,9 +644,8 @@ export class Brush extends BaseComponent i } graphics.forEach((el: IMarkGraphic) => { const elementKey = mark.id + '_' + el.context.key; - el.removeState(IN_BRUSH_STATE); - el.removeState(OUT_BRUSH_STATE); - stateName && el.addState(stateName, true); + removeGraphicState(el, [IN_BRUSH_STATE, OUT_BRUSH_STATE]); + stateName && addGraphicState(el, stateName, true); elementMap[elementKey] = el; }); }); diff --git a/packages/vchart/src/interaction/interaction.ts b/packages/vchart/src/interaction/interaction.ts index b70d7dace8..c90456cd11 100644 --- a/packages/vchart/src/interaction/interaction.ts +++ b/packages/vchart/src/interaction/interaction.ts @@ -2,6 +2,7 @@ import type { StateValue } from '../compile/mark'; import type { IMarkGraphic } from '../mark/interface'; import type { IInteraction } from './interface/common'; import type { ITrigger } from './interface/trigger'; +import { addGraphicState, removeGraphicState } from '../util/graphic-state'; export class Interaction implements IInteraction { private _stateGraphicsByTrigger: Map = new Map(); @@ -89,7 +90,7 @@ export class Interaction implements IInteraction { if (hasReverse) { const m = g.parent?.mark; const hasAnimation = (m as any).hasAnimationByState && (m as any).hasAnimationByState('state'); - g.addState(reverseState, true, hasAnimation); + addGraphicState(g, reverseState, true, hasAnimation); } }); @@ -100,7 +101,7 @@ export class Interaction implements IInteraction { if (hasReverse) { const m = g.parent?.mark; const hasAnimation = (m as any).hasAnimationByState && (m as any).hasAnimationByState('state'); - g.removeState(reverseState, hasAnimation); + removeGraphicState(g, reverseState, hasAnimation); } }); } @@ -119,7 +120,7 @@ export class Interaction implements IInteraction { if (hasState) { const m = g.parent?.mark; const hasAnimation = (m as any).hasAnimationByState && (m as any).hasAnimationByState('state'); - g.removeState(state, hasAnimation); + removeGraphicState(g, state, hasAnimation); } }); @@ -128,7 +129,7 @@ export class Interaction implements IInteraction { if (hasState) { const m = g.parent?.mark; const hasAnimation = (m as any).hasAnimationByState && (m as any).hasAnimationByState('state'); - g.addState(state, true, hasAnimation); + addGraphicState(g, state, true, hasAnimation); } }); } @@ -155,11 +156,11 @@ export class Interaction implements IInteraction { const isStated = statedGraphics && statedGraphics.includes(g); if (isStated) { if (hasState) { - g.addState(state, true, hasAnimation); + addGraphicState(g, state, true, hasAnimation); } } else { if (hasReverse) { - g.addState(reverseState, true, hasAnimation); + addGraphicState(g, reverseState, true, hasAnimation); } } }); @@ -184,7 +185,7 @@ export class Interaction implements IInteraction { if (isStated) { if (hasState) { - g.addState(state, true, hasAnimation); + addGraphicState(g, state, true, hasAnimation); } } }); @@ -211,14 +212,14 @@ export class Interaction implements IInteraction { if (graphics && graphics.length) { if (reverseState && markIdByState[reverseState] && markIdByState[reverseState].includes(mark.id)) { graphics.forEach(g => { - g.removeState(reverseState, hasAnimation); + removeGraphicState(g, reverseState, hasAnimation); }); } if (state && markIdByState[state] && markIdByState[state].includes(mark.id)) { graphics.forEach(g => { if (statedGraphics.includes(g)) { - g.removeState(state, hasAnimation); + removeGraphicState(g, state, hasAnimation); } }); } diff --git a/packages/vchart/src/interaction/triggers/element-active-by-legend.ts b/packages/vchart/src/interaction/triggers/element-active-by-legend.ts index 160461fb2c..a9a317bd0a 100644 --- a/packages/vchart/src/interaction/triggers/element-active-by-legend.ts +++ b/packages/vchart/src/interaction/triggers/element-active-by-legend.ts @@ -7,6 +7,7 @@ import { generateFilterValue } from './util'; import type { IMarkGraphic } from '../../mark/interface'; import type { BaseEventParams } from '../../core'; import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; +import { removeGraphicState } from '../../util/graphic-state'; const type = 'element-active-by-legend'; const defaultOptions: Partial = { @@ -80,7 +81,7 @@ export class ElementActiveByLegend if (g) { const statedGraphics = interaction.getStatedGraphics(this); if (statedGraphics && statedGraphics.includes(g)) { - g.removeState(state); + removeGraphicState(g, state); interaction.setStatedGraphics( this, statedGraphics.filter(sg => sg !== g) diff --git a/packages/vchart/src/interaction/triggers/element-active.ts b/packages/vchart/src/interaction/triggers/element-active.ts index 02a450693d..74a5497cf0 100644 --- a/packages/vchart/src/interaction/triggers/element-active.ts +++ b/packages/vchart/src/interaction/triggers/element-active.ts @@ -5,6 +5,7 @@ import { TRIGGER_TYPE_ENUM } from './enum'; import { STATE_VALUE_ENUM } from '../../compile/mark/interface'; import type { IMarkGraphic } from '../../mark/interface'; import type { BaseEventParams } from '../../event/interface'; +import { addGraphicState, removeGraphicState } from '../../util/graphic-state'; const defaultOptions: Partial = { state: STATE_VALUE_ENUM.STATE_ACTIVE, @@ -43,7 +44,7 @@ export class ElementActive extends BaseTrigger implements const { state, interaction } = this.options; if (this.isGraphicInStateMark(g, state)) { - g.addState(state, true); + addGraphicState(g, state, true); interaction.setStatedGraphics(this, [g]); } @@ -57,7 +58,7 @@ export class ElementActive extends BaseTrigger implements const g = graphic ?? statedGraphics?.[0]; if (g && statedGraphics?.includes(g)) { - g.removeState(state); + removeGraphicState(g, state); interaction.setStatedGraphics( this, statedGraphics.filter(sg => sg !== g) diff --git a/packages/vchart/src/interaction/triggers/element-highlight-by-group.ts b/packages/vchart/src/interaction/triggers/element-highlight-by-group.ts index 95d75dfa21..6f58a5f5bf 100644 --- a/packages/vchart/src/interaction/triggers/element-highlight-by-group.ts +++ b/packages/vchart/src/interaction/triggers/element-highlight-by-group.ts @@ -5,6 +5,7 @@ import type { IMarkGraphic } from '../../mark/interface'; import { isNil } from '@visactor/vutils'; import type { BaseEventParams } from '../../core'; import { highlightDefaultOptions } from './util'; +import { removeGraphicState } from '../../util/graphic-state'; const type = 'element-highlight-by-group'; @@ -84,7 +85,7 @@ export class ElementHighlightByGroup const { interaction } = this.options; const statedGraphics = interaction.getStatedGraphics(this); - g.removeState([this.options.highlightState, this.options.blurState]); + removeGraphicState(g, [this.options.highlightState, this.options.blurState]); interaction.setStatedGraphics( this, diff --git a/packages/vchart/src/interaction/triggers/element-highlight-by-legend.ts b/packages/vchart/src/interaction/triggers/element-highlight-by-legend.ts index 43065be4e9..b1032a83cc 100644 --- a/packages/vchart/src/interaction/triggers/element-highlight-by-legend.ts +++ b/packages/vchart/src/interaction/triggers/element-highlight-by-legend.ts @@ -6,6 +6,7 @@ import { ChartEvent } from '../../constant/event'; import { generateFilterValue } from './util'; import type { IMarkGraphic } from '../../mark/interface/common'; import type { BaseEventParams } from '../../event/interface'; +import { removeGraphicState } from '../../util/graphic-state'; const type = 'element-highlight-by-legend'; const defaultOptions: Partial = { @@ -84,7 +85,7 @@ export class ElementHighlightByLegend if (g) { const statedGraphics = interaction.getStatedGraphics(this); if (statedGraphics && statedGraphics.includes(g)) { - g.removeState([highlightState, blurState]); + removeGraphicState(g, [highlightState, blurState]); interaction.setStatedGraphics( this, statedGraphics.filter(sg => sg !== g) diff --git a/packages/vchart/src/interaction/triggers/element-highlight-by-name.ts b/packages/vchart/src/interaction/triggers/element-highlight-by-name.ts index 8b948c930c..6667b8ca1e 100644 --- a/packages/vchart/src/interaction/triggers/element-highlight-by-name.ts +++ b/packages/vchart/src/interaction/triggers/element-highlight-by-name.ts @@ -6,6 +6,7 @@ import type { BaseEventParams } from '../../event/interface'; import { array } from '@visactor/vutils'; import type { IMarkGraphic } from '../../mark/interface'; import { generateFilterValue } from './util'; +import { removeGraphicState } from '../../util/graphic-state'; const type = 'element-highlight-by-name'; const defaultOptions: Partial = { @@ -95,7 +96,7 @@ export class ElementHighlightByName if (g) { const statedGraphics = interaction.getStatedGraphics(this); if (statedGraphics && statedGraphics.includes(g)) { - g.removeState([highlightState, blurState]); + removeGraphicState(g, [highlightState, blurState]); interaction.setStatedGraphics( this, statedGraphics.filter(sg => sg !== g) diff --git a/packages/vchart/src/interaction/triggers/element-highlight.ts b/packages/vchart/src/interaction/triggers/element-highlight.ts index 6a3bcddc80..dab41d2eb3 100644 --- a/packages/vchart/src/interaction/triggers/element-highlight.ts +++ b/packages/vchart/src/interaction/triggers/element-highlight.ts @@ -7,6 +7,7 @@ import { Factory } from '../../core/factory'; import type { GraphicEventType } from '@visactor/vrender-core'; import { TRIGGER_TYPE_ENUM } from './enum'; import type { BaseEventParams } from '../../event/interface'; +import { removeGraphicState } from '../../util/graphic-state'; const defaultOptions: Partial = { highlightState: STATE_VALUE_ENUM.STATE_HIGHLIGHT, @@ -128,7 +129,7 @@ export class ElementHighlight reset(markGraphic: IMarkGraphic, e?: BaseEventParams) { if (markGraphic) { if (this._markSet.getMarkInId(markGraphic.context.markId)) { - markGraphic.removeState([this.options.highlightState, this.options.blurState]); + removeGraphicState(markGraphic, [this.options.highlightState, this.options.blurState]); } } else { this.resetAll(e); diff --git a/packages/vchart/src/interaction/triggers/element-select.ts b/packages/vchart/src/interaction/triggers/element-select.ts index b13b3acfe1..be8eb65fb1 100644 --- a/packages/vchart/src/interaction/triggers/element-select.ts +++ b/packages/vchart/src/interaction/triggers/element-select.ts @@ -7,6 +7,7 @@ import { parseTriggerOffOfSelect } from './util'; import { Factory } from '../../core/factory'; import { TRIGGER_TYPE_ENUM } from './enum'; import type { BaseEventParams } from '../../event/interface'; +import { addGraphicState, removeGraphicState } from '../../util/graphic-state'; const defaultOptions: Partial = { state: STATE_VALUE_ENUM.STATE_SELECTED, @@ -117,7 +118,7 @@ export class ElementSelect extends BaseTrigger implements if (this._timer) { clearTimeout(this._timer); } - markGraphic.addState(state, true); + addGraphicState(markGraphic, state, true); const newStatedGraphics = this.options.interaction.updateStates( this, @@ -143,7 +144,7 @@ export class ElementSelect extends BaseTrigger implements reset(markGraphic: IMarkGraphic, e?: BaseEventParams) { if (markGraphic) { if (this._markSet.getMarkInId(markGraphic.context.markId)) { - markGraphic.removeState([this.options.state, this.options.reverseState]); + removeGraphicState(markGraphic, [this.options.state, this.options.reverseState]); } } else { this.resetAll(e); diff --git a/packages/vchart/src/mark/base/base-mark.ts b/packages/vchart/src/mark/base/base-mark.ts index ea1e199480..25cc20920f 100644 --- a/packages/vchart/src/mark/base/base-mark.ts +++ b/packages/vchart/src/mark/base/base-mark.ts @@ -55,7 +55,14 @@ import { array, degreeToRadian, isArray, isBoolean, isFunction, isNil, isObject, import { curveTypeTransform, groupData, runEncoder } from '../utils/common'; import type { ICompilableInitOption } from '../../compile/interface'; import { LayoutState } from '../../compile/interface'; -import type { IGroupGraphicAttribute, IGraphicAttribute, IGroup, IGraphic } from '@visactor/vrender-core'; +import type { + IGroupGraphicAttribute, + IGraphicAttribute, + IGroup, + IGraphic, + StateDefinitionsInput, + StateResolveContext +} from '@visactor/vrender-core'; import { createGroup, CustomPath2D } from '@visactor/vrender-core'; import { isStateAttrChangeable } from '../../compile/mark/util'; import { Factory } from '../../core/factory'; @@ -70,6 +77,7 @@ import { CompilableData } from '../../compile/data/compilable-data'; import { getDiffAttributesOfGraphic } from '../../util/mark'; import { log } from '../../util/debug'; import { morph as runMorph } from '../../compile/morph'; +import { addGraphicState, removeGraphicState } from '../../util/graphic-state'; export type ExChannelCall = ( key: string | number | symbol, @@ -257,13 +265,13 @@ export class BaseMark extends GrammarItem implements IMar protected _stateSort?: (stateA: string, stateB: string) => number; - declare protected _product: Maybe; + protected declare _product: Maybe; getProduct() { return this._product; } // 保存上一次的mark,用于morph的时候获取上次的图元 - declare protected _lastMark?: IMark; + protected declare _lastMark?: IMark; /** 初始化 mark data */ protected initMarkData(option: ICompilableInitOption) { @@ -526,7 +534,7 @@ export class BaseMark extends GrammarItem implements IMar return this._simpleStyle; } - declare protected _option: IMarkOption; + protected declare _option: IMarkOption; protected _attributeContext: IModelMarkAttributeContext; @@ -1155,8 +1163,8 @@ export class BaseMark extends GrammarItem implements IMar this._keyGetter = isFunction(this.key) ? (this.key as (datum: Datum) => string) : isValid(this.key) - ? (datum: Datum) => datum?.[this.key as string] - : (datum: Datum) => datum?.[DEFAULT_DATA_KEY]; + ? (datum: Datum) => datum?.[this.key as string] + : (datum: Datum) => datum?.[DEFAULT_DATA_KEY]; this._groupKeyGetter = isValid(this._groupKey) ? (datum: Datum) => { return `${datum?.[this._groupKey]}`; @@ -1171,8 +1179,8 @@ export class BaseMark extends GrammarItem implements IMar const animationState = graphicsAnimationStates.every(state => state === AnimationStateEnum.appear) ? AnimationStateEnum.appear : graphicsAnimationStates.every(state => state === AnimationStateEnum.disappear) - ? AnimationStateEnum.disappear - : graphicsAnimationStates[0]; + ? AnimationStateEnum.disappear + : graphicsAnimationStates[0]; return animationState ?? AnimationStateEnum.none; } @@ -1193,7 +1201,7 @@ export class BaseMark extends GrammarItem implements IMar ? { ...config, // 循环动画的优先级定为最高,不会被屏蔽掉 - priority: type === 'normal' ? (config.priority ?? Infinity) : config.priority + priority: type === 'normal' ? config.priority ?? Infinity : config.priority } : config; } @@ -1318,8 +1326,8 @@ export class BaseMark extends GrammarItem implements IMar return diffState === AnimationStateEnum.exit ? AnimationStateEnum.exit : diffState === AnimationStateEnum.update - ? AnimationStateEnum.update - : AnimationStateEnum.appear; + ? AnimationStateEnum.update + : AnimationStateEnum.appear; }); const customizedState = callback(g); @@ -1545,30 +1553,55 @@ export class BaseMark extends GrammarItem implements IMar g.stateProxy = null; if (g.context.diffState === DiffState.enter || g.context.diffState === DiffState.update) { - g.stateProxy = (stateName: string, nexStates: string[]) => { - return this._runEncoderOfGraphic(this._encoderOfState?.[stateName], g); - }; - g.context.states && g.useStates(g.context.states, hasAnimation); } }; + protected _applySharedStateDefinitions() { + if (!this._product) { + return; + } + + const stateNames = Object.keys(this._encoderOfState ?? {}).filter( + stateName => stateName !== 'group' && stateName !== 'update' + ); + + if (!stateNames.length) { + this._product.sharedStateDefinitions = undefined; + return; + } + + const sortedStateNames = this._stateSort ? stateNames.slice().sort(this._stateSort) : stateNames; + const statePriority = new Map(); + + sortedStateNames.forEach((stateName, index) => { + statePriority.set(stateName, index); + }); + + const sharedStateDefinitions: StateDefinitionsInput> = {}; + + stateNames.forEach(stateName => { + const encoder = this._encoderOfState[stateName]; + + sharedStateDefinitions[stateName] = { + priority: statePriority.get(stateName) ?? 0, + declaredAffectedKeys: Object.keys(encoder ?? {}), + resolver: ({ graphic }: StateResolveContext>) => + this._runEncoderOfGraphic(encoder, graphic as IMarkGraphic) + }; + }); + + this._product.sharedStateDefinitions = sharedStateDefinitions; + } + protected _addProgressiveGraphic(parent: IGroup, g: IMarkGraphic) { (parent as IGroup).incrementalAppendChild(g); } protected _runEncoder(graphics: IMarkGraphic[], noGroupEncode?: boolean) { const attrsByGroup = noGroupEncode ? null : this._runGroupEncoder(this._encoderOfState?.group); - graphics.forEach((g, index) => { - let attrs = this._runEncoderOfGraphic(this._encoderOfState?.update, g); - // 此时需要将最终的正确的样式设置给graphic,这样后续的动画目标属性才会正确,否则会动画样式只有默认状态的样式 - g.currentStates?.forEach((_state: string) => { - const stateAttr = this._runEncoderOfGraphic(this._encoderOfState?.[_state], g); - attrs = { - ...attrs, - ...stateAttr - }; - }); + graphics.forEach(g => { + const attrs = this._runEncoderOfGraphic(this._encoderOfState?.update, g); // 配置的优先级高于encoder if (!isNil(this._markConfig.interactive)) { @@ -1754,6 +1787,7 @@ export class BaseMark extends GrammarItem implements IMar renderInner() { this._updateEncoderByState(); + this._applySharedStateDefinitions(); const data = this._data?.getProduct() ?? [{}]; @@ -1818,9 +1852,9 @@ export class BaseMark extends GrammarItem implements IMar this._graphics.forEach(g => { if (this.state.checkOneState(g, g.context.data, stateInfo) === 'in') { - g.addState(key, true, this.hasAnimationByState('state')); + addGraphicState(g, key, true, this.hasAnimationByState('state')); } else { - g.removeState(key, this.hasAnimationByState('state')); + removeGraphicState(g, key, this.hasAnimationByState('state')); } }); } diff --git a/packages/vchart/src/mark/utils/glyph.ts b/packages/vchart/src/mark/utils/glyph.ts index fa249e08fe..44267562eb 100644 --- a/packages/vchart/src/mark/utils/glyph.ts +++ b/packages/vchart/src/mark/utils/glyph.ts @@ -1,4 +1,5 @@ import type { IMarkGraphic } from '../interface/common'; +import { addGraphicState, removeGraphicState } from '../../util/graphic-state'; export const addRuntimeState = ( g: IMarkGraphic, @@ -14,7 +15,7 @@ export const addRuntimeState = ( g.runtimeStateCache[stateName] = attrs; if (g.hasState(stateName)) { - g.removeState(stateName); + removeGraphicState(g, stateName); } - g.addState(stateName, keepCurrentStates, hasAnimation); + addGraphicState(g, stateName, keepCurrentStates, hasAnimation); }; diff --git a/packages/vchart/src/series/sankey/sankey.ts b/packages/vchart/src/series/sankey/sankey.ts index 19989344b8..bcec07cf57 100644 --- a/packages/vchart/src/series/sankey/sankey.ts +++ b/packages/vchart/src/series/sankey/sankey.ts @@ -40,6 +40,7 @@ import type { ILabelSpec } from '../../component'; import { getDatumOfGraphic } from '../../util'; import { addRuntimeState } from '../../mark/utils/glyph'; import { sankey } from '../../theme/builtin/common/series/sankey'; +import { addGraphicState, removeGraphicState } from '../../util/graphic-state'; export class SankeySeries extends CartesianSeries { static readonly type: string = SeriesTypeEnum.sankey; @@ -520,12 +521,10 @@ export class SankeySeries exten // const states = [STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE]; allNodeElements.forEach(el => { - el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + removeGraphicState(el, [STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE]); }); allLinkElements.forEach(el => { - el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + removeGraphicState(el, [STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE]); }); this._needClear = false; @@ -551,19 +550,19 @@ export class SankeySeries exten highlightNodes.push(linkDatum.target); } - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); // 设置上用户配置选中状态 + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + addGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); // 设置上用户配置选中状态 } else if (linkDatum.target === nodeDatum.key) { // 上游link if (!highlightNodes.includes(linkDatum.source)) { highlightNodes.push(linkDatum.source); } - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); // 设置上用户配置选中状态 + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + addGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); // 设置上用户配置选中状态 } else { - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + addGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); } }); } @@ -586,12 +585,12 @@ export class SankeySeries exten } allLinkElements.forEach(linkEl => { if (linkEl === graphic) { - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio: 1 }); } else { - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + addGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); } }); } @@ -717,11 +716,11 @@ export class SankeySeries exten const linkDatum = getDatumOfGraphic(linkEl) as Datum; if (highlightLinks.includes(linkDatum.key ?? linkDatum.index)) { - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + addGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); } else { - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + addGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); } }); } @@ -786,7 +785,7 @@ export class SankeySeries exten }, 0); const ratio = val / linkDatum.value; - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio }); @@ -803,7 +802,7 @@ export class SankeySeries exten highlightNodes.push(linkDatum.target); } - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio: upSelectedLink.value / linkDatum.value @@ -812,8 +811,8 @@ export class SankeySeries exten return; } - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + addGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); return; }); @@ -843,17 +842,19 @@ export class SankeySeries exten // const states = [STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE]; if (this._linkMark) { allLinkElements.forEach(linkEl => { - // linkEl.removeState(states); - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + removeGraphicState(linkEl, [ + STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, + STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE + ]); }); } if (this._nodeMark) { allNodeElements.forEach(el => { - // el.removeState(states); - el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - el.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + removeGraphicState(el, [ + STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, + STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE + ]); }); } } else { @@ -895,7 +896,7 @@ export class SankeySeries exten if (linkDatum.source === curLinkDatum.source && linkDatum.target === curLinkDatum.target) { // 自身 - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio: 1 }); return; @@ -929,7 +930,7 @@ export class SankeySeries exten }, 0); const ratio = val / linkDatum.value; - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio }); // 设置默认的部分高亮 @@ -948,7 +949,7 @@ export class SankeySeries exten if (!highlightNodes.includes(linkDatum.target)) { highlightNodes.push(linkDatum.target); } - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE); addRuntimeState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, { ratio: upSelectedLink.value / (linkDatum as Datum).value @@ -956,8 +957,8 @@ export class SankeySeries exten return; } - linkEl.removeState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); - linkEl.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); + removeGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS); + addGraphicState(linkEl, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); return; }); @@ -974,12 +975,12 @@ export class SankeySeries exten } graphics.forEach(g => { - g.removeState([STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS]); + removeGraphicState(g, [STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS]); if (highlightNodes.includes((getDatumOfGraphic(g) as Datum).key)) { - g.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); + addGraphicState(g, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS, true); } else { - g.addState(STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); + addGraphicState(g, STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS_REVERSE, true); } }); } diff --git a/packages/vchart/src/util/graphic-state.ts b/packages/vchart/src/util/graphic-state.ts new file mode 100644 index 0000000000..3edd772621 --- /dev/null +++ b/packages/vchart/src/util/graphic-state.ts @@ -0,0 +1,32 @@ +import type { IMarkGraphic } from '../mark/interface'; + +const normalizeStates = (states?: string | string[]) => (Array.isArray(states) ? states : states ? [states] : []); + +export const addGraphicState = ( + graphic: IMarkGraphic, + state: string, + keepCurrentStates: boolean = true, + hasAnimation?: boolean +) => { + if (!state) { + return; + } + + const currentStates = keepCurrentStates ? graphic.currentStates ?? [] : []; + const nextStates = keepCurrentStates ? Array.from(new Set([...currentStates, state])) : [state]; + + graphic.useStates(nextStates, hasAnimation); +}; + +export const removeGraphicState = (graphic: IMarkGraphic, state: string | string[], hasAnimation?: boolean) => { + const states = normalizeStates(state); + + if (!states.length) { + return; + } + + const currentStates = (graphic.currentStates ?? []) as string[]; + const nextStates = currentStates.filter((stateName: string) => !states.includes(stateName)); + + graphic.useStates(nextStates, hasAnimation); +}; From 2a1b6ca9a529ac21d503fae73f08f1c52f666094 Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Tue, 5 May 2026 13:20:27 +0800 Subject: [PATCH 3/8] chore: enable VChart validation on VRender alpha.8 Consume published VRender alpha.8 packages for PR validation. Refresh VChart direct deps and Rush lockfile only. Constraint: Use npm registry @visactor/vrender*@1.1.0-alpha.8. Constraint: No local VRender2 link, file, workspace, yalc, or pack. Rejected: Local VRender2 linking | PR must test published packages. Confidence: high Scope-risk: moderate Directive: Keep VRender release validation on npm artifacts. Tested: rush update; rush install --check-only. Tested: rush test --only @visactor/vchart. Tested: rush build --to @visactor/vchart; browser smoke. --- common/config/rush/pnpm-lock.yaml | 120 ++++++++++++------------- docs/package.json | 4 +- packages/openinula-vchart/package.json | 4 +- packages/react-vchart/package.json | 4 +- packages/vchart-extension/package.json | 8 +- packages/vchart/package.json | 10 +-- tools/story-player/package.json | 6 +- 7 files changed, 78 insertions(+), 78 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 6862a761e8..acd53bb92f 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -37,11 +37,11 @@ importers: specifier: 1.2.4-alpha.5 version: 1.2.4-alpha.5 '@visactor/vrender': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vtable': specifier: 1.19.0-alpha.0 version: 1.19.0-alpha.0 @@ -203,11 +203,11 @@ importers: specifier: workspace:2.0.22 version: link:../vchart '@visactor/vrender-core': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -294,11 +294,11 @@ importers: specifier: workspace:2.0.22 version: link:../vchart-extension '@visactor/vrender-core': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -529,20 +529,20 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-animate': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-components': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-core': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vscale': specifier: ~1.0.23 version: 1.0.23 @@ -695,17 +695,17 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender-animate': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-components': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-core': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -1263,14 +1263,14 @@ importers: specifier: workspace:2.0.22 version: link:../../packages/vchart '@visactor/vrender': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-core': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.2 - version: 1.1.0-alpha.2 + specifier: 1.1.0-alpha.8 + version: 1.1.0-alpha.8 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -3076,29 +3076,29 @@ packages: '@visactor/vrender-animate@1.0.0-alpha.18': resolution: {integrity: sha512-9kTtvp1ef+1t+AtUiza6A7qBQP7SmvOu3/ILGrqs/HGdZVj1XGjbYvD/X/zwKJ3LEb7gGV5fa8x95e4czTvRSA==} - '@visactor/vrender-animate@1.1.0-alpha.2': - resolution: {integrity: sha512-jlegJK92jVvT/PF16nMeRjlN68IoSgvJz/tKgsZVveJCt5276LSm2K5vpIjXNeaEKvmUnKes+45sZZX6kWNC/A==} + '@visactor/vrender-animate@1.1.0-alpha.8': + resolution: {integrity: sha512-T1CxxlF2QXJ9ElfzwV2tidNW2BZrU2kkk4vCEs7tAZbk4blQvFzUKosU+rZnIUXvmUQ1SLcjpGb6nwiJ35qL9Q==} '@visactor/vrender-components@1.0.0-alpha.18': resolution: {integrity: sha512-7Euq+ZfswL74n2pgkaqZSsPxoSa5SPIGyXatN1eUrdzM2Z0kX6U0RcJg01fctvRs4op6WhcecRLqGvnHcBeb9Q==} - '@visactor/vrender-components@1.1.0-alpha.2': - resolution: {integrity: sha512-nmBTBXEPvDRYbvXtOCLZ89+9WK1A+wLsE4RbuKv2omjA9D+XYRE7QZsW3O1g1/XBD8JqDVDiz+tk2Pp11IBBMw==} + '@visactor/vrender-components@1.1.0-alpha.8': + resolution: {integrity: sha512-UgiULMccv93ta4nI3Ee6LO8+LOpiZNDhXAM/xvFbIx9FccpOYWxdQKzH2B7aGzx1HlINz7Pp2+2QWi6+MedPHA==} '@visactor/vrender-core@1.0.0-alpha.18': resolution: {integrity: sha512-0ihtNvCyNkOsWPFgRqowHzq0IcQgS2Wl/nPpKbVtxWKveenwlhA+ZKoQvam6VJyBY7jeNe1pROy0mJMDyVAJQw==} - '@visactor/vrender-core@1.1.0-alpha.2': - resolution: {integrity: sha512-f9BTOK3Rv5yiTxMJMPLV2VYIgwcsakJ/VXCOp88hbWgy/or2pvPW4/42sOLALDMCjSnpd2zGhV2R2vy2+nSD2w==} + '@visactor/vrender-core@1.1.0-alpha.8': + resolution: {integrity: sha512-Wvvr+G9MVCkigaYoK1a7Y29AphgunA9cgGquetMF8Bch9emMv6KeFer2hUgSCRxD90MV53OxNHKtpIKVVSOPkw==} '@visactor/vrender-kits@1.0.0-alpha.18': resolution: {integrity: sha512-Tvolkq+4G8qiPFZo0Aj8M//Yr6jR2h8FNkFEyWM9gbQbEiTkjpmHAJOYnoSsaPtPrcMSlG4EhJSFDk6ymANHVg==} - '@visactor/vrender-kits@1.1.0-alpha.2': - resolution: {integrity: sha512-fXG3SB/PDcmA2oK6FqHTBvb8438BDeeCkpIvzt1WI/lpghzD6wT7Dd3HhY8hxonArDUUqAsorZyFeQFNPezUTQ==} + '@visactor/vrender-kits@1.1.0-alpha.8': + resolution: {integrity: sha512-jBUL5UMlEArtjrbHs0D+cNmr2sNgDpDTVcWNRg43JeK4JZTL+WySx5UkAjkUY03N0arskX96O6g5sQwZG9u5tQ==} - '@visactor/vrender@1.1.0-alpha.2': - resolution: {integrity: sha512-MNW2rTJLgCVa0T4TTavQ+fMJp2PkvkZwxWAwdzdNlFAIX9WriF24L+Xqwu8nvqsJiUz2BEUbgYP+7sTZcn7krA==} + '@visactor/vrender@1.1.0-alpha.8': + resolution: {integrity: sha512-Bn9T9T3yFqpvSS7rVTqvL59A6XPsGDY5TNqJ8K1Mb1GCASfv5YS70NTFmQipvWHrebatWyodKlEEradbflMegA==} '@visactor/vscale@0.18.18': resolution: {integrity: sha512-iRG4kv+5Fv4KX3AxEfV95XU3I6OmF0QizyAhqHxKa7L1MaT+MRvDDk5zHWf1E8gialLbL2xDe3GnT6g/4u5jhA==} @@ -15029,9 +15029,9 @@ snapshots: '@visactor/vrender-core': 1.0.0-alpha.18 '@visactor/vutils': 1.0.4 - '@visactor/vrender-animate@1.1.0-alpha.2': + '@visactor/vrender-animate@1.1.0-alpha.8': dependencies: - '@visactor/vrender-core': 1.1.0-alpha.2 + '@visactor/vrender-core': 1.1.0-alpha.8 '@visactor/vutils': 1.0.23 '@visactor/vrender-components@1.0.0-alpha.18': @@ -15042,11 +15042,11 @@ snapshots: '@visactor/vscale': 1.0.4 '@visactor/vutils': 1.0.4 - '@visactor/vrender-components@1.1.0-alpha.2': + '@visactor/vrender-components@1.1.0-alpha.8': dependencies: - '@visactor/vrender-animate': 1.1.0-alpha.2 - '@visactor/vrender-core': 1.1.0-alpha.2 - '@visactor/vrender-kits': 1.1.0-alpha.2 + '@visactor/vrender-animate': 1.1.0-alpha.8 + '@visactor/vrender-core': 1.1.0-alpha.8 + '@visactor/vrender-kits': 1.1.0-alpha.8 '@visactor/vscale': 1.0.23 '@visactor/vutils': 1.0.23 @@ -15055,7 +15055,7 @@ snapshots: '@visactor/vutils': 1.0.4 color-convert: 2.0.1 - '@visactor/vrender-core@1.1.0-alpha.2': + '@visactor/vrender-core@1.1.0-alpha.8': dependencies: '@visactor/vutils': 1.0.23 color-convert: 2.0.1 @@ -15069,21 +15069,21 @@ snapshots: lottie-web: 5.13.0 roughjs: 4.5.2 - '@visactor/vrender-kits@1.1.0-alpha.2': + '@visactor/vrender-kits@1.1.0-alpha.8': dependencies: '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.1.0-alpha.2 + '@visactor/vrender-core': 1.1.0-alpha.8 '@visactor/vutils': 1.0.23 gifuct-js: 2.1.2 lottie-web: 5.13.0 roughjs: 4.6.6 - '@visactor/vrender@1.1.0-alpha.2': + '@visactor/vrender@1.1.0-alpha.8': dependencies: - '@visactor/vrender-animate': 1.1.0-alpha.2 - '@visactor/vrender-components': 1.1.0-alpha.2 - '@visactor/vrender-core': 1.1.0-alpha.2 - '@visactor/vrender-kits': 1.1.0-alpha.2 + '@visactor/vrender-animate': 1.1.0-alpha.8 + '@visactor/vrender-components': 1.1.0-alpha.8 + '@visactor/vrender-core': 1.1.0-alpha.8 + '@visactor/vrender-kits': 1.1.0-alpha.8 '@visactor/vscale@0.18.18': dependencies: diff --git a/docs/package.json b/docs/package.json index 916924de51..98e9ce617c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,8 +19,8 @@ "@visactor/vchart-theme": "~1.6.6", "@visactor/vmind": "1.2.4-alpha.5", "@visactor/vutils": "~1.0.23", - "@visactor/vrender": "1.1.0-alpha.2", - "@visactor/vrender-kits": "1.1.0-alpha.2", + "@visactor/vrender": "1.1.0-alpha.8", + "@visactor/vrender-kits": "1.1.0-alpha.8", "@visactor/vtable": "1.19.0-alpha.0", "@visactor/vtable-editors": "1.19.0-alpha.0", "@visactor/vtable-gantt": "1.19.0-alpha.0", diff --git a/packages/openinula-vchart/package.json b/packages/openinula-vchart/package.json index bcc4a86dae..4a6f62d2a5 100644 --- a/packages/openinula-vchart/package.json +++ b/packages/openinula-vchart/package.json @@ -30,8 +30,8 @@ "dependencies": { "@visactor/vchart": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "1.1.0-alpha.2", - "@visactor/vrender-kits": "1.1.0-alpha.2", + "@visactor/vrender-core": "1.1.0-alpha.8", + "@visactor/vrender-kits": "1.1.0-alpha.8", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/react-vchart/package.json b/packages/react-vchart/package.json index 3aa5be4964..c9afe14ab1 100644 --- a/packages/react-vchart/package.json +++ b/packages/react-vchart/package.json @@ -36,8 +36,8 @@ "@visactor/vchart": "workspace:2.0.22", "@visactor/vchart-extension": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "1.1.0-alpha.2", - "@visactor/vrender-kits": "1.1.0-alpha.2", + "@visactor/vrender-core": "1.1.0-alpha.8", + "@visactor/vrender-kits": "1.1.0-alpha.8", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/vchart-extension/package.json b/packages/vchart-extension/package.json index 25789045fa..5136ef67d1 100644 --- a/packages/vchart-extension/package.json +++ b/packages/vchart-extension/package.json @@ -26,10 +26,10 @@ "start": "ts-node __tests__/runtime/browser/scripts/initVite.ts && vite serve __tests__/runtime/browser" }, "dependencies": { - "@visactor/vrender-core": "1.1.0-alpha.2", - "@visactor/vrender-kits": "1.1.0-alpha.2", - "@visactor/vrender-components": "1.1.0-alpha.2", - "@visactor/vrender-animate": "1.1.0-alpha.2", + "@visactor/vrender-core": "1.1.0-alpha.8", + "@visactor/vrender-kits": "1.1.0-alpha.8", + "@visactor/vrender-components": "1.1.0-alpha.8", + "@visactor/vrender-animate": "1.1.0-alpha.8", "@visactor/vchart": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", "@visactor/vdataset": "~1.0.23", diff --git a/packages/vchart/package.json b/packages/vchart/package.json index b684b8e7c0..6052a4dc7f 100644 --- a/packages/vchart/package.json +++ b/packages/vchart/package.json @@ -126,11 +126,11 @@ "@visactor/vdataset": "~1.0.23", "@visactor/vscale": "~1.0.23", "@visactor/vlayouts": "~1.0.23", - "@visactor/vrender": "1.1.0-alpha.2", - "@visactor/vrender-core": "1.1.0-alpha.2", - "@visactor/vrender-kits": "1.1.0-alpha.2", - "@visactor/vrender-components": "1.1.0-alpha.2", - "@visactor/vrender-animate": "1.1.0-alpha.2", + "@visactor/vrender": "1.1.0-alpha.8", + "@visactor/vrender-core": "1.1.0-alpha.8", + "@visactor/vrender-kits": "1.1.0-alpha.8", + "@visactor/vrender-components": "1.1.0-alpha.8", + "@visactor/vrender-animate": "1.1.0-alpha.8", "@visactor/vutils-extension": "workspace:2.0.22" }, "publishConfig": { diff --git a/tools/story-player/package.json b/tools/story-player/package.json index 6339cca92c..64bea90749 100644 --- a/tools/story-player/package.json +++ b/tools/story-player/package.json @@ -56,10 +56,10 @@ "vite": "3.2.6" }, "dependencies": { - "@visactor/vrender-core": "1.1.0-alpha.2", - "@visactor/vrender-kits": "1.1.0-alpha.2", + "@visactor/vrender-core": "1.1.0-alpha.8", + "@visactor/vrender-kits": "1.1.0-alpha.8", "@visactor/vchart": "workspace:2.0.22", - "@visactor/vrender": "1.1.0-alpha.2", + "@visactor/vrender": "1.1.0-alpha.8", "@visactor/vutils": "~1.0.23" } } From 6bc51096f693f7460b10320b4e817af385a7c3a3 Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Tue, 5 May 2026 16:34:34 +0800 Subject: [PATCH 4/8] test: cover VChart animation static truth with manual ticker Add deterministic regression coverage for VRender alpha animation behavior. It exercises VChart with ManualTicker instead of browser timing. Constraint: Uses npm VRender through VChart exports. Confidence: high Scope-risk: narrow Tested: eslint touched animation test --quiet Tested: jest manual-ticker.test.ts --runInBand --silent=false Tested: rush test --only @visactor/vchart Tested: rush install --check-only Tested: rush build --to @visactor/vchart --- .../unit/animation/manual-ticker.test.ts | 403 ++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 packages/vchart/__tests__/unit/animation/manual-ticker.test.ts diff --git a/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts b/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts new file mode 100644 index 0000000000..fe9b5662b0 --- /dev/null +++ b/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts @@ -0,0 +1,403 @@ +import VChart, { + ManualTicker, + type IBarChartSpec, + type IChart, + type IMark, + type IMarkGraphic, + type ISeries +} from '../../../src'; +import { createDiv, removeDom } from '../../util/dom'; + +type AnimatedGraphic = IMarkGraphic & { + attribute: Record; + baseAttributes?: Record; + finalAttribute?: Record; + getFinalAttribute?: () => Record | undefined; +}; + +type TraversableGraphic = AnimatedGraphic & { + type?: string; + name?: string; + parent?: TraversableGraphic; + forEachChildren?: (cb: (child: TraversableGraphic) => void | boolean) => void; +}; + +const APPEAR_DURATION = 300; +const UPDATE_DURATION = 300; +const COLOR_BY_SERIES: Record = { + 'east-利润profit': '#8D72F6', + 'east-销售量sales': '#5766EC', + 'north of east-利润profit': '#66A3FE', + 'north of east-销售量sales': '#51D5E6' +}; + +const createChartContainer = () => { + const container = createDiv(); + const dom = createDiv(container); + + container.style.position = 'fixed'; + container.style.width = '500px'; + container.style.height = '500px'; + container.style.top = '0px'; + container.style.left = '0px'; + + return { container, dom }; +}; + +const createManualTicker = () => { + const ticker = new ManualTicker(); + + ticker.autoStop = false; + + return ticker; +}; + +const getGraphicFinalAttribute = (graphic: AnimatedGraphic) => + graphic.finalAttribute ?? graphic.getFinalAttribute?.() ?? {}; + +const expectClose = (actual: number, expected: number) => { + expect(actual).toBeCloseTo(expected, 6); +}; + +const expectBarYLayout = (graphic: AnimatedGraphic, expected: { y: number; y1: number }) => { + expectClose(graphic.attribute.y, expected.y); + expectClose(graphic.attribute.y1, expected.y1); + expectClose(graphic.baseAttributes?.y, expected.y); + expectClose(graphic.baseAttributes?.y1, expected.y1); + expectClose(getGraphicFinalAttribute(graphic).y, expected.y); + expectClose(getGraphicFinalAttribute(graphic).y1, expected.y1); +}; + +const getBarGraphics = (chart: VChart) => { + const model = chart.getChart() as IChart; + const barSeries = model.getAllSeries().find((series: ISeries) => series.type === 'bar'); + + expect(barSeries).toBeDefined(); + if (!barSeries) { + throw new Error('Expected bar series to exist'); + } + + const barMark = barSeries.getMarks().find((mark: IMark) => mark.name === 'bar'); + + expect(barMark).toBeDefined(); + if (!barMark) { + throw new Error('Expected bar mark to exist'); + } + + return barMark.getGraphics() as AnimatedGraphic[]; +}; + +const walkGraphics = (root: TraversableGraphic, visitor: (graphic: TraversableGraphic) => void) => { + visitor(root); + root.forEachChildren?.((child: TraversableGraphic) => { + walkGraphics(child, visitor); + }); +}; + +const getAnimatedLabelTexts = (chart: VChart) => { + const texts: TraversableGraphic[] = []; + + walkGraphics(chart.getStage() as unknown as TraversableGraphic, graphic => { + if (graphic.type === 'text' && graphic.baseAttributes && getGraphicFinalAttribute(graphic).x !== undefined) { + texts.push(graphic); + } + }); + + return texts; +}; + +const getLabelTextByFill = (chart: VChart, fill: string) => { + const label = getAnimatedLabelTexts(chart).find(graphic => graphic.attribute.fill === fill); + + expect(label).toBeDefined(); + if (!label) { + throw new Error(`Expected label with fill ${fill} to exist`); + } + + return label; +}; + +const getVisibleBarByFill = (chart: VChart, fill: string) => { + const bar = getBarGraphics(chart).find( + graphic => graphic.attribute.fill === fill && graphic.attribute.visible !== false + ); + + expect(bar).toBeDefined(); + if (!bar) { + throw new Error(`Expected visible bar with fill ${fill} to exist`); + } + + return bar; +}; + +const getBarCenterX = (graphic: AnimatedGraphic) => { + const attrs = getGraphicFinalAttribute(graphic); + + return attrs.x + attrs.width / 2; +}; + +const expectStaticXLayout = (graphic: AnimatedGraphic, expectedX: number) => { + expectClose(graphic.baseAttributes?.x, expectedX); + expectClose(getGraphicFinalAttribute(graphic).x, expectedX); +}; + +describe('manual ticker animation regressions', () => { + it('keeps default bar appear starts out of static truth', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart( + { + type: 'bar', + width: 400, + height: 300, + data: [ + { + id: 'barData', + values: [{ month: 'Monday', sales: 22 }] + } + ], + xField: 'month', + yField: 'sales', + axes: [ + { orient: 'left', visible: false }, + { orient: 'bottom', visible: false } + ], + animationAppear: { + duration: APPEAR_DURATION, + easing: 'linear' + } + } as IBarChartSpec, + { + dom, + ticker, + animation: true + } + ); + + chart.renderSync(); + + try { + const barGraphic = getBarGraphics(chart)[0]; + const expectedFinalLayout = { + y: getGraphicFinalAttribute(barGraphic).y, + y1: getGraphicFinalAttribute(barGraphic).y1 + }; + + expect(expectedFinalLayout.y).not.toBe(expectedFinalLayout.y1); + expect(barGraphic.baseAttributes?.y).toBe(expectedFinalLayout.y); + expect(barGraphic.baseAttributes?.y1).toBe(expectedFinalLayout.y1); + + ticker.tickAt(APPEAR_DURATION / 2); + + expect(barGraphic.attribute.y).not.toBe(barGraphic.attribute.y1); + expect(barGraphic.baseAttributes?.y).toBe(expectedFinalLayout.y); + expect(barGraphic.baseAttributes?.y1).toBe(expectedFinalLayout.y1); + expect(getGraphicFinalAttribute(barGraphic).y).toBe(expectedFinalLayout.y); + expect(getGraphicFinalAttribute(barGraphic).y1).toBe(expectedFinalLayout.y1); + + ticker.tickAt(APPEAR_DURATION + 50); + + expectBarYLayout(barGraphic, expectedFinalLayout); + expect(barGraphic.attribute.y).not.toBe(barGraphic.attribute.y1); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); + + it('keeps data label update final x at the filtered bar position', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart( + { + type: 'bar', + direction: 'vertical', + width: 500, + height: 300, + xField: ['date', '__DimGroup__'], + yField: '__MeaValue__', + seriesField: '__DimGroupID__', + padding: 0, + region: [ + { + clip: true + } + ], + animation: true, + animationAppear: { + duration: APPEAR_DURATION, + easing: 'linear' + }, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + }, + stackCornerRadius: [4, 4, 0, 0], + color: { + type: 'ordinal', + domain: Object.keys(COLOR_BY_SERIES), + range: Object.values(COLOR_BY_SERIES), + specified: {} + }, + data: { + values: [ + { + date: '2019', + region: 'east', + __OriginalData__: { + date: '2019', + region: 'east', + profit: 10, + sales: 20 + }, + profit: 10, + __MeaId__: 'profit', + __MeaName__: '利润', + __MeaValue__: 10, + __DimGroup__: 'east-利润', + __DimGroupID__: 'east-利润profit' + }, + { + date: '2019', + region: 'east', + __OriginalData__: { + date: '2019', + region: 'east', + profit: 10, + sales: 20 + }, + sales: 20, + __MeaId__: 'sales', + __MeaName__: '销售量', + __MeaValue__: 20, + __DimGroup__: 'east-销售量', + __DimGroupID__: 'east-销售量sales' + }, + { + date: '2019', + region: 'north of east', + __OriginalData__: { + date: '2019', + region: 'north of east', + profit: 10, + sales: 20 + }, + profit: 10, + __MeaId__: 'profit', + __MeaName__: '利润', + __MeaValue__: 10, + __DimGroup__: 'north of east-利润', + __DimGroupID__: 'north of east-利润profit' + }, + { + date: '2019', + region: 'north of east', + __OriginalData__: { + date: '2019', + region: 'north of east', + profit: 10, + sales: 20 + }, + sales: 20, + __MeaId__: 'sales', + __MeaName__: '销售量', + __MeaValue__: 20, + __DimGroup__: 'north of east-销售量', + __DimGroupID__: 'north of east-销售量sales' + } + ] + }, + label: { + visible: true, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + } + }, + legends: { + type: 'discrete', + visible: true, + maxCol: 1, + maxRow: 1, + autoPage: true, + orient: 'right', + position: 'start', + item: { + focus: true, + background: { + state: { + selectedHover: { + fill: '#646A73', + fillOpacity: 0.05 + } + } + } + } + } + } as unknown as IBarChartSpec, + { + dom, + ticker, + animation: true + } + ); + + chart.renderSync(); + + try { + ticker.tickAt(APPEAR_DURATION + 50); + + const retainedSeries = Object.keys(COLOR_BY_SERIES).slice(0, 3); + const initialXBySeries = retainedSeries.reduce>((result, seriesName) => { + result[seriesName] = getLabelTextByFill(chart, COLOR_BY_SERIES[seriesName]).attribute.x; + return result; + }, {}); + + chart.setLegendSelectedDataByIndex(0, retainedSeries); + chart.renderSync(); + + const updateStart = ticker.getTime(); + const expectedXBySeries = retainedSeries.reduce>((result, seriesName) => { + result[seriesName] = getBarCenterX(getVisibleBarByFill(chart, COLOR_BY_SERIES[seriesName])); + return result; + }, {}); + + expect(retainedSeries.some(seriesName => initialXBySeries[seriesName] !== expectedXBySeries[seriesName])).toBe( + true + ); + + retainedSeries.forEach(seriesName => { + expectStaticXLayout(getLabelTextByFill(chart, COLOR_BY_SERIES[seriesName]), expectedXBySeries[seriesName]); + }); + + ticker.tickAt(updateStart + UPDATE_DURATION / 2); + + retainedSeries.forEach(seriesName => { + const label = getLabelTextByFill(chart, COLOR_BY_SERIES[seriesName]); + const startX = initialXBySeries[seriesName]; + const finalX = expectedXBySeries[seriesName]; + + if (startX !== finalX) { + expect(label.attribute.x).toBeGreaterThanOrEqual(Math.min(startX, finalX)); + expect(label.attribute.x).toBeLessThanOrEqual(Math.max(startX, finalX)); + } + expectStaticXLayout(label, finalX); + }); + + ticker.tickAt(updateStart + UPDATE_DURATION + 50); + + retainedSeries.forEach(seriesName => { + const label = getLabelTextByFill(chart, COLOR_BY_SERIES[seriesName]); + const finalX = expectedXBySeries[seriesName]; + + expectClose(label.attribute.x, finalX); + expectStaticXLayout(label, finalX); + }); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); +}); From 5ba9f2c7860641ce46059ca6d768b7b294c0f9bd Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Wed, 6 May 2026 14:30:31 +0800 Subject: [PATCH 5/8] fix: keep miniapp builds from resolving shadow-root fallback VRender alpha leaves a shadow-root fallback require inside the generated VChart miniapp bundle. The lark and wx second-pass Rollup builds should leave that fallback untouched instead of converting it into an unresolved proxy module. Constraint: Do not patch VRender or generated miniapp bundles. Rejected: Commit regenerated miniapp bundles | generated outputs are not source. Confidence: high Scope-risk: narrow Tested: node common/scripts/install-run-rush.js build --to @visactor/vchart Tested: node common/scripts/install-run-rush.js build --only @visactor/lark-vchart Tested: node common/scripts/install-run-rush.js build --only @visactor/wx-vchart Tested: eslint lark/wx rollup configs --quiet Tested: node common/scripts/install-run-rush.js install --check-only --- packages/lark-vchart/rollup.config.ts | 3 ++- packages/wx-vchart/rollup.config.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/lark-vchart/rollup.config.ts b/packages/lark-vchart/rollup.config.ts index 315e9985b7..97be5ae637 100644 --- a/packages/lark-vchart/rollup.config.ts +++ b/packages/lark-vchart/rollup.config.ts @@ -18,7 +18,8 @@ let options = defineConfig([ plugins: [ resolve(), commonjs({ - include: './src/vchart/index.js' + include: './src/vchart/index.js', + ignore: ['./shadow-root'] }), babel({ presets: [['@babel/preset-env']], diff --git a/packages/wx-vchart/rollup.config.ts b/packages/wx-vchart/rollup.config.ts index f76310a3b8..a91b225958 100644 --- a/packages/wx-vchart/rollup.config.ts +++ b/packages/wx-vchart/rollup.config.ts @@ -18,7 +18,8 @@ let options = defineConfig([ plugins: [ resolve(), commonjs({ - include: './miniprogram/src/vchart/index.js' + include: './miniprogram/src/vchart/index.js', + ignore: ['./shadow-root'] }), babel({ // presets: [['@babel/preset-env']], From db138714da0975f0f8724b71113bd453fb3e40d7 Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Wed, 6 May 2026 17:31:10 +0800 Subject: [PATCH 6/8] chore: validate VRender alpha.9 animation fix Consume the npm-published VRender alpha.9 packages and lock the stackCornerRadius repeated legend filtering regression with a manual ticker test. The test interrupts update animations and checks final bar plus clip-path static truth. Constraint: Registry packages only; no local VRender link or pack. Rejected: Browser-only manual repro | timing regressions need deterministic checks. Confidence: high Scope-risk: moderate Directive: Keep this regression on manual ticker timing. Tested: npm test -- --runInBand __tests__/unit/animation/manual-ticker.test.ts Tested: node common/scripts/install-run-rush.js install --check-only Tested: eslint packages/vchart/__tests__/unit/animation/manual-ticker.test.ts --quiet Tested: node common/scripts/install-run-rush.js test --only @visactor/vchart Tested: node common/scripts/install-run-rush.js build --to @visactor/vchart Not-tested: Full browser visual smoke in this turn. --- common/config/rush/pnpm-lock.yaml | 120 ++++----- docs/package.json | 4 +- packages/openinula-vchart/package.json | 4 +- packages/react-vchart/package.json | 4 +- packages/vchart-extension/package.json | 8 +- .../unit/animation/manual-ticker.test.ts | 247 ++++++++++++++++++ packages/vchart/package.json | 10 +- tools/story-player/package.json | 6 +- 8 files changed, 325 insertions(+), 78 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index acd53bb92f..15def21eab 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -37,11 +37,11 @@ importers: specifier: 1.2.4-alpha.5 version: 1.2.4-alpha.5 '@visactor/vrender': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vtable': specifier: 1.19.0-alpha.0 version: 1.19.0-alpha.0 @@ -203,11 +203,11 @@ importers: specifier: workspace:2.0.22 version: link:../vchart '@visactor/vrender-core': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -294,11 +294,11 @@ importers: specifier: workspace:2.0.22 version: link:../vchart-extension '@visactor/vrender-core': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -529,20 +529,20 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-animate': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-components': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-core': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vscale': specifier: ~1.0.23 version: 1.0.23 @@ -695,17 +695,17 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender-animate': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-components': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-core': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -1263,14 +1263,14 @@ importers: specifier: workspace:2.0.22 version: link:../../packages/vchart '@visactor/vrender': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-core': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.8 - version: 1.1.0-alpha.8 + specifier: 1.1.0-alpha.9 + version: 1.1.0-alpha.9 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -3076,29 +3076,29 @@ packages: '@visactor/vrender-animate@1.0.0-alpha.18': resolution: {integrity: sha512-9kTtvp1ef+1t+AtUiza6A7qBQP7SmvOu3/ILGrqs/HGdZVj1XGjbYvD/X/zwKJ3LEb7gGV5fa8x95e4czTvRSA==} - '@visactor/vrender-animate@1.1.0-alpha.8': - resolution: {integrity: sha512-T1CxxlF2QXJ9ElfzwV2tidNW2BZrU2kkk4vCEs7tAZbk4blQvFzUKosU+rZnIUXvmUQ1SLcjpGb6nwiJ35qL9Q==} + '@visactor/vrender-animate@1.1.0-alpha.9': + resolution: {integrity: sha512-dVWOYQeZmttKLBlkl38aDSXzcz8Oqygc8M7RKSCP+jsb0s2kns9jaiaGaK2J1uEEtLxJ4Itz3CGedB+QaqtF9Q==} '@visactor/vrender-components@1.0.0-alpha.18': resolution: {integrity: sha512-7Euq+ZfswL74n2pgkaqZSsPxoSa5SPIGyXatN1eUrdzM2Z0kX6U0RcJg01fctvRs4op6WhcecRLqGvnHcBeb9Q==} - '@visactor/vrender-components@1.1.0-alpha.8': - resolution: {integrity: sha512-UgiULMccv93ta4nI3Ee6LO8+LOpiZNDhXAM/xvFbIx9FccpOYWxdQKzH2B7aGzx1HlINz7Pp2+2QWi6+MedPHA==} + '@visactor/vrender-components@1.1.0-alpha.9': + resolution: {integrity: sha512-4PMPlLiy+Eyo9+Zhx2vKOw6WvpDQYWUaSrr4gRRKUaTmhEtJk7YNHlrhO37YBvv6iHLhKdhIQR23GvFL7AJ/Gg==} '@visactor/vrender-core@1.0.0-alpha.18': resolution: {integrity: sha512-0ihtNvCyNkOsWPFgRqowHzq0IcQgS2Wl/nPpKbVtxWKveenwlhA+ZKoQvam6VJyBY7jeNe1pROy0mJMDyVAJQw==} - '@visactor/vrender-core@1.1.0-alpha.8': - resolution: {integrity: sha512-Wvvr+G9MVCkigaYoK1a7Y29AphgunA9cgGquetMF8Bch9emMv6KeFer2hUgSCRxD90MV53OxNHKtpIKVVSOPkw==} + '@visactor/vrender-core@1.1.0-alpha.9': + resolution: {integrity: sha512-qs7iA2BKFWWQILTkWvLYjcjror2+oXMKsVE35UoqTSHMXgzqk7Em1oGAFgukEKRcYRqKMop6oNfUG8DbvH5dcg==} '@visactor/vrender-kits@1.0.0-alpha.18': resolution: {integrity: sha512-Tvolkq+4G8qiPFZo0Aj8M//Yr6jR2h8FNkFEyWM9gbQbEiTkjpmHAJOYnoSsaPtPrcMSlG4EhJSFDk6ymANHVg==} - '@visactor/vrender-kits@1.1.0-alpha.8': - resolution: {integrity: sha512-jBUL5UMlEArtjrbHs0D+cNmr2sNgDpDTVcWNRg43JeK4JZTL+WySx5UkAjkUY03N0arskX96O6g5sQwZG9u5tQ==} + '@visactor/vrender-kits@1.1.0-alpha.9': + resolution: {integrity: sha512-dACr5rnEkzncekbfraYqcDK9LVYxcUnMN/73OSwrpeog2dbwFDZ/iBoET724NecpmGmi3WAEE+pINB6/acBgvA==} - '@visactor/vrender@1.1.0-alpha.8': - resolution: {integrity: sha512-Bn9T9T3yFqpvSS7rVTqvL59A6XPsGDY5TNqJ8K1Mb1GCASfv5YS70NTFmQipvWHrebatWyodKlEEradbflMegA==} + '@visactor/vrender@1.1.0-alpha.9': + resolution: {integrity: sha512-vaUTjFPwEsZS3iIrYY5VY5bCMa45hWT5JlO1OP9daKMxN+3nU67bRRCAT1vbOTG69RCcYyqarFsTMl3wS8BqEQ==} '@visactor/vscale@0.18.18': resolution: {integrity: sha512-iRG4kv+5Fv4KX3AxEfV95XU3I6OmF0QizyAhqHxKa7L1MaT+MRvDDk5zHWf1E8gialLbL2xDe3GnT6g/4u5jhA==} @@ -15029,9 +15029,9 @@ snapshots: '@visactor/vrender-core': 1.0.0-alpha.18 '@visactor/vutils': 1.0.4 - '@visactor/vrender-animate@1.1.0-alpha.8': + '@visactor/vrender-animate@1.1.0-alpha.9': dependencies: - '@visactor/vrender-core': 1.1.0-alpha.8 + '@visactor/vrender-core': 1.1.0-alpha.9 '@visactor/vutils': 1.0.23 '@visactor/vrender-components@1.0.0-alpha.18': @@ -15042,11 +15042,11 @@ snapshots: '@visactor/vscale': 1.0.4 '@visactor/vutils': 1.0.4 - '@visactor/vrender-components@1.1.0-alpha.8': + '@visactor/vrender-components@1.1.0-alpha.9': dependencies: - '@visactor/vrender-animate': 1.1.0-alpha.8 - '@visactor/vrender-core': 1.1.0-alpha.8 - '@visactor/vrender-kits': 1.1.0-alpha.8 + '@visactor/vrender-animate': 1.1.0-alpha.9 + '@visactor/vrender-core': 1.1.0-alpha.9 + '@visactor/vrender-kits': 1.1.0-alpha.9 '@visactor/vscale': 1.0.23 '@visactor/vutils': 1.0.23 @@ -15055,7 +15055,7 @@ snapshots: '@visactor/vutils': 1.0.4 color-convert: 2.0.1 - '@visactor/vrender-core@1.1.0-alpha.8': + '@visactor/vrender-core@1.1.0-alpha.9': dependencies: '@visactor/vutils': 1.0.23 color-convert: 2.0.1 @@ -15069,21 +15069,21 @@ snapshots: lottie-web: 5.13.0 roughjs: 4.5.2 - '@visactor/vrender-kits@1.1.0-alpha.8': + '@visactor/vrender-kits@1.1.0-alpha.9': dependencies: '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.1.0-alpha.8 + '@visactor/vrender-core': 1.1.0-alpha.9 '@visactor/vutils': 1.0.23 gifuct-js: 2.1.2 lottie-web: 5.13.0 roughjs: 4.6.6 - '@visactor/vrender@1.1.0-alpha.8': + '@visactor/vrender@1.1.0-alpha.9': dependencies: - '@visactor/vrender-animate': 1.1.0-alpha.8 - '@visactor/vrender-components': 1.1.0-alpha.8 - '@visactor/vrender-core': 1.1.0-alpha.8 - '@visactor/vrender-kits': 1.1.0-alpha.8 + '@visactor/vrender-animate': 1.1.0-alpha.9 + '@visactor/vrender-components': 1.1.0-alpha.9 + '@visactor/vrender-core': 1.1.0-alpha.9 + '@visactor/vrender-kits': 1.1.0-alpha.9 '@visactor/vscale@0.18.18': dependencies: diff --git a/docs/package.json b/docs/package.json index 98e9ce617c..d8e19498e3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,8 +19,8 @@ "@visactor/vchart-theme": "~1.6.6", "@visactor/vmind": "1.2.4-alpha.5", "@visactor/vutils": "~1.0.23", - "@visactor/vrender": "1.1.0-alpha.8", - "@visactor/vrender-kits": "1.1.0-alpha.8", + "@visactor/vrender": "1.1.0-alpha.9", + "@visactor/vrender-kits": "1.1.0-alpha.9", "@visactor/vtable": "1.19.0-alpha.0", "@visactor/vtable-editors": "1.19.0-alpha.0", "@visactor/vtable-gantt": "1.19.0-alpha.0", diff --git a/packages/openinula-vchart/package.json b/packages/openinula-vchart/package.json index 4a6f62d2a5..cf107f14f2 100644 --- a/packages/openinula-vchart/package.json +++ b/packages/openinula-vchart/package.json @@ -30,8 +30,8 @@ "dependencies": { "@visactor/vchart": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "1.1.0-alpha.8", - "@visactor/vrender-kits": "1.1.0-alpha.8", + "@visactor/vrender-core": "1.1.0-alpha.9", + "@visactor/vrender-kits": "1.1.0-alpha.9", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/react-vchart/package.json b/packages/react-vchart/package.json index c9afe14ab1..9f362934ab 100644 --- a/packages/react-vchart/package.json +++ b/packages/react-vchart/package.json @@ -36,8 +36,8 @@ "@visactor/vchart": "workspace:2.0.22", "@visactor/vchart-extension": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "1.1.0-alpha.8", - "@visactor/vrender-kits": "1.1.0-alpha.8", + "@visactor/vrender-core": "1.1.0-alpha.9", + "@visactor/vrender-kits": "1.1.0-alpha.9", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/vchart-extension/package.json b/packages/vchart-extension/package.json index 5136ef67d1..994627a02c 100644 --- a/packages/vchart-extension/package.json +++ b/packages/vchart-extension/package.json @@ -26,10 +26,10 @@ "start": "ts-node __tests__/runtime/browser/scripts/initVite.ts && vite serve __tests__/runtime/browser" }, "dependencies": { - "@visactor/vrender-core": "1.1.0-alpha.8", - "@visactor/vrender-kits": "1.1.0-alpha.8", - "@visactor/vrender-components": "1.1.0-alpha.8", - "@visactor/vrender-animate": "1.1.0-alpha.8", + "@visactor/vrender-core": "1.1.0-alpha.9", + "@visactor/vrender-kits": "1.1.0-alpha.9", + "@visactor/vrender-components": "1.1.0-alpha.9", + "@visactor/vrender-animate": "1.1.0-alpha.9", "@visactor/vchart": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", "@visactor/vdataset": "~1.0.23", diff --git a/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts b/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts index fe9b5662b0..20f0745176 100644 --- a/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts +++ b/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts @@ -141,6 +141,178 @@ const expectStaticXLayout = (graphic: AnimatedGraphic, expectedX: number) => { expectClose(getGraphicFinalAttribute(graphic).x, expectedX); }; +const expectStaticRectLayout = ( + graphic: AnimatedGraphic, + expected: { x: number; width: number; y: number; y1: number } +) => { + expectClose(graphic.attribute.x, expected.x); + expectClose(graphic.attribute.width, expected.width); + expectClose(graphic.attribute.y, expected.y); + expectClose(graphic.attribute.y1, expected.y1); + expectClose(graphic.baseAttributes?.x, expected.x); + expectClose(graphic.baseAttributes?.width, expected.width); + expectClose(graphic.baseAttributes?.y, expected.y); + expectClose(graphic.baseAttributes?.y1, expected.y1); + expectClose(getGraphicFinalAttribute(graphic).x, expected.x); + expectClose(getGraphicFinalAttribute(graphic).width, expected.width); + expectClose(getGraphicFinalAttribute(graphic).y, expected.y); + expectClose(getGraphicFinalAttribute(graphic).y1, expected.y1); +}; + +const getBarMarkProduct = (chart: VChart) => { + const model = chart.getChart() as IChart; + const barSeries = model.getAllSeries().find((series: ISeries) => series.type === 'bar'); + + expect(barSeries).toBeDefined(); + if (!barSeries) { + throw new Error('Expected bar series to exist'); + } + + const barMark = barSeries.getMarks().find((mark: IMark) => mark.name === 'bar') as IMark & { + getProduct?: () => AnimatedGraphic; + }; + + expect(barMark).toBeDefined(); + if (!barMark?.getProduct) { + throw new Error('Expected bar mark product to exist'); + } + + return barMark.getProduct(); +}; + +const getBarClipPathRects = (chart: VChart) => + ((getBarMarkProduct(chart).attribute.path ?? []) as AnimatedGraphic[]).map( + path => path.baseAttributes ?? path.attribute + ); + +const createStackCornerLegendSpec = () => + ({ + type: 'bar', + direction: 'vertical', + width: 500, + height: 300, + xField: ['date', '__DimGroup__'], + yField: '__MeaValue__', + seriesField: '__DimGroupID__', + padding: 0, + region: [ + { + clip: true + } + ], + animation: true, + animationAppear: { + duration: APPEAR_DURATION, + easing: 'linear' + }, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + }, + stackCornerRadius: [4, 4, 0, 0], + color: { + type: 'ordinal', + domain: Object.keys(COLOR_BY_SERIES), + range: Object.values(COLOR_BY_SERIES), + specified: {} + }, + data: { + values: [ + { + date: '2019', + region: 'east', + __OriginalData__: { + date: '2019', + region: 'east', + profit: 10, + sales: 20 + }, + profit: 10, + __MeaId__: 'profit', + __MeaName__: '利润', + __MeaValue__: 10, + __DimGroup__: 'east-利润', + __DimGroupID__: 'east-利润profit' + }, + { + date: '2019', + region: 'east', + __OriginalData__: { + date: '2019', + region: 'east', + profit: 10, + sales: 20 + }, + sales: 20, + __MeaId__: 'sales', + __MeaName__: '销售量', + __MeaValue__: 20, + __DimGroup__: 'east-销售量', + __DimGroupID__: 'east-销售量sales' + }, + { + date: '2019', + region: 'north of east', + __OriginalData__: { + date: '2019', + region: 'north of east', + profit: 10, + sales: 20 + }, + profit: 10, + __MeaId__: 'profit', + __MeaName__: '利润', + __MeaValue__: 10, + __DimGroup__: 'north of east-利润', + __DimGroupID__: 'north of east-利润profit' + }, + { + date: '2019', + region: 'north of east', + __OriginalData__: { + date: '2019', + region: 'north of east', + profit: 10, + sales: 20 + }, + sales: 20, + __MeaId__: 'sales', + __MeaName__: '销售量', + __MeaValue__: 20, + __DimGroup__: 'north of east-销售量', + __DimGroupID__: 'north of east-销售量sales' + } + ] + }, + label: { + visible: true, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + } + }, + legends: { + type: 'discrete', + visible: true, + maxCol: 1, + maxRow: 1, + autoPage: true, + orient: 'right', + position: 'start', + item: { + focus: true, + background: { + state: { + selectedHover: { + fill: '#646A73', + fillOpacity: 0.05 + } + } + } + } + } + } as unknown as IBarChartSpec); + describe('manual ticker animation regressions', () => { it('keeps default bar appear starts out of static truth', () => { const { container, dom } = createChartContainer(); @@ -400,4 +572,79 @@ describe('manual ticker animation regressions', () => { removeDom(container); } }); + + it('keeps stack corner clip paths aligned after interrupted legend update animations', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart(createStackCornerLegendSpec(), { + dom, + ticker, + animation: true + }); + + chart.renderSync(); + + try { + ticker.tickAt(APPEAR_DURATION + 50); + + const allSeries = Object.keys(COLOR_BY_SERIES); + const retainedSeries = allSeries.slice(0, 3); + + chart.setLegendSelectedDataByIndex(0, retainedSeries); + chart.renderSync(); + + const firstUpdate = ticker.getTime(); + + ticker.tickAt(firstUpdate + UPDATE_DURATION / 2); + + chart.setLegendSelectedDataByIndex(0, allSeries); + chart.renderSync(); + + const secondUpdate = ticker.getTime(); + + ticker.tickAt(secondUpdate + UPDATE_DURATION / 2); + + chart.setLegendSelectedDataByIndex(0, retainedSeries); + chart.renderSync(); + + const thirdUpdate = ticker.getTime(); + const expectedRects = retainedSeries.map(seriesName => { + const graphic = getVisibleBarByFill(chart, COLOR_BY_SERIES[seriesName]); + const finalAttribute = getGraphicFinalAttribute(graphic); + + return { + x: finalAttribute.x, + width: finalAttribute.width, + y: finalAttribute.y, + y1: finalAttribute.y1 + }; + }); + const clipPathRects = getBarClipPathRects(chart); + + expect(clipPathRects.length).toBe(retainedSeries.length); + clipPathRects.forEach((clipPathRect, index) => { + expectClose(clipPathRect.x, expectedRects[index].x); + expectClose(clipPathRect.width, expectedRects[index].width); + expectClose(clipPathRect.y, expectedRects[index].y); + expectClose(clipPathRect.y1, expectedRects[index].y1); + }); + + ticker.tickAt(thirdUpdate + UPDATE_DURATION + 50); + + const finalClipPathRects = getBarClipPathRects(chart); + + expect(finalClipPathRects.length).toBe(retainedSeries.length); + retainedSeries.forEach((seriesName, index) => { + expectStaticRectLayout(getVisibleBarByFill(chart, COLOR_BY_SERIES[seriesName]), expectedRects[index]); + expectClose(finalClipPathRects[index].x, expectedRects[index].x); + expectClose(finalClipPathRects[index].width, expectedRects[index].width); + expectClose(finalClipPathRects[index].y, expectedRects[index].y); + expectClose(finalClipPathRects[index].y1, expectedRects[index].y1); + }); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); }); diff --git a/packages/vchart/package.json b/packages/vchart/package.json index 6052a4dc7f..5667cd6aeb 100644 --- a/packages/vchart/package.json +++ b/packages/vchart/package.json @@ -126,11 +126,11 @@ "@visactor/vdataset": "~1.0.23", "@visactor/vscale": "~1.0.23", "@visactor/vlayouts": "~1.0.23", - "@visactor/vrender": "1.1.0-alpha.8", - "@visactor/vrender-core": "1.1.0-alpha.8", - "@visactor/vrender-kits": "1.1.0-alpha.8", - "@visactor/vrender-components": "1.1.0-alpha.8", - "@visactor/vrender-animate": "1.1.0-alpha.8", + "@visactor/vrender": "1.1.0-alpha.9", + "@visactor/vrender-core": "1.1.0-alpha.9", + "@visactor/vrender-kits": "1.1.0-alpha.9", + "@visactor/vrender-components": "1.1.0-alpha.9", + "@visactor/vrender-animate": "1.1.0-alpha.9", "@visactor/vutils-extension": "workspace:2.0.22" }, "publishConfig": { diff --git a/tools/story-player/package.json b/tools/story-player/package.json index 64bea90749..69247dd3b7 100644 --- a/tools/story-player/package.json +++ b/tools/story-player/package.json @@ -56,10 +56,10 @@ "vite": "3.2.6" }, "dependencies": { - "@visactor/vrender-core": "1.1.0-alpha.8", - "@visactor/vrender-kits": "1.1.0-alpha.8", + "@visactor/vrender-core": "1.1.0-alpha.9", + "@visactor/vrender-kits": "1.1.0-alpha.9", "@visactor/vchart": "workspace:2.0.22", - "@visactor/vrender": "1.1.0-alpha.8", + "@visactor/vrender": "1.1.0-alpha.9", "@visactor/vutils": "~1.0.23" } } From 645cbef2244f4e06f8adcee083e13e493b94e89a Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Wed, 6 May 2026 20:17:59 +0800 Subject: [PATCH 7/8] feat: upgrade vrender to fix clipPath --- common/config/rush/pnpm-lock.yaml | 120 +++++++++--------- docs/package.json | 4 +- packages/openinula-vchart/package.json | 4 +- packages/react-vchart/package.json | 4 +- packages/vchart-extension/package.json | 8 +- .../unit/animation/manual-ticker.test.ts | 74 +++++++++++ packages/vchart/package.json | 10 +- tools/story-player/package.json | 6 +- 8 files changed, 152 insertions(+), 78 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 15def21eab..5b8a30cce6 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -37,11 +37,11 @@ importers: specifier: 1.2.4-alpha.5 version: 1.2.4-alpha.5 '@visactor/vrender': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vtable': specifier: 1.19.0-alpha.0 version: 1.19.0-alpha.0 @@ -203,11 +203,11 @@ importers: specifier: workspace:2.0.22 version: link:../vchart '@visactor/vrender-core': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -294,11 +294,11 @@ importers: specifier: workspace:2.0.22 version: link:../vchart-extension '@visactor/vrender-core': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -529,20 +529,20 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-animate': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-components': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-core': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vscale': specifier: ~1.0.23 version: 1.0.23 @@ -695,17 +695,17 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender-animate': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-components': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-core': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -1263,14 +1263,14 @@ importers: specifier: workspace:2.0.22 version: link:../../packages/vchart '@visactor/vrender': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-core': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.9 - version: 1.1.0-alpha.9 + specifier: 1.1.0-alpha.11 + version: 1.1.0-alpha.11 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -3076,29 +3076,29 @@ packages: '@visactor/vrender-animate@1.0.0-alpha.18': resolution: {integrity: sha512-9kTtvp1ef+1t+AtUiza6A7qBQP7SmvOu3/ILGrqs/HGdZVj1XGjbYvD/X/zwKJ3LEb7gGV5fa8x95e4czTvRSA==} - '@visactor/vrender-animate@1.1.0-alpha.9': - resolution: {integrity: sha512-dVWOYQeZmttKLBlkl38aDSXzcz8Oqygc8M7RKSCP+jsb0s2kns9jaiaGaK2J1uEEtLxJ4Itz3CGedB+QaqtF9Q==} + '@visactor/vrender-animate@1.1.0-alpha.11': + resolution: {integrity: sha512-RgWD8BYip6QvrkQsXJWvHvboWoyuvHmycxDY4Pq5NKyWxatns5S4n1kwdeViRtBd8bKklVAc/UE4d3BbGv2zJA==} '@visactor/vrender-components@1.0.0-alpha.18': resolution: {integrity: sha512-7Euq+ZfswL74n2pgkaqZSsPxoSa5SPIGyXatN1eUrdzM2Z0kX6U0RcJg01fctvRs4op6WhcecRLqGvnHcBeb9Q==} - '@visactor/vrender-components@1.1.0-alpha.9': - resolution: {integrity: sha512-4PMPlLiy+Eyo9+Zhx2vKOw6WvpDQYWUaSrr4gRRKUaTmhEtJk7YNHlrhO37YBvv6iHLhKdhIQR23GvFL7AJ/Gg==} + '@visactor/vrender-components@1.1.0-alpha.11': + resolution: {integrity: sha512-yGxjh1vYyxiSQoPLxJYpRgLtcQlpT0k+p2IGKvtU7zt6RGFSDSThpSHnAwHcWkFKy6X/6r8P28YmoYl5hhLWSw==} '@visactor/vrender-core@1.0.0-alpha.18': resolution: {integrity: sha512-0ihtNvCyNkOsWPFgRqowHzq0IcQgS2Wl/nPpKbVtxWKveenwlhA+ZKoQvam6VJyBY7jeNe1pROy0mJMDyVAJQw==} - '@visactor/vrender-core@1.1.0-alpha.9': - resolution: {integrity: sha512-qs7iA2BKFWWQILTkWvLYjcjror2+oXMKsVE35UoqTSHMXgzqk7Em1oGAFgukEKRcYRqKMop6oNfUG8DbvH5dcg==} + '@visactor/vrender-core@1.1.0-alpha.11': + resolution: {integrity: sha512-BGTtnZAmRkPredqFwLnVxoRbb/OLRbVfAjR3gz8cWjrz6QwL8WJT7QgzQX/GvjZcIio6q01ONPejLWLnVmVWrQ==} '@visactor/vrender-kits@1.0.0-alpha.18': resolution: {integrity: sha512-Tvolkq+4G8qiPFZo0Aj8M//Yr6jR2h8FNkFEyWM9gbQbEiTkjpmHAJOYnoSsaPtPrcMSlG4EhJSFDk6ymANHVg==} - '@visactor/vrender-kits@1.1.0-alpha.9': - resolution: {integrity: sha512-dACr5rnEkzncekbfraYqcDK9LVYxcUnMN/73OSwrpeog2dbwFDZ/iBoET724NecpmGmi3WAEE+pINB6/acBgvA==} + '@visactor/vrender-kits@1.1.0-alpha.11': + resolution: {integrity: sha512-n/XGF5equ/7quxKUTFMlWGKxu6znp/sN8i/2rIaQ/HyZDQBj558LX4qhFLqWeWikHENv96CYFUrVhOfQXGeqQA==} - '@visactor/vrender@1.1.0-alpha.9': - resolution: {integrity: sha512-vaUTjFPwEsZS3iIrYY5VY5bCMa45hWT5JlO1OP9daKMxN+3nU67bRRCAT1vbOTG69RCcYyqarFsTMl3wS8BqEQ==} + '@visactor/vrender@1.1.0-alpha.11': + resolution: {integrity: sha512-ilKoCLi2Ax4oUmzCbFNmNdmEqcitk7ESlryGAQPUIO1DVJkmu8H2zzO4gUlunEKYkMhCSOj4a1bI8/7jUMO2eQ==} '@visactor/vscale@0.18.18': resolution: {integrity: sha512-iRG4kv+5Fv4KX3AxEfV95XU3I6OmF0QizyAhqHxKa7L1MaT+MRvDDk5zHWf1E8gialLbL2xDe3GnT6g/4u5jhA==} @@ -15029,9 +15029,9 @@ snapshots: '@visactor/vrender-core': 1.0.0-alpha.18 '@visactor/vutils': 1.0.4 - '@visactor/vrender-animate@1.1.0-alpha.9': + '@visactor/vrender-animate@1.1.0-alpha.11': dependencies: - '@visactor/vrender-core': 1.1.0-alpha.9 + '@visactor/vrender-core': 1.1.0-alpha.11 '@visactor/vutils': 1.0.23 '@visactor/vrender-components@1.0.0-alpha.18': @@ -15042,11 +15042,11 @@ snapshots: '@visactor/vscale': 1.0.4 '@visactor/vutils': 1.0.4 - '@visactor/vrender-components@1.1.0-alpha.9': + '@visactor/vrender-components@1.1.0-alpha.11': dependencies: - '@visactor/vrender-animate': 1.1.0-alpha.9 - '@visactor/vrender-core': 1.1.0-alpha.9 - '@visactor/vrender-kits': 1.1.0-alpha.9 + '@visactor/vrender-animate': 1.1.0-alpha.11 + '@visactor/vrender-core': 1.1.0-alpha.11 + '@visactor/vrender-kits': 1.1.0-alpha.11 '@visactor/vscale': 1.0.23 '@visactor/vutils': 1.0.23 @@ -15055,7 +15055,7 @@ snapshots: '@visactor/vutils': 1.0.4 color-convert: 2.0.1 - '@visactor/vrender-core@1.1.0-alpha.9': + '@visactor/vrender-core@1.1.0-alpha.11': dependencies: '@visactor/vutils': 1.0.23 color-convert: 2.0.1 @@ -15069,21 +15069,21 @@ snapshots: lottie-web: 5.13.0 roughjs: 4.5.2 - '@visactor/vrender-kits@1.1.0-alpha.9': + '@visactor/vrender-kits@1.1.0-alpha.11': dependencies: '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.1.0-alpha.9 + '@visactor/vrender-core': 1.1.0-alpha.11 '@visactor/vutils': 1.0.23 gifuct-js: 2.1.2 lottie-web: 5.13.0 roughjs: 4.6.6 - '@visactor/vrender@1.1.0-alpha.9': + '@visactor/vrender@1.1.0-alpha.11': dependencies: - '@visactor/vrender-animate': 1.1.0-alpha.9 - '@visactor/vrender-components': 1.1.0-alpha.9 - '@visactor/vrender-core': 1.1.0-alpha.9 - '@visactor/vrender-kits': 1.1.0-alpha.9 + '@visactor/vrender-animate': 1.1.0-alpha.11 + '@visactor/vrender-components': 1.1.0-alpha.11 + '@visactor/vrender-core': 1.1.0-alpha.11 + '@visactor/vrender-kits': 1.1.0-alpha.11 '@visactor/vscale@0.18.18': dependencies: diff --git a/docs/package.json b/docs/package.json index d8e19498e3..9d3b37d8cc 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,8 +19,8 @@ "@visactor/vchart-theme": "~1.6.6", "@visactor/vmind": "1.2.4-alpha.5", "@visactor/vutils": "~1.0.23", - "@visactor/vrender": "1.1.0-alpha.9", - "@visactor/vrender-kits": "1.1.0-alpha.9", + "@visactor/vrender": "1.1.0-alpha.11", + "@visactor/vrender-kits": "1.1.0-alpha.11", "@visactor/vtable": "1.19.0-alpha.0", "@visactor/vtable-editors": "1.19.0-alpha.0", "@visactor/vtable-gantt": "1.19.0-alpha.0", diff --git a/packages/openinula-vchart/package.json b/packages/openinula-vchart/package.json index cf107f14f2..645f23c411 100644 --- a/packages/openinula-vchart/package.json +++ b/packages/openinula-vchart/package.json @@ -30,8 +30,8 @@ "dependencies": { "@visactor/vchart": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "1.1.0-alpha.9", - "@visactor/vrender-kits": "1.1.0-alpha.9", + "@visactor/vrender-core": "1.1.0-alpha.11", + "@visactor/vrender-kits": "1.1.0-alpha.11", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/react-vchart/package.json b/packages/react-vchart/package.json index 9f362934ab..4ef51209bd 100644 --- a/packages/react-vchart/package.json +++ b/packages/react-vchart/package.json @@ -36,8 +36,8 @@ "@visactor/vchart": "workspace:2.0.22", "@visactor/vchart-extension": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "1.1.0-alpha.9", - "@visactor/vrender-kits": "1.1.0-alpha.9", + "@visactor/vrender-core": "1.1.0-alpha.11", + "@visactor/vrender-kits": "1.1.0-alpha.11", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/vchart-extension/package.json b/packages/vchart-extension/package.json index 994627a02c..b82bf84019 100644 --- a/packages/vchart-extension/package.json +++ b/packages/vchart-extension/package.json @@ -26,10 +26,10 @@ "start": "ts-node __tests__/runtime/browser/scripts/initVite.ts && vite serve __tests__/runtime/browser" }, "dependencies": { - "@visactor/vrender-core": "1.1.0-alpha.9", - "@visactor/vrender-kits": "1.1.0-alpha.9", - "@visactor/vrender-components": "1.1.0-alpha.9", - "@visactor/vrender-animate": "1.1.0-alpha.9", + "@visactor/vrender-core": "1.1.0-alpha.11", + "@visactor/vrender-kits": "1.1.0-alpha.11", + "@visactor/vrender-components": "1.1.0-alpha.11", + "@visactor/vrender-animate": "1.1.0-alpha.11", "@visactor/vchart": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", "@visactor/vdataset": "~1.0.23", diff --git a/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts b/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts index 20f0745176..0c0f4f6b90 100644 --- a/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts +++ b/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts @@ -185,6 +185,22 @@ const getBarClipPathRects = (chart: VChart) => path => path.baseAttributes ?? path.attribute ); +const getBarClipPathGraphics = (chart: VChart) => (getBarMarkProduct(chart).attribute.path ?? []) as AnimatedGraphic[]; + +const clickLegendItem = (chart: VChart, index: number) => { + const legendModel = chart.getComponents().find((component: any) => component.type === 'discreteLegend') as any; + const legendComponent = legendModel?._legendComponent; + const legendItem = legendComponent?._itemsContainer?.getChildren?.()[index]; + + if (!legendComponent?._onClick || !legendItem) { + throw new Error(`Expected legend item ${index} to exist`); + } + + legendComponent._onClick({ + target: legendItem + }); +}; + const createStackCornerLegendSpec = () => ({ type: 'bar', @@ -647,4 +663,62 @@ describe('manual ticker animation regressions', () => { removeDom(container); } }); + + it('keeps stack corner clip paths synced during quick legend reselect update animations', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart(createStackCornerLegendSpec(), { + dom, + ticker, + animation: true + }); + + chart.renderSync(); + + try { + ticker.tickAt(APPEAR_DURATION + 50); + + const allSeries = Object.keys(COLOR_BY_SERIES); + const retainedSeries = allSeries.slice(0, 3); + + clickLegendItem(chart, 3); + chart.renderSync(); + + const hideUpdate = ticker.getTime(); + + ticker.tickAt(hideUpdate + UPDATE_DURATION / 10); + + clickLegendItem(chart, 3); + chart.renderSync(); + + const showUpdate = ticker.getTime(); + + ticker.tickAt(showUpdate + UPDATE_DURATION / 15); + + const clipPaths = getBarClipPathGraphics(chart); + const isAnimatingBackToAll = retainedSeries.some(seriesName => { + const graphic = getVisibleBarByFill(chart, COLOR_BY_SERIES[seriesName]); + const finalAttribute = getGraphicFinalAttribute(graphic); + + return graphic.attribute.x !== finalAttribute.x || graphic.attribute.width !== finalAttribute.width; + }); + + expect(clipPaths.length).toBe(allSeries.length); + expect(isAnimatingBackToAll).toBe(true); + + retainedSeries.forEach((seriesName, index) => { + const graphic = getVisibleBarByFill(chart, COLOR_BY_SERIES[seriesName]); + const clipPath = clipPaths[index]; + + expectClose(clipPath.attribute.x, graphic.attribute.x); + expectClose(clipPath.attribute.width, graphic.attribute.width); + expectClose(clipPath.attribute.y, graphic.attribute.y); + expectClose(clipPath.attribute.y1, graphic.attribute.y1); + }); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); }); diff --git a/packages/vchart/package.json b/packages/vchart/package.json index 5667cd6aeb..796c772e43 100644 --- a/packages/vchart/package.json +++ b/packages/vchart/package.json @@ -126,11 +126,11 @@ "@visactor/vdataset": "~1.0.23", "@visactor/vscale": "~1.0.23", "@visactor/vlayouts": "~1.0.23", - "@visactor/vrender": "1.1.0-alpha.9", - "@visactor/vrender-core": "1.1.0-alpha.9", - "@visactor/vrender-kits": "1.1.0-alpha.9", - "@visactor/vrender-components": "1.1.0-alpha.9", - "@visactor/vrender-animate": "1.1.0-alpha.9", + "@visactor/vrender": "1.1.0-alpha.11", + "@visactor/vrender-core": "1.1.0-alpha.11", + "@visactor/vrender-kits": "1.1.0-alpha.11", + "@visactor/vrender-components": "1.1.0-alpha.11", + "@visactor/vrender-animate": "1.1.0-alpha.11", "@visactor/vutils-extension": "workspace:2.0.22" }, "publishConfig": { diff --git a/tools/story-player/package.json b/tools/story-player/package.json index 69247dd3b7..14e56dc3f4 100644 --- a/tools/story-player/package.json +++ b/tools/story-player/package.json @@ -56,10 +56,10 @@ "vite": "3.2.6" }, "dependencies": { - "@visactor/vrender-core": "1.1.0-alpha.9", - "@visactor/vrender-kits": "1.1.0-alpha.9", + "@visactor/vrender-core": "1.1.0-alpha.11", + "@visactor/vrender-kits": "1.1.0-alpha.11", "@visactor/vchart": "workspace:2.0.22", - "@visactor/vrender": "1.1.0-alpha.9", + "@visactor/vrender": "1.1.0-alpha.11", "@visactor/vutils": "~1.0.23" } } From eb33d40aa11df13e701c5047bd7ffbb80128e58e Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Fri, 8 May 2026 21:38:34 +0800 Subject: [PATCH 8/8] fix: stabilize VChart animations on VRender alpha.15 Keep the alpha.15 dependency bump. Add focused coverage for the two active update-animation bugs. Constraint: Manual-ticker expansion still includes unfinished cases. Rejected: Wait for all drafted cases | user asked to push current work. Confidence: medium Scope-risk: moderate Tested: rush install --check-only Tested: targeted manual-ticker Jest for two fixed bugs Tested: update-spec Jest Tested: touched-file eslint Not-tested: Full manual-ticker has known unfinished failing cases. --- common/config/rush/pnpm-lock.yaml | 120 +- docs/package.json | 4 +- packages/openinula-vchart/package.json | 4 +- packages/react-vchart/package.json | 4 +- packages/vchart-extension/package.json | 8 +- .../unit/animation/manual-ticker.test.ts | 1219 ++++++++++++++++- .../__tests__/unit/core/update-spec.test.ts | 290 ++++ packages/vchart/package.json | 10 +- packages/vchart/src/chart/base/base-chart.ts | 7 +- packages/vchart/src/component/brush/brush.ts | 6 + packages/vchart/src/mark/base/base-mark.ts | 9 +- packages/vchart/src/series/bar/bar.ts | 22 +- .../vchart/src/series/base/base-series.ts | 32 +- packages/vchart/src/series/base/constant.ts | 2 + .../vchart/src/series/box-plot/box-plot.ts | 2 + tools/story-player/package.json | 6 +- 16 files changed, 1639 insertions(+), 106 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 5b8a30cce6..354fd7fa69 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -37,11 +37,11 @@ importers: specifier: 1.2.4-alpha.5 version: 1.2.4-alpha.5 '@visactor/vrender': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vtable': specifier: 1.19.0-alpha.0 version: 1.19.0-alpha.0 @@ -203,11 +203,11 @@ importers: specifier: workspace:2.0.22 version: link:../vchart '@visactor/vrender-core': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -294,11 +294,11 @@ importers: specifier: workspace:2.0.22 version: link:../vchart-extension '@visactor/vrender-core': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -529,20 +529,20 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-animate': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-components': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-core': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vscale': specifier: ~1.0.23 version: 1.0.23 @@ -695,17 +695,17 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender-animate': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-components': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-core': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -1263,14 +1263,14 @@ importers: specifier: workspace:2.0.22 version: link:../../packages/vchart '@visactor/vrender': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-core': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vrender-kits': - specifier: 1.1.0-alpha.11 - version: 1.1.0-alpha.11 + specifier: 1.1.0-alpha.15 + version: 1.1.0-alpha.15 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -3076,29 +3076,29 @@ packages: '@visactor/vrender-animate@1.0.0-alpha.18': resolution: {integrity: sha512-9kTtvp1ef+1t+AtUiza6A7qBQP7SmvOu3/ILGrqs/HGdZVj1XGjbYvD/X/zwKJ3LEb7gGV5fa8x95e4czTvRSA==} - '@visactor/vrender-animate@1.1.0-alpha.11': - resolution: {integrity: sha512-RgWD8BYip6QvrkQsXJWvHvboWoyuvHmycxDY4Pq5NKyWxatns5S4n1kwdeViRtBd8bKklVAc/UE4d3BbGv2zJA==} + '@visactor/vrender-animate@1.1.0-alpha.15': + resolution: {integrity: sha512-P/ZA0IoVYd3YZiBRVogTcMTNe0hbXfWUEsad6zfheeB2uV9x5glNJr3vSWtmPk21dBnP72HY5qCuCoLktCR2aw==} '@visactor/vrender-components@1.0.0-alpha.18': resolution: {integrity: sha512-7Euq+ZfswL74n2pgkaqZSsPxoSa5SPIGyXatN1eUrdzM2Z0kX6U0RcJg01fctvRs4op6WhcecRLqGvnHcBeb9Q==} - '@visactor/vrender-components@1.1.0-alpha.11': - resolution: {integrity: sha512-yGxjh1vYyxiSQoPLxJYpRgLtcQlpT0k+p2IGKvtU7zt6RGFSDSThpSHnAwHcWkFKy6X/6r8P28YmoYl5hhLWSw==} + '@visactor/vrender-components@1.1.0-alpha.15': + resolution: {integrity: sha512-JyktNqO8LHxf/tqX/Qep3C+7InhpOh1PP34xmi+QikDklwP3OcXmEAvh9QUZDRW3fXHay+kj69BNBBv8EFreEw==} '@visactor/vrender-core@1.0.0-alpha.18': resolution: {integrity: sha512-0ihtNvCyNkOsWPFgRqowHzq0IcQgS2Wl/nPpKbVtxWKveenwlhA+ZKoQvam6VJyBY7jeNe1pROy0mJMDyVAJQw==} - '@visactor/vrender-core@1.1.0-alpha.11': - resolution: {integrity: sha512-BGTtnZAmRkPredqFwLnVxoRbb/OLRbVfAjR3gz8cWjrz6QwL8WJT7QgzQX/GvjZcIio6q01ONPejLWLnVmVWrQ==} + '@visactor/vrender-core@1.1.0-alpha.15': + resolution: {integrity: sha512-0fEUh01WVIa1pegOykTzShDVV6/Zi6eLlEtVwSetakn0ORpWcWp/35/lQE/UHDuY7en9L5kDmZs1Rx17nnYThA==} '@visactor/vrender-kits@1.0.0-alpha.18': resolution: {integrity: sha512-Tvolkq+4G8qiPFZo0Aj8M//Yr6jR2h8FNkFEyWM9gbQbEiTkjpmHAJOYnoSsaPtPrcMSlG4EhJSFDk6ymANHVg==} - '@visactor/vrender-kits@1.1.0-alpha.11': - resolution: {integrity: sha512-n/XGF5equ/7quxKUTFMlWGKxu6znp/sN8i/2rIaQ/HyZDQBj558LX4qhFLqWeWikHENv96CYFUrVhOfQXGeqQA==} + '@visactor/vrender-kits@1.1.0-alpha.15': + resolution: {integrity: sha512-ZW1URHOBaccXU6NEFjiN9CY1ZA2njYpowbxtEI63Uoe1wESvqw09ZmrzbFo7/z8EbcSg6VdZlyJq7y0VDnU2Xw==} - '@visactor/vrender@1.1.0-alpha.11': - resolution: {integrity: sha512-ilKoCLi2Ax4oUmzCbFNmNdmEqcitk7ESlryGAQPUIO1DVJkmu8H2zzO4gUlunEKYkMhCSOj4a1bI8/7jUMO2eQ==} + '@visactor/vrender@1.1.0-alpha.15': + resolution: {integrity: sha512-oJY3sx9gqnXYqs8zf5bJBWfh5RK5gplMg2X5GbpQzE5bs5xSYAqSaoNw3U2Bo3GrZAfHODd1c86dvpQ5JAvx4Q==} '@visactor/vscale@0.18.18': resolution: {integrity: sha512-iRG4kv+5Fv4KX3AxEfV95XU3I6OmF0QizyAhqHxKa7L1MaT+MRvDDk5zHWf1E8gialLbL2xDe3GnT6g/4u5jhA==} @@ -15029,9 +15029,9 @@ snapshots: '@visactor/vrender-core': 1.0.0-alpha.18 '@visactor/vutils': 1.0.4 - '@visactor/vrender-animate@1.1.0-alpha.11': + '@visactor/vrender-animate@1.1.0-alpha.15': dependencies: - '@visactor/vrender-core': 1.1.0-alpha.11 + '@visactor/vrender-core': 1.1.0-alpha.15 '@visactor/vutils': 1.0.23 '@visactor/vrender-components@1.0.0-alpha.18': @@ -15042,11 +15042,11 @@ snapshots: '@visactor/vscale': 1.0.4 '@visactor/vutils': 1.0.4 - '@visactor/vrender-components@1.1.0-alpha.11': + '@visactor/vrender-components@1.1.0-alpha.15': dependencies: - '@visactor/vrender-animate': 1.1.0-alpha.11 - '@visactor/vrender-core': 1.1.0-alpha.11 - '@visactor/vrender-kits': 1.1.0-alpha.11 + '@visactor/vrender-animate': 1.1.0-alpha.15 + '@visactor/vrender-core': 1.1.0-alpha.15 + '@visactor/vrender-kits': 1.1.0-alpha.15 '@visactor/vscale': 1.0.23 '@visactor/vutils': 1.0.23 @@ -15055,7 +15055,7 @@ snapshots: '@visactor/vutils': 1.0.4 color-convert: 2.0.1 - '@visactor/vrender-core@1.1.0-alpha.11': + '@visactor/vrender-core@1.1.0-alpha.15': dependencies: '@visactor/vutils': 1.0.23 color-convert: 2.0.1 @@ -15069,21 +15069,21 @@ snapshots: lottie-web: 5.13.0 roughjs: 4.5.2 - '@visactor/vrender-kits@1.1.0-alpha.11': + '@visactor/vrender-kits@1.1.0-alpha.15': dependencies: '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.1.0-alpha.11 + '@visactor/vrender-core': 1.1.0-alpha.15 '@visactor/vutils': 1.0.23 gifuct-js: 2.1.2 lottie-web: 5.13.0 roughjs: 4.6.6 - '@visactor/vrender@1.1.0-alpha.11': + '@visactor/vrender@1.1.0-alpha.15': dependencies: - '@visactor/vrender-animate': 1.1.0-alpha.11 - '@visactor/vrender-components': 1.1.0-alpha.11 - '@visactor/vrender-core': 1.1.0-alpha.11 - '@visactor/vrender-kits': 1.1.0-alpha.11 + '@visactor/vrender-animate': 1.1.0-alpha.15 + '@visactor/vrender-components': 1.1.0-alpha.15 + '@visactor/vrender-core': 1.1.0-alpha.15 + '@visactor/vrender-kits': 1.1.0-alpha.15 '@visactor/vscale@0.18.18': dependencies: diff --git a/docs/package.json b/docs/package.json index 9d3b37d8cc..00fc1e26bd 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,8 +19,8 @@ "@visactor/vchart-theme": "~1.6.6", "@visactor/vmind": "1.2.4-alpha.5", "@visactor/vutils": "~1.0.23", - "@visactor/vrender": "1.1.0-alpha.11", - "@visactor/vrender-kits": "1.1.0-alpha.11", + "@visactor/vrender": "1.1.0-alpha.15", + "@visactor/vrender-kits": "1.1.0-alpha.15", "@visactor/vtable": "1.19.0-alpha.0", "@visactor/vtable-editors": "1.19.0-alpha.0", "@visactor/vtable-gantt": "1.19.0-alpha.0", diff --git a/packages/openinula-vchart/package.json b/packages/openinula-vchart/package.json index 645f23c411..75365ac5e4 100644 --- a/packages/openinula-vchart/package.json +++ b/packages/openinula-vchart/package.json @@ -30,8 +30,8 @@ "dependencies": { "@visactor/vchart": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "1.1.0-alpha.11", - "@visactor/vrender-kits": "1.1.0-alpha.11", + "@visactor/vrender-core": "1.1.0-alpha.15", + "@visactor/vrender-kits": "1.1.0-alpha.15", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/react-vchart/package.json b/packages/react-vchart/package.json index 4ef51209bd..70accd925c 100644 --- a/packages/react-vchart/package.json +++ b/packages/react-vchart/package.json @@ -36,8 +36,8 @@ "@visactor/vchart": "workspace:2.0.22", "@visactor/vchart-extension": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "1.1.0-alpha.11", - "@visactor/vrender-kits": "1.1.0-alpha.11", + "@visactor/vrender-core": "1.1.0-alpha.15", + "@visactor/vrender-kits": "1.1.0-alpha.15", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/vchart-extension/package.json b/packages/vchart-extension/package.json index b82bf84019..27247bcba2 100644 --- a/packages/vchart-extension/package.json +++ b/packages/vchart-extension/package.json @@ -26,10 +26,10 @@ "start": "ts-node __tests__/runtime/browser/scripts/initVite.ts && vite serve __tests__/runtime/browser" }, "dependencies": { - "@visactor/vrender-core": "1.1.0-alpha.11", - "@visactor/vrender-kits": "1.1.0-alpha.11", - "@visactor/vrender-components": "1.1.0-alpha.11", - "@visactor/vrender-animate": "1.1.0-alpha.11", + "@visactor/vrender-core": "1.1.0-alpha.15", + "@visactor/vrender-kits": "1.1.0-alpha.15", + "@visactor/vrender-components": "1.1.0-alpha.15", + "@visactor/vrender-animate": "1.1.0-alpha.15", "@visactor/vchart": "workspace:2.0.22", "@visactor/vutils": "~1.0.23", "@visactor/vdataset": "~1.0.23", diff --git a/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts b/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts index 0c0f4f6b90..d400d92b39 100644 --- a/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts +++ b/packages/vchart/__tests__/unit/animation/manual-ticker.test.ts @@ -1,13 +1,17 @@ import VChart, { ManualTicker, + registerStateTransition, type IBarChartSpec, type IChart, + type ILineChartSpec, type IMark, type IMarkGraphic, type ISeries } from '../../../src'; import { createDiv, removeDom } from '../../util/dom'; +registerStateTransition(); + type AnimatedGraphic = IMarkGraphic & { attribute: Record; baseAttributes?: Record; @@ -24,6 +28,8 @@ type TraversableGraphic = AnimatedGraphic & { const APPEAR_DURATION = 300; const UPDATE_DURATION = 300; +const MARKER_DURATION = 800; +const MARKER_EXIT_DURATION = 1800; const COLOR_BY_SERIES: Record = { 'east-利润profit': '#8D72F6', 'east-销售量sales': '#5766EC', @@ -87,6 +93,44 @@ const getBarGraphics = (chart: VChart) => { return barMark.getGraphics() as AnimatedGraphic[]; }; +const getLineGraphics = (chart: VChart) => { + const model = chart.getChart() as IChart; + const lineSeries = model.getAllSeries().find((series: ISeries) => series.type === 'line'); + + expect(lineSeries).toBeDefined(); + if (!lineSeries) { + throw new Error('Expected line series to exist'); + } + + const lineMark = lineSeries.getMarks().find((mark: IMark) => mark.name === 'line'); + + expect(lineMark).toBeDefined(); + if (!lineMark) { + throw new Error('Expected line mark to exist'); + } + + return lineMark.getGraphics() as AnimatedGraphic[]; +}; + +const getPointGraphics = (chart: VChart) => { + const model = chart.getChart() as IChart; + const lineSeries = model.getAllSeries().find((series: ISeries) => series.type === 'line'); + + expect(lineSeries).toBeDefined(); + if (!lineSeries) { + throw new Error('Expected line series to exist'); + } + + const pointMark = lineSeries.getMarks().find((mark: IMark) => mark.name === 'point'); + + expect(pointMark).toBeDefined(); + if (!pointMark) { + throw new Error('Expected point mark to exist'); + } + + return pointMark.getGraphics() as AnimatedGraphic[]; +}; + const walkGraphics = (root: TraversableGraphic, visitor: (graphic: TraversableGraphic) => void) => { visitor(root); root.forEachChildren?.((child: TraversableGraphic) => { @@ -130,6 +174,36 @@ const getVisibleBarByFill = (chart: VChart, fill: string) => { return bar; }; +const getBarGraphicByKey = (chart: VChart, key: string) => { + const bar = getBarGraphics(chart).find(graphic => graphic.context?.key === key); + + expect(bar).toBeDefined(); + if (!bar) { + throw new Error(`Expected bar with key ${key} to exist`); + } + + return bar; +}; + +const getGraphicDatum = (graphic: AnimatedGraphic) => { + const data = graphic.context?.data; + + return Array.isArray(data) ? data[0] : data; +}; + +const getStateDatum = (datum: any) => (Array.isArray(datum) ? datum[0] : datum); + +const getBarGraphicByDatum = (chart: VChart, predicate: (datum: any) => boolean) => { + const bar = getBarGraphics(chart).find(graphic => predicate(getGraphicDatum(graphic))); + + expect(bar).toBeDefined(); + if (!bar) { + throw new Error('Expected bar with matching datum to exist'); + } + + return bar; +}; + const getBarCenterX = (graphic: AnimatedGraphic) => { const attrs = getGraphicFinalAttribute(graphic); @@ -159,6 +233,28 @@ const expectStaticRectLayout = ( expectClose(getGraphicFinalAttribute(graphic).y1, expected.y1); }; +const simplifyPoints = (points: any[] = []) => + points.map(point => ({ + x: point.x, + y: point.y, + context: point.context + })); + +const expectPointsLayout = (actual: any[], expected: ReturnType) => { + expect(actual.length).toBe(expected.length); + actual.forEach((point, index) => { + expectClose(point.x, expected[index].x); + expectClose(point.y, expected[index].y); + expect(point.context).toBe(expected[index].context); + }); +}; + +const expectLinePointsLayout = (graphic: AnimatedGraphic, expected: ReturnType) => { + expectPointsLayout(graphic.attribute.points, expected); + expectPointsLayout(graphic.baseAttributes?.points, expected); + expectPointsLayout(getGraphicFinalAttribute(graphic).points, expected); +}; + const getBarMarkProduct = (chart: VChart) => { const model = chart.getChart() as IChart; const barSeries = model.getAllSeries().find((series: ISeries) => series.type === 'bar'); @@ -201,6 +297,532 @@ const clickLegendItem = (chart: VChart, index: number) => { }); }; +const getMarkerGraphic = (chart: VChart, componentType: 'markPoint' | 'markLine' | 'markArea') => { + const component = (chart.getChart() as any)?.getComponentsByKey(componentType)?.[0]; + const graphic = component?.getVRenderComponents?.()[0] as TraversableGraphic | undefined; + + expect(graphic).toBeDefined(); + if (!graphic) { + throw new Error(`Expected ${componentType} component graphic to exist`); + } + + return graphic; +}; + +const collectGraphics = (root: TraversableGraphic) => { + const graphics: TraversableGraphic[] = []; + + walkGraphics(root, graphic => { + graphics.push(graphic); + }); + + return graphics; +}; + +const isGraphicAttached = (graphic: TraversableGraphic) => Boolean(graphic.parent || (graphic as any).stage); + +const isIntermediateOpacity = (value: unknown) => typeof value === 'number' && value > 0 && value < 1; + +const hasIntermediateFade = (graphic: TraversableGraphic) => + isIntermediateOpacity(graphic.attribute?.fillOpacity) || isIntermediateOpacity(graphic.attribute?.strokeOpacity); + +const expectMarkerExitFade = ( + chart: VChart, + ticker: ManualTicker, + markerGraphic: TraversableGraphic, + removeMarkerSpec: () => any +) => { + const trackedGraphics = collectGraphics(markerGraphic).filter(graphic => graphic !== markerGraphic); + + expect(trackedGraphics.length).toBeGreaterThan(0); + + chart.updateSpecSync(removeMarkerSpec()); + + const exitStart = ticker.getTime(); + + ticker.tickAt(exitStart + MARKER_EXIT_DURATION / 2); + + expect(trackedGraphics.some(isGraphicAttached)).toBe(true); + expect(trackedGraphics.some(graphic => isGraphicAttached(graphic) && hasIntermediateFade(graphic))).toBe(true); + + ticker.tickAt(exitStart + MARKER_EXIT_DURATION + 50); + + expect(trackedGraphics.some(isGraphicAttached)).toBe(false); +}; + +const createDocBarDatum = (country: string, value: number, measure = 'share_global_co2') => ({ + country, + __OriginalData__: { + country, + [measure]: value + }, + [measure]: value, + __MeaId__: measure, + __MeaName__: measure, + __MeaValue__: value, + __Dim_Y__: country, + __Dim_Color__: measure, + __Dim_Detail__: measure, + __Dim_ColorId__: measure +}); + +const createSameSeriesDocBarSpec = (values: Array<{ country: string; value: number }>) => + ({ + type: 'bar', + direction: 'horizontal', + width: 500, + height: 300, + yField: ['__Dim_Y__'], + xField: '__MeaValue__', + seriesField: '__Dim_ColorId__', + padding: 0, + region: [ + { + clip: true + } + ], + animation: true, + animationAppear: { + duration: APPEAR_DURATION, + easing: 'linear' + }, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + }, + color: { + type: 'ordinal', + domain: ['share_global_co2'], + range: ['#F0A868'] + }, + background: 'transparent', + data: { + id: 'barParallel', + values: values.map(item => createDocBarDatum(item.country, item.value)) + }, + large: false, + label: { + visible: true, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + } + }, + axes: [ + { orient: 'bottom', visible: false }, + { orient: 'left', visible: false } + ] + } as unknown as IBarChartSpec); + +const createSeriesChangeDocBarSpec = (mode: 'grouped' | 'single') => { + const countries = mode === 'grouped' ? ['United States', 'China', 'India'] : ['China', 'United States', 'India']; + const values = countries.flatMap((country, countryIndex) => { + const base = 30 - countryIndex * 5; + + if (mode === 'single') { + return [createDocBarDatum(country, base, 'share_global_co2')]; + } + + return [ + createDocBarDatum(country, base, 'share_global_co2'), + createDocBarDatum(country, base + 10, 'share_global_cumulative_co2') + ]; + }); + + return { + ...createSameSeriesDocBarSpec([]), + yField: mode === 'grouped' ? ['__Dim_Y__', '__Dim_Detail__'] : ['__Dim_Y__'], + color: { + type: 'ordinal', + domain: ['share_global_co2', 'share_global_cumulative_co2'], + range: ['#F0A868', '#7BAA9A'] + }, + data: { + id: 'barParallel', + values + } + } as unknown as IBarChartSpec; +}; + +const createLineGrowthSpec = (values: Array<{ time: string; value: number }>) => + ({ + type: 'line', + width: 500, + height: 300, + xField: 'time', + yField: 'value', + padding: 0, + animation: true, + animationAppear: { + duration: APPEAR_DURATION, + easing: 'linear' + }, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + }, + data: { + values + }, + point: { + visible: false + }, + axes: [ + { orient: 'left', visible: false }, + { orient: 'bottom', visible: false } + ] + } as ILineChartSpec); + +const cosmeticLineValues = [ + { type: 'Nail polish', country: 'Africa', value: 4229 }, + { type: 'Nail polish', country: 'EU', value: 4376 }, + { type: 'Nail polish', country: 'China', value: 3054 }, + { type: 'Nail polish', country: 'USA', value: 12814 }, + { type: 'Eyebrow pencil', country: 'Africa', value: 3932 }, + { type: 'Eyebrow pencil', country: 'EU', value: 3987 }, + { type: 'Eyebrow pencil', country: 'China', value: 5067 }, + { type: 'Eyebrow pencil', country: 'USA', value: 13012 }, + { type: 'Rouge', country: 'Africa', value: 5221 }, + { type: 'Rouge', country: 'EU', value: 3574 }, + { type: 'Rouge', country: 'China', value: 7004 }, + { type: 'Rouge', country: 'USA', value: 11624 }, + { type: 'Lipstick', country: 'Africa', value: 9256 }, + { type: 'Lipstick', country: 'EU', value: 4376 }, + { type: 'Lipstick', country: 'China', value: 9054 }, + { type: 'Lipstick', country: 'USA', value: 8814 }, + { type: 'Eyeshadows', country: 'Africa', value: 3308 }, + { type: 'Eyeshadows', country: 'EU', value: 4572 }, + { type: 'Eyeshadows', country: 'China', value: 12043 }, + { type: 'Eyeshadows', country: 'USA', value: 12998 }, + { type: 'Eyeliner', country: 'Africa', value: 5432 }, + { type: 'Eyeliner', country: 'EU', value: 3417 }, + { type: 'Eyeliner', country: 'China', value: 15067 }, + { type: 'Eyeliner', country: 'USA', value: 12321 }, + { type: 'Foundation', country: 'Africa', value: 13701 }, + { type: 'Foundation', country: 'EU', value: 5231 }, + { type: 'Foundation', country: 'China', value: 10119 }, + { type: 'Foundation', country: 'USA', value: 10342 }, + { type: 'Lip gloss', country: 'Africa', value: 4008 }, + { type: 'Lip gloss', country: 'EU', value: 4572 }, + { type: 'Lip gloss', country: 'China', value: 12043 }, + { type: 'Lip gloss', country: 'USA', value: 22998 }, + { type: 'Mascara', country: 'Africa', value: 18712 }, + { type: 'Mascara', country: 'EU', value: 6134 }, + { type: 'Mascara', country: 'China', value: 10419 }, + { type: 'Mascara', country: 'USA', value: 11261 } +]; + +const createCosmeticPercentLineSpec = () => + ({ + type: 'line', + width: 500, + height: 300, + data: { + values: cosmeticLineValues + }, + percent: true, + xField: 'type', + yField: 'value', + seriesField: 'country', + animation: true, + animationAppear: { + duration: APPEAR_DURATION, + easing: 'linear' + }, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + }, + legends: [{ visible: true, position: 'middle', orient: 'bottom' }], + axes: [ + { + orient: 'left', + label: { + formatMethod: (val: string | string[]) => `${(+val * 100).toFixed(2)}%` + } + } + ] + } as ILineChartSpec); + +const createStateSwitchSpec = (valueOffset = 0) => + ({ + type: 'bar', + width: 400, + height: 300, + data: { + values: [ + { date: '2019', value: 20 + valueOffset }, + { date: '2020', value: 24 + valueOffset }, + { date: '2021', value: 28 + valueOffset } + ] + }, + xField: 'date', + yField: 'value', + animation: true, + animationAppear: { + duration: APPEAR_DURATION, + easing: 'linear' + }, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + }, + animationState: { + duration: UPDATE_DURATION, + easing: 'linear' + }, + bar: { + state: { + custom1: { + fillOpacity: 0.2 + } + } + }, + axes: [ + { orient: 'left', visible: false }, + { orient: 'bottom', visible: false } + ] + } as unknown as IBarChartSpec); + +const createMarkerLineData = () => [ + { year: '2019', value: 10 }, + { year: '2020', value: 18 }, + { year: '2021', value: 14 }, + { year: '2022', value: 22 } +]; + +const createMarkPointExitSpec = (visible = true) => + ({ + type: 'line', + width: 500, + height: 300, + xField: 'year', + yField: 'value', + animation: true, + data: { + values: createMarkerLineData() + }, + point: { + visible: false + }, + markPoint: visible + ? [ + { + animation: { + type: 'callIn', + duration: MARKER_DURATION, + easing: 'linear' + }, + animationExit: { + type: 'fadeOut', + duration: MARKER_EXIT_DURATION, + easing: 'linear' + }, + coordinate: { + year: '2022', + value: 22 + }, + itemContent: { + type: 'text', + text: { + text: '2022', + style: { + fill: '#E8346D' + } + }, + offsetY: 40 + }, + itemLine: { + type: 'type-do', + line: { + style: { + stroke: '#E8346D' + } + } + } + } + ] + : [] + } as any); + +const createDifferenceMarkLineSpec = (visible = true) => + ({ + type: 'bar', + width: 500, + height: 300, + data: [ + { + id: 'barData', + values: [ + { type: 'Autocracies', year: '1930', value: 129 }, + { type: 'Autocracies', year: '2000', value: 89 }, + { type: 'Democracies', year: '1930', value: 22 }, + { type: 'Democracies', year: '2000', value: 87 } + ] + } + ], + xField: ['year', 'type'], + yField: 'value', + seriesField: 'type', + animation: true, + markLine: visible + ? [ + { + type: 'type-step', + connectDirection: 'top', + expandDistance: 30, + coordinates: [ + { type: 'Autocracies', year: '1930', value: 129 }, + { type: 'Autocracies', year: '2000', value: 89 } + ], + coordinatesOffset: [ + { x: 0, y: 0 }, + { x: -5, y: 0 } + ], + line: { + style: { + lineDash: [0], + lineWidth: 2, + stroke: '#000', + cornerRadius: 4 + } + }, + label: { + text: '-45%', + position: 'middle', + style: { + fill: '#000' + } + }, + endSymbol: { + size: 12, + refX: -4 + }, + animation: { + type: 'clipIn', + duration: MARKER_DURATION, + easing: 'linear' + }, + animationExit: { + type: 'fadeOut', + duration: MARKER_EXIT_DURATION, + easing: 'linear' + } + } + ] + : [] + } as any); + +const createMarkAreaExitSpec = (visible = true) => + ({ + type: 'line', + width: 500, + height: 300, + xField: 'date', + yField: 'price', + animation: true, + data: { + values: [ + { date: 'Jan-20', price: 0.134 }, + { date: 'Feb-20', price: 0.134 }, + { date: 'Mar-20', price: 0.136 }, + { date: 'Mar-23', price: 0.166 } + ] + }, + point: { + visible: false + }, + axes: [ + { orient: 'left', min: 0.12, max: 0.18, visible: false }, + { orient: 'bottom', visible: false } + ], + markArea: visible + ? [ + { + coordinates: [ + { date: 'Jan-20', price: 0.18 }, + { date: 'Mar-23', price: 0.18 }, + { date: 'Mar-23', price: 0.12 }, + { date: 'Jan-20', price: 0.12 } + ], + animation: { + type: 'fadeIn', + duration: MARKER_DURATION, + easing: 'linear' + }, + animationExit: { + type: 'fadeOut', + duration: MARKER_EXIT_DURATION, + easing: 'linear' + }, + label: { + text: 'Electricite prices have surged since 2020', + position: 'insideTop' + } + } + ] + : [] + } as any); + +const createRegularMarkLineExitSpec = (visible = true) => + ({ + type: 'scatter', + width: 500, + height: 300, + padding: [12, 20, 12, 12], + xField: 'x', + yField: 'y', + sizeField: 'z', + size: { + type: 'linear', + range: [20, 80] + }, + animation: true, + data: { + id: 'data', + values: [ + { x: 95, y: 95, z: 13.8, name: 'BE', country: 'Belgium' }, + { x: 65.5, y: 126.4, z: 35.3, name: 'US', country: 'United States' }, + { x: 63.4, y: 51.8, z: 15.4, name: 'PT', country: 'Portugal' } + ] + }, + axes: [ + { orient: 'bottom', type: 'linear', min: 60, max: 95, visible: false }, + { orient: 'left', type: 'linear', min: 0, max: 200, visible: false } + ], + markLine: visible + ? [ + { + x: 65, + label: { + visible: true, + position: 'end', + text: 'Safe fat intake 65g/day', + labelBackground: { + visible: false + } + }, + line: { + style: { + stroke: '#000', + lineDash: [0] + } + }, + animation: { + type: 'clipIn', + duration: MARKER_DURATION, + easing: 'linear' + }, + animationExit: { + type: 'fadeOut', + duration: MARKER_EXIT_DURATION, + easing: 'linear' + } + } + ] + : [] + } as any); + const createStackCornerLegendSpec = () => ({ type: 'bar', @@ -394,22 +1016,200 @@ describe('manual ticker animation regressions', () => { } }); - it('keeps data label update final x at the filtered bar position', () => { + it('keeps horizontal bar sorted update final y at the reordered axis positions', () => { const { container, dom } = createChartContainer(); const ticker = createManualTicker(); - const chart = new VChart( - { + const createSpec = (values: Array<{ type: string; value: number }>) => + ({ type: 'bar', - direction: 'vertical', + direction: 'horizontal', width: 500, height: 300, - xField: ['date', '__DimGroup__'], - yField: '__MeaValue__', - seriesField: '__DimGroupID__', padding: 0, - region: [ + data: [ { - clip: true + values + } + ], + yField: 'type', + xField: 'value', + tooltip: false, + animation: true, + animationAppear: { + duration: APPEAR_DURATION, + easing: 'linear' + }, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + } + } as unknown as IBarChartSpec); + const chart = new VChart( + createSpec([ + { type: '1', value: 20 }, + { type: '2', value: 30 }, + { type: '3', value: 10 } + ]), + { + dom, + ticker, + animation: true + } + ); + + chart.renderSync(); + + try { + ticker.tickAt(APPEAR_DURATION + 50); + + const bar1Before = getBarGraphicByKey(chart, '1'); + const bar2Before = getBarGraphicByKey(chart, '2'); + const y1Before = getGraphicFinalAttribute(bar1Before).y; + const y2Before = getGraphicFinalAttribute(bar2Before).y; + + chart.updateSpecSync( + createSpec([ + { type: '2', value: 30 }, + { type: '1', value: 20 }, + { type: '3', value: 10 } + ]) + ); + + const updateStart = ticker.getTime(); + const bar1After = getBarGraphicByKey(chart, '1'); + const bar2After = getBarGraphicByKey(chart, '2'); + const y1After = getGraphicFinalAttribute(bar1After).y; + const y2After = getGraphicFinalAttribute(bar2After).y; + + expectClose(y1After, y2Before); + expectClose(y2After, y1Before); + + ticker.tickAt(updateStart + UPDATE_DURATION / 2); + + expect(bar1After.attribute.y).toBeGreaterThan(Math.min(y1Before, y1After)); + expect(bar1After.attribute.y).toBeLessThan(Math.max(y1Before, y1After)); + expect(bar2After.attribute.y).toBeGreaterThan(Math.min(y2Before, y2After)); + expect(bar2After.attribute.y).toBeLessThan(Math.max(y2Before, y2After)); + expectClose(getGraphicFinalAttribute(bar1After).y, y1After); + expectClose(getGraphicFinalAttribute(bar2After).y, y2After); + + ticker.tickAt(updateStart + UPDATE_DURATION + 50); + + expectClose(bar1After.attribute.y, y1After); + expectClose(bar2After.attribute.y, y2After); + expectClose(bar1After.baseAttributes?.y, y1After); + expectClose(bar2After.baseAttributes?.y, y2After); + expectClose(getGraphicFinalAttribute(bar1After).y, y1After); + expectClose(getGraphicFinalAttribute(bar2After).y, y2After); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); + + it('keeps line resize update final points at the resized layout', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart( + { + type: 'line', + direction: 'vertical', + width: 500, + height: 300, + xField: ['date'], + yField: '__MeaValue__', + padding: 0, + animation: true, + animationAppear: { + duration: APPEAR_DURATION, + easing: 'linear' + }, + animationUpdate: { + duration: UPDATE_DURATION, + easing: 'linear' + }, + background: 'transparent', + data: { + values: [ + { + date: '2019', + __MeaValue__: 10 + }, + { + date: '2020', + __MeaValue__: 12 + }, + { + date: '2021', + __MeaValue__: 14 + }, + { + date: '2022', + __MeaValue__: 16 + } + ] + } + } as ILineChartSpec, + { + dom, + ticker, + animation: true + } + ); + + chart.renderSync(); + + try { + ticker.tickAt(APPEAR_DURATION + 50); + + const lineGraphic = getLineGraphics(chart)[0]; + const beforeResizePoints = simplifyPoints(getGraphicFinalAttribute(lineGraphic).points); + + chart.resizeSync(700, 300); + + const updateStart = ticker.getTime(); + const resizedPoints = simplifyPoints(getGraphicFinalAttribute(lineGraphic).points); + + expect(resizedPoints.some((point, index) => point.x !== beforeResizePoints[index].x)).toBe(true); + expectPointsLayout(getGraphicFinalAttribute(lineGraphic).points, resizedPoints); + + ticker.tickAt(updateStart + UPDATE_DURATION / 2); + + lineGraphic.attribute.points.forEach((point: any, index: number) => { + expect(point.x).toBeGreaterThanOrEqual(Math.min(beforeResizePoints[index].x, resizedPoints[index].x)); + expect(point.x).toBeLessThanOrEqual(Math.max(beforeResizePoints[index].x, resizedPoints[index].x)); + expectClose(point.y, resizedPoints[index].y); + expect(point.context).toBe(resizedPoints[index].context); + }); + expectPointsLayout(getGraphicFinalAttribute(lineGraphic).points, resizedPoints); + + ticker.tickAt(updateStart + UPDATE_DURATION + 50); + + expectLinePointsLayout(lineGraphic, resizedPoints); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); + + it('keeps data label update final x at the filtered bar position', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart( + { + type: 'bar', + direction: 'vertical', + width: 500, + height: 300, + xField: ['date', '__DimGroup__'], + yField: '__MeaValue__', + seriesField: '__DimGroupID__', + padding: 0, + region: [ + { + clip: true } ], animation: true, @@ -721,4 +1521,405 @@ describe('manual ticker animation regressions', () => { removeDom(container); } }); + + it('keeps same-series horizontal bar updates animating with label enabled', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart( + createSameSeriesDocBarSpec([ + { country: 'United States', value: 12.911 }, + { country: 'China', value: 31.953 }, + { country: 'India', value: 8.04 }, + { country: 'Russia', value: 4.55 } + ]), + { + dom, + ticker, + animation: true + } + ); + + chart.renderSync(); + + try { + ticker.tickAt(APPEAR_DURATION + 50); + + const chinaBefore = getBarGraphicByDatum(chart, datum => datum?.__Dim_Y__ === 'China'); + const yBefore = getGraphicFinalAttribute(chinaBefore).y; + + chart.updateSpecSync( + createSameSeriesDocBarSpec([ + { country: 'China', value: 31.953 }, + { country: 'United States', value: 12.911 }, + { country: 'India', value: 8.04 }, + { country: 'Russia', value: 4.55 }, + { country: 'Brazil', value: 1.271 } + ]) + ); + + const updateStart = ticker.getTime(); + const chinaAfter = getBarGraphicByDatum(chart, datum => datum?.__Dim_Y__ === 'China'); + const yAfter = getGraphicFinalAttribute(chinaAfter).y; + + expect(yAfter).not.toBe(yBefore); + expectClose(getGraphicFinalAttribute(chinaAfter).y, yAfter); + + ticker.tickAt(updateStart + UPDATE_DURATION / 2); + + expect(chinaAfter.attribute.y).toBeGreaterThan(Math.min(yBefore, yAfter)); + expect(chinaAfter.attribute.y).toBeLessThan(Math.max(yBefore, yAfter)); + expectClose(getGraphicFinalAttribute(chinaAfter).y, yAfter); + + ticker.tickAt(updateStart + UPDATE_DURATION + 50); + + expectClose(chinaAfter.attribute.y, yAfter); + expectClose(chinaAfter.baseAttributes?.y, yAfter); + expectClose(getGraphicFinalAttribute(chinaAfter).y, yAfter); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); + + it('grows new line data points through update animation instead of placing them directly at final points', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart( + createLineGrowthSpec([ + { time: '2019', value: 10 }, + { time: '2020', value: 12 }, + { time: '2021', value: 14 } + ]), + { + dom, + ticker, + animation: true + } + ); + + chart.renderSync(); + + try { + ticker.tickAt(APPEAR_DURATION + 50); + + const lineGraphic = getLineGraphics(chart)[0]; + const beforePoints = simplifyPoints(getGraphicFinalAttribute(lineGraphic).points); + const beforeTail = beforePoints[beforePoints.length - 1]; + + chart.updateSpecSync( + createLineGrowthSpec([ + { time: '2019', value: 10 }, + { time: '2020', value: 12 }, + { time: '2021', value: 14 }, + { time: '2022', value: 16 }, + { time: '2023', value: 18 } + ]) + ); + + const updateStart = ticker.getTime(); + const finalPoints = simplifyPoints(getGraphicFinalAttribute(lineGraphic).points); + + expect(finalPoints.length).toBe(5); + + ticker.tickAt(updateStart + UPDATE_DURATION / 2); + + const midPoints = simplifyPoints(lineGraphic.attribute.points); + const newMidPoints = midPoints.slice(beforePoints.length); + const newFinalPoints = finalPoints.slice(beforePoints.length); + + expect(newMidPoints.length).toBe(newFinalPoints.length); + expect( + newMidPoints.some((point, index) => point.x !== newFinalPoints[index].x || point.y !== newFinalPoints[index].y) + ).toBe(true); + newMidPoints.forEach((point, index) => { + const finalPoint = newFinalPoints[index]; + + expect(point.x).toBeGreaterThanOrEqual(Math.min(beforeTail.x, finalPoint.x)); + expect(point.x).toBeLessThanOrEqual(Math.max(beforeTail.x, finalPoint.x)); + expect(point.y).toBeGreaterThanOrEqual(Math.min(beforeTail.y, finalPoint.y)); + expect(point.y).toBeLessThanOrEqual(Math.max(beforeTail.y, finalPoint.y)); + }); + + ticker.tickAt(updateStart + UPDATE_DURATION + 50); + + expectLinePointsLayout(lineGraphic, finalPoints); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); + + it('keeps reselected percent line graphics visible after interrupted legend toggles', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart(createCosmeticPercentLineSpec(), { + dom, + ticker, + animation: true + }); + + chart.renderSync(); + + try { + ticker.tickAt(APPEAR_DURATION + 50); + + const allSeries = ['Africa', 'EU', 'China', 'USA']; + const retainedSeries = allSeries.filter(value => value !== 'USA'); + const selectLegendData = (selectedData: string[]) => { + chart.setLegendSelectedDataByIndex(0, selectedData); + chart.renderSync(); + }; + const getLineGraphicByCountry = (country: string) => { + const lineGraphic = getLineGraphics(chart).find((graphic: AnimatedGraphic) => { + const datum = getGraphicDatum(graphic); + + return datum?.country === country; + }); + + expect(lineGraphic).toBeDefined(); + if (!lineGraphic) { + throw new Error(`Expected line graphic for ${country}`); + } + return lineGraphic; + }; + const getPointGraphicByDatum = (country: string, type: string) => { + const pointGraphic = getPointGraphics(chart).find((graphic: AnimatedGraphic) => { + const datum = getGraphicDatum(graphic); + + return datum?.country === country && datum?.type === type; + }); + + expect(pointGraphic).toBeDefined(); + if (!pointGraphic) { + throw new Error(`Expected point graphic for ${country}/${type}`); + } + return pointGraphic; + }; + + selectLegendData(retainedSeries); + ticker.tickAt(ticker.getTime() + UPDATE_DURATION / 4); + + selectLegendData(allSeries); + ticker.tickAt(ticker.getTime() + UPDATE_DURATION / 4); + + selectLegendData(retainedSeries); + ticker.tickAt(ticker.getTime() + UPDATE_DURATION / 4); + + const chinaPointBeforeReselect = getPointGraphicByDatum('China', 'Nail polish'); + const chinaYBeforeReselect = chinaPointBeforeReselect.attribute.y; + + selectLegendData(allSeries); + + const reselectStart = ticker.getTime(); + const chinaPointAfterReselect = getPointGraphicByDatum('China', 'Nail polish'); + const chinaFinalY = getGraphicFinalAttribute(chinaPointAfterReselect).y; + const usaLine = getLineGraphicByCountry('USA'); + const usaPoint = getPointGraphicByDatum('USA', 'Nail polish'); + + expectClose(chinaPointAfterReselect.attribute.y, chinaYBeforeReselect); + expect(chinaFinalY).not.toBe(chinaYBeforeReselect); + expect(getGraphicFinalAttribute(usaLine).points.length).toBe(9); + expect(getGraphicFinalAttribute(usaPoint).x).toBeDefined(); + expect(getGraphicFinalAttribute(usaPoint).y).toBeDefined(); + + ticker.tickAt(reselectStart + UPDATE_DURATION / 4); + + expect(usaLine.attribute.opacity).toBeGreaterThan(0); + expect(usaLine.attribute.opacity).toBeLessThan(1); + + ticker.tickAt(reselectStart + UPDATE_DURATION + 50); + + const finalUsaLine = getLineGraphicByCountry('USA'); + const finalUsaPoint = getPointGraphicByDatum('USA', 'Nail polish'); + + expect(getGraphicFinalAttribute(finalUsaLine).points.length).toBe(9); + expect(finalUsaLine.attribute.points.length).toBe(9); + expect(finalUsaPoint.attribute.x).toBeDefined(); + expect(finalUsaPoint.attribute.y).toBeDefined(); + expectClose(chinaPointAfterReselect.attribute.y, chinaFinalY); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); + + it('keeps retained bars moving when grouped series changes to single-series layout', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart(createSeriesChangeDocBarSpec('grouped'), { + dom, + ticker, + animation: true + }); + + chart.renderSync(); + + try { + ticker.tickAt(APPEAR_DURATION + 50); + + const retainedBefore = getBarGraphicByDatum( + chart, + datum => datum?.__Dim_Y__ === 'China' && datum?.__Dim_ColorId__ === 'share_global_co2' + ); + const yBefore = getGraphicFinalAttribute(retainedBefore).y; + + chart.updateSpecSync(createSeriesChangeDocBarSpec('single')); + + const updateStart = ticker.getTime(); + const retainedAfter = getBarGraphicByDatum( + chart, + datum => datum?.__Dim_Y__ === 'China' && datum?.__Dim_ColorId__ === 'share_global_co2' + ); + const yAfter = getGraphicFinalAttribute(retainedAfter).y; + + expect(retainedAfter).toBe(retainedBefore); + expect(yAfter).not.toBe(yBefore); + + ticker.tickAt(updateStart + UPDATE_DURATION / 2); + + expect(retainedAfter.attribute.y).toBeGreaterThan(Math.min(yBefore, yAfter)); + expect(retainedAfter.attribute.y).toBeLessThan(Math.max(yBefore, yAfter)); + expectClose(getGraphicFinalAttribute(retainedAfter).y, yAfter); + + ticker.tickAt(updateStart + UPDATE_DURATION + 50); + + expectClose(retainedAfter.attribute.y, yAfter); + expectClose(retainedAfter.baseAttributes?.y, yAfter); + expectClose(getGraphicFinalAttribute(retainedAfter).y, yAfter); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); + + it('keeps unchanged custom highlight opacity through updateSpec and state refresh', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart(createStateSwitchSpec(), { + dom, + ticker, + animation: true + }); + + chart.renderSync(); + + try { + ticker.tickAt(APPEAR_DURATION + 50); + + chart.updateState({ + custom1: { + filter: (datum: any) => getStateDatum(datum)?.date === '2021' + } + }); + + const firstStateStart = ticker.getTime(); + + ticker.tickAt(firstStateStart + UPDATE_DURATION + 50); + + const highlightedBefore = getBarGraphicByDatum(chart, datum => datum?.date === '2021'); + + expect(highlightedBefore.currentStates).toContain('custom1'); + expect(highlightedBefore.resolvedStatePatch?.fillOpacity).toBe(0.2); + expectClose(highlightedBefore.attribute.fillOpacity, 0.2); + + chart.updateSpecSync(createStateSwitchSpec(5)); + chart.updateState({ + custom1: { + filter: (datum: any) => getStateDatum(datum)?.date === '2021' + } + }); + + const highlightedAfter = getBarGraphicByDatum(chart, datum => datum?.date === '2021'); + const secondStateStart = ticker.getTime(); + + expect(highlightedAfter.currentStates).toContain('custom1'); + expect(highlightedAfter.resolvedStatePatch?.fillOpacity).toBe(0.2); + expectClose(highlightedAfter.attribute.fillOpacity, 0.2); + + ticker.tickAt(secondStateStart + UPDATE_DURATION / 2); + + expectClose(highlightedAfter.attribute.fillOpacity, 0.2); + + ticker.tickAt(secondStateStart + UPDATE_DURATION + 50); + + expectClose(highlightedAfter.attribute.fillOpacity, 0.2); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); + + it('uses clipIn on difference markLine so the arrow line is geometrically revealed', () => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart(createDifferenceMarkLineSpec(), { + dom, + ticker, + animation: true + }); + + chart.renderSync(); + + try { + const markerGraphic = getMarkerGraphic(chart, 'markLine'); + + ticker.tickAt(MARKER_DURATION / 2); + + const clipRangeGraphics = collectGraphics(markerGraphic).filter( + graphic => typeof graphic.attribute?.clipRange === 'number' + ); + + expect(clipRangeGraphics.length).toBeGreaterThan(0); + expect( + clipRangeGraphics.some(graphic => graphic.attribute.clipRange > 0 && graphic.attribute.clipRange < 1) + ).toBe(true); + + ticker.tickAt(MARKER_DURATION + 50); + + clipRangeGraphics.forEach(graphic => { + expectClose(graphic.attribute.clipRange, 1); + expectClose(graphic.baseAttributes?.clipRange, 1); + expectClose(getGraphicFinalAttribute(graphic).clipRange, 1); + }); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + }); + + it.each([ + ['markPoint', createMarkPointExitSpec, 'markPoint'], + ['difference markLine', createDifferenceMarkLineSpec, 'markLine'], + ['markArea', createMarkAreaExitSpec, 'markArea'], + ['regular markLine', createRegularMarkLineExitSpec, 'markLine'] + ] as const)( + 'runs %s exit fade-out instead of removing the marker immediately', + (_name, createSpec, componentType) => { + const { container, dom } = createChartContainer(); + const ticker = createManualTicker(); + const chart = new VChart(createSpec(true), { + dom, + ticker, + animation: true + }); + + chart.renderSync(); + + try { + ticker.tickAt(MARKER_DURATION + 50); + + expectMarkerExitFade(chart, ticker, getMarkerGraphic(chart, componentType), () => createSpec(false)); + } finally { + chart.release(); + ticker.release(); + removeDom(container); + } + } + ); }); diff --git a/packages/vchart/__tests__/unit/core/update-spec.test.ts b/packages/vchart/__tests__/unit/core/update-spec.test.ts index 4886fba5ab..4ed0a081b2 100644 --- a/packages/vchart/__tests__/unit/core/update-spec.test.ts +++ b/packages/vchart/__tests__/unit/core/update-spec.test.ts @@ -135,6 +135,296 @@ describe('vchart updateSpec test', () => { }); }); +describe('vchart updateSpec mark style reInit test', () => { + it('should preserve component-injected brush state styles after data-only updateSpec', () => { + const container = createDiv(); + const dom = createDiv(container); + const createSpec = (values: Array<{ type: string; value: number }>) => + ({ + type: 'bar', + width: 300, + height: 200, + data: [ + { + id: 'bar', + values + } + ], + xField: 'type', + yField: 'value', + brush: { + visible: true, + brushType: 'rect', + inBrush: { + colorAlpha: 1 + }, + outOfBrush: { + colorAlpha: 0.2 + } + } + } as IBarChartSpec); + const chart = new VChart( + createSpec([ + { type: '1', value: 20 }, + { type: '2', value: 30 } + ]), + { + dom, + animation: false + } + ); + + chart.renderSync(); + + const getBarMark = () => { + const barSeries = chart + .getChart() + ?.getAllSeries() + .find(series => series.type === 'bar'); + const barMark = barSeries?.getMarks().find(mark => mark.name === 'bar') as any; + + expect(barMark).toBeDefined(); + return barMark; + }; + const expectBrushStates = () => { + const barMark = getBarMark(); + + expect(barMark.stateStyle.inBrush.fillOpacity.style).toBe(1); + expect(barMark.stateStyle.outOfBrush.fillOpacity.style).toBe(0.2); + }; + + try { + expectBrushStates(); + + chart.updateSpecSync( + createSpec([ + { type: '1', value: 25 }, + { type: '2', value: 35 } + ]) + ); + + expectBrushStates(); + } finally { + chart.release(); + removeDom(container); + } + }); +}); + +describe('vchart updateSpec field update classification test', () => { + const getBarGraphicById = (chart: VChart, id: string) => { + const barSeries = chart + .getChart() + ?.getAllSeries() + .find(series => series.type === 'bar'); + const barMark = barSeries?.getMarks().find(mark => mark.name === 'bar') as any; + const barGraphic = barMark?.getGraphics().find((graphic: any) => graphic.context?.data?.[0]?.id === id); + + expect(barMark).toBeDefined(); + expect(barGraphic).toBeDefined(); + return barGraphic as any; + }; + const getBarGraphicByDatum = (chart: VChart, predicate: (datum: any) => boolean) => { + const barSeries = chart + .getChart() + ?.getAllSeries() + .find(series => series.type === 'bar'); + const barMark = barSeries?.getMarks().find(mark => mark.name === 'bar') as any; + const barGraphic = barMark?.getGraphics().find((graphic: any) => predicate(graphic.context?.data?.[0])); + + expect(barMark).toBeDefined(); + expect(barGraphic).toBeDefined(); + return barGraphic as any; + }; + const getBarHeight = (graphic: any) => Math.abs(graphic.attribute.y1 - graphic.attribute.y); + + it('should keep seriesField in default data key when a single dimension value equals the series value', () => { + const container = createDiv(); + const dom = createDiv(container); + const chart = new VChart( + { + type: 'bar', + width: 300, + height: 200, + data: [ + { + id: 'bar', + values: [ + { category: 'A', group: 'A', value: 10 }, + { category: 'A', group: 'B', value: 20 } + ] + } + ], + xField: 'category', + yField: 'value', + seriesField: 'group', + axes: [ + { orient: 'left', visible: false }, + { orient: 'bottom', visible: false } + ] + } as IBarChartSpec, + { + dom + } + ); + + chart.renderSync(); + + try { + expect(getBarGraphicByDatum(chart, datum => datum?.group === 'A').context.key).toBe('A_A'); + expect(getBarGraphicByDatum(chart, datum => datum?.group === 'B').context.key).toBe('A_B'); + } finally { + chart.release(); + removeDom(container); + } + }); + + it('should update top-level xField through recompile without remaking chart', () => { + const container = createDiv(); + const dom = createDiv(container); + const createSpec = (xField: 'category' | 'nextCategory') => + ({ + type: 'bar', + width: 300, + height: 200, + dataKey: 'id', + data: [ + { + id: 'bar', + values: [ + { id: 'a', category: 'A', nextCategory: 'A', value: 10 }, + { id: 'b', category: 'B', nextCategory: 'A', value: 20 }, + { id: 'c', category: 'C', nextCategory: 'C', value: 30 } + ] + } + ], + xField, + yField: 'value', + axes: [ + { orient: 'left', visible: false }, + { orient: 'bottom', visible: false } + ] + } as IBarChartSpec); + const chart = new VChart(createSpec('category'), { + dom, + animation: false + }); + + chart.renderSync(); + + try { + const chartBefore = chart.getChart(); + const barBefore = getBarGraphicById(chart, 'b'); + const xBefore = barBefore.attribute.x; + + chart.updateSpecSync(createSpec('nextCategory')); + + const barAfter = getBarGraphicById(chart, 'b'); + + expect(chart.getChart()).toBe(chartBefore); + expect(barAfter.attribute.x).not.toBe(xBefore); + } finally { + chart.release(); + removeDom(container); + } + }); + + it('should update top-level yField through recompile without remaking chart', () => { + const container = createDiv(); + const dom = createDiv(container); + const createSpec = (yField: 'value' | 'nextValue') => + ({ + type: 'bar', + width: 300, + height: 200, + dataKey: 'id', + data: [ + { + id: 'bar', + values: [ + { id: 'a', category: 'A', value: 10, nextValue: 30 }, + { id: 'b', category: 'B', value: 30, nextValue: 30 } + ] + } + ], + xField: 'category', + yField, + axes: [ + { orient: 'left', visible: false }, + { orient: 'bottom', visible: false } + ] + } as IBarChartSpec); + const chart = new VChart(createSpec('value'), { + dom, + animation: false + }); + + chart.renderSync(); + + try { + const chartBefore = chart.getChart(); + const barBefore = getBarGraphicById(chart, 'a'); + const yBefore = barBefore.attribute.y; + const heightBefore = getBarHeight(barBefore); + + chart.updateSpecSync(createSpec('nextValue')); + + const barAfter = getBarGraphicById(chart, 'a'); + + expect(chart.getChart()).toBe(chartBefore); + expect(barAfter.attribute.y).not.toBe(yBefore); + expect(getBarHeight(barAfter)).not.toBe(heightBefore); + } finally { + chart.release(); + removeDom(container); + } + }); + + it('should remake when top-level seriesField changes because mark groupKey is initialized from seriesField', () => { + const container = createDiv(); + const dom = createDiv(container); + const createSpec = (seriesField: 'group' | 'nextGroup') => + ({ + type: 'bar', + width: 300, + height: 200, + data: [ + { + id: 'bar', + values: [ + { category: 'A', value: 10, group: 'old-a', nextGroup: 'new-a' }, + { category: 'B', value: 20, group: 'old-b', nextGroup: 'new-b' } + ] + } + ], + xField: 'category', + yField: 'value', + seriesField, + axes: [ + { orient: 'left', visible: false }, + { orient: 'bottom', visible: false } + ] + } as IBarChartSpec); + const chart = new VChart(createSpec('group'), { + dom, + animation: false + }); + + chart.renderSync(); + + try { + const chartBefore = chart.getChart(); + + chart.updateSpecSync(createSpec('nextGroup')); + + expect(chart.getChart()).not.toBe(chartBefore); + } finally { + chart.release(); + removeDom(container); + } + }); +}); + describe('vchart updateSpec of same spec', () => { let container: HTMLElement; let dom: HTMLElement; diff --git a/packages/vchart/package.json b/packages/vchart/package.json index 796c772e43..2ecda48d2a 100644 --- a/packages/vchart/package.json +++ b/packages/vchart/package.json @@ -126,11 +126,11 @@ "@visactor/vdataset": "~1.0.23", "@visactor/vscale": "~1.0.23", "@visactor/vlayouts": "~1.0.23", - "@visactor/vrender": "1.1.0-alpha.11", - "@visactor/vrender-core": "1.1.0-alpha.11", - "@visactor/vrender-kits": "1.1.0-alpha.11", - "@visactor/vrender-components": "1.1.0-alpha.11", - "@visactor/vrender-animate": "1.1.0-alpha.11", + "@visactor/vrender": "1.1.0-alpha.15", + "@visactor/vrender-core": "1.1.0-alpha.15", + "@visactor/vrender-kits": "1.1.0-alpha.15", + "@visactor/vrender-components": "1.1.0-alpha.15", + "@visactor/vrender-animate": "1.1.0-alpha.15", "@visactor/vutils-extension": "workspace:2.0.22" }, "publishConfig": { diff --git a/packages/vchart/src/chart/base/base-chart.ts b/packages/vchart/src/chart/base/base-chart.ts index 656d59e689..111154fff0 100644 --- a/packages/vchart/src/chart/base/base-chart.ts +++ b/packages/vchart/src/chart/base/base-chart.ts @@ -862,7 +862,12 @@ export class BaseChart extends CompilableBase implements I } private _getSpecKeys(spec: T) { - const ignoreKeys: Record = { width: true, height: true }; + const ignoreKeys: Record = { + width: true, + height: true, + xField: true, + yField: true + }; return Object.keys(spec) .filter(key => !ignoreKeys[key]) .sort(); diff --git a/packages/vchart/src/component/brush/brush.ts b/packages/vchart/src/component/brush/brush.ts index 686a974702..9056ba39f2 100644 --- a/packages/vchart/src/component/brush/brush.ts +++ b/packages/vchart/src/component/brush/brush.ts @@ -112,6 +112,12 @@ export class Brush extends BaseComponent i }); }); } + + reInit(spec?: T) { + super.reInit(spec); + this.init(); + } + private _initNeedOperatedItem() { const seriesUserId = this._spec.seriesId; const seriesIndex = this._spec.seriesIndex; diff --git a/packages/vchart/src/mark/base/base-mark.ts b/packages/vchart/src/mark/base/base-mark.ts index 25cc20920f..0f8fa7dc02 100644 --- a/packages/vchart/src/mark/base/base-mark.ts +++ b/packages/vchart/src/mark/base/base-mark.ts @@ -1657,7 +1657,7 @@ export class BaseMark extends GrammarItem implements IMar // 表示正在被复用,需要重设属性的 // TODO 理论上复用后只会走一次enter,所以这里lastAttrs不需要后续清除,这里需要硬拷贝(通过initAttributes重设属性也行) g.context.lastAttrs = g.attribute; - g.initAttributes({}); + g.initAttributes(finalAttrs); // 为了避免exit一些和enter不一样的属性,所以这里要重置属性 // const finalAttrs = g.getFinalAttribute(); // finalAttrs && g.initAttributes({ ...finalAttrs }); @@ -1889,9 +1889,10 @@ export class BaseMark extends GrammarItem implements IMar } })); g.applyAnimationState(['exit'], [exitConfigList.length === 1 ? exitConfigList[0] : exitConfigList], () => { - // 有可能又被复用了,所以这里需要判断,如果还是在exiting阶段的话才删除 - // TODO 这里如果频繁执行的话,可能会误判 - doRemove(g, key); + // 有可能又被复用了,所以这里需要判断,如果还是在 exiting 阶段的话才删除 + if (g.context.diffState === DiffState.exit && g.isExiting && this._graphicMap.get(key) === g) { + doRemove(g, key); + } }); } } else { diff --git a/packages/vchart/src/series/bar/bar.ts b/packages/vchart/src/series/bar/bar.ts index 05c600305a..e02dca20c2 100644 --- a/packages/vchart/src/series/bar/bar.ts +++ b/packages/vchart/src/series/bar/bar.ts @@ -105,6 +105,22 @@ export class BarSeries extends Cartes AttributeLevel.Series ); } + + this.initRectMarkStyle(); + } + + protected initRectMarkStyle() { + if (!this._barMark) { + return; + } + + const bandAxisHelper = this.direction === Direction.vertical ? this._xAxisHelper : this._yAxisHelper; + const scale = bandAxisHelper?.getScale?.(0); + if (!scale) { + return; + } + + scale.type === 'band' ? this.initBandRectMarkStyle() : this.initLinearRectMarkStyle(); } initLabelMarkStyle(textMark: ITextMark) { @@ -256,11 +272,7 @@ export class BarSeries extends Cartes init(option: IModelInitOption): void { super.init(option); - if (this.direction === 'vertical') { - this._xAxisHelper?.getScale(0).type === 'band' ? this.initBandRectMarkStyle() : this.initLinearRectMarkStyle(); - } else { - this._yAxisHelper?.getScale(0).type === 'band' ? this.initBandRectMarkStyle() : this.initLinearRectMarkStyle(); - } + this.initRectMarkStyle(); } private _shouldDoPreCalculate() { diff --git a/packages/vchart/src/series/base/base-series.ts b/packages/vchart/src/series/base/base-series.ts index c639b4420a..4203fddb08 100644 --- a/packages/vchart/src/series/base/base-series.ts +++ b/packages/vchart/src/series/base/base-series.ts @@ -111,7 +111,7 @@ export abstract class BaseSeries extends BaseModel imp declare getSpecInfo: () => ISeriesSpecInfo; - declare protected _option: ISeriesOption; + protected declare _option: ISeriesOption; // 坐标系信息 readonly coordinate: CoordinateType = 'none'; @@ -240,7 +240,7 @@ export abstract class BaseSeries extends BaseModel imp } protected _dataSet: DataSet; - declare protected _tooltipHelper: ISeriesTooltipHelper | undefined; + protected declare _tooltipHelper: ISeriesTooltipHelper | undefined; get tooltipHelper() { if (!this._tooltipHelper) { this.initTooltip(); @@ -842,8 +842,8 @@ export abstract class BaseSeries extends BaseModel imp const triggerOff = isValid(finalSelectSpec.triggerOff) ? finalSelectSpec.triggerOff : isMultiple - ? ['empty'] - : ['empty', finalSelectSpec.trigger]; + ? ['empty'] + : ['empty', finalSelectSpec.trigger]; return { type: TRIGGER_TYPE_ENUM.ELEMENT_SELECT as string, trigger: finalSelectSpec.trigger as GraphicEventType, @@ -1282,7 +1282,7 @@ export abstract class BaseSeries extends BaseModel imp attributeContext: this._markAttributeContext, componentType: option.componentType, noSeparateStyle, - parent: parent !== false ? (parent ?? this._rootMark) : null + parent: parent !== false ? parent ?? this._rootMark : null }); if (isValid(m)) { @@ -1341,13 +1341,27 @@ export abstract class BaseSeries extends BaseModel imp return key; } + const seriesField = this.getSeriesField(); const dimensionFields = this.getDimensionField(); - key = dimensionFields.map(field => datum[field]).join('_'); + const seriesFieldValue = seriesField ? datum[seriesField] : undefined; + let hasSeriesFieldInDimension = false; + let lastDimensionValue: unknown; - const seriesField = this.getSeriesField(); + for (let i = 0; i < dimensionFields.length; i++) { + const field = dimensionFields[i]; + const dimensionValue = datum[field]; + + key += `${i > 0 ? '_' : ''}${isNil(dimensionValue) ? '' : dimensionValue}`; + hasSeriesFieldInDimension ||= field === seriesField; + lastDimensionValue = dimensionValue; + } - if (seriesField && !dimensionFields.includes(seriesField)) { - key += `_${datum[seriesField]}`; + if ( + seriesField && + !hasSeriesFieldInDimension && + !(dimensionFields.length > 1 && lastDimensionValue === seriesFieldValue) + ) { + key += `_${seriesFieldValue}`; } return key; diff --git a/packages/vchart/src/series/base/constant.ts b/packages/vchart/src/series/base/constant.ts index 2f606ebe6e..e924de6b2c 100644 --- a/packages/vchart/src/series/base/constant.ts +++ b/packages/vchart/src/series/base/constant.ts @@ -17,6 +17,8 @@ export const defaultSeriesIgnoreCheckKeys: { [key: string]: true } = { }; export const defaultSeriesCompileCheckKeys: { [key: string]: true } = { + xField: true, + yField: true, invalidType: true, animation: true, animationAppear: true, diff --git a/packages/vchart/src/series/box-plot/box-plot.ts b/packages/vchart/src/series/box-plot/box-plot.ts index 0ef3be123b..75d5a0b040 100644 --- a/packages/vchart/src/series/box-plot/box-plot.ts +++ b/packages/vchart/src/series/box-plot/box-plot.ts @@ -196,6 +196,8 @@ export class BoxPlotSeries ex AttributeLevel.Series ); } + + this.initBoxPlotMarkStyle(); } initBoxPlotMarkStyle(): void { diff --git a/tools/story-player/package.json b/tools/story-player/package.json index 14e56dc3f4..2d6d381f91 100644 --- a/tools/story-player/package.json +++ b/tools/story-player/package.json @@ -56,10 +56,10 @@ "vite": "3.2.6" }, "dependencies": { - "@visactor/vrender-core": "1.1.0-alpha.11", - "@visactor/vrender-kits": "1.1.0-alpha.11", + "@visactor/vrender-core": "1.1.0-alpha.15", + "@visactor/vrender-kits": "1.1.0-alpha.15", "@visactor/vchart": "workspace:2.0.22", - "@visactor/vrender": "1.1.0-alpha.11", + "@visactor/vrender": "1.1.0-alpha.15", "@visactor/vutils": "~1.0.23" } }