diff --git a/__tests__/unit/plots/bar/interaction-spec.ts b/__tests__/unit/plots/bar/interaction-spec.ts new file mode 100644 index 0000000000..0b1d745580 --- /dev/null +++ b/__tests__/unit/plots/bar/interaction-spec.ts @@ -0,0 +1,58 @@ +import { Bar } from '../../../../src'; +import { salesByArea } from '../../../data/sales'; +import { createDiv } from '../../../utils/dom'; + +describe('bar interaction', () => { + const plot = new Bar(createDiv('x*y'), { + width: 400, + height: 300, + data: salesByArea, + yField: 'area', + xField: 'sales', + }); + + plot.render(); + + it('brush', () => { + plot.update({ + brush: { + enabled: true, + }, + }); + expect(plot.chart.interactions['brush']).toBeDefined(); + + plot.update({ brush: { type: 'circle' } }); + expect(plot.chart.interactions['brush']).toBeDefined(); + + plot.update({ brush: { type: 'x-rect' } }); + // 不同 brush 是互斥的 + expect(plot.chart.interactions['brush']).not.toBeDefined(); + expect(plot.chart.interactions['brush-x']).toBeDefined(); + + plot.update({ brush: { type: 'path' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush']).toBeDefined(); + + plot.update({ brush: { type: 'y-rect' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush-y']).toBeDefined(); + + plot.update({ brush: { type: 'y-rect', action: 'highlight' } }); + expect(plot.chart.interactions['brush-y']).not.toBeDefined(); + expect(plot.chart.interactions['brush-y-highlight']).toBeDefined(); + + plot.update({ brush: { type: 'x-rect', action: 'highlight' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush-y-highlight']).not.toBeDefined(); + expect(plot.chart.interactions['brush-x-highlight']).toBeDefined(); + + plot.update({ brush: { type: 'rect', action: 'highlight' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush']).not.toBeDefined(); + expect(plot.chart.interactions['brush-highlight']).toBeDefined(); + }); + + afterAll(() => { + plot.destroy(); + }); +}); diff --git a/__tests__/unit/plots/column/interaction-spec.ts b/__tests__/unit/plots/column/interaction-spec.ts new file mode 100644 index 0000000000..54315aca48 --- /dev/null +++ b/__tests__/unit/plots/column/interaction-spec.ts @@ -0,0 +1,58 @@ +import { Column } from '../../../../src'; +import { salesByArea } from '../../../data/sales'; +import { createDiv } from '../../../utils/dom'; + +describe('column interaction', () => { + const plot = new Column(createDiv('x*y'), { + width: 400, + height: 300, + data: salesByArea, + xField: 'area', + yField: 'sales', + }); + + plot.render(); + + it('brush', () => { + plot.update({ + brush: { + enabled: true, + }, + }); + expect(plot.chart.interactions['brush']).toBeDefined(); + + plot.update({ brush: { type: 'circle' } }); + expect(plot.chart.interactions['brush']).toBeDefined(); + + plot.update({ brush: { type: 'x-rect' } }); + // 不同 brush 是互斥的 + expect(plot.chart.interactions['brush']).not.toBeDefined(); + expect(plot.chart.interactions['brush-x']).toBeDefined(); + + plot.update({ brush: { type: 'path' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush']).toBeDefined(); + + plot.update({ brush: { type: 'y-rect' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush-y']).toBeDefined(); + + plot.update({ brush: { type: 'y-rect', action: 'highlight' } }); + expect(plot.chart.interactions['brush-y']).not.toBeDefined(); + expect(plot.chart.interactions['brush-y-highlight']).toBeDefined(); + + plot.update({ brush: { type: 'x-rect', action: 'highlight' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush-y-highlight']).not.toBeDefined(); + expect(plot.chart.interactions['brush-x-highlight']).toBeDefined(); + + plot.update({ brush: { type: 'rect', action: 'highlight' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush']).not.toBeDefined(); + expect(plot.chart.interactions['brush-highlight']).toBeDefined(); + }); + + afterAll(() => { + plot.destroy(); + }); +}); diff --git a/__tests__/unit/plots/scatter/interaction-spec.ts b/__tests__/unit/plots/scatter/interaction-spec.ts index 140af843c8..b3241f7a6c 100644 --- a/__tests__/unit/plots/scatter/interaction-spec.ts +++ b/__tests__/unit/plots/scatter/interaction-spec.ts @@ -4,7 +4,7 @@ import { createDiv } from '../../../utils/dom'; import { data } from '../../../data/gender'; describe('scatter: register interaction', () => { - const scatter = new Scatter(createDiv(), { + const plot = new Scatter(createDiv(), { width: 400, height: 300, appendPadding: 10, @@ -24,14 +24,53 @@ describe('scatter: register interaction', () => { ], }); - scatter.render(); + plot.render(); it('define: drag-move', () => { const statisticInteraction = getInteraction('drag-move'); expect(statisticInteraction).toBeDefined(); }); + it('brush', () => { + plot.update({ + brush: { + enabled: true, + }, + }); + expect(plot.chart.interactions['brush']).toBeDefined(); + + plot.update({ brush: { type: 'circle' } }); + expect(plot.chart.interactions['brush']).toBeDefined(); + + plot.update({ brush: { type: 'x-rect' } }); + // 不同 brush 是互斥的 + expect(plot.chart.interactions['brush']).not.toBeDefined(); + expect(plot.chart.interactions['brush-x']).toBeDefined(); + + plot.update({ brush: { type: 'path' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush']).toBeDefined(); + + plot.update({ brush: { type: 'y-rect' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush-y']).toBeDefined(); + + plot.update({ brush: { type: 'y-rect', action: 'highlight' } }); + expect(plot.chart.interactions['brush-y']).not.toBeDefined(); + expect(plot.chart.interactions['brush-y-highlight']).toBeDefined(); + + plot.update({ brush: { type: 'x-rect', action: 'highlight' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush-y-highlight']).not.toBeDefined(); + expect(plot.chart.interactions['brush-x-highlight']).toBeDefined(); + + plot.update({ brush: { type: 'rect', action: 'highlight' } }); + expect(plot.chart.interactions['brush-x']).not.toBeDefined(); + expect(plot.chart.interactions['brush']).not.toBeDefined(); + expect(plot.chart.interactions['brush-highlight']).toBeDefined(); + }); + afterAll(() => { - scatter.destroy(); + plot.destroy(); }); }); diff --git a/docs/api/plots/bar.en.md b/docs/api/plots/bar.en.md index fb38e72c57..2837eecc43 100644 --- a/docs/api/plots/bar.en.md +++ b/docs/api/plots/bar.en.md @@ -117,4 +117,12 @@ Applicable to base bar charts and base bar charts, the Conversion Rate component ### Plot Interactions +Built-in interactions of scatter are as follows: + +| Interaction | Description | Configuration | +| ----------- | ---------------------------------------- | ------------------------------ | +| brush | 用于刷选交互,配置该交互后,可进行刷选。 | `brush: { enabled: true }` | + +`markdown:docs/common/brush.en.md` + `markdown:docs/common/interactions.en.md` diff --git a/docs/api/plots/bar.zh.md b/docs/api/plots/bar.zh.md index f4b825a072..dff6ffbfcd 100644 --- a/docs/api/plots/bar.zh.md +++ b/docs/api/plots/bar.zh.md @@ -117,3 +117,12 @@ order: 3 ### 图表交互 +条形图内置了一些交互,列表如下: + +| 交互 | 描述 | 配置 | +| ----------- | ---------------------------------------- | ------------------------------ | +| brush | 用于刷选交互,配置该交互后,可进行刷选。 | `brush: { enabled: true }` | + +`markdown:docs/common/brush.zh.md` + +`markdown:docs/common/interactions.zh.md` diff --git a/docs/api/plots/column.en.md b/docs/api/plots/column.en.md index 0a3c458e9b..392998816c 100644 --- a/docs/api/plots/column.en.md +++ b/docs/api/plots/column.en.md @@ -117,4 +117,12 @@ Applicable to base bar charts and base bar charts, the Conversion Rate component ### Plot Interactions +Built-in interactions of scatter are as follows: + +| Interaction | Description | Configuration | +| ----------- | ---------------------------------------- | ------------------------------ | +| brush | 用于刷选交互,配置该交互后,可进行刷选。 | `brush: { enabled: true }` | + +`markdown:docs/common/brush.en.md` + `markdown:docs/common/interactions.en.md` diff --git a/docs/api/plots/column.zh.md b/docs/api/plots/column.zh.md index 5ee12c0aaa..137e5a40c9 100644 --- a/docs/api/plots/column.zh.md +++ b/docs/api/plots/column.zh.md @@ -119,4 +119,12 @@ order: 2 ### 图表交互 +柱状图内置了一些交互,列表如下: + +| 交互 | 描述 | 配置 | +| ----------- | ---------------------------------------- | ------------------------------ | +| brush | 用于刷选交互,配置该交互后,可进行刷选。 | `brush: { enabled: true }` | + +`markdown:docs/common/brush.zh.md` + `markdown:docs/common/interactions.zh.md` diff --git a/docs/api/plots/scatter.en.md b/docs/api/plots/scatter.en.md index 55093b6801..6505f75706 100644 --- a/docs/api/plots/scatter.en.md +++ b/docs/api/plots/scatter.en.md @@ -300,4 +300,12 @@ regressionLine: { ### Plot Interactions +Built-in interactions of scatter are as follows: + +| Interaction | Description | Configuration | +| ----------- | ---------------------------------------- | ------------------------------ | +| brush | 用于刷选交互,配置该交互后,可进行刷选。 | `brush: { enabled: true }` | + +`markdown:docs/common/brush.en.md` + `markdown:docs/common/interactions.en.md` diff --git a/docs/api/plots/scatter.zh.md b/docs/api/plots/scatter.zh.md index 9a29d5c3c8..c046bb27e1 100644 --- a/docs/api/plots/scatter.zh.md +++ b/docs/api/plots/scatter.zh.md @@ -300,4 +300,12 @@ regressionLine: { ### 图表交互 +散点图内置了一些交互,列表如下: + +| 交互 | 描述 | 配置 | +| ----------- | ---------------------------------------- | ------------------------------ | +| brush | 用于刷选交互,配置该交互后,可进行刷选。 | `brush: { enabled: true }` | + +`markdown:docs/common/brush.zh.md` + `markdown:docs/common/interactions.zh.md` diff --git a/docs/api/plots/sunburst.zh.md b/docs/api/plots/sunburst.zh.md index 43921e076e..1ae52ece5e 100644 --- a/docs/api/plots/sunburst.zh.md +++ b/docs/api/plots/sunburst.zh.md @@ -43,7 +43,7 @@ type Node = { name: string; value?: number; children: Node[]; } |`Sunburst.SUNBURST_ANCESTOR_FIELD`| 当前节点的祖先节点 | _string_ | |`Sunburst.NODE_ANCESTORS_FIELD`| 当前节点的祖先节点列表 |_object[]_ | |`nodeIndex`| 当前节点在同一父节点下的所有节点中的索引顺序 |_number_ | -| `childNodeCount` | 当前节点的儿子节点数 |_number_ | +| `childNodeCount` | 当前节点的儿子节点数 |_number_ | |`depth`| |_number_ | |`height`| | _number_ | @@ -86,14 +86,14 @@ meta: { 支持配置属性: -| Properties | Type | Description | -| ---------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -| field | _string_ | 数据节点权重映射字段,默认为:`value`. 当你的节点数据格式不是:`{ name: 'xx', value: 'xx' }`, 可以通过该字段来指定,详细见:图表示例 | -| padding | _number\|number[]_ | 默认:`0`。参考:[d3-hierarchy#partition_padding](https://github.com/d3/d3-hierarchy#partition_padding) | -| size | _number[]_ | 默认:`[1, 1]`。参考:[d3-hierarchy#partition_size](https://github.com/d3/d3-hierarchy#partition_size) | -| round | _boolean_ | 默认:`false`。参考:[d3-hierarchy#partition_round](https://github.com/d3/d3-hierarchy#partition_round) | -| sort | _Function_ | 数据节点排序方式,默认:降序。参考: [d3-hierarchy#node_sort](https://github.com/d3/d3-hierarchy#node_sort) | -| ignoreParentValue | _boolean_ | 是否忽略 parentValue, 默认:true。 当设置为 true 时,父节点的权重由子元素决定 | +| Properties | Type | Description | +| ----------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| field | _string_ | 数据节点权重映射字段,默认为:`value`. 当你的节点数据格式不是:`{ name: 'xx', value: 'xx' }`, 可以通过该字段来指定,详细见:图表示例 | +| padding | _number\|number[]_ | 默认:`0`。参考:[d3-hierarchy#partition_padding](https://github.com/d3/d3-hierarchy#partition_padding) | +| size | _number[]_ | 默认:`[1, 1]`。参考:[d3-hierarchy#partition_size](https://github.com/d3/d3-hierarchy#partition_size) | +| round | _boolean_ | 默认:`false`。参考:[d3-hierarchy#partition_round](https://github.com/d3/d3-hierarchy#partition_round) | +| sort | _Function_ | 数据节点排序方式,默认:降序。参考: [d3-hierarchy#node_sort](https://github.com/d3/d3-hierarchy#node_sort) | +| ignoreParentValue | _boolean_ | 是否忽略 parentValue, 默认:true。 当设置为 true 时,父节点的权重由子元素决定 | #### radius @@ -169,9 +169,9 @@ meta: { 旭日图内置了一些交互,列表如下: -| Interaction | Description | Configuration | -| ----------- | ---------------------------------------- | ------------------------------ | -| drill-down | 用于下钻交互,配置该交互后,点击可下钻。 | `drilldown: { enabled: true }` | +| 交互 | 描述 | 配置 | +| ---------- | ---------------------------------------- | ------------------------------ | +| drill-down | 用于下钻交互,配置该交互后,点击可下钻。 | `drilldown: { enabled: true }` | `markdown:docs/common/drill-down.zh.md` diff --git a/docs/common/brush.en.md b/docs/common/brush.en.md new file mode 100644 index 0000000000..eaaea18fb8 --- /dev/null +++ b/docs/common/brush.en.md @@ -0,0 +1,79 @@ +#### brush + +**optional** _BrushCfg_ + +Configuration of brush interaction. + +**Properties** + +Types of _BrushCfg_ are as follows: + +| Properties | Type | Description | +| ---------- | ----------- | ------------------------------------------------------------------------------------------------ | +| enabled | _boolean_ | 是否开启 brush 刷选交互,默认为:'false' | +| type | _string_ | brush 类型,可选项:'rect' \| 'x-rect' \| 'y-rect' \| 'cirlce' \| 'path' (polygon). 默认: 'rect' | +| action | _string_ | brush action, options: 'filter' \| 'highlight'. Default: 'filter' | +| mask | _MaskCfg_ | Configuration of mask. | +| button | _ButtonCfg_ | Configuration of rRset Button,works when action is equal to 'filter' | + +Types of _MaskCfg_ are as follows: + +| Properties | Type | Description | +| ---------- | ------------ | ----------- | +| style | _ShapeAttrs_ | mask 样式 | + +Types of _ButtonCfg_ are as follows: + +```ts +export type ButtonCfg = { + /** + * padding of button + */ + padding?: number | number[]; + /** + * text of button + */ + text?: string; + /** + * custom style of text + */ + textStyle?: { + default?: ShapeAttrs; + }; + /** + * custom style of button + */ + buttonStyle?: { + default?: ShapeAttrs; + active?: ShapeAttrs; + }; +}; +``` + +**Events** + +1. List of vents of `brush-filter` interaction, + +| Event Name | Description | +| -------------------------------------- | -------------------------------------------------------- | +| `G2.BRUSH_FILTER_EVENTS.BEFORE_FILTER` | Hook before brush event to trigger `filter` append | +| `G2.BRUSH_FILTER_EVENTS.AFTER_FILTER` | Hook after brush event to trigger `filter` append | +| `G2.BRUSH_FILTER_EVENTS.BEFORE_RESET` | Hook before brush event to trigger filter `reset` append | +| `G2.BRUSH_FILTER_EVENTS.AFTER_RESET` | Hook after brush event to trigger filter `reset` append | + +example: + + + +2. List of vents of `brush-highlight` interaction, + +| Event Name | Description | +| ---------------------------------------------------- | ------------------------------------------------------------------- | +| `G2.ELEMENT_RANGE_HIGHLIGHT_EVENTS.BEFORE_HIGHLIGHT` | Hook before event to trigger element-range `highlight` append | +| `G2.ELEMENT_RANGE_HIGHLIGHT_EVENTS.AFTER_HIGHLIGHT` | Hook after event to trigger element-range `highlight` append | +| `G2.ELEMENT_RANGE_HIGHLIGHT_EVENTS.BEFORE_CLEAR` | Hook before event to trigger element-range-highlight `reset` append | +| `G2.ELEMENT_RANGE_HIGHLIGHT_EVENTS.AFTER_CLEAR` | Hook after event to trigger element-range-highlight `reset` append | + +example: + + diff --git a/docs/common/brush.zh.md b/docs/common/brush.zh.md new file mode 100644 index 0000000000..ce8f66f525 --- /dev/null +++ b/docs/common/brush.zh.md @@ -0,0 +1,81 @@ +#### brush + +**optional** _BrushCfg_ + +刷选交互配置。 + +**配置项** + +_BrushCfg_ 类型定义如下: + +| 属性 | 类型 | 描述 | +| ------- | --------- | --------------------------------------------------------------------------------------------- | +| enabled | _boolean_ | 是否开启 brush 刷选交互,默认为:'false' | +| type | _string_ | brush 类型,可选项:'rect' \| 'x-rect' \| 'y-rect' \| 'cirlce' \| 'path'(不规则矩形). 默认: 'rect' | +| action | _string_ | brush 操作,可选项:'filter' \| 'highlight'(对应 '筛选过滤' 和 '高亮强调'). 默认: 'filter'. | +| mask | _MaskCfg_ | 刷选交互的蒙层 mask UI 配置 | +| button | _ButtonCfg_ | 刷选交互的重置按钮配置,只在 action 为 filter 的时候,生效 | + +_MaskCfg_ 类型定义如下: + +| 属性 | 类型 | 描述 | +| ----- | ------------ | --------- | +| style | _ShapeAttrs_ | mask 样式 | + +_ButtonCfg_ 类型定义如下: + +```ts +export type ButtonCfg = { + /** + * 文本与按钮边缘的间距 + */ + padding?: number | number[]; + /** + * 按钮文本 + */ + text?: string; + /** + * 自定义文本样式 + */ + textStyle?: { + default?: ShapeAttrs; + }; + /** + * 自定义按钮样式 + */ + buttonStyle?: { + default?: ShapeAttrs; + active?: ShapeAttrs; + }; +}; +``` + +**事件** + +brush 交互相关事件: + +1. `brush-filter`, 事件列表: + +| 事件名称 | 描述 | +| -------------------------------------- | ------------------------------------------------- | +| `G2.BRUSH_FILTER_EVENTS.BEFORE_FILTER` | Brush 事件触发 “filter” 发生之前的钩子 | +| `G2.BRUSH_FILTER_EVENTS.AFTER_FILTER` | Brush 事件触发 “filter” 发生之后的钩子 | +| `G2.BRUSH_FILTER_EVENTS.BEFORE_RESET` | Brush 事件触发筛选(filter) “reset” 发生之前的钩子 | +| `G2.BRUSH_FILTER_EVENTS.AFTER_RESET` | Brush 事件触发筛选(filter) “reset” 发生之后的钩子 | + +示例: + + + +2. `brush-highlight`, 事件列表: + +| 事件名称 | 描述 | +| ---------------------------------------------------- | --------------------------------------------------------------- | +| `G2.ELEMENT_RANGE_HIGHLIGHT_EVENTS.BEFORE_HIGHLIGHT` | 事件触发元素区间范围 (“element-range”) 发生“高亮”之前的钩子 | +| `G2.ELEMENT_RANGE_HIGHLIGHT_EVENTS.AFTER_HIGHLIGHT` | 事件触发元素区间范围 (“element-range”) 发生“高亮”之后的钩子 | +| `G2.ELEMENT_RANGE_HIGHLIGHT_EVENTS.BEFORE_CLEAR` | 事件触发元素区间范围 (“element-range”) 发生高亮“重置”之前的钩子 | +| `G2.ELEMENT_RANGE_HIGHLIGHT_EVENTS.AFTER_CLEAR` | 事件触发元素区间范围 (“element-range”) 发生高亮“重置”之后的钩子 | + +示例: + + diff --git a/examples/dynamic-plots/brush/demo/advanced-brush1.ts b/examples/dynamic-plots/brush/demo/advanced-brush1.ts new file mode 100644 index 0000000000..bc62fc44d6 --- /dev/null +++ b/examples/dynamic-plots/brush/demo/advanced-brush1.ts @@ -0,0 +1,93 @@ +import { Column, G2 } from '@antv/g2plot'; +import { each, groupBy } from '@antv/util'; +import insertCss from 'insert-css'; + +const div1 = document.createElement('div'); +div1.id = 'container1'; +const div2 = document.createElement('div'); +div2.id = 'container2'; + +const container = document.querySelector('#container'); +container.appendChild(div1); +container.appendChild(div2); + +insertCss(` +#container { + display: flex; + flex-direction: column; + padding: 12px 8px; +} +#container1 { + flex: 1; +} +#container2 { + padding-top: 12px; + height: 120px; +} +`); + +fetch('https://gw.alipayobjects.com/os/antfincdn/v6MvZBUBsQ/column-data.json') + .then((res) => res.json()) + .then((data) => { + const plot1 = new Column('container1', { + data, + xField: 'release', + yField: 'count', + meta: { + count: { + alias: 'top2000 唱片总量', + nice: true, + }, + release: { + tickInterval: 5, + alias: '唱片发行年份', + }, + }, + tooltip: { + fields: ['release', 'artist', 'count'], + }, + }); + + plot1.render(); + + const plot2 = new Column('container2', { + data, + xField: 'release', + yField: 'rank', + yAxis: false, + appendPadding: [0, 0, 0, 20], + tooltip: { + containerTpl: '
', + itemTpl: '{value}', + domStyles: { + 'g2-tooltip': { + padding: '2px 4px', + fontSize: '10px', + }, + }, + }, + brush: { + enabled: true, + mask: { + style: { + fill: 'rgba(0,0,0,0.2)', + }, + }, + }, + interactions: [{ type: 'active-region', enable: false }], + }); + + plot2.render(); + + plot2.on(G2.BRUSH_FILTER_EVENTS.AFTER_FILTER, () => { + // after brush filter + const filteredData = plot2.chart.getData(); + const releases = filteredData.map((d) => d.release); + plot1.changeData(data.filter((datum) => releases.includes(datum.release))); + }); + + plot2.on(G2.BRUSH_FILTER_EVENTS.AFTER_RESET, () => { + // after brush filter reset + plot1.changeData(data); + }); + }); diff --git a/examples/dynamic-plots/brush/demo/advanced-brush2.ts b/examples/dynamic-plots/brush/demo/advanced-brush2.ts new file mode 100644 index 0000000000..66533c0398 --- /dev/null +++ b/examples/dynamic-plots/brush/demo/advanced-brush2.ts @@ -0,0 +1,97 @@ +import { Column, G2 } from '@antv/g2plot'; +import { each, groupBy } from '@antv/util'; +import insertCss from 'insert-css'; + +const div1 = document.createElement('div'); +div1.id = 'container1'; +const div2 = document.createElement('div'); +div2.id = 'container2'; + +const container = document.querySelector('#container'); +container.appendChild(div1); +container.appendChild(div2); + +insertCss(` +#container { + display: flex; + flex-direction: column; + padding: 12px 8px; +} +#container1 { + flex: 1; +} +#container2 { + padding-top: 12px; + height: 120px; +} +`); + +fetch('https://gw.alipayobjects.com/os/antfincdn/v6MvZBUBsQ/column-data.json') + .then((res) => res.json()) + .then((data) => { + const plot1 = new Column('container1', { + data, + xField: 'release', + yField: 'count', + meta: { + count: { + alias: 'top2000 唱片总量', + nice: true, + }, + release: { + tickInterval: 5, + alias: '唱片发行年份', + }, + }, + tooltip: { + fields: ['release', 'artist', 'count'], + }, + }); + + plot1.render(); + + // 可以用于筛选 “唱片发行总量高”的作家,然后对 plot1 进行高亮,发掘和“唱片发行年份”的关系 + const data2 = []; + each(groupBy(data, 'artist'), (v, artist) => { + data2.push({ artist, count: v.reduce((a, b) => a + b.count, 0) }); + }); + data2.sort((a, b) => a.count - b.count); + const plot2 = new Column('container2', { + data: data2, + xField: 'artist', + yField: 'count', + yAxis: false, + tooltip: { + containerTpl: '
', + itemTpl: '{value}', + domStyles: { + 'g2-tooltip': { + padding: '2px 4px', + fontSize: '10px', + }, + }, + }, + brush: { + enabled: true, + action: 'highlight', + }, + interactions: [{ type: 'active-region', enable: false }], + }); + + plot2.render(); + + // 监听状态变化 + plot2.on(G2.ELEMENT_RANGE_HIGHLIGHT_EVENTS.AFTER_HIGHLIGHT, (evt) => { + const { highlightElements = [] } = evt.data; + + // active + const artists = highlightElements.map((ele) => ele.getData()?.artist).filter((d) => !!d); + plot1.setState('active', (datum) => artists.includes(datum.artist)); + plot1.setState('active', (datum) => !artists.includes(datum.artist), false); + }); + + plot2.on(G2.ELEMENT_RANGE_HIGHLIGHT_EVENTS.AFTER_CLEAR, () => { + // 取消激活 + plot1.setState('active', () => true, false); + }); + }); diff --git a/examples/dynamic-plots/brush/demo/bar-brush.ts b/examples/dynamic-plots/brush/demo/bar-brush.ts new file mode 100644 index 0000000000..2354bcb3f5 --- /dev/null +++ b/examples/dynamic-plots/brush/demo/bar-brush.ts @@ -0,0 +1,27 @@ +import { Bar } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/antfincdn/v6MvZBUBsQ/column-data.json') + .then((res) => res.json()) + .then((data) => { + const plot = new Bar('container', { + data, + xField: 'count', + yField: 'release', + meta: { + count: { + alias: 'top2000 唱片总量', + nice: true, + }, + release: { + tickInterval: 5, + alias: '唱片发行年份', + }, + }, + brush: { + enabled: true, + action: 'highlight', + }, + }); + + plot.render(); + }); diff --git a/examples/dynamic-plots/brush/demo/column-brush-y-highlight.ts b/examples/dynamic-plots/brush/demo/column-brush-y-highlight.ts new file mode 100644 index 0000000000..e45ad677a4 --- /dev/null +++ b/examples/dynamic-plots/brush/demo/column-brush-y-highlight.ts @@ -0,0 +1,28 @@ +import { Column } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/antfincdn/v6MvZBUBsQ/column-data.json') + .then((res) => res.json()) + .then((data) => { + const plot = new Column('container', { + data, + xField: 'release', + yField: 'count', + meta: { + count: { + alias: 'top2000 唱片总量', + nice: true, + }, + release: { + tickInterval: 5, + alias: '唱片发行年份', + }, + }, + brush: { + enabled: true, + type: 'y-rect', + action: 'highlight', + }, + }); + + plot.render(); + }); diff --git a/examples/dynamic-plots/brush/demo/column-brush-y.ts b/examples/dynamic-plots/brush/demo/column-brush-y.ts new file mode 100644 index 0000000000..3d02245a86 --- /dev/null +++ b/examples/dynamic-plots/brush/demo/column-brush-y.ts @@ -0,0 +1,27 @@ +import { Column } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/antfincdn/v6MvZBUBsQ/column-data.json') + .then((res) => res.json()) + .then((data) => { + const plot = new Column('container', { + data, + xField: 'release', + yField: 'count', + meta: { + count: { + alias: 'top2000 唱片总量', + nice: true, + }, + release: { + tickInterval: 5, + alias: '唱片发行年份', + }, + }, + brush: { + enabled: true, + type: 'y-rect', + }, + }); + + plot.render(); + }); diff --git a/examples/dynamic-plots/brush/demo/column-brush.ts b/examples/dynamic-plots/brush/demo/column-brush.ts new file mode 100644 index 0000000000..6c01a2274c --- /dev/null +++ b/examples/dynamic-plots/brush/demo/column-brush.ts @@ -0,0 +1,27 @@ +import { Column } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/antfincdn/v6MvZBUBsQ/column-data.json') + .then((res) => res.json()) + .then((data) => { + const plot = new Column('container', { + data, + xField: 'release', + yField: 'count', + meta: { + count: { + alias: 'top2000 唱片总量', + nice: true, + }, + release: { + tickInterval: 5, + alias: '唱片发行年份', + }, + }, + brush: { + enabled: true, + action: 'highlight', + }, + }); + + plot.render(); + }); diff --git a/examples/dynamic-plots/brush/demo/meta.json b/examples/dynamic-plots/brush/demo/meta.json new file mode 100644 index 0000000000..d75d021bb9 --- /dev/null +++ b/examples/dynamic-plots/brush/demo/meta.json @@ -0,0 +1,80 @@ +{ + "title": { + "zh": "中文分类", + "en": "Category" + }, + "demos": [ + { + "filename": "scatter-brush.jsx", + "title": { + "zh": "散点图刷选高亮", + "en": "Brush highlight of scatter" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/BlpX0yZrRH/brush-highlight.gif" + }, + { + "filename": "scatter-brush-filter.ts", + "title": { + "zh": "散点图刷选", + "en": "Brush of scatter" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/x6%24CDJpPny/brush-x.gif" + }, + { + "filename": "column-brush.ts", + "title": { + "zh": "柱状图刷选高亮", + "en": "Brush highlight of column" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/svUh77%263iZ/column-brush-highlight.gif" + }, + { + "filename": "column-brush-y.ts", + "title": { + "zh": "柱状图 y 方向刷选", + "en": "Brush on y position of column" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/Rf1WzUaySB/column-brush-y.gif" + }, + { + "filename": "column-brush-y-highlight.ts", + "title": { + "zh": "柱状图 y 方向刷选高亮", + "en": "Brush highlight on y position of column" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/Rf1WzUaySB/column-brush-y.gif" + }, + { + "filename": "bar-brush.ts", + "title": { + "zh": "条形图刷选高亮", + "en": "Brush highlight of bar" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/hksereLMdK/bar-brush-highlight.gif" + }, + { + "filename": "advanced-brush1.ts", + "title": { + "zh": "刷选高级用法1", + "en": "Advanced usage1 of brush" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/OLOyP6XI0e/brush-advanced-usage1.gif" + }, + { + "filename": "advanced-brush2.ts", + "title": { + "zh": "刷选高级用法2", + "en": "Advanced usage2 of brush" + }, + "new": true, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/2wHu3BD2s%26/brush-advanced-usage.gif" + } + ] +} diff --git a/examples/dynamic-plots/brush/demo/scatter-brush-filter.ts b/examples/dynamic-plots/brush/demo/scatter-brush-filter.ts new file mode 100644 index 0000000000..38b9da6954 --- /dev/null +++ b/examples/dynamic-plots/brush/demo/scatter-brush-filter.ts @@ -0,0 +1,26 @@ +import { Scatter } from '@antv/g2plot'; + +fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/scatter.json') + .then((res) => res.json()) + .then((data) => { + const plot = new Scatter('container', { + data, + xField: 'weight', + yField: 'height', + colorField: 'gender', + size: 5, + shape: 'circle', + pointStyle: { + fillOpacity: 1, + }, + brush: { + enabled: true, + mask: { + style: { + fill: 'rgba(255,0,0,0.15)', + }, + }, + }, + }); + plot.render(); + }); diff --git a/examples/dynamic-plots/brush/demo/scatter-brush.jsx b/examples/dynamic-plots/brush/demo/scatter-brush.jsx new file mode 100644 index 0000000000..bb02b6abae --- /dev/null +++ b/examples/dynamic-plots/brush/demo/scatter-brush.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Select } from 'antd'; +import { Scatter } from '@antv/g2plot'; +import insertCss from 'insert-css'; + +class Plot extends React.Component { + chartNodeRef = React.createRef(); + chartRef = React.createRef(); + + componentDidMount() { + const chartDom = this.chartNodeRef.current; + const plot = new Scatter(chartDom, { + data: [], + xField: 'weight', + yField: 'height', + colorField: 'gender', + size: 5, + shape: 'circle', + pointStyle: { + fillOpacity: 1, + }, + brush: { + enabled: true, + // 圈选高亮,不指定默认为: filter + action: 'highlight', + mask: { + style: { + fill: 'rgba(0,0,0,0.15)', + stroke: 'rgba(0,0,0,0.45)', + lineWidth: 0.5, + }, + }, + }, + }); + + // Step 3: 渲染图表 + plot.render(); + this.chartRef.current = plot; + + fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/scatter.json') + .then((res) => res.json()) + .then((data) => { + plot.changeData(data); + }); + } + + handleChange = (v) => { + const plot = this.chartRef.current; + if (plot) { + plot.update({ brush: { type: v } }); + } + }; + + render() { + return ( +
+
+ Brush 类型 + +
+
+
+ ); + } +} + +// 我们用 insert-css 演示引入自定义样式 +// 推荐将样式添加到自己的样式文件中 +// 若拷贝官方代码,别忘了 npm install insert-css +insertCss(` + .select-label { + margin-right: 8px; + } + .select-label:not(:first-of-type) { + margin-left: 8px; + } + .chart-wrapper { + margin-top: 12px; + } +`); + +ReactDOM.render(, document.getElementById('container')); diff --git a/examples/dynamic-plots/brush/index.en.md b/examples/dynamic-plots/brush/index.en.md new file mode 100644 index 0000000000..ba63c295c7 --- /dev/null +++ b/examples/dynamic-plots/brush/index.en.md @@ -0,0 +1,4 @@ +--- +title: Brush +order: 2 +--- diff --git a/examples/dynamic-plots/brush/index.zh.md b/examples/dynamic-plots/brush/index.zh.md new file mode 100644 index 0000000000..0553d18fa5 --- /dev/null +++ b/examples/dynamic-plots/brush/index.zh.md @@ -0,0 +1,4 @@ +--- +title: Brush 刷选 +order: 2 +--- diff --git a/src/adaptor/brush.ts b/src/adaptor/brush.ts new file mode 100644 index 0000000000..cf225e03b2 --- /dev/null +++ b/src/adaptor/brush.ts @@ -0,0 +1,59 @@ +import { filter } from '@antv/util'; +import { Params } from '../core/adaptor'; +import { getInteractionCfg } from '../interactions/brush'; +import { deepAssign } from '../utils'; +import { Options as BaseOptions, BrushCfg, Interaction, Writable } from '../types'; + +/** 先引入brush 交互 */ +import '../interactions/brush'; + +type Options = Pick & { brush?: BrushCfg }; + +const BRUSH_TYPES = ['brush', 'brush-x', 'brush-y', 'brush-highlight', 'brush-x-highlight', 'brush-y-highlight']; + +/** + * brush 交互 + */ +export function brushInteraction(params: Params): Params { + const { options } = params; + + const { brush } = options; + + // 先过滤掉 brush 等交互 + const interactions = filter(options.interactions || [], (i) => BRUSH_TYPES.indexOf(i.type) === -1); + + // 设置 brush 交互 + if (brush?.enabled) { + BRUSH_TYPES.forEach((type) => { + let enable = false; + switch (brush.type) { + case 'x-rect': + enable = type === (brush.action === 'highlight' ? 'brush-x-highlight' : 'brush-x'); + break; + case 'y-rect': + enable = type === (brush.action === 'highlight' ? 'brush-y-highlight' : 'brush-y'); + break; + default: + enable = type === (brush.action === 'highlight' ? 'brush-highlight' : 'brush'); + break; + } + const obj: Writable = { type, enable }; + + if (brush.mask?.style || brush.type) { + obj.cfg = getInteractionCfg(type, brush.type, brush.mask); + } + interactions.push(obj); + }); + + // 塞入 button 配置 (G2Plot 的封装) + if (brush?.action !== 'highlight') { + interactions.push({ + type: 'filter-action', + cfg: { + buttonConfig: brush.button, + }, + }); + } + } + return deepAssign({}, params, { options: { interactions } }); +} diff --git a/src/interactions/actions/drill-down.ts b/src/interactions/actions/drill-down.ts index 0f9667a29d..6936a90bc3 100644 --- a/src/interactions/actions/drill-down.ts +++ b/src/interactions/actions/drill-down.ts @@ -8,6 +8,7 @@ import { deepAssign } from '../../utils/deep-assign'; const PADDING = 4; // 面包屑位置距离树图的距离 const PADDING_LEFT = 0; +// 面包屑位置距离树图的顶部距离 export const PADDING_TOP = 5; /** Group name of breadCrumb: 面包屑 */ @@ -57,6 +58,7 @@ type HistoryCache = { /** * @description 下钻交互的 action + * @author liuzhenying * * 适用于:hierarchy plot */ diff --git a/src/interactions/actions/reset-button.ts b/src/interactions/actions/reset-button.ts new file mode 100644 index 0000000000..5973d56db6 --- /dev/null +++ b/src/interactions/actions/reset-button.ts @@ -0,0 +1,161 @@ +import { Action, IGroup, Util } from '@antv/g2'; +import { get } from '@antv/util'; +import { BBox, ButtonCfg } from '../../types'; +import { deepAssign, normalPadding } from '../../utils'; + +const PADDING_RIGHT = 10; +const PADDING_TOP = 5; + +/** + * Action 中的 Button 按钮配置 + * + * 可能的使用场景:brush filter + */ +export const BUTTON_ACTION_CONFIG: ButtonCfg = { + padding: [8, 10], + text: 'reset', + textStyle: { + default: { + x: 0, + y: 0, + fontSize: 12, + fill: '#333333', + cursor: 'pointer', + }, + }, + buttonStyle: { + default: { + fill: '#f7f7f7', + stroke: '#cccccc', + cursor: 'pointer', + }, + active: { + fill: '#e6e6e6', + }, + }, +}; + +/** + * @override 复写 G2 Button Action, 后续直接使用 GUI + */ +class ButtonAction extends Action { + private buttonGroup: IGroup = null; + private buttonCfg = { + name: 'button', + ...BUTTON_ACTION_CONFIG, + }; + + /** + * 获取 mix 默认的配置和用户配置 + */ + private getButtonCfg(): ButtonCfg & { name: string } { + const { view } = this.context; + const buttonCfg: ButtonCfg = get(view, ['interactions', 'filter-action', 'cfg', 'buttonConfig']); + + return deepAssign(this.buttonCfg, buttonCfg, this.cfg); + } + + /** + * 绘制 Button 和 文本 + */ + private drawButton() { + const config = this.getButtonCfg(); + const group = this.context.view.foregroundGroup.addGroup({ + name: config.name, + }); + const textShape = this.drawText(group); + this.drawBackground(group, textShape.getBBox()); + + this.buttonGroup = group; + } + + /** + * 绘制文本 + */ + private drawText(group: IGroup) { + const config = this.getButtonCfg(); + // 添加文本 + return group.addShape({ + type: 'text', + name: 'button-text', + attrs: { + text: config.text, + ...config.textStyle?.default, + }, + }); + } + + private drawBackground(group: IGroup, bbox: BBox) { + const config = this.getButtonCfg(); + const padding = normalPadding(config.padding); + // 添加背景按钮 + const buttonShape = group.addShape({ + type: 'rect', + name: 'button-rect', + attrs: { + x: bbox.x - padding[3], + y: bbox.y - padding[0], + width: bbox.width + padding[1] + padding[3], + height: bbox.height + padding[0] + padding[2], + ...config.buttonStyle?.default, + }, + }); + buttonShape.toBack(); // 在后面 + + // active 效果内置 + group.on('mouseenter', () => { + buttonShape.attr(config.buttonStyle?.active); + }); + group.on('mouseleave', () => { + buttonShape.attr(config.buttonStyle?.default); + }); + + return buttonShape; + } + + // 重置位置 + private resetPosition() { + const view = this.context.view; + const coord = view.getCoordinate(); + const point = coord.convert({ x: 1, y: 1 }); // 后面直接改成左上角 + const buttonGroup = this.buttonGroup; + const bbox = buttonGroup.getBBox(); + const matrix = Util.transform(null, [ + ['t', point.x - bbox.width - PADDING_RIGHT, point.y + bbox.height + PADDING_TOP], + ]); + buttonGroup.setMatrix(matrix); + } + + /** + * 显示 + */ + public show() { + if (!this.buttonGroup) { + this.drawButton(); + } + this.resetPosition(); + this.buttonGroup.show(); + } + + /** + * 隐藏 + */ + public hide() { + if (this.buttonGroup) { + this.buttonGroup.hide(); + } + } + + /** + * 销毁 + */ + public destroy() { + const buttonGroup = this.buttonGroup; + if (buttonGroup) { + buttonGroup.remove(); + } + super.destroy(); + } +} + +export { ButtonAction }; diff --git a/src/interactions/brush.ts b/src/interactions/brush.ts new file mode 100644 index 0000000000..752e0882ba --- /dev/null +++ b/src/interactions/brush.ts @@ -0,0 +1,303 @@ +import { registerAction, registerInteraction } from '@antv/g2'; +import { BrushCfg } from '../types'; +import { ButtonAction } from './actions/reset-button'; + +registerAction('brush-reset-button', ButtonAction, { + name: 'brush-reset-button', +}); + +registerInteraction('filter-action', {}); + +/** + * G2 已经内置了 brush、brush-x、brush-y 等交互,其它: + * + * 1. element-range-highlight 是否可用重命名为 brush-highlight?(mask 可以移动) + * 2. brush-visible 与 brush 的区别是? + */ + +function isPointInView(context) { + return context.isInPlot(); +} + +/** + * 获取 交互 start 阶段的相关配置 + */ +export function getInteractionCfg(interactionType: string, brushType?: string, mask?: BrushCfg['mask']) { + const maskType = brushType || 'rect'; + + switch (interactionType) { + case 'brush': + return { + showEnable: [ + { trigger: 'plot:mouseenter', action: 'cursor:crosshair' }, + { trigger: 'plot:mouseleave', action: 'cursor:default' }, + ], + start: [ + { + trigger: 'mousedown', + isEnable: isPointInView, + action: ['brush:start', `${maskType}-mask:start`, `${maskType}-mask:show`], + // 对应第二个action的参数 + arg: [null, { maskStyle: mask?.style }], + }, + ], + processing: [ + { + trigger: 'mousemove', + isEnable: isPointInView, + action: [`${maskType}-mask:resize`], + }, + ], + end: [ + { + trigger: 'mouseup', + isEnable: isPointInView, + action: [ + 'brush:filter', + 'brush:end', + `${maskType}-mask:end`, + `${maskType}-mask:hide`, + 'brush-reset-button:show', + ], + }, + ], + rollback: [ + { + trigger: 'brush-reset-button:click', + action: ['brush:reset', 'brush-reset-button:hide', 'cursor:crosshair'], + }, + ], + }; + case 'brush-highlight': + return { + showEnable: [ + { trigger: 'plot:mouseenter', action: 'cursor:crosshair' }, + { trigger: 'mask:mouseenter', action: 'cursor:move' }, + { trigger: 'plot:mouseleave', action: 'cursor:default' }, + { trigger: 'mask:mouseleave', action: 'cursor:crosshair' }, + ], + start: [ + { + trigger: 'plot:mousedown', + isEnable(context) { + // 不要点击在 mask 上重新开始 + return !context.isInShape('mask'); + }, + action: [`${maskType}-mask:start`, `${maskType}-mask:show`], + // 对应第 1 个action的参数 + arg: [{ maskStyle: mask?.style }], + }, + { + trigger: 'mask:dragstart', + action: [`${maskType}-mask:moveStart`], + }, + ], + processing: [ + { + trigger: 'plot:mousemove', + action: [`${maskType}-mask:resize`], + }, + { + trigger: 'mask:drag', + action: [`${maskType}-mask:move`], + }, + { + trigger: 'mask:change', + action: ['element-range-highlight:highlight'], + }, + ], + end: [ + { trigger: 'plot:mouseup', action: [`${maskType}-mask:end`] }, + { trigger: 'mask:dragend', action: [`${maskType}-mask:moveEnd`] }, + { + trigger: 'document:mouseup', + isEnable(context) { + return !context.isInPlot(); + }, + action: ['element-range-highlight:clear', `${maskType}-mask:end`, `${maskType}-mask:hide`], + }, + ], + rollback: [{ trigger: 'dblclick', action: ['element-range-highlight:clear', `${maskType}-mask:hide`] }], + }; + case 'brush-x': + return { + showEnable: [ + { trigger: 'plot:mouseenter', action: 'cursor:crosshair' }, + { trigger: 'plot:mouseleave', action: 'cursor:default' }, + ], + start: [ + { + trigger: 'mousedown', + isEnable: isPointInView, + action: ['brush-x:start', `${maskType}-mask:start`, `${maskType}-mask:show`], + // 对应第二个action的参数 + arg: [null, { maskStyle: mask?.style }], + }, + ], + processing: [ + { + trigger: 'mousemove', + isEnable: isPointInView, + action: [`${maskType}-mask:resize`], + }, + ], + end: [ + { + trigger: 'mouseup', + isEnable: isPointInView, + action: ['brush-x:filter', 'brush-x:end', `${maskType}-mask:end`, `${maskType}-mask:hide`], + }, + ], + rollback: [{ trigger: 'dblclick', action: ['brush-x:reset'] }], + }; + case 'brush-x-highlight': + return { + showEnable: [ + { trigger: 'plot:mouseenter', action: 'cursor:crosshair' }, + { trigger: 'mask:mouseenter', action: 'cursor:move' }, + { trigger: 'plot:mouseleave', action: 'cursor:default' }, + { trigger: 'mask:mouseleave', action: 'cursor:crosshair' }, + ], + start: [ + { + trigger: 'plot:mousedown', + isEnable(context) { + // 不要点击在 mask 上重新开始 + return !context.isInShape('mask'); + }, + action: [`${maskType}-mask:start`, `${maskType}-mask:show`], + // 对应第 1 个action的参数 + arg: [{ maskStyle: mask?.style }], + }, + { + trigger: 'mask:dragstart', + action: [`${maskType}-mask:moveStart`], + }, + ], + processing: [ + { + trigger: 'plot:mousemove', + action: [`${maskType}-mask:resize`], + }, + { + trigger: 'mask:drag', + action: [`${maskType}-mask:move`], + }, + { + trigger: 'mask:change', + action: ['element-range-highlight:highlight'], + }, + ], + end: [ + { trigger: 'plot:mouseup', action: [`${maskType}-mask:end`] }, + { trigger: 'mask:dragend', action: [`${maskType}-mask:moveEnd`] }, + { + trigger: 'document:mouseup', + isEnable(context) { + return !context.isInPlot(); + }, + action: ['element-range-highlight:clear', `${maskType}-mask:end`, `${maskType}-mask:hide`], + }, + ], + rollback: [{ trigger: 'dblclick', action: ['element-range-highlight:clear', `${maskType}-mask:hide`] }], + }; + case 'brush-y': + return { + showEnable: [ + { trigger: 'plot:mouseenter', action: 'cursor:crosshair' }, + { trigger: 'plot:mouseleave', action: 'cursor:default' }, + ], + start: [ + { + trigger: 'mousedown', + isEnable: isPointInView, + action: ['brush-y:start', `${maskType}-mask:start`, `${maskType}-mask:show`], + // 对应第二个action的参数 + arg: [null, { maskStyle: mask?.style }], + }, + ], + processing: [ + { + trigger: 'mousemove', + isEnable: isPointInView, + action: [`${maskType}-mask:resize`], + }, + ], + end: [ + { + trigger: 'mouseup', + isEnable: isPointInView, + action: ['brush-y:filter', 'brush-y:end', `${maskType}-mask:end`, `${maskType}-mask:hide`], + }, + ], + rollback: [{ trigger: 'dblclick', action: ['brush-y:reset'] }], + }; + case 'brush-y-highlight': + return { + showEnable: [ + { trigger: 'plot:mouseenter', action: 'cursor:crosshair' }, + { trigger: 'mask:mouseenter', action: 'cursor:move' }, + { trigger: 'plot:mouseleave', action: 'cursor:default' }, + { trigger: 'mask:mouseleave', action: 'cursor:crosshair' }, + ], + start: [ + { + trigger: 'plot:mousedown', + isEnable(context) { + // 不要点击在 mask 上重新开始 + return !context.isInShape('mask'); + }, + action: [`${maskType}-mask:start`, `${maskType}-mask:show`], + // 对应第 1 个action的参数 + arg: [{ maskStyle: mask?.style }], + }, + { + trigger: 'mask:dragstart', + action: [`${maskType}-mask:moveStart`], + }, + ], + processing: [ + { + trigger: 'plot:mousemove', + action: [`${maskType}-mask:resize`], + }, + { + trigger: 'mask:drag', + action: [`${maskType}-mask:move`], + }, + { + trigger: 'mask:change', + action: ['element-range-highlight:highlight'], + }, + ], + end: [ + { trigger: 'plot:mouseup', action: [`${maskType}-mask:end`] }, + { trigger: 'mask:dragend', action: [`${maskType}-mask:moveEnd`] }, + { + trigger: 'document:mouseup', + isEnable(context) { + return !context.isInPlot(); + }, + action: ['element-range-highlight:clear', `${maskType}-mask:end`, `${maskType}-mask:hide`], + }, + ], + rollback: [{ trigger: 'dblclick', action: ['element-range-highlight:clear', `${maskType}-mask:hide`] }], + }; + + default: + return {}; + } +} + +// 直接拷贝过来的 +registerInteraction('brush', getInteractionCfg('brush')); +// 复写 element-range-highlight interaction +registerInteraction('brush-highlight', getInteractionCfg('brush-highlight')); +// 复写 +registerInteraction('brush-x', getInteractionCfg('brush-x', 'x-rect')); +// 复写 +registerInteraction('brush-y', getInteractionCfg('brush-y', 'y-rect')); +// 新增, x 框选高亮 +registerInteraction('brush-x-highlight', getInteractionCfg('brush-x-highlight', 'x-rect')); +// 新增, y 框选高亮 +registerInteraction('brush-y-highlight', getInteractionCfg('brush-y-highlight', 'y-rect')); diff --git a/src/plots/column/adaptor.ts b/src/plots/column/adaptor.ts index e1fcbea630..741a01fe94 100644 --- a/src/plots/column/adaptor.ts +++ b/src/plots/column/adaptor.ts @@ -18,6 +18,7 @@ import { interval } from '../../adaptor/geometries'; import { flow, transformLabel, deepAssign, findGeometry, adjustYMetaByZero, pick } from '../../utils'; import { getDataWhetherPecentage, getDeepPercent } from '../../utils/transform/percent'; import { Datum } from '../../types'; +import { brushInteraction } from '../../adaptor/brush'; import { ColumnOptions } from './types'; /** @@ -295,6 +296,7 @@ export function adaptor(params: Params, isBar = false) { slider, scrollbar, label, + brushInteraction, interaction, animation, annotation(), diff --git a/src/plots/column/types.ts b/src/plots/column/types.ts index 9165bce854..8a9c8f49c8 100644 --- a/src/plots/column/types.ts +++ b/src/plots/column/types.ts @@ -1,5 +1,5 @@ import { ShapeAttrs } from '@antv/g2'; -import { Options, StyleAttr } from '../../types'; +import { BrushCfg, Options, StyleAttr } from '../../types'; import { OptionWithConversionTag } from '../../adaptor/conversion-tag'; import { OptionWithConnectedArea } from '../../adaptor/connected-area'; import { IntervalGeometryOptions } from '../../adaptor/geometries/interval'; @@ -39,4 +39,7 @@ export interface ColumnOptions readonly columnStyle?: StyleAttr; /** 分组字段,优先级高于 seriesField , isGroup: true 时会根据 groupField 进行分组。*/ readonly groupField?: string; + + // 图表交互 + readonly brush?: BrushCfg; } diff --git a/src/plots/scatter/adaptor.ts b/src/plots/scatter/adaptor.ts index 200151c99f..86e81f6663 100644 --- a/src/plots/scatter/adaptor.ts +++ b/src/plots/scatter/adaptor.ts @@ -2,6 +2,7 @@ import { isNumber } from '@antv/util'; import { Params } from '../../core/adaptor'; import { flow, deepAssign, pick } from '../../utils'; import { point } from '../../adaptor/geometries'; +import { brushInteraction } from '../../adaptor/brush'; import { interaction, animation, theme, scale, annotation } from '../../adaptor/common'; import { findGeometry, transformLabel } from '../../utils'; import { getQuadrantDefaultConfig, getPath, getMeta } from './util'; @@ -324,6 +325,8 @@ export function adaptor(params: Params) { legend, tooltip, label, + // 需要在 interaction 前面 + brushInteraction, interaction, scatterAnnotation, animation, diff --git a/src/plots/scatter/types.ts b/src/plots/scatter/types.ts index 11ac125659..b0c72f8ee8 100644 --- a/src/plots/scatter/types.ts +++ b/src/plots/scatter/types.ts @@ -7,6 +7,7 @@ import { StyleAttr, ShapeAttr, SizeAttr, + BrushCfg, } from '../../types'; interface Labels extends Omit { @@ -60,8 +61,13 @@ export interface ScatterOptions extends Options { readonly pointStyle?: StyleAttr; /** 点颜色映射对应的数据字段名 */ readonly colorField?: string; + + // 图表标注组件 /** 四象限组件 */ readonly quadrant?: QuadrantOptions; /** 归曲线 */ readonly regressionLine?: RegressionLineOptions; + + // 图表交互 + readonly brush?: BrushCfg; } diff --git a/src/types/button.ts b/src/types/button.ts new file mode 100644 index 0000000000..1533ba5fff --- /dev/null +++ b/src/types/button.ts @@ -0,0 +1,30 @@ +import { ShapeAttrs } from '@antv/g2'; + +/** + * @description 一些按钮的类型定义,比如 brush filter 中的 Button + * + * 和 GUI Button 尽量保持一致 + */ +export type ButtonCfg = { + /** + * 文本与按钮边缘的间距 + */ + padding?: number | number[]; + /** + * 按钮文本 + */ + text?: string; + /** + * 自定义文本样式 + */ + textStyle?: { + default?: ShapeAttrs; + }; + /** + * 自定义按钮样式 + */ + buttonStyle?: { + default?: ShapeAttrs; + active?: ShapeAttrs; + }; +}; diff --git a/src/types/index.ts b/src/types/index.ts index 9db4230233..2e09b66e04 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -7,3 +7,7 @@ export * from './meta'; export * from './axis'; export * from './interaction'; export * from './locale'; +export * from './button'; + +/** 去除 readonly 修饰 */ +export type Writable = { -readonly [P in keyof T]: T[P] }; diff --git a/src/types/interaction.ts b/src/types/interaction.ts index f58b0fcdbd..5f2ac607eb 100644 --- a/src/types/interaction.ts +++ b/src/types/interaction.ts @@ -1,6 +1,26 @@ +import { ShapeAttrs } from '@antv/g2'; +import { ButtonCfg } from './button'; + export type Interaction = { readonly type: string; readonly cfg?: Record; /** 是否开启交互, 默认开启 */ readonly enable?: boolean; }; + +/** brush 交互 */ +export type BrushCfg = { + /** Enable or disable the brush interaction */ + readonly enabled?: boolean; + /** brush 类型: '矩形', 'x 方向' 和 'y 方向', 'circle', 'path'(不规则矩形). 默认: 'rect'. */ + readonly type?: 'rect' | 'x-rect' | 'y-rect' | 'circle' | 'path'; + /** brush 操作: '筛选过滤' 和 '高亮强调'. 默认: 'filter'. 目前只在 type 为 'rect' 的情况下生效 */ + readonly action?: 'filter' | 'highlight'; + /** brush mask 的配置 */ + readonly mask?: { + /** mask 蒙层样式 */ + style?: ShapeAttrs; + }; + /** brush button 的配置, 只在 action: 'filter' 的情况下适用 */ + readonly button?: ButtonCfg; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 38142717d5..788dd446c3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -13,3 +13,4 @@ export { renderStatistic, renderGaugeStatistic } from './statistic'; export { measureTextWidth } from './measure-text'; export { isBetween, isRealNumber } from './number'; export * from './data'; +export * from './padding';