Two sovereign countries live entirely inside Italy: San Marino (61 km²) and Vatican City (0.44 km² — the smallest country on earth). Both only exist in the data at detail=high. Fitted to Italy, San Marino is about five pixels and the Vatican is smaller than one — so the map zooms to 400× and the buttons fly you to each enclave. Hover any polygon for its name; markers keep constant size as you zoom. Italy's polygon carves both enclaves out with holes that match their outlines exactly, so the three responses compose seamlessly.
https://api.mapjson.com/v1/geo?filter=Italy&detail=high&properties=name,iso2 https://api.mapjson.com/v1/geo?filter=San%20Marino&detail=high&properties=name,iso2 https://api.mapjson.com/v1/geo?filter=Vatican%20City&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>
<button data-jump="italy">Italy</button>
<button data-jump="sm">San Marino</button>
<button data-jump="va">Vatican City</button>
<div id="tooltip"></div>
<script>
const width = 960, height = 640;
const svg = d3.select("#map").append("svg").attr("viewBox", `0 0 ${width} ${height}`);
const tooltip = d3.select("#tooltip");
const projection = d3.geoMercator();
const path = d3.geoPath(projection);
const base = "https://api.mapjson.com/v1/geo?detail=high&properties=name,iso2&filter=";
const FILL = { IT: "#b8c9a8", SM: "#4a7fb5", VA: "#d4a017" }; // Italy, San Marino, Vatican
const gLand = svg.append("g"); // zoomed (CSS transform)
const gMarks = svg.append("g"); // repositioned each frame — markers stay a fixed size
const places = [
{ name: "San Marino", lng: 12.4578, lat: 43.9424, color: "#4a7fb5" },
{ name: "Vatican City", lng: 12.4534, lat: 41.9033, color: "#d4a017" },
{ name: "Rome", lng: 12.4964, lat: 41.9028, color: "#5a5a5a", 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").attr("stroke-width", 1.2);
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.small ? 2.5 : 4).attr("fill", d => d.color);
m.select("text").attr("y", d => d.small ? -8 : -10).attr("font-size", d => d.small ? 10 : 12).text(d => d.name);
}
const zoom = d3.zoom().scaleExtent([1, 400]).on("zoom", (e) => {
gLand.attr("transform", e.transform);
renderMarks(e.transform);
});
svg.call(zoom);
// fly the viewport to [lng, lat] at zoom k (k = 1 resets to all of Italy)
function jumpTo(lng, lat, k) {
const [x, y] = projection([lng, lat]);
const t = k === 1 ? d3.zoomIdentity
: d3.zoomIdentity.translate(width / 2, height / 2).scale(k).translate(-x, -y);
svg.transition().duration(1400).call(zoom.transform, t);
}
const JUMPS = { italy: [12.5, 42.5, 1], sm: [12.4578, 43.9424, 60], va: [12.4534, 41.9033, 350] };
document.querySelectorAll("[data-jump]").forEach(b =>
b.addEventListener("click", () => jumpTo(...JUMPS[b.dataset.jump])));
Promise.all(["Italy", "San Marino", "Vatican City"].map(f => d3.json(base + encodeURIComponent(f)))).then(topos => {
const feats = topos.flatMap(t => topojson.feature(t, t.objects.geo).features);
// fit the projection to Italy; the enclaves are inside it by definition
projection.fitExtent([[24, 24], [width - 24, height - 24]],
{ type: "FeatureCollection", features: feats.filter(d => d.properties.iso2 === "IT") });
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.6).attr("vector-effect", "non-scaling-stroke")
.style("cursor", "pointer")
.on("mouseover", function (e, d) {
d3.select(this).attr("stroke", "#1a1a1a");
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.zoomTransform(svg.node()));
});
</script>
</body>
</html>