diff --git a/src/marker.js b/src/marker.js
index 5c793cdea4..6d76ed9ff2 100644
--- a/src/marker.js
+++ b/src/marker.js
@@ -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);
@@ -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()));
@@ -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)));
}
diff --git a/src/memoize.js b/src/memoize.js
index eceeac7d07..36233c68bb 100644
--- a/src/memoize.js
+++ b/src/memoize.js
@@ -1,4 +1,4 @@
-const unset = Symbol("unset");
+export const unset = Symbol("unset");
export function memoize1(compute) {
return (compute.length === 1 ? memoize1Arg : memoize1Args)(compute);
diff --git a/test/output/groupMarker.svg b/test/output/groupMarker.svg
new file mode 100644
index 0000000000..bf7d2454d4
--- /dev/null
+++ b/test/output/groupMarker.svg
@@ -0,0 +1,58 @@
+
\ No newline at end of file
diff --git a/test/output/groupMarkerEnd.svg b/test/output/groupMarkerEnd.svg
new file mode 100644
index 0000000000..555af41001
--- /dev/null
+++ b/test/output/groupMarkerEnd.svg
@@ -0,0 +1,533 @@
+
\ No newline at end of file
diff --git a/test/output/groupMarkerMid.svg b/test/output/groupMarkerMid.svg
new file mode 100644
index 0000000000..8be8a4512f
--- /dev/null
+++ b/test/output/groupMarkerMid.svg
@@ -0,0 +1,58 @@
+
\ No newline at end of file
diff --git a/test/output/groupMarkerStart.svg b/test/output/groupMarkerStart.svg
new file mode 100644
index 0000000000..91564afef4
--- /dev/null
+++ b/test/output/groupMarkerStart.svg
@@ -0,0 +1,533 @@
+
\ No newline at end of file
diff --git a/test/plots/group-markers.ts b/test/plots/group-markers.ts
new file mode 100644
index 0000000000..44b319561a
--- /dev/null
+++ b/test/plots/group-markers.ts
@@ -0,0 +1,70 @@
+import * as Plot from "@observablehq/plot";
+import {range} from "d3";
+
+export async function groupMarker() {
+ return Plot.plot({
+ aspectRatio: 1,
+ axis: null,
+ inset: 30,
+ marks: [
+ Plot.line(range(20, 200), {
+ x: (i) => i * Math.sin(i / 40 + ((i % 5) * 2 * Math.PI) / 5),
+ y: (i) => i * Math.cos(i / 40 + ((i % 5) * 2 * Math.PI) / 5),
+ stroke: (i) => `arrow ${i % 5}`,
+ strokeWidth: (i) => Math.round(1 + i / 40),
+ marker: "dot"
+ })
+ ]
+ });
+}
+
+export async function groupMarkerStart() {
+ return Plot.plot({
+ aspectRatio: 1,
+ axis: null,
+ inset: 30,
+ marks: [
+ Plot.line(range(500, 0, -1), {
+ x: (i) => i * Math.sin(i / 100 + ((i % 5) * 2 * Math.PI) / 5),
+ y: (i) => i * Math.cos(i / 100 + ((i % 5) * 2 * Math.PI) / 5),
+ stroke: (i) => `arrow ${i % 5}`,
+ strokeWidth: (i) => i / 100,
+ markerStart: "circle-stroke"
+ })
+ ]
+ });
+}
+
+export async function groupMarkerMid() {
+ return Plot.plot({
+ aspectRatio: 1,
+ axis: null,
+ inset: 30,
+ marks: [
+ Plot.line(range(20, 200), {
+ x: (i) => i * Math.sin(i / 40 + ((i % 5) * 2 * Math.PI) / 5),
+ y: (i) => i * Math.cos(i / 40 + ((i % 5) * 2 * Math.PI) / 5),
+ stroke: (i) => `arrow ${i % 5}`,
+ strokeWidth: (i) => Math.round(i / 40),
+ markerMid: "dot"
+ })
+ ]
+ });
+}
+
+export async function groupMarkerEnd() {
+ return Plot.plot({
+ aspectRatio: 1,
+ axis: null,
+ inset: 30,
+ marks: [
+ Plot.line(range(500), {
+ x: (i) => i * Math.sin(i / 100 + ((i % 5) * 2 * Math.PI) / 5),
+ y: (i) => i * Math.cos(i / 100 + ((i % 5) * 2 * Math.PI) / 5),
+ stroke: (i) => `arrow ${i % 5}`,
+ strokeWidth: (i) => i / 100,
+ markerEnd: "arrow"
+ })
+ ]
+ });
+}
diff --git a/test/plots/index.ts b/test/plots/index.ts
index f510057789..f82888ee67 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -113,6 +113,7 @@ export * from "./graticule.js";
export * from "./greek-gods.js";
export * from "./grid-choropleth.js";
export * from "./grouped-rects.js";
+export * from "./group-markers.js";
export * from "./hadcrut-warming-stripes.js";
export * from "./heatmap.js";
export * from "./hexbin-oranges.js";