@@ -63,14 +63,14 @@ export class Brush extends Mark {
6363 defaults
6464 ) ;
6565 this . _dimension = keyword ( dimension , "dimension" , [ "x" , "y" , "xy" ] ) ;
66- this . _brush = this . _dimension === "x" ? d3BrushX ( ) : this . _dimension === "y" ? d3BrushY ( ) : d3Brush ( ) ;
6766 this . _interval = interval == null ? null : maybeInterval ( interval ) ;
67+ this . _sync = sync ;
68+ this . _states = [ ] ; // per-plot state: {brush, nodes, applyX, applyY, svg}
69+ this . _syncing = false ;
6870 const channelDefaults = { x, y, z, fx : this . fx , fy : this . fy } ;
6971 this . inactive = renderFilter ( true , channelDefaults ) ;
7072 this . context = renderFilter ( false , channelDefaults ) ;
7173 this . focus = renderFilter ( false , channelDefaults ) ;
72- this . _brushNodes = [ ] ;
73- this . _sync = sync ;
7474 }
7575 render ( index , scales , values , dimensions , context ) {
7676 if ( typeof document === "undefined" ) return null ;
@@ -79,19 +79,27 @@ export class Brush extends Mark {
7979 const Y = values . channels ?. y ?. value ;
8080 const FX = values . channels ?. fx ?. value ;
8181 const FY = values . channels ?. fy ?. value ;
82- const { data, _brush, _brushNodes, inactive, context : ctx , focus} = this ;
83- let target , currentNode , syncing ;
82+ const { inactive, context : ctx , focus, _states} = this ;
8483
85- if ( ! index ?. fi ) {
84+ // Per-plot state; context.interaction is fresh for each plot.
85+ let state = context . interaction . brush ;
86+ if ( state ) {
87+ if ( state . mark !== this ) throw new Error ( "only one brush per plot" ) ;
88+ } else {
8689 const dim = this . _dimension ;
8790 const interval = this . _interval ;
8891 if ( context . projection && dim !== "xy" ) throw new Error ( `brush${ dim . toUpperCase ( ) } does not support projections` ) ;
8992 const invertX = precisionInvert ( x , context . projection ) ;
9093 const invertY = precisionInvert ( y , context . projection ) ;
91- const applyX = ( this . _applyX = ( ! context . projection && x ) || ( ( d ) => d ) ) ;
92- const applyY = ( this . _applyY = ( ! context . projection && y ) || ( ( d ) => d ) ) ;
94+ const applyX = ( ! context . projection && x ) || ( ( d ) => d ) ;
95+ const applyY = ( ! context . projection && y ) || ( ( d ) => d ) ;
96+ const brush = dim === "x" ? d3BrushX ( ) : dim === "y" ? d3BrushY ( ) : d3Brush ( ) ;
97+ const nodes = [ ] ;
98+ context . interaction . brush = state = { mark : this , brush, nodes, applyX, applyY, svg : context . ownerSVGElement } ;
99+ _states . push ( state ) ;
93100 context . dispatchValue ( null ) ;
94101 const sync = this . _sync ;
102+ const { data} = this ;
95103 const filterData =
96104 data != null &&
97105 ( ( region ) =>
@@ -105,8 +113,10 @@ export class Brush extends Mark {
105113 )
106114 )
107115 ) ) ;
108- let snapping ;
109- _brush
116+ const self = this ;
117+ let target , currentNode , snapping ;
118+
119+ brush
110120 . extent ( [
111121 [ dimensions . marginLeft - ( dim !== "y" ) , dimensions . marginTop - ( dim !== "x" ) ] ,
112122 [
@@ -115,42 +125,56 @@ export class Brush extends Mark {
115125 ]
116126 ] )
117127 . on ( "start brush end" , function ( event ) {
118- if ( syncing ) return ;
128+ if ( self . _syncing ) return ;
119129 const { selection, type} = event ;
120130 if ( type === "start" && ! snapping ) {
121131 target = event . sourceEvent ?. currentTarget ?? this ;
122- currentNode = _brushNodes . indexOf ( target ) ;
132+ currentNode = nodes . indexOf ( target ) ;
133+ // Clear other facets within this plot
123134 if ( ! sync ) {
124- syncing = true ;
125- selectAll ( _brushNodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( _brush . move , null ) ;
126- syncing = false ;
135+ self . _syncing = true ;
136+ selectAll ( nodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( brush . move , null ) ;
137+ self . _syncing = false ;
127138 }
128- for ( let i = 0 ; i < _brushNodes . length ; ++ i ) {
129- inactive . update ( false , i ) ;
130- ctx . update ( true , i ) ;
131- focus . update ( false , i ) ;
139+ for ( const p of _states ) {
140+ inactive . update ( false , p ) ;
141+ ctx . update ( true , p ) ;
142+ focus . update ( false , p ) ;
132143 }
133144 }
134145
135146 if ( selection === null ) {
136147 if ( type === "end" ) {
137148 context . interaction . brushing = false ;
138149 if ( sync ) {
139- syncing = true ;
140- selectAll ( _brushNodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( _brush . move , null ) ;
141- syncing = false ;
150+ self . _syncing = true ;
151+ selectAll ( nodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( brush . move , null ) ;
152+ self . _syncing = false ;
153+ }
154+ // Clear all other plots
155+ self . _syncing = true ;
156+ for ( const p of _states ) {
157+ if ( p === state ) continue ;
158+ selectAll ( p . nodes ) . call ( p . brush . move , null ) ;
142159 }
143- for ( let i = 0 ; i < _brushNodes . length ; ++ i ) {
144- inactive . update ( true , i ) ;
145- ctx . update ( false , i ) ;
146- focus . update ( false , i ) ;
160+ self . _syncing = false ;
161+ for ( const p of _states ) {
162+ inactive . update ( true , p ) ;
163+ ctx . update ( false , p ) ;
164+ focus . update ( false , p ) ;
147165 }
148166 context . dispatchValue ( null ) ;
149167 } else {
150- for ( let i = sync ? 0 : currentNode , n = sync ? _brushNodes . length : currentNode + 1 ; i < n ; ++ i ) {
151- inactive . update ( false , i ) ;
152- ctx . update ( true , i ) ;
153- focus . update ( false , i ) ;
168+ if ( sync ) {
169+ for ( const p of _states ) {
170+ inactive . update ( false , p ) ;
171+ ctx . update ( true , p ) ;
172+ focus . update ( false , p ) ;
173+ }
174+ } else {
175+ inactive . update ( false , state , currentNode ) ;
176+ ctx . update ( true , state , currentNode ) ;
177+ focus . update ( false , state , currentNode ) ;
154178 }
155179 let value = null ;
156180 if ( event . sourceEvent ) {
@@ -175,14 +199,20 @@ export class Brush extends Mark {
175199 const inX = dim !== "y" && ( ( xi ) => px1 <= xi && xi < px2 ) ;
176200 const inY = dim !== "x" && ( ( yi ) => py1 <= yi && yi < py2 ) ;
177201 if ( sync ) {
178- syncing = true ;
179- selectAll ( _brushNodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( _brush . move , selection ) ;
180- syncing = false ;
202+ self . _syncing = true ;
203+ selectAll ( nodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( brush . move , selection ) ;
204+ self . _syncing = false ;
181205 }
182- for ( let i = sync ? 0 : currentNode , n = sync ? _brushNodes . length : currentNode + 1 ; i < n ; ++ i ) {
183- inactive . update ( false , i ) ;
184- ctx . update ( ! inX ? ( _ , yi ) => ! inY ( yi ) : ! inY ? ( xi ) => ! inX ( xi ) : ( xi , yi ) => ! ( inX ( xi ) && inY ( yi ) ) , i ) ;
185- focus . update ( ! inX ? ( _ , yi ) => inY ( yi ) : ! inY ? inX : ( xi , yi ) => inX ( xi ) && inY ( yi ) , i ) ;
206+ const ctxTest = ! inX ? ( _ , yi ) => ! inY ( yi ) : ! inY ? ( xi ) => ! inX ( xi ) : ( xi , yi ) => ! ( inX ( xi ) && inY ( yi ) ) ;
207+ const focusTest = ! inX ? ( _ , yi ) => inY ( yi ) : ! inY ? inX : ( xi , yi ) => inX ( xi ) && inY ( yi ) ;
208+ if ( sync ) {
209+ inactive . update ( false , state ) ;
210+ ctx . update ( ctxTest , state ) ;
211+ focus . update ( focusTest , state ) ;
212+ } else {
213+ inactive . update ( false , state , currentNode ) ;
214+ ctx . update ( ctxTest , state , currentNode ) ;
215+ focus . update ( focusTest , state , currentNode ) ;
186216 }
187217
188218 const [ x1 , x2 ] = invertX && [ invertX ( px1 ) , invertX ( px2 ) ] . sort ( ascending ) ;
@@ -195,7 +225,7 @@ export class Brush extends Mark {
195225 let r2 = intervalRound ( interval , s2 ) ;
196226 if ( + r1 === + r2 ) r2 = interval . offset ( r1 ) ;
197227 snapping = true ;
198- select ( this ) . call ( _brush . move , [ r1 , r2 ] . map ( dim === "x" ? applyX : applyY ) . sort ( ascending ) ) ;
228+ select ( this ) . call ( brush . move , [ r1 , r2 ] . map ( dim === "x" ? applyX : applyY ) . sort ( ascending ) ) ;
199229 snapping = false ;
200230 return ;
201231 }
@@ -210,45 +240,78 @@ export class Brush extends Mark {
210240 } ) ;
211241 if ( filterData ) region . data = filterData ( region ) ;
212242 context . dispatchValue ( region ) ;
243+
244+ // Sync other plots in data space
245+ if ( type !== "start" ) {
246+ self . _syncing = true ;
247+ for ( const p of _states ) {
248+ if ( p === state ) continue ;
249+ const [ pX1 , pX2 ] = [ p . applyX ( x1 ) , p . applyX ( x2 ) ] . sort ( ascending ) ;
250+ const [ pY1 , pY2 ] = [ p . applyY ( y1 ) , p . applyY ( y2 ) ] . sort ( ascending ) ;
251+ const selection =
252+ dim === "xy"
253+ ? [
254+ [ pX1 , pY1 ] ,
255+ [ pX2 , pY2 ]
256+ ]
257+ : dim === "x"
258+ ? [ pX1 , pX2 ]
259+ : [ pY1 , pY2 ] ;
260+ selectAll ( p . nodes ) . call ( p . brush . move , selection ) ;
261+ const inXp = dim !== "y" && ( ( xi ) => p . applyX ( x1 ) <= xi && xi < p . applyX ( x2 ) ) ;
262+ const inYp = dim !== "x" && ( ( yi ) => p . applyY ( y1 ) <= yi && yi < p . applyY ( y2 ) ) ;
263+ inactive . update ( false , p ) ;
264+ ctx . update (
265+ ! inXp ? ( _ , yi ) => ! inYp ( yi ) : ! inYp ? ( xi ) => ! inXp ( xi ) : ( xi , yi ) => ! ( inXp ( xi ) && inYp ( yi ) ) ,
266+ p
267+ ) ;
268+ focus . update ( ! inXp ? ( _ , yi ) => inYp ( yi ) : ! inYp ? inXp : ( xi , yi ) => inXp ( xi ) && inYp ( yi ) , p ) ;
269+ }
270+ self . _syncing = false ;
271+ }
213272 }
214273 } ) ;
215274 }
216275
217276 const g = create ( "svg:g" ) . attr ( "aria-label" , this . _dimension === "xy" ? "brush" : `brush-${ this . _dimension } ` ) ;
218- g . call ( this . _brush ) ;
277+ g . call ( state . brush ) ;
219278 const sel = g . select ( ".selection" ) ;
220279 applyAttr ( sel , "fill" , this . fill ) ;
221280 applyAttr ( sel , "fill-opacity" , this . fillOpacity ) ;
222281 applyAttr ( sel , "stroke" , this . stroke ) ;
223282 applyAttr ( sel , "stroke-width" , this . strokeWidth ) ;
224283 applyAttr ( sel , "stroke-opacity" , this . strokeOpacity ) ;
225284 const node = g . node ( ) ;
226- this . _brushNodes . push ( node ) ;
285+ state . nodes . push ( node ) ;
227286 return node ;
228287 }
229288 move ( value ) {
230289 if ( value == null ) {
231- selectAll ( this . _brushNodes ) . call ( this . _brush . move , null ) ;
290+ for ( const { brush, nodes} of this . _states ) {
291+ selectAll ( nodes ) . call ( brush . move , null ) ;
292+ }
232293 return ;
233294 }
295+ const dim = this . _dimension ;
234296 const { x1, x2, y1, y2, fx, fy} = value ;
235- const node = this . _brushNodes . find (
236- ( n ) => ( fx === undefined || n . __data__ ?. x === fx ) && ( fy === undefined || n . __data__ ?. y === fy )
237- ) ;
238- if ( ! node ) return ;
239- const [ px1 , px2 ] = [ x1 , x2 ] . map ( this . _applyX ) . sort ( ascending ) ;
240- const [ py1 , py2 ] = [ y1 , y2 ] . map ( this . _applyY ) . sort ( ascending ) ;
241- select ( node ) . call (
242- this . _brush . move ,
243- this . _dimension === "xy"
244- ? [
245- [ px1 , py1 ] ,
246- [ px2 , py2 ]
247- ]
248- : this . _dimension === "x"
249- ? [ px1 , px2 ]
250- : [ py1 , py2 ]
251- ) ;
297+ for ( const { brush, nodes, applyX, applyY} of this . _states ) {
298+ const node = nodes . find (
299+ ( n ) => ( fx === undefined || n . __data__ ?. x === fx ) && ( fy === undefined || n . __data__ ?. y === fy )
300+ ) ;
301+ if ( ! node ) continue ;
302+ const [ px1 , px2 ] = dim !== "y" ? [ x1 , x2 ] . map ( applyX ) . sort ( ascending ) : [ ] ;
303+ const [ py1 , py2 ] = dim !== "x" ? [ y1 , y2 ] . map ( applyY ) . sort ( ascending ) : [ ] ;
304+ const selection =
305+ dim === "xy"
306+ ? [
307+ [ px1 , py1 ] ,
308+ [ px2 , py2 ]
309+ ]
310+ : dim === "x"
311+ ? [ px1 , px2 ]
312+ : [ py1 , py2 ] ;
313+ select ( node ) . call ( brush . move , selection ) ;
314+ }
252315 }
253316}
254317
@@ -281,7 +344,7 @@ function intervalRound(interval, v) {
281344}
282345
283346function renderFilter ( initialTest , channelDefaults = { } ) {
284- const updatePerFacet = [ ] ;
347+ const updates = new WeakMap ( ) ;
285348 return Object . assign (
286349 function ( { render, ...options } = { } ) {
287350 return {
@@ -306,7 +369,9 @@ function renderFilter(initialTest, channelDefaults = {}) {
306369 return next ( I , scales , { ...values , z : G } , dimensions , context ) ;
307370 } ;
308371 let g = render ( initialTest ) ;
309- updatePerFacet . push ( ( test ) => {
372+ const svg = context . ownerSVGElement ;
373+ if ( ! updates . has ( svg ) ) updates . set ( svg , [ ] ) ;
374+ updates . get ( svg ) . push ( ( test ) => {
310375 const transform = g . getAttribute ( "transform" ) ;
311376 g . replaceWith ( ( g = render ( test ) ) ) ;
312377 if ( transform ) g . setAttribute ( "transform" , transform ) ;
@@ -316,8 +381,13 @@ function renderFilter(initialTest, channelDefaults = {}) {
316381 } ;
317382 } ,
318383 {
319- update ( test , i ) {
320- return updatePerFacet [ i ] ?. ( test ) ;
384+ update ( test , state , facet ) {
385+ if ( facet === undefined ) {
386+ const fns = updates . get ( state . svg ) ;
387+ if ( fns ) for ( const fn of fns ) fn ( test ) ;
388+ } else {
389+ updates . get ( state . svg ) ?. [ facet ] ?. ( test ) ;
390+ }
321391 }
322392 }
323393 ) ;
0 commit comments