World countries on the Patterson projection (d3.geoPatterson, from the d3-geo-projection extension) — a compromise cylindrical designed to keep high-latitude countries from ballooning the way they do on Mercator. Click any country for every opt-in property the API returns — including population, gdp, and its land neighbours from the borders property — plus its centroid and bounding box, computed client-side (d3.geoCentroid / d3.geoBounds).
Neighbours come from the API's borders property — authoritative land-border adjacency (ISO2 codes), complete for every country including the ones whose geometry is stitched from a different source (France, Norway, the Netherlands, Morocco). Island nations correctly report no land borders. A neighbour too small to appear at this detail tier (e.g. Gibraltar) has its name filled in from the /v1/catalog endpoint — a complete ISO2 → name list.
https://api.mapjson.com/v1/geo?layer=countries&detail=medium&properties=name,nameOfficial,iso2,iso3,isoNum,continent,subregion,areakm2,population,gdp,capital,capitalLat,capitalLng,currencies,languages,idd,demonym,borders
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Country Details</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-geo-projection@4/dist/d3-geo-projection.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js"></script>
<style>
body { margin: 0; background: #cfe0ea; }
svg { width: 100%; height: 100vh; display: block; }
</style>
</head>
<body>
<svg id="map"></svg>
<pre id="out">click a country</pre>
<script>
const PROPS = "name,nameOfficial,iso2,iso3,isoNum,continent,subregion,areakm2,population,gdp,capital,capitalLat,capitalLng,currencies,languages,idd,demonym,borders";
const url = `https://api.mapjson.com/v1/geo?layer=countries&detail=medium&properties=${PROPS}`;
const width = 960, height = 500;
const svg = d3.select("#map").attr("viewBox", `0 0 ${width} ${height}`);
const out = document.getElementById("out");
Promise.all([
d3.json(url),
d3.json("https://api.mapjson.com/v1/catalog"), // complete iso2 → name list
]).then(([topo, catalog]) => {
const fc = topojson.feature(topo, topo.objects.geo);
// Neighbours come from the `borders` property (iso2 codes). Resolve to names via the
// map data, and fall back to the catalog for territories absent at this detail tier
// (e.g. Gibraltar), so a neighbour never shows as a bare code.
const nameByIso2 = new Map(fc.features.map(f => [f.properties.iso2, f.properties.name]));
for (const c of catalog) if (/^[A-Z]{2}$/.test(c.gid) && !nameByIso2.has(c.gid)) nameByIso2.set(c.gid, c.name);
const projection = d3.geoPatterson().rotate([-11, 0]).fitExtent([[8, 8], [width - 8, height - 8]], fc);
const path = d3.geoPath(projection);
svg.selectAll("path")
.data(fc.features)
.join("path")
.attr("d", path)
.attr("fill", "#d9d0be")
.attr("stroke", "#fff")
.attr("stroke-width", 0.5)
.on("click", (event, d) => {
const p = d.properties;
const centroid = d3.geoCentroid(d); // [lon, lat]
const bounds = d3.geoBounds(d); // [[w,s],[e,n]]
const neighbors = (p.borders || []).map(code => nameByIso2.get(code) || code);
out.textContent = JSON.stringify({
...p,
centroid: centroid.map(v => +v.toFixed(2)),
boundingBox: bounds.map(pt => pt.map(v => +v.toFixed(2))),
neighbors,
}, null, 2);
});
});
</script>
</body>
</html>