Public
Edited
Nov 13, 2023
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
dots = genDots(data)
Insert cell
dots2 = genDots2(data, peoplePerDot)
Insert cell
Insert cell
getProjectedSegments = function (polygon) {
return turf
.lineSegment(polygon)
.features.map((f) => f.geometry.coordinates.map(projection));
}
Insert cell
function genDots(geo) {
// This function is based on code by Ahmad Barclay
let points = { type: "FeatureCollection", features: [] };
for (let polygon of geo.features) {
let bbox = turf.bbox(polygon);
let count = Math.round(polygon.properties.pop / peoplePerDot);
let i = 1;
while (i <= count) {
let point = randomPointInPolygon(polygon, bbox);
point.properties.id =
polygon.properties.item + i.toString().padStart(3, "0");
points.features.push(point);
i++;
}
}
// Shuffle points to avoid plotting one colour on top of the others where there are overlaps
d3.shuffle(points.features);
return points;
}
Insert cell
function genDots2(geo, peoplePerDot) {
let points = { type: "FeatureCollection", features: [] };
for (let polygon of geo.features) {
let bbox = turf.bbox(polygon);
let lineSegments = getProjectedSegments(polygon).map(
([[x0, y0], [x1, y1]]) => ({
coords: [
[x0, y0],
[x1, y1]
],
xmin: Math.min(x0, x1),
xmax: Math.max(x0, x1),
ymin: Math.min(y0, y1),
ymax: Math.max(y0, y1)
})
);
let count = Math.round(polygon.properties.pop / peoplePerDot);
let randomPoints = [];
for (let i = 0, end = count * effort * 2; i < end; i++) {
let unprojectedP = randomPointInPolygon(polygon, bbox);
let p = projectPoint(unprojectedP);
randomPoints.push({
unprojectedP,
p,
d: distanceToNearestSegment(p, lineSegments)
});
}
let i = 1;
while (i <= count) {
let bestIndex = indexOfBest(randomPoints);
let point = randomPoints[bestIndex];

// remove point from randomPoints
randomPoints[bestIndex] = randomPoints[randomPoints.length - 1];
randomPoints.pop();

updateDistances(randomPoints, point.p);

point.unprojectedP.properties.id =
polygon.properties.item + i.toString().padStart(3, "0");
points.features.push(point.unprojectedP);
i++;
}
}
// Shuffle points to avoid plotting one colour on top of the others where there are overlaps
d3.shuffle(points.features);
return points;
}
Insert cell
function indexOfBest(points) {
let bestIndex = 0;
let bestDist = points[0].d;
for (let i = 1; i < points.length; i++) {
if (points[i].d > bestDist) {
bestDist = points[i].d;
bestIndex = i;
}
}
return bestIndex;
}
Insert cell
// Based on https://stackoverflow.com/a/1501725/3347737
function pointToLineDistance(p, [v, w]) {
function dist2(v, w) {
let dx = v[0] - w[0];
let dy = v[1] - w[1];
return dx * dx + dy * dy;
}
function distToSegmentSquared(p, v, w) {
var l2 = dist2(v, w);
if (l2 == 0) return dist2(p, v);
var t =
((p[0] - v[0]) * (w[0] - v[0]) + (p[1] - v[1]) * (w[1] - v[1])) / l2;
t = Math.max(0, Math.min(1, t));
return dist2(p, [v[0] + t * (w[0] - v[0]), v[1] + t * (w[1] - v[1])]);
}
return Math.sqrt(distToSegmentSquared(p, v, w));
}
Insert cell
function distanceToNearestSegment(point, segments) {
let nearest = Infinity;
for (let segment of segments) {
// Do a few quick checks to try to avoid an expensive pointToLineDistance() call
if (point[0] >= segment.xmax + nearest) continue;
if (point[1] >= segment.ymax + nearest) continue;
if (point[0] <= segment.xmin - nearest) continue;
if (point[1] <= segment.ymin - nearest) continue;
let d = pointToLineDistance(point, segment.coords) * edgeSpacing;
if (d < nearest) nearest = d;
}
return nearest;
}
Insert cell
function updateDistances(points, point) {
for (let p of points) {
let dx = point[0] - p.p[0];
let dy = point[1] - p.p[1];
if (Math.abs(dx) >= p.d || Math.abs(dy) >= p.d) continue; // a little optimisation
let d = Math.hypot(dx, dy);
if (d < p.d) p.d = d;
}
}
Insert cell
function randomPointInPolygon(polygon, bbox) {
let point;
do {
point = turf.randomPoint(1, { bbox: bbox }).features[0];
} while (!turf.booleanPointInPolygon(point.geometry.coordinates, polygon));
return point;
}
Insert cell
function idToColourIndex(id, numberOfColours) {
// This is just a made-up function to assign some pretty colours
if (numberOfColours === 1) return 0;
let result = +id[id.length - 4] % numberOfColours;
if (id[id.length - 1] > 6) {
result = (result + +id[id.length - 1]) % numberOfColours;
}
return result;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more