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';