← Examples
points · type=city · capitals · Equal Earth

Cities & Capitals

The /v1/points?type=city endpoint returns the world's most populous cities plus every national capital as a GeoJSON FeatureCollection. Circles are sized by popMetro — metropolitan / urban-agglomeration population, so Greater Tokyo reads ≈ 35.7M, not the ~9M city proper. National capitals (capital: true) are drawn in gold. Every point is pre-tagged with its country and admin1 region, so a single filter narrows the set to a continent, country, or state — no client-side geometry needed.

city (sized by metro population)
national capital

API Call

https://api.mapjson.com/v1/points?type=city&filter=world

Swap filter to narrow it: filter=FR (France), filter=US-MA (Massachusetts), or filter=europe (a continent).

Code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Cities &amp; Capitals</title>
  <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js"></script>
  <style>
    body { margin: 0; background: #16323f; }
    svg { width: 100%; height: 100vh; display: block; }
    circle { transition: opacity 0.1s; }
    #tooltip {
      position: fixed; background: #1a1a1a; color: #fafafa;
      font-family: monospace; font-size: 11px; padding: 6px 10px;
      pointer-events: none; opacity: 0; line-height: 1.6;
    }
  </style>
</head>
<body>
  <div id="tooltip"></div>
  <svg id="map"></svg>
  <script>
    const OCEAN = "#16323f", GRAT = "#264d5e", LAND = "#33505b",
          LAND_STROKE = "#1b3a4b", CITY = "#6fb0cc", CAPITAL = "#edb24a", CAP_STROKE = "#fff6e5";

    const width = 960, height = 500;
    const tooltip = document.getElementById("tooltip");
    const svg = d3.select("#map").attr("viewBox", `0 0 ${width} ${height}`);
    const projection = d3.geoEqualEarth().scale(168).translate([width / 2, height / 2 + 12]);
    const path = d3.geoPath(projection);
    const radius = d3.scaleSqrt().domain([0, 35000000]).range([0.6, 13]);

    svg.append("path").datum({ type: "Sphere" }).attr("fill", OCEAN).attr("d", path);
    svg.append("path").datum(d3.geoGraticule()())
      .attr("fill", "none").attr("stroke", GRAT).attr("stroke-width", 0.3).attr("d", path);

    const LAND_URL = "https://api.mapjson.com/v1/geo?layer=countries&detail=low&format=geojson";
    const CITY_URL = "https://api.mapjson.com/v1/points?type=city&filter=world";

    Promise.all([d3.json(LAND_URL), d3.json(CITY_URL)]).then(([land, cities]) => {
      svg.append("g").selectAll("path").data(land.features).join("path")
        .attr("d", path).attr("fill", LAND).attr("stroke", LAND_STROKE).attr("stroke-width", 0.3);

      // Biggest circles first so smaller ones stay clickable on top
      const feats = cities.features.slice().sort((a, b) => (b.properties.popMetro || 0) - (a.properties.popMetro || 0));

      svg.append("g").selectAll("circle").data(feats).join("circle")
        .attr("cx", d => projection(d.geometry.coordinates)[0])
        .attr("cy", d => projection(d.geometry.coordinates)[1])
        .attr("r", d => radius(d.properties.popMetro || 0))
        .attr("fill", d => d.properties.capital ? CAPITAL : CITY)
        .attr("fill-opacity", 0.85)
        .attr("stroke", d => d.properties.capital ? CAP_STROKE : "none")
        .attr("stroke-width", d => d.properties.capital ? 0.6 : 0)
        .on("mousemove", function (event, d) {
          const p = d.properties;
          tooltip.style.left = (event.clientX + 12) + "px";
          tooltip.style.top  = (event.clientY - 10) + "px";
          tooltip.style.opacity = 1;
          tooltip.innerHTML =
            `<strong>${p.name}${p.capital ? " ★" : ""}</strong><br>` +
            `${p.countryName}${p.region ? " · " + p.region : ""}<br>` +
            (p.popMetro ? p.popMetro.toLocaleString() + " (metro area)" : "population n/a");
        })
        .on("mouseleave", () => { tooltip.style.opacity = 0; });
    });
  </script>
</body>
</html>