Public
Edited
May 14
Insert cell
Insert cell
Insert cell
Inputs.table(aiddata)
Insert cell
aiddata = {
const data = await d3.csv(googleSheetCsvUrl, row => ({
yearDate: d3.timeParse("%Y")(row.year),
yearInt: +row.year,
donor: row.donor?.trim(),
recipient: row.recipient?.trim(),
amount: +row.commitment_amount_usd_constant,
purpose: row.coalesced_purpose_name?.trim()
}));

const filtered = data.filter(d =>
d.yearDate && !isNaN(d.amount) && d.purpose && d.purpose !== "UNSPECIFIED"
);

filtered.columns = Object.keys(filtered[0]);
return filtered;
}
Insert cell
geoJSON = FileAttachment("countries-50m.json").json()
Insert cell
Insert cell

donadoPorPais = d3.rollup(
aiddata,
v => d3.sum(v, d => d.amount),
d => corregirNombre(d.donor)
)


Insert cell

recibidoPorPais = d3.rollup(
aiddata,
v => d3.sum(v, d => d.amount),
d => corregirNombre(d.recipient)
)

Insert cell

balancePorPais = new Map()

Insert cell
allCountries = new Set([...donadoPorPais.keys(), ...recibidoPorPais.keys()])

Insert cell
allCountries.forEach(country => {
const donado = donadoPorPais.get(country) || 0;
const recibido = recibidoPorPais.get(country) || 0;
balancePorPais.set(country, recibido - donado);
})

Insert cell
balancePorPais
Insert cell
geoDataConBalance = geoJSON.features.map(feature => {
const name = feature.properties.ADMIN;

const donado = donadoPorPais.get(name) || 0;
const recibido = recibidoPorPais.get(name) || 0;
const balance = recibido - donado;

feature.properties.donado = donado;
feature.properties.recibido = recibido;
feature.properties.balance = balance;

feature.properties.tipo = donado > recibido ? 'donador' :
recibido > donado ? 'receptor' : 'no data';

return feature;
});

Insert cell
maxDonado = d3.max(geoDataConBalance, d => d.properties.donado);

Insert cell
maxRecibido = d3.max(geoDataConBalance, d => d.properties.recibido);

Insert cell

colorDonador = d3.scaleSequentialLog(d3.interpolateReds)
.domain([1e5, maxDonado]);

Insert cell
colorReceptor = d3.scaleSequentialLog(d3.interpolateBlues)
.domain([1e5, maxRecibido]);

Insert cell

maxAbsBalance = d3.max(geoDataConBalance, d => Math.abs(d.properties.balance));

Insert cell
color = d3.scaleDiverging()
.domain([-maxAbsBalance, 0, maxAbsBalance])
.interpolator(d3.interpolateRdBu);
Insert cell

projection = d3.geoNaturalEarth1()
.fitSize([800, 500], geoJSON)
Insert cell

path = d3.geoPath().projection(projection)
Insert cell
geoJSON.features.some(d => d.properties.ADMIN === "United States")

Insert cell
geoJSON.features.map(d => d.properties.ADMIN).filter(name => name.toLowerCase().includes("united"))

Insert cell
function nombreMasCercano(nombre, candidatos) {
return d3.least(candidatos, c => d3.pairs([nombre, c])[0][0].localeCompare(c));
}
Insert cell
Insert cell
paisesAid = new Set(aiddata.flatMap(d => [d.donor, d.recipient]));

Insert cell
noEncontrados = Array.from(paisesAid).filter(p => !nombresGeo.has(p));

Insert cell
equivalencias = new Map([
["Korea", "South Korea"],
["Slovak Republic", "Slovakia"],
["United States", "United States of America"],
["Czech Republic", "Czechia"]
]);

Insert cell
function corregirNombre(nombre) {
return equivalencias.get(nombre) || nombre;
}

Insert cell
{
const width = 800;
const height = 500;

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const g = svg.append("g");

const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "rgba(0,0,0,0.7)")
.style("color", "#fff")
.style("padding", "6px 12px")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("visibility", "hidden");

g.selectAll("path")
.data(geoDataConBalance)
.join("path")
.attr("d", path)
.attr("fill", d => {
const props = d.properties;
if (props.tipo === 'donador') return colorDonador(props.donado);
if (props.tipo === 'receptor') return colorReceptor(props.recibido);
return '#ccc'; // para neutros o sin datos
})
.attr("stroke", "#999")
.on("mouseover", (event, d) => {
tooltip
.style("visibility", "visible")
.html(`
<strong>${d.properties.NAME}</strong><br>
Donado: ${d3.format(",")(d.properties.donado)} USD<br>
Recibido: ${d3.format(",")(d.properties.recibido)} USD<br>
Tipo: ${d.properties.tipo}
`);
})

.on("mousemove", event => {
tooltip.style("top", (event.pageY - 10) + "px")
.style("left", (event.pageX + 10) + "px");
})
.on("mouseout", () => tooltip.style("visibility", "hidden"));

svg.call(d3.zoom()
.scaleExtent([1, 8])
.on("zoom", event => {
g.attr("transform", event.transform);
}));

const legendX = 20;
const legendY = height - 40;
const legendWidth = 300;
const legendHeight = 12;
const steps = 100;

const colorLegendScale = d3.scaleDiverging(d3.interpolateRdBu)
.domain([1, 0.5, 0]);

svg.selectAll("rect.legend")
.data(d3.range(steps))
.enter()
.append("rect")
.attr("x", (d, i) => legendX + i * (legendWidth / steps))
.attr("y", legendY)
.attr("width", legendWidth / steps)
.attr("height", legendHeight)
.attr("fill", (d, i) => colorLegendScale(i / (steps - 1)));

svg.append("text")
.attr("x", legendX)
.attr("y", legendY - 5)
.text("Receptor -- Donador")
.attr("font-size", "11px");

svg.append("text")
.attr("x", legendX)
.attr("y", legendY + legendHeight + 12)
.attr("font-size", "10px")
.text("Receptor");

svg.append("text")
.attr("x", legendX + legendWidth / 2)
.attr("y", legendY + legendHeight + 12)
.attr("text-anchor", "middle")
.attr("font-size", "10px")

svg.append("text")
.attr("x", legendX + legendWidth)
.attr("y", legendY + legendHeight + 12)
.attr("text-anchor", "end")
.attr("font-size", "10px")
.text("Donador");

return svg.node();
}

Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require('d3@7')
Insert cell
googleSheetCsvUrl = 'https://docs.google.com/spreadsheets/d/1YiuHdfZv_JZ-igOemKJMRaU8dkucfmHxOP6Od3FraW8/gviz/tq?tqx=out:csv'
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more