From 477a69eba8411a51bba7041c4b1b5cec212c01b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 8 Jun 2022 12:36:32 +0200 Subject: [PATCH 1/3] delaunay updates: - group by z or stroke for delaunayLink - group by z or stroke for delaunayMesh --- src/marks/delaunay.js | 132 +- test/output/penguinCulmenDelaunaySpecies.svg | 1084 +++++++++++++++++ test/plots/index.js | 1 + test/plots/penguin-culmen-delaunay-species.js | 12 + test/plots/penguin-culmen-delaunay.js | 2 +- 5 files changed, 1174 insertions(+), 57 deletions(-) create mode 100644 test/output/penguinCulmenDelaunaySpecies.svg create mode 100644 test/plots/penguin-culmen-delaunay-species.js diff --git a/src/marks/delaunay.js b/src/marks/delaunay.js index f233c3f4df..0e882b7119 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"; @@ -33,7 +33,8 @@ export class DelaunayLink extends Mark { data, [ {name: "x", value: x, scale: "x"}, - {name: "y", value: y, scale: "y"} + {name: "y", value: y, scale: "y"}, + {name: "z", value: maybeZ(options), optional: true} ], options, linkDefaults @@ -42,58 +43,64 @@ 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]); - } - - // 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 links(index) { + let i = -1; + const newIndex = []; + const newChannels = {}; + for (const k in channels) newChannels[k] = []; + + const X1 = []; + const X2 = []; + const Y1 = []; + const Y2 = []; + + 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(); } } @@ -105,7 +112,8 @@ export class DelaunayMesh extends Mark { data, [ {name: "x", value: x, scale: "x"}, - {name: "y", value: y, scale: "y"} + {name: "y", value: y, scale: "y"}, + {name: "z", value: maybeZ(options), optional: true} ], options, defaults @@ -115,16 +123,28 @@ 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]); + const newChannels = {}; + for (const k in channels) newChannels[k] = {[index[0]]: channels[k][index[0]]}; + select(this).append("path") + .datum(index[0]) + .call(applyDirectStyles, mark) + .call(applyTransform, x, y, offset + dx, offset + dy) + .attr("d", render(delaunay, dimensions)) + .call(applyChannelStyles, mark, newChannels) + .call(applyMarkers, mark, newChannels); + }; + } 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(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/penguinCulmenDelaunaySpecies.svg b/test/output/penguinCulmenDelaunaySpecies.svg new file mode 100644 index 0000000000..65fc7618b5 --- /dev/null +++ b/test/output/penguinCulmenDelaunaySpecies.svg @@ -0,0 +1,1084 @@ + + + + + 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..b4ccd98de4 --- /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.delaunayLink(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke: "#ccc", z: "species"}), + 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..f420b24813 100644 --- a/test/plots/penguin-culmen-delaunay.js +++ b/test/plots/penguin-culmen-delaunay.js @@ -5,7 +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.delaunayLink(data, {x: "culmen_depth_mm", y: "culmen_length_mm", stroke: "culmen_length_mm", z: null}), Plot.hull(data, {x: "culmen_depth_mm", y: "culmen_length_mm"}) ] }); From a7f110fa5ded2ac99d38e8aa652abf54bcc57ec8 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 8 Jun 2022 10:56:17 -0700 Subject: [PATCH 2/3] a few fixes --- src/marks/delaunay.js | 55 +++++++++----------- test/output/penguinCulmenDelaunay.svg | 4 +- test/output/penguinCulmenDelaunayMesh.svg | 4 +- test/output/penguinCulmenDelaunaySpecies.svg | 8 +-- 4 files changed, 34 insertions(+), 37 deletions(-) diff --git a/src/marks/delaunay.js b/src/marks/delaunay.js index 0e882b7119..94e9ba3b17 100644 --- a/src/marks/delaunay.js +++ b/src/marks/delaunay.js @@ -28,13 +28,13 @@ const hullDefaults = { export class DelaunayLink extends Mark { constructor(data, options = {}) { - const {x, y, curve, tension} = options; + const {x, y, z, stroke, curve, tension} = options; super( data, [ {name: "x", value: x, scale: "x"}, {name: "y", value: y, scale: "y"}, - {name: "z", value: maybeZ(options), optional: true} + {name: "z", value: maybeZ({z, stroke}), optional: true} ], options, linkDefaults @@ -52,12 +52,11 @@ export class DelaunayLink extends Mark { const newIndex = []; const newChannels = {}; for (const k in channels) newChannels[k] = []; - const X1 = []; const X2 = []; const Y1 = []; const Y2 = []; - + function link(ti, tj) { ti = index[ti]; tj = index[tj]; @@ -68,7 +67,7 @@ export class DelaunayLink extends Mark { 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]; @@ -77,22 +76,23 @@ export class DelaunayLink extends Mark { 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); + .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") @@ -107,13 +107,13 @@ export class DelaunayLink extends Mark { 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: "z", value: maybeZ(options), optional: true} + {name: "z", value: maybeZ({z, stroke}), optional: true} ], options, defaults @@ -126,22 +126,19 @@ export class DelaunayMesh extends Mark { render(index, {x, y}, {x: X, y: Y, z: Z, ...channels}, dimensions) { const {dx, dy} = this; const mark = this; - function mesh(render) { - return function (index) { + function mesh(render) { + return function(index) { const delaunay = Delaunay.from(index, i => X[i], i => Y[i]); - const newChannels = {}; - for (const k in channels) newChannels[k] = {[index[0]]: channels[k][index[0]]}; select(this).append("path") .datum(index[0]) .call(applyDirectStyles, mark) - .call(applyTransform, x, y, offset + dx, offset + dy) .attr("d", render(delaunay, dimensions)) - .call(applyChannelStyles, mark, newChannels) - .call(applyMarkers, mark, newChannels); + .call(applyChannelStyles, mark, channels); }; } return create("svg:g") .call(applyIndirectStyles, this, 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))) diff --git a/test/output/penguinCulmenDelaunay.svg b/test/output/penguinCulmenDelaunay.svg index 0b5463d6f8..9783ddae01 100644 --- a/test/output/penguinCulmenDelaunay.svg +++ b/test/output/penguinCulmenDelaunay.svg @@ -1083,7 +1083,7 @@ - - + + \ 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 index 65fc7618b5..5da0dd93bd 100644 --- a/test/output/penguinCulmenDelaunaySpecies.svg +++ b/test/output/penguinCulmenDelaunaySpecies.svg @@ -1070,15 +1070,15 @@ - + - + - + - + \ No newline at end of file From 63d80b36c49733ddf0d5635f94bf386ec9e632d9 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 8 Jun 2022 11:03:13 -0700 Subject: [PATCH 3/3] no group by stroke for delaunayLink --- src/marks/delaunay.js | 4 +- test/output/penguinCulmenDelaunay.svg | 3 - test/output/penguinCulmenDelaunaySpecies.svg | 987 +----------------- test/plots/penguin-culmen-delaunay-species.js | 2 +- test/plots/penguin-culmen-delaunay.js | 3 +- 5 files changed, 8 insertions(+), 991 deletions(-) diff --git a/src/marks/delaunay.js b/src/marks/delaunay.js index 94e9ba3b17..3af87639db 100644 --- a/src/marks/delaunay.js +++ b/src/marks/delaunay.js @@ -28,13 +28,13 @@ const hullDefaults = { export class DelaunayLink extends Mark { constructor(data, options = {}) { - const {x, y, z, stroke, 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: "z", value: maybeZ({z, stroke}), optional: true} + {name: "z", value: z, optional: true} ], options, linkDefaults diff --git a/test/output/penguinCulmenDelaunay.svg b/test/output/penguinCulmenDelaunay.svg index 9783ddae01..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/penguinCulmenDelaunaySpecies.svg b/test/output/penguinCulmenDelaunaySpecies.svg index 5da0dd93bd..95cc67db33 100644 --- a/test/output/penguinCulmenDelaunaySpecies.svg +++ b/test/output/penguinCulmenDelaunaySpecies.svg @@ -80,994 +80,15 @@ 21 culmen_depth_mm → - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/test/plots/penguin-culmen-delaunay-species.js b/test/plots/penguin-culmen-delaunay-species.js index b4ccd98de4..c5a08e19dc 100644 --- a/test/plots/penguin-culmen-delaunay-species.js +++ b/test/plots/penguin-culmen-delaunay-species.js @@ -5,7 +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: "#ccc", z: "species"}), + 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 f420b24813..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", z: null}), - 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"}) ] }); }