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