Skip to content

Commit

Permalink
better marker strategy (#2095)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock authored Jun 26, 2024
1 parent 0a0176b commit e7f25c9
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 26 deletions.
72 changes: 47 additions & 25 deletions src/marker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {select, selectAll, group} from "d3";
import {create} from "./context.js";
import {unset} from "./memoize.js";
import {keyof} from "./options.js";

export function markers(mark, {marker, markerStart = marker, markerMid = marker, markerEnd = marker} = {}) {
mark.markerStart = maybeMarker(markerStart);
Expand Down Expand Up @@ -108,10 +109,49 @@ export function applyGroupedMarkers(path, mark, {stroke: S, z: Z}, context) {
return applyMarkersColor(path, mark, S && (([i]) => S[i]), Z, context);
}

const START = 1;
const END = 2;

/**
* When rendering lines or areas with variable aesthetics, a single series
* produces multiple path elements. The first path element is a START segment;
* the last path element is an END segment. When there is only a single path
* element, it is both a START and an END segment.
*/
function getGroupedOrientation(path, Z) {
const O = new Uint8Array(Z.length);
const D = path.data().filter((I) => I.length > 1);
const n = D.length;

// Forward pass to find start segments.
for (let i = 0, z = unset; i < n; ++i) {
const I = D[i];
if (I.length > 1) {
const i = I[0];
if (z !== (z = keyof(Z[i]))) O[i] |= START;
}
}

// Backwards pass to find end segments.
for (let i = n - 1, z = unset; i >= 0; --i) {
const I = D[i];
if (I.length > 1) {
const i = I[0];
if (z !== (z = keyof(Z[i]))) O[i] |= END;
}
}

return ([i]) => O[i];
}

function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, strokeof = () => stroke, Z, context) {
if (!markerStart && !markerMid && !markerEnd) return;
const iriByMarkerColor = new Map();
function applyMarker(marker) {
const orient = Z && getGroupedOrientation(path, Z);

function applyMarker(name, marker, filter) {
return function (i) {
if (filter && !filter(i)) return;
const color = strokeof(i);
let iriByColor = iriByMarkerColor.get(marker);
if (!iriByColor) iriByMarkerColor.set(marker, (iriByColor = new Map()));
Expand All @@ -122,30 +162,12 @@ function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, st
node.setAttribute("id", id);
iriByColor.set(color, (iri = `url(#${id})`));
}
return iri;
this.setAttribute(name, iri);
};
}

if (!(markerStart || markerMid || markerEnd)) return;

const start = markerStart && applyMarker(markerStart);
const mid = markerMid && applyMarker(markerMid);
const end = markerEnd && applyMarker(markerEnd);
if (Z) {
for (const g of group(
path.filter((i) => i.length >= 2),
(d) => Z[select(d).datum()[0]]
).values()) {
if (start) select(g.at(0)).attr("marker-start", start);
if (mid) {
selectAll(g.slice(1)).attr("marker-start", mid);
selectAll(g).attr("marker-mid", mid);
}
if (end) select(g.at(-1)).attr("marker-end", end);
}
} else {
if (start) path.attr("marker-start", start);
if (mid) path.attr("marker-mid", mid);
if (end) path.attr("marker-end", end);
}
if (markerStart) path.each(applyMarker("marker-start", markerStart, orient && ((i) => orient(i) & START)));
if (markerMid && orient) path.each(applyMarker("marker-start", markerMid, (i) => !(orient(i) & START)));
if (markerMid) path.each(applyMarker("marker-mid", markerMid));
if (markerEnd) path.each(applyMarker("marker-end", markerEnd, orient && ((i) => orient(i) & END)));
}
2 changes: 1 addition & 1 deletion src/memoize.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const unset = Symbol("unset");
export const unset = Symbol("unset");

export function memoize1(compute) {
return (compute.length === 1 ? memoize1Arg : memoize1Args)(compute);
Expand Down

0 comments on commit e7f25c9

Please sign in to comment.