300 lines
9.0 KiB
HTML
300 lines
9.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Real-Time Weather Dashboard - Iteration 1</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: 900px;
|
|
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;
|
|
}
|
|
|
|
.chart-container {
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.bar {
|
|
fill: #667eea;
|
|
transition: fill 0.3s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.bar:hover {
|
|
fill: #764ba2;
|
|
}
|
|
|
|
.axis text {
|
|
font-size: 12px;
|
|
fill: #4a5568;
|
|
}
|
|
|
|
.axis path,
|
|
.axis line {
|
|
stroke: #cbd5e0;
|
|
}
|
|
|
|
.tooltip {
|
|
position: absolute;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
padding: 10px 15px;
|
|
border-radius: 8px;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
font-size: 14px;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 50px;
|
|
color: #718096;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
#viz-container {
|
|
padding: 20px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.5em;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="viz-container">
|
|
<h1>Global Temperature Anomalies</h1>
|
|
<p class="subtitle">Monthly temperature deviations from baseline (1951-1980)</p>
|
|
|
|
<div class="loading">Loading climate data...</div>
|
|
<div class="chart-container"></div>
|
|
|
|
<div class="attribution">
|
|
<p><strong>Visualization inspired by:</strong> <a href="https://observablehq.com/@d3/bar-chart" target="_blank">D3 Bar Chart Gallery</a></p>
|
|
<p><strong>Data from:</strong> NASA GISS Surface Temperature Analysis (simulated)</p>
|
|
<p><strong>Created:</strong> 2025-10-10T23:50:15Z</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata section (required for state tracking) -->
|
|
<div id="metadata" style="display:none;">
|
|
{
|
|
"iteration": 1,
|
|
"web_source": "https://observablehq.com/@d3/bar-chart",
|
|
"techniques_learned": [
|
|
"Basic D3 bar chart with scales",
|
|
"SVG axis generation",
|
|
"Interactive tooltips with transitions"
|
|
],
|
|
"data_source": "https://data.giss.nasa.gov/gistemp/",
|
|
"created": "2025-10-10T23:50:15Z"
|
|
}
|
|
</div>
|
|
|
|
<div class="tooltip"></div>
|
|
|
|
<!-- External libraries -->
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
|
|
<script>
|
|
/**
|
|
* Global Temperature Anomalies Bar Chart - Iteration 1
|
|
*
|
|
* Web Source: https://observablehq.com/@d3/bar-chart
|
|
* Techniques Learned:
|
|
* 1. Basic D3 bar chart with linear scales
|
|
* 2. SVG axis generation with d3.axisBottom/Left
|
|
* 3. Interactive tooltips with smooth transitions
|
|
*
|
|
* Data Source: NASA GISS Surface Temperature Analysis (simulated)
|
|
*
|
|
* Created: 2025-10-10T23:50:15Z
|
|
* Run ID: test_run_001
|
|
*/
|
|
|
|
// Simulated climate data (normally would fetch from API)
|
|
const data = [
|
|
{ month: 'Jan', anomaly: 0.89 },
|
|
{ month: 'Feb', anomaly: 0.95 },
|
|
{ month: 'Mar', anomaly: 1.08 },
|
|
{ month: 'Apr', anomaly: 1.12 },
|
|
{ month: 'May', anomaly: 0.87 },
|
|
{ month: 'Jun', anomaly: 0.92 },
|
|
{ month: 'Jul', anomaly: 1.01 },
|
|
{ month: 'Aug', anomaly: 0.99 },
|
|
{ month: 'Sep', anomaly: 0.91 },
|
|
{ month: 'Oct', anomaly: 1.05 },
|
|
{ month: 'Nov', anomaly: 0.97 },
|
|
{ month: 'Dec', anomaly: 0.84 }
|
|
];
|
|
|
|
// Set up dimensions
|
|
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
|
|
const width = Math.min(800, window.innerWidth - 100) - margin.left - margin.right;
|
|
const height = 400 - margin.top - margin.bottom;
|
|
|
|
// Remove loading message
|
|
document.querySelector('.loading').style.display = 'none';
|
|
|
|
// Create SVG
|
|
const svg = d3.select('.chart-container')
|
|
.append('svg')
|
|
.attr('width', width + margin.left + margin.right)
|
|
.attr('height', height + margin.top + margin.bottom)
|
|
.attr('role', 'img')
|
|
.attr('aria-label', 'Bar chart showing monthly temperature anomalies')
|
|
.append('g')
|
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
|
|
// Learned Technique 1: Using D3 scales for data-to-pixel mapping
|
|
const x = d3.scaleBand()
|
|
.domain(data.map(d => d.month))
|
|
.range([0, width])
|
|
.padding(0.2);
|
|
|
|
const y = d3.scaleLinear()
|
|
.domain([0, d3.max(data, d => d.anomaly) * 1.1])
|
|
.range([height, 0]);
|
|
|
|
// Learned Technique 2: Creating axes with D3
|
|
const xAxis = d3.axisBottom(x);
|
|
const yAxis = d3.axisLeft(y)
|
|
.ticks(5)
|
|
.tickFormat(d => d + '°C');
|
|
|
|
svg.append('g')
|
|
.attr('class', 'axis')
|
|
.attr('transform', `translate(0,${height})`)
|
|
.call(xAxis);
|
|
|
|
svg.append('g')
|
|
.attr('class', 'axis')
|
|
.call(yAxis);
|
|
|
|
// Add Y axis label
|
|
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('Temperature Anomaly (°C)');
|
|
|
|
// Get tooltip element
|
|
const tooltip = d3.select('.tooltip');
|
|
|
|
// Learned Technique 3: Interactive tooltips with transitions
|
|
const bars = svg.selectAll('.bar')
|
|
.data(data)
|
|
.enter()
|
|
.append('rect')
|
|
.attr('class', 'bar')
|
|
.attr('x', d => x(d.month))
|
|
.attr('width', x.bandwidth())
|
|
.attr('y', height)
|
|
.attr('height', 0)
|
|
.attr('role', 'graphics-symbol')
|
|
.attr('aria-label', d => `${d.month}: ${d.anomaly} degrees Celsius`)
|
|
.on('mouseover', function(event, d) {
|
|
tooltip
|
|
.style('opacity', 1)
|
|
.html(`<strong>${d.month}</strong><br>Anomaly: ${d.anomaly}°C`)
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 28) + 'px');
|
|
})
|
|
.on('mouseout', function() {
|
|
tooltip.style('opacity', 0);
|
|
});
|
|
|
|
// Animate bars on load
|
|
bars.transition()
|
|
.duration(800)
|
|
.delay((d, i) => i * 50)
|
|
.attr('y', d => y(d.anomaly))
|
|
.attr('height', d => height - y(d.anomaly));
|
|
|
|
// Keyboard navigation support
|
|
bars.on('focus', function(event, d) {
|
|
tooltip
|
|
.style('opacity', 1)
|
|
.html(`<strong>${d.month}</strong><br>Anomaly: ${d.anomaly}°C`)
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 28) + 'px');
|
|
})
|
|
.on('blur', function() {
|
|
tooltip.style('opacity', 0);
|
|
});
|
|
|
|
// Make bars keyboard accessible
|
|
bars.attr('tabindex', 0);
|
|
|
|
// Responsive resize
|
|
window.addEventListener('resize', function() {
|
|
const newWidth = Math.min(800, window.innerWidth - 100) - margin.left - margin.right;
|
|
|
|
svg.attr('width', newWidth + margin.left + margin.right);
|
|
|
|
x.range([0, newWidth]);
|
|
|
|
svg.select('.axis')
|
|
.attr('transform', `translate(0,${height})`)
|
|
.call(xAxis);
|
|
|
|
bars
|
|
.attr('x', d => x(d.month))
|
|
.attr('width', x.bandwidth());
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|