Public
Edited
May 16
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
Insert cell
aiddata
Insert cell
Insert cell
totals = {
let totals = {}
aiddata.forEach(d => {
// Inicializa al pais como donante
if (!totals[d.donor]){
totals[d.donor] = { donated: 0, received: 0 };
}
totals[d.donor].donated += d.amount;
// Inicializa al pais como receptor
if (!totals[d.recipient]){
totals[d.recipient] = { donated: 0, received: 0 };
}
totals[d.recipient].received += d.amount;
})

return totals
}
Insert cell
Insert cell
// Si el neto es mayor a 0 es porque dona mas, si es menor a 0 es porque recibe mas
dataByCountry = {
const dataByCountry = Object.entries(totals).map(([country, d]) => ({
country,
donated: d.donated,
received: d.received,
neto: d.donated - d.received
}))
return dataByCountry
}
Insert cell
width = 960
Insert cell
height = 520
Insert cell
Insert cell
projection = d3.geoNaturalEarth1().fitSize([width, height], world)
Insert cell
path = d3.geoPath(projection);
Insert cell
max = d3.max(dataByCountry, d => Math.abs(d.neto));
Insert cell
Insert cell
color = d3.scaleDiverging(d3.interpolateRdBu).domain([-max, 0, max]);
Insert cell
byName = new Map(dataByCountry.map(d => [d.country, d]));
Insert cell
Insert cell
tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("pointer-events", "none")
.style("background", "white")
.style("padding", "6px 8px")
.style("border", "1px solid #999")
.style("border-radius", "4px")
.style("font", "12px")
.style("display", "none");
Insert cell
format = d3.format("$.2~s");
Insert cell
Insert cell
legend({
color,
title: "Saldo neto",
width: 200,
tickFormat: "$.2~s"
})
Insert cell
{
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("max-width", "100%");

const countries = world.features; // Obtenemos las caracteristicas de posicion de la variable world

svg.append("g")
.selectAll("path")
.data(countries)
.join("path")
.attr("d", path) // usamos la proyección para trazar cada país
.attr("fill", d => {
const datos = byName.get(d.properties.name);
if (datos) {
return color(datos.neto); // color por saldo neto
} else {
return "#ddd"; // gris si no hay datos
}
})
.attr("stroke", "#999")
.on("mousemove", (event, d) => { // Mientras el mouse este en movimiento sobre una region en especifico
const datos = byName.get(d.properties.name);
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY + 10) + "px")
.style("display", "block")
.html(`
<b>${d.properties.name}</b><br/>
Donado: ${datos ? format(datos.donated) : "n/a"}<br/>
Recibido: ${datos ? format(datos.received) : "n/a"}<br/>
Neto: ${datos ? format(datos.neto) : "n/a"}
`); // Este html le dara un estilo al marco del tooltip para mostrar la data de un pais correspondiente
})
.on("mouseout", () => {
tooltip.style("display", "none");
});

return svg.node()
}
Insert cell
Insert cell
Insert cell
newDatasetByCountry = {
const dataset = new Map();
for (const row of aiddata){
// Aseguramos que exista ese pais en el dataset
if (!dataset.has(row.donor)){
dataset.set(row.donor, {donado: 0, recibido: 0});
}
dataset.get(row.donor).donado += row.amount;
if (!dataset.has(row.recipient)){
dataset.set(row.recipient, {donado: 0, recibido: 0});
}
dataset.get(row.recipient).recibido += row.amount;
}

// Aniadimos un campo “clasificacion" para saber si dona más de lo que recibe
for (const [pais, obj] of dataset) {
const neto = obj.donado - obj.recibido;
obj.balance = neto;
obj.clasificacion = neto > 0 ? "Donante" :
neto < 0 ? "Receptor" :
"Neutral";
}

return dataset
}
Insert cell
max
Insert cell
color
Insert cell
width
Insert cell
height
Insert cell
vecinosIdx = topojson.neighbors(topoJSON.objects.countries.geometries)
Insert cell
idxByName = new Map(world.features.map((f, i) => [f.properties.name, i]));
Insert cell
Insert cell
tooltip2 = d3.select(document.body)
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("pointer-events", "none")
.style("padding", "6px 10px")
.style("background", "white")
.style("border", "1px solid #999")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("display", "none");
Insert cell
Insert cell
legend({
color,
title: "Saldo neto",
width: 200,
tickFormat: "$.2~s"
})
Insert cell
{
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("max-width", "100%");

const countries = world.features; // Obtenemos las caracteristicas de posicion de la variable world

svg.append("g")
.selectAll("path")
.data(countries)
.join("path")
.attr("d", path)
.attr("fill", d => {
const datos = byName.get(d.properties.name)
return datos ? color(datos.neto) : "#ddd"
})
.attr("stroke", "#999")
.attr("stroke-width", 0.3)
.on("mousemove", (event, d) => {
const nombre = d.properties.name
const datos = byName.get(nombre);
if(!datos) return;
const idx = idxByName.get(nombre);
console.log(vecinosIdx)
const vecinosNom = vecinosIdx[idx].map(j => world.features[j].properties.name);
const vecinosContraste = vecinosNom
.map(n => ({nombre: n, info: byName.get(n)}))
.filter(v => v.info && Math.sign(v.info.neto) != Math.sign(datos?.neto))

const fila = v => `
<tr>
<td>${v.nombre}</td>
<td style="text-align:right">${format(v.info.donated)}</td>
<td style="text-align:right">${format(v.info.received)}</td>
<td style="text-align:center">${v.info.neto>0?"Donante":"Receptor"}</td>
</tr>`; // Esta es una variable que guarda una etiqueta HTML para la tabla
tooltip2
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY + 10) + "px")
.style("display", "block")
.html(`
<b>${nombre}</b><br/>
Donado: ${datos ? format(datos.donated) : "n/a"}<br/>
Recibido: ${datos ? format(datos.received) : "n/a"}<br/>
Neto: ${datos ? format(datos.neto) : "n/a"}<br/>
<b>Vecinos con signo opuesto</b><br/>
<table style="border-collapse:collapse;font-size:11px;">
<thead>
<tr>
<th>País</th>
<th>Donado</th>
<th>Recibido</th>
<th>Balance</th>
</tr>
</thead>
<tbody>
${vecinosContraste.length
? vecinosContraste.map(fila).join("")
: "<tr><td colspan='4'>(ninguno)</td></tr>"}
</tbody>
</table>
`);

d3.selectAll("path")
.attr("stroke-width", p =>
vecinosContraste.some(v => v.nombre === p.properties.name) ? 1.2 : 0.3)
.attr("stroke", p =>
vecinosContraste.some(v => v.nombre === p.properties.name) ? "#000" : "#999");
}).on("mouseout", () => {
tooltip2.style("display", "none")
d3.selectAll("path")
.attr("stroke-width", 0.3)
.attr("stroke", "#999")
})

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
d3fetch = await import("https://cdn.jsdelivr.net/npm/d3-fetch@3/dist/d3-fetch.min.js")
Insert cell
topojson = await import("https://unpkg.com/topojson-client@3?module")
Insert cell
world = {
const topoJSON = await d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json");
return topojson.feature(topoJSON, topoJSON.objects.countries);
}
Insert cell
import {legend} from "@d3/color-legend"
Insert cell
topoJSON = await d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json")
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