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 @@
+
\ 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"})
]
});
}