@@ -60,14 +60,14 @@ export class Brush extends Mark {
6060 defaults
6161 ) ;
6262 this . _dimension = keyword ( dimension , "dimension" , [ "x" , "y" , "xy" ] ) ;
63- this . _brush = this . _dimension === "x" ? d3BrushX ( ) : this . _dimension === "y" ? d3BrushY ( ) : d3Brush ( ) ;
6463 this . _interval = interval == null ? null : maybeInterval ( interval ) ;
64+ this . _sync = sync ;
65+ this . _states = [ ] ; // per-plot state: {brush, nodes, applyX, applyY, svg}
66+ this . _syncing = false ;
6567 const channelDefaults = { x, y, z, fx : this . fx , fy : this . fy } ;
6668 this . inactive = renderFilter ( true , channelDefaults ) ;
6769 this . context = renderFilter ( false , channelDefaults ) ;
6870 this . focus = renderFilter ( false , channelDefaults ) ;
69- this . _brushNodes = [ ] ;
70- this . _sync = sync ;
7171 }
7272 render ( index , scales , values , dimensions , context ) {
7373 if ( typeof document === "undefined" ) return null ;
@@ -76,19 +76,27 @@ export class Brush extends Mark {
7676 const Y = values . channels ?. y ?. value ;
7777 const FX = values . channels ?. fx ?. value ;
7878 const FY = values . channels ?. fy ?. value ;
79- const { data, _brush, _brushNodes, inactive, context : ctx , focus} = this ;
80- let target , currentNode , syncing ;
79+ const { inactive, context : ctx , focus, _states} = this ;
8180
82- if ( ! index ?. fi ) {
81+ // Per-plot state; context.interaction is fresh for each plot.
82+ let state = context . interaction . brush ;
83+ if ( state ) {
84+ if ( state . mark !== this ) throw new Error ( "only one brush per plot" ) ;
85+ } else {
8386 const dim = this . _dimension ;
8487 const interval = this . _interval ;
8588 if ( context . projection && dim !== "xy" ) throw new Error ( `brush${ dim . toUpperCase ( ) } does not support projections` ) ;
8689 const invertX = precisionInvert ( x , context . projection ) ;
8790 const invertY = precisionInvert ( y , context . projection ) ;
88- const applyX = ( this . _applyX = ( ! context . projection && x ) || ( ( d ) => d ) ) ;
89- const applyY = ( this . _applyY = ( ! context . projection && y ) || ( ( d ) => d ) ) ;
91+ const applyX = ( ! context . projection && x ) || ( ( d ) => d ) ;
92+ const applyY = ( ! context . projection && y ) || ( ( d ) => d ) ;
93+ const brush = dim === "x" ? d3BrushX ( ) : dim === "y" ? d3BrushY ( ) : d3Brush ( ) ;
94+ const nodes = [ ] ;
95+ context . interaction . brush = state = { mark : this , brush, nodes, applyX, applyY, svg : context . ownerSVGElement } ;
96+ _states . push ( state ) ;
9097 context . dispatchValue ( null ) ;
9198 const sync = this . _sync ;
99+ const { data} = this ;
92100 const filterData =
93101 data != null &&
94102 ( ( region ) =>
@@ -102,8 +110,10 @@ export class Brush extends Mark {
102110 )
103111 )
104112 ) ) ;
105- let snapping ;
106- _brush
113+ const self = this ;
114+ let target , currentNode , snapping ;
115+
116+ brush
107117 . extent ( [
108118 [ dimensions . marginLeft - ( dim !== "y" ) , dimensions . marginTop - ( dim !== "x" ) ] ,
109119 [
@@ -112,41 +122,55 @@ export class Brush extends Mark {
112122 ]
113123 ] )
114124 . on ( "start brush end" , function ( event ) {
115- if ( syncing ) return ;
125+ if ( self . _syncing ) return ;
116126 const { selection, type} = event ;
117127 if ( type === "start" && ! snapping ) {
118128 target = event . sourceEvent ?. currentTarget ?? this ;
119- currentNode = _brushNodes . indexOf ( target ) ;
129+ currentNode = nodes . indexOf ( target ) ;
130+ // Clear other facets within this plot
120131 if ( ! sync ) {
121- syncing = true ;
122- selectAll ( _brushNodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( _brush . move , null ) ;
123- syncing = false ;
132+ self . _syncing = true ;
133+ selectAll ( nodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( brush . move , null ) ;
134+ self . _syncing = false ;
124135 }
125- for ( let i = 0 ; i < _brushNodes . length ; ++ i ) {
126- inactive . update ( false , i ) ;
127- ctx . update ( true , i ) ;
128- focus . update ( false , i ) ;
136+ for ( const p of _states ) {
137+ inactive . update ( false , p ) ;
138+ ctx . update ( true , p ) ;
139+ focus . update ( false , p ) ;
129140 }
130141 }
131142
132143 if ( selection === null ) {
133144 if ( type === "end" ) {
134145 if ( sync ) {
135- syncing = true ;
136- selectAll ( _brushNodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( _brush . move , null ) ;
137- syncing = false ;
146+ self . _syncing = true ;
147+ selectAll ( nodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( brush . move , null ) ;
148+ self . _syncing = false ;
149+ }
150+ // Clear all other plots
151+ self . _syncing = true ;
152+ for ( const p of _states ) {
153+ if ( p === state ) continue ;
154+ selectAll ( p . nodes ) . call ( p . brush . move , null ) ;
138155 }
139- for ( let i = 0 ; i < _brushNodes . length ; ++ i ) {
140- inactive . update ( true , i ) ;
141- ctx . update ( false , i ) ;
142- focus . update ( false , i ) ;
156+ self . _syncing = false ;
157+ for ( const p of _states ) {
158+ inactive . update ( true , p ) ;
159+ ctx . update ( false , p ) ;
160+ focus . update ( false , p ) ;
143161 }
144162 context . dispatchValue ( null ) ;
145163 } else {
146- for ( let i = sync ? 0 : currentNode , n = sync ? _brushNodes . length : currentNode + 1 ; i < n ; ++ i ) {
147- inactive . update ( false , i ) ;
148- ctx . update ( true , i ) ;
149- focus . update ( false , i ) ;
164+ if ( sync ) {
165+ for ( const p of _states ) {
166+ inactive . update ( false , p ) ;
167+ ctx . update ( true , p ) ;
168+ focus . update ( false , p ) ;
169+ }
170+ } else {
171+ inactive . update ( false , state , currentNode ) ;
172+ ctx . update ( true , state , currentNode ) ;
173+ focus . update ( false , state , currentNode ) ;
150174 }
151175 let value = null ;
152176 if ( event . sourceEvent ) {
@@ -170,14 +194,20 @@ export class Brush extends Mark {
170194 const inX = dim !== "y" && ( ( xi ) => px1 <= xi && xi < px2 ) ;
171195 const inY = dim !== "x" && ( ( yi ) => py1 <= yi && yi < py2 ) ;
172196 if ( sync ) {
173- syncing = true ;
174- selectAll ( _brushNodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( _brush . move , selection ) ;
175- syncing = false ;
197+ self . _syncing = true ;
198+ selectAll ( nodes . filter ( ( _ , i ) => i !== currentNode ) ) . call ( brush . move , selection ) ;
199+ self . _syncing = false ;
176200 }
177- for ( let i = sync ? 0 : currentNode , n = sync ? _brushNodes . length : currentNode + 1 ; i < n ; ++ i ) {
178- inactive . update ( false , i ) ;
179- ctx . update ( ! inX ? ( _ , yi ) => ! inY ( yi ) : ! inY ? ( xi ) => ! inX ( xi ) : ( xi , yi ) => ! ( inX ( xi ) && inY ( yi ) ) , i ) ;
180- focus . update ( ! inX ? ( _ , yi ) => inY ( yi ) : ! inY ? inX : ( xi , yi ) => inX ( xi ) && inY ( yi ) , i ) ;
201+ const ctxTest = ! inX ? ( _ , yi ) => ! inY ( yi ) : ! inY ? ( xi ) => ! inX ( xi ) : ( xi , yi ) => ! ( inX ( xi ) && inY ( yi ) ) ;
202+ const focusTest = ! inX ? ( _ , yi ) => inY ( yi ) : ! inY ? inX : ( xi , yi ) => inX ( xi ) && inY ( yi ) ;
203+ if ( sync ) {
204+ inactive . update ( false , state ) ;
205+ ctx . update ( ctxTest , state ) ;
206+ focus . update ( focusTest , state ) ;
207+ } else {
208+ inactive . update ( false , state , currentNode ) ;
209+ ctx . update ( ctxTest , state , currentNode ) ;
210+ focus . update ( focusTest , state , currentNode ) ;
181211 }
182212
183213 const [ x1 , x2 ] = invertX && [ invertX ( px1 ) , invertX ( px2 ) ] . sort ( ascending ) ;
@@ -190,7 +220,7 @@ export class Brush extends Mark {
190220 let r2 = intervalRound ( interval , s2 ) ;
191221 if ( + r1 === + r2 ) r2 = interval . offset ( r1 ) ;
192222 snapping = true ;
193- select ( this ) . call ( _brush . move , [ r1 , r2 ] . map ( dim === "x" ? applyX : applyY ) . sort ( ascending ) ) ;
223+ select ( this ) . call ( brush . move , [ r1 , r2 ] . map ( dim === "x" ? applyX : applyY ) . sort ( ascending ) ) ;
194224 snapping = false ;
195225 return ;
196226 }
@@ -205,45 +235,78 @@ export class Brush extends Mark {
205235 } ) ;
206236 if ( filterData ) region . data = filterData ( region ) ;
207237 context . dispatchValue ( region ) ;
238+
239+ // Sync other plots in data space
240+ if ( type !== "start" ) {
241+ self . _syncing = true ;
242+ for ( const p of _states ) {
243+ if ( p === state ) continue ;
244+ const [ pX1 , pX2 ] = [ p . applyX ( x1 ) , p . applyX ( x2 ) ] . sort ( ascending ) ;
245+ const [ pY1 , pY2 ] = [ p . applyY ( y1 ) , p . applyY ( y2 ) ] . sort ( ascending ) ;
246+ const selection =
247+ dim === "xy"
248+ ? [
249+ [ pX1 , pY1 ] ,
250+ [ pX2 , pY2 ]
251+ ]
252+ : dim === "x"
253+ ? [ pX1 , pX2 ]
254+ : [ pY1 , pY2 ] ;
255+ selectAll ( p . nodes ) . call ( p . brush . move , selection ) ;
256+ const inXp = dim !== "y" && ( ( xi ) => p . applyX ( x1 ) <= xi && xi < p . applyX ( x2 ) ) ;
257+ const inYp = dim !== "x" && ( ( yi ) => p . applyY ( y1 ) <= yi && yi < p . applyY ( y2 ) ) ;
258+ inactive . update ( false , p ) ;
259+ ctx . update (
260+ ! inXp ? ( _ , yi ) => ! inYp ( yi ) : ! inYp ? ( xi ) => ! inXp ( xi ) : ( xi , yi ) => ! ( inXp ( xi ) && inYp ( yi ) ) ,
261+ p
262+ ) ;
263+ focus . update ( ! inXp ? ( _ , yi ) => inYp ( yi ) : ! inYp ? inXp : ( xi , yi ) => inXp ( xi ) && inYp ( yi ) , p ) ;
264+ }
265+ self . _syncing = false ;
266+ }
208267 }
209268 } ) ;
210269 }
211270
212271 const g = create ( "svg:g" ) . attr ( "aria-label" , this . _dimension === "xy" ? "brush" : `brush-${ this . _dimension } ` ) ;
213- g . call ( this . _brush ) ;
272+ g . call ( state . brush ) ;
214273 const sel = g . select ( ".selection" ) ;
215274 applyAttr ( sel , "fill" , this . fill ) ;
216275 applyAttr ( sel , "fill-opacity" , this . fillOpacity ) ;
217276 applyAttr ( sel , "stroke" , this . stroke ) ;
218277 applyAttr ( sel , "stroke-width" , this . strokeWidth ) ;
219278 applyAttr ( sel , "stroke-opacity" , this . strokeOpacity ) ;
220279 const node = g . node ( ) ;
221- this . _brushNodes . push ( node ) ;
280+ state . nodes . push ( node ) ;
222281 return node ;
223282 }
224283 move ( value ) {
225284 if ( value == null ) {
226- selectAll ( this . _brushNodes ) . call ( this . _brush . move , null ) ;
285+ for ( const { brush, nodes} of this . _states ) {
286+ selectAll ( nodes ) . call ( brush . move , null ) ;
287+ }
227288 return ;
228289 }
290+ const dim = this . _dimension ;
229291 const { x1, x2, y1, y2, fx, fy} = value ;
230- const node = this . _brushNodes . find (
231- ( n ) => ( fx === undefined || n . __data__ ?. x === fx ) && ( fy === undefined || n . __data__ ?. y === fy )
232- ) ;
233- if ( ! node ) return ;
234- const [ px1 , px2 ] = [ x1 , x2 ] . map ( this . _applyX ) . sort ( ascending ) ;
235- const [ py1 , py2 ] = [ y1 , y2 ] . map ( this . _applyY ) . sort ( ascending ) ;
236- select ( node ) . call (
237- this . _brush . move ,
238- this . _dimension === "xy"
239- ? [
240- [ px1 , py1 ] ,
241- [ px2 , py2 ]
242- ]
243- : this . _dimension === "x"
244- ? [ px1 , px2 ]
245- : [ py1 , py2 ]
246- ) ;
292+ for ( const { brush, nodes, applyX, applyY} of this . _states ) {
293+ const node = nodes . find (
294+ ( n ) => ( fx === undefined || n . __data__ ?. x === fx ) && ( fy === undefined || n . __data__ ?. y === fy )
295+ ) ;
296+ if ( ! node ) continue ;
297+ const [ px1 , px2 ] = dim !== "y" ? [ x1 , x2 ] . map ( applyX ) . sort ( ascending ) : [ ] ;
298+ const [ py1 , py2 ] = dim !== "x" ? [ y1 , y2 ] . map ( applyY ) . sort ( ascending ) : [ ] ;
299+ const selection =
300+ dim === "xy"
301+ ? [
302+ [ px1 , py1 ] ,
303+ [ px2 , py2 ]
304+ ]
305+ : dim === "x"
306+ ? [ px1 , px2 ]
307+ : [ py1 , py2 ] ;
308+ select ( node ) . call ( brush . move , selection ) ;
309+ }
247310 }
248311}
249312
@@ -276,7 +339,7 @@ function intervalRound(interval, v) {
276339}
277340
278341function renderFilter ( initialTest , channelDefaults = { } ) {
279- const updatePerFacet = [ ] ;
342+ const updates = new WeakMap ( ) ;
280343 return Object . assign (
281344 function ( { render, ...options } = { } ) {
282345 return {
@@ -301,7 +364,9 @@ function renderFilter(initialTest, channelDefaults = {}) {
301364 return next ( I , scales , { ...values , z : G } , dimensions , context ) ;
302365 } ;
303366 let g = render ( initialTest ) ;
304- updatePerFacet . push ( ( test ) => {
367+ const svg = context . ownerSVGElement ;
368+ if ( ! updates . has ( svg ) ) updates . set ( svg , [ ] ) ;
369+ updates . get ( svg ) . push ( ( test ) => {
305370 const transform = g . getAttribute ( "transform" ) ;
306371 g . replaceWith ( ( g = render ( test ) ) ) ;
307372 if ( transform ) g . setAttribute ( "transform" , transform ) ;
@@ -311,8 +376,13 @@ function renderFilter(initialTest, channelDefaults = {}) {
311376 } ;
312377 } ,
313378 {
314- update ( test , i ) {
315- return updatePerFacet [ i ] ?. ( test ) ;
379+ update ( test , state , facet ) {
380+ if ( facet === undefined ) {
381+ const fns = updates . get ( state . svg ) ;
382+ if ( fns ) for ( const fn of fns ) fn ( test ) ;
383+ } else {
384+ updates . get ( state . svg ) ?. [ facet ] ?. ( test ) ;
385+ }
316386 }
317387 }
318388 ) ;
0 commit comments