Public
Edited
May 10
Insert cell
Insert cell
Plot.plot({
projection: "equal-earth",
width,
marks: [
Plot.geo(land, { fill: "#000" }),
Plot.graticule(),
Plot.sphere()
]
})
Insert cell
land = topojson.feature(land50m, land50m.objects.land)
Insert cell
land50m = FileAttachment("land-50m.json").json()
Insert cell
tradedata = {
const data = await d3.csv(googlesheeturl, row => ({
country: row.country,
year: +row.year, // Assuming year is a number
food_insecurity_rate: +row.food_insecurity_rate, // Assuming these are numbers
severe_food_insec_rate: +row.severe_food_insec_rate,
undernourished_pct: +row.undernourished_pct,
low_food_access_pct: +row.low_food_access_pct,
food_outlet_density: +row.food_outlet_density,
gdp_per_capita: +row.gdp_per_capita,
poverty_rate: +row.poverty_rate,
income_class: row.income_class,
gini_index: +row.gini_index,
population: +row.population,
pop_under_15_pct: +row.pop_under_15_pct,
pop_over_65_pct: +row.pop_over_65_pct,
median_age: +row.median_age,
education_avg_yrs: +row.education_avg_yrs,
adult_literacy_rate: +row.adult_literacy_rate,
child_stunting_pct: +row.child_stunting_pct,
child_wasting_pct: +row.child_wasting_pct,
child_overweight_pct: +row.child_overweight_pct,
adult_obesity_pct: +row.adult_obesity_pct,
anaemia_women_pct: +row.anaemia_women_pct,
}));

// Add the original column names to the data object
data.columns = Object.keys(data[0]);

return data;
}
Insert cell
googlesheeturl = 'https://docs.google.com/spreadsheets/d/18nL3T6CKlu1vDPuVHAkaAc66wpRDrCnTurj-U1ch1sM/gviz/tq?tqx=out:csv'
Insert cell
// 1️⃣ Available countries
availableCountries = new Set(tradedata.map(d => d.country))

Insert cell
// 2️⃣ Organize data by country & year
dataByCountryYear = (() => {
const result = {};
tradedata.forEach(d => {
if (!result[d.country]) result[d.country] = {};
result[d.country][d.year] = d;
});
return result;
})();

Insert cell
// 3️⃣ Load world GeoJSON
worldGeoJSON = d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson")

Insert cell
// 4️⃣ Available years
years = Array.from(new Set(tradedata.map(d => d.year))).sort()

Insert cell
// 5️⃣ Year slider
viewof selectedYear = {
const min = d3.min(years), max = d3.max(years);
const defaultValue = years.includes(2020) ? 2020 : years[Math.floor(years.length/2)];
const form = html`<form style="font-family:sans-serif;font-size:.9em;">
<label style="width:700px;display:inline-block">
Year: <span style="font-weight:bold">${defaultValue}</span>
<input type="range" min=${min} max=${max} value=${defaultValue} step="1" style="width:100%">
</label>
</form>`;
const input = form.querySelector("input"), output = form.querySelector("span");
input.oninput = () => {
output.textContent = input.value;
form.value = +input.value;
form.dispatchEvent(new Event("input"));
};
form.value = +input.value;
return form;
}

Insert cell
// 6️⃣ Factor radio buttons
viewof selectedFactor = {
const options = [
["food_insecurity_rate","Food Insecurity","#e41a1c"],
["income_class","Income Level","#984ea3"],
["education_avg_yrs","Education","#4daf4a"],
["median_age","Age","#ff7f00"]
];
const form = html`<form style="font-family:sans-serif;font-size:.9em;margin-bottom:10px;">
${options.map(([v,label,color]) => html`<label style="margin-right:15px">
<input type="radio" name="factor" value=${v} ${v==="food_insecurity_rate"?"checked":""}>
<span style="color:${color}">${label}</span>
</label>`)}
</form>`;
form.querySelectorAll("input").forEach(input => {
input.onclick = () => {
form.value = input.value;
form.dispatchEvent(new Event("input"));
};
});
form.value = "food_insecurity_rate";
return form;
}

Insert cell
// 7️⃣ Country‐code mapping
countryCodeMap = (() => {
const mapping = new Map([
["USA","USA"],
["CAN","CAN"],
["MEX","MEX"]
// add manual overrides here
]);
tradedata.forEach(d => {
if (!mapping.has(d.country)) mapping.set(d.country, d.country);
});
return mapping;
})();

Insert cell
// A Set of every country key in your CSV (ISO3 codes)
countryKeySet = new Set(tradedata.map(d => d.country.trim()));

Insert cell
map = {
const world = await worldGeoJSON;
const width = 900, height = 500;
const svg = d3.create("svg")
.attr("viewBox",[0,0,width,height])
.style("max-width","100%").style("height","auto");

const projection = d3.geoNaturalEarth1()
.fitSize([width, height], world);
const path = d3.geoPath(projection);

// your existing color scales…
const foodScale = d3.scaleLinear().domain([0,10,20,30,50]).range(["#fff5f0","#fdcab5","#fc8a6a","#e34a33","#b30000"]).clamp(true);
const incomeScale = d3.scaleOrdinal().domain(["Low","Lower Middle","Upper Middle","High"]).range(["#edf8fb","#b3cde3","#8c96c6","#88419d"]);
const eduScale = d3.scaleLinear().domain([0,5,10,15]).range(["#f7fcf5","#c7e9c0","#74c476","#238b45"]).clamp(true);
const ageScale = d3.scaleLinear().domain([15,25,35,45]).range(["#feedde","#fdbe85","#fd8d3c","#d94701"]).clamp(true);

const tooltip = d3.select(document.body).append("div.tooltip")
.style("position","absolute")
.style("visibility","hidden")
.style("background","white")
.style("border","1px solid #ddd")
.style("border-radius","5px")
.style("padding","10px")
.style("font-size","12px")
.style("pointer-events","none");

svg.selectAll("path")
.data(world.features)
.join("path")
.attr("d", path)
.attr("stroke", "#ccc")
.attr("stroke-width", 0.5)
.attr("fill", d => {
// pick ISO3 if in your CSV, otherwise fallback to full country name
const key = countryKeySet.has(d.properties.iso_a3)
? d.properties.iso_a3
: d.properties.name;
const row = (dataByCountryYear[key] || {})[selectedYear];
if (!row) return "#eee";
switch (selectedFactor) {
case "food_insecurity_rate": return foodScale(row.food_insecurity_rate);
case "income_class": return incomeScale(row.income_class);
case "education_avg_yrs": return eduScale(row.education_avg_yrs);
case "median_age": return ageScale(row.median_age);
default: return "#eee";
}
})
.on("mouseover", (event,d) => {
const key = countryKeySet.has(d.properties.iso_a3)
? d.properties.iso_a3
: d.properties.name;
const row = (dataByCountryYear[key] || {})[selectedYear];
if (!row) return;
tooltip.style("visibility","visible").html(`
<strong>${d.properties.name}</strong><br>
Food Insecurity: ${row.food_insecurity_rate.toFixed(1)}%<br>
Income Class: ${row.income_class}<br>
Education: ${row.education_avg_yrs.toFixed(1)} yrs<br>
Median Age: ${row.median_age.toFixed(1)} yrs<br>
GDP per Capita: $${row.gdp_per_capita.toFixed(0)}<br>
Poverty Rate: ${row.poverty_rate.toFixed(1)}%
`);
})
.on("mousemove", e =>
tooltip.style("top", (e.pageY + 10) + "px")
.style("left", (e.pageX + 10) + "px")
)
.on("mouseout", () =>
tooltip.style("visibility","hidden")
);

return svg.node();
}

Insert cell
// 9️⃣ Country‐level dual‐axis chart (USA)
countryAnalysis = {
const width = 900, height = 350;
const margin = {top:40,right:120,bottom:40,left:50};
const svg = d3.create("svg")
.attr("viewBox",[0,0,width,height])
.style("max-width","100%").style("height","auto");

const usaData = tradedata.filter(d=>d.country==="USA").sort((a,b)=>a.year-b.year);
const x = d3.scaleLinear().domain(d3.extent(usaData,d=>d.year)).range([margin.left,width-margin.right]);
const y1 = d3.scaleLinear().domain([0,d3.max(usaData,d=>d.food_insecurity_rate)*1.1]).nice().range([height-margin.bottom,margin.top]);
const y2 = d3.scaleLinear().domain([0,d3.max(usaData,d=>d.education_avg_yrs)*1.1]).nice().range([height-margin.bottom,margin.top]);

svg.append("g").attr("transform",`translate(0,${height-margin.bottom})`)
.call(d3.axisBottom(x).ticks(usaData.length).tickFormat(d=>d)).selectAll("text").attr("font-size","10px");
svg.append("g").attr("transform",`translate(${margin.left},0)`)
.call(d3.axisLeft(y1).ticks(5).tickFormat(d=>d+"%"))
.call(g=>g.selectAll("text").attr("fill","#e41a1c"))
.call(g=>g.selectAll("line").attr("stroke","#e41a1c"));
svg.append("g").attr("transform",`translate(${width-margin.right},0)`)
.call(d3.axisRight(y2).ticks(5))
.call(g=>g.selectAll("text").attr("fill","#4daf4a"))
.call(g=>g.selectAll("line").attr("stroke","#4daf4a"));

const line1 = d3.line().x(d=>x(d.year)).y(d=>y1(d.food_insecurity_rate));
const line2 = d3.line().x(d=>x(d.year)).y(d=>y2(d.education_avg_yrs));

svg.append("path").datum(usaData).attr("fill","none").attr("stroke","#e41a1c").attr("stroke-width",2).attr("d",line1);
svg.append("path").datum(usaData).attr("fill","none").attr("stroke","#4daf4a").attr("stroke-width",2).attr("d",line2);

svg.selectAll(".dot1").data(usaData).join("circle")
.attr("cx",d=>x(d.year)).attr("cy",d=>y1(d.food_insecurity_rate)).attr("r",4).attr("fill","#e41a1c");
svg.selectAll(".dot2").data(usaData).join("circle")
.attr("cx",d=>x(d.year)).attr("cy",d=>y2(d.education_avg_yrs)).attr("r",4).attr("fill","#4daf4a");

// legend
svg.append("circle").attr("cx",width-margin.right+15).attr("cy",margin.top+10).attr("r",4).style("fill","#e41a1c");
svg.append("text").attr("x",width-margin.right+25).attr("y",margin.top+10).attr("alignment-baseline","middle")
.style("fill","#e41a1c").style("font-size","12px").text("Food Insecurity Rate (%)");
svg.append("circle").attr("cx",width-margin.right+15).attr("cy",margin.top+30).attr("r",4).style("fill","#4daf4a");
svg.append("text").attr("x",width-margin.right+25).attr("y",margin.top+30).attr("alignment-baseline","middle")
.style("fill","#4daf4a").style("font-size","12px").text("Education (Avg. Years)");

svg.append("text").attr("x",width/2).attr("y",20).attr("text-anchor","middle")
.attr("font-size","16px").attr("font-weight","bold")
.text("USA Food Insecurity & Education Trends (2015–2025)");
return svg.node();
}

Insert cell
// Debug: which country names in tradedata never match any feature.properties.name?
missingCountries = {
const world = await worldGeoJSON;
const geoNames = new Set(world.features.map(f => f.properties.name.trim()));
const dataNames = new Set(tradedata.map(d => d.country.trim()));
return [...dataNames].filter(name => !geoNames.has(name));
}

Insert cell
{
// Parse the dataset from text format
function parseDataset(content) {
const objectRegex = /\{[^{}]*\}/g;
const matches = content.match(objectRegex);
if (!matches) return [];
return matches.map(objString => {
const keyValuePairs = {};
const keyValueRegex = /(\w+):\s*("[^"]*"|\d+\.\d+|\d+|true|false)/g;
let match;
while ((match = keyValueRegex.exec(objString)) !== null) {
const key = match[1];
let value = match[2];
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
} else if (!isNaN(parseFloat(value))) {
value = parseFloat(value);
}
keyValuePairs[key] = value;
}
return keyValuePairs;
});
}
// Map countries to continents
const continentMapping = {
'USA': 'North America',
'CAN': 'North America',
'MEX': 'North America',
'BRA': 'South America'
};
// Add continent to each data point
const dataWithContinent = tradedata.map(item => ({
...item,
continent: continentMapping[item.country] || 'Unknown'
}));
// Get unique years and continents
const years = [...new Set(dataWithContinent.map(item => item.year))].sort();
const continents = [...new Set(dataWithContinent.map(item => item.continent))];
// Education and age classification functions
function getEducationCategory(years) {
if (years < 6) return "Low";
if (years < 10) return "Medium";
return "High";
}

function getAgeCategory(age) {
if (age < 25) return "Young";
if (age < 40) return "Middle-aged";
return "Older";
}
// Process data by year and continent
const processedDataByYearAndContinent = {};
years.forEach(year => {
processedDataByYearAndContinent[year] = {};
// Process for each continent
continents.forEach(continent => {
const continentYearData = dataWithContinent.filter(
item => item.year === year && item.continent === continent
);
if (continentYearData.length === 0) return;
// Group by income class
const groupedByIncome = {};
continentYearData.forEach(item => {
if (!groupedByIncome[item.income_class]) {
groupedByIncome[item.income_class] = [];
}
groupedByIncome[item.income_class].push(item);
});
// Calculate average metrics by income class
const processedData = Object.keys(groupedByIncome).map(incomeClass => {
const items = groupedByIncome[incomeClass];
const avgFoodInsecurity = items.reduce((sum, item) => sum + item.food_insecurity_rate, 0) / items.length;
const avgEducation = items.reduce((sum, item) => sum + item.education_avg_yrs, 0) / items.length;
const avgAge = items.reduce((sum, item) => sum + item.median_age, 0) / items.length;
return {
name: incomeClass,
value: avgFoodInsecurity,
avgEducation,
avgAge,
countryCount: items.length,
countries: items.map(item => item.country).join(', '),
continent
};
});
processedDataByYearAndContinent[year][continent] = processedData;
});
// Add "All" continents combined data
const allContinentsData = {};
Object.values(processedDataByYearAndContinent[year]).forEach(continentData => {
continentData.forEach(item => {
if (!allContinentsData[item.name]) {
allContinentsData[item.name] = { ...item, countryCount: 0, countries: '' };
} else {
const currentCount = allContinentsData[item.name].countryCount;
const newCount = currentCount + item.countryCount;
allContinentsData[item.name].value =
(allContinentsData[item.name].value * currentCount + item.value * item.countryCount) / newCount;
allContinentsData[item.name].avgEducation =
(allContinentsData[item.name].avgEducation * currentCount + item.avgEducation * item.countryCount) / newCount;
allContinentsData[item.name].avgAge =
(allContinentsData[item.name].avgAge * currentCount + item.avgAge * item.countryCount) / newCount;
allContinentsData[item.name].countryCount = newCount;
allContinentsData[item.name].countries =
allContinentsData[item.name].countries
? allContinentsData[item.name].countries + ', ' + item.countries
: item.countries;
}
});
});
processedDataByYearAndContinent[year]['All'] = Object.values(allContinentsData);
});
// Set up chart dimensions
const margin = {top: 50, right: 150, bottom: 70, left: 50};
const width = 700;
const height = 550;
const visWidth = width - margin.left - margin.right;
const visHeight = height - margin.top - margin.bottom;
// The radius of the pie chart
const radius = Math.min(visWidth, visHeight) / 2;
// Create SVG element
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);
// Add chart group translated to center
const g = svg.append('g')
.attr('transform', `translate(${margin.left + visWidth/2}, ${margin.top + visHeight/2})`);
// Color scale for income classes
const colorScale = {
'Low': '#FF8042',
'Lower Middle': '#FFBB28',
'Upper Middle': '#00C49F',
'High': '#0088FE'
};
// Create arc generator
const arc = d3.arc()
.innerRadius(radius * 0.5) // Donut chart
.outerRadius(radius);
// Create pie layout
const pie = d3.pie()
.sort(null)
.value(d => d.value);
// Get input parameters
const selectedYear = inputYear || years[years.length - 2]; // Use input or default to 2024
const selectedContinent = inputContinent || 'All'; // Use input or default to 'All'
// Get data for the selected year and continent
const chartData = processedDataByYearAndContinent[selectedYear][selectedContinent] || [];
// Add arcs to the pie chart
const arcs = g.selectAll('path')
.data(pie(chartData))
.join('path')
.attr('d', arc)
.attr('fill', d => colorScale[d.data.name] || '#ccc')
.attr('stroke', 'white')
.attr('stroke-width', 2)
.attr('opacity', 0.85)
.on('mouseover', function(event, d) {
// Highlight the segment
d3.select(this)
.attr('opacity', 1)
.attr('stroke', '#333')
.attr('stroke-width', 3);
// Show tooltip
const data = d.data;
const educationCategory = getEducationCategory(data.avgEducation);
const ageCategory = getAgeCategory(data.avgAge);
const tooltip = d3.select('#tooltip')
.style('visibility', 'visible')
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px');
tooltip.select('#tooltip-content').html(`
<div style="font-weight: bold; font-size: 16px; margin-bottom: 8px;">${data.name} Income Class</div>
<div style="margin-bottom: 5px;"><span style="font-weight: 600;">Food Insecurity Rate:</span> ${data.value.toFixed(2)}%</div>
<div style="margin-bottom: 5px;"><span style="font-weight: 600;">Education:</span> ${data.avgEducation.toFixed(1)} years (${educationCategory})</div>
<div style="margin-bottom: 5px;"><span style="font-weight: 600;">Median Age:</span> ${data.avgAge.toFixed(1)} (${ageCategory})</div>
<div style="margin-bottom: 5px;"><span style="font-weight: 600;">Countries:</span> ${data.countryCount} (${data.countries})</div>
<div style="margin-top: 10px; padding-top: 8px; border-top: 1px solid #eee; font-size: 12px; color: #666;">
Insights: ${educationCategory} education + ${ageCategory} population
${data.value > 20 ? " = Higher food insecurity risk" :
data.value > 10 ? " = Moderate food insecurity risk" :
" = Lower food insecurity risk"}
</div>
`);
})
.on('mouseout', function() {
// Reset segment appearance
d3.select(this)
.attr('opacity', 0.85)
.attr('stroke', 'white')
.attr('stroke-width', 2);
// Hide tooltip
d3.select('#tooltip')
.style('visibility', 'hidden');
});
// Add labels inside the pie segments if there's enough space
g.selectAll('text')
.data(pie(chartData))
.join('text')
.attr('transform', d => {
const [x, y] = arc.centroid(d);
return `translate(${x}, ${y})`;
})
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.attr('fill', 'white')
.text(d => d.data.value > 8 ? `${d.data.name}` : '');
// Add title
svg.append('text')
.attr('x', width / 2)
.attr('y', 30)
.attr('text-anchor', 'middle')
.attr('font-size', '20px')
.attr('font-weight', 'bold')
.text('Food Insecurity by Demographics');
// Add subtitle
svg.append('text')
.attr('x', width / 2)
.attr('y', 50)
.attr('text-anchor', 'middle')
.attr('font-size', '14px')
.text(`Year: ${selectedYear} | Continent: ${selectedContinent}`);
// Add legend
const legend = svg.append('g')
.attr('transform', `translate(${width - margin.right + 20}, ${margin.top + 50})`);
const legendData = Object.entries(colorScale).map(([name, color]) => ({ name, color }));
legendData.forEach((d, i) => {
const legendItem = legend.append('g')
.attr('transform', `translate(0, ${i * 25})`);
legendItem.append('rect')
.attr('width', 15)
.attr('height', 15)
.attr('fill', d.color);
legendItem.append('text')
.attr('x', 25)
.attr('y', 12)
.text(d.name);
});
// Add explanatory text
svg.append('text')
.attr('x', width / 2)
.attr('y', height - 30)
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.text('Pie segments size represents the average food insecurity rate for each income class');
svg.append('text')
.attr('x', width / 2)
.attr('y', height - 15)
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.text('Hover over segments to see details about education and age demographics');
// Create tooltip element if it doesn't exist
if (!document.getElementById('tooltip')) {
const tooltipDiv = document.createElement('div');
tooltipDiv.id = 'tooltip';
tooltipDiv.style.position = 'absolute';
tooltipDiv.style.visibility = 'hidden';
tooltipDiv.style.backgroundColor = 'white';
tooltipDiv.style.borderRadius = '5px';
tooltipDiv.style.padding = '10px';
tooltipDiv.style.boxShadow = '0 0 10px rgba(0,0,0,0.25)';
tooltipDiv.style.pointerEvents = 'none';
tooltipDiv.style.maxWidth = '300px';
tooltipDiv.style.zIndex = '1000';
const contentDiv = document.createElement('div');
contentDiv.id = 'tooltip-content';
tooltipDiv.appendChild(contentDiv);
document.body.appendChild(tooltipDiv);
}
return svg.node();
}
Insert cell
viewof inputYear = {
const form = html`<form style="display: flex; align-items: center; gap: 8px;">
<label>Year: <select name="year"></select></label>
</form>`;
const years = [2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025];
const select = form.querySelector("select");
years.forEach(year => {
const option = document.createElement("option");
option.value = year;
option.textContent = year;
if (year === 2020) option.selected = true;
select.appendChild(option);
});
form.value = select.value;
select.addEventListener("input", () => {
form.value = select.value;
form.dispatchEvent(new Event("input"));
});
return form;
}
Insert cell
viewof inputContinent = {
const form = html`<form style="display: flex; align-items: center; gap: 8px;">
<label>Continent: <select name="continent"></select></label>
</form>`;
const continents = ["All", "North America", "South America"];
const select = form.querySelector("select");
continents.forEach(continent => {
const option = document.createElement("option");
option.value = continent;
option.textContent = continent;
if (continent === "All") option.selected = true;
select.appendChild(option);
});
form.value = select.value;
select.addEventListener("input", () => {
form.value = select.value;
form.dispatchEvent(new Event("input"));
});
return form;
}
Insert cell
viewof selectedYear2 = Inputs.select(years, {value: "2020", label: "Select Year"})

Insert cell
viewof selectedContinent = Inputs.select(["All", "North America", "South America"], {value: "All", label: "Select Continent"})
Insert cell
{
const continentMapping = {
'USA': 'North America', 'CAN': 'North America', 'MEX': 'North America',
'GTM': 'North America', 'HND': 'North America', 'SLV': 'North America',
'CRI': 'North America', 'PAN': 'North America', 'CUB': 'North America',
'DOM': 'North America', 'HTI': 'North America', 'JAM': 'North America',
'TTO': 'North America',
'BRA': 'South America', 'ARG': 'South America', 'COL': 'South America',
'CHL': 'South America', 'PER': 'South America', 'VEN': 'South America',
'ECU': 'South America', 'BOL': 'South America', 'PRY': 'South America',
'URY': 'South America', 'GUY': 'South America', 'SUR': 'South America',
'GBR': 'Europe', 'FRA': 'Europe', 'DEU': 'Europe', 'ITA': 'Europe',
'ESP': 'Europe', 'NOR': 'Europe', 'SWE': 'Europe', 'FIN': 'Europe',
'DNK': 'Europe', 'ISL': 'Europe', 'CHE': 'Europe', 'AUT': 'Europe',
'BEL': 'Europe', 'NLD': 'Europe', 'LUX': 'Europe', 'PRT': 'Europe',
'GRC': 'Europe', 'POL': 'Europe', 'CZE': 'Europe', 'SVK': 'Europe',
'HUN': 'Europe', 'ROU': 'Europe', 'BGR': 'Europe', 'SRB': 'Europe',
'HRV': 'Europe', 'SVN': 'Europe', 'ALB': 'Europe', 'MKD': 'Europe',
'BIH': 'Europe', 'MNE': 'Europe', 'KOS': 'Europe', 'UKR': 'Europe',
'BLR': 'Europe', 'MDA': 'Europe',
'RUS': 'Asia', 'KAZ': 'Asia', 'UZB': 'Asia', 'TKM': 'Asia',
'KGZ': 'Asia', 'TJK': 'Asia', 'GEO': 'Asia', 'ARM': 'Asia',
'AZE': 'Asia', 'TUR': 'Asia', 'IRN': 'Asia', 'IRQ': 'Asia',
'SAU': 'Asia', 'YEM': 'Asia', 'SYR': 'Asia', 'JOR': 'Asia',
'ARE': 'Asia', 'QAT': 'Asia', 'KWT': 'Asia', 'BHR': 'Asia',
'OMN': 'Asia', 'ISR': 'Asia', 'LBN': 'Asia', 'PSE': 'Asia',
'CHN': 'Asia', 'JPN': 'Asia', 'IND': 'Asia', 'IDN': 'Asia',
'KOR': 'Asia', 'PAK': 'Asia', 'BGD': 'Asia', 'PHL': 'Asia',
'VNM': 'Asia', 'THA': 'Asia', 'MYS': 'Asia', 'SGP': 'Asia',
'MMR': 'Asia', 'NPL': 'Asia', 'LKA': 'Asia', 'KHM': 'Asia',
'LAO': 'Asia', 'MNG': 'Asia', 'BTN': 'Asia', 'MDV': 'Asia',
'ZAF': 'Africa', 'EGY': 'Africa', 'NGA': 'Africa', 'KEN': 'Africa',
'ETH': 'Africa', 'TZA': 'Africa', 'DZA': 'Africa', 'MAR': 'Africa',
'LBY': 'Africa', 'TUN': 'Africa', 'GHA': 'Africa', 'ZWE': 'Africa',
'ZMB': 'Africa', 'AGO': 'Africa', 'MOZ': 'Africa', 'UGA': 'Africa',
'CMR': 'Africa', 'CIV': 'Africa', 'SDN': 'Africa', 'SSD': 'Africa',
'SEN': 'Africa', 'MLI': 'Africa', 'BFA': 'Africa', 'NER': 'Africa',
'TCD': 'Africa', 'SOM': 'Africa', 'RWA': 'Africa', 'BDI': 'Africa',
'BEN': 'Africa', 'TGO': 'Africa', 'MWI': 'Africa', 'ERI': 'Africa',
'GAB': 'Africa', 'GNQ': 'Africa', 'GIN': 'Africa', 'SLE': 'Africa',
'LBR': 'Africa', 'COG': 'Africa', 'COD': 'Africa', 'DJI': 'Africa',
'COM': 'Africa', 'CPV': 'Africa', 'STP': 'Africa', 'MDG': 'Africa',
'MUS': 'Africa', 'SYC': 'Africa',
'AUS': 'Oceania', 'NZL': 'Oceania', 'PNG': 'Oceania', 'FJI': 'Oceania'
};

const dataWithContinent = tradedata.map(d => ({
...d,
continent: continentMapping[d.country] || 'Unknown'
}));

const years = [...new Set(dataWithContinent.map(d => d.year))].sort();
const continentsList = ['All', ...[...new Set(dataWithContinent.map(d => d.continent))].filter(d => d !== 'Unknown')];

const getEducationCategory = y => y < 6 ? 'Low' : y < 10 ? 'Medium' : 'High';
const getAgeCategory = a => a < 25 ? 'Young' : a < 40 ? 'Middle-aged' : 'Older';

const processedByYearContinent = {};
years.forEach(year => {
processedByYearContinent[year] = {};
continentsList.forEach(cont => {
if (cont === 'All') return;
const rows = dataWithContinent.filter(r => r.year === year && r.continent === cont);
if (!rows.length) return;
const byIncome = d3.group(rows, r => r.income_class);
processedByYearContinent[year][cont] = Array.from(byIncome, ([cls, arr]) => ({
name: cls,
value: d3.mean(arr, d => d.food_insecurity_rate),
avgEducation: d3.mean(arr, d => d.education_avg_yrs),
avgAge: d3.mean(arr, d => d.median_age),
countryCount: arr.length,
countries: arr.map(d => d.country).join(', '),
continent: cont
}));
});

const merged = {};
Object.values(processedByYearContinent[year]).forEach(list => {
list.forEach(d => {
if (!merged[d.name]) merged[d.name] = { ...d };
else {
const c = merged[d.name].countryCount + d.countryCount;
merged[d.name].value = (merged[d.name].value * merged[d.name].countryCount + d.value * d.countryCount) / c;
merged[d.name].avgEducation = (merged[d.name].avgEducation * merged[d.name].countryCount + d.avgEducation * d.countryCount) / c;
merged[d.name].avgAge = (merged[d.name].avgAge * merged[d.name].countryCount + d.avgAge * d.countryCount) / c;
merged[d.name].countries += `, ${d.countries}`;
merged[d.name].countryCount = c;
}
});
});
processedByYearContinent[year]['All'] = Object.values(merged);
});

// Main container
const container = html`<div style="display:flex;flex-direction:column;align-items:center;font-family:sans-serif;width:100%;"></div>`;

// Title
const titleDiv = html`<div style="text-align:center;margin-bottom:20px;">
<h2 style="margin-bottom:5px;">Food Insecurity Distribution</h2>
<div class="subtitle-container" style="font-size:14px;color:#555;"></div>
</div>`;
container.appendChild(titleDiv);

// Store subtitle container reference
const subtitleContainer = titleDiv.querySelector('.subtitle-container');

// Controls row
const controlsRow = html`<div style="display:flex;justify-content:center;gap:30px;margin-bottom:20px;"></div>`;
container.appendChild(controlsRow);

// Year control
const yearControl = html`<div>
<label style="font-weight:bold;">Year:</label>
<select id="yearSelect" style="padding:4px;border-radius:4px;margin-left:5px;"></select>
</div>`;
controlsRow.appendChild(yearControl);

const yearSelect = yearControl.querySelector('#yearSelect');
years.forEach(y => {
yearSelect.appendChild(html`<option value="${y}" ${y===2020?'selected':''}>${y}</option>`);
});

// Continent control
const continentControl = html`<div>
<label style="font-weight:bold;">Continent:</label>
<select id="contSelect" style="padding:4px;border-radius:4px;margin-left:5px;"></select>
</div>`;
controlsRow.appendChild(continentControl);

const contSelect = continentControl.querySelector('#contSelect');
continentsList.forEach(c => contSelect.appendChild(html`<option value="${c}">${c}</option>`));

// Chart container
const chartContainer = html`<div style="width:700px;height:700px;position:relative;"></div>`;
container.appendChild(chartContainer);

// Create SVG
const width = 700;
const height = 700;
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', `0 0 ${width} ${height}`)
.style('max-width', '100%')
.style('height', 'auto');
chartContainer.appendChild(svg.node());

// Add defs for patterns
const defs = svg.append('defs');

// Color palette inspired by your image
const colorMapping = {
'Low': '#5F4B8B', // Deep Purple
'Lower Middle': '#E69873', // Peach/Orange
'Upper Middle': '#76A5AF', // Teal
'High': '#815839' // Brown
};

// Additional colors for patterns
const extendedColors = [
'#9B7ED9', // Light Purple
'#D473A2', // Pink
'#2E0854', // Dark Purple
'#998476', // Gray-Brown
'#4D7B85', // Dark Teal
'#EE964B' // Orange
];

// Function to create a striped pattern
function createStripedPattern(id, color, orientation = 'horizontal') {
const pattern = defs.append('pattern')
.attr('id', id)
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', 8)
.attr('height', 8);
if (orientation === 'horizontal') {
pattern.append('rect')
.attr('width', 8)
.attr('height', 8)
.attr('fill', d3.color(color).darker(0.2));
pattern.append('line')
.attr('x1', 0)
.attr('y1', 4)
.attr('x2', 8)
.attr('y2', 4)
.attr('stroke', d3.color(color).brighter(0.5))
.attr('stroke-width', 2);
} else {
pattern.append('rect')
.attr('width', 8)
.attr('height', 8)
.attr('fill', d3.color(color).darker(0.2));
pattern.append('line')
.attr('x1', 4)
.attr('y1', 0)
.attr('x2', 4)
.attr('y2', 8)
.attr('stroke', d3.color(color).brighter(0.5))
.attr('stroke-width', 2);
}
return pattern;
}

// Create patterns for each color
Object.entries(colorMapping).forEach(([name, color], i) => {
createStripedPattern(`stripes-${name}`, color, i % 2 === 0 ? 'horizontal' : 'vertical');
});

// Tooltip setup
let tooltip = document.createElement('div');
tooltip.id = 'tooltip';
Object.assign(tooltip.style, {
position: 'absolute',
visibility: 'hidden',
background: '#fff',
borderRadius: '5px',
padding: '10px',
boxShadow: '0 0 10px rgba(0,0,0,0.25)',
pointerEvents: 'none',
maxWidth: '280px',
zIndex: 1000,
font: '13px sans-serif',
transition: 'opacity 0.3s ease',
opacity: 0
});
document.body.appendChild(tooltip);

// Function to create donut chart
function createDonutChart(data) {
svg.selectAll('*:not(defs)').remove();
const radius = Math.min(width, height) / 2 - 40;
const innerRadius = radius * 0.5;
// Main group
const g = svg.append('g')
.attr('transform', `translate(${width/2}, ${height/2})`);
// Calculate total for percentages
const total = d3.sum(data, d => d.value);
// Calculate percentage for each slice
data.forEach(d => {
d.percentage = (d.value / total) * 100;
});
// Sort data by value (descending)
data.sort((a, b) => b.value - a.value);
// Create pie generator
const pie = d3.pie()
.sort(null)
.padAngle(0.03)
.value(d => d.value);
const pieData = pie(data);
// Create arc generators for outer and inner rings
const outerArc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(radius);
// Function to create arc with custom thickness
function customArc(innerRadiusRatio, outerRadiusRatio) {
return d3.arc()
.innerRadius(radius * innerRadiusRatio)
.outerRadius(radius * outerRadiusRatio)
.padAngle(0.03)
.cornerRadius(5);
}
// Calculate segment size based on data value
const segmentScale = d3.scaleLinear()
.domain([d3.min(data, d => d.value), d3.max(data, d => d.value)])
.range([0.15, 0.3]);
// Create segments
pieData.forEach((d, i) => {
const segmentThickness = segmentScale(d.data.value);
const innerRatio = 0.5 + (i * 0.01); // Slight offset for each segment
const outerRatio = innerRatio + segmentThickness;
const arc = customArc(innerRatio, outerRatio);
// Create segment
const segment = g.append('path')
.datum(d)
.attr('d', arc)
.attr('fill', colorMapping[d.data.name])
.attr('stroke', '#fff')
.attr('stroke-width', 1.5)
.style('opacity', 0.9)
.on('mouseover', function(event, d) {
// Highlight segment
d3.select(this)
.transition()
.duration(200)
.attr('transform',
`scale(1.05) rotate(${(d.startAngle + d.endAngle) / 2 * (180/Math.PI)})`);
// Show tooltip
const eduC = getEducationCategory(d.data.avgEducation);
const ageC = getAgeCategory(d.data.avgAge);
tooltip.innerHTML = `
<div style="font-weight:bold;font-size:16px;margin-bottom:8px;">${d.data.name} Income Class</div>
<div><b>Percentage:</b> ${d.data.percentage.toFixed(1)}%</div>
<div><b>Food Insecurity:</b> ${d.data.value.toFixed(2)}%</div>
<div><b>Education:</b> ${d.data.avgEducation.toFixed(1)} yrs (${eduC})</div>
<div><b>Median Age:</b> ${d.data.avgAge.toFixed(1)} (${ageC})</div>
<div><b>Countries:</b> ${d.data.countryCount}</div>
`;
tooltip.style.visibility = 'visible';
tooltip.style.left = `${event.pageX + 10}px`;
tooltip.style.top = `${event.pageY - 10}px`;
tooltip.style.opacity = 1;
})
.on('mouseout', function() {
// Restore segment
d3.select(this)
.transition()
.duration(200)
.attr('transform', '');
// Hide tooltip
tooltip.style.opacity = 0;
tooltip.style.visibility = 'hidden';
});
// Add small stripes pattern on top
const patternArc = customArc(outerRatio - 0.03, outerRatio);
g.append('path')
.datum(d)
.attr('d', patternArc)
.attr('fill', `url(#stripes-${d.data.name})`)
.attr('stroke', '#fff')
.attr('stroke-width', 0.5)
.style('opacity', 0.8);
// Add percentage labels
if (d.data.percentage > 10) {
const labelArc = customArc((innerRatio + outerRatio) / 2 - 0.05, (innerRatio + outerRatio) / 2 + 0.05);
const pos = labelArc.centroid(d);
const midAngle = (d.startAngle + d.endAngle) / 2;
const isRight = midAngle < Math.PI;
g.append('text')
.attr('x', pos[0])
.attr('y', pos[1])
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('fill', 'white')
.attr('font-size', '16px')
.attr('font-weight', 'bold')
.text(`${d.data.percentage.toFixed(1)}%`);
}
});
// Create subtle connecting lines between segments
pieData.forEach((d, i) => {
if (i < pieData.length - 1) {
const nextD = pieData[i+1];
// Connect end of current to start of next
const startPoint = outerArc.centroid(d);
const endPoint = outerArc.centroid(nextD);
g.append('line')
.attr('x1', startPoint[0])
.attr('y1', startPoint[1])
.attr('x2', endPoint[0])
.attr('y2', endPoint[1])
.attr('stroke', '#fff')
.attr('stroke-width', 1)
.attr('stroke-opacity', 0.5)
.attr('stroke-dasharray', '3,3');
}
});
// Add center label
g.append('text')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', '18px')
.attr('font-weight', 'bold')
.text('Food Security');
g.append('text')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', '14px')
.attr('y', 25)
.text('by Income Class');
}

// Function to update chart data
function updateChart() {
const selectedYear = +yearSelect.value;
const selectedContinent = contSelect.value;
const data = processedByYearContinent[selectedYear][selectedContinent] || [];
subtitleContainer.textContent = `Year: ${selectedYear} | Continent: ${selectedContinent}`;
createDonutChart(data);
}

// Event listeners for controls
yearSelect.addEventListener('change', updateChart);
contSelect.addEventListener('change', updateChart);

// Initialize
updateChart();

// Add custom styles
const style = document.createElement('style');
style.textContent = `
select {
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 8px;
font-size: 14px;
}
path:hover {
cursor: pointer;
}
`;
document.head.appendChild(style);

return container;
}
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