From 9fbcc32ec16675329c168cc390468f3cbe64f27a Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Tue, 21 Apr 2026 18:00:30 +0800 Subject: [PATCH 1/2] fix(vchart): preserve legend filtering after updateSpec recompiles Discrete legends with custom data callbacks stopped filtering after `updateSpec(..., { reMake: false })`. `BaseLegend.clear()` released the vrender legend component but kept its stale instance reference. The next compile pass reused that dead instance and skipped the event-binding path that forwards legend clicks into VChart selection and data filtering. Null the cached legend component during cleanup, add a focused regression test for the callback-backed legend update path, and record the issue spec/plan/tasks alongside the code change. Constraint: Preserve `reMake: false` update path and discrete legend filtering semantics Rejected: Force `reMake` for updated legends | broader rebuild cost and masks lifecycle bug Confidence: high Scope-risk: narrow Reversibility: clean Directive: If legend cleanup changes, verify recompile recreates vrender components Tested: jest __tests__/unit/core/vchart-event.test.ts --runInBand (packages/vchart) Tested: npm run compile (packages/vchart) Not-tested: Manual browser reproduction against the original issue sandbox Related: #4566 --- .../__tests__/unit/core/vchart-event.test.ts | 130 ++++++++++++++++++ .../src/component/legend/base-legend.ts | 1 + .../plan.md | 44 ++++++ .../spec.md | 43 ++++++ .../tasks.md | 28 ++++ 5 files changed, 246 insertions(+) create mode 100644 specs/011-fix-discrete-legend-filter-after-update-spec/plan.md create mode 100644 specs/011-fix-discrete-legend-filter-after-update-spec/spec.md create mode 100644 specs/011-fix-discrete-legend-filter-after-update-spec/tasks.md diff --git a/packages/vchart/__tests__/unit/core/vchart-event.test.ts b/packages/vchart/__tests__/unit/core/vchart-event.test.ts index 993e3c13d8..536b33e0c5 100644 --- a/packages/vchart/__tests__/unit/core/vchart-event.test.ts +++ b/packages/vchart/__tests__/unit/core/vchart-event.test.ts @@ -193,3 +193,133 @@ describe('vchart event test', () => { expect(handleTooltipRelease).toBeCalledTimes(1); }); }); + +describe('discrete legend with custom data after updateSpec', () => { + let container: HTMLElement; + let dom: HTMLElement; + let vchart: VChart; + + const data1 = [ + { x: '周一', type: '早餐', y: 15 }, + { x: '周一', type: '午餐', y: 25 }, + { x: '周二', type: '早餐', y: 12 }, + { x: '周二', type: '午餐', y: 30 } + ]; + + const data2 = [ + { x: '周一', type: '早餐2', y: 15 }, + { x: '周一', type: '午餐2', y: 25 }, + { x: '周二', type: '早餐2', y: 12 }, + { x: '周二', type: '午餐2', y: 30 }, + { x: '周一', type: '饮料2', y: 22 }, + { x: '周二', type: '饮料2', y: 43 } + ]; + + beforeEach(() => { + container = createDiv(); + dom = createDiv(container); + dom.id = 'container'; + container.style.position = 'fixed'; + container.style.width = '500px'; + container.style.height = '500px'; + container.style.top = '0px'; + container.style.left = '0px'; + + const spec = { + type: 'common', + animation: false, + data: [ + { + id: 'id0', + values: data1 + } + ], + seriesField: 'type', + legends: [ + { + orient: 'right', + visible: true, + data: (items: any[]) => items + } + ], + series: [ + { + type: 'line', + id: 'line', + dataIndex: 0, + xField: ['x'], + yField: 'y' + } + ], + axes: [ + { orient: 'left', seriesIndex: [0] }, + { orient: 'bottom', label: { visible: true }, type: 'band' } + ] + } as any; + + vchart = new VChart(spec, { + dom, + animation: false + }); + vchart.renderSync(); + }); + + afterEach(() => { + vchart.release(); + removeDom(container); + }); + + it('should keep legend filtering working after updateSpecSync without remake', () => { + const nextSpec = { + ...vchart.getSpec(), + data: [ + { + id: 'id0', + values: data2 + } + ], + legends: [ + { + orient: 'right', + visible: true, + data: (items: any[]) => items + } + ] + }; + + vchart.updateSpecSync(nextSpec, false, undefined, { + change: false, + reMake: false + }); + + const legend = vchart.getComponents().find(com => com.type === 'discreteLegend') as any; + const legendComponent = legend.getVRenderComponents()[0] as any; + const legendItem = legendComponent._itemsContainer.getChildren()[0]; + const series = vchart.getChart()?.getAllSeries()[0]; + const vrenderLegendClickSpy = jest.fn(); + + legendComponent.addEventListener('legendItemClick', vrenderLegendClickSpy); + + expect(legend.getLegendDefaultData()).toEqual(['早餐2', '午餐2', '饮料2']); + expect(series?.getViewData()?.latestData.map((datum: any) => datum.type)).toEqual([ + '早餐2', + '午餐2', + '早餐2', + '午餐2', + '饮料2', + '饮料2' + ]); + + legendComponent._onClick({ target: legendItem }); + + expect(vrenderLegendClickSpy).toHaveBeenCalledTimes(1); + expect(vrenderLegendClickSpy.mock.calls[0]?.[0]?.detail?.currentSelected).toEqual(['午餐2', '饮料2']); + expect(legend.getSelectedData()).toEqual(['午餐2', '饮料2']); + expect(series?.getViewData()?.latestData.map((datum: any) => datum.type)).toEqual([ + '午餐2', + '午餐2', + '饮料2', + '饮料2' + ]); + }); +}); diff --git a/packages/vchart/src/component/legend/base-legend.ts b/packages/vchart/src/component/legend/base-legend.ts index 8a289e532d..95dcf7eb50 100644 --- a/packages/vchart/src/component/legend/base-legend.ts +++ b/packages/vchart/src/component/legend/base-legend.ts @@ -304,6 +304,7 @@ export abstract class BaseLegend extends BaseCompon clear(): void { super.clear(); + this._legendComponent = null; this._cacheAttrs = null; this._preSelectedData = null; } diff --git a/specs/011-fix-discrete-legend-filter-after-update-spec/plan.md b/specs/011-fix-discrete-legend-filter-after-update-spec/plan.md new file mode 100644 index 0000000000..3321da7e89 --- /dev/null +++ b/specs/011-fix-discrete-legend-filter-after-update-spec/plan.md @@ -0,0 +1,44 @@ +# Implementation Plan: Fix Discrete Legend Filtering After updateSpec + +**Branch**: `011-fix-discrete-legend-filter-after-update-spec` | **Date**: 2026-04-21 | **Spec**: [/Users/bytedance/Documents/GitHub/VChart3/specs/011-fix-discrete-legend-filter-after-update-spec/spec.md](/Users/bytedance/Documents/GitHub/VChart3/specs/011-fix-discrete-legend-filter-after-update-spec/spec.md) +**Input**: Feature specification from `/specs/011-fix-discrete-legend-filter-after-update-spec/spec.md` + +## Summary + +Fix the discrete legend regression reported in issue #4566 by clearing the stale vrender legend instance during `BaseLegend.clear()`. The root cause is that the `reCompile` path releases the old legend component but leaves `_legendComponent` populated, so the next layout pass reuses a dead instance and skips the event-binding path that connects legend clicks back into VChart filtering. Add a focused regression test that reproduces `updateSpecSync(..., { reMake: false })` with `legends.data: items => items`. + +## Technical Context + +**Language/Version**: TypeScript 4.9.x +**Primary Dependencies**: `@visactor/vrender-components`, `@visactor/vutils`, `@visactor/vdataset` +**Testing**: Jest 26 unit tests under `packages/vchart/__tests__`, plus `tsc --noEmit` +**Target Platform**: Browser and cross-platform chart runtime +**Project Type**: Monorepo charting library package (`packages/vchart`) +**Constraints**: Keep the fix scoped to legend lifecycle cleanup; avoid changing discrete legend filtering semantics; preserve existing `reMake: true` behavior + +## Constitution Check + +- Pass: The change is narrowly scoped to the documented bug path. +- Pass: The fix reuses the existing legend rebuild path instead of adding a new special-case update branch. +- Pass: Regression coverage is added for the exact user-visible interaction. + +## Project Structure + +```text +specs/011-fix-discrete-legend-filter-after-update-spec/ +├── plan.md +├── spec.md +└── tasks.md +``` + +```text +packages/vchart/ +├── src/component/legend/base-legend.ts +└── __tests__/unit/core/vchart-event.test.ts +``` + +## Implementation Strategy + +1. Reproduce the regression with a focused event test that uses a discrete legend `data` callback and `updateSpecSync(..., { reMake: false })`. +2. Fix legend lifecycle cleanup by nulling `_legendComponent` after releasing the old vrender component so the next compile path recreates and rebinds it. +3. Run the focused Jest test file and TypeScript compile check to verify the regression and avoid collateral breakage. diff --git a/specs/011-fix-discrete-legend-filter-after-update-spec/spec.md b/specs/011-fix-discrete-legend-filter-after-update-spec/spec.md new file mode 100644 index 0000000000..57b5a8aeb6 --- /dev/null +++ b/specs/011-fix-discrete-legend-filter-after-update-spec/spec.md @@ -0,0 +1,43 @@ +# Feature Specification: Fix Discrete Legend Filtering After updateSpec + +**Feature Branch**: `011-fix-discrete-legend-filter-after-update-spec` +**Created**: 2026-04-21 +**Status**: Draft +**Input**: User description: "Fix VChart issue #4566: after `updateSpec`, clicking a discrete legend with a custom `data` callback no longer triggers legend filtering unless `reMake` is true" + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Keep Discrete Legend Filtering Active After updateSpec (Priority: P1) + +As a chart author using a discrete legend with a custom `data` callback, I want legend clicks to keep filtering the latest series data after `updateSpec(..., { reMake: false })` so that I can update chart data without remaking the chart and losing legend interaction. + +**Why this priority**: The reported bug breaks the primary discrete-legend interaction path immediately after a common update flow and regresses behavior that previously worked in 1.x. + +**Independent Test**: Render a chart whose legend uses `data: items => items`, call `updateSpecSync` with new legend values and `reMake: false`, click a legend item, and verify both the legend selected state and filtered series view data update to the new legend domain. + +**Acceptance Scenarios**: + +1. **Given** a discrete legend configured with a custom `data` callback, **When** `updateSpecSync` updates the backing data with `reMake: false`, **Then** the legend still exposes the latest legend items after the update. +2. **Given** the same chart after `updateSpecSync`, **When** the user clicks a legend item, **Then** VChart updates the legend selected data and re-filters series view data according to the clicked legend item. + +### Edge Cases + +- The `reCompile` path must rebuild or rebind the underlying vrender legend component after cleanup so event handlers are not lost. +- Existing discrete legends without a custom `data` callback must keep their current interaction behavior. +- `reMake: true` behavior must remain unchanged. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: After `updateSpec` on a discrete legend with a custom `data` callback, the legend component MUST use the updated legend item set when `reMake` is false. +- **FR-002**: After the same update path, clicking a legend item MUST update `selectedData` and trigger the normal series data filtering flow. +- **FR-003**: Legend cleanup on the `reCompile` path MUST NOT retain a released vrender legend component instance. +- **FR-004**: Existing discrete legend behavior outside this update path MUST remain backward compatible. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: In the issue #4566 reproduction, `updateSpec(..., { reMake: false })` no longer disables legend filtering when `legends.data` is a function. +- **SC-002**: A focused automated regression test proves legend selection and series filtering still work after the update. diff --git a/specs/011-fix-discrete-legend-filter-after-update-spec/tasks.md b/specs/011-fix-discrete-legend-filter-after-update-spec/tasks.md new file mode 100644 index 0000000000..4fd90fb642 --- /dev/null +++ b/specs/011-fix-discrete-legend-filter-after-update-spec/tasks.md @@ -0,0 +1,28 @@ +# Tasks: Discrete Legend Filtering After updateSpec + +**Input**: Design documents from `/specs/011-fix-discrete-legend-filter-after-update-spec/` +**Prerequisites**: plan.md, spec.md + +**Tests**: Include focused regression coverage for discrete legend interaction after `updateSpecSync(..., { reMake: false })`. + +## Phase 1: Setup + +- [x] T001 Inspect discrete legend update, cleanup, and recompile lifecycle around `BaseLegend` and `DiscreteLegend` + +## Phase 2: User Story 1 - Keep Discrete Legend Filtering Active After updateSpec (Priority: P1) + +**Goal**: Preserve legend click filtering after `updateSpec` when the legend uses a custom `data` callback and the chart avoids remake + +### Tests for User Story 1 + +- [x] T002 [US1] Add a regression test for `updateSpecSync(..., { reMake: false })` with `legends.data: items => items` in `packages/vchart/__tests__/unit/core/vchart-event.test.ts` + +### Implementation for User Story 1 + +- [x] T003 [US1] Clear the stale vrender legend instance in `packages/vchart/src/component/legend/base-legend.ts` so legend events are rebound on recompile + +## Phase 3: Verification + +- [x] T004 Run the focused Jest test file for `packages/vchart/__tests__/unit/core/vchart-event.test.ts` +- [x] T005 Run `npm run compile` in `packages/vchart` +- [x] T006 Update task tracking after implementation From 6847417b3b1d439cdc1bab0b7ee4fb17aa036753 Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Tue, 21 Apr 2026 18:01:22 +0800 Subject: [PATCH 2/2] chore(vchart): record release note for legend updateSpec fix Add the required Rush changefile for the discrete legend filtering fix so release automation can emit the patch note without manual changelog reconstruction later. Constraint: Submit a Rush change file with source changes in this repository Rejected: Fold into previous commit | fix commit already passed hooks intact Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep changefile comments aligned with the shipped bug-fix description Tested: Added JSON under common/changes/@visactor/vchart and staged it cleanly Not-tested: Release workflow generation from this changefile Related: #4566 --- .../fix-legend-filter-after-updatespec_20260421.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vchart/fix-legend-filter-after-updatespec_20260421.json diff --git a/common/changes/@visactor/vchart/fix-legend-filter-after-updatespec_20260421.json b/common/changes/@visactor/vchart/fix-legend-filter-after-updatespec_20260421.json new file mode 100644 index 0000000000..05cfdb91f7 --- /dev/null +++ b/common/changes/@visactor/vchart/fix-legend-filter-after-updatespec_20260421.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "fix: preserve discrete legend filtering after updateSpec when `legends.data` is a callback (Issue #4566)", + "type": "patch" + } + ], + "packageName": "@visactor/vchart", + "email": "openai@example.com" +}