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

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