Public
Edited
Dec 23
3 stars
Also listed in…
Tricontours
Insert cell
Insert cell
display(contour.thresholds(40), triangles.flat())
Insert cell
contour = d3
.tricontour()
.triangulate(triangulate)
.thresholds(10)
Insert cell
triangulated = triangulate(data, d => d[0], d => d[1])
Insert cell
triangulate = (points, x, y) => {
const halfedges = Int32Array.from({ length: 3 * triangles.length }, () => -1);
const inedges = Int32Array.from({ length: points.length }).fill(-1);

const hull = [];
const index = new Map();
for (let i = 0; i < triangles.length; i++) {
const tri = triangles[i];
index.set([tri[0], tri[1]].join("/"), 3 * i);
index.set([tri[1], tri[2]].join("/"), 3 * i + 1);
index.set([tri[2], tri[0]].join("/"), 3 * i + 2);
}
let t = 0,
a;
for (const tri of triangles) {
for (const e of [[tri[0], tri[1]], [tri[1], tri[2]], [tri[2], tri[0]]]) {
if ((a = index.get([e[1], e[0]].join("/"))) !== undefined) {
halfedges[a] = t;
halfedges[t] = a;
}
t++;
}
}

const _triangles = triangles.flat(2);

const segments = new Map(
Array.from(halfedges)
.map((j, i) =>
j === -1
? [_triangles[i], _triangles[i % 3 === 2 ? i - 2 : i + 1]]
: null
)
.filter(d => !!d)
);

let i, j, e;
while ((e = [...segments].pop())) {
i = e[0];
do {
j = i;
hull.push(j);
i = segments.get(j);
segments.delete(j);
} while (!hull.includes(i));
}

t = 0;
for (const tri of triangles) {
for (const e of [[tri[0], tri[1]], [tri[1], tri[2]], [tri[2], tri[0]]]) {
if (inedges[e[1]] === -1 || halfedges[t] === -1) {
inedges[e[1]] = t;
}
t++;
}
}

return {
points: points.map(d => [x(d), y(d)]).flat(),
triangles: _triangles,
halfedges,
inedges,
hull
};
}
Insert cell
Insert cell
md`
<div style="width: 460px; max-width: 460px; float:left">
${display(d3.tricontour(), d3.Delaunay.from(data).triangles)}</div>
<div style="width: 460px; max-width: 460px; float:left">
${display(contour.thresholds(10), triangles.flat())}</div>
<div style="clear:both"/>
`
Insert cell
Insert cell
d3 = require("d3@7", "d3-delaunay@6", "d3-tricontour@1")
Insert cell
scales = ({
x: d3.scaleLinear().range(d3.extent(data, d => d[0])),
y: d3.scaleLinear().range(d3.extent(data, d => d[1])),
color: d3
.scaleSequential(d3.interpolateCividis)
.domain(d3.extent(data, d => d[2]))
})
Insert cell
function display(contour, triangles) {
const svg = d3
.create("svg")
.style("background", "#333")
.attr("viewBox", [
scales.x(-1.5 / 20),
scales.y(-1.5 / 20),
scales.x(1 + 1.5 / 20) - scales.x(-1.5 / 20),
scales.y(1 + 1.5 / 20) - scales.y(-1.5 / 20)
]);

const g = svg.append("g");

const path = d3.geoPath().pointRadius(scales.x(2 / width) - scales.x(0));

const contours = contour(data);

g.append("g")
.selectAll("path")
.data(contours)
.join("path")
.attr("d", path)
.attr("fill", (d) => scales.color(d.value))
.attr("fill-opacity", 0.8)
.append("title")
.text((d) => d.value);

function vertices(a) {
return Array.from(triangles)
.filter((_, i) => i % 3 === a)
.map((d) => data[d]);
}

g.append("path")
.attr("vector-effect", "non-scaling-stroke")
.attr(
"d",
path({
type: "MultiLineString",
coordinates: d3.zip(vertices(0), vertices(1), vertices(2), vertices(0))
})
)
.attr("fill", "none")
.attr("stroke-width", 1)
.attr("stroke", "white");

const pts = g
.append("g")
.selectAll("circle")
.data(data)
.join("path")
.attr("d", (d) => path({ type: "Point", coordinates: d }))
.attr("fill", (d) => scales.color(d[2]));

path.pointRadius(2 * path.pointRadius());

const coordinates = Array.from(triangulated.hull).map((i) => data[i]);
coordinates.push(coordinates[0]);

g.append("path")
.attr("vector-effect", "non-scaling-stroke")
.attr(
"d",
path({
type: "LineString",
coordinates
})
)
.attr("fill", "none")
.attr("stroke-width", 2)
.attr("stroke", "red");

g.append("g")
.selectAll("circle")
.data(triangulated.hull)
.join("path")
.attr("d", (d) => path({ type: "Point", coordinates: data[d] }))
.attr("fill", "red");

g.append("g")
.selectAll("text")
.data(data)
.join("text")
.style("fill", "white")
.style("font-size", scales.x(14 / width) - scales.x(0))
.attr("transform", (d) => `translate(${d[0]}, ${d[1]})`)
.text((d, i) => i);

return svg.node();
}
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more