← Examples
countries · high detail · multiple filters

Strait of Gibraltar

Gibraltar (ISO GI) is about 7 km² — it only exists in the data at detail=high, and vanishes at lower resolutions. Three filter calls — Spain, Morocco, and Gibraltar — are drawn together to frame the strait, each as its own polygon with its ISO 3166-1 code. Ceuta is a Spanish city with no ISO 3166-1 code, so it can't be a polygon here — it's a marker. Hover to see names; scroll to zoom in on the Rock.

API Calls

https://api.mapjson.com/v1/geo?filter=Spain&detail=high&properties=name,iso2
https://api.mapjson.com/v1/geo?filter=Morocco&detail=high&properties=name,iso2
https://api.mapjson.com/v1/geo?filter=Gibraltar&detail=high&properties=name,iso2

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; }
  </style>
</head>
<body>
  <div id="map"></div>
  <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");

    // A tight Mercator on the strait (no fitExtent — the countries themselves are much larger).
    const projection = d3.geoMercator().center([-5.45, 35.95]).scale(22000).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 = { ES: "#b8c9a8", MA: "#d9c39a", GI: "#c0392b" };   // Spain, Morocco, Gibraltar

    const gLand = svg.append("g");     // zoomed (CSS transform)
    const gMarks = svg.append("g");    // NOT zoomed — repositioned each frame so markers stay a fixed size

    // Ceuta has no ISO 3166-1 code (it's a city of Spain), so it's a marker, not a polygon.
    const places = [
      { name: "Gibraltar", lng: -5.3536, lat: 36.1408 },
      { name: "Ceuta",     lng: -5.3213, lat: 35.8894, marker: true },
      { name: "Tangier",   lng: -5.8129, lat: 35.7595, small: true },
      { name: "Tarifa",    lng: -5.6045, lat: 36.0143, small: true },
    ];
    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("font-weight", 600)
        .attr("fill", "#1a1a1a").attr("stroke", "#fff").attr("stroke-width", 3).attr("paint-order", "stroke");
      const m = ent.merge(mk).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.marker ? 6 : d.small ? 2.5 : 3)
        .attr("fill", d => d.marker ? "#2f6fb0" : "#444").attr("stroke-width", d => d.marker ? 1.5 : 0.8);
      m.select("text").attr("y", d => (d.marker ? 6 : d.small ? 2.5 : 3) * -1 - 6)
        .attr("font-size", d => d.small ? 10 : 12).text(d => d.name);
    }

    Promise.all(["Spain", "Morocco", "Gibraltar"].map(f => d3.json(base + f))).then(topos => {
      const feats = topos.flatMap(t => topojson.feature(t, t.objects.geo).features);
      gLand.selectAll("path").data(feats).join("path")
        .attr("d", path)
        .attr("fill", d => fill[d.properties.iso2] || "#d9d0be")
        .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); });
    });

    renderMarks(d3.zoomIdentity);

    // scroll / pinch to zoom in on the Rock; drag to pan. Land scales; markers stay a fixed size.
    svg.call(d3.zoom().scaleExtent([1, 60]).on("zoom", (e) => {
      gLand.attr("transform", e.transform);
      renderMarks(e.transform);
    }));
  </script>
</body>
</html>