Every country's capital plotted from the capital, capitalLat and
capitalLng properties. The map below is rendered client-side like every other
example — but the code shown is a complete Node.js script that produces the same SVG
on a server. The API is plain JSON over HTTPS and d3-geo needs no DOM, so the exact same
call and projection code run anywhere: cron jobs, build steps, PDF pipelines, or an
<img> endpoint.
https://api.mapjson.com/v1/geo?properties=name,capital,capitalLat,capitalLng
Run with node capitals.mjs > capitals.svg (Node 18+,
npm install d3 topojson-client). No browser, no DOM, no headless Chrome —
d3's path generator returns plain strings.
// capitals.mjs — render a world capitals map to SVG, no browser required
// npm install d3 topojson-client
// node capitals.mjs > capitals.svg
import * as d3 from "d3";
import { feature } from "topojson-client";
const API = "https://api.mapjson.com/v1/geo?properties=name,capital,capitalLat,capitalLng";
const topo = await (await fetch(API)).json();
const fc = feature(topo, topo.objects.geo);
const width = 960, height = 500;
const projection = d3.geoNaturalEarth1().fitExtent([[8, 8], [width - 8, height - 8]], fc);
const path = d3.geoPath(projection); // no canvas context → returns SVG path strings
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<");
const countries = fc.features.map((f) =>
`<path d="${path(f)}" fill="#d9d0be" stroke="#fff" stroke-width="0.5"/>`);
const capitals = fc.features
.filter((f) => f.properties.capitalLat != null)
.map((f) => {
const p = f.properties;
const [x, y] = projection([p.capitalLng, p.capitalLat]);
return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="2" fill="#b22222">` +
`<title>${esc(p.capital)} — ${esc(p.name)}</title></circle>`;
});
process.stdout.write(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">
<rect width="${width}" height="${height}" fill="#cfe0ea"/>
${countries.join("\n")}
${capitals.join("\n")}
</svg>
`);