Skip to content

Commit 771c643

Browse files
committed
three subgraphs of Delaunay: Gabriel, Urquhart, and minimal spanning tree
for #928
1 parent 90d2238 commit 771c643

9 files changed

+1508
-2
lines changed

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export {Arrow, arrow} from "./marks/arrow.js";
44
export {BarX, BarY, barX, barY} from "./marks/bar.js";
55
export {boxX, boxY} from "./marks/box.js";
66
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
7-
export {delaunayLink, delaunayMesh, hull, voronoi, voronoiMesh} from "./marks/delaunay.js";
7+
export {delaunayLink, delaunayMesh, hull, voronoi, voronoiMesh, gabrielMesh, urquhartMesh, mstMesh} from "./marks/delaunay.js";
88
export {Dot, dot, dotX, dotY, circle, hexagon} from "./marks/dot.js";
99
export {Frame, frame} from "./marks/frame.js";
1010
export {Hexgrid, hexgrid} from "./marks/hexgrid.js";

src/marks/delaunay.js

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {create, group, path, select, Delaunay} from "d3";
1+
import {bisector, create, extent, group, path, select, Delaunay} from "d3";
22
import {Curve} from "../curve.js";
33
import {constant, maybeTuple, maybeZ} from "../options.js";
44
import {Mark} from "../plot.js";
@@ -266,3 +266,120 @@ export function voronoi(data, options) {
266266
export function voronoiMesh(data, options) {
267267
return delaunayMark(VoronoiMesh, data, options);
268268
}
269+
270+
class GabrielMesh extends AbstractDelaunayMark {
271+
constructor(data, options) {
272+
super(data, options, voronoiMeshDefaults);
273+
this.fill = "none";
274+
}
275+
_accept(delaunay) {
276+
const {points, triangles} = delaunay;
277+
return (i) => {
278+
const a = triangles[i];
279+
const b = triangles[i % 3 === 2 ? i - 2 : i + 1];
280+
return [a, b].includes(delaunay.find((points[2 * a] + points[2 * b]) / 2, (points[2 * a + 1] + points[2 * b + 1]) / 2, a));
281+
};
282+
}
283+
_render(delaunay) {
284+
const p = new path();
285+
const {points, halfedges, triangles} = delaunay;
286+
const accept = this._accept(delaunay);
287+
for (let i = 0, n = triangles.length; i < n; ++i) {
288+
const j = halfedges[i];
289+
if (i < j) continue;
290+
if (accept(i)) {
291+
const a = triangles[i];
292+
const b = triangles[i % 3 === 2 ? i - 2 : i + 1];
293+
p.moveTo(points[2 * a], points[2 * a + 1]);
294+
p.lineTo(points[2 * b], points[2 * b + 1]);
295+
}
296+
}
297+
return "" + p;
298+
}
299+
}
300+
301+
class UrquhartMesh extends GabrielMesh {
302+
constructor(data, options) {
303+
super(data, options, voronoiMeshDefaults);
304+
this.fill = "none";
305+
}
306+
_accept(delaunay, score = euclidean2) {
307+
const {halfedges, points, triangles} = delaunay;
308+
const n = triangles.length;
309+
const removed = new Uint8Array(n);
310+
for (let e = 0; e < n; e += 3) {
311+
const p0 = triangles[e], p1 = triangles[e + 1], p2 = triangles[e + 2];
312+
const p01 = score(points, p0, p1), p12 = score(points, p1, p2), p20 = score(points, p2, p0);
313+
removed[p20 > p01 && p20 > p12 ? Math.max(e + 2, halfedges[e + 2])
314+
: p12 > p01 && p12 > p20 ? Math.max(e + 1, halfedges[e + 1])
315+
: Math.max(e, halfedges[e])] = 1;
316+
}
317+
return (i) => !removed[i];
318+
}
319+
}
320+
321+
function euclidean2(points, i, j) {
322+
return (points[i * 2] - points[j * 2]) ** 2 + (points[i * 2 + 1] - points[j * 2 + 1]) ** 2;
323+
}
324+
325+
class MSTMesh extends GabrielMesh {
326+
constructor(data, options) {
327+
super(data, options, voronoiMeshDefaults);
328+
this.fill = "none";
329+
}
330+
_accept(delaunay, score = euclidean2) {
331+
const {points, triangles} = delaunay;
332+
const set = new Uint8Array(points.length / 2);
333+
const tree = new Set();
334+
const heap = [];
335+
336+
const bisect = bisector(([v]) => -v).left;
337+
function heap_insert(x, v) {
338+
heap.splice(bisect(heap, -v), 0, [v, x]);
339+
}
340+
function heap_pop() {
341+
return heap.length && heap.pop()[1];
342+
}
343+
344+
// Initialize the heap with the outgoing edges of vertex zero.
345+
set[0] = 1;
346+
for (const i of delaunay.neighbors(0)) {
347+
heap_insert([0, i], score(points, 0, i));
348+
}
349+
350+
// For each remaining minimum edge in the heap…
351+
let edge;
352+
while (edge = heap_pop()) {
353+
const [i, j] = edge;
354+
355+
// If j is already connected, skip; otherwise add the new edge to point j.
356+
if (set[j]) continue;
357+
set[j] = 1;
358+
tree.add(`${extent([i, j])}`);
359+
360+
// Add each unconnected neighbor k of point j to the heap.
361+
for (const k of delaunay.neighbors(j)) {
362+
if (set[k]) continue;
363+
heap_insert([j, k], score(points, j, k));
364+
}
365+
}
366+
367+
return (i) => {
368+
const a = triangles[i];
369+
const b = triangles[i % 3 === 2 ? i - 2 : i + 1];
370+
return tree.has(`${extent([a, b])}`);
371+
};
372+
}
373+
}
374+
375+
export function gabrielMesh(data, options) {
376+
return delaunayMark(GabrielMesh, data, options);
377+
}
378+
379+
export function urquhartMesh(data, options) {
380+
return delaunayMark(UrquhartMesh, data, options);
381+
}
382+
383+
export function mstMesh(data, options) {
384+
return delaunayMark(MSTMesh, data, options);
385+
}

test/output/penguinCulmenGabriel.svg

Lines changed: 449 additions & 0 deletions
Loading

test/output/penguinCulmenMST.svg

Lines changed: 449 additions & 0 deletions
Loading

test/output/penguinCulmenUrquhart.svg

Lines changed: 449 additions & 0 deletions
Loading

test/plots/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ export {default as penguinCulmenArray} from "./penguin-culmen-array.js";
130130
export {default as penguinCulmenDelaunay} from "./penguin-culmen-delaunay.js";
131131
export {default as penguinCulmenDelaunayMesh} from "./penguin-culmen-delaunay-mesh.js";
132132
export {default as penguinCulmenDelaunaySpecies} from "./penguin-culmen-delaunay-species.js";
133+
export {default as penguinCulmenGabriel} from "./penguin-culmen-gabriel.js";
134+
export {default as penguinCulmenMST} from "./penguin-culmen-mst.js";
135+
export {default as penguinCulmenUrquhart} from "./penguin-culmen-urquhart.js";
133136
export {default as penguinCulmenVoronoi} from "./penguin-culmen-voronoi.js";
134137
export {default as penguinVoronoi1D} from "./penguin-voronoi-1d.js";
135138
export {default as penguinDodge} from "./penguin-dodge.js";

test/plots/penguin-culmen-gabriel.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
4+
export default async function() {
5+
const data = await (await d3.csv("data/penguins.csv", d3.autoType));
6+
return Plot.plot({
7+
marks: [
8+
Plot.delaunayMesh(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke: "species", z: "species"}),
9+
Plot.gabrielMesh(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke: "species", z: "species", strokeOpacity: 1}),
10+
Plot.dot(data, {x: "culmen_depth_mm", y: "culmen_length_mm", fill:"species", z: "species"})
11+
]
12+
});
13+
}

test/plots/penguin-culmen-mst.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
4+
export default async function() {
5+
const data = await (await d3.csv("data/penguins.csv", d3.autoType));
6+
return Plot.plot({
7+
marks: [
8+
Plot.delaunayMesh(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke: "species", z: "species"}),
9+
Plot.mstMesh(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke:"species", z: "species", strokeOpacity: 1}),
10+
Plot.dot(data, {x: "culmen_depth_mm", y: "culmen_length_mm", fill:"species", z: "species"})
11+
]
12+
});
13+
}

test/plots/penguin-culmen-urquhart.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
4+
export default async function() {
5+
const data = await (await d3.csv("data/penguins.csv", d3.autoType));
6+
return Plot.plot({
7+
marks: [
8+
Plot.delaunayMesh(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke: "species", z: "species"}),
9+
Plot.urquhartMesh(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke:"species", z: "species", strokeOpacity: 1}),
10+
Plot.dot(data, {x: "culmen_depth_mm", y: "culmen_length_mm", fill:"species", z: "species"})
11+
]
12+
});
13+
}

0 commit comments

Comments
 (0)