← Examples
Dorling cartogram · data · area · art

Dorling Earth

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.

How it's made

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.

API Calls

https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=low&properties=name,areakm2,continent&format=geojson

Code

<!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>