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
donorVsRecipient = {
const recipients = d3.rollup(
aiddata,
v => d3.sum(v, d => d.amount),
d => d.recipient
);

const donors = d3.rollup(
aiddata,
v => d3.sum(v, d => d.amount),
d => d.donor
);

const paises = new Set([...recipients.keys(), ...donors.keys()]);

const data = Array.from(paises, pais => ({
pais,
recibido: recipients.get(pais) || 0,
donado: donors.get(pais) || 0,
diferencia: Math.abs((recipients.get(pais) || 0) - (donors.get(pais) || 0))
}))
.sort((a, b) => b.diferencia - a.diferencia)
.slice(0, 15);

return data;
}
Insert cell

viewof divergentBarChart = {
donorVsRecipient.sort((a, b) => {
const maxA = Math.max(a.donado, a.recibido);
const maxB = Math.max(b.donado, b.recibido);
return maxB - maxA;
});

const width = 800;
const barHeight = 35;
const margin = { top: 30, right: 30, bottom: 30, left: 130 };
const height = donorVsRecipient.length * (barHeight + 10) + margin.top + margin.bottom;
const innerWidth = width - margin.left - margin.right;

const formatMoney = value => {
if (value >= 1000000000) {
return `$${(value / 1000000000).toFixed(1)}B`;
} else if (value >= 1000000) {
return `$${(value / 1000000).toFixed(1)}M`;
} else if (value >= 1000) {
return `$${(value / 1000).toFixed(0)}K`;
} else {
return `$${value}`;
}
};

const maxValue = d3.max(donorVsRecipient, d => Math.max(d.donado, d.recibido));
const xScale = d3.scaleLinear()
.domain([0, maxValue * 1.1])
.range([0, innerWidth / 2 - 20]);

const yScale = d3.scaleBand()
.domain(donorVsRecipient.map(d => d.pais))
.range([margin.top, height - margin.bottom])
.padding(0.2);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; font-family: Arial, sans-serif;");

svg.append("g")
.selectAll("line")
.data(donorVsRecipient)
.join("line")
.attr("x1", margin.left)
.attr("x2", width - margin.right)
.attr("y1", d => yScale(d.pais) + yScale.bandwidth() / 2)
.attr("y2", d => yScale(d.pais) + yScale.bandwidth() / 2)
.attr("stroke", "#f0f0f0")
.attr("stroke-dasharray", "3,3");

const midPoint = margin.left + innerWidth / 2;

svg.append("g")
.selectAll("rect.donado")
.data(donorVsRecipient)
.join("rect")
.attr("class", "donado")
.attr("x", d => midPoint - xScale(d.donado))
.attr("y", d => yScale(d.pais))
.attr("width", d => xScale(d.donado))
.attr("height", yScale.bandwidth())
.attr("fill", "#4CAF50");

svg.append("g")
.selectAll("rect.recibido")
.data(donorVsRecipient)
.join("rect")
.attr("class", "recibido")
.attr("x", midPoint)
.attr("y", d => yScale(d.pais))
.attr("width", d => xScale(d.recibido))
.attr("height", yScale.bandwidth())
.attr("fill", "#F44336");

svg.append("g")
.attr("text-anchor", "end")
.selectAll("text")
.data(donorVsRecipient)
.join("text")
.attr("x", margin.left - 10)
.attr("y", d => yScale(d.pais) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "#333")
.style("font-size", "14px")
.text(d => d.pais);

svg.append("text")
.attr("x", midPoint - innerWidth / 4)
.attr("y", margin.top / 2)
.attr("text-anchor", "middle")
.attr("fill", "#4CAF50")
.attr("font-weight", "bold")
.style("font-size", "14px")
.text("Donado");

svg.append("text")
.attr("x", midPoint + innerWidth / 4)
.attr("y", margin.top / 2)
.attr("text-anchor", "middle")
.attr("fill", "#F44336")
.attr("font-weight", "bold")
.style("font-size", "14px")
.text("Recibido");

svg.append("g")
.selectAll("text.donado")
.data(donorVsRecipient)
.join("text")
.attr("class", "donado")
.attr("text-anchor", "middle")
.attr("x", d => {
const barWidth = xScale(d.donado);
return barWidth > 60 ? midPoint - barWidth / 2 : null;
})
.attr("y", d => yScale(d.pais) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "white")
.attr("font-weight", "bold")
.style("font-size", "12px")
.text(d => d.donado > 0 ? formatMoney(d.donado) : "");

svg.append("g")
.selectAll("text.recibido")
.data(donorVsRecipient)
.join("text")
.attr("class", "recibido")
.attr("text-anchor", "middle")
.attr("x", d => {
const barWidth = xScale(d.recibido);
return barWidth > 60 ? midPoint + barWidth / 2 : null;
})
.attr("y", d => yScale(d.pais) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "white")
.attr("font-weight", "bold")
.style("font-size", "12px")
.text(d => d.recibido > 0 ? formatMoney(d.recibido) : "");

svg.append("g")
.selectAll("text.donado-outside")
.data(donorVsRecipient.filter(d => xScale(d.donado) <= 60 && d.donado > 0))
.join("text")
.attr("class", "donado-outside")
.attr("text-anchor", "end")
.attr("x", d => midPoint - xScale(d.donado) - 5)
.attr("y", d => yScale(d.pais) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "#4CAF50")
.attr("font-weight", "bold")
.style("font-size", "12px")
.text(d => formatMoney(d.donado));

svg.append("g")
.selectAll("text.recibido-outside")
.data(donorVsRecipient.filter(d => xScale(d.recibido) <= 60 && d.recibido > 0))
.join("text")
.attr("class", "recibido-outside")
.attr("text-anchor", "start")
.attr("x", d => midPoint + xScale(d.recibido) + 5)
.attr("y", d => yScale(d.pais) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "#F44336")
.attr("font-weight", "bold")
.style("font-size", "12px")
.text(d => formatMoney(d.recibido));

return svg.node();
}
Insert cell
Insert cell
geoJSON2 = d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson")
Insert cell
data = {
const donors = d3.rollup(
aiddata,
v => d3.sum(v, d => d.amount),
d => d.donor
);

const recipients = d3.rollup(
aiddata,
v => d3.sum(v, d => d.amount),
d => d.recipient
);

const allCountries = new Set([...donors.keys(), ...recipients.keys()]);

return Array.from(allCountries).map(country => ({
country,
donated: donors.get(country) || 0,
received: recipients.get(country) || 0
}));
}

Insert cell
colorScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, d3.max(data, d => d.donated)])
Insert cell
radiusScale = d3.scaleSqrt()
.domain([0, d3.max(data, d => d.received)])
.range([0, 20]);
Insert cell

function starPath(innerRadius, outerRadius, points) {
let path = "";
for (let i = 0; i < points * 2; i++) {
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const angle = (Math.PI * i) / points;
const x = radius * Math.sin(angle);
const y = -radius * Math.cos(angle);
path += (i === 0 ? "M" : "L") + x + "," + y;
}
return path + "Z";
}
Insert cell
viewof svg = {
const width = 960;
const height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const projection = d3.geoNaturalEarth1().fitSize([width, height], geoJSON2);
const path = d3.geoPath().projection(projection);
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "20px")
.style("font-weight", "bold");
svg.append("text")
.attr("x", width / 2)
.attr("y", 50)
.attr("text-anchor", "middle")
.style("font-size", "13px")
.style("fill", "#555");
svg.append("g")
.selectAll("path")
.data(geoJSON2.features)
.join("path")
.attr("d", path)
.attr("fill", d => {
const countryData = data.find(c => c.country === d.properties.name);
return countryData ? colorScale(countryData.donated) : "#eee";
})
.attr("stroke", "#999");
svg.append("g")
.selectAll("path.star")
.data(data)
.join("path")
.attr("class", "star")
.attr("transform", d => {
const country = geoJSON2.features.find(f => f.properties.name === d.country);
if (!country) return "translate(-100,-100)";
const [x, y] = projection(d3.geoCentroid(country));
return `translate(${x},${y})`;
})
.attr("d", d => {
const size = radiusScale(d.received);
return starPath(size/2, size, 10);
})
.attr("fill", "orange")
.attr("stroke", "#333")
.attr("stroke-width", 0.5)
.attr("opacity", 0.8)
.append("title")
.text(d => `${d.country}\nDonado: $${d.donated.toLocaleString()}\nRecibido: $${d.received.toLocaleString()}`);
return svg.node();
}
Insert cell
Insert cell
purposes = {
const counts = d3.rollup(aiddata, v => v.length, d => d.purpose);
return Array.from(counts.entries())
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 5)
.map(d => d[0]);
}

Insert cell
top5 = Array.from(purposes.entries())
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 5)
.map(d => d[0]);
Insert cell
byCountry = {
const filtered = aiddata.filter(d => purposes.includes(d.purpose));
const nested = d3.rollups(
filtered,
v => d3.sum(v, d => d.amount),
d => d.recipient,
d => d.purpose
);

return Object.fromEntries(nested.map(([country, entries]) => {
const total = d3.sum(entries, d => d[1]);
const values = purposes.map(p => {
const match = entries.find(d => d[0] === p);
return {
purpose: p,
value: match ? match[1] : 0,
ratio: match ? match[1] / total : 0
};
});
return [country, values];
}));
}
Insert cell
colorScale1 = d3.scaleOrdinal()
.domain(purposes)
.range(d3.schemeCategory10)
Insert cell
viewof svg1 = {
const width = 960;
const height = 600;
const margin = { top: 40, right: 40, bottom: 40, left: 40 };
const svg = d3.create("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("width", "100%")
.attr("height", "100%")
.style("font-family", "Arial, sans-serif")
.style("background-color", "#f9f9f9");
const projection = d3.geoOrthographic()
.scale(300)
.translate([width / 2, height / 2])
.center([0, 0])
.rotate([-30, -20, 0]);
const path = d3.geoPath().projection(projection);
const globeGroup = svg.append("g");
const tooltip = svg.append("g")
.attr("class", "tooltip")
.style("display", "none");
tooltip.append("rect")
.attr("width", 250)
.attr("height", 150)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill", "white")
.attr("stroke", "#ccc")
.attr("stroke-width", 1)
.attr("opacity", 0.9);
const tooltipText = tooltip.append("text")
.attr("x", 10)
.attr("y", 20)
.style("font-size", "14px");
const tooltipChart = tooltip.append("g")
.attr("transform", "translate(10, 30)");
const colorScale = d3.scaleOrdinal(d3.schemePaired);
globeGroup.append("circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", 300)
.attr("fill", "#cceeff");
const graticule = d3.geoGraticule();
globeGroup.append("path")
.datum(graticule)
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "#ddd")
.attr("stroke-width", 0.5);
const countries = globeGroup.append("g")
.selectAll("path")
.data(geoJSON2.features)
.join("path")
.attr("d", path)
.attr("fill", d => {
const countryName = d.properties.name;
return byCountry[countryName] ? "#5ab4ac" : "#e8e8e8";
})
.attr("stroke", "#666")
.attr("stroke-width", 0.5)
.attr("stroke-opacity", 0.8)
.style("cursor", d => byCountry[d.properties.name] ? "pointer" : "default")
.on("mouseover", function(event, d) {
const countryName = d.properties.name;
const data = byCountry[countryName];
d3.select(this)
.attr("fill", "#ff9e6d")
.attr("stroke-width", 1.5);
tooltip.style("display", "block");
const [x, y] = d3.pointer(event, svg.node());
tooltip.attr("transform", `translate(${x + 20}, ${y - 100})`);
tooltipText.text(countryName);
tooltipChart.selectAll("*").remove();
const barWidth = 230;
const barHeight = 100;
const barMargin = { top: 10, right: 10, bottom: 10, left: 80 };
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, barWidth - barMargin.left - barMargin.right]);
const yScale = d3.scaleBand()
.domain(data.map(d => d.purpose))
.range([0, barHeight - barMargin.top - barMargin.bottom])
.padding(0.2);
tooltipChart.append("g")
.attr("transform", `translate(${barMargin.left}, ${barHeight - barMargin.bottom})`)
.call(d3.axisBottom(xScale).ticks(3).tickSize(0))
.style("font-size", "8px");
tooltipChart.append("g")
.attr("transform", `translate(${barMargin.left}, ${barMargin.top})`)
.call(d3.axisLeft(yScale).tickSize(0))
.style("font-size", "8px");
tooltipChart.selectAll(".bar")
.data(data)
.join("rect")
.attr("class", "bar")
.attr("x", barMargin.left)
.attr("y", d => yScale(d.purpose) + barMargin.top)
.attr("height", yScale.bandwidth())
.attr("fill", (d, i) => colorScale(i))
.attr("width", 0)
.transition()
.duration(300)
.attr("width", d => xScale(d.value));
tooltipChart.selectAll(".value")
.data(data)
.join("text")
.attr("class", "value")
.attr("x", d => barMargin.left + xScale(d.value) + 5)
.attr("y", d => yScale(d.purpose) + barMargin.top + yScale.bandwidth() / 2 + 4)
.attr("font-size", "8px")
.attr("opacity", 0)
.text(d => d.value.toFixed(1))
.transition()
.duration(500)
.attr("opacity", 1);
})
.on("mouseout", function(d) {
d3.select(this)
.attr("fill", d => {
const countryName = d.properties.name;
return byCountry[countryName] ? "#5ab4ac" : "#e8e8e8";
})
.attr("stroke-width", 0.5);
tooltip.style("display", "none");
});
let drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged);
svg.call(drag);
let rotateX = -30;
let rotateY = -20;
let rotateZ = 0;
function dragstarted() {
d3.select(this).style("cursor", "grabbing");
}
function dragged(event) {
rotateX += event.dx * 0.5;
rotateY -= event.dy * 0.5;
projection.rotate([rotateX, rotateY, rotateZ]);
countries.attr("d", path);
globeGroup.select("path").attr("d", path);
}
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