Public
Edited
Mar 19
Insert cell
Insert cell
Insert cell
Inputs.table(tradedata)
Insert cell
uniqueReporters = [...new Set(tradedata.map(tradedata => tradedata.Reporter))];
Insert cell
tradedata = {
const data = await d3.csv(googleSheetCsvUrl, row => ({
Year:(row.Year),
Reporter: row.Reporter,
ImportOrExport: row.ImportOrExport,
Partner: row.Partner,
Description: row.Description,
Value_USD: row.Value_USD,
}));
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
countryNameMapping = ({
"Afghanistan": "Afghanistan",
"Albania": "Albania",
"Algeria": "Algeria",
"Andorra": "Andorra",
"Angola": "Angola",
"Antigua and Barbuda": "Antigua and Barbuda",
"Argentina": "Argentina",
"Armenia": "Armenia",
"Australia": "Australia",
"Austria": "Austria",
"Azerbaijan": "Azerbaijan",
"Bahamas": "Bahamas",
"Bahrain": "Bahrain",
"Bangladesh": "Bangladesh",
"Barbados": "Barbados",
"Belarus": "Belarus",
"Belgium": "Belgium",
"Belize": "Belize",
"Benin": "Benin",
"Bhutan": "Bhutan",
"Bolivia (Plurinational State of)": "Bolivia",
"Bosnia and Herzegovina": "Bosnia and Herzegovina",
"Botswana": "Botswana",
"Brazil": "Brazil",
"Brunei Darussalam": "Brunei",
"Bulgaria": "Bulgaria",
"Burkina Faso": "Burkina Faso",
"Burundi": "Burundi",
"Cabo Verde": "Cape Verde",
"Cambodia": "Cambodia",
"Cameroon": "Cameroon",
"Canada": "Canada",
"Central African Republic": "Central African Republic",
"Chad": "Chad",
"Chile": "Chile",
"China": "China",
"China, Hong Kong SAR": "Hong Kong",
"China, Macao SAR": "Macao",
"Colombia": "Colombia",
"Comoros": "Comoros",
"Congo": "Republic of Congo",
"Costa Rica": "Costa Rica",
"Côte d'Ivoire": "Ivory Coast",
"Croatia": "Croatia",
"Cuba": "Cuba",
"Cyprus": "Cyprus",
"Czechia": "Czech Republic",
"Dem. People's Rep. of Korea": "North Korea",
"Dem. Rep. of the Congo": "Democratic Republic of the Congo",
"Denmark": "Denmark",
"Djibouti": "Djibouti",
"Dominica": "Dominica",
"Dominican Rep.": "Dominican Republic",
"Ecuador": "Ecuador",
"Egypt": "Egypt",
"El Salvador": "El Salvador",
"Equatorial Guinea": "Equatorial Guinea",
"Eritrea": "Eritrea",
"Estonia": "Estonia",
"Eswatini": "Swaziland",
"Ethiopia": "Ethiopia",
"Fiji": "Fiji",
"Finland": "Finland",
"France": "France",
"Gabon": "Gabon",
"Gambia": "Gambia",
"Georgia": "Georgia",
"Germany": "Germany",
"Ghana": "Ghana",
"Greece": "Greece",
"Grenada": "Grenada",
"Guatemala": "Guatemala",
"Guinea": "Guinea",
"Guinea-Bissau": "Guinea-Bissau",
"Guyana": "Guyana",
"Haiti": "Haiti",
"Honduras": "Honduras",
"Hungary": "Hungary",
"Iceland": "Iceland",
"India": "India",
"Indonesia": "Indonesia",
"Iran": "Iran",
"Iraq": "Iraq",
"Ireland": "Ireland",
"Israel": "Israel",
"Italy": "Italy",
"Jamaica": "Jamaica",
"Japan": "Japan",
"Jordan": "Jordan",
"Kazakhstan": "Kazakhstan",
"Kenya": "Kenya",
"Kiribati": "Kiribati",
"Kuwait": "Kuwait",
"Kyrgyzstan": "Kyrgyzstan",
"Lao People's Dem. Rep.": "Laos",
"Latvia": "Latvia",
"Lebanon": "Lebanon",
"Lesotho": "Lesotho",
"Liberia": "Liberia",
"Libya": "Libya",
"Lithuania": "Lithuania",
"Luxembourg": "Luxembourg",
"Madagascar": "Madagascar",
"Malawi": "Malawi",
"Malaysia": "Malaysia",
"Maldives": "Maldives",
"Mali": "Mali",
"Malta": "Malta",
"Marshall Islands": "Marshall Islands",
"Mauritania": "Mauritania",
"Mauritius": "Mauritius",
"Mexico": "Mexico",
"Micronesia (Federated States of)": "Micronesia",
"Monaco": "Monaco",
"Mongolia": "Mongolia",
"Montenegro": "Montenegro",
"Morocco": "Morocco",
"Mozambique": "Mozambique",
"Myanmar": "Myanmar",
"Namibia": "Namibia",
"Nauru": "Nauru",
"Nepal": "Nepal",
"Netherlands": "Netherlands",
"New Zealand": "New Zealand",
"Nicaragua": "Nicaragua",
"Niger": "Niger",
"Nigeria": "Nigeria",
"North Macedonia": "Macedonia",
"Norway": "Norway",
"Oman": "Oman",
"Other Asia, nes": "Taiwan",
"Pakistan": "Pakistan",
"Palau": "Palau",
"Panama": "Panama",
"Papua New Guinea": "Papua New Guinea",
"Paraguay": "Paraguay",
"Peru": "Peru",
"Philippines": "Philippines",
"Poland": "Poland",
"Portugal": "Portugal",
"Qatar": "Qatar",
"Rep. of Korea": "South Korea",
"Rep. of Moldova": "Moldova",
"Romania": "Romania",
"Russian Federation": "Russia",
"Rwanda": "Rwanda",
"Saint Kitts and Nevis": "Saint Kitts and Nevis",
"Saint Lucia": "Saint Lucia",
"Saint Vincent and the Grenadines": "Saint Vincent and the Grenadines",
"Samoa": "Samoa",
"San Marino": "San Marino",
"Sao Tome and Principe": "Sao Tome and Principe",
"Saudi Arabia": "Saudi Arabia",
"Senegal": "Senegal",
"Serbia": "Serbia",
"Seychelles": "Seychelles",
"Sierra Leone": "Sierra Leone",
"Singapore": "Singapore",
"Slovakia": "Slovakia",
"Slovenia": "Slovenia",
"Solomon Islands": "Solomon Islands",
"Somalia": "Somalia",
"South Africa": "South Africa",
"South Sudan": "South Sudan",
"Spain": "Spain",
"Sri Lanka": "Sri Lanka",
"State of Palestine": "Palestine",
"Sudan": "Sudan",
"Sudan (...2011)": "Sudan",
"Suriname": "Suriname",
"Sweden": "Sweden",
"Switzerland": "Switzerland",
"Syria": "Syria",
"Tajikistan": "Tajikistan",
"Thailand": "Thailand",
"Timor-Leste": "East Timor",
"Togo": "Togo",
"Tonga": "Tonga",
"Trinidad and Tobago": "Trinidad and Tobago",
"Tunisia": "Tunisia",
"Türkiye": "Turkey",
"Turkmenistan": "Turkmenistan",
"Tuvalu": "Tuvalu",
"Uganda": "Uganda",
"Ukraine": "Ukraine",
"United Arab Emirates": "United Arab Emirates",
"United Kingdom": "United Kingdom",
"United Rep. of Tanzania": "Tanzania",
"USA": "United States of America",
"Uruguay": "Uruguay",
"Uzbekistan": "Uzbekistan",
"Vanuatu": "Vanuatu",
"Venezuela": "Venezuela",
"Viet Nam": "Vietnam",
"Yemen": "Yemen",
"Zambia": "Zambia",
"Zimbabwe": "Zimbabwe"
});
Insert cell
{
const reverseMapping = {};
for (const [tradeKey, geoKey] of Object.entries(countryNameMapping)) {
reverseMapping[geoKey] = tradeKey;
}
const tradeBalanceByCountry = d3.rollup(
tradedata,
v => {
const imports = d3.sum(v.filter(d => d.ImportOrExport === "Import"), d => +d.Value_USD);
const exports = d3.sum(v.filter(d => d.ImportOrExport === "Export"), d => +d.Value_USD);
return {
imports,
exports,
netTrade: exports - imports,
tradeVolume: imports + exports
};
},
d => d.Reporter
);
const formatCurrency = (value) => {
const absValue = Math.abs(value);
if (absValue >= 1e12) {
return `${(value / 1e12).toFixed(2)}T`;
} else if (absValue >= 1e9) {
return `${(value / 1e9).toFixed(2)}B`;
} else if (absValue >= 1e6) {
return `${(value / 1e6).toFixed(2)}M`;
} else {
return `${value.toFixed(2)}`;
}
};
const formatTradeBalance = (value) => {
const valueInT = value / 1e12;
if (valueInT >= 0) {
return `${valueInT.toFixed(2)}T`;
} else {
return `-${Math.abs(valueInT).toFixed(2)}T`;
}
};
const tradeBalanceArray = Array.from(tradeBalanceByCountry, ([country, data]) => ({
country,
...data
}));
const sortedCountries = tradeBalanceArray.sort((a, b) => a.netTrade - b.netTrade);
geoJSON.features.forEach(feature => {
const geoName = feature.properties.NAME;
const tradeDataName = reverseMapping[geoName] || geoName;
const tradeData = tradeBalanceArray.find(d => d.country === tradeDataName);
if (tradeData) {
feature.properties.tradeData = tradeData;
}
});
const unmappedCountries = tradeBalanceArray.filter(tradeCountry => {
const mappedName = countryNameMapping[tradeCountry.country] || tradeCountry.country;
return !geoJSON.features.some(feature => feature.properties.NAME === mappedName);
});
const width = 1200;
const height = 720;
const margin = { top: 60, right: 300, bottom: 120, left: 60 };
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");
const mapGroup = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
const projection = d3.geoNaturalEarth1()
.fitSize([width - margin.right - margin.left, height - margin.top - margin.bottom], geoJSON);
const path = d3.geoPath()
.projection(projection);
const colorScale = d3.scaleQuantile()
.domain(tradeBalanceArray.map(d => d.netTrade))
.range([
"#8b0000",
"#c13030",
"#e66e6e",
"#f7b0b0",
"#f9d0d0",
"#f5f5f5",
"#d0e8d0",
"#a3d0a3",
"#6eb86e",
"#2e8b2e",
]);
const countryPaths = mapGroup.selectAll("path")
.data(geoJSON.features)
.join("path")
.attr("fill", d => {
if (d.properties.tradeData) {
return colorScale(d.properties.tradeData.netTrade);
}
return "#e0e0e0";
})
.attr("d", path)
.attr("stroke", "#fff")
.attr("stroke-width", 0.5)
.attr("stroke-opacity", 0.8);
const top3Surplus = sortedCountries.slice(-3).map(d => d.country);
const top3Deficit = sortedCountries.slice(0, 3).map(d => d.country);
countryPaths.each(function(d) {
const country = d3.select(this);
if (d.properties.tradeData) {
const countryName = d.properties.tradeData.country;
if (countryName === "USA" || top3Surplus.includes(countryName) || top3Deficit.includes(countryName)) {
country.attr("stroke", "#000000")
.attr("stroke-width", 1.5)
.attr("stroke-opacity", 1);
}
}
});
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "24px")
.attr("font-weight", "bold")
.text("Global Import-Export Balance");
const legendGroup = svg.append("g")
.attr("transform", `translate(${width - margin.right + 50}, ${margin.top + 20})`);
legendGroup.append("text")
.attr("x", 0)
.attr("y", -15)
.attr("font-weight", "bold")
.attr("font-size", "16px")
.text("Trade Balance");
const legendHeight = 200;
const legendWidth = 20;
const legendEntries = colorScale.range().length;
const legendRectHeight = legendHeight / legendEntries;
colorScale.range().forEach((color, i) => {
legendGroup.append("rect")
.attr("x", 0)
.attr("y", i * legendRectHeight)
.attr("width", legendWidth)
.attr("height", legendRectHeight)
.style("fill", color);
});
legendGroup.append("text")
.attr("x", legendWidth + 10)
.attr("y", 0)
.attr("dominant-baseline", "hanging")
.attr("font-size", "12px")
.text("Trade Surplus");
legendGroup.append("text")
.attr("x", legendWidth + 10)
.attr("y", legendHeight / 2)
.attr("dominant-baseline", "middle")
.attr("font-size", "12px")
.text("Balanced");
legendGroup.append("text")
.attr("x", legendWidth + 10)
.attr("y", legendHeight)
.attr("dominant-baseline", "auto")
.attr("font-size", "12px")
.text("Trade Deficit");
const legendValues = [
{ pos: 0, label: colorScale.invertExtent(colorScale.range()[0])[1] / 1e12 },
{ pos: legendHeight / 2, label: 0 },
{ pos: legendHeight, label: colorScale.invertExtent(colorScale.range()[legendEntries-1])[0] / 1e12 }
];
legendValues.forEach(v => {
legendGroup.append("text")
.attr("x", legendWidth + 80)
.attr("y", v.pos)
.attr("dominant-baseline", v.pos === 0 ? "hanging" : (v.pos === legendHeight ? "auto" : "middle"))
.attr("font-size", "12px")
.text(`${v.label.toFixed(1)}T`);
});
const listsGroup = svg.append("g")
.attr("transform", `translate(${width - margin.right + 40}, ${margin.top + legendHeight + 80})`);
listsGroup.append("rect")
.attr("x", -10)
.attr("y", -20)
.attr("width", 220)
.attr("height", 330)
.attr("fill", "#ffffff")
.attr("stroke", "#cccccc")
.attr("stroke-width", 1)
.attr("rx", 5)
.attr("ry", 5);
listsGroup.append("text")
.attr("x", 100)
.attr("y", 0)
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "16px")
.text("Top 5 Trade Surplus");
const topSurplus = sortedCountries.slice(-5).reverse();
topSurplus.forEach((d, i) => {
listsGroup.append("text")
.attr("x", 10)
.attr("y", 30 + i * 25)
.attr("font-size", "14px")
.text(`${d.country}: ${formatTradeBalance(d.netTrade)}`);
});
listsGroup.append("text")
.attr("x", 100)
.attr("y", 170)
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "16px")
.text("Top 5 Trade Deficit");
const topDeficit = sortedCountries.slice(0, 5);
topDeficit.forEach((d, i) => {
listsGroup.append("text")
.attr("x", 10)
.attr("y", 200 + i * 25)
.attr("font-size", "14px")
.text(`${d.country}: ${formatTradeBalance(d.netTrade)}`);
});
const questionsGroup = svg.append("g")
.attr("transform", `translate(${margin.left}, ${height - margin.bottom + 10})`);
questionsGroup.append("rect")
.attr("x", -10)
.attr("y", -95)
.attr("width", width - margin.left - margin.right - 60)
.attr("height", 95)
.attr("fill", "rgba(255, 255, 255, 0.8)")
.attr("stroke", "#ddd")
.attr("rx", 5)
.attr("ry", 5);
questionsGroup.append("text")
.attr("x", 0)
.attr("y", -75)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("Q1: Which countries import more goods than they export?");
questionsGroup.append("text")
.attr("x", 15)
.attr("y", -55)
.attr("font-size", "13px")
.text("Countries in red shades have trade deficits (import > export). USA has the largest deficit.");
questionsGroup.append("text")
.attr("x", 0)
.attr("y", -35)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("Q2: Are the biggest importers all clustered in one area?");
questionsGroup.append("text")
.attr("x", 15)
.attr("y", -15)
.attr("font-size", "13px")
.text("Major importing countries are distributed globally, with concentrations in N. America, Europe, and E. Asia.");
svg.append("rect")
.attr("x", width / 2 - 300)
.attr("y", height - 40)
.attr("width", 600)
.attr("height", 30)
.attr("fill", "#f8f8f8")
.attr("stroke", "#ddd")
.attr("rx", 5)
.attr("ry", 5);
svg.append("text")
.attr("x", width / 2)
.attr("y", height - 20)
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.text("Color represents trade balance (exports - imports). Hover over countries for details.");
const tooltip = svg.append("g")
.attr("class", "tooltip")
.style("display", "none");
tooltip.append("rect")
.attr("width", 220)
.attr("height", 100)
.attr("fill", "white")
.attr("stroke", "#888")
.attr("rx", 5)
.attr("ry", 5)
.attr("opacity", 0.9);
const tooltipTitle = tooltip.append("text")
.attr("x", 10)
.attr("y", 25)
.attr("font-weight", "bold")
.attr("font-size", "14px");
const tooltipExports = tooltip.append("text")
.attr("x", 10)
.attr("y", 50)
.attr("font-size", "12px");
const tooltipImports = tooltip.append("text")
.attr("x", 10)
.attr("y", 70)
.attr("font-size", "12px");
const tooltipNetTrade = tooltip.append("text")
.attr("x", 10)
.attr("y", 90)
.attr("font-size", "12px")
.attr("font-weight", "bold");
countryPaths
.on("mouseover", function(event, d) {
const currentStroke = d3.select(this).attr("stroke");
const currentStrokeWidth = d3.select(this).attr("stroke-width");
const currentStrokeOpacity = d3.select(this).attr("stroke-opacity");
d3.select(this)
.attr("data-original-stroke", currentStroke)
.attr("data-original-stroke-width", currentStrokeWidth)
.attr("data-original-stroke-opacity", currentStrokeOpacity)
.attr("stroke", "#000")
.attr("stroke-width", 2)
.attr("stroke-opacity", 1);
if (d.properties.tradeData) {
const data = d.properties.tradeData;
let tooltipX = Math.min(Math.max(event.offsetX, 100), width - 240);
let tooltipY = Math.min(Math.max(event.offsetY - 120, 20), height - 120);
tooltip
.attr("transform", `translate(${tooltipX}, ${tooltipY})`)
.style("display", "block");
tooltipTitle.text(`${d.properties.NAME}`);
tooltipExports.text(`Exports: ${(data.exports / 1e12).toFixed(2)}T`);
tooltipImports.text(`Imports: ${(data.imports / 1e12).toFixed(2)}T`);
tooltipNetTrade.text(`Net Trade: ${formatTradeBalance(data.netTrade)}`);
}
})
.on("mousemove", function(event) {
let tooltipX = Math.min(Math.max(event.offsetX, 100), width - 240);
let tooltipY = Math.min(Math.max(event.offsetY - 120, 20), height - 120);
tooltip.attr("transform", `translate(${tooltipX}, ${tooltipY})`);
})
.on("mouseout", function() {
d3.select(this)
.attr("stroke", d3.select(this).attr("data-original-stroke"))
.attr("stroke-width", d3.select(this).attr("data-original-stroke-width"))
.attr("stroke-opacity", d3.select(this).attr("data-original-stroke-opacity"));
tooltip.style("display", "none");
})
.append("title")
.text(d => {
if (d.properties.tradeData) {
const data = d.properties.tradeData;
return `${d.properties.NAME} (${data.country})\nExports: ${(data.exports / 1e12).toFixed(2)}T\nImports: ${(data.imports / 1e12).toFixed(2)}T\nNet Trade: ${formatTradeBalance(data.netTrade)}`;
}
return `${d.properties.NAME}: No data`;
});
return svg.node();
}
Insert cell
{
const countryNeighbors = {
"Germany": ["France", "Netherlands", "Belgium", "Luxembourg", "Denmark", "Poland", "Czechia", "Austria", "Switzerland"],
"France": ["Spain", "Italy", "Switzerland", "Germany", "Belgium", "Luxembourg", "Andorra", "Monaco"],
"USA": ["Canada", "Mexico"],
"Canada": ["USA"],
"Mexico": ["USA", "Guatemala", "Belize"],
"China": ["Russian Federation", "Mongolia", "North Korea", "Vietnam", "Laos", "Myanmar", "India", "Bhutan", "Nepal", "Pakistan", "Kazakhstan", "Kyrgyzstan", "Tajikistan"],
"United Kingdom": ["Ireland"],
"Brazil": ["Argentina", "Paraguay", "Uruguay", "Bolivia (Plurinational State of)", "Peru", "Colombia", "Venezuela", "Guyana", "Suriname"],
"Russian Federation": ["Norway", "Finland", "Estonia", "Latvia", "Lithuania", "Poland", "Belarus", "Ukraine", "Georgia", "Azerbaijan", "Kazakhstan", "Mongolia", "China", "North Korea"],
"Japan": ["Rep. of Korea", "China", "Russian Federation"],
"Australia": ["Indonesia", "Papua New Guinea", "New Zealand"],
"India": ["Pakistan", "China", "Nepal", "Bhutan", "Bangladesh", "Myanmar"],
"South Africa": ["Namibia", "Botswana", "Zimbabwe", "Mozambique", "Eswatini", "Lesotho"],
"Poland": ["Germany", "Czechia", "Slovakia", "Ukraine", "Belarus", "Lithuania", "Russian Federation"],
"Netherlands": ["Germany", "Belgium"],
"Belgium": ["France", "Netherlands", "Germany", "Luxembourg"],
"Switzerland": ["France", "Germany", "Austria", "Liechtenstein", "Italy"],
"Italy": ["France", "Switzerland", "Austria", "Slovenia", "San Marino", "Vatican"],
"Spain": ["Portugal", "France", "Andorra", "Morocco"],
"Philippines": ["Malaysia", "Indonesia", "Vietnam"],
"Rep. of Korea": ["North Korea", "Japan", "China"],
"Singapore": ["Malaysia", "Indonesia", "Thailand"],
"Other Asia, nes": ["China", "India", "Japan"],
"Israel": ["Egypt", "Jordan", "Lebanon", "Syria", "State of Palestine"],
"Oman": ["United Arab Emirates", "Yemen", "Saudi Arabia"],
"Slovakia": ["Poland", "Czechia", "Austria", "Hungary", "Ukraine"],
"Indonesia": ["Malaysia", "Papua New Guinea", "Timor-Leste", "Philippines", "Singapore"],
"China, Hong Kong SAR": ["China"],
"Denmark": ["Germany", "Sweden"],
"Norway": ["Sweden", "Finland", "Russian Federation"],
"Portugal": ["Spain"],
"Bangladesh": ["India", "Myanmar"],
"Sweden": ["Norway", "Finland", "Denmark"],
"Qatar": ["Saudi Arabia", "United Arab Emirates", "Bahrain"],
"Hungary": ["Slovakia", "Ukraine", "Romania", "Serbia", "Croatia", "Slovenia", "Austria"],
"Finland": ["Sweden", "Norway", "Russian Federation"],
"Malaysia": ["Thailand", "Indonesia", "Brunei Darussalam", "Singapore"],
"Botswana": ["South Africa", "Namibia", "Zimbabwe", "Zambia"],
"Ireland": ["United Kingdom"],
"Thailand": ["Myanmar", "Laos", "Cambodia", "Malaysia"],
"Romania": ["Hungary", "Ukraine", "Moldova", "Bulgaria", "Serbia"],
"Ukraine": ["Belarus", "Russian Federation", "Poland", "Slovakia", "Hungary", "Romania", "Moldova"],
"Austria": ["Germany", "Czechia", "Slovakia", "Hungary", "Slovenia", "Italy", "Switzerland", "Liechtenstein"],
"China, Macao SAR": ["China"],
"Saudi Arabia": ["Jordan", "Iraq", "Kuwait", "Qatar", "United Arab Emirates", "Oman", "Yemen"],
"Argentina": ["Chile", "Bolivia (Plurinational State of)", "Paraguay", "Brazil", "Uruguay"],
"El Salvador": ["Guatemala", "Honduras"],
"Türkiye": ["Greece", "Bulgaria", "Georgia", "Armenia", "Azerbaijan", "Iran", "Iraq", "Syria"],
"Nigeria": ["Niger", "Chad", "Cameroon", "Benin"],
"Colombia": ["Panama", "Venezuela", "Brazil", "Peru", "Ecuador"],
"Algeria": ["Morocco", "Western Sahara", "Mauritania", "Mali", "Niger", "Libya", "Tunisia"],
"Viet Nam": ["China", "Laos", "Cambodia"],
"Venezuela": ["Colombia", "Brazil", "Guyana"],
"Gabon": ["Equatorial Guinea", "Cameroon", "Congo", "Dem. Rep. of the Congo"],
"Sri Lanka": ["India", "Maldives", "Indonesia"],
"Chile": ["Peru", "Bolivia (Plurinational State of)", "Argentina"],
"Belarus": ["Russian Federation", "Ukraine", "Poland", "Lithuania", "Latvia"],
"Costa Rica": ["Nicaragua", "Panama"],
"Trinidad and Tobago": ["Venezuela", "Guyana", "Grenada"],
"Bulgaria": ["Romania", "Serbia", "North Macedonia", "Greece", "Türkiye"],
"Syria": ["Türkiye", "Iraq", "Jordan", "Lebanon", "Israel"],
"New Zealand": ["Australia", "Fiji", "Tonga"],
"Greece": ["Albania", "North Macedonia", "Bulgaria", "Türkiye"],
"Brunei Darussalam": ["Malaysia", "Indonesia", "Philippines"],
"United Arab Emirates": ["Saudi Arabia", "Oman", "Qatar"],
"Azerbaijan": ["Russian Federation", "Georgia", "Armenia", "Türkiye", "Iran"],
"Lithuania": ["Latvia", "Belarus", "Poland", "Russian Federation"],
"Kazakhstan": ["Russian Federation", "China", "Kyrgyzstan", "Uzbekistan", "Turkmenistan"],
"Tunisia": ["Algeria", "Libya"],
"Dominican Rep.": ["Haiti", "Cuba", "Puerto Rico"],
"Pakistan": ["Iran", "Afghanistan", "China", "India"],
"Ecuador": ["Colombia", "Peru"],
"Sudan (...2011)": ["Egypt", "Libya", "Chad", "Central African Republic", "South Sudan", "Ethiopia", "Eritrea"],
"Morocco": ["Algeria", "Western Sahara", "Spain"],
"Jordan": ["Syria", "Iraq", "Saudi Arabia", "Israel", "State of Palestine"],
"Slovenia": ["Italy", "Austria", "Hungary", "Croatia"],
"Angola": ["Namibia", "Zambia", "Dem. Rep. of the Congo", "Congo"],
"Peru": ["Ecuador", "Colombia", "Brazil", "Bolivia (Plurinational State of)", "Chile"],
"Cambodia": ["Thailand", "Laos", "Viet Nam"],
"Luxembourg": ["Belgium", "France", "Germany"],
"Paraguay": ["Bolivia (Plurinational State of)", "Brazil", "Argentina"],
"Guatemala": ["Mexico", "Belize", "El Salvador", "Honduras"],
"Croatia": ["Slovenia", "Hungary", "Serbia", "Bosnia and Herzegovina", "Montenegro"],
"Côte d'Ivoire": ["Liberia", "Guinea", "Mali", "Burkina Faso", "Ghana"],
"Cuba": ["United States", "Mexico", "Haiti"],
"Yemen": ["Saudi Arabia", "Oman"],
"Bolivia (Plurinational State of)": ["Brazil", "Paraguay", "Argentina", "Chile", "Peru"],
"Zambia": ["Angola", "Dem. Rep. of the Congo", "Tanzania", "Malawi", "Mozambique", "Zimbabwe", "Botswana", "Namibia"],
"Estonia": ["Latvia", "Russian Federation", "Finland"],
"Serbia": ["Hungary", "Romania", "Bulgaria", "North Macedonia", "Kosovo", "Montenegro", "Bosnia and Herzegovina", "Croatia"],
"Egypt": ["Libya", "Sudan", "Israel", "State of Palestine"],
"Zimbabwe": ["Zambia", "Mozambique", "South Africa", "Botswana"],
"Panama": ["Costa Rica", "Colombia"],
"Libya": ["Tunisia", "Algeria", "Niger", "Chad", "Sudan", "Egypt"],
"Ghana": ["Côte d'Ivoire", "Burkina Faso", "Togo"],
"Congo": ["Gabon", "Cameroon", "Central African Republic", "Dem. Rep. of the Congo", "Angola"],
"State of Palestine": ["Israel", "Jordan", "Egypt"],
"Jamaica": ["Cuba", "Haiti", "Cayman Islands"],
"Iceland": ["Norway", "United Kingdom", "Denmark"],
"Iran": ["Türkiye", "Iraq", "Pakistan", "Afghanistan", "Turkmenistan", "Azerbaijan", "Armenia"],
"Czechia": ["Germany", "Poland", "Slovakia", "Austria"],
"Mali": ["Algeria", "Niger", "Burkina Faso", "Côte d'Ivoire", "Guinea", "Senegal", "Mauritania"],
"Kenya": ["Ethiopia", "Somalia", "South Sudan", "Uganda", "Tanzania"],
"Mozambique": ["Tanzania", "Malawi", "Zambia", "Zimbabwe", "South Africa", "Eswatini"],
"Kuwait": ["Iraq", "Saudi Arabia"],
"Bahrain": ["Saudi Arabia", "Qatar", "United Arab Emirates"],
"Seychelles": ["Madagascar", "Mauritius", "Tanzania"],
"Myanmar": ["China", "India", "Bangladesh", "Thailand", "Laos"],
"Nepal": ["India", "China"],
"Burkina Faso": ["Mali", "Niger", "Benin", "Togo", "Ghana", "Côte d'Ivoire"],
"Honduras": ["Guatemala", "El Salvador", "Nicaragua"],
"Papua New Guinea": ["Indonesia", "Australia", "Solomon Islands"],
"Lebanon": ["Syria", "Israel"],
"Malta": ["Italy", "Tunisia", "Libya"],
"Sudan": ["Egypt", "Libya", "Chad", "Central African Republic", "South Sudan", "Ethiopia", "Eritrea"],
"Ethiopia": ["Eritrea", "Djibouti", "Somalia", "Kenya", "South Sudan", "Sudan"],
"United Rep. of Tanzania": ["Kenya", "Uganda", "Rwanda", "Burundi", "Dem. Rep. of the Congo", "Zambia", "Malawi", "Mozambique"],
"Latvia": ["Estonia", "Lithuania", "Belarus", "Russian Federation"],
"Nicaragua": ["Honduras", "Costa Rica"],
"Mongolia": ["Russian Federation", "China"],
"Iraq": ["Türkiye", "Iran", "Kuwait", "Saudi Arabia", "Jordan", "Syria"],
"Cameroon": ["Nigeria", "Chad", "Central African Republic", "Congo", "Gabon", "Equatorial Guinea"],
"Dem. Rep. of the Congo": ["Congo", "Central African Republic", "South Sudan", "Uganda", "Rwanda", "Burundi", "Tanzania", "Zambia", "Angola"],
"Lao People's Dem. Rep.": ["China", "Myanmar", "Thailand", "Cambodia", "Viet Nam"],
"Uzbekistan": ["Kazakhstan", "Kyrgyzstan", "Tajikistan", "Afghanistan", "Turkmenistan"],
"North Macedonia": ["Kosovo", "Serbia", "Bulgaria", "Greece", "Albania"],
"Uruguay": ["Brazil", "Argentina"],
"Guyana": ["Venezuela", "Brazil", "Suriname"],
"Uganda": ["South Sudan", "Kenya", "Tanzania", "Rwanda", "Dem. Rep. of the Congo"],
"Mauritania": ["Western Sahara", "Algeria", "Mali", "Senegal", "Gambia"],
"Kyrgyzstan": ["Kazakhstan", "China", "Tajikistan", "Uzbekistan"],
"Suriname": ["Guyana", "Brazil", "French Guiana"],
"Armenia": ["Georgia", "Azerbaijan", "Iran", "Türkiye"],
"Cyprus": ["Turkey", "Syria", "Lebanon"],
"Gambia": ["Senegal", "Mauritania", "Guinea-Bissau"],
"Georgia": ["Russian Federation", "Azerbaijan", "Armenia", "Türkiye"]
};
function getNeighbors(country) {
return countryNeighbors[country] || [];
}
function analyzeTradeData() {
const countryData = [];
uniqueReporters.forEach(reporter => {
const neighbors = getNeighbors(reporter);
const imports = tradedata.filter(d => d.Reporter === reporter && d.ImportOrExport === "Import");
const exports = tradedata.filter(d => d.Reporter === reporter && d.ImportOrExport === "Export");
const totalImportValue = d3.sum(imports, d => +d.Value_USD);
const totalExportValue = d3.sum(exports, d => +d.Value_USD);
const importPartners = [...new Set(imports.map(d => d.Partner))];
const exportPartners = [...new Set(exports.map(d => d.Partner))];
let neighborImportValue = 0;
let neighborExportValue = 0;
const importNeighbors = [];
const exportNeighbors = [];
importPartners.forEach(partner => {
if (neighbors.includes(partner)) {
importNeighbors.push(partner);
neighborImportValue += d3.sum(imports.filter(d => d.Partner === partner), d => +d.Value_USD);
}
});
exportPartners.forEach(partner => {
if (neighbors.includes(partner)) {
exportNeighbors.push(partner);
neighborExportValue += d3.sum(exports.filter(d => d.Partner === partner), d => +d.Value_USD);
}
});
countryData.push({
country: reporter,
totalNeighbors: neighbors.length,
imports: {
total: totalImportValue,
neighbor: neighborImportValue,
neighborCount: importNeighbors.length,
neighborPercent: totalImportValue > 0 ? (neighborImportValue / totalImportValue) * 100 : 0,
neighbors: importNeighbors
},
exports: {
total: totalExportValue,
neighbor: neighborExportValue,
neighborCount: exportNeighbors.length,
neighborPercent: totalExportValue > 0 ? (neighborExportValue / totalExportValue) * 100 : 0,
neighbors: exportNeighbors
}
});
});
return countryData.sort((a, b) => {
if (b.totalNeighbors !== a.totalNeighbors) {
return b.totalNeighbors - a.totalNeighbors;
}
return a.country.localeCompare(b.country);
});
}
const countryData = analyzeTradeData();
const margin = {top: 60, right: 200, bottom: 80, left: 150};
const width = 960;
const displayCountries = countryData;
const height = Math.max(600, 60 + displayCountries.length * 20 + 80);
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "24px")
.text("Trade with Neighboring Countries vs. Total Trade");
const g = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
const y = d3.scaleBand()
.domain(displayCountries.map(d => d.country))
.range([0, innerHeight])
.padding(0.3);
const maxValue = d3.max(displayCountries, d =>
Math.max(d.imports.total, d.exports.total)
);
const x = d3.scaleLinear()
.domain([0, maxValue * 1.1])
.range([0, innerWidth]);
const colors = {
totalImport: "#ff9966",
totalExport: "#66b3ff",
neighborImport: "#cc5200",
neighborExport: "#0066cc"
};
g.append("g")
.call(d3.axisLeft(y))
.selectAll("text")
.attr("font-size", "8px");
g.append("g")
.attr("transform", `translate(0, ${innerHeight})`)
.call(d3.axisBottom(x).ticks(10)
.tickFormat(d => d3.format(".1f")(d / 1e12) + "T"))
.append("text")
.attr("x", innerWidth / 2)
.attr("y", 40)
.attr("fill", "#000")
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.text("Trade Value (Trillion USD)");
g.selectAll(".row-background")
.data(displayCountries)
.join("rect")
.attr("class", "row-background")
.attr("y", d => y(d.country))
.attr("x", 0)
.attr("width", innerWidth)
.attr("height", y.bandwidth())
.attr("fill", (d, i) => i % 2 === 0 ? "#f8f8f8" : "#ffffff")
.attr("opacity", 0.5);
g.selectAll(".bar-total-import")
.data(displayCountries)
.join("rect")
.attr("class", "bar-total-import")
.attr("y", d => y(d.country))
.attr("x", 0)
.attr("height", y.bandwidth() / 2)
.attr("width", d => x(d.imports.total))
.attr("fill", colors.totalImport)
.attr("stroke", "#fff")
.attr("stroke-width", 0.5);
g.selectAll(".bar-neighbor-import")
.data(displayCountries)
.join("rect")
.attr("class", "bar-neighbor-import")
.attr("y", d => y(d.country))
.attr("x", 0)
.attr("height", y.bandwidth() / 2)
.attr("width", d => x(d.imports.neighbor))
.attr("fill", colors.neighborImport)
.attr("stroke", "#fff")
.attr("stroke-width", 0.5);
g.selectAll(".bar-total-export")
.data(displayCountries)
.join("rect")
.attr("class", "bar-total-export")
.attr("y", d => y(d.country) + y.bandwidth() / 2)
.attr("x", 0)
.attr("height", y.bandwidth() / 2)
.attr("width", d => x(d.exports.total))
.attr("fill", colors.totalExport)
.attr("stroke", "#fff")
.attr("stroke-width", 0.5);
g.selectAll(".bar-neighbor-export")
.data(displayCountries)
.join("rect")
.attr("class", "bar-neighbor-export")
.attr("y", d => y(d.country) + y.bandwidth() / 2)
.attr("x", 0)
.attr("height", y.bandwidth() / 2)
.attr("width", d => x(d.exports.neighbor))
.attr("fill", colors.neighborExport)
.attr("stroke", "#fff")
.attr("stroke-width", 0.5);
g.selectAll(".label-neighbor-import")
.data(displayCountries.filter(d => d.imports.neighborPercent > 20))
.join("text")
.attr("class", "label-neighbor-import")
.attr("y", d => y(d.country) + y.bandwidth() / 4)
.attr("x", d => x(d.imports.neighbor) + 2)
.attr("dy", "0.35em")
.attr("font-size", "7px")
.attr("fill", "#000")
.text(d => `${Math.round(d.imports.neighborPercent)}%`);
g.selectAll(".label-neighbor-export")
.data(displayCountries.filter(d => d.exports.neighborPercent > 20))
.join("text")
.attr("class", "label-neighbor-export")
.attr("y", d => y(d.country) + y.bandwidth() * 3/4)
.attr("x", d => x(d.exports.neighbor) + 2)
.attr("dy", "0.35em")
.attr("font-size", "7px")
.attr("fill", "#000")
.text(d => `${Math.round(d.exports.neighborPercent)}%`);
const legend = svg.append("g")
.attr("transform", `translate(${width - margin.right + 20}, ${margin.top})`);
const legendItems = [
{ label: "Total Imports", color: colors.totalImport },
{ label: "Neighbor Imports", color: colors.neighborImport },
{ label: "Total Exports", color: colors.totalExport },
{ label: "Neighbor Exports", color: colors.neighborExport }
];
legend.selectAll("rect")
.data(legendItems)
.join("rect")
.attr("x", 0)
.attr("y", (d, i) => i * 25)
.attr("width", 15)
.attr("height", 15)
.attr("fill", d => d.color);
legend.selectAll("text")
.data(legendItems)
.join("text")
.attr("x", 25)
.attr("y", (d, i) => i * 25 + 12)
.text(d => d.label);
const explanation = svg.append("g")
.attr("transform", `translate(${width - margin.right + 10}, ${margin.top + 120})`);
explanation.append("text")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("Key Insights:");
const insights = [
"• For most countries, trade with neighbors",
" represents only a small fraction of their",
" total trade volume",
"",
"• Even countries with many neighbors tend",
" to trade more with distant partners than",
" with their immediate neighbors",
"",
"• Import and export patterns are generally",
" similar for each country, with some",
" exceptions in specific regions",
"",
"• The darker portions of the bars show the",
" percentage of trade with neighboring",
" countries - note how small this is for",
" many major economies"
];
explanation.selectAll(".insight")
.data(insights)
.join("text")
.attr("class", "insight")
.attr("x", 0)
.attr("y", (d, i) => 25 + i * 20)
.attr("font-size", "12px")
.text(d => d);
svg.append("g")
.attr("transform", `translate(${margin.left}, ${height - 45})`)
.append("text")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("Q1: Most countries trade much more with distant partners than with their immediate neighbors.");
svg.append("g")
.attr("transform", `translate(${margin.left}, ${height - 25})`)
.append("text")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("Q2: Import and export patterns with neighbors are similar, with only minor differences for most countries.");
const tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background-color", "white")
.style("border", "1px solid black")
.style("padding", "10px")
.style("border-radius", "5px")
.style("font-size", "12px")
.style("z-index", "10")
.style("max-width", "300px");
const allBars = g.selectAll(".bar-total-import, .bar-neighbor-import, .bar-total-export, .bar-neighbor-export");
allBars.on("mouseover", function(event, d) {
const bar = d3.select(this);
const isImport = bar.classed("bar-total-import") || bar.classed("bar-neighbor-import");
const isNeighbor = bar.classed("bar-neighbor-import") || bar.classed("bar-neighbor-export");
const tradeType = isImport ? "Import" : "Export";
const scopeType = isNeighbor ? "with neighbors" : "total";
const totalValue = isImport ? d.imports.total : d.exports.total;
const neighborValue = isImport ? d.imports.neighbor : d.exports.neighbor;
const percent = isImport ? d.imports.neighborPercent : d.exports.neighborPercent;
const neighbors = isImport ? d.imports.neighbors : d.exports.neighbors;
const formatValue = (val) => {
if (val >= 1e12) {
return (val / 1e12).toFixed(2) + " trillion USD";
} else if (val >= 1e9) {
return (val / 1e9).toFixed(2) + " billion USD";
} else {
return (val / 1e6).toFixed(2) + " million USD";
}
};
let content = `<strong>${d.country} ${tradeType}s</strong><br>`;
content += `Total ${tradeType}s: ${formatValue(totalValue)}<br>`;
content += `${tradeType}s with Neighbors: ${formatValue(neighborValue)}<br>`;
content += `Percentage with Neighbors: ${percent.toFixed(1)}%<br>`;
content += `<br>Trading with ${neighbors.length} of ${d.totalNeighbors} neighbors:<br>`;
content += neighbors.length > 0 ? neighbors.join(", ") : "None";
tooltip
.style("visibility", "visible")
.html(content)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px");
})
.on("mouseout", function() {
tooltip.style("visibility", "hidden");
});
return svg.node();
}
Insert cell
{
const topFiveGoods = (() => {
const goodsRollup = d3.rollups(
tradedata,
v => d3.sum(v, d => +d.Value_USD),
d => d.Description
);
goodsRollup.sort((a, b) => d3.descending(a[1], b[1]));
return goodsRollup.slice(0, 5).map(d => d[0]);
})();
const countryGoodMapper = (() => {
const filtered = tradedata.filter(d => topFiveGoods.includes(d.Description));
const roll = d3.rollups(
filtered,
v => d3.sum(v, d => +d.Value_USD),
d => d.Reporter,
d => d.Description,
d => d.ImportOrExport
);
const map = new Map();
for (const [country, goodsArray] of roll) {
for (const [desc, typeArray] of goodsArray) {
for (const [type, sumVal] of typeArray) {
const key = country + "||" + desc + "||" + type;
map.set(key, sumVal);
}
}
}
return map;
})();
const maxVal = d3.max(countryGoodMapper.values());
const uniqueTopGoodsReporters = [...new Set(
tradedata
.filter(d => topFiveGoods.includes(d.Description))
.map(d => d.Reporter)
)];
const width = 960;
const height = 550;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("display", "block")
.style("margin", "0 auto");
const projection = d3.geoNaturalEarth1().fitSize([width, height], geoJSON);
const path = d3.geoPath(projection);
const countries = svg.selectAll("path")
.data(geoJSON.features)
.join("path")
.attr("d", path)
.attr("fill", "#eee")
.attr("stroke", "#999")
.attr("stroke-width", 0.5);
const colorScale = d3.scaleSequential(d3.interpolateBlues).clamp(true);
colorScale.domain([0, maxVal]);
const container = d3.create("div");
const controlsDiv = d3.create("div")
.style("text-align", "center")
.style("margin-bottom", "20px");
controlsDiv.append("h2")
.text("Top 5 Types of Traded Goods by Country")
.style("margin-bottom", "5px");
controlsDiv.append("label")
.text("Select Goods Category: ")
.style("margin-right", "5px")
.style("font-weight", "bold");
const selectGoodsChk = controlsDiv.append("select")
.style("margin-right", "20px")
.style("padding", "5px")
.style("border-radius", "4px");
topFiveGoods.forEach(gd => {
selectGoodsChk.append("option").text(gd).attr("value", gd);
});
controlsDiv.append("label")
.text("Select Trade Type: ")
.style("margin-right", "5px")
.style("font-weight", "bold");
const selectType = controlsDiv.append("select")
.style("padding", "5px")
.style("border-radius", "4px");
["Export", "Import"].forEach(t => {
selectType.append("option").text(t).attr("value", t);
});
container.node().appendChild(controlsDiv.node());
container.node().appendChild(svg.node());
const rankingsDiv = d3.create("div")
.style("margin-top", "10px")
.style("padding", "10px")
.style("border", "1px solid #ddd")
.style("border-radius", "5px")
.style("background-color", "#f9f9f9");
container.node().appendChild(rankingsDiv.node());
function findTopCountries(good, tradeType, limit = 10) {
const countryValues = [];
uniqueTopGoodsReporters.forEach(country => {
const key = country + "||" + good + "||" + tradeType;
const value = countryGoodMapper.get(key) || 0;
if (value > 0) {
countryValues.push({ country, value });
}
});
return countryValues.sort((a, b) => b.value - a.value).slice(0, limit);
}
function updateMap() {
const chosenGood = selectGoodsChk.node().value;
const chosenType = selectType.node().value;
countries
.transition()
.duration(500)
.attr("fill", d => {
const geoName = d.properties.NAME || d.properties.ADMIN || d.properties.name;
const mappedName = reverseMapCountryName(geoName);
const key = mappedName + "||" + chosenGood + "||" + chosenType;
const val = countryGoodMapper.get(key) || 0;
return val > 0 ? colorScale(val) : "#eee";
});
const topCountries = findTopCountries(chosenGood, chosenType);
rankingsDiv.html("");
rankingsDiv.append("h3")
.text(`Top Countries for ${chosenType}ing ${chosenGood}`)
.style("margin-top", "5px");
const rankingsList = rankingsDiv.append("ol")
.style("padding-left", "25px");
topCountries.forEach(item => {
const formattedValue = formatValue(item.value);
rankingsList.append("li")
.text(`${item.country}: ${formattedValue}`);
});
updateAnalysis(chosenGood, chosenType, topCountries);
}
function formatValue(value) {
if (value >= 1e12) {
return (value / 1e12).toFixed(2) + " trillion USD";
} else if (value >= 1e9) {
return (value / 1e9).toFixed(2) + " billion USD";
} else {
return (value / 1e6).toFixed(2) + " million USD";
}
}
function reverseMapCountryName(geoName) {
return countryNameMapping[geoName] || geoName;
}
selectGoodsChk.on("change", updateMap);
selectType.on("change", updateMap);
const legendWidth = 250;
const legendHeight = 15;
const legendX = width - 270;
const legendY = height - 50;
const defs = svg.append("defs");
const gradient = defs.append("linearGradient")
.attr("id", "goodsGradient")
.attr("x1", "0%").attr("x2", "100%")
.attr("y1", "0%").attr("y2", "0%");
const n = 50;
for (let i = 0; i < n; i++) {
const t = i / (n - 1);
const val = t * maxVal;
gradient.append("stop")
.attr("offset", `${(t*100)}%`)
.attr("stop-color", colorScale(val));
}
const legendG = svg.append("g")
.attr("transform", `translate(${legendX},${legendY})`);
legendG.append("rect")
.attr("width", legendWidth)
.attr("height", legendHeight)
.style("fill", "url(#goodsGradient)");
const legendScale = d3.scaleLinear()
.domain([0, maxVal])
.range([0, legendWidth]);
const formatLegendValue = d3.format(".2s");
const legendAxis = d3.axisBottom(legendScale)
.ticks(5)
.tickFormat(d => {
if (d >= 1e12) {
return (d / 1e12).toFixed(1) + "T";
} else if (d >= 1e9) {
return (d / 1e9).toFixed(1) + "B";
} else if (d >= 1e6) {
return (d / 1e6).toFixed(1) + "M";
} else {
return d;
}
});
legendG.append("g")
.attr("transform", `translate(0, ${legendHeight})`)
.call(legendAxis);
legendG.append("text")
.attr("x", 0)
.attr("y", -5)
.attr("font-weight", "bold")
.text("Trade Value (USD)");
const analysisDiv = d3.create("div")
.style("margin-top", "20px")
.style("padding", "15px")
.style("border", "1px solid #ccc")
.style("border-radius", "5px")
.style("background-color", "#f5f5f5");
container.node().appendChild(analysisDiv.node());
function updateAnalysis(good, tradeType, topCountries) {
analysisDiv.html("");
analysisDiv.append("h3")
.text("Analysis")
.style("margin-top", "0");
const regionCounts = new Map();
topCountries.forEach(item => {
let region = getCountryRegion(item.country);
regionCounts.set(region, (regionCounts.get(region) || 0) + 1);
});
const mainRegions = Array.from(regionCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(d => d[0])
.join(", ");
analysisDiv.append("p")
.html(`<strong>Q1:</strong> The top countries ${tradeType.toLowerCase()}ing <strong>${good}</strong> are primarily located in <strong>${mainRegions}</strong>.`);
const isRegionallyClustered = Array.from(regionCounts.values()).some(count => count >= 3);
analysisDiv.append("p")
.html(`These main exporters are ${isRegionallyClustered ? "geographically clustered" : "distributed across different regions"}, suggesting that ${good} ${tradeType.toLowerCase()}s are ${isRegionallyClustered ? "regionally specialized" : "globally distributed"}.`);
analysisDiv.append("p")
.html(`<strong>Q2:</strong> Certain regions show distinct patterns in receiving these goods. For example, ${getRandomTopImporterInsight(good)}`);
}
function getCountryRegion(country) {
const regionMap = {
"USA": "North America",
"Canada": "North America",
"Mexico": "North America",
"Germany": "Europe",
"France": "Europe",
"United Kingdom": "Europe",
"Italy": "Europe",
"Spain": "Europe",
"Netherlands": "Europe",
"Switzerland": "Europe",
"Belgium": "Europe",
"Sweden": "Europe",
"Poland": "Europe",
"China": "Asia",
"Japan": "Asia",
"India": "Asia",
"Russian Federation": "Asia",
"Rep. of Korea": "Asia",
"Singapore": "Asia",
"Malaysia": "Asia",
"Indonesia": "Asia",
"Viet Nam": "Asia",
"Thailand": "Asia",
"Saudi Arabia": "Middle East",
"United Arab Emirates": "Middle East",
"Iran": "Middle East",
"Israel": "Middle East",
"Türkiye": "Middle East",
"Brazil": "South America",
"Argentina": "South America",
"Colombia": "South America",
"Chile": "South America",
"South Africa": "Africa",
"Nigeria": "Africa",
"Egypt": "Africa",
"Australia": "Oceania",
"New Zealand": "Oceania"
};
return regionMap[country] || "Other";
}
function getRandomTopImporterInsight(good) {
const insights = {
"Machinery, appliances, and tools": "Asian countries tend to import more machinery for manufacturing, while African nations import more finished tools and appliances.",
"Metals": "Developing economies in Asia and Africa tend to import more raw metals, while developed nations import more processed metal products.",
"Chemicals and pharmaceuticals": "European countries tend to be both major importers and exporters, creating regional trade networks, while developing nations primarily import these goods.",
"Textiles": "Western countries tend to import finished textile products, while manufacturing hubs in Asia import raw materials.",
"Minerals, stone, and their products": "Resource-poor countries in East Asia show higher import rates, while resource-rich countries in Africa and South America are primary exporters.",
"plastics and rubber": "Manufacturing hubs in Asia show strong import patterns for raw materials, while exporting finished goods.",
"Unspecified": "Trade patterns vary widely by region, with no clear geographic clustering for this category."
};
return insights[good] || "different regions show distinct specialization in both imports and exports of these goods";
}
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "20px")
.attr("font-weight", "bold")
.text("Global Distribution of Top 5 Traded Goods");
countries.append("title")
.text(d => {
const geoName = d.properties.NAME || d.properties.ADMIN || d.properties.name;
return geoName;
});
countries
.on("mouseover", function(event, d) {
const chosenGood = selectGoodsChk.node().value;
const chosenType = selectType.node().value;
const geoName = d.properties.NAME || d.properties.ADMIN || d.properties.name;
const mappedName = reverseMapCountryName(geoName);
const key = mappedName + "||" + chosenGood + "||" + chosenType;
const val = countryGoodMapper.get(key) || 0;
d3.select(this)
.attr("stroke", "#000")
.attr("stroke-width", 1.5);
const tooltip = svg.append("g")
.attr("class", "tooltip")
.attr("transform", `translate(${event.pageX - 100}, ${event.pageY - 100})`);
tooltip.append("rect")
.attr("width", 220)
.attr("height", 100)
.attr("fill", "white")
.attr("stroke", "#888")
.attr("rx", 5)
.attr("ry", 5)
.attr("opacity", 0.9);
tooltip.append("text")
.attr("x", 10)
.attr("y", 25)
.attr("font-weight", "bold")
.attr("font-size", "14px")
.text(`${geoName}`);
tooltip.append("text")
.attr("x", 10)
.attr("y", 50)
.attr("font-size", "12px")
.text(`${chosenGood} ${chosenType}: ${formatValue(val)}`);
})
.on("mouseout", function() {
d3.select(this)
.attr("stroke", "#999")
.attr("stroke-width", 0.5);
svg.selectAll(".tooltip").remove();
});
updateMap();
return container.node();
}
Insert cell
Insert cell
d3 = require('d3@7')
Insert cell
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