|
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";
|
@@ -313,42 +313,53 @@ export function maybeClip(clip) {
|
313 | 313 | return maybeKeyword(clip, "clip", ["frame", "sphere"]);
|
314 | 314 | }
|
315 | 315 |
|
316 |
| -// TODO avoid creating a new clip-path each time? |
| 316 | +function clipDefs({ownerSVGElement}) { |
| 317 | + const svg = select(ownerSVGElement); |
| 318 | + const defs = svg.select("defs.clip"); |
| 319 | + return defs.size() ? defs : svg.insert("defs", ":first-child").attr("class", "clip"); |
| 320 | +} |
| 321 | + |
317 | 322 | // Note: may mutate selection.node!
|
| 323 | +const frame = Symbol("frame"); |
318 | 324 | function applyClip(selection, mark, dimensions, context) {
|
319 | 325 | let clipUrl;
|
320 | 326 | switch (mark.clip) {
|
321 | 327 | case "frame": {
|
322 |
| - const {width, height, marginLeft, marginRight, marginTop, marginBottom} = dimensions; |
323 |
| - const id = getClipId(); |
324 |
| - clipUrl = `url(#${id})`; |
325 |
| - selection = create("svg:g", context) |
326 |
| - .call((g) => |
327 |
| - g |
328 |
| - .append("svg:clipPath") |
329 |
| - .attr("id", id) |
330 |
| - .append("rect") |
331 |
| - .attr("x", marginLeft) |
332 |
| - .attr("y", marginTop) |
333 |
| - .attr("width", width - marginRight - marginLeft) |
334 |
| - .attr("height", height - marginTop - marginBottom) |
335 |
| - ) |
336 |
| - .each(function () { |
337 |
| - this.appendChild(selection.node()); |
338 |
| - selection.node = () => this; // Note: mutation! |
339 |
| - }); |
| 328 | + const clips = context.clips ?? (context.clips = new WeakMap()); |
| 329 | + if (!clips.has(frame)) { |
| 330 | + const {width, height, marginLeft, marginRight, marginTop, marginBottom} = dimensions; |
| 331 | + const id = getClipId(); |
| 332 | + clips.set(frame, id); |
| 333 | + clipDefs(context) |
| 334 | + .append("clipPath") |
| 335 | + .attr("id", id) |
| 336 | + .append("rect") |
| 337 | + .attr("x", marginLeft) |
| 338 | + .attr("y", marginTop) |
| 339 | + .attr("width", width - marginRight - marginLeft) |
| 340 | + .attr("height", height - marginTop - marginBottom); |
| 341 | + } |
| 342 | + selection = create("svg:g", context).each(function () { |
| 343 | + this.appendChild(selection.node()); |
| 344 | + selection.node = () => this; // Note: mutation! |
| 345 | + }); |
| 346 | + clipUrl = `url(#${clips.get(frame)})`; |
340 | 347 | break;
|
341 | 348 | }
|
342 | 349 | case "sphere": {
|
| 350 | + const clips = context.clips ?? (context.clips = new WeakMap()); |
343 | 351 | const {projection} = context;
|
344 | 352 | if (!projection) throw new Error(`the "sphere" clip option requires a projection`);
|
345 |
| - const id = getClipId(); |
346 |
| - clipUrl = `url(#${id})`; |
347 |
| - selection |
348 |
| - .append("clipPath") |
349 |
| - .attr("id", id) |
350 |
| - .append("path") |
351 |
| - .attr("d", geoPath(projection)({type: "Sphere"})); |
| 353 | + if (!clips.has(projection)) { |
| 354 | + const id = getClipId(); |
| 355 | + clips.set(projection, id); |
| 356 | + clipDefs(context) |
| 357 | + .append("clipPath") |
| 358 | + .attr("id", id) |
| 359 | + .append("path") |
| 360 | + .attr("d", geoPath(projection)({type: "Sphere"})); |
| 361 | + } |
| 362 | + clipUrl = `url(#${clips.get(projection)})`; |
352 | 363 | break;
|
353 | 364 | }
|
354 | 365 | }
|
|
0 commit comments