Spaces:
Running
Running
<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> |