Public
Edited
Feb 16, 2023
1 star
Insert cell
Insert cell
Insert cell
Insert cell
bbox = [-60, -10, 40, 60]
Insert cell
points = ({
type: "FeatureCollection",
features: Array.from({ length: 100 }, () => [
bbox[0] + (bbox[2] - bbox[0]) * Math.random(),
bbox[1] + (bbox[3] - bbox[1]) * Math.random()
]).map((d) => ({
type: "Feature",
geometry: { type: "Point", coordinates: d }
}))
})
Insert cell
cells = clippedVoronoi(points, padding)
Insert cell
function clippedVoronoi(points, padding = 0) {
const coords = points.features.map(d3.geoCentroid);
const X = d3.extent(coords, (d) => d[0]);
const Y = d3.extent(coords, (d) => d[1]);

// the projection sends the shapes to a rectangle which is a bit larger
// than the data’s bounding-box.
const projection = d3
.geoEquirectangular()
.fitExtent(
[
[X[0], Y[0]],
[X[1], Y[1]]
],
points
)
.clipExtent([
[X[0] - padding, Y[0] - padding],
[X[1] + padding, Y[1] + padding]
]);
const path = d3.geoPath(projection);

// Unproject each of the voronoi polygons, reclaiming the spherical coordinates
// of their points. However it's not as simple as it sounds, because… when we clip
// to the (planar) rectangle, we get edges that look straight on the plane, but are
// not segments of great circles. So we need to make sure that the segments are not
// too long. And when they are too long, we have to sample by splitting them into
// small pieces, in order to have enough intermediate points.
return {
type: "FeatureCollection",
features: d3
.geoVoronoi(points)
.polygons()
.features.map((feature) => {
let p = path(feature);
if (p) {
const planarRing = p
.replace(/^M|Z$/g, "")
.split("L")
.map((d) => d.split(",").map((d) => +d));

// needed because we'll use d3.pairs
planarRing.push(planarRing[0]);

const sphericalRing = d3.pairs(planarRing).flatMap(([a, b]) => {
const dist = Math.hypot(a[0] - b[0], a[1] - b[1]); // sample about every 1 degree
const inter = d3.interpolate(a, b);
const steps = Math.ceil(dist);
return d3
.range(steps)
.map((i) => projection.invert(inter(i / steps)));
});

// needed to close the ring, per GeoJSON
sphericalRing.push(sphericalRing[0]);

return {
...feature,
geometry: { type: "Polygon", coordinates: [sphericalRing] }
};
}
})
};
}
Insert cell
import { d3 } from "@visionscarto/geo"
Insert cell
Insert cell
turf = require("@turf/turf@6")
Insert cell
options = ({ bbox })
Insert cell
pts = turf.randomPoint(100, options)
Insert cell
voronoiPolygons = turf.voronoi(pts, options)
Insert cell
Plot.plot({
projection: "equirectangular",
marks: [Plot.geo(voronoiPolygons), Plot.geo(pts, { fill: "black" })]
})
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