diff --git a/docs/features/markers.md b/docs/features/markers.md index cea6193b4b..7fd8290392 100644 --- a/docs/features/markers.md +++ b/docs/features/markers.md @@ -23,6 +23,9 @@ A **marker** defines a graphic drawn on vertices of a [line](../marks/line.md) o + + +

@@ -53,6 +56,9 @@ The following named markers are supported: * *dot* - a filled *circle* without a stroke and 2.5px radius * *circle*, equivalent to *circle-fill* - a filled circle with a white stroke and 3px radius * *circle-stroke* - a hollow circle with a colored stroke and a white fill and 3px radius +* *tick* - a small opposing line +* *tick-x* - a small horizontal line +* *tick-y* - a small vertical line If **marker** is true, it defaults to *circle*. If **marker** is a function, it will be called with a given *color* and must return an [SVG marker element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker). diff --git a/docs/marks/rule.md b/docs/marks/rule.md index e81758ba0a..08bee8bde6 100644 --- a/docs/marks/rule.md +++ b/docs/marks/rule.md @@ -120,7 +120,23 @@ Plot.plot({ ``` ::: -Rules can also be a stylistic choice, as in the lollipop 🍭 chart below, serving the role of a skinny [bar](./bar.md) topped with a [dot](./dot.md). +Rules can indicate uncertainty or error by setting the [**marker** option](../features/markers.md) to *tick*; this draws a small perpendicular line at the start and end of the rule. For example, to simulate ±10% error: + +:::plot +```js +Plot.plot({ + x: {label: null}, + y: {percent: true}, + marks: [ + Plot.barY(alphabet, {x: "letter", y: "frequency", fill: "blue"}), + Plot.ruleX(alphabet, {x: "letter", y1: (d) => d.frequency * 0.9, y2: (d) => d.frequency * 1.1, marker: "tick"}), + Plot.ruleY([0]) + ] +}) +``` +::: + +Rules can also be a stylistic choice, as in the lollipop 🍭 chart below, serving the role of a skinny [bar](./bar.md) topped with a [*dot* marker](../features/markers.md). :::plot https://observablehq.com/@observablehq/plot-lollipop ```js @@ -128,8 +144,7 @@ Plot.plot({ x: {label: null, tickPadding: 6, tickSize: 0}, y: {percent: true}, marks: [ - Plot.ruleX(alphabet, {x: "letter", y: "frequency", strokeWidth: 2}), - Plot.dot(alphabet, {x: "letter", y: "frequency", fill: "currentColor", r: 4}) + Plot.ruleX(alphabet, {x: "letter", y: "frequency", strokeWidth: 2, markerEnd: "dot"}) ] }) ``` diff --git a/src/marker.d.ts b/src/marker.d.ts index 698b31a632..3a2cc7071a 100644 --- a/src/marker.d.ts +++ b/src/marker.d.ts @@ -7,8 +7,20 @@ * - *circle-fill* - a filled circle with a white stroke and 3px radius * - *circle-stroke* - a stroked circle with a white fill and 3px radius * - *circle* - alias for *circle-fill* + * - *tick* - a small opposing line + * - *tick-x* - a small horizontal line + * - *tick-y* - a small vertical line */ -export type MarkerName = "arrow" | "arrow-reverse" | "dot" | "circle" | "circle-fill" | "circle-stroke"; +export type MarkerName = + | "arrow" + | "arrow-reverse" + | "dot" + | "circle" + | "circle-fill" + | "circle-stroke" + | "tick" + | "tick-x" + | "tick-y"; /** A custom marker implementation. */ export type MarkerFunction = (color: string, context: {document: Document}) => SVGMarkerElement; diff --git a/src/marker.js b/src/marker.js index 2d682a9778..5c793cdea4 100644 --- a/src/marker.js +++ b/src/marker.js @@ -24,6 +24,12 @@ function maybeMarker(marker) { return markerCircleFill; case "circle-stroke": return markerCircleStroke; + case "tick": + return markerTick("auto"); + case "tick-x": + return markerTick(90); + case "tick-y": + return markerTick(0); } throw new Error(`invalid marker: ${marker}`); } @@ -79,6 +85,18 @@ function markerCircleStroke(color, context) { .node(); } +function markerTick(orient) { + return (color, context) => + create("svg:marker", context) + .attr("viewBox", "-3 -3 6 6") + .attr("markerWidth", 6) + .attr("markerHeight", 6) + .attr("orient", orient) + .attr("stroke", color) + .call((marker) => marker.append("path").attr("d", "M0,-3v6")) + .node(); +} + let nextMarkerId = 0; export function applyMarkers(path, mark, {stroke: S}, context) { diff --git a/test/output/errorBarX.svg b/test/output/errorBarX.svg new file mode 100644 index 0000000000..27d6c88856 --- /dev/null +++ b/test/output/errorBarX.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + E + T + A + O + I + N + S + H + R + D + L + C + U + M + W + F + G + Y + P + B + V + K + J + X + Q + Z + + + letter + + + + + + + + + + + + 0.00 + 0.02 + 0.04 + 0.06 + 0.08 + 0.10 + 0.12 + + + frequency → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/errorBarY.svg b/test/output/errorBarY.svg new file mode 100644 index 0000000000..7496482e1f --- /dev/null +++ b/test/output/errorBarY.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + 0.00 + 0.01 + 0.02 + 0.03 + 0.04 + 0.05 + 0.06 + 0.07 + 0.08 + 0.09 + 0.10 + 0.11 + 0.12 + 0.13 + + + ↑ frequency + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + E + T + A + O + I + N + S + H + R + D + L + C + U + M + W + F + G + Y + P + B + V + K + J + X + Q + Z + + + letter + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/error-bar.ts b/test/plots/error-bar.ts new file mode 100644 index 0000000000..66036a9b2d --- /dev/null +++ b/test/plots/error-bar.ts @@ -0,0 +1,24 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export async function errorBarX() { + const alphabet = await d3.csv("data/alphabet.csv", d3.autoType); + return Plot.plot({ + marks: [ + Plot.barX(alphabet, {x: "frequency", y: "letter", sort: {y: "-x"}, fill: "steelblue"}), + Plot.ruleY(alphabet, {x1: (d) => d.frequency * 0.9, x2: (d) => d.frequency * 1.1, y: "letter", marker: "tick"}), + Plot.ruleX([0]) + ] + }); +} + +export async function errorBarY() { + const alphabet = await d3.csv("data/alphabet.csv", d3.autoType); + return Plot.plot({ + marks: [ + Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "-y"}, fill: "steelblue"}), + Plot.ruleX(alphabet, {x: "letter", y1: (d) => d.frequency * 0.9, y2: (d) => d.frequency * 1.1, marker: "tick"}), + Plot.ruleY([0]) + ] + }); +} diff --git a/test/plots/index.ts b/test/plots/index.ts index 0a412b4b67..379e35b789 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -83,6 +83,7 @@ export * from "./empty-legend.js"; export * from "./empty-x.js"; export * from "./empty.js"; export * from "./energy-production.js"; +export * from "./error-bar.js"; export * from "./facet-reindex.js"; export * from "./faithful-density-1d.js"; export * from "./faithful-density.js"; @@ -292,8 +293,8 @@ export * from "./symbol-set.js"; export * from "./text-overflow.js"; export * from "./this-is-just-to-say.js"; export * from "./time-axis.js"; -export * from "./tip.js"; export * from "./tip-format.js"; +export * from "./tip.js"; export * from "./title.js"; export * from "./traffic-horizon.js"; export * from "./travelers-covid-drop.js";