Public
Edited
Nov 11, 2022
Insert cell
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
Inputs.table(aiddata)
Insert cell
margin = ({top: 30, right: 30, bottom: 20, left: 60})
Insert cell
visWidth = width - margin.left - margin.right
Insert cell
visHeight = 500 - margin.top - margin.bottom
Insert cell
aid_by_year = d3.rollup(aiddata.sort((a, b) => d3.ascending(a.yearInt, b.yearInt)),
group => d3.rollup(group,
sub_group => sub_group,
sub_d => sub_d.donor),
d => d.yearInt)
Insert cell
amnt_donated_by_country_and_year = d3.rollup(aiddata,
group => d3.rollup(group,
sub_group => d3.sum(sub_group, entry => entry.amount),
d => d.yearInt),
d => d.donor)
Insert cell
amnt_received_by_country_and_year = d3.rollup(aiddata,
group => d3.rollup(group,
sub_group => d3.sum(sub_group, entry => entry.amount),
d => d.yearInt),
d => d.recipient)
Insert cell
all_countries = new Set(Array.from(amnt_donated_by_country_and_year.keys())
.concat(Array.from(amnt_received_by_country_and_year.keys())))
Insert cell
years = Array.from(d3.group(aiddata, d => d.yearInt).keys())
Insert cell
function donation_amnt(country, year) {
if (amnt_donated_by_country_and_year.get(country)) {
return amnt_donated_by_country_and_year.get(country).get(year) ?? 0
}
return 0
}
Insert cell
function receipt_amnt(country, year) {
if (amnt_received_by_country_and_year.get(country)) {
return amnt_received_by_country_and_year.get(country).get(year) ?? 0
}
return 0
}
Insert cell
deltas_by_country = new Map(d3.map(all_countries,
country => [country,
d3.map(years,
year => Object.fromEntries([["year", year],
["delta", donation_amnt(country, year) - receipt_amnt(country, year)]])
)
]
)
)
Insert cell
rows = 12
Insert cell
cols = 4
Insert cell
grid = d3.cross(d3.range(rows), d3.range(cols), (row, col) => ({ row, col }))
Insert cell
grid_data = d3.zip(Array.from(deltas_by_country), grid).map(
([[country, deltas], { row, col }]) => ({
country,
deltas,
row,
col,
})
)
Insert cell
row = d3.scaleBand()
.domain(d3.range(rows))
.range([0, visHeight * 3])
.paddingInner(0.5)
Insert cell
col = d3.scaleBand()
.domain(d3.range(cols))
.range([0, visWidth])
.paddingInner(0.2)
Insert cell
x = d3.scaleLinear()
.domain(d3.extent(years))
.range([0, col.bandwidth()])
Insert cell
xAxis = d3.axisBottom(x)
.tickFormat(d => `${d}`)
Insert cell
country_to_scale_and_area = Object.fromEntries(
grid_data.map(d => {
var max_delta = d3.max(d.deltas, d => d.delta)
var min_delta = d3.min(d.deltas, d => d.delta)

if (max_delta > 0 && min_delta > 0) {
min_delta = 0
}
if (min_delta < 0 && max_delta < 0) {
max_delta = 0
}
const y = d3.scaleLinear()
.domain([min_delta, max_delta])
.range([row.bandwidth(), 12])
const area = d3.area()
.x(d => x(d.year))
.y1(d => y(d.delta))
.y0(d => y(0));

// const area = d3.line()
// .x(d => x(d.year))
// .y(d => y(d.delta))

return [d.country, {y, area}]
})
)
Insert cell
function custom_format(num) {
if (num == 0) {
return `$0`
}
else if (Math.abs(num) < 1000000) {
return `$${num / 1000}K`
}
else if (Math.abs(num) < 1000000000) {
return `$${num / 1000000}M`
}
else {
return `$${num / 1000000000}B`
}
}
Insert cell
{
const svg = d3.create('svg')
.attr('width', visWidth + margin.left + margin.right)
.attr('height', (visHeight * 3) + margin.top + margin.bottom);
const g = svg.append("g")
.attr('transform', `translate(${margin.left}, ${margin.top})`);

const cells = g.append('g')
.selectAll('g')
.data(grid_data)
.join('g')
.attr('class', 'cell')
.attr('transform', d => `translate(${col(d.col)}, ${row(d.row)})`);
cells.append('path')
.attr('d', d => country_to_scale_and_area[d.country].area(d.deltas))
.attr('fill', 'steelblue')

cells.append('text')
.attr('text-anchor', 'start')
.attr('fill', 'black')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('font-weight', 'bold')
.attr('dominant-baseline', 'middle')
.attr('x', 5)
.text(d => d.country)

cells.append("line")
.attr("x1", 0)
.attr("y1", d => country_to_scale_and_area[d.country].y(0))
.attr("x2", col.bandwidth())
.attr("y2", d => country_to_scale_and_area[d.country].y(0))
.style("stroke", "black")
// add the y axis for each cell
cells.each(function(d) {
const group = d3.select(this);

// get the y-scale for this industry
const yAxis = d3.axisLeft(country_to_scale_and_area[d.country].y)
.ticks(3)
.tickFormat(custom_format)
group.call(yAxis)
.call(g => g.select('.domain').remove())
});

const xAxes = cells.append('g')
.attr('transform', d => `translate(0,${row.bandwidth()})`)
.call(xAxis)
.call(g => g.select('.domain').remove())
.call(g => g.selectAll('line').attr('stroke', '#c0c0c0'))
return svg.node();
}
Insert cell
geoJSON = FileAttachment("countries-50m.json").json()
Insert cell
googleSheetCsvUrl = 'https://docs.google.com/spreadsheets/d/1YiuHdfZv_JZ-igOemKJMRaU8dkucfmHxOP6Od3FraW8/gviz/tq?tqx=out:csv'
Insert cell
import {Legend, Swatches} from "@d3/color-legend"
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more