Skip to content

Commit c911b1a

Browse files
Filmbostock
andauthored
stable clip paths (#1624)
* Use (and re-use) the same clip-path when two renders are clipped with the same shape * update test snapshots * no defs; use WeakMap * remove unused maybeClip --------- Co-authored-by: Mike Bostock <[email protected]>
1 parent 699e2d6 commit c911b1a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+395
-1017
lines changed

src/style.js

+40-29
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {geoPath, group, namespaces} from "d3";
1+
import {geoPath, group, namespaces, select} from "d3";
22
import {create} from "./context.js";
33
import {defined, nonempty} from "./defined.js";
44
import {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!
307306
function 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!
355366
export function applyIndirectStyles(selection, mark, dimensions, context) {
356367
applyClip(selection, mark, dimensions, context);

test/output/aaplBollingerCandlestick.svg

+5-11
Loading

test/output/aaplCloseClip.svg

+4-7
Loading

test/output/armadillo.svg

+4-7
Loading

test/output/bandClip.svg

+3-3
Loading

test/output/bandClip2.svg

+3-3
Loading

test/output/contourVapor.svg

+3-3
Loading

test/output/differenceFilterX.svg

+3-3
Loading

test/output/differenceFilterY1.svg

+3-3
Loading

test/output/differenceFilterY2.svg

+3-3
Loading

test/output/differenceX.svg

+3-3
Loading

test/output/differenceY.svg

+3-3
Loading

test/output/differenceY1.svg

+3-3
Loading

test/output/differenceYClip.svg

+7-13
Loading

0 commit comments

Comments
 (0)