From 96cdad5ee87cc0426fa3fda0356d266f8a340e99 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 19:15:57 +0000 Subject: [PATCH 1/3] fix: guard collection-mark matching in filterGraphicsByDatum Co-authored-by: David --- ...icsbydatum-collection-4678_2026-09-19.json | 10 ++ .../chart/filter-graphics-by-datum.test.ts | 145 ++++++++++++++++++ packages/vchart/src/chart/base/base-chart.ts | 5 +- 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 common/changes/@visactor/vchart/fix-filtergraphicsbydatum-collection-4678_2026-09-19.json create mode 100644 packages/vchart/__tests__/unit/chart/filter-graphics-by-datum.test.ts diff --git a/common/changes/@visactor/vchart/fix-filtergraphicsbydatum-collection-4678_2026-09-19.json b/common/changes/@visactor/vchart/fix-filtergraphicsbydatum-collection-4678_2026-09-19.json new file mode 100644 index 0000000000..b5685ae511 --- /dev/null +++ b/common/changes/@visactor/vchart/fix-filtergraphicsbydatum-collection-4678_2026-09-19.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "fix: ignore unmatched datums in filterGraphicsByDatum collection-mark matching so setHovered does not throw", + "type": "patch" + } + ], + "packageName": "@visactor/vchart" +} diff --git a/packages/vchart/__tests__/unit/chart/filter-graphics-by-datum.test.ts b/packages/vchart/__tests__/unit/chart/filter-graphics-by-datum.test.ts new file mode 100644 index 0000000000..7b2b2c91d1 --- /dev/null +++ b/packages/vchart/__tests__/unit/chart/filter-graphics-by-datum.test.ts @@ -0,0 +1,145 @@ +import { GlobalScale } from '../../../src/scale/global-scale'; +import { DataSet, DataView, csvParser } from '@visactor/vdataset'; +import type { ILineChartSpec } from '../../../src/chart/line/interface'; +import { LineChart, registerLineChart } from '../../../src/chart/line/line'; +import { EventDispatcher } from '../../../src/event/event-dispatcher'; +import { MarkTypeEnum } from '../../../src/mark/interface/type'; +import type { IMark } from '../../../src/mark/interface'; +import type { IMarkGraphic } from '../../../src/mark/interface/common'; +import { getTestCompiler } from '../../util/factory/compiler'; +import { getTheme, initChartDataSet } from '../../util/context'; + +registerLineChart(); + +const dataSet = new DataSet(); +initChartDataSet(dataSet); +dataSet.registerParser('csv', csvParser); + +function createLineChart() { + const dataView = new DataView(dataSet); + dataView.parse( + `x,type,y +1,1,850 +2,2,740 +3,3,900`, + { + type: 'csv' + } + ); + const spec = { + type: 'line', + data: dataView, + xField: 'x', + yField: 'y', + animation: false + }; + const transformer = new LineChart.transformerConstructor({ + type: 'line', + seriesType: 'line', + getTheme: getTheme, + mode: 'desktop-browser' + }); + const info = transformer.initChartSpec(spec as any); + const chart = new LineChart( + spec as any, + { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + eventDispatcher: new EventDispatcher({} as any, { addEventListener: () => {} } as any), + globalInstance: { + isAnimationEnable: () => true, + getContainer: () => ({}), + getTooltipHandlerByUser: (() => undefined) as () => undefined + }, + render: {} as any, + dataSet, + map: new Map(), + container: null, + mode: 'desktop-browser', + getCompiler: getTestCompiler, + globalScale: new GlobalScale([], { getAllSeries: () => [] as any[] } as any), + getTheme: getTheme, + onError: () => {}, + getSpecInfo: () => info + } as any + ); + chart.created(transformer); + chart.init(); + return chart; +} + +function findMark(chart: LineChart, type: string): IMark { + const mark = chart + .getAllSeries()[0] + .getMarks() + .find(m => m.type === type); + if (!mark) { + throw new Error(`Expected ${type} mark to exist`); + } + return mark; +} + +describe('filterGraphicsByDatum collection marks', () => { + const lineData = [ + { time: '2:00', value: 8, series: 'sys' }, + { time: '4:00', value: 9, series: 'sys' }, + { time: '6:00', value: 11, series: 'sys' } + ]; + const otherSeriesDatum = { time: '2:00', value: 6, series: 'dia' }; + const unmatchedDatum = { time: '20:00', value: 12, series: 'sys' }; + + test('does not throw when hovered datums are longer than a collection mark datum array', () => { + const chart = createLineChart(); + const lineMark = findMark(chart, MarkTypeEnum.line); + const lineGraphic = { id: 'line-graphic' } as unknown as IMarkGraphic; + jest.spyOn(lineMark, 'getGraphics').mockReturnValue([lineGraphic]); + + expect(() => + chart.filterGraphicsByDatum([...lineData, unmatchedDatum], { + getDatum: () => lineData + }) + ).not.toThrow(); + }); + + test('ignores unmatched extra datums on collection marks and still picks an exact match', () => { + const chart = createLineChart(); + const lineMark = findMark(chart, MarkTypeEnum.line); + const lineGraphic = { id: 'line-graphic' } as unknown as IMarkGraphic; + jest.spyOn(lineMark, 'getGraphics').mockReturnValue([lineGraphic]); + + const unmatched = chart.filterGraphicsByDatum([...lineData, unmatchedDatum], { + getDatum: () => lineData + }); + expect(unmatched).toEqual([]); + + const matched = chart.filterGraphicsByDatum(lineData, { + getDatum: () => lineData + }); + expect(matched).toEqual([lineGraphic]); + }); + + test('skips a shorter collection mark and still highlights matched point graphics', () => { + const chart = createLineChart(); + const lineMark = findMark(chart, MarkTypeEnum.line); + const pointMark = findMark(chart, MarkTypeEnum.symbol); + const lineGraphic = { id: 'line-graphic' } as unknown as IMarkGraphic; + const sysPoint = { id: 'sys-point' } as unknown as IMarkGraphic; + const diaPoint = { id: 'dia-point' } as unknown as IMarkGraphic; + + jest.spyOn(lineMark, 'getGraphics').mockReturnValue([lineGraphic]); + jest.spyOn(pointMark, 'getGraphics').mockReturnValue([sysPoint, diaPoint]); + + const hovered = [lineData[0], otherSeriesDatum]; + const picked = chart.filterGraphicsByDatum(hovered, { + getDatum: (el, mark) => { + if (mark.type === MarkTypeEnum.line) { + // Collection marks often have fewer datums than a multi-series hover list. + return [lineData[0]]; + } + return el === sysPoint ? lineData[0] : otherSeriesDatum; + } + }); + + expect(picked).toEqual([sysPoint, diaPoint]); + }); +}); diff --git a/packages/vchart/src/chart/base/base-chart.ts b/packages/vchart/src/chart/base/base-chart.ts index 862951b6cb..56b73baebe 100644 --- a/packages/vchart/src/chart/base/base-chart.ts +++ b/packages/vchart/src/chart/base/base-chart.ts @@ -1748,7 +1748,10 @@ export class BaseChart extends CompilableBase implements I const elDatum = getDatumOfElement(e, m, s, r) as Datum[]; const isPick = // eslint-disable-next-line max-nested-callbacks, eqeqeq - elDatum && (datum as Datum[]).every((d, index) => keys.every(k => d[k] == elDatum[index][k])); + elDatum && + (datum as Datum[]).every( + (d, index) => elDatum[index] && keys.every(k => d[k] == elDatum[index][k]) + ); if (isPick) { pickElements.push(e); From 9619ad22245a7d6600054f3692907be1c6ecfecd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 19:21:42 +0000 Subject: [PATCH 2/3] fix: keep eqeqeq lint disable on collection-mark compare Co-authored-by: David --- packages/vchart/src/chart/base/base-chart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vchart/src/chart/base/base-chart.ts b/packages/vchart/src/chart/base/base-chart.ts index 56b73baebe..ee586108e1 100644 --- a/packages/vchart/src/chart/base/base-chart.ts +++ b/packages/vchart/src/chart/base/base-chart.ts @@ -1747,9 +1747,9 @@ export class BaseChart extends CompilableBase implements I graphics.filter(e => { const elDatum = getDatumOfElement(e, m, s, r) as Datum[]; const isPick = - // eslint-disable-next-line max-nested-callbacks, eqeqeq elDatum && (datum as Datum[]).every( + // eslint-disable-next-line max-nested-callbacks, eqeqeq (d, index) => elDatum[index] && keys.every(k => d[k] == elDatum[index][k]) ); From 3fb02fddaf98eb39258895b7d2aa8087f7de8173 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 19:25:36 +0000 Subject: [PATCH 3/3] test: scope hover regression to line and point marks Co-authored-by: David --- .../vchart/__tests__/unit/chart/filter-graphics-by-datum.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vchart/__tests__/unit/chart/filter-graphics-by-datum.test.ts b/packages/vchart/__tests__/unit/chart/filter-graphics-by-datum.test.ts index 7b2b2c91d1..609dbf71b5 100644 --- a/packages/vchart/__tests__/unit/chart/filter-graphics-by-datum.test.ts +++ b/packages/vchart/__tests__/unit/chart/filter-graphics-by-datum.test.ts @@ -131,6 +131,7 @@ describe('filterGraphicsByDatum collection marks', () => { const hovered = [lineData[0], otherSeriesDatum]; const picked = chart.filterGraphicsByDatum(hovered, { + filter: (_series, mark) => mark === lineMark || mark === pointMark, getDatum: (el, mark) => { if (mark.type === MarkTypeEnum.line) { // Collection marks often have fewer datums than a multi-series hover list.