Throw away the coastlines and keep the numbers. Every nation becomes a circle sized by its land area — the authoritative areakm2 mapjson serves — and a force simulation packs them, each tugged toward its true position but forbidden to overlap. Enormous, near-empty Russia and Canada bully aside the dense little cluster of Europe. A world map you read by weight, not by shape. One /v1/geo call.
One mapjson call brings back each country's polygon (for its position) and its areakm2. Every country becomes a node placed at its projected centroid, with a radius on a square-root scale of area. A d3.forceSimulation then runs three forces — a gentle pull back toward the true centroid on each axis, and a collision force at each radius — so the bubbles settle into a tight, non-overlapping packing that still echoes the real geography. Hover a bubble for the country and its size.
https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=low&properties=name,areakm2,continent&format=geojson
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Dorling Earth</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { margin: 0; background: #fafafa; font-family: system-ui, sans-serif; }
.wrap { max-width: 980px; margin: 2rem auto; padding: 0 1rem; }
#map { background: #f7f5ef; border-radius: 4px; overflow: hidden; }
#map svg { width: 100%; display: block; }
</style>
</head>
<body>
<div class="wrap">
<div id="map"></div>
<div id="tooltip" style="position:fixed;opacity:0"></div>
<button id="repaint" style="margin-top:.7rem;font:12px ui-monospace,monospace;padding:6px 12px">↻ re-pack</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<script>
const W = 960, H = 560;
const svg = d3.select("#map").append("svg").attr("viewBox", `0 0 ${W} ${H}`);
const tip = d3.select("#tooltip");
const CONTS = ["Africa", "Asia", "Europe", "North America", "South America", "Oceania", "Antarctica", "Seven seas (open ocean)"];
const color = d3.scaleOrdinal(CONTS, ["#e07a3c", "#c0453a", "#3f6fb0", "#4f9e5a", "#e0b23c", "#7a4fa0", "#8aa0b8", "#8aa0b8"]);
let base = null;
function paint() {
svg.selectAll("*").remove();
svg.append("rect").attr("width", W).attr("height", H).attr("fill", "#f7f5ef");
const nodes = base.map((d) => ({ ...d, x: d.cx + (Math.random() - 0.5) * 4, y: d.cy + (Math.random() - 0.5) * 4 }));
const sim = d3.forceSimulation(nodes)
.force("x", d3.forceX((d) => d.cx).strength(0.35))
.force("y", d3.forceY((d) => d.cy).strength(0.55))
.force("collide", d3.forceCollide((d) => d.r + 0.7).strength(0.9))
.stop();
for (let i = 0; i < 320; i++) sim.tick();
svg.append("g").selectAll("circle").data(nodes).join("circle")
.attr("cx", (d) => d.x).attr("cy", (d) => d.y).attr("r", (d) => d.r)
.attr("fill", (d) => color(d.continent)).attr("fill-opacity", 0.85)
.attr("stroke", "#fff").attr("stroke-width", 0.8).style("cursor", "pointer")
.on("mousemove", (e, d) => tip.style("opacity", 1).style("left", (e.clientX + 12) + "px").style("top", (e.clientY - 8) + "px").html(`<b>${d.name}</b> · ${Math.round(d.area).toLocaleString()} km²`))
.on("mouseout", () => tip.style("opacity", 0));
}
d3.json("https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=low&properties=name,areakm2,continent&format=geojson").then((w) => {
const projection = d3.geoEqualEarth().rotate([-11, 0]).fitExtent([[10, 10], [W - 10, H - 10]], { type: "Sphere" });
const path = d3.geoPath(projection);
const rScale = d3.scaleSqrt([0, d3.max(w.features, (f) => f.properties.areakm2 || 0)], [1.5, 52]);
base = w.features.map((f) => {
const c = path.centroid(f), a = f.properties.areakm2 || 0;
if (!isFinite(c[0]) || a <= 0) return null;
return { cx: c[0], cy: c[1], r: rScale(a), area: a, name: f.properties.name, continent: f.properties.continent };
}).filter(Boolean);
paint();
});
document.getElementById("repaint").addEventListener("click", paint);
</script>
</body>
</html>