diff --git a/__tests__/unit/api/interaction.spec.ts b/__tests__/unit/api/interaction.spec.ts index b0bcc15186..a30cb9ef55 100644 --- a/__tests__/unit/api/interaction.spec.ts +++ b/__tests__/unit/api/interaction.spec.ts @@ -89,3 +89,97 @@ describe('Clear EventEmitter', () => { expect(emitter?.getEvents()['legend:filter']).toBeUndefined(); }); }); + +describe('BrushXFilter', () => { + it('should not change Y scale domain when filtering X axis.', async () => { + // @see https://github.com/antvis/G2/issues/7272 + const chart = new Chart({ + canvas: createNodeGCanvas(640, 480), + }); + + chart.options({ + type: 'point', + data: [ + { x: 1, y: 10 }, + { x: 2, y: 50 }, + { x: 3, y: 30 }, + { x: 4, y: 80 }, + { x: 5, y: 20 }, + { x: 6, y: 60 }, + { x: 7, y: 40 }, + { x: 8, y: 90 }, + { x: 9, y: 15 }, + { x: 10, y: 70 }, + ], + encode: { x: 'x', y: 'y' }, + interaction: { brushXFilter: true }, + }); + + await chart.render(); + + const initialYDomain = chart.getScale().y.getOptions().domain; + + // Emit brush:filter with an X selection to simulate brushXFilter. + const { emitter } = chart.getContext(); + const xSelection = [3, 8]; + emitter.emit('brush:filter', { + nativeEvent: false, + data: { selection: [xSelection, initialYDomain] }, + }); + + // Wait for update to complete. + await new Promise((resolve) => setTimeout(resolve, 100)); + + const filteredYDomain = chart.getScale().y.getOptions().domain; + + // Y scale domain should remain unchanged after brushXFilter. + expect(filteredYDomain).toEqual(initialYDomain); + }); +}); + +describe('BrushYFilter', () => { + it('should not change X scale domain when filtering Y axis.', async () => { + // @see https://github.com/antvis/G2/issues/7272 + const chart = new Chart({ + canvas: createNodeGCanvas(640, 480), + }); + + chart.options({ + type: 'point', + data: [ + { x: 1, y: 10 }, + { x: 2, y: 50 }, + { x: 3, y: 30 }, + { x: 4, y: 80 }, + { x: 5, y: 20 }, + { x: 6, y: 60 }, + { x: 7, y: 40 }, + { x: 8, y: 90 }, + { x: 9, y: 15 }, + { x: 10, y: 70 }, + ], + encode: { x: 'x', y: 'y' }, + interaction: { brushYFilter: true }, + }); + + await chart.render(); + + const initialXDomain = chart.getScale().x.getOptions().domain; + + // Emit brush:filter with a Y selection to simulate brushYFilter. + const { emitter } = chart.getContext(); + const ySelection = [20, 70]; + emitter.emit('brush:filter', { + nativeEvent: false, + data: { selection: [initialXDomain, ySelection] }, + }); + + // Wait for update to complete. + await new Promise((resolve) => setTimeout(resolve, 100)); + + const filteredXDomain = chart.getScale().x.getOptions().domain; + + // X scale domain should remain unchanged after brushYFilter. + expect(filteredXDomain).toEqual(initialXDomain); + }); +}); diff --git a/src/interaction/brushFilter.ts b/src/interaction/brushFilter.ts index 91ca18043e..837d1040e4 100644 --- a/src/interaction/brushFilter.ts +++ b/src/interaction/brushFilter.ts @@ -81,7 +81,13 @@ export function brushFilter( }; } -export function BrushFilter({ hideX = true, hideY = true, ...rest }) { +export function BrushFilter({ + hideX = true, + hideY = true, + filterX = true, + filterY = true, + ...rest +}) { return (target, viewInstances, emitter) => { const { container, view, options: viewOptions, update, setState } = target; const plotArea = selectPlotArea(container); @@ -112,6 +118,18 @@ export function BrushFilter({ hideX = true, hideY = true, ...rest }) { // Update the domain of x and y scale to filter data. const [domainX, domainY] = selection; + // Capture the current domains of the non-filtered axes from the view + // so they can be explicitly preserved (avoiding re-inference changes). + const { scale: currentScale } = newView; + const preservedDomainX = + !filterX && currentScale.x + ? currentScale.x.getOptions().domain + : null; + const preservedDomainY = + !filterY && currentScale.y + ? currentScale.y.getOptions().domain + : null; + setState('brushFilter', (options) => { const { marks } = options; const newMarks = marks.map((mark) => @@ -126,9 +144,17 @@ export function BrushFilter({ hideX = true, hideY = true, ...rest }) { mark, { // Set nice to false to avoid modify domain. + // For filtered axes: use the brush selection domain. + // For non-filtered axes: explicitly preserve the current domain. scale: { - x: { domain: domainX, nice: false }, - y: { domain: domainY, nice: false }, + ...(filterX && { x: { domain: domainX, nice: false } }), + ...(filterY && { y: { domain: domainY, nice: false } }), + ...(preservedDomainX && { + x: { domain: preservedDomainX, nice: false }, + }), + ...(preservedDomainY && { + y: { domain: preservedDomainY, nice: false }, + }), }, }, ), diff --git a/src/interaction/brushXFilter.ts b/src/interaction/brushXFilter.ts index fff1bb4368..58bf1d8897 100644 --- a/src/interaction/brushXFilter.ts +++ b/src/interaction/brushXFilter.ts @@ -4,6 +4,9 @@ import { brushXRegion } from './brushXHighlight'; export function BrushXFilter(options) { return BrushFilter({ hideX: true, + hideY: false, + filterX: true, + filterY: false, ...options, brushRegion: brushXRegion, }); diff --git a/src/interaction/brushYFilter.ts b/src/interaction/brushYFilter.ts index 5dbb1c0053..c33ceee7f0 100644 --- a/src/interaction/brushYFilter.ts +++ b/src/interaction/brushYFilter.ts @@ -3,7 +3,10 @@ import { brushYRegion } from './brushYHighlight'; export function BrushYFilter(options) { return BrushFilter({ + hideX: false, hideY: true, + filterX: false, + filterY: true, ...options, brushRegion: brushYRegion, });