Published
Edited
Jun 27, 2022
14 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
run = {
for (let i = 0; i < 150; i++)
yield mapUrban.update(i);
}
Insert cell
function lloydsRelaxation(voronoi, radius) {
const {
delaunay: {points}
} = voronoi;
for (let i = 0; i < points.length / 2; i++) { //for every cell
const R = Math.hypot(points[2 * i], points[2 * i + 1]); //radius of points
if (R < radius) { // if the points are within the circle
const c = voronoi.cellPolygon(i)
//Return centroid of cell, squeezed withoun the bounding circle, if it goes outside the range
const p = d3.polygonCentroid(
c.map(([x, y]) => {
const r0 = Math.hypot(x, y); //radius of each corner of the polygon
const a = radius / Math.max(r0, radius); //reduction factor to fit corners back in the circle
return [x * a, y * a]; //reduction factor applied
})
);

//Move point towards the centroid of its cell
const f = 0.5;
points[2 * i] += (p[0] - points[2 * i]) * f;
points[2 * i + 1] += (p[1] - points[2 * i + 1]) * f;
}
}
}
Insert cell
discUniformTransportBatched = {
let indices = [],
projection,
deltas;

// batched
return (
points,
radius,
{ dirs = 3, strength = 1, profile = chordAreaInverse } = {}
) => {
const n = points.length / 2;
if (n !== indices.length) { //if indices array hasn't been set up yet
indices = Uint32Array.from(d3.range(n)); //create array of points
projection = new Float32Array(n); //list of points
deltas = new Float32Array(2 * n); // list of point coords
}

const a = 2 * Math.PI * Math.random(); //random angle along circle
deltas.fill(0);
for (let d = 0; d < dirs; d++) { //iterate n times
const ap = a + ((Math.PI * d) / dirs), //rotate round circle with successive iterations
sa = Math.sin(ap),
ca = Math.cos(ap);
for (let i = 0; i < n; i++)
projection[i] = ca * points[2 * i] + sa * points[2 * i + 1]; //

indices.sort((i, j) => projection[i] - projection[j]);

for (let k = 0; k < n; k++) { //for every point
const i = indices[k],
ideal = radius * profile(k / (n + 1)), //
delta = ideal - projection[i];
deltas[2 * i] += ca * delta;
deltas[2 * i + 1] += sa * delta;
}
}
for (let i = 0; i < points.length; i++)
points[i] += (deltas[i] / dirs) * strength;
};
}
Insert cell
function preserveContinuity(voronoi, codes) {
const {
delaunay: {points}
} = voronoi;

const counts = {};
const colors = codes.map((d) => (counts[d] = (counts[d] || 0) + 1) -1 );

//This bit returns whether all cells in a commune are contiguous or not
let cutoff = 10;
do {
let changed = false;
for (let i = 0; i < codes.length; i++) { //for each cell
for (const j of voronoi.delaunay.neighbors(i)) { //for every neighbour of this cell
if (codes[i] === codes[j] && colors[j] !== colors[i]) { //if the commune is the same, but the commune increment is different?
colors[j] = colors[i] = Math.min(colors[j], colors[i]); //re-assign the increment of both to be the same.
changed = true; //then mark as done
}
}
}
if (!changed) cutoff = 0; //if no re-assignments then stop
} while (cutoff-- > 0); //decrement cutoff on each iteration

for (let i = 0; i < colors.length; i++) { //for each cell
const f = colors[i]; //increment of each cell
if (f > 0) { //if cells are not completely contiguous within a commune
console.log("fix continuity for point", i, codes[i]);
const base = [];
//Return all the combinations of cells in the same commune
for (let j = 0; j < i; j++) if (codes[j] === codes[i]) base.push(j);

//Move towards the average poisition of all the cells in the same commune,
points[2 * i] +=
0.7 * (d3.mean(base, (i) => points[2 * i]) - points[2 * i]);
points[2 * i + 1] +=
0.7 * (d3.mean(base, (i) => points[2 * i + 1]) - points[2 * i + 1]);
}
}
}
Insert cell
Insert cell
thames2 = thames.places.map(d => {
const I = d3.range(points.length / 2);
return Object.assign(d,{blocs: I.filter((i) => names.get(codes[i]) === d.place)})
})
Insert cell
// jrus' fast (and presice) approximation of the chord area inverse function
import { chordAreaInverse2 as chordAreaInverse } from "@fil/chord-area-inverse"
Insert cell
codes = points0.map((d) => d.GSSCode)
Insert cell
stroke = (c) => (fill(c)) // highlight a commune
Insert cell
fill = colorBy === "commune"
? codeColor
: colorBy === "population density"
? (code) => densityColor(density.get(code))
: (code) => ruralColor(density.get(code))
Insert cell
ruralColor = d3
.scaleThreshold()
.domain([50, 100, 250])
.range("e6deb2-9ac1ae-ecb45b-e07a5f".split("-").map((d) => `#${d}`))
Insert cell
densityColor = d3
.scaleSequentialSqrt(d3.interpolateCool)
.domain(d3.extent(density, ([, v]) => v))
Insert cell
codeColor = d3
.scaleOrdinal()
.domain(density.keys())
.range(
[].concat(
d3.schemeTableau10,
d3.schemeAccent,
d3.schemeCategory10,
d3.schemeDark2
)
)
Insert cell
listToArray = function(voronoi) {
let points = voronoi.delaunay.points
let pointsTest = [];
for (let i = 0; i < points.length - 1; i+= 2) {
pointsTest.push([points[i], points[i+1]])
}
return pointsTest
}
Insert cell
Insert cell
density = new Map(
d3.sort(
geo.features.map((f) => [
f.properties.GSSCode,
(population.get(f.properties.GSSCode) / d3.geoArea(f)) *
((4 * Math.PI) / 510.1e6) //population per km2 (converting from steridians)
]),
([, v]) => v //sort by density asc
)
)
Insert cell
height = Math.min(670, width)
Insert cell
projection = d3.geoMercator().fitSize([height, height], geo)
Insert cell
geo.features.map((f) => f)
Insert cell
points = (
replay,
points0
.map((d) => d.coords)
.flat()
.map((d) => d - height / 2 + 0.01 * (Math.random() - 0.5)) //shift points left and up by half the height of the canvas...and add a small random variable
)
Insert cell
points0 = {
let rest = 0;
const phi = Math.PI * (3 - Math.sqrt(5));
return (
d3
.sort(geo.features, ({ properties: {GSSCode}}) => density.get(GSSCode))
.flatMap((f) => {
const coords = projection(d3.geoCentroid(f));
const {
properties: { GSSCode }
} = f;
const pop = population.get(GSSCode);
const length = Math.round ((pop + rest) / precision); //number of cells required to represent area
// we keep track of the difference between the current sum and the true sum, and add
// the people we missed (or overcounted) to the next unit, so that the grand total is correct.
rest += pop - length * precision;
return Array.from({ length }, (_,i) => {
const r = 0.5 * Math.sqrt(0.5 + i);
return {
coords: [
coords[0] + r * Math.sin(i * phi),
coords[1] + r * Math.cos(i * phi)
],
GSSCode
};
});
})
);
}
Insert cell
polygonClipping = require("polygon-clipping")
Insert cell
names = new Map(geo.features.map((f) => [f.properties.GSSCode, f.id]))
Insert cell
Insert cell
Insert cell
import { Legend, Swatches } from "@d3/color-legend"
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