1
- import { geoPath , group , namespaces } from "d3" ;
1
+ import { geoPath , group , namespaces , select } from "d3" ;
2
2
import { create } from "./context.js" ;
3
3
import { defined , nonempty } from "./defined.js" ;
4
4
import { formatDefault } from "./format.js" ;
@@ -302,43 +302,25 @@ export function* groupIndex(I, position, mark, channels) {
302
302
}
303
303
}
304
304
305
- // TODO avoid creating a new clip-path each time?
306
305
// Note: may mutate selection.node!
307
306
function applyClip ( selection , mark , dimensions , context ) {
308
307
let clipUrl ;
309
308
const { clip = context . clip } = mark ;
310
309
switch ( clip ) {
311
310
case "frame" : {
312
- const { width, height, marginLeft, marginRight, marginTop, marginBottom} = dimensions ;
313
- const id = getClipId ( ) ;
314
- clipUrl = `url(#${ id } )` ;
315
- selection = create ( "svg:g" , context )
316
- . call ( ( g ) =>
317
- g
318
- . append ( "svg:clipPath" )
319
- . attr ( "id" , id )
320
- . append ( "rect" )
321
- . attr ( "x" , marginLeft )
322
- . attr ( "y" , marginTop )
323
- . attr ( "width" , width - marginRight - marginLeft )
324
- . attr ( "height" , height - marginTop - marginBottom )
325
- )
326
- . each ( function ( ) {
327
- this . appendChild ( selection . node ( ) ) ;
328
- selection . node = ( ) => this ; // Note: mutation!
329
- } ) ;
311
+ // Wrap the G element with another (untransformed) G element, applying the
312
+ // clip to the parent G element so that the clip path is not affected by
313
+ // the mark’s transform. To simplify the adoption of this fix, mutate the
314
+ // passed-in selection.node to return the parent G element.
315
+ selection = create ( "svg:g" , context ) . each ( function ( ) {
316
+ this . appendChild ( selection . node ( ) ) ;
317
+ selection . node = ( ) => this ; // Note: mutation!
318
+ } ) ;
319
+ clipUrl = getFrameClip ( context , dimensions ) ;
330
320
break ;
331
321
}
332
322
case "sphere" : {
333
- const { projection} = context ;
334
- if ( ! projection ) throw new Error ( `the "sphere" clip option requires a projection` ) ;
335
- const id = getClipId ( ) ;
336
- clipUrl = `url(#${ id } )` ;
337
- selection
338
- . append ( "clipPath" )
339
- . attr ( "id" , id )
340
- . append ( "path" )
341
- . attr ( "d" , geoPath ( projection ) ( { type : "Sphere" } ) ) ;
323
+ clipUrl = getProjectionClip ( context ) ;
342
324
break ;
343
325
}
344
326
}
@@ -351,6 +333,35 @@ function applyClip(selection, mark, dimensions, context) {
351
333
applyAttr ( selection , "clip-path" , clipUrl ) ;
352
334
}
353
335
336
+ function memoizeClip ( clip ) {
337
+ const cache = new WeakMap ( ) ;
338
+ return ( context , dimensions ) => {
339
+ let url = cache . get ( context ) ;
340
+ if ( ! url ) {
341
+ const id = getClipId ( ) ;
342
+ select ( context . ownerSVGElement ) . append ( "clipPath" ) . attr ( "id" , id ) . call ( clip , context , dimensions ) ;
343
+ cache . set ( context , ( url = `url(#${ id } )` ) ) ;
344
+ }
345
+ return url ;
346
+ } ;
347
+ }
348
+
349
+ const getFrameClip = memoizeClip ( ( clipPath , context , dimensions ) => {
350
+ const { width, height, marginLeft, marginRight, marginTop, marginBottom} = dimensions ;
351
+ clipPath
352
+ . append ( "rect" )
353
+ . attr ( "x" , marginLeft )
354
+ . attr ( "y" , marginTop )
355
+ . attr ( "width" , width - marginRight - marginLeft )
356
+ . attr ( "height" , height - marginTop - marginBottom ) ;
357
+ } ) ;
358
+
359
+ const getProjectionClip = memoizeClip ( ( clipPath , context ) => {
360
+ const { projection} = context ;
361
+ if ( ! projection ) throw new Error ( `the "sphere" clip option requires a projection` ) ;
362
+ clipPath . append ( "path" ) . attr ( "d" , geoPath ( projection ) ( { type : "Sphere" } ) ) ;
363
+ } ) ;
364
+
354
365
// Note: may mutate selection.node!
355
366
export function applyIndirectStyles ( selection , mark , dimensions , context ) {
356
367
applyClip ( selection , mark , dimensions , context ) ;
0 commit comments