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.
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.
https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=medium&properties=name,continent&format=geojson
<!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>