diff --git a/src/index.js b/src/index.js
index a6e5dc7a48..fdf6606da7 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,7 +4,16 @@ 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 {
+ delaunayLink,
+ delaunayMesh,
+ hull,
+ voronoi,
+ voronoiMesh,
+ gabrielMesh,
+ urquhartMesh,
+ mstMesh
+} from "./marks/delaunay.js";
export {Density, density} from "./marks/density.js";
export {Dot, dot, dotX, dotY, circle, hexagon} from "./marks/dot.js";
export {Frame, frame} from "./marks/frame.js";
diff --git a/src/marks/delaunay.js b/src/marks/delaunay.js
index 22d43015d9..b08d5b5de4 100644
--- a/src/marks/delaunay.js
+++ b/src/marks/delaunay.js
@@ -1,4 +1,4 @@
-import {group, path, select, Delaunay} from "d3";
+import {bisector, extent, group, path, select, Delaunay} from "d3";
import {create} from "../context.js";
import {Curve} from "../curve.js";
import {constant, maybeTuple, maybeZ} from "../options.js";
@@ -298,3 +298,130 @@ export function voronoi(data, options) {
export function voronoiMesh(data, options) {
return delaunayMark(VoronoiMesh, data, options);
}
+
+class GabrielMesh extends AbstractDelaunayMark {
+ constructor(data, options) {
+ super(data, options, voronoiMeshDefaults);
+ this.fill = "none";
+ }
+ _accept(delaunay) {
+ const {points, triangles} = delaunay;
+ return (i) => {
+ const a = triangles[i];
+ const b = triangles[i % 3 === 2 ? i - 2 : i + 1];
+ return [a, b].includes(
+ delaunay.find((points[2 * a] + points[2 * b]) / 2, (points[2 * a + 1] + points[2 * b + 1]) / 2, a)
+ );
+ };
+ }
+ _render(delaunay) {
+ const p = new path();
+ const {points, halfedges, triangles} = delaunay;
+ const accept = this._accept(delaunay);
+ for (let i = 0, n = triangles.length; i < n; ++i) {
+ const j = halfedges[i];
+ if (i < j) continue;
+ if (accept(i)) {
+ const a = triangles[i];
+ const b = triangles[i % 3 === 2 ? i - 2 : i + 1];
+ p.moveTo(points[2 * a], points[2 * a + 1]);
+ p.lineTo(points[2 * b], points[2 * b + 1]);
+ }
+ }
+ return "" + p;
+ }
+}
+
+class UrquhartMesh extends GabrielMesh {
+ constructor(data, options) {
+ super(data, options, voronoiMeshDefaults);
+ this.fill = "none";
+ }
+ _accept(delaunay, score = euclidean2) {
+ const {halfedges, points, triangles} = delaunay;
+ const n = triangles.length;
+ const removed = new Uint8Array(n);
+ for (let e = 0; e < n; e += 3) {
+ const p0 = triangles[e],
+ p1 = triangles[e + 1],
+ p2 = triangles[e + 2];
+ const p01 = score(points, p0, p1),
+ p12 = score(points, p1, p2),
+ p20 = score(points, p2, p0);
+ removed[
+ p20 > p01 && p20 > p12
+ ? Math.max(e + 2, halfedges[e + 2])
+ : p12 > p01 && p12 > p20
+ ? Math.max(e + 1, halfedges[e + 1])
+ : Math.max(e, halfedges[e])
+ ] = 1;
+ }
+ return (i) => !removed[i];
+ }
+}
+
+function euclidean2(points, i, j) {
+ return (points[i * 2] - points[j * 2]) ** 2 + (points[i * 2 + 1] - points[j * 2 + 1]) ** 2;
+}
+
+class MSTMesh extends GabrielMesh {
+ constructor(data, options) {
+ super(data, options, voronoiMeshDefaults);
+ this.fill = "none";
+ }
+ _accept(delaunay, score = euclidean2) {
+ const {points, triangles} = delaunay;
+ const set = new Uint8Array(points.length / 2);
+ const tree = new Set();
+ const heap = [];
+
+ const bisect = bisector(([v]) => -v).left;
+ function heap_insert(x, v) {
+ heap.splice(bisect(heap, -v), 0, [v, x]);
+ }
+ function heap_pop() {
+ return heap.length && heap.pop()[1];
+ }
+
+ // Initialize the heap with the outgoing edges of vertex zero.
+ set[0] = 1;
+ for (const i of delaunay.neighbors(0)) {
+ heap_insert([0, i], score(points, 0, i));
+ }
+
+ // For each remaining minimum edge in the heap…
+ let edge;
+ while ((edge = heap_pop())) {
+ const [i, j] = edge;
+
+ // If j is already connected, skip; otherwise add the new edge to point j.
+ if (set[j]) continue;
+ set[j] = 1;
+ tree.add(`${extent([i, j])}`);
+
+ // Add each unconnected neighbor k of point j to the heap.
+ for (const k of delaunay.neighbors(j)) {
+ if (set[k]) continue;
+ heap_insert([j, k], score(points, j, k));
+ }
+ }
+
+ return (i) => {
+ const a = triangles[i];
+ const b = triangles[i % 3 === 2 ? i - 2 : i + 1];
+ return tree.has(`${extent([a, b])}`);
+ };
+ }
+}
+
+export function gabrielMesh(data, options) {
+ return delaunayMark(GabrielMesh, data, options);
+}
+
+export function urquhartMesh(data, options) {
+ return delaunayMark(UrquhartMesh, data, options);
+}
+
+export function mstMesh(data, options) {
+ return delaunayMark(MSTMesh, data, options);
+}
diff --git a/test/output/penguinCulmenGabriel.svg b/test/output/penguinCulmenGabriel.svg
new file mode 100644
index 0000000000..f60dd2bc05
--- /dev/null
+++ b/test/output/penguinCulmenGabriel.svg
@@ -0,0 +1,449 @@
+
\ No newline at end of file
diff --git a/test/output/penguinCulmenMST.svg b/test/output/penguinCulmenMST.svg
new file mode 100644
index 0000000000..b381338592
--- /dev/null
+++ b/test/output/penguinCulmenMST.svg
@@ -0,0 +1,449 @@
+
\ No newline at end of file
diff --git a/test/output/penguinCulmenUrquhart.svg b/test/output/penguinCulmenUrquhart.svg
new file mode 100644
index 0000000000..91009163ab
--- /dev/null
+++ b/test/output/penguinCulmenUrquhart.svg
@@ -0,0 +1,449 @@
+
\ No newline at end of file
diff --git a/test/plots/index.js b/test/plots/index.js
index b3f4d78380..098db09ffe 100644
--- a/test/plots/index.js
+++ b/test/plots/index.js
@@ -139,6 +139,9 @@ 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 penguinCulmenGabriel} from "./penguin-culmen-gabriel.js";
+export {default as penguinCulmenMST} from "./penguin-culmen-mst.js";
+export {default as penguinCulmenUrquhart} from "./penguin-culmen-urquhart.js";
export {default as penguinCulmenVoronoi} from "./penguin-culmen-voronoi.js";
export {default as penguinVoronoi1D} from "./penguin-voronoi-1d.js";
export {default as penguinDensity} from "./penguin-density.js";
diff --git a/test/plots/penguin-culmen-gabriel.js b/test/plots/penguin-culmen-gabriel.js
new file mode 100644
index 0000000000..2b1f8ea8af
--- /dev/null
+++ b/test/plots/penguin-culmen-gabriel.js
@@ -0,0 +1,19 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function () {
+ const data = await 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", z: "species"}),
+ Plot.gabrielMesh(data, {
+ x: "culmen_depth_mm",
+ y: "culmen_length_mm",
+ stroke: "species",
+ z: "species",
+ strokeOpacity: 1
+ }),
+ Plot.dot(data, {x: "culmen_depth_mm", y: "culmen_length_mm", fill: "species", z: "species"})
+ ]
+ });
+}
diff --git a/test/plots/penguin-culmen-mst.js b/test/plots/penguin-culmen-mst.js
new file mode 100644
index 0000000000..63c2de79f7
--- /dev/null
+++ b/test/plots/penguin-culmen-mst.js
@@ -0,0 +1,19 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function () {
+ const data = await 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", z: "species"}),
+ Plot.mstMesh(data, {
+ x: "culmen_depth_mm",
+ y: "culmen_length_mm",
+ stroke: "species",
+ z: "species",
+ strokeOpacity: 1
+ }),
+ Plot.dot(data, {x: "culmen_depth_mm", y: "culmen_length_mm", fill: "species", z: "species"})
+ ]
+ });
+}
diff --git a/test/plots/penguin-culmen-urquhart.js b/test/plots/penguin-culmen-urquhart.js
new file mode 100644
index 0000000000..8ddb366c50
--- /dev/null
+++ b/test/plots/penguin-culmen-urquhart.js
@@ -0,0 +1,19 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+export default async function () {
+ const data = await 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", z: "species"}),
+ Plot.urquhartMesh(data, {
+ x: "culmen_depth_mm",
+ y: "culmen_length_mm",
+ stroke: "species",
+ z: "species",
+ strokeOpacity: 1
+ }),
+ Plot.dot(data, {x: "culmen_depth_mm", y: "culmen_length_mm", fill: "species", z: "species"})
+ ]
+ });
+}