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. Hover any country for every opt-in property the API returns, plus three things computed client-side that the API doesn't provide at all: its centroid and bounding box (d3.geoCentroid / d3.geoBounds), and its topologically adjacent neighbors (topojson.neighbors, derived from shared arcs in the same response — no separate borders dataset).
Neighbor detection is exact — it comes from arcs literally shared between two borders in the topology — with one honest gap: a handful of countries (France, Norway, the Netherlands, Morocco/Western Sahara) get their boundary from a different source than their neighbors, so the shared-arc check has nothing to match. Try France to see how that's reported, rather than silently showing an empty list.
https://api.mapjson.com/v1/geo?layer=countries&detail=medium&properties=name,nameOfficial,iso2,iso3,isoNum,continent,subregion,areakm2,capital,capitalLat,capitalLng,currencies,languages,idd,demonym
<!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">hover a country</pre>
<script>
const PROPS = "name,nameOfficial,iso2,iso3,isoNum,continent,subregion,areakm2,capital,capitalLat,capitalLng,currencies,languages,idd,demonym";
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");
d3.json(url).then(topo => {
const objects = topo.objects.geo.geometries;
const fc = topojson.feature(topo, topo.objects.geo);
// Adjacency from the topology's shared arcs — no separate borders file
const neighborIdx = topojson.neighbors(objects);
const projection = d3.geoPatterson().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("mousemove", (event, d) => {
const i = fc.features.indexOf(d);
const p = d.properties;
const centroid = d3.geoCentroid(d); // [lon, lat]
const bounds = d3.geoBounds(d); // [[w,s],[e,n]]
const neighbors = neighborIdx[i].map(j => fc.features[j].properties.name);
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>