Public
Edited
May 26
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,
recipient: row.recipient,
amount: +row.commitment_amount_usd_constant,
purpose: row.coalesced_purpose_name,
}));
data.columns = Object.keys(data[0]);
return data;
}
Insert cell
geoJSON = FileAttachment("countries-50m.json").json()
Insert cell
Insert cell
width = 950
Insert cell
height = 950
Insert cell
totalsByDonor = d3.rollups(
aiddata,
v => d3.sum(v, d => d.amount),
d => d.donor
)
Insert cell
top20Donadores = new Set(
totalsByDonor
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 20)
.map(d => d[0])
)
Insert cell
totalsByRecipient = d3.rollups(
aiddata,
v => d3.sum(v, d => d.amount),
d => d.recipient
)
Insert cell
top10Recipients = new Set(
totalsByRecipient
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 10)
.map(d => d[0])
)
Insert cell
donors = Array.from(top20Donadores)
Insert cell
recipients = Array.from(top10Recipients)
Insert cell
nodes = [...donors, ...recipients]
Insert cell
index = new Map(
nodes.map((d, i) => [d, i])
)
Insert cell
{
const interRadius = Math.min(width, height) * 0.5 - 100;
const dentroRadius = interRadius - 30;

const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("width", width)
.attr("height", height)
.style("font", "12px sans-serif");

const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("pointer-events", "none")
.style("background", "#fff")
.style("padding", "6px 10px")
.style("border", "1px solid #ccc")
.style("border-radius", "6px")
.style("box-shadow", "2px 2px 5px rgba(0,0,0,0.1)")
.style("font", "12px sans-serif")
.style("visibility", "hidden");
const matrix = Array.from({ length: nodes.length }, () => new Array(nodes.length).fill(0));
for (const d of aiddata) {
if (top20Donadores.has(d.donor) && top10Recipients.has(d.recipient)) {
const i = index.get(d.donor);
const j = index.get(d.recipient);
matrix[i][j] += d.amount;
}
}

const chord = d3.chordDirected()
.padAngle(0.05)
.sortSubgroups(d3.descending)(matrix);

const arc = d3.arc().innerRadius(dentroRadius).outerRadius(interRadius);
const ribbon = d3.ribbon().radius(dentroRadius);

const color = d3.scaleOrdinal()
.domain(nodes)
.range(nodes.map(n => donors.includes(n) ? "#1f77b4" : "#d62728"));

const groupPaths = svg.append("g")
.selectAll("path")
.data(chord.groups)
.join("path")
.attr("fill", d => color(nodes[d.index]))
.attr("stroke", "#000")
.attr("d", arc)
.style("cursor", "pointer")
.on("mouseover", (event, d) => {
const i = d.index;
highlight(i);

const name = nodes[i];
const total = d3.sum(matrix[i]) + d3.sum(matrix.map(row => row[i]));
tooltip
.html(`<b>${name}</b><br>Total: $${d3.format(",.0f")(total)}`)
.style("visibility", "visible");
})
.on("mousemove", event =>
tooltip
.style("top", (event.pageY + 10) + "px")
.style("left", (event.pageX + 10) + "px"))
.on("mouseout", () => {
unhighlight();
tooltip.style("visibility", "hidden");
});

const ribbonPaths = svg.append("g")
.selectAll("path")
.data(chord)
.join("path")
.attr("fill", d => color(nodes[d.source.index]))
.attr("d", ribbon)
.style("fill-opacity", 0.65)
.style("cursor", "default")
.on("mouseover", (event, d) => {
highlight(d.source.index);

tooltip
.html(`<b>${nodes[d.source.index]}</b> → <b>${nodes[d.target.index]}</b><br>$${d3.format(",.0f")(d.source.value)}`)
.style("visibility", "visible");
})
.on("mousemove", event =>
tooltip
.style("top", (event.pageY + 10) + "px")
.style("left", (event.pageX + 10) + "px"))
.on("mouseout", () => {
unhighlight();
tooltip.style("visibility", "hidden");
});

svg.append("g")
.selectAll("text")
.data(chord.groups)
.join("text")
.each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("dy", "0.35em")
.attr("transform", d => `
rotate(${(d.angle * 180 / Math.PI - 90)})
translate(${interRadius + 12})
${d.angle > Math.PI ? "rotate(180)" : ""}
`)
.attr("text-anchor", d => d.angle > Math.PI ? "end" : "start")
.text(d => nodes[d.index]);

function highlight(index) {
groupPaths
.style("opacity", d => d.index === index ? 1 : 0.1);

ribbonPaths
.style("opacity", d =>
d.source.index === index || d.target.index === index ? 0.7 : 0.05);
}

function unhighlight() {
groupPaths.style("opacity", 1);
ribbonPaths.style("opacity", 0.65);
}

return svg.node();
}

Insert cell
Insert cell
topPurposes = d3.rollups(
aiddata,
v => d3.sum(v, d => d.amount),
d => d.purpose
)
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 5)
.map(d => d[0])

Insert cell
filteredData = aiddata.filter(d =>
topPurposes.includes(d.purpose)
)
Insert cell
topDonors = d3.rollups(
filteredData,
v => d3.sum(v, d => d.amount),
d => d.donor
)
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 15)
.map(d => d[0]);
Insert cell
topRecipients = d3.rollups(
filteredData,
v => d3.sum(v, d => d.amount),
d => d.recipient
)
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 10)
.map(d => d[0]);
Insert cell
filteredAidData = aiddata.filter(d =>
topDonors.includes(d.donor) &&
topRecipients.includes(d.recipient) &&
topPurposes.includes(d.purpose)
);
Insert cell
groupedData = {
const map = d3.rollups(
filteredAidData,
v => d3.sum(v, d => d.amount),
d => d.donor,
d => d.recipient,
d => d.purpose
);

const result = [];
for (const [donor, recipients] of map) {
for (const [recipient, purposes] of recipients) {
for (const [purpose, total] of purposes) {
result.push({ donor, recipient, purpose, amount: total });
}
}
}
return result;
}
Insert cell
{
const margin = { top: 60, right: 250, bottom: 120, left: 200 };
const width = 1000;
const height = 600;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

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

const donors = topDonors;
const recipients = topRecipients;
const purposes = topPurposes;

const donorScale = d3.scaleBand().domain(donors).range([0, innerHeight]).padding(0.15);
const recipientScale = d3.scaleBand().domain(recipients).range([0, innerWidth]).padding(0.15);
const color = d3.scaleOrdinal().domain(purposes).range(d3.schemeCategory10);

const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

g.append("g")
.selectAll("rect")
.data(groupedData)
.join("rect")
.attr("x", d => recipientScale(d.recipient))
.attr("y", d => donorScale(d.donor))
.attr("width", recipientScale.bandwidth())
.attr("height", donorScale.bandwidth())
.attr("fill", d => color(d.purpose))
.append("title")
.text(d => `Donor: ${d.donor}\nRecipient: ${d.recipient}\nPurpose: ${d.purpose}\nAmount: $${d3.format(",.0f")(d.amount)}`);

svg.append("text")
.attr("x", 15)
.attr("y", margin.top + innerHeight / 2)
.attr("text-anchor", "middle")
.attr("transform", `rotate(-90, 15, ${margin.top + innerHeight / 2})`)
.attr("font-size", "13px")
.attr("font-weight", "bold")
.text("País Donante");

svg.append("text")
.attr("x", margin.left + innerWidth / 2)
.attr("y", height - 35)
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.attr("font-weight", "bold")
.text("País Receptor");

svg.append("text")
.attr("x", margin.left + innerWidth / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "16px")
.attr("font-weight", "bold")
.text("Distribución de propósitos por Donante-Receptor (Top 5 propósitos)");

const legend = svg.append("g")
.attr("transform", `translate(${width - margin.right + 30}, ${margin.top})`);

purposes.forEach((p, i) => {
legend.append("rect")
.attr("x", 0)
.attr("y", i * 22)
.attr("width", 14)
.attr("height", 14)
.attr("fill", color(p));

legend.append("text")
.attr("x", 20)
.attr("y", i * 22 + 11)
.text(p)
.attr("font-size", "12px")
.attr("alignment-baseline", "middle");
});

return svg.node();
}

Insert cell
Insert cell
Insert cell
{
const year = selectedYear;
const dataYear = aiddata
.filter(d => d.yearInt === year)
.filter(d => topDonors.includes(d.donor) && topRecipients.includes(d.recipient));

// Sumar montos por par
const matrixData = d3.rollups(
dataYear,
v => d3.sum(v, d => d.amount),
d => d.donor,
d => d.recipient
).flatMap(([donor, recipientData]) =>
recipientData.map(([recipient, amount]) => ({
donor,
recipient,
amount
}))
);

const margin = { top: 60, right: 20, bottom: 120, left: 200 };
const width = 1000;
const height = 600;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

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

const donorScale = d3.scaleBand().domain(topDonors).range([0, innerHeight]).padding(0.15);
const recipientScale = d3.scaleBand().domain(topRecipients).range([0, innerWidth]).padding(0.15);

const color = d3.scaleSequential()
.domain([0, d3.max(matrixData, d => d.amount)])
.interpolator(d3.interpolateYlGnBu);

const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

// Cuadrados
g.selectAll("rect")
.data(matrixData)
.join("rect")
.attr("x", d => recipientScale(d.recipient))
.attr("y", d => donorScale(d.donor))
.attr("width", recipientScale.bandwidth())
.attr("height", donorScale.bandwidth())
.attr("fill", d => color(d.amount))
.append("title")
.text(d => `Donante: ${d.donor}\nReceptor: ${d.recipient}\nMonto: $${d3.format(",.0f")(d.amount)}`);

// Ejes
const xAxis = d3.axisBottom(recipientScale);
g.append("g")
.attr("transform", `translate(0, ${innerHeight})`)
.call(xAxis)
.selectAll("text")
.attr("text-anchor", "end")
.attr("transform", "rotate(-45)")
.attr("dx", "-0.6em")
.attr("dy", "0.1em")
.attr("font-size", "11px");

const yAxis = d3.axisLeft(donorScale);
g.append("g")
.call(yAxis)
.selectAll("text")
.attr("font-size", "11px");

// Títulos
svg.append("text")
.attr("x", margin.left + innerWidth / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "16px")
.attr("font-weight", "bold")
.text(`Relaciones de Donación - Año ${year}`);

svg.append("text")
.attr("x", margin.left + innerWidth / 2)
.attr("y", height - 35)
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.attr("font-weight", "bold")
.text("País Receptor");

svg.append("text")
.attr("x", 15)
.attr("y", margin.top + innerHeight / 2)
.attr("text-anchor", "middle")
.attr("transform", `rotate(-90, 15, ${margin.top + innerHeight / 2})`)
.attr("font-size", "13px")
.attr("font-weight", "bold")
.text("País Donante");

return svg.node();
}

Insert cell
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