From ed5b71e22492b94d1157a6cb7c3f92e130ee1e45 Mon Sep 17 00:00:00 2001 From: Visiky <736929286@qq.com> Date: Sat, 16 Oct 2021 00:11:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(venn):=20=E9=9F=A6=E6=81=A9=E5=9B=BE?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E5=A2=9E=E5=BC=BA=EF=BC=88=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=9B=BE=E4=BE=8B=E6=BF=80=E6=B4=BB=E5=85=83=E7=B4=A0=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=20&=20=E5=A2=9E=E5=BC=BAactive=E3=80=81highlight?= =?UTF-8?q?=E3=80=81selected=E4=BA=A4=E4=BA=92=EF=BC=89=20(#2911)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/bugs/issue-2908-spec.ts | 51 ++++++++ __tests__/unit/plots/venn/interaction-spec.ts | 119 +++++++++++++++--- docs/api/plots/venn.en.md | 12 +- docs/api/plots/venn.zh.md | 25 ++-- .../more-plots/venn/demo/element-active.ts | 103 +++++++++++++++ .../more-plots/venn/demo/element-highlight.ts | 103 +++++++++++++++ examples/more-plots/venn/demo/interaction.ts | 15 --- .../more-plots/venn/demo/legend-active.ts | 27 ++++ .../more-plots/venn/demo/legend-highlight.ts | 33 +++++ examples/more-plots/venn/demo/meta.json | 36 ++++++ package.json | 2 +- src/core/plot.ts | 4 +- src/plots/venn/adaptor.ts | 44 +++++-- src/plots/venn/constant.ts | 28 ++++- src/plots/venn/interaction/action.ts | 67 ---------- src/plots/venn/interaction/index.ts | 17 --- src/plots/venn/interactions/actions/active.ts | 31 +++++ .../venn/interactions/actions/highlight.ts | 37 ++++++ .../venn/interactions/actions/selected.ts | 65 ++++++++++ src/plots/venn/interactions/index.ts | 55 ++++++++ src/plots/venn/interactions/util.ts | 12 ++ src/plots/venn/utils.ts | 14 ++- src/types/attr.ts | 2 +- src/types/common.ts | 2 + 24 files changed, 758 insertions(+), 146 deletions(-) create mode 100644 __tests__/bugs/issue-2908-spec.ts create mode 100644 examples/more-plots/venn/demo/element-active.ts create mode 100644 examples/more-plots/venn/demo/element-highlight.ts create mode 100644 examples/more-plots/venn/demo/legend-active.ts create mode 100644 examples/more-plots/venn/demo/legend-highlight.ts delete mode 100644 src/plots/venn/interaction/action.ts delete mode 100644 src/plots/venn/interaction/index.ts create mode 100644 src/plots/venn/interactions/actions/active.ts create mode 100644 src/plots/venn/interactions/actions/highlight.ts create mode 100644 src/plots/venn/interactions/actions/selected.ts create mode 100644 src/plots/venn/interactions/index.ts create mode 100644 src/plots/venn/interactions/util.ts diff --git a/__tests__/bugs/issue-2908-spec.ts b/__tests__/bugs/issue-2908-spec.ts new file mode 100644 index 0000000000..7b315c5aad --- /dev/null +++ b/__tests__/bugs/issue-2908-spec.ts @@ -0,0 +1,51 @@ +import { Venn } from '../../src'; +import { createDiv } from '../utils/dom'; + +describe('#2908, venn', () => { + const plot = new Venn(createDiv(), { + width: 400, + height: 500, + setsField: 'sets', + sizeField: 'size', + data: [ + { sets: ['A'], size: 10, label: 'A' }, + { sets: ['B'], size: 10, label: 'B' }, + ], + interactions: [{ type: 'legend-active', enable: true }], + }); + plot.render(); + + it('legend interaction', () => { + let labels = plot.chart.geometries[0].elements[0].shape + .getParent() + .getChildren() + .map((c) => c.get('origin').data.label); + + expect(labels[0]).toBe('A'); + expect(labels[1]).toBe('B'); + + const legendComponent = plot.chart.getController('legend').getComponents()[0]; + const legendContainer = legendComponent.component.get('container'); + + const legendTarget = legendContainer.findById('-legend-item-A'); + const box = legendTarget.getBBox(); + plot.chart.emit('legend-item:mouseenter', { + x: (box.x + box.maxX) / 2, + y: (box.y + box.maxY) / 2, + target: legendTarget, + }); + + // 图例交互,还是保持原序 + labels = plot.chart.geometries[0].elements[0].shape + .getParent() + .getChildren() + .map((c) => c.get('origin').data.label); + + expect(labels[0]).toBe('A'); + expect(labels[1]).toBe('B'); + }); + + afterAll(() => { + plot.destroy(); + }); +}); diff --git a/__tests__/unit/plots/venn/interaction-spec.ts b/__tests__/unit/plots/venn/interaction-spec.ts index 7560fc4a97..073485a7ae 100644 --- a/__tests__/unit/plots/venn/interaction-spec.ts +++ b/__tests__/unit/plots/venn/interaction-spec.ts @@ -1,7 +1,12 @@ import { IGroup } from '@antv/g-base'; import InteractionContext from '@antv/g2/lib/interaction/context'; import { Venn } from '../../../../src'; -import { VennElementActive, VennElementSelected } from '../../../../src/plots/venn/interaction/action'; +import { VennElementActive } from '../../../../src/plots/venn/interactions/actions/active'; +import { VennElementHighlight } from '../../../../src/plots/venn/interactions/actions/highlight'; +import { + VennElementSelected, + VennElementSingleSelected, +} from '../../../../src/plots/venn/interactions/actions/selected'; import { createDiv } from '../../../utils/dom'; describe('venn', () => { @@ -38,13 +43,12 @@ describe('venn', () => { }); const context = new InteractionContext(plot.chart); + // @ts-ignore const vennElementActive = new VennElementActive(context); // 模拟 active context.event = { - data: { - data: plot.chart.getData()[0], - }, + target: plot.chart.getElements()[0].shape, }; vennElementActive.active(); @@ -60,9 +64,7 @@ describe('venn', () => { // 模拟 第二次 active context.event = { - data: { - data: plot.chart.getData()[1], - }, + target: plot.chart.getElements()[1].shape, }; vennElementActive.active(); @@ -82,7 +84,7 @@ describe('venn', () => { vennElementActive.destroy(); }); - it('venn: selected', () => { + it('venn-element-selected', () => { plot.update({ state: { selected: { @@ -98,13 +100,12 @@ describe('venn', () => { }); const context = new InteractionContext(plot.chart); + // @ts-ignore const vennElementSelected = new VennElementSelected(context); // 模拟 selected context.event = { - data: { - data: plot.chart.getData()[0], - }, + target: plot.chart.getElements()[0].shape, }; vennElementSelected.toggle(); @@ -124,9 +125,7 @@ describe('venn', () => { // 模拟第二个元素的 selected context.event = { - data: { - data: plot.chart.getData()[1], - }, + target: plot.chart.getElements()[1].shape, }; vennElementSelected.toggle(); @@ -145,6 +144,98 @@ describe('venn', () => { vennElementSelected.destroy(); }); + it('venn-element-single-selected', () => { + plot.update({ + state: { + selected: { + style: { + lineWidth: 2, + }, + }, + }, + interactions: [ + { type: 'venn-element-selected', enable: false }, + { type: 'venn-element-single-selected', enable: true }, + ], + }); + + const context = new InteractionContext(plot.chart); + // @ts-ignore + const vennElementSelected = new VennElementSingleSelected(context); + + // 模拟 selected + context.event = { + target: plot.chart.getElements()[0].shape, + }; + vennElementSelected.selected(); + + const elements = plot.chart.geometries[0].elements; + + // 第一个元素 点击 有样式 + expect(plot.getStates().length).toBe(1); + expect(plot.getStates()[0].state).toBe('selected'); + expect(elements[0].getStates()[0]).toBe('selected'); + expect((elements[0].shape as IGroup).getChildren()[0].attr('lineWidth')).toBe(2); + + // 模拟 selected + context.event = { + target: plot.chart.getElements()[1].shape, + }; + vennElementSelected.selected(); + expect(plot.getStates().length).toBe(1); + expect(elements[0].getStates()[0]).toBeUndefined(); + expect((elements[0].shape as IGroup).getChildren()[0].attr('lineWidth')).toBe(0); + expect(elements[1].getStates()[0]).toBe('selected'); + + // 所有元素的 selected state 为 false + vennElementSelected.reset(); + vennElementSelected.destroy(); + }); + + it('venn-element-highlight', () => { + plot.update({ + state: { + inactive: { + style: { + fillOpacity: 0.3, + }, + }, + }, + interactions: [ + { type: 'venn-element-single-selected', enable: false }, + { type: 'venn-element-active', enable: false }, + { type: 'venn-element-highlight', enable: true }, + ], + }); + + const context = new InteractionContext(plot.chart); + // @ts-ignore + const action = new VennElementHighlight(context); + + // 模拟 selected + context.event = { + target: plot.chart.getElements()[0].shape, + }; + action.highlight(); + const elements = plot.chart.geometries[0].elements; + // 第一个元素 点击 有样式 + expect(elements[0].getStates()[0]).toBe('active'); + expect((elements[0].shape as IGroup).getChildren()[0].attr('fillOpacity')).not.toBe(0.3); + expect(elements[1].getStates()[0]).toBe('inactive'); + expect((elements[1].shape as IGroup).getChildren()[0].attr('fillOpacity')).toBe(0.3); + action.toggle(); + + context.event = { + target: plot.chart.getElements()[1].shape, + }; + action.toggle(); + expect(elements[0].getStates().includes('inactive')).toBe(true); + expect((elements[0].shape as IGroup).getChildren()[0].attr('fillOpacity')).toBe(0.3); + expect((elements[1].shape as IGroup).getChildren()[0].attr('fillOpacity')).not.toBe(0.3); + + action.destroy(); + }); + afterAll(() => { plot.destroy(); }); diff --git a/docs/api/plots/venn.en.md b/docs/api/plots/venn.en.md index 2e5c1d718a..99cd7b1ce5 100644 --- a/docs/api/plots/venn.en.md +++ b/docs/api/plots/venn.en.md @@ -24,6 +24,10 @@ Configure the chart data source. For example: ]; ``` +```sign +💡 注意:这里的数据是包含交集部分的数据量的。如上数据源,含有两个集合:`A` 和 `B`, 其中:`{ sets: ['A'], size: 5 }` 代表的是含有 A 集合的有 5 个(其实有 2 个是包含 B 集合的) +``` + #### setsField **optional** _string_ @@ -135,14 +139,16 @@ Default configuration: `markdown:docs/common/tooltip.en.md` -### Plot Interactions +### Plot Interactions ✨ There are interactions for venn diagrams, listed below: | interaction | description | configuration method | | ---|--|--| -| venn-element-active | enable the "mouse-over venn diagram element triggers active" interaction | `interactions:[{ type: 'venn-element-active', enabled: true }]` | -| venn-element-selected | enable the interaction "trigger selected when mouse clicked on venn diagram element", multiple options available | `interactions:[{ type: 'venn-element-selected', enabled: true }]` | +| venn-element-active | enable the "mouse-over venn diagram element triggers active" interaction | `interactions:[{ type: 'venn-element-active'}]` | +| venn-element-selected | enable the interaction "trigger selected when mouse clicked on venn diagram element", multiple options available | `interactions:[{ type: 'venn-element-selected'}]` | +| venn-element-single-selected | enable the interaction "trigger selected when mouse clicked on venn diagram element", single selected | `interactions:[{ type: 'venn-element-single-selected'}]` | +| venn-element-highlight | enable the interaction "trigger highlight when mouse clicked on venn diagram element" | `interactions:[{ type: 'venn-element-highlight'}]` | `markdown:docs/common/interactions.en.md` diff --git a/docs/api/plots/venn.zh.md b/docs/api/plots/venn.zh.md index efdf06b673..5e1cd5177b 100644 --- a/docs/api/plots/venn.zh.md +++ b/docs/api/plots/venn.zh.md @@ -13,15 +13,18 @@ order: 12 **required** _object_ -设置图表数据源。数据源为对象集合,例如: +设置图表数据源。数据源为对象集合. 例如: ```ts - const data = [ - { sets: ['A'], size: 5 }, - { sets: ['B'], size: 10 }, - { sets: ['A', 'B'], size: 2 }, - ... - ]; +const data = [ + { sets: ['A'], size: 5 }, + { sets: ['B'], size: 10 }, + { sets: ['A', 'B'], size: 2 }, +]; +``` + +```sign +💡 注意:这里的数据是包含交集部分的数据量的。如上数据源,含有两个集合:`A` 和 `B`, 其中:`{ sets: ['A'], size: 5 }` 代表的是含有 A 集合的有 5 个(其实有 2 个是包含 B 集合的) ``` #### setsField @@ -133,14 +136,16 @@ order: 12 `markdown:docs/common/tooltip.zh.md` -### 图表交互 +### 图表交互 ✨ 内置了针对 venn 图交互,列表如下: | 交互 | 描述 | 配置方式 | | ---|---|---| -| venn-element-active | 开启「鼠标移入 venn 图元素时触发 active」的交互 | `interactions:[{ type: 'venn-element-active', enabled: true }]` | -| venn-element-selected | 开启「鼠标点击 venn 图元素时触发 selected」的交互,可多选 | `interactions:[{ type: 'venn-element-selected', enabled: true }]` | +| venn-element-active | 开启「鼠标移入 venn 图元素时触发 active」的交互 | `interactions:[{ type: 'venn-element-active' }]` | +| venn-element-selected | 开启「鼠标点击 venn 图元素时触发 selected」的交互,可多选 | `interactions:[{ type: 'venn-element-selected' }]` | +| venn-element-single-selected | 开启「鼠标点击 venn 图元素时触发 selected」的交互,单选 | `interactions:[{ type: 'venn-element-single-selected' }]` | +| venn-element-highlight | 开启「鼠标点击 venn 图元素时触发 高亮」的交互 | `interactions:[{ type: 'venn-element-highlight' }]` | `markdown:docs/common/interactions.zh.md` diff --git a/examples/more-plots/venn/demo/element-active.ts b/examples/more-plots/venn/demo/element-active.ts new file mode 100644 index 0000000000..301ee36cf9 --- /dev/null +++ b/examples/more-plots/venn/demo/element-active.ts @@ -0,0 +1,103 @@ +import { Venn, G2 } from '@antv/g2plot'; +import { isEqual } from '@antv/util'; + +const { getActionClass, registerAction } = G2; +const ElementActiveAction = getActionClass('venn-element-active') as any; + +/** 用一个变量来存储 label 的初始化 visible 状态 */ +const VISIBLE_STATUS = 'visible-status'; + +// toggle labels 的 visible 状态 +function toggleLabels(view, stateName) { + const activeElements = view.geometries[0].elements.filter((ele) => ele.getStates().includes(stateName)); + const activeDatas = activeElements.map((ele) => ele.getData()); + const labels = view.geometries[0].labelsContainer.getChildren(); + labels.forEach((label) => { + label.set(VISIBLE_STATUS, label.get('visible')); + if (!activeDatas.find((d) => isEqual(d, label.get('origin').data))) { + label.hide(); + } else { + label.show(); + } + }); +} + +// 重置 labels 的 visible 状态 +function resetLabels(view) { + const labels = view.geometries[0].labelsContainer.getChildren(); + labels.forEach((label) => { + const visible = label.get(VISIBLE_STATUS) !== undefined ? label.get(VISIBLE_STATUS) : true; + label.set('visible', visible); + label.set(VISIBLE_STATUS, undefined); + }); +} + +/** + * @override 自定义韦恩图 · 图形元素激活交互 + */ +class VennElementActive extends ElementActiveAction { + /** 激活图形元素 */ + active() { + super.active(); + this.toggleLabels(); + } + + /** toggle 图形元素激活状态 */ + toggle() { + super.toggle(); + this.toggleLabels(); + } + + /** 重置 */ + reset() { + super.reset(); + this.resetLabels(); + } + + /** + * toggle labels 的 visible 状态 + */ + toggleLabels() { + toggleLabels(this.context.view, this.stateName); + } + + /** + * 重置 labels 的 visible 状态 + */ + resetLabels() { + resetLabels(this.context.view); + } +} +// 自定义注册韦恩图 · 图形元素激活交互 +registerAction('venn-element-active', VennElementActive as any); + +const data = [ + { sets: ['A'], size: 12, label: 'A' }, + { sets: ['B'], size: 12, label: 'B' }, + { sets: ['C'], size: 12, label: 'C' }, + { sets: ['A', 'B'], size: 2, label: 'A&B' }, + { sets: ['A', 'C'], size: 2, label: 'A&C' }, + { sets: ['B', 'C'], size: 2, label: 'B&C' }, + { sets: ['A', 'B', 'C'], size: 1, label: 'A&B&C' }, +]; + +const plot = new Venn('container', { + data, + setsField: 'sets', + sizeField: 'size', + pointStyle: { fillOpacity: 0.8 }, + label: { + formatter: (datum) => { + let size = datum.size; + data.forEach((d) => { + if (d.label !== datum.label) { + const contains = datum.sets.reduce((a, b) => a && d.sets.includes(b), true); + size -= contains ? d.size : 0; + } + }); + return `${datum.label} ${size}`; + }, + }, + interactions: [{ type: 'venn-element-active' }], +}); +plot.render(); diff --git a/examples/more-plots/venn/demo/element-highlight.ts b/examples/more-plots/venn/demo/element-highlight.ts new file mode 100644 index 0000000000..4c57b9747a --- /dev/null +++ b/examples/more-plots/venn/demo/element-highlight.ts @@ -0,0 +1,103 @@ +import { Venn, G2 } from '@antv/g2plot'; +import { isEqual } from '@antv/util'; + +const { getActionClass, registerAction } = G2; +const ElementHighlightAction: any = getActionClass('venn-element-highlight'); + +/** 用一个变量来存储 label 的初始化 visible 状态 */ +const VISIBLE_STATUS = 'visible-status'; + +// toggle labels 的 visible 状态 +function toggleLabels(view, stateName) { + const activeElements = view.geometries[0].elements.filter((ele) => ele.getStates().includes(stateName)); + const activeDatas = activeElements.map((ele) => ele.getData()); + const labels = view.geometries[0].labelsContainer.getChildren(); + labels.forEach((label) => { + label.set(VISIBLE_STATUS, label.get('visible')); + if (!activeDatas.find((d) => isEqual(d, label.get('origin').data))) { + label.hide(); + } else { + label.show(); + } + }); +} + +// 重置 labels 的 visible 状态 +function resetLabels(view) { + const labels = view.geometries[0].labelsContainer.getChildren(); + labels.forEach((label) => { + const visible = label.get(VISIBLE_STATUS) !== undefined ? label.get(VISIBLE_STATUS) : true; + label.set('visible', visible); + label.set(VISIBLE_STATUS, undefined); + }); +} + +/** + * @override 自定义韦恩图 · 图形元素高亮交互 + */ +class VennElementHighlight extends ElementHighlightAction { + /** 激活图形元素 */ + highlight() { + super.highlight(); + this.toggleLabels(); + } + + /** toggle 图形元素激活状态 */ + toggle() { + super.toggle(); + this.toggleLabels(); + } + + /** 重置 */ + reset() { + super.reset(); + this.resetLabels(); + } + + /** + * toggle labels 的 visible 状态 + */ + toggleLabels() { + toggleLabels(this.context.view, this.stateName); + } + + /** + * 重置 labels 的 visible 状态 + */ + resetLabels() { + resetLabels(this.context.view); + } +} + +// 自定义注册韦恩图 · 图形元素高亮交互 +registerAction('venn-element-highlight', VennElementHighlight as any); + +const data = [ + { sets: ['A'], size: 12, label: 'A' }, + { sets: ['B'], size: 12, label: 'B' }, + { sets: ['C'], size: 12, label: 'C' }, + { sets: ['A', 'B'], size: 2, label: 'A&B' }, + { sets: ['A', 'C'], size: 2, label: 'A&C' }, + { sets: ['B', 'C'], size: 2, label: 'B&C' }, + { sets: ['A', 'B', 'C'], size: 1 }, +]; +const plot = new Venn('container', { + data, + setsField: 'sets', + sizeField: 'size', + pointStyle: { fillOpacity: 0.8 }, + label: { + formatter: (datum) => { + let size = datum.size; + data.forEach((d) => { + if (d.label !== datum.label) { + const contains = datum.sets.reduce((a, b) => a && d.sets.includes(b), true); + size -= contains ? d.size : 0; + } + }); + return `${datum.label} ${size}`; + }, + }, + interactions: [{ type: 'venn-element-highlight' }], +}); +plot.render(); diff --git a/examples/more-plots/venn/demo/interaction.ts b/examples/more-plots/venn/demo/interaction.ts index eda85ddb92..2177a7c840 100644 --- a/examples/more-plots/venn/demo/interaction.ts +++ b/examples/more-plots/venn/demo/interaction.ts @@ -14,21 +14,6 @@ const plot = new Venn('container', { sizeField: 'size', pointStyle: { fillOpacity: 0.8 }, padding: [0, 10], - state: { - active: { - style: { - fillOpacity: 1, - stroke: 'black', - lineWidth: 1, - }, - }, - selected: { - style: { - stroke: 'black', - lineWidth: 2, - }, - }, - }, interactions: [ { type: 'venn-element-active', enable: true }, { type: 'venn-element-selected', enable: true }, diff --git a/examples/more-plots/venn/demo/legend-active.ts b/examples/more-plots/venn/demo/legend-active.ts new file mode 100644 index 0000000000..cb68421756 --- /dev/null +++ b/examples/more-plots/venn/demo/legend-active.ts @@ -0,0 +1,27 @@ +import { Venn } from '@antv/g2plot'; + +const plot = new Venn('container', { + data: [ + { sets: ['A'], size: 12, label: 'A' }, + { sets: ['B'], size: 12, label: 'B' }, + { sets: ['C'], size: 12, label: 'C' }, + { sets: ['A', 'B'], size: 2, label: 'A&B' }, + { sets: ['A', 'C'], size: 2, label: 'A&C' }, + { sets: ['B', 'C'], size: 2, label: 'B&C' }, + { sets: ['A', 'B', 'C'], size: 1 }, + ], + setsField: 'sets', + sizeField: 'size', + pointStyle: { fillOpacity: 0.8 }, + interactions: [ + { + type: 'legend-active', + cfg: { + // 自定义图例激活交互的触发行为和反馈 + start: [{ trigger: 'legend-item:click', action: ['list-active:active', 'venn-element-active:active'] }], + end: [{ trigger: 'legend-item:dblclick', action: ['list-active:reset', 'venn-element-active:reset'] }], + }, + }, + ], +}); +plot.render(); diff --git a/examples/more-plots/venn/demo/legend-highlight.ts b/examples/more-plots/venn/demo/legend-highlight.ts new file mode 100644 index 0000000000..b0d2b3fe47 --- /dev/null +++ b/examples/more-plots/venn/demo/legend-highlight.ts @@ -0,0 +1,33 @@ +import { Venn } from '@antv/g2plot'; + +const plot = new Venn('container', { + data: [ + { sets: ['A'], size: 12, label: 'A' }, + { sets: ['B'], size: 12, label: 'B' }, + { sets: ['C'], size: 12, label: 'C' }, + { sets: ['A', 'B'], size: 2, label: 'A&B' }, + { sets: ['A', 'C'], size: 2, label: 'A&C' }, + { sets: ['B', 'C'], size: 2, label: 'B&C' }, + { sets: ['A', 'B', 'C'], size: 1 }, + ], + setsField: 'sets', + sizeField: 'size', + interactions: [ + { + type: 'legend-highlight', + cfg: { + // 自定义图例高亮交互的触发行为 + start: [ + { + trigger: 'legend-item:click', + action: ['legend-item-highlight:highlight', 'venn-element-highlight:highlight'], + }, + ], + end: [ + { trigger: 'legend-item:dblclick', action: ['legend-item-highlight:reset', 'venn-element-highlight:reset'] }, + ], + }, + }, + ], +}); +plot.render(); diff --git a/examples/more-plots/venn/demo/meta.json b/examples/more-plots/venn/demo/meta.json index 774347f9b4..0f6d9091ae 100644 --- a/examples/more-plots/venn/demo/meta.json +++ b/examples/more-plots/venn/demo/meta.json @@ -59,6 +59,42 @@ "en": "venn plot - with element action" }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/x%24Jxds2L2T/0ff5000b-d23e-49a8-a02a-4eb02d7a8e47.png" + }, + { + "filename": "element-active.ts", + "title": { + "zh": "自定义韦恩图元素激活交互", + "en": "Custom venn element active" + }, + "new": true, + "screenshot": "" + }, + { + "filename": "element-highlight.ts", + "title": { + "zh": "自定义韦恩图元素高亮交互", + "en": "Custom venn element highlight" + }, + "new": true, + "screenshot": "" + }, + { + "filename": "legend-active.ts", + "title": { + "zh": "自定义韦恩图图例激活交互", + "en": "Custom venn legend active" + }, + "new": true, + "screenshot": "" + }, + { + "filename": "legend-highlight.ts", + "title": { + "zh": "自定义韦恩图图例高亮交互", + "en": "Custom venn legend highlight" + }, + "new": true, + "screenshot": "" } ] } diff --git a/package.json b/package.json index 550bbaa451..3a18687b6a 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "limit-size": [ { "path": "dist/g2plot.min.js", - "limit": "990 Kb" + "limit": "995 Kb" }, { "path": "dist/g2plot.min.js", diff --git a/src/core/plot.ts b/src/core/plot.ts index 301518bffb..f2acc6a34c 100644 --- a/src/core/plot.ts +++ b/src/core/plot.ts @@ -20,6 +20,7 @@ export type PickOptions = Pick< | 'supportCSSTransform' | 'limitInPlot' | 'locale' + | 'defaultInteractions' >; const SOURCE_ATTRIBUTE_NAME = 'data-chart-source-type'; @@ -90,7 +91,7 @@ export abstract class Plot extends EE { * 创建 G2 实例 */ private createG2() { - const { width, height } = this.options; + const { width, height, defaultInteractions } = this.options; this.chart = new Chart({ container: this.container, @@ -98,6 +99,7 @@ export abstract class Plot extends EE { ...this.getChartSize(width, height), localRefresh: false, // 默认关闭,目前 G 还有一些位置问题,难以排查! ...pick(this.options, PLOT_CONTAINER_OPTIONS), + defaultInteractions, }); // 给容器增加标识,知道图表的来源区别于 G2 diff --git a/src/plots/venn/adaptor.ts b/src/plots/venn/adaptor.ts index ecc55ccf05..3d68e4e94b 100644 --- a/src/plots/venn/adaptor.ts +++ b/src/plots/venn/adaptor.ts @@ -19,7 +19,7 @@ import { CustomInfo, VennData, VennOptions } from './types'; import { ID_FIELD } from './constant'; import './shape'; import './label'; -import './interaction'; +import './interactions'; /** 图例默认预留空间 */ export const LEGEND_SPACE = 40; @@ -29,15 +29,15 @@ export const LEGEND_SPACE = 40; */ function colorMap(params: Params, data: VennData, colorPalette?: string[]) { const { chart, options } = params; - const { setsField } = options; + const { blendMode, setsField } = options; const { colors10, colors20 } = chart.getTheme(); let palette = colorPalette; if (!isArray(palette)) { palette = data.filter((d) => d[setsField].length === 1).length <= 10 ? colors10 : colors20; } - const colorMap = getColorMap(palette, data, options); + const map = getColorMap(palette, data, blendMode, setsField); - return (id: string) => colorMap.get(id) || palette[0]; + return (id: string) => map.get(id) || palette[0]; } /** @@ -49,7 +49,8 @@ function transformColor(params: Params, data: VennData): VennOption if (typeof color !== 'function') { const colorPalette = typeof color === 'string' ? [color] : color; - return (datum: Datum) => colorMap(params, data, colorPalette)(datum[ID_FIELD]); + const map = colorMap(params, data, colorPalette); + return (datum: Datum) => map(datum[ID_FIELD]); } return color; } @@ -173,7 +174,7 @@ function label(params: Params): Params { const { label } = options; // 获取容器大小 - const [t, r, b, l] = normalPadding(chart.appendPadding); + const [t, , , l] = normalPadding(chart.appendPadding); // 传入 label 布局函数所需的 自定义参数 const customLabelInfo = { offsetX: l, offsetY: t }; @@ -223,6 +224,35 @@ export function axis(params: Params): Params { return params; } +/** + * 韦恩图 interaction 交互适配器 + */ +function vennInteraction(params: Params): Params { + const { options, chart } = params; + const { interactions } = options; + + if (interactions) { + const MAP = { + 'legend-active': 'venn-legend-active', + 'legend-highlight': 'venn-legend-highlight', + }; + interaction( + deepAssign({}, params, { + options: { + interactions: interactions.map((i) => ({ + ...i, + type: MAP[i.type] || i.type, + })), + }, + }) + ); + } + + chart.removeInteraction('legend-active'); + chart.removeInteraction('legend-highlight'); + return params; +} + /** * 图适配器 * @param chart @@ -240,7 +270,7 @@ export function adaptor(params: Params) { legend, axis, tooltip, - interaction, + vennInteraction, animation // ... 其他的 adaptor flow )(params); diff --git a/src/plots/venn/constant.ts b/src/plots/venn/constant.ts index 2a29dcf4f3..470515f902 100644 --- a/src/plots/venn/constant.ts +++ b/src/plots/venn/constant.ts @@ -26,10 +26,26 @@ export const DEFAULT_OPTIONS: Partial = { }, }, // 默认不开启 图例筛选交互 - interactions: [ - { type: 'legend-filter', enable: false }, - // hover 激活的时候,元素的层级展示不太好 先移除该交互 - { type: 'legend-highlight', enable: false }, - { type: 'legend-active', enable: false }, - ], + interactions: [{ type: 'legend-filter', enable: false }], + state: { + active: { + style: { + stroke: '#000', + }, + }, + selected: { + style: { + stroke: '#000', + lineWidth: 2, + }, + }, + inactive: { + style: { + fillOpacity: 0.3, + strokeOpacity: 0.3, + }, + }, + }, + // 韦恩图的默认内置注册的交互 + defaultInteractions: ['tooltip', 'venn-legend-active'], }; diff --git a/src/plots/venn/interaction/action.ts b/src/plots/venn/interaction/action.ts deleted file mode 100644 index ba6c95f5f4..0000000000 --- a/src/plots/venn/interaction/action.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { InteractionAction } from '@antv/g2'; - -class VennElementState extends InteractionAction { - /** tofront: 同步所有元素的位置 */ - protected syncElementsPos() { - const elements = this.context.view.geometries[0].elements; - elements.forEach((elem) => { - elem.shape.toFront(); - }); - } -} - -export class VennElementActive extends VennElementState { - /** hover */ - public active() { - const { data } = this.context.event.data; - const elements = this.context.view.geometries[0].elements; - - elements.forEach((elem) => { - const activeState = data === elem.getData(); - elem.setState('active', activeState); - }); - // tofront: 同步所有元素的位置 - this.syncElementsPos(); - } - - /** 重置 */ - public reset() { - const elements = this.context.view.geometries[0].elements; - - elements.forEach((elem) => { - // 所有元素的 state 统一 false - elem.setState('active', false); - }); - // tofront: 同步所有元素的位置 - this.syncElementsPos(); - } -} - -export class VennElementSelected extends VennElementState { - /** 切换 */ - public toggle() { - const { data } = this.context.event.data; - const elements = this.context.view.geometries[0].elements; - - elements.forEach((elem) => { - if (data === elem.getData()) { - const selectedState = elem.getStates().includes('selected'); - elem.setState('selected', !selectedState); - } - }); - // tofront: 同步所有元素的位置 - this.syncElementsPos(); - } - - /** 重置 */ - public reset() { - const elements = this.context.view.geometries[0].elements; - - elements.forEach((elem) => { - // 所有元素的 state 统一 false - elem.setState('selected', false); - }); - // tofront: 同步所有元素的位置 - this.syncElementsPos(); - } -} diff --git a/src/plots/venn/interaction/index.ts b/src/plots/venn/interaction/index.ts deleted file mode 100644 index 50669daaf9..0000000000 --- a/src/plots/venn/interaction/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { registerInteraction, registerAction } from '@antv/g2'; -import { VennElementActive, VennElementSelected } from './action'; - -registerAction('venn-element-active', VennElementActive); -registerAction('venn-element-selected', VennElementSelected); - -// 移动到 venn elment 上的 active -registerInteraction('venn-element-active', { - start: [{ trigger: 'element:mouseenter', action: 'venn-element-active:active' }], - end: [{ trigger: 'element:mouseleave', action: 'venn-element-active:reset' }], -}); - -// 点击 venn element (可多选) -registerInteraction('venn-element-selected', { - start: [{ trigger: 'element:click', action: 'venn-element-selected:toggle' }], - rollback: [{ trigger: 'dblclick', action: ['venn-element-selected:reset'] }], -}); diff --git a/src/plots/venn/interactions/actions/active.ts b/src/plots/venn/interactions/actions/active.ts new file mode 100644 index 0000000000..decb845b32 --- /dev/null +++ b/src/plots/venn/interactions/actions/active.ts @@ -0,0 +1,31 @@ +import { getActionClass } from '@antv/g2'; +import { placeElementsOrdered } from '../util'; + +const ElementActiveAction: any = getActionClass('element-active'); + +export class VennElementActive extends ElementActiveAction { + /** + * 同步所有元素的位置 + */ + protected syncElementsPos() { + placeElementsOrdered(this.context.view); + } + + /** 激活图形元素 */ + public active() { + super.active(); + this.syncElementsPos(); + } + + /** toggle 图形元素激活状态 */ + public toggle() { + super.toggle(); + this.syncElementsPos(); + } + + /** 重置 */ + public reset() { + super.reset(); + this.syncElementsPos(); + } +} diff --git a/src/plots/venn/interactions/actions/highlight.ts b/src/plots/venn/interactions/actions/highlight.ts new file mode 100644 index 0000000000..527b753334 --- /dev/null +++ b/src/plots/venn/interactions/actions/highlight.ts @@ -0,0 +1,37 @@ +import { getActionClass } from '@antv/g2'; +import { placeElementsOrdered } from '../util'; + +const ElementHighlightAction: any = getActionClass('element-highlight'); + +export class VennElementHighlight extends ElementHighlightAction { + /** + * 同步所有元素的位置 + */ + protected syncElementsPos() { + placeElementsOrdered(this.context.view); + } + + /** 高亮图形元素 */ + public highlight() { + super.highlight(); + this.syncElementsPos(); + } + + /** toggle 图形元素高亮状态 */ + public toggle() { + super.toggle(); + this.syncElementsPos(); + } + + /** 清楚 */ + public clear() { + super.clear(); + this.syncElementsPos(); + } + + /** 重置 */ + public reset() { + super.reset(); + this.syncElementsPos(); + } +} diff --git a/src/plots/venn/interactions/actions/selected.ts b/src/plots/venn/interactions/actions/selected.ts new file mode 100644 index 0000000000..61c1fe6f0c --- /dev/null +++ b/src/plots/venn/interactions/actions/selected.ts @@ -0,0 +1,65 @@ +import { getActionClass } from '@antv/g2'; +import { placeElementsOrdered } from '../util'; + +const ElementSelectedAction: any = getActionClass('element-selected'); +const ElementSingleSelectedAction: any = getActionClass('element-single-selected'); + +/** + * 韦恩图元素 多选交互 + */ +export class VennElementSelected extends ElementSelectedAction { + /** + * 同步所有元素的位置 + */ + protected syncElementsPos() { + placeElementsOrdered(this.context.view); + } + + /** 激活图形元素 */ + public selected() { + super.selected(); + this.syncElementsPos(); + } + + /** toggle 图形元素激活状态 */ + public toggle() { + super.toggle(); + this.syncElementsPos(); + } + + /** 重置 */ + public reset() { + super.reset(); + this.syncElementsPos(); + } +} + +/** + * 韦恩图元素 单选交互 + */ +export class VennElementSingleSelected extends ElementSingleSelectedAction { + /** + * 同步所有元素的位置 + */ + protected syncElementsPos() { + placeElementsOrdered(this.context.view); + } + + /** 激活图形元素 */ + public selected() { + super.selected(); + this.syncElementsPos(); + } + + /** toggle 图形元素激活状态 */ + public toggle() { + super.toggle(); + this.syncElementsPos(); + } + + /** 重置 */ + public reset() { + super.reset(); + this.syncElementsPos(); + } +} diff --git a/src/plots/venn/interactions/index.ts b/src/plots/venn/interactions/index.ts new file mode 100644 index 0000000000..05e8cef55c --- /dev/null +++ b/src/plots/venn/interactions/index.ts @@ -0,0 +1,55 @@ +import { registerInteraction, registerAction } from '@antv/g2'; +import { VennElementActive } from './actions/active'; +import { VennElementHighlight } from './actions/highlight'; +import { VennElementSelected, VennElementSingleSelected } from './actions/selected'; + +/** ================== 注册交互反馈 aciton ================== */ + +registerAction('venn-element-active', VennElementActive as any); +registerAction('venn-element-highlight', VennElementHighlight as any); +registerAction('venn-element-selected', VennElementSelected as any); +registerAction('venn-element-single-selected', VennElementSingleSelected as any); + +/** ================== 注册交互 ================== */ + +// ========= Active 交互 ========= +registerInteraction('venn-element-active', { + start: [{ trigger: 'element:mouseenter', action: 'venn-element-active:active' }], + end: [{ trigger: 'element:mouseleave', action: 'venn-element-active:reset' }], +}); + +// ========= 高亮 交互 ========= +registerInteraction('venn-element-highlight', { + start: [{ trigger: 'element:mouseenter', action: 'venn-element-highlight:highlight' }], + end: [{ trigger: 'element:mouseleave', action: 'venn-element-highlight:reset' }], +}); + +// ========= Selected 交互 ========= +// 点击 venn element (可多选) +registerInteraction('venn-element-selected', { + start: [{ trigger: 'element:click', action: 'venn-element-selected:toggle' }], + rollback: [{ trigger: 'dblclick', action: ['venn-element-selected:reset'] }], +}); +// 点击 venn element (单选) +registerInteraction('venn-element-single-selected', { + start: [{ trigger: 'element:click', action: 'venn-element-single-selected:toggle' }], + rollback: [{ trigger: 'dblclick', action: ['venn-element-single-selected:reset'] }], +}); + +// ========= 韦恩图的图例事件,单独注册 ========= +// legend hover,element active +registerInteraction('venn-legend-active', { + start: [{ trigger: 'legend-item:mouseenter', action: ['list-active:active', 'venn-element-active:active'] }], + end: [{ trigger: 'legend-item:mouseleave', action: ['list-active:reset', 'venn-element-active:reset'] }], +}); + +// legend hover,element active +registerInteraction('venn-legend-highlight', { + start: [ + { + trigger: 'legend-item:mouseenter', + action: ['legend-item-highlight:highlight', 'venn-element-highlight:highlight'], + }, + ], + end: [{ trigger: 'legend-item:mouseleave', action: ['legend-item-highlight:reset', 'venn-element-highlight:reset'] }], +}); diff --git a/src/plots/venn/interactions/util.ts b/src/plots/venn/interactions/util.ts new file mode 100644 index 0000000000..d6299e36e1 --- /dev/null +++ b/src/plots/venn/interactions/util.ts @@ -0,0 +1,12 @@ +import { View } from '@antv/g2'; + +/** tofront: 同步所有元素的位置 */ +export function placeElementsOrdered(view: View) { + if (!view) { + return; + } + const elements = view.geometries[0].elements; + elements.forEach((elem) => { + elem.shape.toFront(); + }); +} diff --git a/src/plots/venn/utils.ts b/src/plots/venn/utils.ts index 66564d490b..4ac6aa9957 100644 --- a/src/plots/venn/utils.ts +++ b/src/plots/venn/utils.ts @@ -6,6 +6,13 @@ import { intersectionAreaPath, computeTextCentres } from './layout/diagram'; import { ID_FIELD, PATH_FIELD } from './constant'; import { VennData, VennOptions } from './types'; +type ColorMapFunction = ( + colorPalette: string[], + data: VennData, + blendMode: VennOptions['blendMode'], + setsField: VennOptions['setsField'] +) => Map; + /** * 获取 颜色映射 * @usage colorMap.get(id) => color @@ -13,8 +20,7 @@ import { VennData, VennOptions } from './types'; * @returns Map */ export const getColorMap = memoize( - (colorPalette: string[], data: VennData, options: VennOptions) => { - const { blendMode, setsField } = options; + ((colorPalette, data, blendMode, setsField) => { const colorMap = new Map(); const colorPaletteLen = colorPalette.length; data.forEach((d, idx) => { @@ -31,9 +37,9 @@ export const getColorMap = memoize( }); return colorMap; - }, + }) as ColorMapFunction, (...params) => JSON.stringify(params) -); +) as ColorMapFunction; /** * 给韦恩图数据进行布局 diff --git a/src/types/attr.ts b/src/types/attr.ts index ec98bb4be3..1818890334 100644 --- a/src/types/attr.ts +++ b/src/types/attr.ts @@ -6,7 +6,7 @@ import { Datum } from './common'; export type ShapeStyle = ShapeAttrs; /** 颜色映射 */ -export type ColorAttr = string | string[] | ((datum: Datum) => string) | object; +export type ColorAttr = string | string[] | ((datum: Datum, defaultColor?: string) => string); /** pattern 映射*/ export type PatternAttr = | CanvasPattern diff --git a/src/types/common.ts b/src/types/common.ts index 9361a979e7..01c91f5bab 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -132,4 +132,6 @@ export type Options = { readonly state?: State; /** 是否对超出坐标系范围的 Geometry 进行剪切 */ readonly limitInPlot?: boolean; + /** 内置注册的交互 */ + readonly defaultInteractions?: string[]; };