diff --git a/src/marks/delaunay.js b/src/marks/delaunay.js index f233c3f4df..3af87639db 100644 --- a/src/marks/delaunay.js +++ b/src/marks/delaunay.js @@ -1,6 +1,6 @@ -import {create, path, Delaunay} from "d3"; +import {create, group, path, select, Delaunay} from "d3"; import {Curve} from "../curve.js"; -import {maybeTuple} from "../options.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"; @@ -28,12 +28,13 @@ const hullDefaults = { export class DelaunayLink extends Mark { constructor(data, options = {}) { - const {x, y, curve, tension} = options; + const {x, y, z, curve, tension} = options; super( data, [ {name: "x", value: x, scale: "x"}, - {name: "y", value: y, scale: "y"} + {name: "y", value: y, scale: "y"}, + {name: "z", value: z, optional: true} ], options, linkDefaults @@ -42,70 +43,77 @@ export class DelaunayLink extends Mark { markers(this, options); } render(index, {x, y}, channels, dimensions) { - const {x: X, y: Y} = channels; + const {x: X, y: Y, z: Z} = channels; const {dx, dy, curve} = this; - let i = -1; - const newIndex = []; - const newChannels = {}; - for (const k in channels) newChannels[k] = []; + const mark = this; - 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]); - } + 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 = []; - // TODO Group by z or stroke. - const {halfedges, hull, triangles} = Delaunay.from(index, i => X[i], i => Y[i]); - const m = (halfedges.length >> 1) + hull.length; - const X1 = new Float64Array(m); - const X2 = new Float64Array(m); - const Y1 = new Float64Array(m); - const Y2 = new Float64Array(m); - 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]); + 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(g => g.selectAll() - .data(newIndex) - .enter() - .append("path") - .call(applyDirectStyles, this) - .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, this, newChannels) - .call(applyMarkers, this, newChannels)) + .call(Z + ? g => g.selectAll().data(group(index, i => Z[i]).values()).enter().append("g").each(links) + : g => g.datum(index).each(links)) .node(); } } export class DelaunayMesh extends Mark { constructor(data, options = {}, defaults = meshDefaults) { - const {x, y} = options; + const {x, y, z, stroke} = options; super( data, [ {name: "x", value: x, scale: "x"}, - {name: "y", value: y, scale: "y"} + {name: "y", value: y, scale: "y"}, + {name: "z", value: maybeZ({z, stroke}), optional: true} ], options, defaults @@ -115,16 +123,25 @@ export class DelaunayMesh extends Mark { _render(delaunay) { return delaunay.render(); } - render(index, {x, y}, {x: X, y: Y}, dimensions) { + render(index, {x, y}, {x: X, y: Y, z: Z, ...channels}, dimensions) { const {dx, dy} = this; - // TODO Group by z or stroke. - const delaunay = Delaunay.from(index, i => X[i], i => Y[i]); + 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(g => g.append("path") - .call(applyDirectStyles, this) - .call(applyTransform, x, y, offset + dx, offset + dy) - .attr("d", this._render(delaunay, 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(); } } diff --git a/test/output/penguinCulmenDelaunay.svg b/test/output/penguinCulmenDelaunay.svg index 0b5463d6f8..cd7b5bd723 100644 --- a/test/output/penguinCulmenDelaunay.svg +++ b/test/output/penguinCulmenDelaunay.svg @@ -1083,7 +1083,4 @@ - - - \ No newline at end of file diff --git a/test/output/penguinCulmenDelaunayMesh.svg b/test/output/penguinCulmenDelaunayMesh.svg index 434e9f27a3..5386cc79cd 100644 --- a/test/output/penguinCulmenDelaunayMesh.svg +++ b/test/output/penguinCulmenDelaunayMesh.svg @@ -80,8 +80,8 @@ 21 culmen_depth_mm → - - + + diff --git a/test/output/penguinCulmenDelaunaySpecies.svg b/test/output/penguinCulmenDelaunaySpecies.svg new file mode 100644 index 0000000000..95cc67db33 --- /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/plots/index.js b/test/plots/index.js index 8d9548980d..c0665b8749 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -124,6 +124,7 @@ 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"; diff --git a/test/plots/penguin-culmen-delaunay-species.js b/test/plots/penguin-culmen-delaunay-species.js new file mode 100644 index 0000000000..c5a08e19dc --- /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", 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 index 7c6f0f5e1b..beb612db25 100644 --- a/test/plots/penguin-culmen-delaunay.js +++ b/test/plots/penguin-culmen-delaunay.js @@ -5,8 +5,7 @@ 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"}), - Plot.hull(data, {x: "culmen_depth_mm", y: "culmen_length_mm"}) + Plot.delaunayLink(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke: "culmen_length_mm"}) ] }); }