From eb854199b0496530975a93ab370a04c72dd86229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Nov 2024 21:48:41 +0100 Subject: [PATCH 1/5] waffle tips (#2132) * waffle tips * test faceting * simpler x * support waffle pointer, and simplify * crazy logic to compute the waffles' centroids * remove single hint * prettier * tidy * pass cell dimensions as channels * prettier * no filter on polygon * prettier * fix tip option type * fewer channels * default maxRadius to infinity --------- Co-authored-by: Mike Bostock --- src/mark.d.ts | 3 +- src/marks/waffle.js | 242 ++- test/output/wafflePointer.svg | 450 +++++ test/output/wafflePointerFractional.svg | 136 ++ test/output/waffleTip.svg | 67 + test/output/waffleTipFacet.svg | 2084 ++++++++++++++++++++++ test/output/waffleTipFacetX.svg | 2080 ++++++++++++++++++++++ test/output/waffleTipFacetXY.svg | 2085 +++++++++++++++++++++++ test/output/waffleTipUnit.svg | 447 +++++ test/output/waffleTipUnitX.svg | 447 +++++ test/output/waffleTipX.svg | 70 + test/plots/waffle.ts | 114 ++ 12 files changed, 8149 insertions(+), 76 deletions(-) create mode 100644 test/output/wafflePointer.svg create mode 100644 test/output/wafflePointerFractional.svg create mode 100644 test/output/waffleTip.svg create mode 100644 test/output/waffleTipFacet.svg create mode 100644 test/output/waffleTipFacetX.svg create mode 100644 test/output/waffleTipFacetXY.svg create mode 100644 test/output/waffleTipUnit.svg create mode 100644 test/output/waffleTipUnitX.svg create mode 100644 test/output/waffleTipX.svg diff --git a/src/mark.d.ts b/src/mark.d.ts index 4e5a60cbed..c40b1a1a56 100644 --- a/src/mark.d.ts +++ b/src/mark.d.ts @@ -1,6 +1,7 @@ import type {Channel, ChannelDomainSort, ChannelValue, ChannelValues, ChannelValueSpec} from "./channel.js"; import type {Context} from "./context.js"; import type {Dimensions} from "./dimensions.js"; +import type {PointerOptions} from "./interactions/pointer.js"; import type {TipOptions} from "./marks/tip.js"; import type {plot} from "./plot.js"; import type {ScaleFunctions} from "./scales.js"; @@ -288,7 +289,7 @@ export interface MarkOptions { title?: ChannelValue; /** Whether to generate a tooltip for this mark, and any tip options. */ - tip?: boolean | TipPointer | (TipOptions & {pointer?: TipPointer}); + tip?: boolean | TipPointer | (TipOptions & PointerOptions & {pointer?: TipPointer}); /** * How to clip the mark; one of: diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 4a95d38ec6..3b2b6d5774 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -1,9 +1,11 @@ import {extent, namespaces} from "d3"; +import {valueObject} from "../channel.js"; import {create} from "../context.js"; import {composeRender} from "../mark.js"; -import {hasXY, identity, indexOf} from "../options.js"; +import {hasXY, identity, indexOf, isObject} from "../options.js"; import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, getPatternId} from "../style.js"; import {template} from "../template.js"; +import {initializer} from "../transforms/basic.js"; import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js"; import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; @@ -14,8 +16,8 @@ const waffleDefaults = { }; export class WaffleX extends BarX { - constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) { - super(data, {...options, render: composeRender(render, waffleRender("x"))}, waffleDefaults); + constructor(data, {unit = 1, gap = 1, round, multiple, ...options} = {}) { + super(data, wafflePolygon("x", options), waffleDefaults); this.unit = Math.max(0, unit); this.gap = +gap; this.round = maybeRound(round); @@ -24,8 +26,8 @@ export class WaffleX extends BarX { } export class WaffleY extends BarY { - constructor(data, {unit = 1, gap = 1, round, render, multiple, ...options} = {}) { - super(data, {...options, render: composeRender(render, waffleRender("y"))}, waffleDefaults); + constructor(data, {unit = 1, gap = 1, round, multiple, ...options} = {}) { + super(data, wafflePolygon("y", options), waffleDefaults); this.unit = Math.max(0, unit); this.gap = +gap; this.round = maybeRound(round); @@ -33,17 +35,19 @@ export class WaffleY extends BarY { } } -function waffleRender(y) { - return function (index, scales, values, dimensions, context) { - const {ariaLabel, href, title, ...visualValues} = values; - const {unit, gap, rx, ry, round} = this; - const {document} = context; - const Y1 = values.channels[`${y}1`].value; - const Y2 = values.channels[`${y}2`].value; +function wafflePolygon(y, options) { + const x = y === "y" ? "x" : "y"; + const y1 = `${y}1`; + const y2 = `${y}2`; + return initializer(waffleRender(options), function (data, facets, channels, scales, dimensions) { + const {round, unit} = this; + const Y1 = channels[y1].value; + const Y2 = channels[y2].value; // We might not use all the available bandwidth if the cells don’t fit evenly. - const barwidth = this[y === "y" ? "_width" : "_height"](scales, values, dimensions); - const barx = this[y === "y" ? "_x" : "_y"](scales, values, dimensions); + const xy = valueObject({...(x in channels && {[x]: channels[x]}), [y1]: channels[y1], [y2]: channels[y2]}, scales); + const barwidth = this[y === "y" ? "_width" : "_height"](scales, xy, dimensions); + const barx = this[y === "y" ? "_x" : "_y"](scales, xy, dimensions); // The length of a unit along y in pixels. const scale = unit * scaleof(scales.scales[y]); @@ -55,63 +59,98 @@ function waffleRender(y) { const cx = Math.min(barwidth / multiple, scale * multiple); const cy = scale * multiple; - // TODO insets? - const transform = y === "y" ? ([x, y]) => [x * cx, -y * cy] : ([x, y]) => [y * cy, x * cx]; + // The reference position. const tx = (barwidth - multiple * cx) / 2; const x0 = typeof barx === "function" ? (i) => barx(i) + tx : barx + tx; const y0 = scales[y](0); - // Create a base pattern with shared attributes for cloning. - const patternId = getPatternId(); - const basePattern = document.createElementNS(namespaces.svg, "pattern"); - basePattern.setAttribute("width", y === "y" ? cx : cy); - basePattern.setAttribute("height", y === "y" ? cy : cx); - basePattern.setAttribute("patternUnits", "userSpaceOnUse"); - const basePatternRect = basePattern.appendChild(document.createElementNS(namespaces.svg, "rect")); - basePatternRect.setAttribute("x", gap / 2); - basePatternRect.setAttribute("y", gap / 2); - basePatternRect.setAttribute("width", (y === "y" ? cx : cy) - gap); - basePatternRect.setAttribute("height", (y === "y" ? cy : cx) - gap); - if (rx != null) basePatternRect.setAttribute("rx", rx); - if (ry != null) basePatternRect.setAttribute("ry", ry); - - return create("svg:g", context) - .call(applyIndirectStyles, this, dimensions, context) - .call(this._transform, this, scales) - .call((g) => - g - .selectAll() - .data(index) - .enter() - .append(() => basePattern.cloneNode(true)) - .attr("id", (i) => `${patternId}-${i}`) - .select("rect") - .call(applyDirectStyles, this) - .call(applyChannelStyles, this, visualValues) - ) - .call((g) => - g - .selectAll() - .data(index) - .enter() - .append("path") - .attr("transform", y === "y" ? template`translate(${x0},${y0})` : template`translate(${y0},${x0})`) - .attr( - "d", - (i) => - `M${wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple) - .map(transform) - .join("L")}Z` - ) - .attr("fill", (i) => `url(#${patternId}-${i})`) - .attr("stroke", this.stroke == null ? null : (i) => `url(#${patternId}-${i})`) - .call(applyChannelStyles, this, {ariaLabel, href, title}) - ) - .node(); + // TODO insets? + const transform = y === "y" ? ([x, y]) => [x * cx, -y * cy] : ([x, y]) => [y * cy, x * cx]; + const mx = typeof x0 === "function" ? (i) => x0(i) - barwidth / 2 : () => x0; + const [ix, iy] = y === "y" ? [0, 1] : [1, 0]; + + const n = Y2.length; + const P = new Array(n); + const X = new Float64Array(n); + const Y = new Float64Array(n); + + for (let i = 0; i < n; ++i) { + P[i] = wafflePoints(round(Y1[i] / unit), round(Y2[i] / unit), multiple).map(transform); + const c = P[i].pop(); // extract the transformed centroid + X[i] = c[ix] + mx(i); + Y[i] = c[iy] + y0; + } + + return { + channels: { + polygon: {value: P, source: null, filter: null}, + [`c${x}`]: {value: [cx, x0], source: null, filter: null}, + [`c${y}`]: {value: [cy, y0], source: null, filter: null}, + [x]: {value: X, scale: null, source: null}, + [y1]: {value: Y, scale: null, source: channels[y1]}, + [y2]: {value: Y, scale: null, source: channels[y2]} + } + }; + }); +} + +function waffleRender({render, ...options}) { + return { + ...options, + render: composeRender(render, function (index, scales, values, dimensions, context) { + const {gap, rx, ry} = this; + const {channels, ariaLabel, href, title, ...visualValues} = values; + const {document} = context; + const polygon = channels.polygon.value; + const [cx, x0] = channels.cx.value; + const [cy, y0] = channels.cy.value; + + // Create a base pattern with shared attributes for cloning. + const patternId = getPatternId(); + const basePattern = document.createElementNS(namespaces.svg, "pattern"); + basePattern.setAttribute("width", cx); + basePattern.setAttribute("height", cy); + basePattern.setAttribute("patternUnits", "userSpaceOnUse"); + const basePatternRect = basePattern.appendChild(document.createElementNS(namespaces.svg, "rect")); + basePatternRect.setAttribute("x", gap / 2); + basePatternRect.setAttribute("y", gap / 2); + basePatternRect.setAttribute("width", cx - gap); + basePatternRect.setAttribute("height", cy - gap); + if (rx != null) basePatternRect.setAttribute("rx", rx); + if (ry != null) basePatternRect.setAttribute("ry", ry); + + return create("svg:g", context) + .call(applyIndirectStyles, this, dimensions, context) + .call(this._transform, this, scales) + .call((g) => + g + .selectAll() + .data(index) + .enter() + .append(() => basePattern.cloneNode(true)) + .attr("id", (i) => `${patternId}-${i}`) + .select("rect") + .call(applyDirectStyles, this) + .call(applyChannelStyles, this, visualValues) + ) + .call((g) => + g + .selectAll() + .data(index) + .enter() + .append("path") + .attr("transform", template`translate(${x0},${y0})`) + .attr("d", (i) => `M${polygon[i].join("L")}Z`) + .attr("fill", (i) => `url(#${patternId}-${i})`) + .attr("stroke", this.stroke == null ? null : (i) => `url(#${patternId}-${i})`) + .call(applyChannelStyles, this, {ariaLabel, href, title}) + ) + .node(); + }) }; } -// A waffle is a approximately rectangular shape, but may have one or two corner +// A waffle is approximately a rectangular shape, but may have one or two corner // cuts if the starting or ending value is not an even multiple of the number of // columns (the width of the waffle in cells). We can represent any waffle by // 8 points; below is a waffle of five columns representing the interval 2–11: @@ -148,14 +187,11 @@ function waffleRender(y) { // Waffles can also represent fractional intervals (e.g., 2.4–10.1). These // require additional corner cuts, so the implementation below generates a few // more points. +// +// The last point describes the centroid (used for pointing) function wafflePoints(i1, i2, columns) { - if (i1 < 0 || i2 < 0) { - const k = Math.ceil(-Math.min(i1, i2) / columns); // shift negative to positive - return wafflePoints(i1 + k * columns, i2 + k * columns, columns).map(([x, y]) => [x, y - k]); - } - if (i2 < i1) { - return wafflePoints(i2, i1, columns); - } + if (i2 < i1) return wafflePoints(i2, i1, columns); // ensure i1 <= i2 + if (i1 < 0) return wafflePointsOffset(i1, i2, columns, Math.ceil(-Math.min(i1, i2) / columns)); // ensure i1 >= 0 const x1f = Math.floor(i1 % columns); const x1c = Math.ceil(i1 % columns); const x2f = Math.floor(i2 % columns); @@ -177,9 +213,49 @@ function wafflePoints(i1, i2, columns) { points.push([x2f, y2c]); if (y2c > y1c) points.push([0, y2c]); } + points.push(waffleCentroid(i1, i2, columns)); return points; } +function wafflePointsOffset(i1, i2, columns, k) { + return wafflePoints(i1 + k * columns, i2 + k * columns, columns).map(([x, y]) => [x, y - k]); +} + +function waffleCentroid(i1, i2, columns) { + const r = Math.floor(i2 / columns) - Math.floor(i1 / columns); + return r === 0 + ? // Single row + waffleRowCentroid(i1, i2, columns) + : r === 1 + ? // Two incomplete rows; use the midpoint of their overlap if any, otherwise the larger row + Math.floor(i2 % columns) > Math.ceil(i1 % columns) + ? [(Math.floor(i2 % columns) + Math.ceil(i1 % columns)) / 2, Math.floor(i2 / columns)] + : i2 % columns > columns - (i1 % columns) + ? waffleRowCentroid(i2 - (i2 % columns), i2, columns) + : waffleRowCentroid(i1, columns * Math.ceil(i1 / columns), columns) + : // At least one full row; take the midpoint of all the rows that include the middle + [columns / 2, (Math.round(i1 / columns) + Math.round(i2 / columns)) / 2]; +} + +function waffleRowCentroid(i1, i2, columns) { + const c = Math.floor(i2) - Math.floor(i1); + return c === 0 + ? // Single cell + [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (((i1 + i2) / 2) % 1)] + : c === 1 + ? // Two incomplete cells; use the overlap if large enough, otherwise use the largest + (i2 % 1) - (i1 % 1) > 0.5 + ? [Math.ceil(i1 % columns), Math.floor(i2 / columns) + ((i1 % 1) + (i2 % 1)) / 2] + : i2 % 1 > 1 - (i1 % 1) + ? [Math.floor(i2 % columns) + 0.5, Math.floor(i2 / columns) + (i2 % 1) / 2] + : [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (1 + (i1 % 1)) / 2] + : // At least one full cell; take the midpoint + [ + Math.ceil(i1 % columns) + Math.ceil(Math.floor(i2) - Math.ceil(i1)) / 2, + Math.floor(i1 / columns) + (i2 >= 1 + i1 ? 0.5 : ((i1 + i2) / 2) % 1) + ]; +} + function maybeRound(round) { if (round === undefined || round === false) return Number; if (round === true) return Math.round; @@ -200,12 +276,28 @@ function spread(domain) { return max - min; } -export function waffleX(data, options = {}) { +export function waffleX(data, {tip, ...options} = {}) { if (!hasXY(options)) options = {...options, y: indexOf, x2: identity}; - return new WaffleX(data, maybeStackX(maybeIntervalX(maybeIdentityX(options)))); + return new WaffleX(data, {tip: waffleTip(tip), ...maybeStackX(maybeIntervalX(maybeIdentityX(options)))}); } -export function waffleY(data, options = {}) { +export function waffleY(data, {tip, ...options} = {}) { if (!hasXY(options)) options = {...options, x: indexOf, y2: identity}; - return new WaffleY(data, maybeStackY(maybeIntervalY(maybeIdentityY(options)))); + return new WaffleY(data, {tip: waffleTip(tip), ...maybeStackY(maybeIntervalY(maybeIdentityY(options)))}); +} + +/** + * Waffle tips behave a bit unpredictably because we they are driven by the + * waffle centroid; you could be hovering over a waffle segment, but more than + * 40px away from its centroid, or closer to the centroid of another segment. + * We’d rather show a tip, even if it’s the “wrong” one, so we increase the + * default maxRadius to Infinity. The “right” way to fix this would be to use + * signed distance to the waffle geometry rather than the centroid. + */ +function waffleTip(tip) { + return tip === true + ? {maxRadius: Infinity} + : isObject(tip) && tip.maxRadius === undefined + ? {...tip, maxRadius: Infinity} + : undefined; } diff --git a/test/output/wafflePointer.svg b/test/output/wafflePointer.svg new file mode 100644 index 0000000000..d20a88f20c --- /dev/null +++ b/test/output/wafflePointer.svg @@ -0,0 +1,450 @@ + + + + + 0 + 5 + 10 + 15 + 20 + 25 + 30 + + + + 0 + 1 + 2 + + + x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/wafflePointerFractional.svg b/test/output/wafflePointerFractional.svg new file mode 100644 index 0000000000..7c587be531 --- /dev/null +++ b/test/output/wafflePointerFractional.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.51 + 0.99 + 0.5 + 6 + 0.3 + 1.6 + 9.1 + 2 + 18 + 6 + 0.5 + 2.5 + 46 + 34 + 20 + 7 + 0.5 + 0.1 + 0 + 2.5 + 1 + 0.1 + 0.8 + + + \ No newline at end of file diff --git a/test/output/waffleTip.svg b/test/output/waffleTip.svg new file mode 100644 index 0000000000..2d5277a659 --- /dev/null +++ b/test/output/waffleTip.svg @@ -0,0 +1,67 @@ + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/waffleTipFacet.svg b/test/output/waffleTipFacet.svg new file mode 100644 index 0000000000..4c35675277 --- /dev/null +++ b/test/output/waffleTipFacet.svg @@ -0,0 +1,2084 @@ + + + + + 0 + + + 1 + + + + + + 0 + 10 + 20 + 30 + 40 + 50 + 60 + 70 + 80 + + + + + + 0 + 1 + 2 + + + 0 + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/waffleTipFacetX.svg b/test/output/waffleTipFacetX.svg new file mode 100644 index 0000000000..7b1ec373e7 --- /dev/null +++ b/test/output/waffleTipFacetX.svg @@ -0,0 +1,2080 @@ + + + + + 0 + + + 1 + + + + + + 0 + 1 + 2 + + + + + + 0 + 20 + 40 + 60 + 80 + + + 0 + 20 + 40 + 60 + 80 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/waffleTipFacetXY.svg b/test/output/waffleTipFacetXY.svg new file mode 100644 index 0000000000..7923ce8524 --- /dev/null +++ b/test/output/waffleTipFacetXY.svg @@ -0,0 +1,2085 @@ + + + + + 0 + + + 1 + + + + + 0 + + + 1 + + + 2 + + + + + + 0 + 50 + + + 0 + 50 + + + 0 + 50 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/waffleTipUnit.svg b/test/output/waffleTipUnit.svg new file mode 100644 index 0000000000..41a47d10d2 --- /dev/null +++ b/test/output/waffleTipUnit.svg @@ -0,0 +1,447 @@ + + + + + 0 + 5 + 10 + 15 + 20 + 25 + 30 + + + + 0 + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/waffleTipUnitX.svg b/test/output/waffleTipUnitX.svg new file mode 100644 index 0000000000..8fb0bb719b --- /dev/null +++ b/test/output/waffleTipUnitX.svg @@ -0,0 +1,447 @@ + + + + + 0 + 1 + 2 + + + + 0 + 5 + 10 + 15 + 20 + 25 + 30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/waffleTipX.svg b/test/output/waffleTipX.svg new file mode 100644 index 0000000000..72ca0297f9 --- /dev/null +++ b/test/output/waffleTipX.svg @@ -0,0 +1,70 @@ + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + + + quantity → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/waffle.ts b/test/plots/waffle.ts index d5fe683bdf..8212911b19 100644 --- a/test/plots/waffle.ts +++ b/test/plots/waffle.ts @@ -1,5 +1,6 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; +import {svg} from "htl"; const demographics = d3.csvParse( `group,label,freq @@ -247,6 +248,101 @@ export async function waffleYGrouped() { }); } +export function wafflePointer() { + const random = d3.randomLcg(42); + const data = Array.from({length: 100}, (_, i) => ({x: i % 3, fill: random()})); + return Plot.plot({ + y: {inset: 12}, + marks: [ + Plot.waffleY(data, {x: "x", y: 1, fill: "#888"}), + Plot.waffleY(data, Plot.pointer({x: "x", y: 1, fill: "fill"})) + ] + }); +} + +export function wafflePointerFractional() { + const values = [0.51, 0.99, 0.5, 6, 0.3, 1.6, 9.1, 2, 18, 6, 0.5, 2.5, 46, 34, 20, 7, 0.5, 0.1, 0, 2.5, 1, 0.1, 0.8]; + const multiple = 16; + return Plot.plot({ + axis: null, + y: {insetTop: 12}, + color: {scheme: "Dark2"}, + marks: [ + Plot.waffleY(values, { + x: null, + multiple, + fill: (d, i) => i % 7, + tip: true + }), + Plot.waffleY(values, { + x: null, + multiple, + // eslint-disable-next-line + render: (index, scales, values, dimensions, context, next) => { + const format = (d: number) => +d.toFixed(2); + const y1 = (values.channels.y1 as any).source.value; + const y2 = (values.channels.y2 as any).source.value; + return svg`${Array.from( + index, + (i) => + svg`${format(y2[i] - y1[i])}` + )}`; + } + }) + ] + }); +} + +export function waffleTip() { + return Plot.plot({ + color: {type: "sqrt", scheme: "spectral"}, + y: {inset: 12}, + marks: [Plot.waffleY([1, 4, 9, 24, 46, 66, 7], {x: null, fill: Plot.identity, tip: true})] + }); +} + +export function waffleTipUnit() { + return Plot.plot({ + y: {inset: 12}, + marks: [Plot.waffleY({length: 100}, {x: (d, i) => i % 3, y: 1, fill: d3.randomLcg(42), tip: true})] + }); +} + +export function waffleTipFacet() { + return Plot.plot({ + marks: [ + Plot.waffleY({length: 500}, {x: (d, i) => i % 3, fx: (d, i) => i % 2, y: 1, fill: d3.randomLcg(42), tip: true}) + ] + }); +} + +export function waffleTipX() { + return Plot.plot({ + style: {overflow: "visible"}, + color: {type: "sqrt", scheme: "spectral"}, + x: {label: "quantity"}, + y: {inset: 12}, + marks: [Plot.waffleX([1, 4, 9, 24, 46, 66, 7], {y: null, fill: Plot.identity, tip: true})] + }); +} + +export function waffleTipUnitX() { + return Plot.plot({ + height: 300, + y: {inset: 12}, + marks: [ + Plot.waffleX( + {length: 100}, + {multiple: 5, y: (d, i) => i % 3, x: 1, fill: d3.randomLcg(42), tip: {format: {x: false}}} + ) + ] + }); +} + export function waffleHref() { return Plot.plot({ inset: 10, @@ -265,6 +361,24 @@ export function waffleHref() { }); } +export function waffleTipFacetX() { + return Plot.plot({ + height: 500, + marks: [ + Plot.waffleX({length: 500}, {y: (d, i) => i % 3, fx: (d, i) => i % 2, x: 1, fill: d3.randomLcg(42), tip: true}) + ] + }); +} + +export function waffleTipFacetXY() { + return Plot.plot({ + height: 600, + marks: [ + Plot.waffleX({length: 500}, {fx: (d, i) => i % 3, fy: (d, i) => i % 2, x: 1, fill: d3.randomLcg(42), tip: true}) + ] + }); +} + export function waffleShapes() { const k = 10; let offset = 0; From 5fb8616447a3e1cfd98841a458ac5f026fa0c34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Nov 2024 23:40:46 +0100 Subject: [PATCH 2/5] fix waffle stroke (when constant) (#2204) Co-authored-by: Mike Bostock --- src/marks/waffle.js | 2 +- test/output/waffleShapes.svg | 40 +-- test/output/waffleStroke.svg | 24 +- test/output/waffleStrokeMixed.svg | 12 +- test/output/waffleStrokeNegative.svg | 12 +- test/output/waffleStrokePositive.svg | 12 +- test/output/waffleStrokeWidth.svg | 346 +++++++++++++++++++++++++ test/output/waffleStrokeWidthConst.svg | 346 +++++++++++++++++++++++++ test/plots/waffle.ts | 14 + 9 files changed, 757 insertions(+), 51 deletions(-) create mode 100644 test/output/waffleStrokeWidth.svg create mode 100644 test/output/waffleStrokeWidthConst.svg diff --git a/src/marks/waffle.js b/src/marks/waffle.js index 3b2b6d5774..1e848bcc7e 100644 --- a/src/marks/waffle.js +++ b/src/marks/waffle.js @@ -142,7 +142,7 @@ function waffleRender({render, ...options}) { .attr("transform", template`translate(${x0},${y0})`) .attr("d", (i) => `M${polygon[i].join("L")}Z`) .attr("fill", (i) => `url(#${patternId}-${i})`) - .attr("stroke", this.stroke == null ? null : (i) => `url(#${patternId}-${i})`) + .attr("stroke", this.stroke == null ? null : "none") .call(applyChannelStyles, this, {ariaLabel, href, title}) ) .node(); diff --git a/test/output/waffleShapes.svg b/test/output/waffleShapes.svg index c1e66aeb8a..798d51ad81 100644 --- a/test/output/waffleShapes.svg +++ b/test/output/waffleShapes.svg @@ -83,120 +83,120 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + \ No newline at end of file diff --git a/test/output/waffleStroke.svg b/test/output/waffleStroke.svg index 2ade48d2f0..b8b4d65b17 100644 --- a/test/output/waffleStroke.svg +++ b/test/output/waffleStroke.svg @@ -66,12 +66,12 @@ - - - - - - + + + + + + @@ -92,11 +92,11 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/test/output/waffleStrokeMixed.svg b/test/output/waffleStrokeMixed.svg index fceae2f5af..a8fe5022d4 100644 --- a/test/output/waffleStrokeMixed.svg +++ b/test/output/waffleStrokeMixed.svg @@ -70,12 +70,12 @@ - - - - - - + + + + + + diff --git a/test/output/waffleStrokeNegative.svg b/test/output/waffleStrokeNegative.svg index 8e03565a10..263cc64772 100644 --- a/test/output/waffleStrokeNegative.svg +++ b/test/output/waffleStrokeNegative.svg @@ -100,12 +100,12 @@ - - - - - - + + + + + + diff --git a/test/output/waffleStrokePositive.svg b/test/output/waffleStrokePositive.svg index cb2ab9cd2b..7e6499571f 100644 --- a/test/output/waffleStrokePositive.svg +++ b/test/output/waffleStrokePositive.svg @@ -100,12 +100,12 @@ - - - - - - + + + + + + diff --git a/test/output/waffleStrokeWidth.svg b/test/output/waffleStrokeWidth.svg new file mode 100644 index 0000000000..1667ceb425 --- /dev/null +++ b/test/output/waffleStrokeWidth.svg @@ -0,0 +1,346 @@ + + + + + 0 + 10 + 20 + 30 + 40 + 50 + 60 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/waffleStrokeWidthConst.svg b/test/output/waffleStrokeWidthConst.svg new file mode 100644 index 0000000000..8ca80f7ae4 --- /dev/null +++ b/test/output/waffleStrokeWidthConst.svg @@ -0,0 +1,346 @@ + + + + + 0 + 10 + 20 + 30 + 40 + 50 + 60 + 70 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/waffle.ts b/test/plots/waffle.ts index 8212911b19..ead9a1bf96 100644 --- a/test/plots/waffle.ts +++ b/test/plots/waffle.ts @@ -361,6 +361,20 @@ export function waffleHref() { }); } +export function waffleStrokeWidth() { + return Plot.plot({ + inset: 10, + marks: [Plot.waffleY({length: 77}, {y: 1, stroke: (d, i) => i % 7, gap: 15, strokeWidth: 15, strokeOpacity: 0.8})] + }); +} + +export function waffleStrokeWidthConst() { + return Plot.plot({ + inset: 10, + marks: [Plot.waffleY({length: 77}, {y: 1, stroke: "black", gap: 15, strokeWidth: 15, strokeOpacity: 0.8})] + }); +} + export function waffleTipFacetX() { return Plot.plot({ height: 500, From f889fd920ce6e2ba1765abc711ee99a17699132b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 22 Nov 2024 13:13:14 -0800 Subject: [PATCH 3/5] add examples to sidebar (#2247) --- docs/.vitepress/config.ts | 3 ++- docs/features/plots.md | 6 ++++++ docs/getting-started.md | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 1edd74a8cf..f1a0381ac8 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -60,7 +60,8 @@ export default defineConfig({ items: [ {text: "What is Plot?", link: "/what-is-plot"}, {text: "Why Plot?", link: "/why-plot"}, - {text: "Getting started", link: "/getting-started"} + {text: "Getting started", link: "/getting-started"}, + {text: "Examples", link: "https://observablehq.com/@observablehq/plot-gallery"} ] }, { diff --git a/docs/features/plots.md b/docs/features/plots.md index 34548522e7..ce1e3e89ed 100644 --- a/docs/features/plots.md +++ b/docs/features/plots.md @@ -1,3 +1,9 @@ +--- +prev: + text: Getting started + link: /getting-started +--- +