Skip to content

Commit

Permalink
fix markers on lines with variable aesthetics (#2094)
Browse files Browse the repository at this point in the history
* for lines with variable aesthetics we want to maintain the higher-level semantics of markers:
- markerStart matches the start of the line
- markerMid matches the points which are not at the start or the end
- markerEnd matches the end of the line

Since these lines are implemented as multiple paths, we have change the low-level implementation of markers:
- markerStart only applies to the first segment of a line
- markerMid applies to all the segments, complemented by the start of all but the first segments
- markerEnd only applies to the last segment of a line

closes #2093

* better marker strategy (#2095)

---------

Co-authored-by: Mike Bostock <[email protected]>
  • Loading branch information
Fil and mbostock authored Jul 28, 2024
1 parent 2f7a2c7 commit 3d6aa1e
Show file tree
Hide file tree
Showing 8 changed files with 1,304 additions and 10 deletions.
59 changes: 50 additions & 9 deletions src/marker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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 @@ -100,18 +102,56 @@ function markerTick(orient) {
let nextMarkerId = 0;

export function applyMarkers(path, mark, {stroke: S}, context) {
return applyMarkersColor(path, mark, S && ((i) => S[i]), context);
return applyMarkersColor(path, mark, S && ((i) => S[i]), null, context);
}

export function applyGroupedMarkers(path, mark, {stroke: S}, context) {
return applyMarkersColor(path, mark, S && (([i]) => S[i]), context);
export function applyGroupedMarkers(path, mark, {stroke: S, z: Z}, context) {
return applyMarkersColor(path, mark, S && (([i]) => S[i]), Z, context);
}

function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, strokeof = () => stroke, 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();
const orient = Z && getGroupedOrientation(path, Z);

function applyMarker(marker) {
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,11 +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) path.attr("marker-start", applyMarker(markerStart));
if (markerMid) path.attr("marker-mid", applyMarker(markerMid));
if (markerEnd) path.attr("marker-end", applyMarker(markerEnd));
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
58 changes: 58 additions & 0 deletions test/output/groupMarker.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 3d6aa1e

Please sign in to comment.