diff --git a/README.md b/README.md index cb65820f43..15bd682ec9 100644 --- a/README.md +++ b/README.md @@ -691,6 +691,7 @@ All marks support the following style options: * **target** - link target (e.g., “_blank” for a new window); for use with the **href** channel * **ariaDescription** - a textual description of the mark’s contents * **ariaHidden** - if true, hide this content from the accessibility tree +* **pointerEvents** - the [pointer events](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events) (*e.g.*, *none*) * **clip** - if true, the mark is clipped to the frame’s dimensions For all marks except [text](#plottextdata-options), the **dx** and **dy** options are rendered as a transform property, possibly including a 0.5px offset on low-density screens. @@ -982,6 +983,42 @@ Plot.cellY(simpsons.map(d => d.imdb_rating)) Equivalent to [Plot.cell](#plotcelldata-options), except that if the **y** option is not specified, it defaults to [0, 1, 2, …], and if the **fill** option is not specified and **stroke** is not a channel, the fill defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …]. +### Delaunay + +[a Voronoi diagram of penguin culmens, showing the length and depth of several species](https://observablehq.com/@observablehq/plot-delaunay) + +[Source](./src/marks/delaunay.js) · [Examples](https://observablehq.com/@observablehq/plot-delaunay) · Plot provides a handful of marks for Delaunay and Voronoi diagrams (using [d3-delaunay](https://github.com/d3/d3-delaunay) and [Delaunator](https://github.com/mapbox/delaunator)). These marks require the **x** and **y** channels to be specified. + +#### Plot.delaunayLink(*data*, *options*) + +Draws links for each edge of the Delaunay triangulation of the points given by the **x** and **y** channels. Supports the same options as the [link mark](#link), except that **x1**, **y1**, **x2**, and **y2** are derived automatically from **x** and **y**. When an aesthetic channel is specified (such as **stroke** or **strokeWidth**), the link inherits the corresponding channel value from one of its two endpoints arbitrarily. + +If a **z** channel is specified, the input points are grouped by *z*, and separate Delaunay triangulations are constructed for each group. + +#### Plot.delaunayMesh(*data*, *options*) + +Draws a mesh of the Delaunay triangulation of the points given by the **x** and **y** channels. The **stroke** option defaults to _currentColor_, and the **strokeOpacity** defaults to 0.2. The **fill** option is not supported. When an aesthetic channel is specified (such as **stroke** or **strokeWidth**), the mesh inherits the corresponding channel value from one of its constituent points arbitrarily. + +If a **z** channel is specified, the input points are grouped by *z*, and separate Delaunay triangulations are constructed for each group. + +#### Plot.hull(*data*, *options*) + +Draws a convex hull around the points given by the **x** and **y** channels. The **stroke** option defaults to _currentColor_ and the **fill** option defaults to _none_. When an aesthetic channel is specified (such as **stroke** or **strokeWidth**), the hull inherits the corresponding channel value from one of its constituent points arbitrarily. + +If a **z** channel is specified, the input points are grouped by *z*, and separate convex hulls are constructed for each group. If the **z** channel is not specified, it defaults to either the **fill** channel, if any, or the **stroke** channel, if any. + +#### Plot.voronoi(*data*, *options*) + +Draws polygons for each cell of the Voronoi tesselation of the points given by the **x** and **y** channels. + +If a **z** channel is specified, the input points are grouped by *z*, and separate Voronoi tesselations are constructed for each group. + +#### Plot.voronoiMesh(*data*, *options*) + +Draws a mesh for the cell boundaries of the Voronoi tesselation of the points given by the **x** and **y** channels. The **stroke** option defaults to _currentColor_, and the **strokeOpacity** defaults to 0.2. The **fill** option is not supported. When an aesthetic channel is specified (such as **stroke** or **strokeWidth**), the mesh inherits the corresponding channel value from one of its constituent points arbitrarily. + +If a **z** channel is specified, the input points are grouped by *z*, and separate Voronoi tesselations are constructed for each group. + ### Dot [a scatterplot](https://observablehq.com/@observablehq/plot-dot) diff --git a/img/voronoi.png b/img/voronoi.png new file mode 100644 index 0000000000..90a8431a3f Binary files /dev/null and b/img/voronoi.png differ diff --git a/src/index.js b/src/index.js index 6bb74d0e41..21315476bb 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ export {Arrow, arrow} from "./marks/arrow.js"; export {BarX, BarY, barX, barY} from "./marks/bar.js"; export {boxX, boxY} from "./marks/box.js"; export {Cell, cell, cellX, cellY} from "./marks/cell.js"; +export {delaunayLink, delaunayMesh, hull, voronoi, voronoiMesh} from "./marks/delaunay.js"; export {Dot, dot, dotX, dotY, circle, hexagon} from "./marks/dot.js"; export {Frame, frame} from "./marks/frame.js"; export {Hexgrid, hexgrid} from "./marks/hexgrid.js"; diff --git a/src/marks/delaunay.js b/src/marks/delaunay.js new file mode 100644 index 0000000000..16d4d85287 --- /dev/null +++ b/src/marks/delaunay.js @@ -0,0 +1,258 @@ +import {create, group, path, select, Delaunay} from "d3"; +import {Curve} from "../curve.js"; +import {maybeTuple, maybeZ} from "../options.js"; +import {Mark} from "../plot.js"; +import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js"; +import {markers, applyMarkers} from "./marker.js"; + +const delaunayLinkDefaults = { + ariaLabel: "delaunay link", + fill: "none", + stroke: "currentColor", + strokeMiterlimit: 1 +}; + +const delaunayMeshDefaults = { + ariaLabel: "delaunay mesh", + fill: null, + stroke: "currentColor", + strokeOpacity: 0.2 +}; + +const hullDefaults = { + ariaLabel: "hull", + fill: "none", + stroke: "currentColor", + strokeWidth: 1.5, + strokeMiterlimit: 1 +}; + +const voronoiDefaults = { + ariaLabel: "voronoi", + fill: "none", + stroke: "currentColor", + strokeMiterlimit: 1 +}; + +const voronoiMeshDefaults = { + ariaLabel: "voronoi mesh", + fill: null, + stroke: "currentColor", + strokeOpacity: 0.2 +}; + +class DelaunayLink extends Mark { + constructor(data, options = {}) { + const {x, y, z, curve, tension} = options; + super( + data, + [ + {name: "x", value: x, scale: "x"}, + {name: "y", value: y, scale: "y"}, + {name: "z", value: z, optional: true} + ], + options, + delaunayLinkDefaults + ); + this.curve = Curve(curve, tension); + markers(this, options); + } + render(index, {x, y}, channels, dimensions) { + const {x: X, y: Y, z: Z} = channels; + const {dx, dy, curve} = this; + const mark = this; + + function links(index) { + let i = -1; + const newIndex = []; + const newChannels = {}; + for (const k in channels) newChannels[k] = []; + const X1 = []; + const X2 = []; + const Y1 = []; + const Y2 = []; + + function link(ti, tj) { + ti = index[ti]; + tj = index[tj]; + newIndex.push(++i); + X1[i] = X[ti]; + Y1[i] = Y[ti]; + X2[i] = X[tj]; + Y2[i] = Y[tj]; + for (const k in channels) newChannels[k].push(channels[k][tj]); + } + + const {halfedges, hull, triangles} = Delaunay.from(index, i => X[i], i => Y[i]); + for (let i = 0; i < halfedges.length; ++i) { // inner edges + const j = halfedges[i]; + if (j > i) link(triangles[i], triangles[j]); + } + for (let i = 0; i < hull.length; ++i) { // convex hull + link(hull[i], hull[(i + 1) % hull.length]); + } + + select(this) + .selectAll() + .data(newIndex) + .join("path") + .call(applyDirectStyles, mark) + .attr("d", i => { + const p = path(); + const c = curve(p); + c.lineStart(); + c.point(X1[i], Y1[i]); + c.point(X2[i], Y2[i]); + c.lineEnd(); + return p; + }) + .call(applyChannelStyles, mark, newChannels) + .call(applyMarkers, mark, newChannels); + } + + return create("svg:g") + .call(applyIndirectStyles, this, dimensions) + .call(applyTransform, x, y, offset + dx, offset + dy) + .call(Z + ? g => g.selectAll().data(group(index, i => Z[i]).values()).enter().append("g").each(links) + : g => g.datum(index).each(links)) + .node(); + } +} + +class AbstractDelaunayMark extends Mark { + constructor(data, options = {}, defaults, zof = ({z}) => z) { + const {x, y} = options; + super( + data, + [ + {name: "x", value: x, scale: "x"}, + {name: "y", value: y, scale: "y"}, + {name: "z", value: zof(options), optional: true} + ], + options, + defaults + ); + } + render(index, {x, y}, {x: X, y: Y, z: Z, ...channels}, dimensions) { + const {dx, dy} = this; + const mark = this; + function mesh(render) { + return function(index) { + const delaunay = Delaunay.from(index, i => X[i], i => Y[i]); + select(this).append("path") + .datum(index[0]) + .call(applyDirectStyles, mark) + .attr("d", render(delaunay, dimensions)) + .call(applyChannelStyles, mark, channels); + }; + } + return create("svg:g") + .call(applyIndirectStyles, this, dimensions) + .call(applyTransform, x, y, offset + dx, offset + dy) + .call(Z + ? g => g.selectAll().data(group(index, i => Z[i]).values()).enter().append("g").each(mesh(this._render)) + : g => g.datum(index).each(mesh(this._render))) + .node(); + } +} + +class DelaunayMesh extends AbstractDelaunayMark { + constructor(data, options = {}) { + super(data, options, delaunayMeshDefaults); + this.fill = "none"; + } + _render(delaunay) { + return delaunay.render(); + } +} + +class Hull extends AbstractDelaunayMark { + constructor(data, options = {}) { + super(data, options, hullDefaults, maybeZ); + } + _render(delaunay) { + return delaunay.renderHull(); + } +} + +class Voronoi extends Mark { + constructor(data, options = {}) { + const {x, y, z} = options; + super( + data, + [ + {name: "x", value: x, scale: "x"}, + {name: "y", value: y, scale: "y"}, + {name: "z", value: z, optional: true} + ], + options, + voronoiDefaults + ); + } + render(index, {x, y}, channels, dimensions) { + const {x: X, y: Y, z: Z} = channels; + const {dx, dy} = this; + + function cells(index) { + const delaunay = Delaunay.from(index, i => X[i], i => Y[i]); + const voronoi = voronoiof(delaunay, dimensions); + select(this) + .selectAll() + .data(index) + .enter() + .append("path") + .call(applyDirectStyles, this) + .attr("d", (_, i) => voronoi.renderCell(i)) + .call(applyChannelStyles, this, channels); + } + + return create("svg:g") + .call(applyIndirectStyles, this, dimensions) + .call(applyTransform, x, y, offset + dx, offset + dy) + .call(Z + ? g => g.selectAll().data(group(index, i => Z[i]).values()).enter().append("g").each(cells) + : g => g.datum(index).each(cells)) + .node(); + } +} + +class VoronoiMesh extends AbstractDelaunayMark { + constructor(data, options) { + super(data, options, voronoiMeshDefaults); + this.fill = "none"; + } + _render(delaunay, dimensions) { + return voronoiof(delaunay, dimensions).render(); + } +} + +function voronoiof(delaunay, dimensions) { + const {width, height, marginTop, marginRight, marginBottom, marginLeft} = dimensions; + return delaunay.voronoi([marginLeft, marginTop, width - marginRight, height - marginBottom]); +} + +function delaunayMark(DelaunayMark, data, {x, y, ...options} = {}) { + ([x, y] = maybeTuple(x, y)); + return new DelaunayMark(data, {...options, x, y}); +} + +export function delaunayLink(data, options) { + return delaunayMark(DelaunayLink, data, options); +} + +export function delaunayMesh(data, options) { + return delaunayMark(DelaunayMesh, data, options); +} + +export function hull(data, options) { + return delaunayMark(Hull, data, options); +} + +export function voronoi(data, options) { + return delaunayMark(Voronoi, data, options); +} + +export function voronoiMesh(data, options) { + return delaunayMark(VoronoiMesh, data, options); +} diff --git a/src/marks/marker.js b/src/marks/marker.js index cb18d14905..8d63d7fb90 100644 --- a/src/marks/marker.js +++ b/src/marks/marker.js @@ -77,11 +77,11 @@ function markerCircleStroke(color) { let nextMarkerId = 0; -export function applyMarkers(path, mark, {stroke: S}) { +export function applyMarkers(path, mark, {stroke: S} = {}) { return applyMarkersColor(path, mark, S && (i => S[i])); } -export function applyGroupedMarkers(path, mark, {stroke: S}) { +export function applyGroupedMarkers(path, mark, {stroke: S} = {}) { return applyMarkersColor(path, mark, S && (([i]) => S[i])); } diff --git a/src/style.js b/src/style.js index 919c6bf2a6..4fc0804dff 100644 --- a/src/style.js +++ b/src/style.js @@ -30,6 +30,7 @@ export function styles( opacity, mixBlendMode, paintOrder, + pointerEvents, shapeRendering }, { @@ -121,6 +122,7 @@ export function styles( mark.opacity = impliedNumber(copacity, 1); mark.mixBlendMode = impliedString(mixBlendMode, "normal"); mark.paintOrder = impliedString(paintOrder, "normal"); + mark.pointerEvents = impliedString(pointerEvents, "auto"); mark.shapeRendering = impliedString(shapeRendering, "auto"); return [ @@ -261,6 +263,7 @@ export function applyIndirectStyles(selection, mark, {width, height, marginLeft, applyAttr(selection, "stroke-dashoffset", mark.strokeDashoffset); applyAttr(selection, "shape-rendering", mark.shapeRendering); applyAttr(selection, "paint-order", mark.paintOrder); + applyAttr(selection, "pointer-events", mark.pointerEvents); if (mark.clip === "frame") { const id = `plot-clip-${++nextClipId}`; selection diff --git a/test/output/penguinCulmenDelaunay.svg b/test/output/penguinCulmenDelaunay.svg new file mode 100644 index 0000000000..cd7b5bd723 --- /dev/null +++ b/test/output/penguinCulmenDelaunay.svg @@ -0,0 +1,1086 @@ + + + + + 34 + + + 36 + + + 38 + + + 40 + + + 42 + + + 44 + + + 46 + + + 48 + + + 50 + + + 52 + + + 54 + + + 56 + + + 58 + ↑ culmen_length_mm + + + + 14 + + + 15 + + + 16 + + + 17 + + + 18 + + + 19 + + + 20 + + + 21 + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/penguinCulmenDelaunayMesh.svg b/test/output/penguinCulmenDelaunayMesh.svg new file mode 100644 index 0000000000..5386cc79cd --- /dev/null +++ b/test/output/penguinCulmenDelaunayMesh.svg @@ -0,0 +1,430 @@ + + + + + 34 + + + 36 + + + 38 + + + 40 + + + 42 + + + 44 + + + 46 + + + 48 + + + 50 + + + 52 + + + 54 + + + 56 + + + 58 + ↑ culmen_length_mm + + + + 14 + + + 15 + + + 16 + + + 17 + + + 18 + + + 19 + + + 20 + + + 21 + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/penguinCulmenDelaunaySpecies.svg b/test/output/penguinCulmenDelaunaySpecies.svg new file mode 100644 index 0000000000..5677e8ed9e --- /dev/null +++ b/test/output/penguinCulmenDelaunaySpecies.svg @@ -0,0 +1,105 @@ + + + + + 34 + + + 36 + + + 38 + + + 40 + + + 42 + + + 44 + + + 46 + + + 48 + + + 50 + + + 52 + + + 54 + + + 56 + + + 58 + ↑ culmen_length_mm + + + + 14 + + + 15 + + + 16 + + + 17 + + + 18 + + + 19 + + + 20 + + + 21 + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/penguinCulmenVoronoi.svg b/test/output/penguinCulmenVoronoi.svg new file mode 100644 index 0000000000..7c9cd5a4a7 --- /dev/null +++ b/test/output/penguinCulmenVoronoi.svg @@ -0,0 +1,771 @@ + + + + + 34 + + + 36 + + + 38 + + + 40 + + + 42 + + + 44 + + + 46 + + + 48 + + + 50 + + + 52 + + + 54 + + + 56 + + + 58 + ↑ culmen_length_mm + + + + 14 + + + 15 + + + 16 + + + 17 + + + 18 + + + 19 + + + 20 + + + 21 + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/index.js b/test/plots/index.js index bfcf4724a3..c0665b8749 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -122,6 +122,10 @@ export {default as musicRevenue} from "./music-revenue.js"; export {default as ordinalBar} from "./ordinal-bar.js"; export {default as penguinCulmen} from "./penguin-culmen.js"; export {default as penguinCulmenArray} from "./penguin-culmen-array.js"; +export {default as penguinCulmenDelaunay} from "./penguin-culmen-delaunay.js"; +export {default as penguinCulmenDelaunayMesh} from "./penguin-culmen-delaunay-mesh.js"; +export {default as penguinCulmenDelaunaySpecies} from "./penguin-culmen-delaunay-species.js"; +export {default as penguinCulmenVoronoi} from "./penguin-culmen-voronoi.js"; export {default as penguinDodge} from "./penguin-dodge.js"; export {default as penguinDodgeHexbin} from "./penguin-dodge-hexbin.js"; export {default as penguinFacetDodge} from "./penguin-facet-dodge.js"; diff --git a/test/plots/penguin-culmen-delaunay-mesh.js b/test/plots/penguin-culmen-delaunay-mesh.js new file mode 100644 index 0000000000..d86203533e --- /dev/null +++ b/test/plots/penguin-culmen-delaunay-mesh.js @@ -0,0 +1,12 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const data = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + marks: [ + Plot.delaunayMesh(data, {x: "culmen_depth_mm", y: "culmen_length_mm"}), + Plot.dot(data, {x: "culmen_depth_mm", y: "culmen_length_mm"}) + ] + }); +} diff --git a/test/plots/penguin-culmen-delaunay-species.js b/test/plots/penguin-culmen-delaunay-species.js new file mode 100644 index 0000000000..a3ad29e4e3 --- /dev/null +++ b/test/plots/penguin-culmen-delaunay-species.js @@ -0,0 +1,12 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const data = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + marks: [ + Plot.delaunayMesh(data, {x: "culmen_depth_mm", y: "culmen_length_mm", z: "species", stroke: "species", strokeOpacity: 1}), + Plot.hull(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke: "species", strokeWidth: 3}) + ] + }); +} diff --git a/test/plots/penguin-culmen-delaunay.js b/test/plots/penguin-culmen-delaunay.js new file mode 100644 index 0000000000..beb612db25 --- /dev/null +++ b/test/plots/penguin-culmen-delaunay.js @@ -0,0 +1,11 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const data = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + marks: [ + Plot.delaunayLink(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke: "culmen_length_mm"}) + ] + }); +} diff --git a/test/plots/penguin-culmen-voronoi.js b/test/plots/penguin-culmen-voronoi.js new file mode 100644 index 0000000000..0bcf10cf7b --- /dev/null +++ b/test/plots/penguin-culmen-voronoi.js @@ -0,0 +1,12 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + marks: [ + Plot.dot(penguins, {x: "culmen_depth_mm", y: "culmen_length_mm", fill: "currentColor", r: 1.5}), + Plot.voronoi(penguins, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke: "species"}) + ] + }); +}