Public
Edited
Jun 17, 2022
1 fork
59 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
points0 = {
let rest = 0;
const phi = Math.PI * (3 - Math.sqrt(5));
return (
d3
// sort the features by what we're plotting, so that missing or excess
// rural population from a commune goes to another rural commune
.sort(geo.features, ({ properties: { code } }) => density.get(code))
.flatMap((f) => {
const coords = projection(d3.geoCentroid(f));
const {
properties: { code }
} = f;
const pop = population.get(code);
const length = Math.round((pop + rest) / precision);
// 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.1 * Math.sqrt(0.5 + i);
return {
coords: [
coords[0] + r * Math.sin(i * phi),
coords[1] + r * Math.cos(i * phi)
],
code
};
});
})
);
}
Insert cell
Insert cell
points = (replay,
points0
.map((d) => d.coords)
.flat()
.map((d) => d - height / 2 + 0.01 * (Math.random() - 0.5)))
Insert cell
codes = points0.map((d) => d.code)
Insert cell
run = {
for (let i = 0; i < 150; i++) {
yield mapUrban.update(i);
}
}
Insert cell
fill = colorBy === "commune"
? codeColor
: colorBy === "population density"
? (code) => densityColor(density.get(code))
: (code) => ruralColor(density.get(code))
Insert cell
stroke = (c) => (c === code ? "white" : fill(c)) // highlight a commune
Insert cell
densityColor = d3
.scaleSequentialSqrt(d3.interpolateCool)
.domain(d3.extent(density, ([, v]) => v))
Insert cell
ruralColor = d3
.scaleThreshold()
.domain([50, 100, 250])
.range("e6deb2-9ac1ae-ecb45b-e07a5f".split("-").map((d) => `#${d}`))
Insert cell
codeColor = d3
.scaleOrdinal()
.domain(density.keys())
.range(
[].concat(
d3.schemeTableau10,
d3.schemeAccent,
d3.schemeCategory10,
d3.schemeDark2
)
)
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) {
indices = Uint32Array.from(d3.range(n));
projection = new Float32Array(n);
deltas = new Float32Array(2 * n);
}

const a = 2 * Math.PI * Math.random();
deltas.fill(0);
for (let d = 0; d < dirs; d++) {
const ap = a + (Math.PI * d) / dirs,
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++) {
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
// jrus' fast (and precise) approximation of the chord area inverse function
import { chordAreaInverse2 as chordAreaInverse } from "@fil/chord-area-inverse"
Insert cell
function lloydsRelaxation(voronoi, radius) {
const {
delaunay: { points }
} = voronoi;
for (let i = 0; i < points.length / 2; i++) {
const R = Math.hypot(points[2 * i], points[2 * i + 1]);
if (R < radius) {
const c = voronoi.cellPolygon(i);
const p = d3.polygonCentroid(
c.map(([x, y]) => {
const r0 = Math.hypot(x, y);
const a = radius / Math.max(r0, radius);
return [x * a, y * a];
})
);
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
function preserveContinuity(voronoi, codes) {
const {
delaunay: { points }
} = voronoi;

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

let cutoff = 10;
do {
let changed = false;
for (let i = 0; i < codes.length; i++) {
for (const j of voronoi.delaunay.neighbors(i)) {
if (codes[i] === codes[j] && colors[j] !== colors[i]) {
colors[j] = colors[i] = Math.min(colors[j], colors[i]);
changed = true;
}
}
}
if (!changed) cutoff = 0;
} while (cutoff-- > 0);

for (let i = 0; i < colors.length; i++) {
const f = colors[i];
if (f > 0) {
console.log("fix continuity for point", i, codes[i]);
const base = [];
for (let j = 0; j < i; j++) if (codes[j] === codes[i]) base.push(j);
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
projection = d3.geoMercator().fitSize([height, height], geo)
Insert cell
height = Math.min(670, width)
Insert cell
// source "https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/departements/41-loir-et-cher/communes-41-loir-et-cher.geojson"
geo = FileAttachment("41.json").json()
Insert cell
names = new Map(geo.features.map((f) => [f.properties.code, f.properties.nom]))
Insert cell
density = new Map(
d3.sort(
geo.features.map((f) => [
f.properties.code,
(population.get(f.properties.code) / d3.geoArea(f)) *
((4 * Math.PI) / 510.1e6) // population per km2
]),
([, v]) => v
)
)
Insert cell
population = FileAttachment("population41.csv")
.csv()
.then((d) => new Map(d.map(({ code, population }) => [code, +population])))
Insert cell
Insert cell
rivers = [
{
name: "La Loire",
places: [
"Saint-Laurent-Nouan",
"Montlivault",
"Candé-sur-Beuvron",
"Monteaux"
]
},
{
name: "Le Loir",
places: [
"Saint-Jean-Froidmentel",
"Fréteval",
"Pezou",
"Lisle",
"Vendôme",
"Naveil",
"Mazangé",
"Saint-Rimay",
"Montoire-sur-le-Loir",
"Couture-sur-Loir"
]
},
{
name: "Le Cher",
places: ["Selles-sur-Cher", "Saint-Aignan", "Montrichard Val de Cher"]
}
]
Insert cell
import { Legend, Swatches } from "@d3/color-legend"
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