diff --git a/__tests__/integration/snapshots/interaction/indices-line-chart-facet-brush-shared/step0.png b/__tests__/integration/snapshots/interaction/indices-line-chart-facet-brush-shared/step0.png
index fc52b75104..5a1ac28124 100644
Binary files a/__tests__/integration/snapshots/interaction/indices-line-chart-facet-brush-shared/step0.png and b/__tests__/integration/snapshots/interaction/indices-line-chart-facet-brush-shared/step0.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step0.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step0.png
new file mode 100644
index 0000000000..61c5d34a06
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step0.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step1.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step1.png
new file mode 100644
index 0000000000..19745cd4fb
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step1.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step10.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step10.png
new file mode 100644
index 0000000000..cbb5002997
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step10.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step11.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step11.png
new file mode 100644
index 0000000000..5f945b7372
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step11.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step2.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step2.png
new file mode 100644
index 0000000000..8a90fa0acc
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step2.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step3.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step3.png
new file mode 100644
index 0000000000..17665b2bc4
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step3.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step4.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step4.png
new file mode 100644
index 0000000000..4fb3f8ee11
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step4.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step5.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step5.png
new file mode 100644
index 0000000000..f2b4d7e69f
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step5.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step6.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step6.png
new file mode 100644
index 0000000000..4b340fd1c2
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step6.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step7.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step7.png
new file mode 100644
index 0000000000..c82d7ac116
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step7.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step8.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step8.png
new file mode 100644
index 0000000000..4c6f39283f
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step8.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step9.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step9.png
new file mode 100644
index 0000000000..4637809417
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-custom/step9.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-style/step0.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-style/step0.png
new file mode 100644
index 0000000000..cd1b0b58e6
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush-handle-style/step0.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-reverse/step0.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-reverse/step0.png
index ea7627127e..1b83eed4bb 100644
Binary files a/__tests__/integration/snapshots/interaction/penguins-point-brush-reverse/step0.png and b/__tests__/integration/snapshots/interaction/penguins-point-brush-reverse/step0.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush-reverse/step1.png b/__tests__/integration/snapshots/interaction/penguins-point-brush-reverse/step1.png
index 3067989676..c68d5fed2d 100644
Binary files a/__tests__/integration/snapshots/interaction/penguins-point-brush-reverse/step1.png and b/__tests__/integration/snapshots/interaction/penguins-point-brush-reverse/step1.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush/step10.png b/__tests__/integration/snapshots/interaction/penguins-point-brush/step10.png
new file mode 100644
index 0000000000..88a1e27f85
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush/step10.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush/step11.png b/__tests__/integration/snapshots/interaction/penguins-point-brush/step11.png
new file mode 100644
index 0000000000..126c86f31a
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush/step11.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush/step4.png b/__tests__/integration/snapshots/interaction/penguins-point-brush/step4.png
new file mode 100644
index 0000000000..97a54663c7
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush/step4.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush/step5.png b/__tests__/integration/snapshots/interaction/penguins-point-brush/step5.png
new file mode 100644
index 0000000000..5534cd8ce7
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush/step5.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush/step6.png b/__tests__/integration/snapshots/interaction/penguins-point-brush/step6.png
new file mode 100644
index 0000000000..73542eb7e0
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush/step6.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush/step7.png b/__tests__/integration/snapshots/interaction/penguins-point-brush/step7.png
new file mode 100644
index 0000000000..6bc3996662
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush/step7.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush/step8.png b/__tests__/integration/snapshots/interaction/penguins-point-brush/step8.png
new file mode 100644
index 0000000000..acae5d8bf9
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush/step8.png differ
diff --git a/__tests__/integration/snapshots/interaction/penguins-point-brush/step9.png b/__tests__/integration/snapshots/interaction/penguins-point-brush/step9.png
new file mode 100644
index 0000000000..4961efb6f5
Binary files /dev/null and b/__tests__/integration/snapshots/interaction/penguins-point-brush/step9.png differ
diff --git a/__tests__/plots/interaction/index.ts b/__tests__/plots/interaction/index.ts
index 54a39772f9..09b885f0d0 100644
--- a/__tests__/plots/interaction/index.ts
+++ b/__tests__/plots/interaction/index.ts
@@ -58,3 +58,5 @@ export { populationIntervalDiverging } from './population-interval-diverging';
export { stateAgesIntervalNormalized } from './stateages-interval-normalized';
export { aaplLineSliderFilterTranspose } from './appl-line-slider-filter-transpose';
export { alphabetIntervalFunnelLegendFilter } from './alphabet-interval-funnel-legend-filter';
+export { penguinsPointBrushHandleStyle } from './penguins-point-brush-handle-style';
+export { penguinsPointBrushHandleCustom } from './penguins-point-brush-handle-custom';
diff --git a/__tests__/plots/interaction/penguins-point-brush-handle-custom.ts b/__tests__/plots/interaction/penguins-point-brush-handle-custom.ts
new file mode 100644
index 0000000000..0a95b1e832
--- /dev/null
+++ b/__tests__/plots/interaction/penguins-point-brush-handle-custom.ts
@@ -0,0 +1,102 @@
+import { G2Spec, PLOT_CLASS_NAME } from '../../../src';
+import { brush, brushSteps } from './penguins-point-brush';
+
+function createPathRender(path) {
+ return (group, options, document) => {
+ if (!group.handle) {
+ const path = document.createElement('path');
+ group.handle = path;
+ group.appendChild(group.handle);
+ }
+ const { handle } = group;
+ const { width, height, ...rest } = options;
+ if (width === undefined || height === undefined) return handle;
+ handle.style.d = path(width, height);
+ handle.attr(rest);
+ return handle;
+ };
+}
+
+export function penguinsPointBrushHandleCustom(): G2Spec {
+ return {
+ type: 'point',
+ data: {
+ type: 'fetch',
+ value: 'data/penguins.csv',
+ },
+ encode: {
+ color: 'species',
+ x: 'culmen_length_mm',
+ y: 'culmen_depth_mm',
+ },
+ state: {
+ inactive: { stroke: 'gray', opacity: 0.5 },
+ },
+ interaction: {
+ brushHighlight: {
+ maskHandleSize: 30,
+ maskHandleNRender: createPathRender(
+ (width, height) =>
+ `M0,${height / 2}L${width / 2},${-height / 2}L${width},${
+ height / 2
+ },Z`,
+ ),
+ maskHandleERender: createPathRender(
+ (width, height) =>
+ `M${width / 2},0L${(width * 3) / 2},${height / 2}L${
+ width / 2
+ },${height},Z`,
+ ),
+ maskHandleSRender: createPathRender(
+ (width, height) =>
+ `M0,${height / 2}L${width / 2},${(height / 2) * 3}L${width},${
+ height / 2
+ },Z`,
+ ),
+ maskHandleWRender: createPathRender(
+ (width, height) =>
+ `M${width / 2},0L${-width},${height / 2}L${width / 2},${height},Z`,
+ ),
+ maskHandleNWRender: createPathRender(
+ (width, height) =>
+ `M0,0L${width},${height / 2}L${width / 2},${height},Z`,
+ ),
+ maskHandleNERender: createPathRender(
+ (width, height) =>
+ `M0,${height / 2}L${width},0L${width / 2},${height},Z`,
+ ),
+ maskHandleSERender: createPathRender(
+ (width, height) =>
+ `M${width / 2},0L${width},${height}L0,${height / 2},Z`,
+ ),
+ maskHandleSWRender: createPathRender(
+ (width, height) =>
+ `M${width / 2},0L${width},${height / 2}L0,${height},Z`,
+ ),
+ maskHandleNFill: 'blue',
+ maskHandleEFill: 'red',
+ maskHandleSFill: 'green',
+ maskHandleWFill: 'yellow',
+ maskHandleNWFill: 'black',
+ maskHandleNEFill: 'steelblue',
+ maskHandleSEFill: 'pink',
+ maskHandleSWFill: 'orange',
+ },
+ },
+ };
+}
+
+penguinsPointBrushHandleCustom.steps = ({ canvas }) => {
+ const { document } = canvas;
+ const plot = document.getElementsByClassName(PLOT_CLASS_NAME)[0];
+
+ return [
+ {
+ changeState: () => {
+ brush(plot, 100, 100, 200, 200);
+ },
+ },
+ ];
+};
+
+penguinsPointBrushHandleCustom.steps = brushSteps;
diff --git a/__tests__/plots/interaction/penguins-point-brush-handle-style.ts b/__tests__/plots/interaction/penguins-point-brush-handle-style.ts
new file mode 100644
index 0000000000..7435bb70be
--- /dev/null
+++ b/__tests__/plots/interaction/penguins-point-brush-handle-style.ts
@@ -0,0 +1,44 @@
+import { G2Spec, PLOT_CLASS_NAME } from '../../../src';
+import { brush } from './penguins-point-brush';
+
+export function penguinsPointBrushHandleStyle(): G2Spec {
+ return {
+ type: 'point',
+ data: {
+ type: 'fetch',
+ value: 'data/penguins.csv',
+ },
+ encode: {
+ color: 'species',
+ x: 'culmen_length_mm',
+ y: 'culmen_depth_mm',
+ },
+ state: {
+ inactive: { stroke: 'gray', opacity: 0.5 },
+ },
+ interaction: {
+ brushHighlight: {
+ maskHandleNFill: 'blue',
+ maskHandleEFill: 'red',
+ maskHandleSFill: 'green',
+ maskHandleWFill: 'yellow',
+ maskHandleNWFill: 'black',
+ maskHandleNEFill: 'steelblue',
+ maskHandleSEFill: 'pink',
+ maskHandleSWFill: 'orange',
+ },
+ },
+ };
+}
+
+penguinsPointBrushHandleStyle.steps = ({ canvas }) => {
+ const { document } = canvas;
+ const plot = document.getElementsByClassName(PLOT_CLASS_NAME)[0];
+ return [
+ {
+ changeState: () => {
+ brush(plot, 100, 100, 200, 200);
+ },
+ },
+ ];
+};
diff --git a/__tests__/plots/interaction/penguins-point-brush.ts b/__tests__/plots/interaction/penguins-point-brush.ts
index ee37e781cd..fe111d057f 100644
--- a/__tests__/plots/interaction/penguins-point-brush.ts
+++ b/__tests__/plots/interaction/penguins-point-brush.ts
@@ -40,21 +40,8 @@ export function brush(plot, x, y, x1, y1) {
}
export function dragMask(plot, x, y, x1, y1) {
- const mask = plot.getElementsByClassName('mask')[0];
- mask.dispatchEvent(
- new CustomEvent('dragstart', {
- // @ts-ignore
- offsetX: x,
- offsetY: y,
- }),
- );
- mask.dispatchEvent(
- new CustomEvent('drag', {
- // @ts-ignore
- offsetX: x1,
- offsetY: y1,
- }),
- );
+ const mask = plot.getElementById('selection');
+ drag(mask, x, y, x1, y1);
}
export function dblclick(plot, x = 200, y = 200) {
@@ -76,10 +63,26 @@ export function dblclick(plot, x = 200, y = 200) {
);
}
-penguinsPointBrush.steps = ({ canvas }) => {
+export function drag(shape, x, y, x1, y1) {
+ shape.dispatchEvent(
+ new CustomEvent('dragstart', {
+ // @ts-ignore
+ offsetX: x,
+ offsetY: y,
+ }),
+ );
+ shape.dispatchEvent(
+ new CustomEvent('drag', {
+ // @ts-ignore
+ offsetX: x1,
+ offsetY: y1,
+ }),
+ );
+}
+
+export function brushSteps({ canvas }) {
const { document } = canvas;
const plot = document.getElementsByClassName(PLOT_CLASS_NAME)[0];
-
return [
{
changeState: () => {
@@ -101,5 +104,28 @@ penguinsPointBrush.steps = ({ canvas }) => {
dragMask(plot, 100, 100, 640, 480);
},
},
+ ...resize(plot),
];
-};
+}
+
+// Origin mask: [490, 330, 640, 480]
+export function resize(plot) {
+ const handles = [
+ ['handle-n', 500, 330, 500, 200], // [490, 200, 640, 480]
+ ['handle-e', 640, 300, 600, 300], // [490, 200, 600, 480]
+ ['handle-s', 500, 480, 500, 300], // [490, 200, 600, 300]
+ ['handle-w', 490, 200, 400, 200], // [400, 200, 500, 300]
+ ['handle-nw', 400, 200, 300, 300], // [300, 300, 500, 300]
+ ['handle-ne', 500, 300, 600, 200], // [300, 200, 600, 300]
+ ['handle-se', 600, 300, 500, 200], // [300, 200, 500, 200]
+ ['handle-sw', 300, 200, 400, 300], // [400, 200, 500, 300]
+ ] as const;
+ return handles.map(([id, x, y, x1, y1]) => ({
+ changeState: () => {
+ const handle = plot.getElementById(id);
+ drag(handle, x, y, x1, y1);
+ },
+ }));
+}
+
+penguinsPointBrush.steps = brushSteps;
diff --git a/site/docs/spec/interaction/brushHighlight.zh.md b/site/docs/spec/interaction/brushHighlight.zh.md
index f7fa3e1846..f06686f5aa 100644
--- a/site/docs/spec/interaction/brushHighlight.zh.md
+++ b/site/docs/spec/interaction/brushHighlight.zh.md
@@ -40,3 +40,170 @@ chart.render();
| series | 是否是系列元素 | `boolean` | false |
| facet | 是否跨分面 | `boolean` | false |
| `mask${StyleAttrs}` | brush 的样式 | `number\| string` | - |
+
+# Brush
+
+支持八个方向的 resize 和自定义对应的 handle。
+
+## 案例
+
+### 设置样式
+
+八个方向的 handle 的名字分别如下(按照东南西北命名),按照 `mask[handleName][styleAttribute]` 格式设置对应的属性,也可以通过 `maskHandleSize` 设置宽度。
+
+
+
+```js
+chart.options({
+ type: 'point',
+ data: {
+ type: 'fetch',
+ value: 'data/penguins.csv',
+ },
+ encode: {
+ color: 'species',
+ x: 'culmen_length_mm',
+ y: 'culmen_depth_mm',
+ },
+ state: {
+ inactive: { stroke: 'gray', opacity: 0.5 },
+ },
+ interaction: {
+ brushHighlight: {
+ maskHandleNFill: 'blue',
+ maskHandleEFill: 'red',
+ maskHandleSFill: 'green',
+ maskHandleWFill: 'yellow',
+ maskHandleNWFill: 'black',
+ maskHandleNEFill: 'steelblue',
+ maskHandleSEFill: 'pink',
+ maskHandleSWFill: 'orange',
+ },
+ },
+});
+```
+
+### 自定义 Handle
+
+可以通过 `mask[handleName]Render` 指定 handle 的渲染函数,用于渲染自定义的 handle。其中该函数签名如下。
+
+```js
+function render(
+ g, // 挂载容器
+ options, // 样式属性,通过 mask[handleName][styleAttribute] 设置
+ document, // 画布 document,用于创建自图形
+) {
+ // 需要返回创建的图形
+}
+```
+
+下面是一个创建 path handle 的例子:
+
+```js
+function renderPath(group, options, document) {
+ // 创建逻辑
+ // 如果是第一次渲染,就创建并且挂在图形
+ if (!group.handle) {
+ // 通过 document.createElement 去新建图形
+ const path = document.createElement('path');
+ group.handle = path;
+ group.appendChild(group.handle);
+ }
+
+ // 更新逻辑
+ const { handle } = group;
+ const { width, height, ...rest } = options;
+ if (width === undefined || height === undefined) return handle;
+ handle.attr(rest);
+
+ // 返回对应的 shape
+ return handle;
+}
+```
+
+
+
+```js
+function createPathRender(path) {
+ return (group, options, document) => {
+ if (!group.handle) {
+ const path = document.createElement('path');
+ group.handle = path;
+ group.appendChild(group.handle);
+ }
+ const { handle } = group;
+ const { width, height, ...rest } = options;
+ if (width === undefined || height === undefined) return handle;
+ handle.style.d = path(width, height);
+ handle.attr(rest);
+ return handle;
+ };
+}
+
+chart.options({
+ type: 'point',
+ data: {
+ type: 'fetch',
+ value: 'data/penguins.csv',
+ },
+ encode: {
+ color: 'species',
+ x: 'culmen_length_mm',
+ y: 'culmen_depth_mm',
+ },
+ state: {
+ inactive: { stroke: 'gray', opacity: 0.5 },
+ },
+ interaction: {
+ brushHighlight: {
+ maskHandleSize: 30,
+ maskHandleNRender: createPathRender(
+ (width, height) =>
+ `M0,${height / 2}L${width / 2},${-height / 2}L${width},${
+ height / 2
+ },Z`,
+ ),
+ maskHandleERender: createPathRender(
+ (width, height) =>
+ `M${width / 2},0L${(width * 3) / 2},${height / 2}L${
+ width / 2
+ },${height},Z`,
+ ),
+ maskHandleSRender: createPathRender(
+ (width, height) =>
+ `M0,${height / 2}L${width / 2},${(height / 2) * 3}L${width},${
+ height / 2
+ },Z`,
+ ),
+ maskHandleWRender: createPathRender(
+ (width, height) =>
+ `M${width / 2},0L${-width},${height / 2}L${width / 2},${height},Z`,
+ ),
+ maskHandleNWRender: createPathRender(
+ (width, height) =>
+ `M0,0L${width},${height / 2}L${width / 2},${height},Z`,
+ ),
+ maskHandleNERender: createPathRender(
+ (width, height) =>
+ `M0,${height / 2}L${width},0L${width / 2},${height},Z`,
+ ),
+ maskHandleSERender: createPathRender(
+ (width, height) =>
+ `M${width / 2},0L${width},${height}L0,${height / 2},Z`,
+ ),
+ maskHandleSWRender: createPathRender(
+ (width, height) =>
+ `M${width / 2},0L${width},${height / 2}L0,${height},Z`,
+ ),
+ maskHandleNFill: 'blue',
+ maskHandleEFill: 'red',
+ maskHandleSFill: 'green',
+ maskHandleWFill: 'yellow',
+ maskHandleNWFill: 'black',
+ maskHandleNEFill: 'steelblue',
+ maskHandleSEFill: 'pink',
+ maskHandleSWFill: 'orange',
+ },
+ },
+});
+```
diff --git a/site/examples/interaction/interaction/demo/focus-context.ts b/site/examples/interaction/interaction/demo/focus-context.ts
index 1c68454443..11e92f0f15 100644
--- a/site/examples/interaction/interaction/demo/focus-context.ts
+++ b/site/examples/interaction/interaction/demo/focus-context.ts
@@ -36,7 +36,7 @@ const context = new Chart({
paddingLeft: 40,
paddingTop: 0,
paddingBottom: 0,
- height: 80,
+ height: 60,
});
context
@@ -51,10 +51,39 @@ context
.animate(false)
.axis(false)
.interaction('tooltip', false)
- .interaction('brushXHighlight', true);
+ .interaction('brushXHighlight', {
+ series: true,
+ maskOpacity: 0.3,
+ maskFill: '#777',
+ maskHandleWRender: createPathRender((width, height) => ({
+ d: 'M-0.5,31.5c-2.5,0,-4.5,2,-4.5,4.5v30c0,2.5,2,4.5,4.5,4.5V31.5z',
+ transform: `translate(${width / 2}, ${-height / 2})`,
+ })),
+ maskHandleERender: createPathRender((width, height) => ({
+ d: 'M0.5,31.5c2.5,0,4.5,2,4.5,4.5v30c0,2.5,-2,4.5,-4.5,4.5V31.5z',
+ transform: `translate(${width / 2}, ${-height / 2})`,
+ })),
+ maskHandleEFill: '#D3D8E0',
+ maskHandleWFill: '#D3D8E0',
+ });
context.render();
+function createPathRender(compute) {
+ return (group, options, document) => {
+ if (!group.handle) {
+ const path = document.createElement('path');
+ group.handle = path;
+ group.appendChild(group.handle);
+ }
+ const { handle } = group;
+ const { width, height, ...rest } = options;
+ if (width === undefined || height === undefined) return handle;
+ handle.attr({ ...compute(width, height), ...rest });
+ return handle;
+ };
+}
+
// Add event listeners to communicate.
focus.on('brush:filter', (e) => {
const { nativeEvent } = e;
diff --git a/src/interaction/brushHighlight.ts b/src/interaction/brushHighlight.ts
index a09a083f5c..12b2ccf92d 100644
--- a/src/interaction/brushHighlight.ts
+++ b/src/interaction/brushHighlight.ts
@@ -1,6 +1,8 @@
import { DisplayObject, Rect, Path } from '@antv/g';
-import { subObject } from '../utils/helper';
+import { subObject, omitPrefixObject } from '../utils/helper';
import { selectionOf, pixelsOf } from '../utils/scale';
+import { createElement } from '../utils/createElement';
+import { select, Selection } from '../utils/selection';
import {
selectG2Elements,
selectPlotArea,
@@ -35,6 +37,180 @@ function bboxOf(root: DisplayObject) {
return [0, 0, width, height];
}
+function applyStyle(selection: Selection, style: Record) {
+ for (const [key, value] of Object.entries(style)) {
+ selection.style(key, value);
+ }
+}
+
+const ResizableMask = createElement((g) => {
+ const {
+ x,
+ y,
+ width,
+ height,
+ class: className,
+ renders = {},
+ handleSize: size = 10,
+ document,
+ ...style
+ } = g.attributes;
+
+ if (
+ !document ||
+ width === undefined ||
+ height === undefined ||
+ x === undefined ||
+ y === undefined
+ )
+ return;
+
+ const half = size / 2;
+
+ const renderRect = (g, options, document) => {
+ if (!g.handle) {
+ g.handle = document.createElement('rect');
+ g.append(g.handle);
+ }
+ const { handle } = g;
+ handle.attr(options);
+ return handle;
+ };
+
+ const { render: handleNRender = renderRect, ...handleNStyle } = subObject(
+ omitPrefixObject(style, 'handleNW', 'handleNE'),
+ 'handleN',
+ );
+
+ const { render: handleERender = renderRect, ...handleEStyle } = subObject(
+ style,
+ 'handleE',
+ );
+ const { render: handleSRender = renderRect, ...handleSStyle } = subObject(
+ omitPrefixObject(style, 'handleSE', 'handleSW'),
+ 'handleS',
+ );
+ const { render: handleWRender = renderRect, ...handleWStyle } = subObject(
+ style,
+ 'handleW',
+ );
+ const { render: handleNWRender = renderRect, ...handleNWStyle } = subObject(
+ style,
+ 'handleNW',
+ );
+ const { render: handleNERender = renderRect, ...handleNEStyle } = subObject(
+ style,
+ 'handleNE',
+ );
+ const { render: handleSERender = renderRect, ...handleSEStyle } = subObject(
+ style,
+ 'handleSE',
+ );
+ const { render: handleSWRender = renderRect, ...handleSWStyle } = subObject(
+ style,
+ 'handleSW',
+ );
+
+ const renderHandle = (g, renderNode) => {
+ const { id } = g;
+ const { x, y, ...style } = g.attributes;
+ const handle = renderNode(g, { x: 0, y: 0, ...style }, document);
+ handle.id = id;
+ handle.style.draggable = true;
+ };
+
+ const appendHandle = (handleRender) => {
+ return () => {
+ const Node = createElement((g) => renderHandle(g, handleRender));
+ return new Node({});
+ };
+ };
+
+ const container = select(g)
+ .attr('className', className)
+ .style('x', x)
+ .style('y', y)
+ .style('draggable', true);
+
+ container
+ .maybeAppend('selection', 'rect')
+ .style('draggable', true)
+ .style('fill', 'transparent')
+ .call(applyStyle, { width, height, ...omitPrefixObject(style, 'handle') });
+
+ container
+ .maybeAppend('handle-n', appendHandle(handleNRender))
+ .style('x', half)
+ .style('y', -half)
+ .style('width', width - size)
+ .style('height', size)
+ .style('fill', 'transparent')
+ .call(applyStyle, handleNStyle);
+
+ container
+ .maybeAppend('handle-e', appendHandle(handleERender))
+ .style('x', width - half)
+ .style('y', half)
+ .style('width', size)
+ .style('height', height - size)
+ .style('fill', 'transparent')
+ .call(applyStyle, handleEStyle);
+
+ container
+ .maybeAppend('handle-s', appendHandle(handleSRender))
+ .style('x', half)
+ .style('y', height - half)
+ .style('width', width - size)
+ .style('height', size)
+ .style('fill', 'transparent')
+ .call(applyStyle, handleSStyle);
+
+ container
+ .maybeAppend('handle-w', appendHandle(handleWRender))
+ .style('x', -half)
+ .style('y', half)
+ .style('width', size)
+ .style('height', height - size)
+ .style('fill', 'transparent')
+ .call(applyStyle, handleWStyle);
+
+ container
+ .maybeAppend('handle-nw', appendHandle(handleNWRender))
+ .style('x', -half)
+ .style('y', -half)
+ .style('width', size)
+ .style('height', size)
+ .style('fill', 'transparent')
+ .call(applyStyle, handleNWStyle);
+
+ container
+ .maybeAppend('handle-ne', appendHandle(handleNERender))
+ .style('x', width - half)
+ .style('y', -half)
+ .style('width', size)
+ .style('height', size)
+ .style('fill', 'transparent')
+ .call(applyStyle, handleNEStyle);
+
+ container
+ .maybeAppend('handle-se', appendHandle(handleSERender))
+ .style('x', width - half)
+ .style('y', height - half)
+ .style('width', size)
+ .style('height', size)
+ .style('fill', 'transparent')
+ .call(applyStyle, handleSEStyle);
+
+ container
+ .maybeAppend('handle-sw', appendHandle(handleSWRender))
+ .style('x', -half)
+ .style('y', height - half)
+ .style('width', size)
+ .style('height', size)
+ .style('fill', 'transparent')
+ .call(applyStyle, handleSWStyle);
+});
+
export function brush(
root: DisplayObject,
{
@@ -47,6 +223,16 @@ export function brush(
fill = '#777',
fillOpacity = '0.3',
stroke = '#fff',
+ selectedHandles = [
+ 'handle-n',
+ 'handle-e',
+ 'handle-s',
+ 'handle-w',
+ 'handle-nw',
+ 'handle-ne',
+ 'handle-se',
+ 'handle-sw',
+ ],
...style
}: Record,
) {
@@ -55,7 +241,7 @@ export function brush(
let moveStart = null; // Start point of moving mask.
let mask = null; // Mask instance.
let background = null;
- let resizing = false;
+ let creating = false;
const [originX, originY, width, height] = extent;
@@ -81,7 +267,7 @@ export function brush(
pointerEvents: 'none',
},
});
- mask = new Rect({
+ mask = new ResizableMask({
// @ts-ignore
style: {
x: 0,
@@ -89,6 +275,7 @@ export function brush(
width: 0,
height: 0,
draggable: true,
+ document: root.ownerDocument,
},
className: 'mask',
});
@@ -97,9 +284,10 @@ export function brush(
};
const initNormalMask = () => {
- mask = new Rect({
+ mask = new ResizableMask({
// @ts-ignore
style: {
+ document: root.ownerDocument,
x: 0,
y: 0,
...style,
@@ -120,7 +308,7 @@ export function brush(
start = null;
end = null;
moveStart = null;
- resizing = false;
+ creating = false;
mask = null;
background = null;
brushended();
@@ -176,29 +364,69 @@ export function brush(
updateMask(currentStart, currentEnd);
};
+ const handles = {
+ 'handle-n': { vector: [0, 1, 0, 0], cursor: 'ns-resize' },
+ 'handle-e': { vector: [0, 0, 1, 0], cursor: 'ew-resize' },
+ 'handle-s': { vector: [0, 0, 0, 1], cursor: 'ns-resize' },
+ 'handle-w': { vector: [1, 0, 0, 0], cursor: 'ew-resize' },
+ 'handle-nw': { vector: [1, 1, 0, 0], cursor: 'nwse-resize' },
+ 'handle-ne': { vector: [0, 1, 1, 0], cursor: 'nesw-resize' },
+ 'handle-se': { vector: [0, 0, 1, 1], cursor: 'nwse-resize' },
+ 'handle-sw': { vector: [1, 0, 0, 1], cursor: 'nesw-resize' },
+ };
+
+ const isMask = (target) => {
+ return isSelection(target) || isHandle(target);
+ };
+
+ const isHandle = (target) => {
+ const { id } = target;
+ if (selectedHandles.indexOf(id) === -1) return false;
+ return new Set(Object.keys(handles)).has(id);
+ };
+
+ const isSelection = (target) => {
+ return target === mask.getElementById('selection');
+ };
+
// If target is plot area, create mask.
// If target is mask, about to update position.
const dragstart = (event) => {
const { target } = event;
const [offsetX, offsetY] = brushMousePosition(root, event);
- if (target === mask) {
- moveStart = [offsetX, offsetY];
+ if (!mask || !isMask(target)) {
+ initMask(offsetX, offsetY);
+ creating = true;
return;
}
- initMask(offsetX, offsetY);
- resizing = true;
+ if (isMask(target)) {
+ moveStart = [offsetX, offsetY];
+ }
};
- // If target is plot area, resize mask.
- // If target is mask, move mask.
const drag = (event) => {
+ const { target } = event;
const mouse = brushMousePosition(root, event);
if (!start) return;
- if (moveStart) return moveMask(mouse);
- updateMask(start, mouse);
+ // If target is plot area, resize mask.
+ if (!moveStart) return updateMask(start, mouse);
+
+ // If target is selection area, move mask.
+ if (isSelection(target)) return moveMask(mouse);
+
+ // If target is handle area, resize mask.
+ const [dx, dy] = [mouse[0] - moveStart[0], mouse[1] - moveStart[1]];
+ const { id } = target;
+ if (handles[id]) {
+ const [sx, sy, ex, ey] = handles[id].vector;
+ return updateMask(
+ [start[0] + dx * sx, start[1] + dy * sy],
+ [end[0] + dx * ex, end[1] + dy * ey],
+ );
+ }
};
- // If target is plot area, finish resizing.
+ // If target is plot area, finish creating.
// If target is mask, finish moving mask.
const dragend = (event) => {
if (moveStart) {
@@ -211,21 +439,22 @@ export function brush(
}
end = brushMousePosition(root, event);
const [fx, fy, fx1, fy1] = updateMask(start, end);
- resizing = false;
+ creating = false;
brushcreated(fx, fy, fx1, fy1, event);
};
// Hide mask.
const click = (event) => {
const { target } = event;
- if (mask && target !== mask) removeMask();
+ if (mask && !isMask(target)) removeMask();
};
// Update cursor depends on hovered element.
const pointermove = (event) => {
const { target } = event;
- if (target === mask && !resizing) setCursor(root, 'move');
- else setCursor(root, 'crosshair');
+ if (!mask || !isMask(target) || creating) setCursor(root, 'crosshair');
+ else if (isSelection(target)) setCursor(root, 'move');
+ else if (isHandle(target)) setCursor(root, handles[target.id].cursor);
};
const pointerleave = () => {
@@ -251,6 +480,8 @@ export function brush(
if (mask) removeMask();
},
destroy() {
+ removeMask();
+ setCursor(root, 'default');
root.removeEventListener('dragstart', dragstart);
root.removeEventListener('drag', drag);
root.removeEventListener('dragend', dragend);
@@ -291,6 +522,7 @@ export function brushHighlight(
root,
{
elements: elementof,
+ selectedHandles,
siblings: siblingsof = (root) => [],
datum,
brushRegion,
@@ -393,6 +625,7 @@ export function brushHighlight(
extent,
brushRegion,
reverse,
+ selectedHandles,
brushended: () => {
const handler = series ? seriesBrushend : brushended;
emitter.emit('brush:end', { nativeEvent: true });
@@ -423,9 +656,11 @@ export function brushHighlight(
emitter.on('brush:remove', onRemove);
// Remove event handlers.
+ const preBrushDestroy = brushHandler.destroy.bind(brushHandler);
brushHandler.destroy = () => {
emitter.off('brush:highlight', onHighlight);
emitter.off('brush:remove', onRemove);
+ preBrushDestroy();
};
return brushHandler;
@@ -464,6 +699,7 @@ export function BrushHighlight({ facet, brushKey, ...rest }) {
emitter,
scale,
coordinate,
+ selectedHandles: undefined,
...defaultOptions,
...rest,
});
@@ -488,6 +724,7 @@ export function BrushHighlight({ facet, brushKey, ...rest }) {
emitter,
scale,
coordinate,
+ selectedHandles: undefined,
...defaultOptions,
...rest,
});
diff --git a/src/interaction/brushXHighlight.ts b/src/interaction/brushXHighlight.ts
index f81aa489ce..a8f4610581 100644
--- a/src/interaction/brushXHighlight.ts
+++ b/src/interaction/brushXHighlight.ts
@@ -9,5 +9,6 @@ export function BrushXHighlight(options) {
return BrushHighlight({
...options,
brushRegion: brushXRegion,
+ selectedHandles: ['handle-e', 'handle-w'],
});
}
diff --git a/src/interaction/brushYHighlight.ts b/src/interaction/brushYHighlight.ts
index 691f017cea..d8ee8c1ef6 100644
--- a/src/interaction/brushYHighlight.ts
+++ b/src/interaction/brushYHighlight.ts
@@ -9,5 +9,6 @@ export function BrushYHighlight(options) {
return BrushHighlight({
...options,
brushRegion: brushYRegion,
+ selectedHandles: ['handle-n', 'handle-s'],
});
}
diff --git a/src/shape/area/curve.ts b/src/shape/area/curve.ts
index 3660d5cd9b..f7401ce203 100644
--- a/src/shape/area/curve.ts
+++ b/src/shape/area/curve.ts
@@ -11,7 +11,7 @@ import {
getTransform,
} from '../utils';
import { subObject } from '../../utils/helper';
-import { createElement } from '../createElement';
+import { createElement } from '../../utils/createElement';
const DoubleArea = createElement((g) => {
const { areaPath, connectPath, areaStyle, connectStyle } = g.attributes;
diff --git a/src/shape/connector/connector.ts b/src/shape/connector/connector.ts
index bca678a404..3526ebe957 100644
--- a/src/shape/connector/connector.ts
+++ b/src/shape/connector/connector.ts
@@ -4,7 +4,7 @@ import { PathStyleProps } from '@antv/g';
import { Marker } from '@antv/gui';
import { line as d3line } from 'd3-shape';
import { ShapeComponent as SC, Vector2, WithPrefix } from '../../runtime';
-import { createElement } from '../../shape/createElement';
+import { createElement } from '../../utils/createElement';
import { isTranspose } from '../../utils/coordinate';
import { subObject } from '../../utils/helper';
import { select } from '../../utils/selection';
diff --git a/src/shape/line/curve.ts b/src/shape/line/curve.ts
index ba3fce2468..a06648beda 100644
--- a/src/shape/line/curve.ts
+++ b/src/shape/line/curve.ts
@@ -10,7 +10,7 @@ import {
getShapeTheme,
getTransform,
} from '../utils';
-import { createElement } from '../createElement';
+import { createElement } from '../../utils/createElement';
import { subObject } from '../../utils/helper';
import { angleWithQuadrant, dist, sub } from '../../utils/vector';
diff --git a/src/shape/text/advance.ts b/src/shape/text/advance.ts
index 51f0334c4e..1c42fc0f56 100644
--- a/src/shape/text/advance.ts
+++ b/src/shape/text/advance.ts
@@ -10,7 +10,7 @@ import {
import { Marker } from '@antv/gui';
import { line } from 'd3-shape';
import { WithPrefix } from '../../runtime';
-import { createElement } from '../createElement';
+import { createElement } from '../../utils/createElement';
import { applyStyle } from '../utils';
import { subObject } from '../../utils/helper';
import { select } from '../../utils/selection';
diff --git a/src/shape/text/badge.ts b/src/shape/text/badge.ts
index 445ccc08b2..8293ba3856 100644
--- a/src/shape/text/badge.ts
+++ b/src/shape/text/badge.ts
@@ -1,7 +1,7 @@
import { TextStyleProps, DisplayObject } from '@antv/g';
import { Marker } from '@antv/gui';
import { ShapeComponent as SC, WithPrefix } from '../../runtime';
-import { createElement } from '../../shape/createElement';
+import { createElement } from '../../utils/createElement';
import { subObject } from '../../utils/helper';
import { select } from '../../utils/selection';
import { applyStyle, getShapeTheme } from '../../shape/utils';
diff --git a/src/shape/createElement.ts b/src/utils/createElement.ts
similarity index 94%
rename from src/shape/createElement.ts
rename to src/utils/createElement.ts
index c0855a65bc..d506f86c83 100644
--- a/src/shape/createElement.ts
+++ b/src/utils/createElement.ts
@@ -6,7 +6,7 @@ export type ElementDescriptor = {
export function createElement>(
descriptor: ElementDescriptor | ElementDescriptor['render'],
-): new () => DisplayObject {
+): new (T?) => DisplayObject {
const render =
typeof descriptor === 'function' ? descriptor : descriptor.render;
return class extends CustomElement {
diff --git a/src/utils/helper.ts b/src/utils/helper.ts
index 3a71ed4d48..aa4cdc53d3 100644
--- a/src/utils/helper.ts
+++ b/src/utils/helper.ts
@@ -116,6 +116,17 @@ export function filterPrefixObject(
);
}
+export function omitPrefixObject(
+ obj: Record,
+ ...prefixes: string[]
+) {
+ return Object.fromEntries(
+ Object.entries(obj).filter(([key]) =>
+ prefixes.every((prefix) => !key.startsWith(prefix)),
+ ),
+ );
+}
+
export function maybePercentage(x: number | string, size: number) {
if (x === undefined) return null;
if (typeof x === 'number') return x;