Public
Edited
Nov 9, 2023
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
// add cells here
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
{
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
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 = 60
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
color_received = i => d3['schemeBlues'][overlap * 2 + 1][i + (i >= 0) + overlap]
Insert cell
color_donated = i => d3['schemeOranges'][overlap * 2 + 1][i + (i >= 0) + overlap]
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
chart = {
const mode = 'offset';
const svg = d3.select(DOM.svg(width, h));
// Define Axes
const xAxis = d3.axisBottom().scale(x);


const yDomain = [0, -2000000000, -4000000000, -6000000000]; // 根据您的需求定义y轴的范围

const yAxisRight = d3.axisRight()
.scale(y)
.tickSize(-width/100) // 使刻度线的长度与图表宽度相同
.tickValues(yDomain) // 手动指定要显示的刻度值
.tickFormat("") // 空字符串会使刻度不显示任何文本
.tickSizeOuter(0); // 将外部刻度线的长度设置为0


// Area Chart
const g = svg.append("g").attr("transform", `translate(0, 0)`)
g.append("clipPath")
.attr("id", "clipy")
.append("rect")
.attr("width", width)
.attr("height", step)
g.append("defs").append("path")
.attr("id", "path-def")
.datum(modifiedReceiveData[0].data)
.attr("d", area);

g.append("g")
.attr("clip-path", "url(#clipy)")
.selectAll("use")
.data(overlaps)
.enter().append("use")
.attr("fill", d => color_received(d.index))
.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")


// Area Chart for donated data
g.append("defs").append("path")
.attr("id", "path-def-donate")
.datum(modifiedDonateData[0].data) // Replace with your data for donated values
.attr("d", area);

g.append("g")
.attr("clip-path", "url(#clipy)")
.selectAll("use")
.data(overlaps) // Assuming overlaps is the same for donated data
.enter().append("use")
.attr("fill", d => color_donated(d.index)) // Use the color scheme for donated data
.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");


svg.append("g")
.attr("class", "axisRight")
.attr("transform", `translate(${width}, 0) scale(1, -1) translate(0, -50)`)
.call(yAxisRight);
// Append Axes last
svg.append("g").call(xAxis);
return svg.node();
}
Insert cell
width
Insert cell
charts = {
const mode = 'offset';
const margin = { top: 50, right: 20, bottom: 30, left: 50 }; // Define the margin
const width = 928 - margin.left - margin.right; // Adjust the width
const height = 4850 - margin.top - margin.bottom; // Adjust the height

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

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

// 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", 'steelblue')
.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}`);

// Add country names
svg.append("text")
.attr("x", 0) // Adjust the x position for the country name
.attr("y", index * 100 + 80) // Adjust the y position for the country name
.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", 'orange')
.attr("transform", d => mode === "mirror" && d.index < 0 // Adjust the condition to match the receive data positioning
? `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 last
svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`) // Translate the x-axis
.call(xAxis);

return svg.node();
}

Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

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