420 lines
12 KiB
HTML
420 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>Interactive Line Chart with Brush - Iteration 3</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #0f2027 0%, #203a43 50%, #2c5364 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
#viz-container {
|
|
max-width: 1100px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 15px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
|
padding: 30px;
|
|
}
|
|
|
|
h1 {
|
|
color: #2d3748;
|
|
margin-bottom: 10px;
|
|
font-size: 2em;
|
|
}
|
|
|
|
.subtitle {
|
|
color: #718096;
|
|
margin-bottom: 30px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.chart-main {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.chart-context {
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.line {
|
|
fill: none;
|
|
stroke: #2c5364;
|
|
stroke-width: 2;
|
|
}
|
|
|
|
.area {
|
|
fill: url(#gradient);
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.brush .selection {
|
|
fill: #2c5364;
|
|
fill-opacity: 0.2;
|
|
stroke: #2c5364;
|
|
stroke-width: 2;
|
|
}
|
|
|
|
.axis text {
|
|
font-size: 11px;
|
|
fill: #4a5568;
|
|
}
|
|
|
|
.axis path,
|
|
.axis line {
|
|
stroke: #cbd5e0;
|
|
}
|
|
|
|
.grid line {
|
|
stroke: #e2e8f0;
|
|
stroke-opacity: 0.7;
|
|
}
|
|
|
|
.grid path {
|
|
stroke-width: 0;
|
|
}
|
|
|
|
.dot {
|
|
fill: #2c5364;
|
|
stroke: white;
|
|
stroke-width: 2;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.chart-main:hover .dot {
|
|
opacity: 1;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.attribution {
|
|
margin-top: 30px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #e2e8f0;
|
|
font-size: 0.85em;
|
|
color: #718096;
|
|
}
|
|
|
|
.attribution a {
|
|
color: #2c5364;
|
|
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 CO2 Emissions Timeline</h1>
|
|
<p class="subtitle">Interactive timeline with brush selection for detailed exploration</p>
|
|
|
|
<div class="chart-main"></div>
|
|
<div class="chart-context"></div>
|
|
|
|
<div class="attribution">
|
|
<p><strong>Visualization inspired by:</strong> <a href="https://observablehq.com/@d3/focus-context" target="_blank">D3 Focus + Context via Brushing</a></p>
|
|
<p><strong>Data from:</strong> Carbon Intensity API (simulated)</p>
|
|
<p><strong>Created:</strong> 2025-10-10T23:52:30Z</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata section (required for state tracking) -->
|
|
<div id="metadata" style="display:none;">
|
|
{
|
|
"iteration": 3,
|
|
"web_source": "https://observablehq.com/@d3/focus-context",
|
|
"techniques_learned": [
|
|
"Brush selection for focus + context pattern",
|
|
"Coordinated views with zoom synchronization",
|
|
"Area charts with linear gradients"
|
|
],
|
|
"data_source": "https://api.carbonintensity.org.uk",
|
|
"created": "2025-10-10T23:52:30Z"
|
|
}
|
|
</div>
|
|
|
|
<div class="tooltip"></div>
|
|
|
|
<!-- External libraries -->
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
|
|
<script>
|
|
/**
|
|
* Global CO2 Emissions Timeline - Iteration 3
|
|
*
|
|
* Web Source: https://observablehq.com/@d3/focus-context
|
|
* Techniques Learned:
|
|
* 1. Brush selection for focus + context pattern
|
|
* 2. Coordinated views with zoom synchronization
|
|
* 3. Area charts with linear gradients
|
|
*
|
|
* Data Source: Carbon Intensity API (simulated)
|
|
*
|
|
* Created: 2025-10-10T23:52:30Z
|
|
* Run ID: test_run_001
|
|
*/
|
|
|
|
// Generate simulated time series data
|
|
const startDate = new Date(2010, 0, 1);
|
|
const endDate = new Date(2024, 11, 31);
|
|
const data = [];
|
|
|
|
for (let d = new Date(startDate); d <= endDate; d.setMonth(d.getMonth() + 1)) {
|
|
const baseValue = 400 + Math.random() * 50;
|
|
const trend = (d.getTime() - startDate.getTime()) / (endDate.getTime() - startDate.getTime()) * 30;
|
|
data.push({
|
|
date: new Date(d),
|
|
value: baseValue + trend + Math.sin(d.getMonth() / 12 * Math.PI * 2) * 15
|
|
});
|
|
}
|
|
|
|
// Set up dimensions
|
|
const margin = { top: 20, right: 30, bottom: 110, left: 60 };
|
|
const margin2 = { top: 430, right: 30, bottom: 30, left: 60 };
|
|
const width = Math.min(1000, window.innerWidth - 100) - margin.left - margin.right;
|
|
const height = 500 - margin.top - margin.bottom;
|
|
const height2 = 500 - margin2.top - margin2.bottom;
|
|
|
|
// Create main SVG
|
|
const svg = d3.select('.chart-main')
|
|
.append('svg')
|
|
.attr('width', width + margin.left + margin.right)
|
|
.attr('height', height + margin.top + margin.bottom)
|
|
.attr('role', 'img')
|
|
.attr('aria-label', 'Line chart showing CO2 emissions over time with brush selection');
|
|
|
|
// Define gradient for area
|
|
const gradient = svg.append('defs')
|
|
.append('linearGradient')
|
|
.attr('id', 'gradient')
|
|
.attr('x1', '0%')
|
|
.attr('y1', '0%')
|
|
.attr('x2', '0%')
|
|
.attr('y2', '100%');
|
|
|
|
gradient.append('stop')
|
|
.attr('offset', '0%')
|
|
.attr('stop-color', '#2c5364')
|
|
.attr('stop-opacity', 0.5);
|
|
|
|
gradient.append('stop')
|
|
.attr('offset', '100%')
|
|
.attr('stop-color', '#2c5364')
|
|
.attr('stop-opacity', 0.1);
|
|
|
|
// Main chart group
|
|
const focus = svg.append('g')
|
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
|
|
// Context chart group
|
|
const context = svg.append('g')
|
|
.attr('transform', `translate(${margin2.left},${margin2.top})`);
|
|
|
|
// Scales for main chart
|
|
const x = d3.scaleTime()
|
|
.domain(d3.extent(data, d => d.date))
|
|
.range([0, width]);
|
|
|
|
const y = d3.scaleLinear()
|
|
.domain([d3.min(data, d => d.value) * 0.95, d3.max(data, d => d.value) * 1.05])
|
|
.range([height, 0]);
|
|
|
|
// Scales for context chart
|
|
const x2 = d3.scaleTime()
|
|
.domain(x.domain())
|
|
.range([0, width]);
|
|
|
|
const y2 = d3.scaleLinear()
|
|
.domain(y.domain())
|
|
.range([height2, 0]);
|
|
|
|
// Line generators
|
|
const line = d3.line()
|
|
.x(d => x(d.date))
|
|
.y(d => y(d.value));
|
|
|
|
const line2 = d3.line()
|
|
.x(d => x2(d.date))
|
|
.y(d => y2(d.value));
|
|
|
|
// Learned Technique 3: Area charts with linear gradients
|
|
const area = d3.area()
|
|
.x(d => x(d.date))
|
|
.y0(height)
|
|
.y1(d => y(d.value));
|
|
|
|
const area2 = d3.area()
|
|
.x(d => x2(d.date))
|
|
.y0(height2)
|
|
.y1(d => y2(d.value));
|
|
|
|
// Grid lines
|
|
const xGrid = d3.axisBottom(x)
|
|
.tickSize(height)
|
|
.tickFormat('');
|
|
|
|
focus.append('g')
|
|
.attr('class', 'grid')
|
|
.call(xGrid);
|
|
|
|
// Axes
|
|
const xAxis = d3.axisBottom(x);
|
|
const yAxis = d3.axisLeft(y).tickFormat(d => d + ' ppm');
|
|
const xAxis2 = d3.axisBottom(x2);
|
|
|
|
focus.append('g')
|
|
.attr('class', 'axis axis--x')
|
|
.attr('transform', `translate(0,${height})`)
|
|
.call(xAxis);
|
|
|
|
focus.append('g')
|
|
.attr('class', 'axis axis--y')
|
|
.call(yAxis);
|
|
|
|
context.append('g')
|
|
.attr('class', 'axis axis--x')
|
|
.attr('transform', `translate(0,${height2})`)
|
|
.call(xAxis2);
|
|
|
|
// Add Y axis label
|
|
focus.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')
|
|
.style('font-size', '12px')
|
|
.text('CO2 Concentration (ppm)');
|
|
|
|
// Draw area and line on main chart
|
|
focus.append('path')
|
|
.datum(data)
|
|
.attr('class', 'area')
|
|
.attr('d', area);
|
|
|
|
focus.append('path')
|
|
.datum(data)
|
|
.attr('class', 'line')
|
|
.attr('d', line);
|
|
|
|
// Draw area and line on context chart
|
|
context.append('path')
|
|
.datum(data)
|
|
.attr('class', 'area')
|
|
.attr('d', area2);
|
|
|
|
context.append('path')
|
|
.datum(data)
|
|
.attr('class', 'line')
|
|
.attr('d', line2);
|
|
|
|
// Add dots for hover interaction
|
|
const tooltip = d3.select('.tooltip');
|
|
|
|
const dots = focus.selectAll('.dot')
|
|
.data(data)
|
|
.enter()
|
|
.append('circle')
|
|
.attr('class', 'dot')
|
|
.attr('cx', d => x(d.date))
|
|
.attr('cy', d => y(d.value))
|
|
.attr('r', 4)
|
|
.on('mouseover', function(event, d) {
|
|
d3.select(this).attr('r', 6);
|
|
tooltip
|
|
.style('opacity', 1)
|
|
.html(`
|
|
<strong>${d3.timeFormat('%B %Y')(d.date)}</strong><br>
|
|
CO2: ${d.value.toFixed(2)} ppm
|
|
`)
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 28) + 'px');
|
|
})
|
|
.on('mouseout', function() {
|
|
d3.select(this).attr('r', 4);
|
|
tooltip.style('opacity', 0);
|
|
});
|
|
|
|
// Learned Technique 1 & 2: Brush selection for focus + context
|
|
const brush = d3.brushX()
|
|
.extent([[0, 0], [width, height2]])
|
|
.on('brush end', brushed);
|
|
|
|
context.append('g')
|
|
.attr('class', 'brush')
|
|
.call(brush)
|
|
.call(brush.move, x.range()); // Initialize with full range
|
|
|
|
function brushed(event) {
|
|
if (event.selection) {
|
|
const [x0, x1] = event.selection.map(x2.invert);
|
|
x.domain([x0, x1]);
|
|
|
|
// Update main chart
|
|
focus.select('.line')
|
|
.attr('d', line);
|
|
|
|
focus.select('.area')
|
|
.attr('d', area);
|
|
|
|
focus.select('.axis--x')
|
|
.call(xAxis);
|
|
|
|
dots
|
|
.attr('cx', d => x(d.date))
|
|
.attr('cy', d => y(d.value));
|
|
|
|
focus.select('.grid')
|
|
.call(xGrid);
|
|
}
|
|
}
|
|
|
|
// Keyboard accessibility
|
|
svg.attr('tabindex', 0)
|
|
.on('keydown', function(event) {
|
|
if (event.key === 'r' || event.key === 'R') {
|
|
// Reset zoom
|
|
x.domain(d3.extent(data, d => d.date));
|
|
context.select('.brush').call(brush.move, x2.range());
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|