{
const svg = d3.create('svg')
.attr('width', visWidth)
.attr('height', visHeight);
const g = svg.append('g');
const projection = d3.geoNaturalEarth1()
.scale(visWidth / 6.5)
.translate([visWidth / 2, visHeight / 2]);
const path = d3.geoPath().projection(projection);
const donated = d3.rollup(aiddata, v => d3.sum(v, d => d.amount), d => d.donor);
const received = d3.rollup(aiddata, v => d3.sum(v, d => d.amount), d => d.recipient);
const nameFixes = {
"United States of America": "United States",
"South Korea": "Korea, Republic of",
"Russia": "Russian Federation",
"Iran": "Iran (Islamic Republic of)",
"Vietnam": "Viet Nam",
"Syria": "Syrian Arab Republic",
"Libya": "Libyan Arab Jamahiriya",
"Venezuela": "Venezuela (Bolivarian Republic of)",
"North Korea": "Korea, Democratic People's Republic of",
"Taiwan": "Taiwan Province of China"
};
const balanceByCountry = new Map();
const totals = [];
Array.from(new Set([...donated.keys(), ...received.keys()])).forEach(country => {
const normalized = nameFixes[country] ?? country;
const don = donated.get(country) ?? 0;
const rec = received.get(country) ?? 0;
const net = don - rec;
balanceByCountry.set(normalized, net);
totals.push(Math.abs(net));
});
const maxAbsNet = d3.max(totals);
const colorScale = d3.scaleDiverging()
.domain([-maxAbsNet, 0, maxAbsNet])
.interpolator(d3.interpolateRdBu);
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.8)")
.style("color", "white")
.style("padding", "6px 10px")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("visibility", "hidden");
g.selectAll("path")
.data(geoJSON.features)
.join("path")
.attr("d", path)
.attr("fill", d => {
const net = balanceByCountry.get(d.properties.NAME);
return net !== undefined ? colorScale(net) : "#ccc";
})
.attr("stroke", "#999")
.attr("stroke-width", 0.5)
.on("mouseover", (event, d) => {
const name = d.properties.NAME;
const net = balanceByCountry.get(name);
const donatedAmt = donated.get(nameFixes[name] ?? name) ?? 0;
const receivedAmt = received.get(nameFixes[name] ?? name) ?? 0;
tooltip.style("visibility", "visible")
.html(`<strong>${name}</strong><br>
Donado: $${d3.format(",.0f")(donatedAmt)}<br>
Recibido: $${d3.format(",.0f")(receivedAmt)}<br>
Balance neto: $${d3.format("+,.0f")(net ?? 0)}`);
})
.on("mousemove", event => {
tooltip.style("top", (event.pageY + 10) + "px")
.style("left", (event.pageX + 10) + "px");
})
.on("mouseout", () => tooltip.style("visibility", "hidden"));
const defs = svg.append("defs");
const linearGradient = defs.append("linearGradient")
.attr("id", "choroplethGradient");
linearGradient.selectAll("stop")
.data([
{ offset: "0%", color: "#d73027" },
{ offset: "50%", color: "#f7f7f7" },
{ offset: "100%", color: "#1a9850" }
])
.join("stop")
.attr("offset", d => d.offset)
.attr("stop-color", d => d.color);
const legendGroup = svg.append("g")
.attr("transform", `translate(${visWidth - 220},${visHeight - 60})`);
legendGroup.append("rect")
.attr("width", 180)
.attr("height", 12)
.attr("fill", "url(#choroplethGradient)");
legendGroup.append("text")
.attr("x", 0)
.attr("y", -5)
.text("Balance neto (Donado - Recibido)")
.attr("font-size", 11)
.attr("font-weight", "bold");
legendGroup.append("text")
.attr("x", 0)
.attr("y", 28)
.text("Receptor neto")
.attr("font-size", 10);
legendGroup.append("text")
.attr("x", 155)
.attr("y", 28)
.text("Donante neto")
.attr("font-size", 10);
return svg.node();
}