Every country with no coastline carries a landlocked: true property — the same always-on flag as disputed, set for all 44 landlocked sovereign states. This map shades them in amber. Two go a step further: doubly landlocked countries, where every neighbour is also landlocked, so you must cross at least two borders to reach the sea. They aren't hardcoded here — they're found live by walking the shared-border topology with topojson.neighbors().
A country is doubly landlocked when all of its neighbours are landlocked too — reaching open ocean means crossing at least two international borders. Rather than list them by hand, the map derives them from its own geometry: topojson.neighbors() reports which countries share an arc (a land border), then we keep the landlocked ones whose neighbours are all landlocked.
Computed live from the topology, the only two on Earth are ….
https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=medium&properties=name
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Landlocked Countries</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: #1b3a4b; }
svg { width: 100%; height: 100vh; display: block; }
.country { transition: opacity 0.1s; }
.country:hover { opacity: 0.82; }
#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 = "#1b3a4b", GRAT = "#2c5468", LAND = "#d8cfbc",
SINGLE = "#e39a3c", DOUBLE = "#b23a1e", STROKE = "#9c8f73";
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);
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);
d3.json("https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=medium&properties=name")
.then(topo => {
const obj = topo.objects.geo;
const features = topojson.feature(topo, obj).features; // same order as geometries
const neighbors = topojson.neighbors(obj.geometries); // shared-border adjacency
// landlocked is an always-on flag; doubly landlocked = every neighbour landlocked too
const isLandlocked = features.map(f => f.properties.landlocked === true);
features.forEach((f, i) => {
f.properties._landlocked = isLandlocked[i];
f.properties._doubly = isLandlocked[i]
&& neighbors[i].length > 0
&& neighbors[i].every(j => isLandlocked[j]);
f.properties._nbr = neighbors[i].length;
});
svg.append("g").selectAll("path").data(features).join("path")
.attr("class", "country")
.attr("d", path)
.attr("fill", d => d.properties._doubly ? DOUBLE : d.properties._landlocked ? SINGLE : LAND)
.attr("stroke", STROKE)
.attr("stroke-width", d => d.properties._doubly ? 0.8 : 0.3)
.on("mousemove", function (event, d) {
const s = d.properties._doubly ? `doubly landlocked · all ${d.properties._nbr} neighbours landlocked`
: d.properties._landlocked ? "landlocked" : "has a coastline";
tooltip.style.left = (event.clientX + 12) + "px";
tooltip.style.top = (event.clientY - 10) + "px";
tooltip.style.opacity = 1;
tooltip.innerHTML = `<strong>${d.properties.name || d.properties.gid}</strong><br>${s}`;
})
.on("mouseleave", () => { tooltip.style.opacity = 0; });
// Label the doubly-landlocked pair — Liechtenstein is a pixel at world scale
const OFFSET = { UZ: [0, -8, "middle"], LI: [-46, -22, "end"] };
const g = svg.append("g");
features.filter(d => d.properties._doubly).forEach(d => {
const c = path.centroid(d);
const [dx, dy, anchor] = OFFSET[d.properties.gid] || [0, -10, "middle"];
g.append("line").attr("x1", c[0]).attr("y1", c[1]).attr("x2", c[0] + dx).attr("y2", c[1] + dy + 3)
.attr("stroke", "#f2ece0").attr("stroke-width", 0.6).attr("opacity", 0.7);
g.append("circle").attr("cx", c[0]).attr("cy", c[1]).attr("r", 2.6)
.attr("fill", DOUBLE).attr("stroke", "#f2ece0").attr("stroke-width", 0.8);
g.append("text").attr("x", c[0] + dx).attr("y", c[1] + dy).attr("text-anchor", anchor)
.attr("font-family", "monospace").attr("font-size", "10px").attr("fill", "#f6efe0")
.attr("paint-order", "stroke").attr("stroke", OCEAN).attr("stroke-width", 2.4).attr("stroke-linejoin", "round")
.text(d.properties.name);
});
});
</script>
</body>
</html>