1- import { geoPath , group , namespaces } from "d3" ;
1+ import { geoPath , group , namespaces , select } from "d3" ;
22import { create } from "./context.js" ;
33import { defined , nonempty } from "./defined.js" ;
44import { formatDefault } from "./format.js" ;
@@ -302,43 +302,25 @@ export function* groupIndex(I, position, mark, channels) {
302302 }
303303}
304304
305- // TODO avoid creating a new clip-path each time?
306305// Note: may mutate selection.node!
307306function applyClip ( selection , mark , dimensions , context ) {
308307 let clipUrl ;
309308 const { clip = context . clip } = mark ;
310309 switch ( clip ) {
311310 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 ) ;
330320 break ;
331321 }
332322 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 ) ;
342324 break ;
343325 }
344326 }
@@ -351,6 +333,35 @@ function applyClip(selection, mark, dimensions, context) {
351333 applyAttr ( selection , "clip-path" , clipUrl ) ;
352334}
353335
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+
354365// Note: may mutate selection.node!
355366export function applyIndirectStyles ( selection , mark , dimensions , context ) {
356367 applyClip ( selection , mark , dimensions , context ) ;
0 commit comments