world-indicators / index.html
fdaudens's picture
fdaudens HF Staff
Add 1 files
39f8822 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>World Development Indicators</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body {
font-family: 'Arial', sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f7fa;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 30px;
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 10px;
font-size: 2.2em;
}
.subtitle {
text-align: center;
color: #7f8c8d;
margin-bottom: 30px;
font-size: 1.1em;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 25px;
align-items: center;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.08);
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-right: auto;
}
.filter-btn {
padding: 8px 16px;
border: none;
border-radius: 25px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
font-size: 0.9em;
display: flex;
align-items: center;
}
.filter-btn i {
margin-right: 5px;
font-size: 0.9em;
}
.filter-btn:hover {
opacity: 0.9;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.filter-btn.active {
transform: scale(1.05);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.year-controls {
display: flex;
align-items: center;
gap: 15px;
background-color: rgba(255, 255, 255, 0.8);
padding: 10px 15px;
border-radius: 30px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.year-slider {
width: 250px;
-webkit-appearance: none;
height: 6px;
background: #e0e0e0;
border-radius: 5px;
outline: none;
}
.year-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #3498db;
cursor: pointer;
transition: all 0.2s;
}
.year-slider::-webkit-slider-thumb:hover {
background: #2980b9;
transform: scale(1.1);
}
.play-btn {
background-color: #2ecc71;
color: white;
border: none;
padding: 10px 20px;
border-radius: 30px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.play-btn:hover {
background-color: #27ae60;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.play-btn.playing {
background-color: #e74c3c;
}
.year-display {
font-weight: bold;
min-width: 50px;
text-align: center;
font-size: 1.1em;
color: #2c3e50;
background-color: #f8f9fa;
padding: 5px 10px;
border-radius: 20px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.chart-container {
width: 100%;
height: 650px;
position: relative;
border-radius: 8px;
overflow: hidden;
background-color: white;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
border: 1px solid #eee;
}
.tooltip {
position: absolute;
padding: 15px;
background: rgba(0, 0, 0, 0.9);
color: white;
border-radius: 8px;
pointer-events: none;
text-align: left;
opacity: 0;
transition: opacity 0.2s;
font-size: 14px;
z-index: 10;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
max-width: 240px;
line-height: 1.5;
backdrop-filter: blur(2px);
}
.tooltip:after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -8px;
border-width: 8px;
border-style: solid;
border-color: rgba(0, 0, 0, 0.9) transparent transparent transparent;
}
.loading {
text-align: center;
padding: 50px;
font-size: 18px;
color: #7f8c8d;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #3498db;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.axis-label {
font-size: 12px;
fill: #555;
font-weight: 500;
}
.disclaimer {
text-align: center;
font-size: 13px;
color: #7f8c8d;
margin-top: 20px;
line-height: 1.5;
}
.chart-title {
font-size: 14px;
font-weight: bold;
fill: #444;
}
@media (max-width: 768px) {
.container {
padding: 15px;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.filter-buttons {
margin-right: 0;
margin-bottom: 15px;
justify-content: center;
}
.year-controls {
width: 100%;
justify-content: space-between;
}
.year-slider {
width: 100%;
}
.chart-container {
height: 500px;
}
h1 {
font-size: 1.8em;
}
.subtitle {
font-size: 1em;
}
}
</style>
</head>
<body>
<div class="container">
<h1>World Development Indicators</h1>
<p class="subtitle">Life Expectancy vs. GDP per capita (1950-2023)</p>
<div class="controls">
<div class="filter-buttons" id="region-buttons">
<!-- Region buttons will be dynamically generated here -->
</div>
<div class="year-controls">
<button class="play-btn" id="play-btn">
<i class="fas fa-play"></i> Play
</button>
<span class="year-display" id="year-value">1990</span>
<input type="range" class="year-slider" id="year-slider" min="1950" max="2023" value="1990" step="1">
</div>
</div>
<div class="chart-container" id="chart-area">
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Loading data visualization...</p>
</div>
</div>
<div class="tooltip" id="tooltip"></div>
<p class="disclaimer">Note: For countries missing region data in their year, we use the region from their latest available data.<br>Some countries/territories may not have complete indicator coverage for all years.</p>
</div>
<script>
// Configuration
const config = {
margin: { top: 60, right: 60, bottom: 80, left: 90 },
minBubbleSize: 3,
maxBubbleSize: 45,
animationDuration: 500,
playInterval: 800,
regions: {
"Africa": { color: "#e74c3c", icon: "globe-africa" },
"Asia": { color: "#3498db", icon: "globe-asia" },
"Europe": { color: "#2ecc71", icon: "globe-europe" },
"North America": { color: "#f1c40f", icon: "globe-americas" },
"Oceania": { color: "#9b59b6", icon: "map-marked-alt" },
"South America": { color: "#1abc9c", icon: "map-marked-alt" },
"Antarctica": { color: "#95a5a6", icon: "snowflake" }
}
};
// State
let state = {
data: null,
filteredData: null,
countryRegions: {}, // Store the most recent region for each country
minYear: 1950,
maxYear: 2023,
currentYear: 1990,
activeRegions: Object.keys(config.regions),
isPlaying: false,
playIntervalId: null,
svg: null,
x: null,
y: null,
size: null
};
// DOM elements
const dom = {
chartArea: d3.select("#chart-area"),
loading: d3.select("#loading"),
tooltip: d3.select("#tooltip"),
yearSlider: d3.select("#year-slider"),
yearValue: d3.select("#year-value"),
playBtn: d3.select("#play-btn"),
regionButtons: d3.select("#region-buttons")
};
// Initialize the visualization
async function init() {
// Load data from CSV file
await loadData();
// Setup UI controls
setupControls();
// Initialize chart
initChart();
// Render initial view
updateChart();
// Hide loading
dom.loading.style("display", "none");
}
// Load and process data from CSV
async function loadData() {
try {
// Load the CSV data
const rawData = await d3.csv("world-data.csv");
// First pass: Create a lookup table for country regions from the most recent data
const latestYearData = {};
// Find all unique entities
const entities = new Set(rawData.map(d => d.Entity));
// For each entity, find the most recent year that has region data
entities.forEach(entity => {
// Get all records for this entity, sorted by year (descending)
const entityData = rawData
.filter(d => d.Entity === entity)
.sort((a, b) => b.Year - a.Year);
// Find the first record with region data
const latestDataWithRegion = entityData.find(d =>
d['World regions according to OWID'] && d['World regions according to OWID'] !== ""
);
if (latestDataWithRegion) {
latestYearData[entity] = latestDataWithRegion['World regions according to OWID'];
} else {
// Fallback if no region data exists at all
latestYearData[entity] = 'Unknown';
}
});
// Process the data with region fallback
const processedData = rawData.map(d => {
// Use the region from our lookup table as fallback
const region = d['World regions according to OWID'] ||
latestYearData[d.Entity] ||
'Unknown';
return {
entity: d.Entity,
code: d.Code,
year: +d.Year,
lifeExpectancy: d['Life expectancy - Sex: all - Age: 0 - Variant: estimates'] ?
+d['Life expectancy - Sex: all - Age: 0 - Variant: estimates'] : null,
gdpPerCapita: d['GDP per capita, PPP (constant 2021 international $)'] ?
+d['GDP per capita, PPP (constant 2021 international $)'] : null,
population: d['Population (historical)'] ?
+d['Population (historical)'] : null,
region: region
};
}).filter(d =>
d.lifeExpectancy !== null &&
d.gdpPerCapita !== null &&
d.population !== null
);
// Filter out any invalid data points
state.data = processedData.filter(d =>
!isNaN(d.year) &&
!isNaN(d.lifeExpectancy) &&
!isNaN(d.gdpPerCapita) &&
!isNaN(d.population)
);
// Determine min and max years from data
state.minYear = d3.min(state.data, d => d.year);
state.maxYear = d3.max(state.data, d => d.year);
state.currentYear = state.minYear;
// Update slider range
dom.yearSlider.attr("min", state.minYear)
.attr("max", state.maxYear)
.attr("value", state.currentYear);
dom.yearValue.text(state.currentYear);
console.log("Data loaded successfully:", state.data);
} catch (error) {
console.error("Error loading data:", error);
dom.loading.html(`<p style="color: #e74c3c;">Error loading data. Please check if the 'world-data.csv' file exists.</p>`);
}
}
// Setup UI controls
function setupControls() {
// Create region buttons
dom.regionButtons.selectAll("*").remove();
Object.entries(config.regions).forEach(([region, { color, icon }]) => {
const btn = dom.regionButtons.append("button")
.attr("class", "filter-btn active")
.style("background-color", color)
.html(`<i class="fas fa-${icon}"></i> ${region}`)
.on("click", function() {
const isActive = d3.select(this).classed("active");
d3.select(this).classed("active", !isActive);
if (isActive) {
state.activeRegions = state.activeRegions.filter(r => r !== region);
} else {
state.activeRegions.push(region);
}
updateChart();
});
});
// Year slider event
dom.yearSlider.on("input", function() {
state.currentYear = +this.value;
dom.yearValue.text(state.currentYear);
updateChart();
});
// Play button event
dom.playBtn.on("click", function() {
state.isPlaying = !state.isPlaying;
if (state.isPlaying) {
d3.select(this).classed("playing", true)
.html('<i class="fas fa-pause"></i> Pause');
state.playIntervalId = setInterval(() => {
state.currentYear = state.currentYear < state.maxYear ?
state.currentYear + 1 : state.minYear;
dom.yearSlider.property("value", state.currentYear);
dom.yearValue.text(state.currentYear);
updateChart();
}, config.playInterval);
} else {
d3.select(this).classed("playing", false)
.html('<i class="fas fa-play"></i> Play');
if (state.playIntervalId) {
clearInterval(state.playIntervalId);
}
}
});
}
// Initialize the chart structure
function initChart() {
// Clear any existing SVG
dom.chartArea.selectAll("svg").remove();
// Create SVG
const width = dom.chartArea.node().clientWidth;
const height = dom.chartArea.node().clientHeight;
state.svg = dom.chartArea.append("svg")
.attr("width", width)
.attr("height", height);
// Create main chart group
const chartGroup = state.svg.append("g")
.attr("transform", `translate(${config.margin.left}, ${config.margin.top})`);
// Create scales
const innerWidth = width - config.margin.left - config.margin.right;
const innerHeight = height - config.margin.top - config.margin.bottom;
state.x = d3.scaleLog()
.range([0, innerWidth]);
state.y = d3.scaleLinear()
.range([innerHeight, 0]);
state.size = d3.scaleSqrt()
.range([config.minBubbleSize, config.maxBubbleSize]);
// Add axes groups
chartGroup.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${innerHeight})`);
chartGroup.append("g")
.attr("class", "y-axis");
// Add axis labels
chartGroup.append("text")
.attr("class", "axis-label")
.attr("x", innerWidth / 2)
.attr("y", innerHeight + 50)
.text("GDP per capita (PPP, constant 2021 international $)");
chartGroup.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("x", -innerHeight / 2)
.attr("y", -50)
.text("Life Expectancy (years)");
// Add title
chartGroup.append("text")
.attr("class", "chart-title")
.attr("x", innerWidth / 2)
.attr("y", -30)
.attr("text-anchor", "middle")
.text("Life Expectancy vs. GDP per capita");
// Add year display on chart
chartGroup.append("text")
.attr("id", "chart-year")
.attr("x", innerWidth - 10)
.attr("y", -10)
.attr("text-anchor", "end")
.style("font-size", "28px")
.style("font-weight", "bold")
.style("fill", "#2c3e50")
.style("opacity", 0.9)
.text(state.currentYear);
}
// Update chart with current data
function updateChart() {
if (!state.data) return;
// Filter data for current year and active regions
state.filteredData = state.data.filter(d =>
d.year === state.currentYear &&
state.activeRegions.includes(d.region)
);
// Get SVG dimensions
const width = dom.chartArea.node().clientWidth;
const height = dom.chartArea.node().clientHeight;
const innerWidth = width - config.margin.left - config.margin.right;
const innerHeight = height - config.margin.top - config.margin.bottom;
// Update scales
state.x.domain([
d3.min(state.data, d => d.gdpPerCapita) * 0.8,
d3.max(state.data, d => d.gdpPerCapita) * 1.2
]);
state.y.domain([
d3.min(state.data, d => d.lifeExpectancy) * 0.9,
d3.max(state.data, d => d.lifeExpectancy) * 1.05
]);
state.size.domain([
d3.min(state.data, d => d.population),
d3.max(state.data, d => d.population)
]);
// Update axes
const xAxis = d3.axisBottom(state.x).ticks(5, "$,.0f");
const yAxis = d3.axisLeft(state.y);
state.svg.select(".x-axis")
.transition()
.duration(config.animationDuration)
.call(xAxis);
state.svg.select(".y-axis")
.transition()
.duration(config.animationDuration)
.call(yAxis);
// Update year display
state.svg.select("#chart-year")
.text(state.currentYear);
// Create transition for bubbles
const t = state.svg.transition()
.duration(config.animationDuration);
// Bind data to circles
const circles = state.svg.selectAll("g.country")
.data(state.filteredData, d => d.code + d.year);
// Exit old bubbles
circles.exit()
.transition(t)
.attr("r", 0)
.remove();
// Enter new bubbles
const newCircles = circles.enter()
.append("g")
.attr("class", "country")
.attr("transform", d => {
const xPos = state.x(d.gdpPerCapita) + config.margin.left;
const yPos = state.y(d.lifeExpectancy) + config.margin.top;
return `translate(${xPos}, ${yPos})`;
})
.on("mouseover", function(event, d) {
dom.tooltip.style("opacity", 1)
.html(`
<div style="margin-bottom: 5px; font-size: 16px; font-weight: bold; color: ${config.regions[d.region].color}">
<i class="fas fa-${config.regions[d.region].icon}" style="margin-right: 5px;"></i>${d.entity}
</div>
<div><strong>Year:</strong> ${d.year}</div>
<div><strong>Region:</strong> ${d.region}</div>
<div><strong>Life Expectancy:</strong> ${d.lifeExpectancy.toFixed(1)} years</div>
<div><strong>GDP per capita:</strong> $${d.gdpPerCapita.toLocaleString('en-US', {maximumFractionDigits: 0})}</div>
<div><strong>Population:</strong> ${d3.format(",.0f")(d.population)}</div>
`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px");
})
.on("mouseout", function() {
dom.tooltip.style("opacity", 0);
});
newCircles.append("circle")
.attr("r", 0)
.attr("fill", d => config.regions[d.region] ? config.regions[d.region].color : "#95a5a6")
.attr("opacity", 0.8)
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.transition(t)
.attr("r", d => state.size(d.population));
// Update existing bubbles
circles.transition(t)
.attr("transform", d => {
const xPos = state.x(d.gdpPerCapita) + config.margin.left;
const yPos = state.y(d.lifeExpectancy) + config.margin.top;
return `translate(${xPos}, ${yPos})`;
})
.select("circle")
.attr("r", d => state.size(d.population))
.attr("fill", d => config.regions[d.region] ? config.regions[d.region].color : "#95a5a6");
// Add country labels for larger bubbles
circles.selectAll("text").remove();
newCircles.filter(d => state.size(d.population) > 15)
.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", "#fff")
.style("pointer-events", "none")
.text(d => d.code);
}
// Initialize the application
window.addEventListener("load", init);
// Handle window resize
window.addEventListener("resize", function() {
if (state.svg) {
initChart();
updateChart();
}
});
</script>
</body>
</html>