// --- The Map ---
{
const width = 960;
const height = 500;
const projection = d3.geoNaturalEarth1()
.fitSize([width, height], {type: "Sphere"});
const path = d3.geoPath(projection);
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("max-width", "100%")
.style("background", "#f7fbff");
// Ocean
svg.append("path")
.datum({type: "Sphere"})
.attr("d", path)
.attr("fill", "#e8f0fe")
.attr("stroke", "#b0c4de")
.attr("stroke-width", 0.5);
// Countries
const countries = topojson.feature(world, world.objects.countries);
svg.append("g")
.selectAll("path")
.data(countries.features)
.join("path")
.attr("d", path)
.attr("fill", "#e8e8e0")
.attr("stroke", "#ccc")
.attr("stroke-width", 0.3);
// Country borders
svg.append("path")
.datum(topojson.mesh(world, world.objects.countries, (a, b) => a !== b))
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "#bbb")
.attr("stroke-width", 0.3);
// Conflict dots
const maxEvents = d3.max(filteredConflicts, d => d.events) || 1;
const conflictDots = svg.append("g")
.selectAll("circle")
.data(filteredConflicts.sort((a, b) => b.events - a.events))
.join("circle")
.attr("cx", d => {
const p = projection([d.lng, d.lat]);
return p ? p[0] : -100;
})
.attr("cy", d => {
const p = projection([d.lng, d.lat]);
return p ? p[1] : -100;
})
.attr("r", d => Math.max(1.5, Math.sqrt(d.events) * 1.2))
.attr("fill", "#e53935")
.attr("fill-opacity", 0.45)
.attr("stroke", "#c62828")
.attr("stroke-width", 0.3)
.attr("stroke-opacity", 0.4);
conflictDots.append("title")
.text(d => `${d.events} conflict event${d.events > 1 ? 's' : ''} (${d.lat.toFixed(1)}°, ${d.lng.toFixed(1)}°)`);
// Choke point markers
const chokeG = svg.append("g")
.selectAll("g")
.data(chokepoints)
.join("g")
.attr("transform", d => {
const p = projection([d.lng, d.lat]);
return p ? `translate(${p[0]},${p[1]})` : `translate(-100,-100)`;
});
// Outer ring
chokeG.append("circle")
.attr("r", 10)
.attr("fill", "none")
.attr("stroke", "#ff9800")
.attr("stroke-width", 2)
.attr("opacity", 0.7);
// Inner dot
chokeG.append("circle")
.attr("r", 3)
.attr("fill", "#ff9800")
.attr("opacity", 0.9);
// Tooltip
chokeG.append("title")
.text(d => `${d.name} — ${d.context}`);
// Labels
chokeG.append("text")
.attr("x", 14)
.attr("y", 4)
.attr("font-size", "8px")
.attr("font-family", "'Source Sans 3', sans-serif")
.attr("font-weight", "600")
.attr("fill", "#e65100")
.text(d => d.name);
// Legend
const legend = svg.append("g")
.attr("transform", `translate(20, ${height - 80})`);
legend.append("rect")
.attr("width", 180).attr("height", 70)
.attr("fill", "white").attr("opacity", 0.85)
.attr("rx", 4);
// Conflict dot legend
legend.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 5)
.attr("fill", "#e53935").attr("opacity", 0.5);
legend.append("text").attr("x", 28).attr("y", 19)
.attr("font-size", "10px").attr("fill", "#333")
.text("Conflict events");
// Choke point legend
legend.append("circle").attr("cx", 15).attr("cy", 35).attr("r", 6)
.attr("fill", "none").attr("stroke", "#ff9800").attr("stroke-width", 2);
legend.append("circle").attr("cx", 15).attr("cy", 35).attr("r", 2)
.attr("fill", "#ff9800");
legend.append("text").attr("x", 28).attr("y", 39)
.attr("font-size", "10px").attr("fill", "#333")
.text("Maritime choke point");
// Size legend
legend.append("circle").attr("cx", 10).attr("cy", 55).attr("r", 2)
.attr("fill", "#e53935").attr("opacity", 0.5);
legend.append("circle").attr("cx", 25).attr("cy", 55).attr("r", 4)
.attr("fill", "#e53935").attr("opacity", 0.5);
legend.append("circle").attr("cx", 45).attr("cy", 55).attr("r", 7)
.attr("fill", "#e53935").attr("opacity", 0.5);
legend.append("text").attr("x", 60).attr("y", 59)
.attr("font-size", "9px").attr("fill", "#666")
.text("Size = event count");
return svg.node();
}