Public
Edited
Apr 28
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const width = 928;
const height = width;
const {names, colors, linkValues} = data;
const outerRadius = Math.min(width, height) * 0.5 - 70;
const innerRadius = outerRadius - 10;
const tickStep = d3.tickStep(0, d3.sum(data.flat()), 100);
const formatValue = d3.format(".1~%");
const chord = d3.chord()
.padAngle(10 / innerRadius)
.sortSubgroups(d3.descending)
.sortChords(d3.descending);

const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);

const ribbon = d3.ribbon()
.radius(innerRadius - 1)
.padAngle(1 / innerRadius);

const color = d3.scaleOrdinal(names, colors);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "width: 100%; height: auto; font: 10px sans-serif;");

const chords = chord(data);

const group = svg.append("g")
.selectAll()
.data(chords.groups)
.join("g");

group.append("path")
.attr("fill", d => color(names[d.index]))
.attr("d", arc);

group.append("title")
.text(d => `${names[d.index]}\n${formatValue(d.value)}`);

const groupTick = group.append("g")
.selectAll()
.data(d => groupTicks(d, tickStep))
.join("g")
.attr("transform", d => `rotate(${d.angle * 180 / Math.PI - 90}) translate(${outerRadius},0)`);

groupTick.append("text")
.attr("x", 8)
.attr("dy", "0.35em")
.attr("transform", d => d.angle > Math.PI ? "rotate(180) translate(-16)" : null)
.attr("text-anchor", d => d.angle > Math.PI ? "end" : null);

group.select("text")
.attr("font-weight", "bold")
.text(function(d) {
return this.getAttribute("text-anchor") === "end"
? `↑ ${names[d.index]}`
: `${names[d.index]} ↓`;
});
svg.append("g")
.attr("fill-opacity", 0.8)
.selectAll("path")
.data(chords)
.join("path")
.style("mix-blend-mode", "multiply")
.attr("fill", d => color(names[d.source.index]))
.attr("d", ribbon)
.append("title")
.text(function(d){
let sourceName = names[d.source.index];
let targetName = names[d.target.index];

let sourceToTargetValue = linkValues[`${sourceName}_${targetName}`];
let targetToSourceValue = linkValues[`${targetName}_${sourceName}`];

let lineTitle = '';

if (sourceToTargetValue){
let fromSourceToTargetValue = formatBigNumber(linkValues[`${sourceName}_${targetName}`]);
lineTitle+=`${fromSourceToTargetValue} ${names[d.target.index]} → ${names[d.source.index]}`
}

if (targetToSourceValue && d.source.index !== d.target.index){
let fromTargetToSourceValue = formatBigNumber(linkValues[`${targetName}_${sourceName}`]);
lineTitle+=`\n${fromTargetToSourceValue} ${names[d.source.index]} → ${names[d.target.index]}`
}
return lineTitle;
});

return svg.node();
}
Insert cell
async function processData(dataType, viewType, countryLimit = 10) {
let tradeData, categoriesData, countriesData;
if (dataType === "exports") {
tradeData = await FileAttachment("exports.csv").csv();
if (viewType === "categories") {
const categoriesData1 = await FileAttachment("export_categories.csv").csv();
const categoriesData2 = await FileAttachment("export_categories@1.csv").csv();
categoriesData = [...categoriesData1, ...categoriesData2];
}
} else {
tradeData = await FileAttachment("imports.csv").csv();
if (viewType === "categories") {
const categoriesData1 = await FileAttachment("import_categories.csv").csv();
const categoriesData2 = await FileAttachment("import_categories@1.csv").csv();
categoriesData = [...categoriesData1, ...categoriesData2];
}
}
countriesData = await FileAttachment("countries.csv").csv();
const sourceColumn = "from";
const targetColumn = "to";
const volumeColumn = "value";
let items = [];
let volumeByItem = {};
let countryToContinent = {};
countriesData.forEach(d => {
countryToContinent[d.Country] = d.Continent;
});
if (viewType === "countries") {
tradeData.forEach(d => {
const source = d[sourceColumn];
const volume = parseVolume(d[volumeColumn]);
volumeByItem[source] = (volumeByItem[source] || 0) + volume;
});
items = Object.entries(volumeByItem)
.sort((a, b) => b[1] - a[1])
.slice(0, parseInt(countryLimit))
.map(d => d[0]);
} else if (viewType === "regions") {
tradeData.forEach(d => {
const source = d[sourceColumn];
const continent = countryToContinent[source] || "Unknown";
const volume = parseVolume(d[volumeColumn]);
volumeByItem[continent] = (volumeByItem[continent] || 0) + volume;
});
items = Object.entries(volumeByItem)
.sort((a, b) => b[1] - a[1])
.map(d => d[0]);
} else if (viewType === "categories") {
categoriesData.forEach(d => {
const category = d.category;
const volume = parseVolume(d[volumeColumn]);
volumeByItem[category] = (volumeByItem[category] || 0) + volume;
});
items = Object.entries(volumeByItem)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(d => d[0]);
}
const n = items.length;
const matrix = Array(n).fill().map(() => Array(n).fill(0));
const linkValues = {};
if (viewType === "countries") {
tradeData.forEach(d => {
const source = d[sourceColumn];
const target = d[targetColumn];
const sourceIdx = items.indexOf(source);
const targetIdx = items.indexOf(target);
if (sourceIdx !== -1 && targetIdx !== -1) {
const volume = parseVolume(d[volumeColumn]);
const maxValue = 1e10;

linkValues[`${source}_${target}`] = volume;
matrix[sourceIdx][targetIdx] += volume / maxValue;
}
});
} else if (viewType === "regions") {
tradeData.forEach(d => {
const source = countryToContinent[d[sourceColumn]] || "Unknown";
const target = countryToContinent[d[targetColumn]] || "Unknown";
const sourceIdx = items.indexOf(source);
const targetIdx = items.indexOf(target);
if (sourceIdx !== -1 && targetIdx !== -1) {
const volume = parseVolume(d[volumeColumn]);
const maxValue = 1e10;

linkValues[`${source}_${target}`] = volume;
matrix[sourceIdx][targetIdx] += volume / maxValue;
}
});
} else if (viewType === "categories") {
categoriesData.forEach(d => {
const category = d.category;
const target = d[targetColumn];
const sourceIdx = items.indexOf(category);
const targetIdx = items.indexOf(target);
if (sourceIdx !== -1 && targetIdx !== -1) {
const volume = parseVolume(d[volumeColumn]);
const maxValue = 1e10;

linkValues[`${category}_${target}`] = volume;
matrix[sourceIdx][targetIdx] += volume / maxValue;
}
});
}
let colors;
if (viewType === "regions") {
const regionColors = {
"America": "#1f77b4",
"Europe": "#ff7f0e",
"Asia": "#2ca02c",
"Africa": "#d62728",
"Oceania": "#9467bd",
"Unknown": "#999999"
};
colors = items.map(region => regionColors[region] || "#999999");
} else if (viewType === "categories") {
const categoryToCountry = {};
const countryColors = {};
const uniqueCountries = [...new Set(categoriesData.map(d => d.from))];
uniqueCountries.forEach((country, i) => {
countryColors[country] = d3.interpolateRainbow(i / uniqueCountries.length);
});
categoriesData.forEach(d => {
categoryToCountry[d.category] = d.from;
});
colors = items.map(category => {
const country = categoryToCountry[category] || "Unknown";
return countryColors[country] || "#999999";
});
} else {
colors = d3.quantize(d3.interpolateRainbow, items.length);
}

const formattedData = Object.assign(matrix, {
names: items,
colors: colors,
linkValues: linkValues
});
return formattedData;
}
Insert cell
data = processData(dataType, viewType, countryLimit)
Insert cell
function groupTicks(d, step) {
const k = (d.endAngle - d.startAngle) / d.value;
return d3.range(0, d.value, step).map(value => {
return {value: value, angle: value * k + d.startAngle};
});
}
Insert cell
// Function for parsing volume
function parseVolume(volStr) {
if (!volStr) return 0;
volStr = volStr.replace('$', '');
let factor = 1;
if (volStr.endsWith('B')) {
factor = 1e9;
volStr = volStr.replace('B', '');
} else if (volStr.endsWith('M')) {
factor = 1e6;
volStr = volStr.replace('M', '');
} else if (volStr.endsWith('K')) {
factor = 1e3;
volStr = volStr.replace('K', '');
}
return parseFloat(volStr) * factor;
}
Insert cell
//convert big number to readable format
function formatBigNumber(num) {
if (num >= 1e12) return (num / 1e12).toFixed(1) + 'T';
if (num >= 1e9) return (num / 1e9).toFixed(1) + 'B';
if (num >= 1e6) return (num / 1e6).toFixed(1) + 'M';
if (num >= 1e3) return (num / 1e3).toFixed(1) + 'k';
return num.toString();
}
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