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