Skip to content

Commit 3d6aa1e

Browse files
Filmbostock
andauthored
fix markers on lines with variable aesthetics (#2094)
* 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]>
1 parent 2f7a2c7 commit 3d6aa1e

File tree

8 files changed

+1304
-10
lines changed

8 files changed

+1304
-10
lines changed

src/marker.js

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {create} from "./context.js";
2+
import {unset} from "./memoize.js";
3+
import {keyof} from "./options.js";
24

35
export function markers(mark, {marker, markerStart = marker, markerMid = marker, markerEnd = marker} = {}) {
46
mark.markerStart = maybeMarker(markerStart);
@@ -100,18 +102,56 @@ function markerTick(orient) {
100102
let nextMarkerId = 0;
101103

102104
export function applyMarkers(path, mark, {stroke: S}, context) {
103-
return applyMarkersColor(path, mark, S && ((i) => S[i]), context);
105+
return applyMarkersColor(path, mark, S && ((i) => S[i]), null, context);
104106
}
105107

106-
export function applyGroupedMarkers(path, mark, {stroke: S}, context) {
107-
return applyMarkersColor(path, mark, S && (([i]) => S[i]), context);
108+
export function applyGroupedMarkers(path, mark, {stroke: S, z: Z}, context) {
109+
return applyMarkersColor(path, mark, S && (([i]) => S[i]), Z, context);
108110
}
109111

110-
function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, strokeof = () => stroke, context) {
112+
const START = 1;
113+
const END = 2;
114+
115+
/**
116+
* When rendering lines or areas with variable aesthetics, a single series
117+
* produces multiple path elements. The first path element is a START segment;
118+
* the last path element is an END segment. When there is only a single path
119+
* element, it is both a START and an END segment.
120+
*/
121+
function getGroupedOrientation(path, Z) {
122+
const O = new Uint8Array(Z.length);
123+
const D = path.data().filter((I) => I.length > 1);
124+
const n = D.length;
125+
126+
// Forward pass to find start segments.
127+
for (let i = 0, z = unset; i < n; ++i) {
128+
const I = D[i];
129+
if (I.length > 1) {
130+
const i = I[0];
131+
if (z !== (z = keyof(Z[i]))) O[i] |= START;
132+
}
133+
}
134+
135+
// Backwards pass to find end segments.
136+
for (let i = n - 1, z = unset; i >= 0; --i) {
137+
const I = D[i];
138+
if (I.length > 1) {
139+
const i = I[0];
140+
if (z !== (z = keyof(Z[i]))) O[i] |= END;
141+
}
142+
}
143+
144+
return ([i]) => O[i];
145+
}
146+
147+
function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, strokeof = () => stroke, Z, context) {
148+
if (!markerStart && !markerMid && !markerEnd) return;
111149
const iriByMarkerColor = new Map();
150+
const orient = Z && getGroupedOrientation(path, Z);
112151

113-
function applyMarker(marker) {
152+
function applyMarker(name, marker, filter) {
114153
return function (i) {
154+
if (filter && !filter(i)) return;
115155
const color = strokeof(i);
116156
let iriByColor = iriByMarkerColor.get(marker);
117157
if (!iriByColor) iriByMarkerColor.set(marker, (iriByColor = new Map()));
@@ -122,11 +162,12 @@ function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, st
122162
node.setAttribute("id", id);
123163
iriByColor.set(color, (iri = `url(#${id})`));
124164
}
125-
return iri;
165+
this.setAttribute(name, iri);
126166
};
127167
}
128168

129-
if (markerStart) path.attr("marker-start", applyMarker(markerStart));
130-
if (markerMid) path.attr("marker-mid", applyMarker(markerMid));
131-
if (markerEnd) path.attr("marker-end", applyMarker(markerEnd));
169+
if (markerStart) path.each(applyMarker("marker-start", markerStart, orient && ((i) => orient(i) & START)));
170+
if (markerMid && orient) path.each(applyMarker("marker-start", markerMid, (i) => !(orient(i) & START)));
171+
if (markerMid) path.each(applyMarker("marker-mid", markerMid));
172+
if (markerEnd) path.each(applyMarker("marker-end", markerEnd, orient && ((i) => orient(i) & END)));
132173
}

src/memoize.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const unset = Symbol("unset");
1+
export const unset = Symbol("unset");
22

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

test/output/groupMarker.svg

Lines changed: 58 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)