-
Notifications
You must be signed in to change notification settings - Fork 221
fix: skip default select when interactions already registers element-select #4696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| 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<IPieSeriesSpec> { | ||
| protected _computeLayoutRadius() { | ||
| return 100; | ||
| } | ||
| } | ||
|
|
||
| const dummyEvent = { | ||
| on: (): void => undefined, | ||
| off: (): void => undefined, | ||
| emit: (): void => undefined | ||
| }; | ||
|
|
||
| function createPieSeries(spec: Partial<IPieSeriesSpec> = {}) { | ||
| 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<IPieSeriesSpec>) { | ||
| return series.getInteractionTriggers().filter(item => item.trigger.type === TRIGGER_TYPE_ENUM.ELEMENT_SELECT); | ||
| } | ||
|
|
||
| function createGraphic(mark: IMark): IMarkGraphic { | ||
| const graphic = { | ||
| context: { markId: mark.id }, | ||
| currentStates: [] as string[], | ||
| hasState(state: string) { | ||
| return graphic.currentStates.includes(state); | ||
| }, | ||
| setStates(states?: string[] | null) { | ||
| graphic.currentStates = states ?? []; | ||
| } | ||
| }; | ||
| return graphic as unknown as IMarkGraphic; | ||
| } | ||
|
|
||
| function getIsMultiple(trigger: ReturnType<PieSeries<IPieSeriesSpec>['getInteractionTriggers']>[number]['trigger']) { | ||
| return (trigger as { isMultiple?: boolean }).isMultiple; | ||
| } | ||
|
|
||
| function startSelects(selectTriggers: ReturnType<typeof getSelectTriggers>, 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(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); | ||
|
|
||
| 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(getIsMultiple(selectTriggers[0].trigger)).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(getIsMultiple(selectTriggers[0].trigger)).toBe(true); | ||
|
|
||
| const mark = selectTriggers[0].marks[0]; | ||
| const statedLists = startSelects(selectTriggers, [createGraphic(mark), createGraphic(mark)]); | ||
|
|
||
| expect(statedLists[0].length).toBeGreaterThan(1); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -956,6 +956,14 @@ export abstract class BaseSeries<T extends ISeriesSpec> extends BaseModel<T> 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] 替换默认选中时保留 selected_reverse 状态 当系列配置 |
||
| } | ||
| } | ||
|
|
||
| if (interactions && interactions.length) { | ||
| interactions.forEach(interaction => { | ||
| const marks: IMark[] = filterMarksOfInteraction(interaction, this.getMarks()); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] 保留自定义交互作用范围外的默认选中
这里只要存在
element-select就删除整个默认 select,忽略了markNames/markIds的作用范围。例如柱线组合图在图表级配置interactions: [{ type: 'element-select', markNames: ['bar'], isMultiple: true }]:该配置会传给两个系列,折线系列虽然没有匹配的 bar 图元,也会先删除默认 select,随后又因匹配结果为空而不注册自定义交互,最终折线点完全无法选中。单个折线图配置markNames: ['line']时,未被覆盖的 point 同样失去选中能力。上述两个场景均已验证基线通过、PR 失败。请先解析实际匹配的图元,仅处理重叠范围内的冲突,保留其他图元的默认选中,并补充回归测试。