← Examples
countries · high detail · multiple filters · satellite tiles

Strait of Hormuz

The Strait of Hormuz — the chokepoint between Iran and Oman's Musandam Peninsula, and the sea lane for roughly a fifth of the world's traded oil — is only about 21 nautical miles wide at its narrowest. This map draws all eight littoral states around it, each from its own high-detail filter call: Iran, Iraq, Kuwait, Saudi Arabia, Bahrain, Qatar, the UAE, and Oman. Two dashed lines measure the narrows; the note below explains why both “33 km” and “39 km” get quoted. Hover a country for its ISO code; scroll to zoom into the strait — the measurements appear as you do.

Imagery © Esri, Maxar, Earthstar Geographics
Scroll / pinch to zoom into the strait (bottom-right) — the two measurement labels fade in.

Measuring the chokepoint

The strait's famous “narrowest point” is measured between Larak Island (Iran) and Great Quoin Island (Oman) — a tiny islet off the tip of Musandam — at 21 nautical miles. That single figure gets quoted two ways: 21 nautical miles is ≈ 39 km, but read as 21 statute miles it becomes ≈ 33 km. Same distance, different mile (red line).

Great Quoin is too small for Natural Earth's 10m data, so it isn't in Oman's polygon at all — mapjson can't draw it, so it's a hand-placed marker. The nearest actual coastlines the data does carry — Larak to the Musandam mainland — sit about 49 km apart (grey line).

API Calls

https://api.mapjson.com/v1/geo?filter={country}&detail=high&properties=name,iso2

// {country} = Iran · Iraq · Kuwait · Saudi Arabia · Bahrain · Qatar · United Arab Emirates · Oman

Code

<!DOCTYPE html>
<html>
<head>
  <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>
    svg { width: 100%; display: block; background: #7ab4cc; }
    #tooltip { position: fixed; background: #000; color: #fff; font-size: 12px; padding: 4px 8px; pointer-events: none; opacity: 0; }
    #tiles-toggle { font: 11px monospace; padding: 6px 12px; margin: 8px 0; cursor: pointer; }
    #attribution { font: 10px monospace; color: #888; }
  </style>
</head>
<body>
  <div id="map"></div>
  <button id="tiles-toggle">Hide satellite</button> <span id="attribution">Imagery © Esri</span>
  <div id="tooltip"></div>
  <script>
    const width = 960, height = 560;
    const svg = d3.select("#map").append("svg").attr("viewBox", `0 0 ${width} ${height}`);
    const tooltip = d3.select("#tooltip");

    // Mercator framed by hand on the whole Persian Gulf.
    const projection = d3.geoMercator().center([53.0, 26.9]).scale(3200).translate([width / 2, height / 2]);
    const path = d3.geoPath(projection);

    const base = "https://api.mapjson.com/v1/geo?detail=high&properties=name,iso2&filter=";
    const fill = { IR:"#c1734e", IQ:"#8aa06a", KW:"#e2a13e", SA:"#cdbd82", BH:"#a97bb0", QA:"#d98b64", AE:"#d9c169", OM:"#7fa07c" };
    const countries = ["Iran", "Iraq", "Kuwait", "Saudi Arabia", "Bahrain", "Qatar", "United Arab Emirates", "Oman"];

    // Narrowest is Larak (Iran) → Great Quoin islet (Oman): 21 nmi ≈ 39 km (or 33 km read as
    // statute miles). Great Quoin is absent from Natural Earth 10m. The nearest coastlines the
    // data carries — Larak → Musandam mainland — are ~49 km apart.
    const LARAK_Q = [56.379, 26.839], GREAT_QUOIN = [56.517, 26.483];
    const LARAK_C = [56.341, 26.828], MUSANDAM_TIP = [56.364, 26.386];

    const gLand      = svg.append("g");         // zoomed (CSS transform)
    const gTiles     = gLand.append("g");       // satellite raster tiles (back)
    const gCountries = gLand.append("g");       // country polygons
    const gLines     = gLand.append("g");       // measurement lines (front)
    const gMarks     = svg.append("g");         // NOT zoomed — fixed-size dots & labels

    [ { a: LARAK_Q, b: GREAT_QUOIN,  color: "#b0402f", dash: "6 4" },   // official narrowest → islet
      { a: LARAK_C, b: MUSANDAM_TIP, color: "#33475a", dash: "3 4" },   // coastline gap in the data
    ].forEach((L) => { const p1 = projection(L.a), p2 = projection(L.b);
      gLines.append("line").attr("x1", p1[0]).attr("y1", p1[1]).attr("x2", p2[0]).attr("y2", p2[1])
        .attr("stroke", L.color).attr("stroke-width", 1.4).attr("stroke-dasharray", L.dash).attr("vector-effect", "non-scaling-stroke"); });

    // ---- satellite basemap (Esri World Imagery) — placed in map space so it scales with zoom ----
    let tilesOn = true;
    const TILE_Z = 7;
    function drawTiles() {
      const n = 1 << TILE_Z, size = projection.scale() * 2 * Math.PI / n;
      const c0 = projection.invert([0, 0]), c1 = projection.invert([width, height]);
      const xT = (lng) => (lng + 180) / 360 * n;
      const yT = (lat) => (1 - Math.asinh(Math.tan(lat * Math.PI / 180)) / Math.PI) / 2 * n;
      for (let x = Math.floor(xT(Math.min(c0[0], c1[0]))); x <= Math.floor(xT(Math.max(c0[0], c1[0]))); x++)
        for (let y = Math.floor(yT(Math.max(c0[1], c1[1]))); y <= Math.floor(yT(Math.min(c0[1], c1[1]))); y++) {
          const lng = x / n * 360 - 180, lat = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) * 180 / Math.PI;
          const p = projection([lng, lat]);
          gTiles.append("image")
            .attr("href", `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/${TILE_Z}/${y}/${x}`)
            .attr("x", p[0]).attr("y", p[1]).attr("width", size + 0.6).attr("height", size + 0.6)
            .attr("preserveAspectRatio", "none");
        }
    }
    function applyTiles() {
      gTiles.style("display", tilesOn ? null : "none");
      gCountries.selectAll("path").attr("fill-opacity", tilesOn ? 0.5 : 1);
      document.getElementById("attribution").style.display = tilesOn ? "" : "none";
      document.getElementById("tiles-toggle").textContent = tilesOn ? "Hide satellite" : "Show satellite";
    }
    drawTiles();
    applyTiles();
    document.getElementById("tiles-toggle").addEventListener("click", () => { tilesOn = !tilesOn; applyTiles(); });

    // Cities (dots), the Great Quoin islet (hollow dot — not in the data), seas & measurement
    // labels (no dot). Islet + measurement labels only appear once zoomed in (declutter).
    const places = [
      { name: "Basra",        lng: 47.82, lat: 30.51, type: "city", small: true },
      { name: "Kuwait City",  lng: 47.98, lat: 29.37, type: "city" },
      { name: "Dammam",       lng: 50.10, lat: 26.43, type: "city", small: true },
      { name: "Manama",       lng: 50.58, lat: 26.23, type: "city" },
      { name: "Doha",         lng: 51.53, lat: 25.29, type: "city" },
      { name: "Abu Dhabi",    lng: 54.37, lat: 24.45, type: "city" },
      { name: "Dubai",        lng: 55.27, lat: 25.20, type: "city" },
      { name: "Bandar Abbas", lng: 56.27, lat: 27.18, type: "city" },
      { name: "Khasab",       lng: 56.24, lat: 26.18, type: "city", small: true },
      { name: "Muscat",       lng: 58.54, lat: 23.61, type: "city" },
      { name: "Larak I.",     lng: 56.37, lat: 26.85, type: "island", zoom: 4 },
      { name: "Great Quoin I.", lng: 56.517, lat: 26.483, type: "islet", zoom: 4 },
      { name: "21 nmi · 39 km (33 mi)", lng: 56.448, lat: 26.661, type: "measure", dy: -6, accent: true, zoom: 4 },
      { name: "≈ 49 km coastline",      lng: 56.353, lat: 26.607, type: "measure", dy: 15, zoom: 4 },
      { name: "Strait of Hormuz", lng: 57.05, lat: 26.20, type: "sea", strait: true },
      { name: "Persian Gulf", lng: 51.60, lat: 27.30, type: "sea" },
      { name: "Gulf of Oman", lng: 58.30, lat: 24.70, type: "sea" },
    ];
    function renderMarks(t) {
      const mk = gMarks.selectAll("g.mk").data(places);
      const ent = mk.enter().append("g").attr("class", "mk");
      ent.append("circle").attr("stroke", "#fff");
      ent.append("text").attr("text-anchor", "middle").attr("font-family", "monospace")
        .attr("stroke", "#fff").attr("stroke-width", 3).attr("paint-order", "stroke");
      const m = ent.merge(mk)
        .style("opacity", d => (d.zoom && t.k < d.zoom) ? 0 : 1)
        .attr("transform", d => { const p = projection([d.lng, d.lat]); return `translate(${t.applyX(p[0])},${t.applyY(p[1])})`; });
      m.select("circle")
        .attr("r", d => (d.type === "sea" || d.type === "measure") ? 0 : d.type === "islet" ? 2.4 : d.type === "island" ? 2.4 : (d.small ? 2.5 : 3.4))
        .attr("fill", d => d.type === "islet" ? "#7ab4cc" : "#333")
        .attr("stroke", d => d.type === "islet" ? "#b0402f" : "#fff")
        .attr("stroke-width", d => (d.type === "sea" || d.type === "measure") ? 0 : d.type === "islet" ? 1.3 : 0.9);
      m.select("text")
        .attr("y", d => d.type === "sea" ? 4 : d.type === "measure" ? (d.dy || 0) : (-(d.small ? 2.5 : 3.4) - 6))
        .attr("font-size", d => d.type === "measure" ? 11 : d.type === "sea" ? 12 : (d.small || d.type === "island" || d.type === "islet") ? 10 : 12)
        .attr("font-weight", d => d.type === "sea" ? 400 : 600)
        .attr("font-style", d => d.type === "sea" ? "italic" : "normal")
        .attr("letter-spacing", d => d.type === "sea" ? "0.14em" : "0")
        .attr("fill", d => (d.accent || d.strait) ? "#b0402f" : d.type === "sea" ? "#2f6fb0" : d.type === "measure" ? "#33475a" : "#1a1a1a")
        .text(d => d.name);
    }

    Promise.all(countries.map(f => d3.json(base + encodeURIComponent(f)))).then(topos => {
      const feats = topos.flatMap(t => topojson.feature(t, t.objects.geo).features);
      gCountries.selectAll("path").data(feats).join("path")
        .attr("d", path)
        .attr("fill", d => fill[d.properties.iso2] || "#d9d0be")
        .attr("fill-opacity", tilesOn ? 0.5 : 1)
        .attr("stroke", "#fff").attr("stroke-width", 0.7).attr("vector-effect", "non-scaling-stroke")
        .style("cursor", "pointer")
        .on("mouseover", function (e, d) { d3.select(this).attr("stroke", "#1a1a1a").raise();
          tooltip.style("opacity", 1).text(d.properties.name + " (" + d.properties.iso2 + ")"); })
        .on("mousemove", e => tooltip.style("left", (e.clientX + 12) + "px").style("top", (e.clientY - 8) + "px"))
        .on("mouseout", function () { d3.select(this).attr("stroke", "#fff"); tooltip.style("opacity", 0); });
    });

    const zoom = d3.zoom().scaleExtent([1, 80]).on("zoom", (e) => { gLand.attr("transform", e.transform); renderMarks(e.transform); });
    svg.call(zoom);
    // load already zoomed into the Gulf so it fills the frame (drag/scroll to explore)
    const c0 = projection([53.6, 27.0]), k0 = 1.3;
    svg.call(zoom.transform, d3.zoomIdentity.translate(width / 2 - k0 * c0[0], height / 2 - k0 * c0[1]).scale(k0));
  </script>
</body>
</html>