← Examples
countries · world · landlocked · topology · Equal Earth

Landlocked Countries

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().

has a coastline
landlocked (44)
doubly landlocked (2)

Doubly landlocked

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 .

API Call

https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=medium&properties=name

Code

<!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>