404 lines
12 KiB
HTML
404 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Animated Scatter Plot - Iteration 5</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
#viz-container {
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 15px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
padding: 30px;
|
|
}
|
|
|
|
h1 {
|
|
color: #2d3748;
|
|
margin-bottom: 10px;
|
|
font-size: 2em;
|
|
}
|
|
|
|
.subtitle {
|
|
color: #718096;
|
|
margin-bottom: 30px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.controls {
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
gap: 15px;
|
|
align-items: center;
|
|
}
|
|
|
|
.controls button {
|
|
padding: 10px 20px;
|
|
background: #667eea;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: background 0.3s;
|
|
}
|
|
|
|
.controls button:hover {
|
|
background: #764ba2;
|
|
}
|
|
|
|
.controls button.active {
|
|
background: #764ba2;
|
|
}
|
|
|
|
.chart-container {
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.dot {
|
|
stroke: #fff;
|
|
stroke-width: 1.5px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.axis text {
|
|
font-size: 11px;
|
|
fill: #4a5568;
|
|
}
|
|
|
|
.axis path,
|
|
.axis line {
|
|
stroke: #cbd5e0;
|
|
}
|
|
|
|
.tooltip {
|
|
position: absolute;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
color: white;
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
font-size: 13px;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.legend {
|
|
margin-top: 20px;
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 15px;
|
|
height: 15px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.attribution {
|
|
margin-top: 30px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #e2e8f0;
|
|
font-size: 0.85em;
|
|
color: #718096;
|
|
}
|
|
|
|
.attribution a {
|
|
color: #667eea;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.attribution a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="viz-container">
|
|
<h1>Economic Indicators by Country</h1>
|
|
<p class="subtitle">GDP vs Life Expectancy with animated transitions</p>
|
|
|
|
<div class="controls">
|
|
<button id="play-btn">Play Animation</button>
|
|
<button id="reset-btn">Reset</button>
|
|
<span id="year-label" style="font-weight: bold; color: #667eea;">Year: 2020</span>
|
|
</div>
|
|
|
|
<div class="chart-container"></div>
|
|
|
|
<div class="legend">
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #e74c3c;"></div>
|
|
<span>Africa</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #3498db;"></div>
|
|
<span>Asia</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #2ecc71;"></div>
|
|
<span>Europe</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #f39c12;"></div>
|
|
<span>Americas</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="attribution">
|
|
<p><strong>Visualization inspired by:</strong> <a href="https://observablehq.com/@d3/scatterplot" target="_blank">D3 Scatterplot</a></p>
|
|
<p><strong>Data from:</strong> World Bank API (simulated)</p>
|
|
<p><strong>Created:</strong> 2025-10-10T23:55:15Z</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata section (required for state tracking) -->
|
|
<div id="metadata" style="display:none;">
|
|
{
|
|
"iteration": 5,
|
|
"web_source": "https://observablehq.com/@d3/scatterplot",
|
|
"techniques_learned": [
|
|
"Scatter plot with size encoding",
|
|
"Animated data transitions between states",
|
|
"Color scales for categorical data"
|
|
],
|
|
"data_source": "https://api.worldbank.org",
|
|
"created": "2025-10-10T23:55:15Z"
|
|
}
|
|
</div>
|
|
|
|
<div class="tooltip"></div>
|
|
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
|
|
<script>
|
|
/**
|
|
* Economic Indicators by Country - Iteration 5
|
|
*
|
|
* Web Source: https://observablehq.com/@d3/scatterplot
|
|
* Techniques Learned:
|
|
* 1. Scatter plot with size encoding
|
|
* 2. Animated data transitions between states
|
|
* 3. Color scales for categorical data
|
|
*
|
|
* Data Source: World Bank API (simulated)
|
|
*
|
|
* Created: 2025-10-10T23:55:15Z
|
|
* Run ID: test_run_001
|
|
*/
|
|
|
|
// Generate simulated data for multiple years
|
|
const regions = ['Africa', 'Asia', 'Europe', 'Americas'];
|
|
const countries = [
|
|
{ name: 'USA', region: 'Americas' },
|
|
{ name: 'China', region: 'Asia' },
|
|
{ name: 'India', region: 'Asia' },
|
|
{ name: 'Germany', region: 'Europe' },
|
|
{ name: 'Brazil', region: 'Americas' },
|
|
{ name: 'Nigeria', region: 'Africa' },
|
|
{ name: 'Japan', region: 'Asia' },
|
|
{ name: 'France', region: 'Europe' },
|
|
{ name: 'UK', region: 'Europe' },
|
|
{ name: 'South Africa', region: 'Africa' }
|
|
];
|
|
|
|
const years = [2020, 2021, 2022, 2023, 2024];
|
|
const dataByYear = {};
|
|
|
|
years.forEach(year => {
|
|
dataByYear[year] = countries.map(country => ({
|
|
country: country.name,
|
|
region: country.region,
|
|
gdp: Math.random() * 50000 + 10000 + (year - 2020) * 2000,
|
|
lifeExpectancy: Math.random() * 15 + 65 + (year - 2020) * 0.3,
|
|
population: Math.random() * 300000000 + 50000000
|
|
}));
|
|
});
|
|
|
|
let currentYear = 2020;
|
|
let isPlaying = false;
|
|
let animationInterval;
|
|
|
|
const margin = { top: 20, right: 30, bottom: 50, left: 60 };
|
|
const width = Math.min(900, window.innerWidth - 100) - margin.left - margin.right;
|
|
const height = 500 - margin.top - margin.bottom;
|
|
|
|
const svg = d3.select('.chart-container')
|
|
.append('svg')
|
|
.attr('width', width + margin.left + margin.right)
|
|
.attr('height', height + margin.top + margin.bottom)
|
|
.append('g')
|
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
|
|
const tooltip = d3.select('.tooltip');
|
|
|
|
// Learned Technique 3: Color scales for categorical data
|
|
const colorScale = d3.scaleOrdinal()
|
|
.domain(regions)
|
|
.range(['#e74c3c', '#3498db', '#2ecc71', '#f39c12']);
|
|
|
|
// Scales
|
|
const x = d3.scaleLinear()
|
|
.domain([0, 70000])
|
|
.range([0, width]);
|
|
|
|
const y = d3.scaleLinear()
|
|
.domain([60, 85])
|
|
.range([height, 0]);
|
|
|
|
// Learned Technique 1: Size encoding for population
|
|
const size = d3.scaleSqrt()
|
|
.domain([0, 400000000])
|
|
.range([5, 40]);
|
|
|
|
// Axes
|
|
const xAxis = d3.axisBottom(x).tickFormat(d => '$' + d/1000 + 'K');
|
|
const yAxis = d3.axisLeft(y).tickFormat(d => d + ' yrs');
|
|
|
|
svg.append('g')
|
|
.attr('class', 'axis')
|
|
.attr('transform', `translate(0,${height})`)
|
|
.call(xAxis);
|
|
|
|
svg.append('g')
|
|
.attr('class', 'axis')
|
|
.call(yAxis);
|
|
|
|
// Axis labels
|
|
svg.append('text')
|
|
.attr('x', width / 2)
|
|
.attr('y', height + 40)
|
|
.style('text-anchor', 'middle')
|
|
.style('fill', '#4a5568')
|
|
.text('GDP per capita (USD)');
|
|
|
|
svg.append('text')
|
|
.attr('transform', 'rotate(-90)')
|
|
.attr('y', 0 - margin.left)
|
|
.attr('x', 0 - (height / 2))
|
|
.attr('dy', '1em')
|
|
.style('text-anchor', 'middle')
|
|
.style('fill', '#4a5568')
|
|
.text('Life Expectancy (years)');
|
|
|
|
// Draw initial data
|
|
updateChart(currentYear);
|
|
|
|
function updateChart(year) {
|
|
const data = dataByYear[year];
|
|
|
|
// Learned Technique 2: Animated transitions between states
|
|
const dots = svg.selectAll('.dot')
|
|
.data(data, d => d.country);
|
|
|
|
dots.enter()
|
|
.append('circle')
|
|
.attr('class', 'dot')
|
|
.attr('cx', d => x(d.gdp))
|
|
.attr('cy', d => y(d.lifeExpectancy))
|
|
.attr('r', 0)
|
|
.attr('fill', d => colorScale(d.region))
|
|
.merge(dots)
|
|
.transition()
|
|
.duration(800)
|
|
.attr('cx', d => x(d.gdp))
|
|
.attr('cy', d => y(d.lifeExpectancy))
|
|
.attr('r', d => size(d.population));
|
|
|
|
dots.exit()
|
|
.transition()
|
|
.duration(500)
|
|
.attr('r', 0)
|
|
.remove();
|
|
|
|
// Re-attach event listeners
|
|
svg.selectAll('.dot')
|
|
.on('mouseover', function(event, d) {
|
|
d3.select(this)
|
|
.transition()
|
|
.duration(200)
|
|
.attr('r', size(d.population) * 1.2);
|
|
|
|
tooltip
|
|
.style('opacity', 1)
|
|
.html(`
|
|
<strong>${d.country}</strong><br>
|
|
Region: ${d.region}<br>
|
|
GDP: $${d.gdp.toFixed(0)}<br>
|
|
Life Exp: ${d.lifeExpectancy.toFixed(1)} yrs<br>
|
|
Pop: ${(d.population / 1000000).toFixed(1)}M
|
|
`)
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 28) + 'px');
|
|
})
|
|
.on('mouseout', function(event, d) {
|
|
d3.select(this)
|
|
.transition()
|
|
.duration(200)
|
|
.attr('r', size(d.population));
|
|
|
|
tooltip.style('opacity', 0);
|
|
});
|
|
|
|
document.getElementById('year-label').textContent = `Year: ${year}`;
|
|
}
|
|
|
|
// Controls
|
|
document.getElementById('play-btn').addEventListener('click', function() {
|
|
if (isPlaying) {
|
|
clearInterval(animationInterval);
|
|
this.textContent = 'Play Animation';
|
|
this.classList.remove('active');
|
|
isPlaying = false;
|
|
} else {
|
|
this.textContent = 'Pause';
|
|
this.classList.add('active');
|
|
isPlaying = true;
|
|
|
|
animationInterval = setInterval(() => {
|
|
currentYear++;
|
|
if (currentYear > 2024) {
|
|
currentYear = 2020;
|
|
}
|
|
updateChart(currentYear);
|
|
}, 1500);
|
|
}
|
|
});
|
|
|
|
document.getElementById('reset-btn').addEventListener('click', function() {
|
|
if (isPlaying) {
|
|
clearInterval(animationInterval);
|
|
document.getElementById('play-btn').textContent = 'Play Animation';
|
|
document.getElementById('play-btn').classList.remove('active');
|
|
isPlaying = false;
|
|
}
|
|
currentYear = 2020;
|
|
updateChart(currentYear);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|