Public
Edited
Nov 11, 2023
4 forks
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
Insert cell
geoJSON = FileAttachment("countries-50m.json").json()
Insert cell
Insert cell
Insert cell
Insert cell
import {legend, Swatches} from "@d3/color-legend"
Insert cell
Insert cell
Insert cell
Insert cell
donateData = d3.rollups(
aiddata,
v => d3.sum(v, d => d.amount),
d => d.donor,
d => d.yearDate,
);

Insert cell
modifiedDonateData = donateData.map(countryData => {
return {
country: countryData[0],
data: countryData[1]
.map(item => {
return {
date: new Date(item[0]),
amount: item[1]
};
})
.sort((a, b) => a.date - b.date)
};
});

Insert cell
{const allCountriesSet = new Set(countries); // Assuming allCountries is the list of all countries

allCountriesSet.forEach(country => {
const foundCountry = modifiedDonateData.find(data => data.country === country);
if (!foundCountry) {
modifiedDonateData.push({ country: country, data: [] });
}
});
}
Insert cell
receiveData = d3.rollups(
aiddata,
v => d3.sum(v, d => d.amount),
d => d.recipient,
d => d.yearDate,
);
Insert cell
modifiedReceiveData = receiveData.map(countryData => {
return {
country: countryData[0],
data: countryData[1]
.map(item => {
return {
date: new Date(item[0]),
amount: item[1]
};
})
.sort((a, b) => a.date - b.date)
};
});

Insert cell
{const allCountriesSet = new Set(countries); // Assuming allCountries is the list of all countries

allCountriesSet.forEach(country => {
const foundCountry = modifiedReceiveData.find(data => data.country === country);
if (!foundCountry) {
modifiedReceiveData.push({ country: country, data: [] });
}
});
}
Insert cell
countries = new Set([...d3.map(aiddata, d => d.donor), ...d3.map(aiddata, d => d.recipient)]);
Insert cell
max_received = Math.max(...receiveData.map(data => Math.max(...data[1].map(item => item[1]))));

Insert cell
median_received = d3.median(receiveData.map(data => d3.median(data[1].map(item => item[1]))));

Insert cell
max_donated = Math.max(...donateData.map(data => Math.max(...data[1].map(item => item[1]))));
Insert cell
median_donated = d3.median(donateData.map(data => d3.median(data[1].map(item => item[1]))));

Insert cell
max_amount = Math.max(max_donated, max_received)
Insert cell
x = d3.scaleTime()
.range([0, width])
.domain(d3.extent(aiddata.map(d => d.yearDate)))
Insert cell
overlap = 1
Insert cell
h = 50
Insert cell
y = {
const max = max_amount
return d3.scaleLinear()
.range([ h * overlap, -overlap * h ])
.domain([-max, +max])
}
Insert cell
area = d3.area()
.curve(d3.curveBasis)
.x(d => x(d.date))
.y0(0)
.y1(d => y(d.amount))
Insert cell
margin = ({top: 5, right: 1, bottom: 20, left: 40})
Insert cell
step = 50
Insert cell
overlaps = Array.from({length: overlap * 2} , (_, i) => Object.assign({index: i < overlap ? -i - 1: i - overlap}))
Insert cell
{
const allCountriesArray = Array.from(countries);

modifiedDonateData.sort((a, b) => allCountriesArray.indexOf(a.country) - allCountriesArray.indexOf(b.country));

modifiedReceiveData.sort((a, b) => allCountriesArray.indexOf(a.country) - allCountriesArray.indexOf(b.country));

}
Insert cell
allCountriesArray = Array.from(countries);
Insert cell
// Sort to make sure that countries have similar patterns in terms of receiving/donating money are close in the country list. This help to make a more organized and user-friendly visualization.
// Calculate the absolute sum of the data in the array
{const getAbsoluteSum = (data) => {
return Math.abs(d3.sum(data, d => d.amount));
};

// Sorting allCountriesArray based on the absolute value in modifiedData arrays
allCountriesArray.sort((a, b) => {
const countryDataA = modifiedReceiveData.find(entry => entry.country === a) || modifiedDonateData.find(entry => entry.country === a);
const countryDataB = modifiedReceiveData.find(entry => entry.country === b) || modifiedDonateData.find(entry => entry.country === b);

const absoluteSumA = countryDataA ? getAbsoluteSum(countryDataA.data) : 0;
const absoluteSumB = countryDataB ? getAbsoluteSum(countryDataB.data) : 0;

return absoluteSumB - absoluteSumA;
});

// Sort modifiedDonateData based on the sorted allCountriesArray
modifiedDonateData.sort((a, b) => allCountriesArray.indexOf(a.country) - allCountriesArray.indexOf(b.country));

// Sort modifiedReceiveData based on the sorted allCountriesArray
modifiedReceiveData.sort((a, b) => allCountriesArray.indexOf(a.country) - allCountriesArray.indexOf(b.country));
}
Insert cell
charts = {
const mode = 'offset';
const margin = { top: 50, right: 20, bottom: 30, left: 50 };
const width = 928 - margin.left - margin.right;
const height = 4850 - margin.top - margin.bottom;

const svg = d3.select(DOM.svg(width + margin.left + margin.right, height + margin.top + margin.bottom));

// Define Axes
const xAxis = d3.axisBottom().scale(x);

const y_scale = d3.scaleLinear()
.domain([max_amount,0])
.range([-step, 0]);

const yAxis = d3.axisRight().scale(y_scale).tickValues(d3.range(0, max_amount,2000000000))
.tickFormat(d3.format(".0s"));
// Loop through the modifiedReceiveData array to create charts for each country
modifiedReceiveData.forEach((countryData, index) => {
const g = svg.append("g").attr("transform", `translate(${margin.left}, ${index * 100 + margin.top})`); // Adjust the positioning

g.append("clipPath")
.attr("id", `clipy-${index}`)
.append("rect")
.attr("width", width)
.attr("height", step);

g.append("defs").append("path")
.attr("id", `path-def-receive-${index}`)
.datum(countryData.data)
.attr("d", area);

g.append("g")
.attr("clip-path", `url(#clipy-${index})`)
.selectAll("use")
.data(overlaps)
.enter().append("use")
.attr("fill", 'rgba(70, 130, 180, 0.5)')
.attr("transform", d => mode === "mirror" && d.index < 0
? `scale(1,-1) translate(0, ${d.index * step})`
: `translate(0,${(d.index + 1) * step})`)
.attr("href", `#path-def-receive-${index}`);

g.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(0, 50)`)

.call(yAxis);

// Add country names
svg.append("text")
.attr("x", 0)
.attr("y", index * 100 + 40)
.text(countryData.country)
.style("font-size", "12px")
.style("font-family", "Calibri, sans-serif");

});



// Loop through the modifiedDonateData array to create charts for each country
modifiedDonateData.forEach((countryData, index) => {
const g = svg.append("g").attr("transform", `translate(${margin.left}, ${index * 100 + margin.top})`); // Adjust the positioning for donated data

g.append("clipPath")
.attr("id", `clipy-donate-${index}`)
.append("rect")
.attr("width", width)
.attr("height", step);

g.append("defs").append("path")
.attr("id", `path-def-donate-${index}`)
.datum(countryData.data)
.attr("d", area);

g.append("g")
.attr("clip-path", `url(#clipy-donate-${index})`)
.selectAll("use")
.data(overlaps)
.enter().append("use")
.attr("fill", 'rgba(255, 165, 0, 0.5)')


.attr("transform", d => mode === "mirror" && d.index < 0
? `scale(1,-1) translate(0, ${d.index * step})`
: `translate(0,${(d.index + 1) * step})`)
.attr("href", `#path-def-donate-${index}`);






});
// Create a moving rule that follows the mouse.
const rule = svg.append("line")
.attr("stroke", "#000")
.attr("y1", margin.top - 6)
.attr("y2", height - margin.bottom - 1)
.attr("x1", 0.5)
.attr("x2", 0.5);

svg.on("mousemove touchmove", (event) => {
const x = d3.pointer(event, svg.node())[0] + 0.5;
rule.attr("x1", x).attr("x2", x);
});


// Append Axes
svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.call(xAxis);


// Append Legend
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", `translate(${width - margin.right}, ${0})`);


legend.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 20)
.attr("height", 20)
.style("fill", 'rgba(70, 130, 180, 0.5)');

legend.append("text")
.attr("x", 30)
.attr("y", 15)
.text("Receipts")
.style("font-size", "12px")
.style("font-family", "Calibri, sans-serif");


legend.append("rect")
.attr("x", 0)
.attr("y", 30)
.attr("width", 20)
.attr("height", 20)
.style("fill", 'rgba(255, 165, 0, 0.5)');

legend.append("text")
.attr("x", 30)
.attr("y", 45)
.text("Donations")
.style("font-size", "12px")
.style("font-family", "Calibri, sans-serif");




return svg.node();
}

Insert cell
Insert cell
top_10 = d3.rollups(
aiddata,
group => d3.sum(group, d => d.amount),
d => d.purpose
).sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 11);
Insert cell
top_10_cleaned = d3.map(top_10, d => d[0]).filter(d => d !== "Sectors not specified");
Insert cell
aiddata_cleaned = d3.map(aiddata,d => ({
date: d.yearDate,
key: d.purpose,
value: d.amount,
})).filter(d => top_10_cleaned.includes(d.key));
Insert cell
groupedData = Array.from(
d3.rollups(
aiddata_cleaned,
v => d3.sum(v, d => d.value),
d => d.date,
d => d.key
),
([date, data]) => data.map(([key, value]) => ({ date, key, value }))
).flat();
Insert cell
test = {
const newData = [];
const dates = [...new Set(groupedData.map(item => item.date))]; // Extract unique dates

dates.forEach(date => {
const filteredData = groupedData.filter(item => item.date === date);
const existingKeys = filteredData.map(item => item.key);
const missingKeys = top_10_cleaned.filter(key => !existingKeys.includes(key));

missingKeys.forEach(missingKey => {
newData.push({ date: date, key: missingKey, value: 0 });
});

filteredData.forEach(item => {
newData.push(item);
});
});
return newData
}
Insert cell
// sortedData = test.sort((a, b) => {
// if (a.date !== b.date) {
// return d3.ascending(a.date, b.date); // Sort by date
// }
// return d3.ascending(a.key, b.key); // Sort by key
// });

Insert cell
sortedData = test.sort((a, b) => {
if (a.date !== b.date) {
return d3.ascending(a.date, b.date); // Sort by date
}
const keyComparison = top_10_cleaned.indexOf(a.key) - top_10_cleaned.indexOf(b.key);
return keyComparison !== 0 ? keyComparison : d3.ascending(a.key, b.key); // Sort by key's position in top_10_cleaned, then by key
});

Insert cell
Insert cell
Insert cell
Swatches(d3.scaleOrdinal(top_10_cleaned, d3.schemeTableau10), {
columns: "180px"
})
Insert cell
chartBasic = {
const height = 500
const width = 900
const svg = d3.create('svg')
.attr('height', height)
.attr('width', width)
const keys = Array.from(d3.group(sortedData, d => d.key).keys())
const values = Array.from(d3.rollup(sortedData, ([d]) => d.value, d => +d.date, d => d.key))

console.log(values)
const series = d3.stack()
.keys(keys)
.value(([, values], key) => values.get(key))
.order(d3.stackOrderNone)
.offset(d3.stackOffsetExpand) // d3.stackOffsetExpand for normalized
// .offset(null)
(values)
const x = d3.scaleUtc()
.domain(d3.extent(sortedData, d => d.date))
.range([margin.left, width - margin.right])
const y = d3.scaleLinear()
.domain([0, d3.max(series, d => d3.max(d, d => d[1]))])
.range([height - margin.bottom, margin.top])
const c = d3.scaleOrdinal()
.domain(keys)
.range(d3.schemeTableau10)
// .range(d3.schemePastel2)
const area = d3.area()
.x(d => x(d.data[0]))
.y0(d => y(d[0]))
.y1(d => y(d[1]))

const xAxis = g => g
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
const yAxis = g => g
.attr('transform', `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select('.domain').remove())
.call(g => g.select('.tick:last-of-type text').clone()
.attr('x', 3)
.attr('text-anchor', 'start')
.attr('font-weight', 'bold')
.text(sortedData.y))
svg.append('g')
.selectAll('path')
.data(series)
.join('path')
.attr('fill', ({key}) => c(key))
.attr('d', area)
.append('title')
.text(({key}) => key)
svg.append('g')
.call(xAxis)
svg.append('g')
.call(yAxis)
return svg.node()
}
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