← Examples
typography · text-as-shape · art

Typographic Earth

A world map with no borders drawn at all — every country is filled with its own name, repeated and tiled to the exact shape of its territory. Russia is a broad slab of RUSSIA·RUSSIA; Chile a thin vertical ribbon of CHILE; the tiny states shrink to a single word. Scroll to zoom, drag to pan — the words grow as you go in. One /v1/geo call.

How it's made

One mapjson call returns each country's polygon and its name. For every country, an SVG clipPath is cut to its outline, and rows of monospace text — the name repeated across the width — are laid down inside the bounding box and clipped to that shape. The type size scales with the country's height, so big nations read boldly and small ones fade to a single instance. No stroke anywhere: the shapes exist only as the words that fill them, tinted by continent.

API Calls

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

Code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Typographic 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: #fbfaf6; border-radius: 4px; overflow: hidden; }
    #map svg { width: 100%; display: block; cursor: grab; }
  </style>
</head>
<body>
  <div class="wrap"><div id="map"></div></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 CONTS = ["Africa", "Asia", "Europe", "North America", "South America", "Oceania", "Antarctica", "Seven seas (open ocean)"];
  const color = d3.scaleOrdinal(CONTS, ["#b45a25", "#a5352c", "#2f5a97", "#3d7a48", "#b58a24", "#5f3f83", "#7a8a9a", "#7a8a9a"]);

  d3.json("https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=medium&properties=name,continent&format=geojson").then((w) => {
    const projection = d3.geoEqualEarth().rotate([-11, 0]).fitExtent([[14, 14], [W - 14, H - 14]], { type: "Sphere" });
    const path = d3.geoPath(projection);
    svg.append("rect").attr("width", W).attr("height", H).attr("fill", "#fbfaf6");
    const defs = svg.append("defs");
    const zoomG = svg.append("g");

    w.features.forEach((f, i) => {
      const name = (f.properties.name || "").toUpperCase();
      if (!name) return;
      const b = path.bounds(f);
      if (!isFinite(b[0][0])) return;
      const bw = b[1][0] - b[0][0], bh = b[1][1] - b[0][1];
      if (bw < 3 || bh < 3) return;
      const cid = "clip" + i;
      defs.append("clipPath").attr("id", cid).append("path").attr("d", path(f));
      const fs = Math.max(4, Math.min(bh * 0.42, 24));
      const rowH = fs * 1.0, chW = fs * 0.62;
      const reps = Math.max(1, Math.ceil((bw + chW * name.length) / (chW * (name.length + 1))));
      const txt = (name + "·").repeat(reps);
      const g = zoomG.append("g").attr("clip-path", `url(#${cid})`)
        .attr("font-family", "'IBM Plex Mono', monospace").attr("font-size", fs)
        .attr("fill", color(f.properties.continent)).attr("letter-spacing", "-0.5px");
      for (let y = b[0][1] + fs; y < b[1][1] + rowH; y += rowH) {
        g.append("text").attr("x", b[0][0] - (y % (rowH * 2) < rowH ? 0 : chW * 1.4)).attr("y", y).text(txt);
      }
    });

    const zoom = d3.zoom().scaleExtent([1, 12]).translateExtent([[0, 0], [W, H]]).extent([[0, 0], [W, H]])
      .on("zoom", (e) => zoomG.attr("transform", e.transform));
    svg.call(zoom).style("cursor", "grab");
  });
  </script>
</body>
</html>