diff --git a/__tests__/integration/chart-on-brush-filter.spec.ts b/__tests__/integration/chart-on-brush-filter.spec.ts new file mode 100644 index 0000000000..8ecc9ff21e --- /dev/null +++ b/__tests__/integration/chart-on-brush-filter.spec.ts @@ -0,0 +1,52 @@ +import { chartOnBrushFilter as render } from '../plots/api/chart-on-brush-filter'; +import { PLOT_CLASS_NAME } from '../../src'; +import { dblclick, brush } from '../plots/interaction/penguins-point-brush'; +import { createDOMGCanvas } from './utils/createDOMGCanvas'; +import { createPromise, receiveExpectData } from './utils/event'; +import './utils/useCustomFetch'; + +describe('chart.on', () => { + const canvas = createDOMGCanvas(640, 480); + const { finished, chart } = render({ canvas }); + + chart.off(); + + it('chart.on("element:filter", callback) should provide selection when filtering', async () => { + await finished; + const { document } = canvas; + const plot = document.getElementsByClassName(PLOT_CLASS_NAME)[0]; + + // Brush plot. + const [filtered, resolve] = createPromise(); + chart.on( + 'brush:filter', + receiveExpectData(resolve, { + selection: [ + [34.99184225303586, 44.72635552737214], + [15.877014192597635, 20.13017874955966], + ], + }), + ); + brush(plot, 100, 100, 300, 300); + await filtered; + + // Reset plot. + const [rested, resolve1] = createPromise(); + chart.off(); + chart.on( + 'brush:filter', + receiveExpectData(resolve1, { + selection: [ + [32.1, 59.6], + [13.1, 21.5], + ], + }), + ); + setTimeout(() => dblclick(plot), 1000); + await rested; + }); + + afterAll(() => { + canvas?.destroy(); + }); +}); diff --git a/__tests__/integration/chart-on-series-element.spec.ts b/__tests__/integration/chart-on-series-element.spec.ts index 724d8c1a4e..372f4254e2 100644 --- a/__tests__/integration/chart-on-series-element.spec.ts +++ b/__tests__/integration/chart-on-series-element.spec.ts @@ -3,68 +3,70 @@ import { createDOMGCanvas } from './utils/createDOMGCanvas'; import { dispatchEvent, createPromise, receiveExpectData } from './utils/event'; import './utils/useSnapshotMatchers'; -const data = [ - { - month: 'Jan', - city: 'Tokyo', - temperature: 7, - }, - { - month: 'Feb', - city: 'Tokyo', - temperature: 6.9, - }, - { - month: 'Mar', - city: 'Tokyo', - temperature: 9.5, - }, - { - month: 'Apr', - city: 'Tokyo', - temperature: 14.5, - }, - { - month: 'May', - city: 'Tokyo', - temperature: 18.4, - }, - { - month: 'Jun', - city: 'Tokyo', - temperature: 21.5, - }, - { - month: 'Jul', - city: 'Tokyo', - temperature: 25.2, - }, - { - month: 'Aug', - city: 'Tokyo', - temperature: 26.5, - }, - { - month: 'Sep', - city: 'Tokyo', - temperature: 23.3, - }, - { - month: 'Oct', - city: 'Tokyo', - temperature: 18.3, - }, - { - month: 'Nov', - city: 'Tokyo', - temperature: 13.9, - }, - { - month: 'Dec', - city: 'Tokyo', - temperature: 9.6, - }, -]; +const data = { + data: [ + { + month: 'Jan', + city: 'Tokyo', + temperature: 7, + }, + { + month: 'Feb', + city: 'Tokyo', + temperature: 6.9, + }, + { + month: 'Mar', + city: 'Tokyo', + temperature: 9.5, + }, + { + month: 'Apr', + city: 'Tokyo', + temperature: 14.5, + }, + { + month: 'May', + city: 'Tokyo', + temperature: 18.4, + }, + { + month: 'Jun', + city: 'Tokyo', + temperature: 21.5, + }, + { + month: 'Jul', + city: 'Tokyo', + temperature: 25.2, + }, + { + month: 'Aug', + city: 'Tokyo', + temperature: 26.5, + }, + { + month: 'Sep', + city: 'Tokyo', + temperature: 23.3, + }, + { + month: 'Oct', + city: 'Tokyo', + temperature: 18.3, + }, + { + month: 'Nov', + city: 'Tokyo', + temperature: 13.9, + }, + { + month: 'Dec', + city: 'Tokyo', + temperature: 9.6, + }, + ], +}; describe('chart.on', () => { const canvas = createDOMGCanvas(640, 480); diff --git a/__tests__/integration/utils/event.ts b/__tests__/integration/utils/event.ts index 51ff6674d7..55f0dd541c 100644 --- a/__tests__/integration/utils/event.ts +++ b/__tests__/integration/utils/event.ts @@ -9,12 +9,14 @@ export function createPromise() { export function receiveExpectData( resolve, data: any = { - genre: 'Sports', - sold: 275, + data: { + genre: 'Sports', + sold: 275, + }, }, ) { return (event) => { - expect(event.data.data).toEqual(data); + expect(event.data).toEqual(data); resolve(); }; } diff --git a/__tests__/plots/api/chart-on-brush-filter.ts b/__tests__/plots/api/chart-on-brush-filter.ts new file mode 100644 index 0000000000..bb6ed63a6f --- /dev/null +++ b/__tests__/plots/api/chart-on-brush-filter.ts @@ -0,0 +1,33 @@ +import { Chart } from '../../../src'; + +export function chartOnBrushFilter(context) { + const { container, canvas } = context; + + const chart = new Chart({ + theme: 'classic', + container, + canvas, + }); + + chart.options({ + type: 'point', + data: { + type: 'fetch', + value: 'data/penguins.csv', + }, + encode: { + color: 'species', + x: 'culmen_length_mm', + y: 'culmen_depth_mm', + }, + interaction: { brushFilter: true }, + }); + + chart.on('brush:filter', (event) => { + console.log(event.data.selection); + }); + + const finished = chart.render(); + + return { chart, finished }; +} diff --git a/__tests__/plots/api/index.ts b/__tests__/plots/api/index.ts index 8e2e4900ae..e5fe6ee66f 100644 --- a/__tests__/plots/api/index.ts +++ b/__tests__/plots/api/index.ts @@ -14,3 +14,4 @@ export { chartEmitLegendFilter } from './chart-emit-legend-filter'; export { chartChangeSizePolar } from './chart-change-size-polar'; export { chartChangeDataFacet } from './chart-change-data-facet'; export { chartRenderClearAnimation } from './chart-render-clear-animation'; +export { chartOnBrushFilter } from './chart-on-brush-filter'; diff --git a/site/docs/spec/interaction/brushFilter.zh.md b/site/docs/spec/interaction/brushFilter.zh.md index c9c7325df3..9ffc406d19 100644 --- a/site/docs/spec/interaction/brushFilter.zh.md +++ b/site/docs/spec/interaction/brushFilter.zh.md @@ -38,3 +38,17 @@ chart.render(); | ------------------- | -------------- | ------------------------------ | ------ | | reverse | brush 是否反转 | `boolean` | false | | `mask${StyleAttrs}` | brush 的样式 | `number \| string` | - | + +## 案例 + +获得当前筛选数据,会在每次筛选和重置的时候触发以下事件: + +```js +chart.on('brush:filter', (event) => { + const { selection } = event.data; + const [domainX, domainY] = selection; + const [minX, maxX] = domainX; + const [minY, maxY] = domainY; + console.log(minX, maxX, minY, maxY); +}); +``` diff --git a/src/interaction/brushFilter.ts b/src/interaction/brushFilter.ts index d6fc49b4b4..4ff90592fe 100644 --- a/src/interaction/brushFilter.ts +++ b/src/interaction/brushFilter.ts @@ -48,14 +48,14 @@ export function brushFilter( root.addEventListener('click', click); // Filter when brush created. - function brushcreated(x, y, x1, y1) { - filter(x, y, x1, y1); + function brushcreated(x, y, x1, y1, event) { + filter(x, y, x1, y1, event); brush.remove(); } // Reset when dblclick. function click(e) { - if (isDblclick(e)) reset(); + if (isDblclick(e)) reset(e); } return () => { @@ -65,7 +65,7 @@ export function brushFilter( } export function BrushFilter({ hideX = true, hideY = true, ...rest }) { - return (target, viewInstances) => { + return (target, viewInstances, emitter) => { const { container, view, options: viewOptions, update } = target; const plotArea = selectPlotArea(container); const defaultOptions = { @@ -82,7 +82,7 @@ export function BrushFilter({ hideX = true, hideY = true, ...rest }) { return brushFilter(plotArea, { brushRegion: (x, y, x1, y1) => [x, y, x1, y1], - filter: async (x, y, x1, y1) => { + filter: async (x, y, x1, y1, event) => { // Avoid redundant filter. if (filtering) return; filtering = true; @@ -99,6 +99,8 @@ export function BrushFilter({ hideX = true, hideY = true, ...rest }) { // Update the domain of x and y scale to filter data. const { marks } = viewOptions; + const domainX = domainOf(scaleX, [p0[0], p1[0]]); + const domainY = domainOf(scaleY, [p0[1], p1[1]]); const newMarks = marks.map((mark) => deepMix( { @@ -111,13 +113,18 @@ export function BrushFilter({ hideX = true, hideY = true, ...rest }) { mark, { scale: { - x: { domain: domainOf(scaleX, [p0[0], p1[0]]) }, - y: { domain: domainOf(scaleY, [p0[1], p1[1]]) }, + x: { domain: domainX }, + y: { domain: domainY }, }, }, ), ); + // Emit event. + event.data = event.data || {}; + event.data.selection = [domainX, domainY]; + emitter.emit('brush:filter', event); + // Rerender and update view. const newOptions = { ...viewOptions, @@ -129,8 +136,18 @@ export function BrushFilter({ hideX = true, hideY = true, ...rest }) { filtering = false; filtered = true; }, - reset: () => { + reset: (event) => { if (filtering || !filtered) return; + + // Emit event. + const { scale } = view; + const { x: scaleX, y: scaleY } = scale; + const domainX = scaleX.getOptions().domain; + const domainY = scaleY.getOptions().domain; + event.data = event.data || {}; + event.data.selection = [domainX, domainY]; + emitter.emit('brush:filter', event); + filtered = false; newView = view; update(viewOptions); diff --git a/src/interaction/brushHighlight.ts b/src/interaction/brushHighlight.ts index 97a33fb10e..3b0c126596 100644 --- a/src/interaction/brushHighlight.ts +++ b/src/interaction/brushHighlight.ts @@ -211,7 +211,7 @@ export function brush( end = brushMousePosition(root, event); const [fx, fy, fx1, fy1] = updateMask(start, end); resizing = false; - brushcreated(fx, fy, fx1, fy1); + brushcreated(fx, fy, fx1, fy1, event); }; // Hide mask.