From de2231e3af5563ddebdba36b8dfce0ca5a732b0c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 02:29:43 +0000 Subject: [PATCH 1/2] fix: skip default select when interactions already registers element-select When spec.interactions includes element-select (including isMultiple: true), do not also register the default single-select trigger so the two do not fight. Co-authored-by: David --- ...lement-select-default-4563_2026-09-20.json | 10 ++ .../unit/interaction/element-select.test.ts | 150 ++++++++++++++++++ .../vchart/src/series/base/base-series.ts | 8 + 3 files changed, 168 insertions(+) create mode 100644 common/changes/@visactor/vchart/fix-interactions-element-select-default-4563_2026-09-20.json create mode 100644 packages/vchart/__tests__/unit/interaction/element-select.test.ts diff --git a/common/changes/@visactor/vchart/fix-interactions-element-select-default-4563_2026-09-20.json b/common/changes/@visactor/vchart/fix-interactions-element-select-default-4563_2026-09-20.json new file mode 100644 index 0000000000..635be6953c --- /dev/null +++ b/common/changes/@visactor/vchart/fix-interactions-element-select-default-4563_2026-09-20.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "fix: skip default single-select when spec.interactions already registers element-select, so isMultiple multi-select works", + "type": "patch" + } + ], + "packageName": "@visactor/vchart" +} diff --git a/packages/vchart/__tests__/unit/interaction/element-select.test.ts b/packages/vchart/__tests__/unit/interaction/element-select.test.ts new file mode 100644 index 0000000000..c306bee48d --- /dev/null +++ b/packages/vchart/__tests__/unit/interaction/element-select.test.ts @@ -0,0 +1,150 @@ +import { DataSet, DataView } from '@visactor/vdataset'; +import type { ISeriesOption } from '../../../src/series/interface'; +import { PieSeries, registerPieSeries } from '../../../src/series/pie/pie'; +import type { IPieSeriesSpec } from '../../../src/series/pie/interface'; +import { ElementSelect } from '../../../src/interaction/triggers/element-select'; +import { Interaction } from '../../../src/interaction/interaction'; +import { TRIGGER_TYPE_ENUM } from '../../../src/interaction/triggers/enum'; +import type { IMark } from '../../../src/mark/interface'; +import type { IMarkGraphic } from '../../../src/mark/interface/common'; +import { initChartDataSet, seriesOption } from '../../util/context'; + +registerPieSeries(); + +const dataSet = new DataSet(); +initChartDataSet(dataSet); + +class TestPieSeries extends PieSeries { + protected _computeLayoutRadius() { + return 100; + } +} + +const dummyEvent = { + on: () => undefined, + off: () => undefined, + emit: () => undefined +}; + +function createPieSeries(spec: Partial = {}) { + const dataView = new DataView(dataSet); + dataView.parse( + [ + { type: 'oxygen', value: 46.6 }, + { type: 'silicon', value: 27.72 }, + { type: 'aluminum', value: 8.13 } + ], + { + type: 'array' + } + ); + + const series = new TestPieSeries( + { + type: 'pie', + data: dataView, + valueField: 'value', + categoryField: 'type', + ...spec + }, + seriesOption({ dataSet }) as ISeriesOption + ); + series.created(); + series.init({}); + return series; +} + +function getSelectTriggers(series: PieSeries) { + return series.getInteractionTriggers().filter(item => item.trigger.type === TRIGGER_TYPE_ENUM.ELEMENT_SELECT); +} + +function createGraphic(mark: IMark): IMarkGraphic { + return { + context: { markId: mark.id }, + currentStates: [] as string[], + hasState(state: string) { + return this.currentStates.includes(state); + }, + setStates(states?: string[] | null) { + this.currentStates = states ?? []; + } + } as unknown as IMarkGraphic; +} + +function startSelects( + selectTriggers: ReturnType, + graphics: IMarkGraphic[] +) { + const instances = selectTriggers.map(({ trigger, marks }) => { + const interaction = new Interaction(); + const instance = new ElementSelect({ + ...(trigger as any), + marks, + event: dummyEvent, + interaction + }); + return { interaction, instance }; + }); + + graphics.forEach(graphic => { + instances.forEach(({ instance }) => instance.start(graphic)); + }); + + return instances.map(({ interaction, instance }) => interaction.getStatedGraphics(instance) ?? []); +} + +describe('element-select vs default select', () => { + test('interactions isMultiple accumulates without default single-select fighting it', () => { + const series = createPieSeries({ + interactions: [ + { + type: 'element-select', + isMultiple: true + } + ] + }); + + const triggers = series.getInteractionTriggers(); + const selectTriggers = getSelectTriggers(series); + + expect(selectTriggers).toHaveLength(1); + expect(selectTriggers[0].trigger.isMultiple).toBe(true); + expect(triggers.some(item => item.trigger.type === TRIGGER_TYPE_ENUM.DIMENSION_HOVER)).toBe(true); + expect(triggers.some(item => item.trigger.type === TRIGGER_TYPE_ENUM.ELEMENT_HIGHLIGHT)).toBe(true); + + const mark = selectTriggers[0].marks[0]; + const statedLists = startSelects(selectTriggers, [createGraphic(mark), createGraphic(mark)]); + + expect(statedLists.some(graphics => graphics.length > 1)).toBe(true); + }); + + test('default select without interactions stays single-select', () => { + const series = createPieSeries(); + const selectTriggers = getSelectTriggers(series); + + expect(selectTriggers).toHaveLength(1); + expect(selectTriggers[0].trigger.isMultiple).toBe(false); + + const mark = selectTriggers[0].marks[0]; + const statedLists = startSelects(selectTriggers, [createGraphic(mark), createGraphic(mark)]); + + expect(statedLists[0]).toHaveLength(1); + }); + + test('select.mode multiple still accumulates without interactions', () => { + const series = createPieSeries({ + select: { + mode: 'multiple' + } + }); + const selectTriggers = getSelectTriggers(series); + + expect(selectTriggers).toHaveLength(1); + expect(selectTriggers[0].trigger.isMultiple).toBe(true); + + const mark = selectTriggers[0].marks[0]; + const statedLists = startSelects(selectTriggers, [createGraphic(mark), createGraphic(mark)]); + + expect(statedLists[0].length).toBeGreaterThan(1); + }); +}); diff --git a/packages/vchart/src/series/base/base-series.ts b/packages/vchart/src/series/base/base-series.ts index 0e0b816711..35c3b19025 100644 --- a/packages/vchart/src/series/base/base-series.ts +++ b/packages/vchart/src/series/base/base-series.ts @@ -956,6 +956,14 @@ export abstract class BaseSeries extends BaseModel imp const { interactions } = this._spec; const res = this._parseDefaultInteractionConfig(mainMarks); + // Custom element-select is not isEqual to default select, so both would stay live and fight. + if (interactions?.some(interaction => interaction.type === TRIGGER_TYPE_ENUM.ELEMENT_SELECT)) { + const defaultSelectIndex = res.findIndex(item => item.trigger.type === TRIGGER_TYPE_ENUM.ELEMENT_SELECT); + if (defaultSelectIndex >= 0) { + res.splice(defaultSelectIndex, 1); + } + } + if (interactions && interactions.length) { interactions.forEach(interaction => { const marks: IMark[] = filterMarksOfInteraction(interaction, this.getMarks()); From 312350584a389c56dbfc6010f6cdceb8591509b0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 02:32:55 +0000 Subject: [PATCH 2/2] test: format element-select interaction regression Co-authored-by: David --- .../unit/interaction/element-select.test.ts | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/vchart/__tests__/unit/interaction/element-select.test.ts b/packages/vchart/__tests__/unit/interaction/element-select.test.ts index c306bee48d..a088482d69 100644 --- a/packages/vchart/__tests__/unit/interaction/element-select.test.ts +++ b/packages/vchart/__tests__/unit/interaction/element-select.test.ts @@ -21,9 +21,9 @@ class TestPieSeries extends PieSeries { } const dummyEvent = { - on: () => undefined, - off: () => undefined, - emit: () => undefined + on: (): void => undefined, + off: (): void => undefined, + emit: (): void => undefined }; function createPieSeries(spec: Partial = {}) { @@ -59,22 +59,24 @@ function getSelectTriggers(series: PieSeries) { } function createGraphic(mark: IMark): IMarkGraphic { - return { + const graphic = { context: { markId: mark.id }, currentStates: [] as string[], hasState(state: string) { - return this.currentStates.includes(state); + return graphic.currentStates.includes(state); }, setStates(states?: string[] | null) { - this.currentStates = states ?? []; + graphic.currentStates = states ?? []; } - } as unknown as IMarkGraphic; + }; + return graphic as unknown as IMarkGraphic; } -function startSelects( - selectTriggers: ReturnType, - graphics: IMarkGraphic[] -) { +function getIsMultiple(trigger: ReturnType['getInteractionTriggers']>[number]['trigger']) { + return (trigger as { isMultiple?: boolean }).isMultiple; +} + +function startSelects(selectTriggers: ReturnType, graphics: IMarkGraphic[]) { const instances = selectTriggers.map(({ trigger, marks }) => { const interaction = new Interaction(); const instance = new ElementSelect({ @@ -108,7 +110,7 @@ describe('element-select vs default select', () => { const selectTriggers = getSelectTriggers(series); expect(selectTriggers).toHaveLength(1); - expect(selectTriggers[0].trigger.isMultiple).toBe(true); + expect(getIsMultiple(selectTriggers[0].trigger)).toBe(true); expect(triggers.some(item => item.trigger.type === TRIGGER_TYPE_ENUM.DIMENSION_HOVER)).toBe(true); expect(triggers.some(item => item.trigger.type === TRIGGER_TYPE_ENUM.ELEMENT_HIGHLIGHT)).toBe(true); @@ -123,7 +125,7 @@ describe('element-select vs default select', () => { const selectTriggers = getSelectTriggers(series); expect(selectTriggers).toHaveLength(1); - expect(selectTriggers[0].trigger.isMultiple).toBe(false); + expect(getIsMultiple(selectTriggers[0].trigger)).toBe(false); const mark = selectTriggers[0].marks[0]; const statedLists = startSelects(selectTriggers, [createGraphic(mark), createGraphic(mark)]); @@ -140,7 +142,7 @@ describe('element-select vs default select', () => { const selectTriggers = getSelectTriggers(series); expect(selectTriggers).toHaveLength(1); - expect(selectTriggers[0].trigger.isMultiple).toBe(true); + expect(getIsMultiple(selectTriggers[0].trigger)).toBe(true); const mark = selectTriggers[0].marks[0]; const statedLists = startSelects(selectTriggers, [createGraphic(mark), createGraphic(mark)]);