Public
Edited
Feb 15, 2023
6 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
squares = {
// this file was computed with a target quality of 1%
// ((the whole process took the browser 4 hours))
if (high)
return yield new Map(await FileAttachment("squares-001.json").json());

const squares = new Map();
for (const f of d3.sort(world.features, d3.geoArea).reverse()) {
squares.set(f.id, await qualityToeplitz(f));
yield squares;
await Promises.delay(30);
}
}
Insert cell
quality = 0.25 // try 0.05 to create high-quality squares
Insert cell
mutable current = null
Insert cell
async function qualityToeplitz(f) {
let scale = 60,
t,
e = Infinity;
while (e > quality) {
mutable current = { id: f.id, name: names.get(f.id), scale };
t = await toeplitz(points(f, scale));
e = errorDiagonal(t) + 2 * errorEdges(t);
scale *= 1.3;
}
return t;
}
Insert cell
toeplitz = async function (points) {
const test = testSquare(points);
let min = 0;
let solution;

for (let i = 0; i < points.length; i++) {
for (let j = 0; j < i; j++) {
const a = points[i];
const b = points[j];
const dist2 = d3.geoDistance(a, b);
if (dist2 <= min) continue;
const m = d3.geoInterpolate(a, b)(0.5); // center
const r = attitude({ axis: m, angle: -90 });
const c = r(a);
const d = r(b);
const t = test(a, c, b, d);
if (t) {
min = dist2;
solution = t;
}
}
if (i % 100 === 0) await Promises.delay(10);
}
return solution;
}
Insert cell
points = (feature, scale) => {
const c = d3.geoCentroid(feature);
const projection = d3
.geoAzimuthalEquidistant()
.rotate([-c[0], -c[1]])
.fitSize([scale, scale], feature);
const geopath = d3.geoPath().projection(projection);

const path = svg`<path d="${geopath(feature)}" stroke=black fill=none>`;
const l = path.getTotalLength();

const pts = d3.range(0, l).map((i) => {
const p = path.getPointAtLength(i);
return projection.invert([p.x, p.y]);
});
const d0 = d3.geoDistance(pts[0], pts[1]) * (15500 / Math.PI);

// shuffling makes the algorithm a bit faster
return Object.assign(d3.shuffle(pts), { d0 });
}
Insert cell
// todo: for better performance, we could probably use a quadtree on the locally projected shape
function testSquare(points) {
const q = new kdbush(points);
const find = (c) => {
return geokdbush.around(q, ...c, 1, /* in kilometers! */ points.d0)[0];
};
return (a, c, b, d) => (c = find(c)) && (d = find(d)) && [a, c, b, d];
}
Insert cell
Insert cell
update_map = {
const g = d3.select(map).select("g");
g.html("")
.selectAll()
.data(squares)
.join("path")
.style("stroke", ([id, a]) => (a ? "red" : "blue"))
.datum(([id, a]) =>
a
? { type: "Polygon", coordinates: [[...a, a[0]]], id }
: {
type: "Point",
coordinates: d3.geoCentroid(
world.features.find((p) => p.id === id)
),
id
}
)
.append("title")
.text(({ id }) => names.get(id));
map.render();
}
Insert cell
Insert cell
Insert cell
errorDiagonal = ([a, b, c, d]) =>
Math.abs(Math.log(d3.geoDistance(a, c) / d3.geoDistance(b, d)))
Insert cell
errorEdges = ([a, b, c, d]) => {
const [lo, hi] = d3.extent(
d3.pairs([a, b, c, d, a]).map(([a, b]) => d3.geoDistance(a, b))
);
return Math.log(hi / lo);
}
Insert cell
median_errors = [
d3.median(squares, ([, d]) => errorDiagonal(d)),
d3.median(squares, ([, d]) => errorEdges(d))
]
Insert cell
Insert cell
topo = d3.json(
"https://cdn.jsdelivr.net/npm/visionscarto-world-atlas@0.1.0/world/110m.json"
)
Insert cell
world = topojson.feature(topo, topo.objects.countries)
Insert cell
land = topojson.feature(topo, topo.objects.land)
Insert cell
names = d3
.tsv(
"https://cdn.jsdelivr.net/npm/visionscarto-world-atlas@0.0.6/world/110m.tsv"
)
.then((d) => new Map(d.map(({ id, name_long }) => [id, name_long])))
Insert cell
height = width
Insert cell
d3 = require("d3@7")
Insert cell
attitude = require("attitude")
Insert cell
Insert cell
kdbush = import("https://cdn.skypack.dev/kdbush@3").then((d) => d.default)
Insert cell
geokdbush = import("https://cdn.skypack.dev/geokdbush@1").then((d) => d.default)
Insert cell
import { zoom } from "@d3/versor-zooming"
Insert cell
import { degrees } from "@fil/math"
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