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";