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,
recipient: row.recipient,
//amount: +row.commitment_amount_usd_constant,
amount: +row.commitment_amount_usd_constant.replace(/,/g, ''),

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
Insert cell
visWidth = width
Insert cell
visHeight = width * 0.625
Insert cell
// tu código aquí
{
const svg = d3.create('svg')
.attr('width', visWidth)
.attr('height', visHeight);

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

// Proyección y path
const projection = d3.geoNaturalEarth1()
.scale(visWidth / 6.5)
.translate([visWidth / 2, visHeight / 2]);

const path = d3.geoPath().projection(projection);

g.append('g')
.selectAll('path')
.data(geoJSON.features)
.join('path')
.attr('d', path)
.attr('fill', '#eee')
.attr('stroke', '#999');


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 countryTotals = Array.from(new Set([...donated.keys(), ...received.keys()]), country => {
const normalized = nameFixes[country] ?? country;
const donatedAmount = donated.get(country) ?? 0;
const receivedAmount = received.get(country) ?? 0;
return {
country: normalized,
donated: donatedAmount,
received: receivedAmount,
total: donatedAmount + receivedAmount
};
}).filter(d => d.total > 0);

// Escala
const radius = d3.scaleSqrt()
.domain([0, d3.max(countryTotals, d => d.total)])
.range([0, 25]);

const pie = d3.pie()
.value(d => d.value);

const color = d3.scaleOrdinal()
.domain(["donated", "received"])
.range(["red", "blue"]);

const countryGroups = g.append('g')
.selectAll('g')
.data(countryTotals.filter(d =>
geoJSON.features.some(f => f.properties.NAME === d.country)
))
.join('g')
.attr('transform', d => {
const match = geoJSON.features.find(f => f.properties.NAME === d.country);
const [x, y] = path.centroid(match);
return `translate(${x}, ${y})`;
});

// Tooltip
const tooltip = d3.select('body').append('div')
.style('position', 'absolute')
.style('pointer-events', 'none')
.style('background', 'rgba(0, 0, 0, 0.8)')
.style('color', 'white')
.style('padding', '6px 10px')
.style('border-radius', '4px')
.style('font-size', '12px')
.style('visibility', 'hidden');

countryGroups.each(function(d) {
const thisGroup = d3.select(this);
const pieData = pie([
{ category: "donated", value: d.donated },
{ category: "received", value: d.received }
]);
const r = radius(d.total);
const arc = d3.arc()
.innerRadius(0)
.outerRadius(r);

thisGroup.selectAll('path')
.data(pieData)
.join('path')
.attr('d', arc)
.attr('fill', d => color(d.data.category))
.on('mouseover', function(event) {
tooltip
.style('visibility', 'visible')
.html(`<strong>${d.country}</strong><br>
Donó: $${d3.format(",.0f")(d.donated)}<br>
Recibió: $${d3.format(",.0f")(d.received)}`);
})
.on('mousemove', function(event) {
tooltip
.style('top', (event.pageY + 10) + 'px')
.style('left', (event.pageX + 10) + 'px');
})
.on('mouseout', () => tooltip.style('visibility', 'hidden'));
});

// Leyenda
const legend = svg.append('g')
.attr('transform', `translate(${visWidth - 140},${visHeight - 80})`);

legend.append('text')
.text('Leyenda:')
.attr('font-weight', 'bold')
.attr('y', -10);

legend.append('circle')
.attr('cx', 0)
.attr('cy', 10)
.attr('r', 6)
.attr('fill', 'red');

legend.append('text')
.attr('x', 12)
.attr('y', 14)
.text('Donado');

legend.append('circle')
.attr('cx', 0)
.attr('cy', 30)
.attr('r', 6)
.attr('fill', 'blue');

legend.append('text')
.attr('x', 12)
.attr('y', 34)
.text('Recibido');

return svg.node();
}

Insert cell
Insert cell

{
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();
}

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