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` 设置宽度。 + +custom-style + +```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; +} +``` + +custom-brush + +```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;