Published
Edited
Dec 7, 2020
1 star
Insert cell
Insert cell
aiddata = (await d3.csv(googleSheetCsvUrl, row => ({
yearDate: d3.timeParse('%Y')(row.year),
yearInt: +row.year,
aiddata_id: row.aiddata_id,
aiddata_2_id: row.aiddata_2_id,
donor: row.donor,
recipient: row.recipient,
commitment_amount: +row.commitment_amount_usd_constant,
coalesced_purpose_code: row.coalesced_purpose_code,
coalesced_purpose_name: row.coalesced_purpose_name,
})))
Insert cell
Insert cell
geoJSON = FileAttachment("countries-50m.json").json()
Insert cell
Insert cell
donors = d3.group(aiddata, d => d.donor)
Insert cell
countryList = {
let retVar = []
const donors = d3.group(aiddata, d => d.donor)
const recipients = d3.group(aiddata, d => d.recipient)
for (const country of donors) {
if (! retVar.find(d => d == country[0])) {
retVar.push(country[0])
}
}
for (const country of recipients) {
if (! retVar.find(d => d == country[0])) {
retVar.push(country[0])
}
}
return retVar.sort((a, b) => d3.ascending(a, b))
}
Insert cell
yearList = {
let retVar = []
const donors = d3.group(aiddata, d => d.yearInt)
const recipients = d3.group(aiddata, d => d.yearInt)
for (const country of donors) {
if (! retVar.find(d => d == country[0])) {
retVar.push(country[0])
}
}
for (const country of recipients) {
if (! retVar.find(d => d == country[0])) {
retVar.push(country[0])
}
}
return retVar.sort((a, b) => d3.ascending(a, b))
}
Insert cell
donorsOverTime = {
let retVar = {}
const grouped = d3.group(aiddata, d => d.donor)
for (const name of grouped){
let temp = d3.rollup(name[1], v => d3.sum(v, d => d.commitment_amount), d=> d.yearInt)
let tempArr = Array.from(temp, ([year, amount]) => ({year, amount}))
let sorted = tempArr.sort((a, b) => d3.ascending(a.year, b.year))
retVar[name[0]] = sorted
}
return retVar
}
Insert cell
recipientsOverTime = {
let retVar = {}
const grouped = d3.group(aiddata, d => d.recipient)
for (const name of grouped){
let temp = d3.rollup(name[1], v => d3.sum(v, d => d.commitment_amount), d=> d.yearInt)
let tempArr = Array.from(temp, ([year, amount]) => ({year, amount}))
let sorted = tempArr.sort((a, b) => d3.ascending(a.year, b.year))
retVar[name[0]] = sorted
}
return retVar
}
Insert cell
countryTimeObj = {
let retVar = {}
for (const country of countryList) {
let donated = []
if (country in donorsOverTime) {
donated = donorsOverTime[country]
}
let received = []
if (country in recipientsOverTime) {
received = recipientsOverTime[country]
}
retVar[country] = {donated : donated, received : received}
}
return retVar
}
Insert cell
countryTimeList = {
let retVar = []
for (const country of countryList) {
let toPush = {country : country,
donated : countryTimeObj[country]['donated'],
received : countryTimeObj[country]['received']}
retVar.push(toPush)
}
return retVar
}
Insert cell
fullList = {
let retVar = []
for (const country of countryTimeList) {
let toPush = {}
let donated = []
let received = []
for (const year of yearList) {
let arrDon = (country.donated.find(d => d.year == year))
if (arrDon){
donated.push(arrDon)
} else {
donated.push({year : year, amount : 0})
}
let arrRec = (country.received.find(d => d.year == year))
if (arrRec){
received.push(arrRec)
} else {
received.push({year : year, amount : 0})
}
}
toPush['country'] = country.country
toPush['donated'] = donated
toPush['received'] = received
retVar.push(toPush)
}
return retVar
}
Insert cell
numRows = 47
Insert cell
numCols = 1
Insert cell
coordinates = d3.cross(d3.range(numRows), d3.range(numCols))
Insert cell
gridPositions = coordinates.map(([row, col]) => ({row, col}))
Insert cell
pairings = d3.zip(fullList, gridPositions)
Insert cell
data1 = pairings.map(([data, pos]) => ({...data, ...pos}))
Insert cell
margin = ({top:50, bottom:50, left:50, right:300})
Insert cell
visWidth = 500
Insert cell
visHeight = 3000
Insert cell
width = visWidth + margin.left + margin.right
Insert cell
height = visHeight + margin.top + margin.bottom
Insert cell
row = d3.scaleBand()
.domain(d3.range(numRows))
.range([0, visHeight])
.padding(0.4);
Insert cell
col = d3.scaleBand()
.domain(d3.range(numCols))
.range([0, visWidth])
.padding(0.1);
Insert cell
graphHeight = row.bandwidth()
Insert cell
graphWidth = col.bandwidth()
Insert cell
xScales = {
let retVar = {}
for(const country of data1){
let scale = d3.scaleLinear()
.domain(d3.extent(country['donated'], d => d.year))
.range([0, graphWidth])
retVar[country.country] = scale
}
return retVar
}

Insert cell
yScales = {
let retVar = []
let i = 0
for(const country of data1){
let maxAmount = d3.max([d3.max(country['donated'], d => d.amount), d3.max(country['received'], d => d.amount)])
let scale = d3.scaleLinear()
.domain([0, maxAmount]).nice()
.range([graphHeight, 0]);
retVar[country.country] = scale
i = i + 1
}
return retVar
}
Insert cell
yaxesScales = {
let retVar = []
let i = 0
for(const country of data1){
let maxAmount = d3.max([d3.max(country['donated'], d => d.amount), d3.max(country['received'], d => d.amount)])
let scale = d3.scaleLinear()
.domain([0, maxAmount/1000000]).nice()
.range([graphHeight, 0]);
retVar[country.country] = scale
i = i + 1
}
return retVar
}
Insert cell
lines = {
let retVar = {}
for (const country of data1){
let line = d3.line()
.x(d => xScales[country.country](d.year))
.y(d => yScales[country.country](d.amount))
retVar[country.country] = line
}
return retVar
}
Insert cell
xaxes = {
let retVar = {}
for (const country of data1){
retVar[country.country] = d3.axisBottom(xScales[country.country]).tickFormat(d3.format("d"))
}
return retVar
}
Insert cell
yaxes = {
let retVar = {}
for (const country of data1){
retVar[country.country] = d3.axisLeft(yaxesScales[country.country]).ticks(2)
}
return retVar
}
Insert cell
Insert cell
{
const svg = d3.create('svg')
.attr('width', visWidth + margin.left + margin.right)
.attr('height', visHeight + margin.top + margin.bottom);
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);

// add a group for each cell and position it according to its row and column
const cells = g.selectAll('.cell')
.data(data1)
.join('g')
.attr('class', 'cell')
.attr('transform', d => `translate(${col(d.col)}, ${row(d.row)})`);
cells.append('path')
.attr('fill', 'none')
.attr('stroke', 'green')
.attr('stroke-width', 1.5)
.attr('d', d=> lines[d.country](d['received']))
cells.append('path')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', d=> lines[d.country](d['donated']))
cells.append('g')
.call(xaxes['Australia'])
.attr('transform', d => `translate(${0}, ${row.bandwidth()})`)
cells.append('g')
.each( function (s) {
var svg1 = d3.select(this);
var d = svg1.datum();
svg1.call(yaxes[d.country]);})
.attr('transform', d => `translate(${0}, ${0})`)
cells.append('text')
.attr('font-size', 12)
.attr('font-family', 'sans-serif')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'left')
.attr('x', col.bandwidth())
.attr('y', row.bandwidth() / 2)
.text(d => d.country);
svg.append("circle")
.attr("cx",visWidth + 50)
.attr("cy",margin.top - 30)
.attr("r", 6)
.attr("fill", "steelblue")
svg.append("circle")
.attr("cx",visWidth + 50)
.attr("cy",margin.top)
.attr("r", 6)
.attr("fill", "green")
svg.append("text")
.attr("x", visWidth + 60)
.attr("y", margin.top - 30)
.text("Donations given (in million USD)")
.attr("font-size", 12)
.attr("dominant-baseline","middle")
svg.append("text")
.attr("x", visWidth + 60)
.attr("y", margin.top)
.text("Donations received (in million USD)")
.attr("font-size", 12)
.attr("dominant-baseline","middle")
svg.append("text")
.attr("x", margin.left + visWidth / 2)
.attr("y", margin.top - 30)
.attr('font-family', 'sans-serif')
.text("Aid Given and Received Over Time")
.attr("font-size", 18)
.attr("dominant-baseline","middle")
.attr("text-anchor", "middle")
// // add a rectangle to each group and make it take up the entire cell
// cells.append('rect')
// .attr('width', col.bandwidth())
// .attr('height', row.bandwidth())
// .attr('fill', 'white')
// .attr('stroke', 'red');
// // add the list of words to each group
// cells.selectAll('text')
// .data(d => d.country)
// .join('text')
// .attr('font-size', 15)
// .attr('font-family', 'sans-serif')
// .attr('dominant-baseline', 'hanging')
// .attr('x', 5)
// .attr('y', (d, i) => i * 15 + 2)
// .text(d => d);
return svg.node();
}
Insert cell
Insert cell
topTenCauses = {
let temp = d3.rollup(aiddata, v => d3.sum(v, d => d.commitment_amount), d=> d.coalesced_purpose_name)
let tempArr = Array.from(temp, ([cause, amount]) => ({cause, amount}))
return tempArr.sort((a,b) => d3.descending(a.amount, b.amount)).slice(0,10)
}
Insert cell
causesOverTime = {
let retVar = {}
const grouped = d3.group(aiddata, d => d.coalesced_purpose_name)
for (const name of grouped){
let temp = d3.rollup(name[1], v => d3.sum(v, d => d.commitment_amount), d=> d.yearInt)
let tempArr = Array.from(temp, ([year, amount]) => ({year, amount}))
let sorted = tempArr.sort((a, b) => d3.ascending(a.year, b.year))
if(topTenCauses.find(d => d.cause == name[0])) {
retVar[name[0]] = sorted
}
}
return retVar
}
Insert cell
causeList = Object.keys(causesOverTime)
Insert cell
causeTimeList = {
let retVar = []
for (const cause of causeList) {
let toPush = {cause : cause,
donations : causesOverTime[cause]}
retVar.push(toPush)
}
return retVar
}
Insert cell
fullCauseList = {
let retVar = []
for (const cause of causeTimeList) {
let toPush = {}
let donations = []
for (const year of yearList) {
let arrDon = (cause.donations.find(d => d.year == year))
if (arrDon){
donations.push(arrDon)
} else {
donations.push({year : year, amount : 0})
}
}
toPush['cause'] = cause.cause
toPush['donations'] = donations
retVar.push(toPush)
}
return retVar
}
Insert cell
data2 = {
let retVar = []
for (const year in yearList){
let toPush = {}
toPush['year'] = yearList[year]
for (const cause of fullCauseList) {
let amount = cause.donations.find(d => d.year == yearList[year])
if(amount) {
toPush[cause.cause] = amount.amount
} else {
toPush[cause.cause] = 0
}
}
retVar.push(toPush)
}
return retVar
}
Insert cell
dataSummed = {
let retVar = []
let count = 0
let startYear = 0
let toPush = {}
for (let i = 0; i < data2.length; i ++) {
if (i % 5 == 0) {
count = 0
toPush = {}
toPush['year'] = data2[i].year
for (const key of Object.keys(data2[i])) {
if (key != 'year') {
toPush[key] = 0
}
}
}
for (const key of Object.keys(data2[i])) {
if (key != 'year') {
toPush[key] = toPush[key] + data2[i][key]
}
}
count = count + 1
if (((i + 1) % 5 ==0) || (i == data2.length - 1)) {
for (const key of Object.keys(data2[i])) {
if (key != 'year') {
toPush[key] = toPush[key] /count
}
}
retVar.push(toPush)
}
}
return retVar
}
Insert cell
margin2 = ({top : 100, bottom : 100, right : 300, left : 100})
Insert cell
vis2Width = 500
Insert cell
vis2Height = 500
Insert cell
stackedData = d3.stack()
.keys(causeList)(dataSummed)
Insert cell
maxStacked = d3.max(stackedData, d => d3.max(d, d => d[1]))
Insert cell
Insert cell
{
const svg = d3.create('svg')
.attr('width', vis2Width + margin2.left + margin2.right)
.attr('height', vis2Height + margin2.top + margin2.bottom);
const g = svg.append('g')
.attr('transform', `translate(${margin2.left}, ${margin2.top})`);
let x = d3.scaleLinear()
.domain(d3.extent(data2, d => d.year))
.range([0, vis2Width])
let y = d3.scaleLinear()
.domain([0, maxStacked])
.range([vis2Height, 0])
let yaxisScale = d3.scaleLinear()
.domain([0, maxStacked / 1000000])
.range([vis2Height, 0])
let color = d3.scaleOrdinal()
.domain(causeList)
.range(d3.schemeTableau10);
let xaxis = g.append("g")
.attr("transform", "translate(0," + vis2Height + ")")
.call(d3.axisBottom(x).tickFormat(d3.format("d")))
let yaxis = g.append("g")
.attr("transform", "translate(0,0)")
.call(d3.axisLeft(yaxisScale).tickFormat(d3.format("d")))
let xLabel = g.append('text')
.attr("x", vis2Width /2)
.attr("y", vis2Height + 40)
.text("Year")
.attr("font-size", 14)
.attr('font-family', 'sans-serif')
.attr("dominant-baseline","middle")
.attr("text-anchor", "middle")
let yLabel = g.append('text')
.attr("x", 0)
.attr("y", 0)
.text("Aid amount (in million USD)")
.attr("font-size", 14)
.attr('font-family', 'sans-serif')
.attr("dominant-baseline","middle")
.attr("text-anchor", "middle")
.attr("transform", `translate(${-50}, ${vis2Height/2})rotate(-90)`)
let highlight = function(event, d){
// reduce opacity of all groups
d3.selectAll(".myArea").style("opacity", .1)
// expect the one that is hovered
d3.select(this).style("opacity", 1)
}

// And when it is not hovered anymore
let noHighlight = function(event, d){
d3.selectAll(".myArea").style("opacity", 1)
}

let area = d3.area()
.x(function(d) { return x(d.data.year); })
.y0(function(d) { return y(d[0]); })
.y1(function(d) { return y(d[1]); })
g.selectAll("mylayers")
.data(stackedData)
.enter()
.append("path")
.attr("class", function(d) { return "myArea " + d.key })
.style("fill", function(d) { return color(d.key); })
.attr("d", area)
.on("mouseover", highlight)
.on("mouseleave", noHighlight)
var size = 20
g.selectAll("myrect")
.data(causeList)
.enter()
.append("rect")
.attr("x", 400)
.attr("y", function(d,i){ return 10 + i*(size+5)})
.attr("width", size)
.attr("height", size)
.style("fill", function(d){ return color(d)})

g.selectAll("mylabels")
.data(causeList)
.enter()
.append("text")
.attr("x", 400 + size*1.2)
.attr("y", function(d,i){ return 10 + i*(size+5) + (size/2)})
.style("fill", function(d){ return color(d)})
.text(function(d){ return d})
.attr("text-anchor", "left")
.style("alignment-baseline", "middle")

let title = g.append('text')
.attr("x", vis2Width / 2)
.attr("y", -40)
.text("Ten Most Common Categories of Aid Given Over Time")
.attr("font-size", 18)
.attr('font-family', 'sans-serif')
.attr("dominant-baseline","middle")
.attr("text-anchor", "middle")
let caveat = g.append('text')
.attr("x", vis2Width /2)
.attr("y", vis2Height + 80)
.text("Values computed as average of 5 year periods starting from 1973")
.attr("font-size", 12)
.attr('font-family', 'sans-serif')
.attr("dominant-baseline","middle")
.attr("text-anchor", "middle")
return svg.node()
}
Insert cell
Insert cell
Insert cell
import {legend, swatches} from "@d3/color-legend"
Insert cell
Insert cell
d3 = require('d3@6')
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