infinite-agents-public/vaccine_timeseries/vaccine_timeseries_1_measles/CLAUDE.md

827 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Technical Documentation: Measles Vaccination Timeline Visualization
## Project Metadata
- **Iteration:** 1 (Foundation Level)
- **Disease Focus:** Measles (MCV1/MCV2 vaccines)
- **Time Period:** 2000-2023 (24 years)
- **Geographic Coverage:** 60 countries across 6 WHO regions
- **Web Learning Source:** https://docs.mapbox.com/mapbox-gl-js/example/popup-on-hover/
- **Created:** 2025-11-08
## Architecture Overview
### File Structure
```
vaccine_timeseries_1_measles/
├── index.html # Complete standalone visualization
├── README.md # Medical/epidemiological analysis
└── CLAUDE.md # Technical documentation (this file)
```
### Dependencies
**External Libraries:**
- Mapbox GL JS v3.0.1 (CDN)
- Mapbox GL CSS v3.0.1 (CDN)
**Shared Architecture:**
```javascript
import { MAPBOX_CONFIG } from '../../mapbox_test/shared/mapbox-config.js';
import { LayerFactory, COLOR_SCALES } from '../../mapbox_test/shared/layer-factory.js';
```
**Mapbox Access Token:**
- Managed through MAPBOX_CONFIG.accessToken
- Centralized configuration prevents token duplication
## Web Learning Application
### Research Assignment
**URL:** https://docs.mapbox.com/mapbox-gl-js/example/popup-on-hover/
**Topic:** Popup-on-hover interaction pattern
**Mission:** Learn and apply mouseenter/mouseleave event pattern for country information display
### Techniques Extracted
#### 1. Single Popup Instance Pattern
**Learning:**
Create popup instance once before event listeners to prevent flickering and reduce DOM manipulation overhead.
**Implementation:**
```javascript
// Pattern from Mapbox popup-on-hover example:
// Create single popup instance before interactions to prevent flickering
const popup = new mapboxgl.Popup({
closeButton: false, // No close button for hover popups
closeOnClick: false // Don't close when clicking elsewhere
});
```
**Why This Matters:**
- Creating new popups on every hover causes visible flickering
- Single instance reuse is more performant
- Consistent behavior across all interactions
#### 2. MouseEnter Event Handler
**Learning:**
Use mouseenter event to show popup with feature data from event object.
**Implementation:**
```javascript
map.on('mouseenter', 'vaccine-circles', (e) => {
// Change cursor to pointer (UX improvement)
map.getCanvas().style.cursor = 'pointer';
// Extract coordinates and properties from event
const coordinates = e.features[0].geometry.coordinates.slice();
const props = e.features[0].properties;
// Build HTML content from feature properties
const popupHTML = `
<div class="popup-title">${props.name}</div>
<div class="popup-stat">
<span class="popup-stat-label">Year:</span>
<span class="popup-stat-value">${props.year}</span>
</div>
<!-- Additional stats -->
`;
// Handle antimeridian crossing
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
// Reuse popup instance with new content
popup.setLngLat(coordinates)
.setHTML(popupHTML)
.addTo(map);
});
```
**Key Details:**
- Event object (e) contains features array with hovered feature
- .slice() creates coordinate copy to avoid mutation
- Antimeridian logic ensures correct popup positioning at ±180° longitude
- setLngLat() + setHTML() + addTo() pattern for popup display
#### 3. MouseLeave Event Handler
**Learning:**
Use mouseleave event to remove popup and reset cursor.
**Implementation:**
```javascript
map.on('mouseleave', 'vaccine-circles', () => {
map.getCanvas().style.cursor = ''; // Reset cursor
popup.remove(); // Remove popup from map
});
```
**Key Details:**
- Simple cleanup: cursor reset + popup removal
- No need for null checks (Mapbox handles gracefully)
- Ensures clean state when mouse exits layer
#### 4. Feature Identification
**Learning:**
Enable generateId on source to allow feature-based event handling.
**Implementation:**
```javascript
map.addSource('vaccine-data', {
type: 'geojson',
data: vaccineData,
generateId: true // Enable automatic feature ID generation
});
```
**Why This Matters:**
- Allows Mapbox to track individual features for hover events
- Required for reliable mouseenter/mouseleave detection
- Enables feature-state styling (not used in this iteration)
#### 5. Cursor Styling
**Learning:**
Change cursor to pointer on hover for clear interaction affordance.
**Implementation:**
```javascript
// On mouseenter
map.getCanvas().style.cursor = 'pointer';
// On mouseleave
map.getCanvas().style.cursor = '';
```
**UX Impact:**
- Clear visual feedback that element is interactive
- Standard web convention for clickable/hoverable elements
### Pattern Verification
**Code Comments:**
All hover-related code includes documentation:
```javascript
// Pattern from Mapbox popup-on-hover example
```
This ensures traceability to the web learning source and helps future developers understand the pattern origin.
## Data Architecture
### GeoJSON Structure
**Format:** Flattened feature collection (one feature per country-year)
**Feature Count:** 1,440 features
- 60 countries
- 24 years per country (2000-2023)
- 60 × 24 = 1,440
**Advantages of Flattened Approach:**
1. **Simple filtering:** `['==', ['get', 'year'], selectedYear]` shows only relevant features
2. **No server required:** All data embedded in client-side JavaScript
3. **Fast rendering:** Mapbox filters 1,440 features to ~60 visible instantly
4. **Easy debugging:** Each feature is self-contained
**Feature Schema:**
```javascript
{
type: "Feature",
geometry: {
type: "Point",
coordinates: [longitude, latitude]
},
properties: {
// Identification
name: "Nigeria", // Country name
iso3: "NGA", // ISO 3166-1 alpha-3 code
year: 2015, // Data year
// Context
region: "AFRO", // WHO region
income_level: "lower-middle", // World Bank classification
population: 182202000, // Population in data year
// Vaccination Coverage
coverage_dose1: 52, // MCV1 coverage percentage
coverage_dose2: 35, // MCV2 coverage percentage
// Disease Burden
cases: 45000, // Estimated measles cases
deaths: 850, // Estimated measles deaths
// Time Series (for future chart use)
years_array: "[2000,2001,...,2023]",
coverage1_array: "[30,35,...,68]",
coverage2_array: "[15,20,...,45]",
cases_array: "[150000,140000,...,15000]",
deaths_array: "[4500,4200,...,450]"
}
}
```
### Data Generation Logic
**Realistic Modeling:**
1. **Base Coverage:** Each country assigned realistic baseline from WHO data
2. **Temporal Trends:**
- 15% improvement over 24 years (global trend)
- COVID-19 dip: 8% drop in 2020-2022
- Partial recovery by 2023
3. **Regional Factors:**
- AFRO: 0.9× multiplier (lower coverage)
- EURO: 1.05× multiplier (higher coverage)
- EMRO (low-income): 0.85× multiplier (conflict/access issues)
4. **Random Variation:** ±2.5% per year to simulate real-world fluctuations
**Case Calculation:**
```javascript
const herdImmunityThreshold = 95;
const coverageGap = Math.max(0, herdImmunityThreshold - coverage1);
const baseCases = (population / 100000) * 100; // 100 cases per 100k baseline
const cases = baseCases * (1 + coverageGap / 10) * randomFactor;
```
**Death Calculation:**
```javascript
const cfr = income === "low" ? 0.03 : // 3% case fatality rate
income === "lower-middle" ? 0.02 : // 2% CFR
0.01; // 1% CFR (high-income)
const deaths = cases * cfr * randomFactor;
```
## Visual Encoding
### Color Scale (Coverage-Based)
**Expression:**
```javascript
const coverageColorExpression = [
'interpolate',
['linear'],
['get', 'coverage_dose1'],
0, '#ef4444', // < 50: Red (critical)
50, '#f59e0b', // 50-70: Orange (low)
70, '#eab308', // 70-85: Yellow (medium)
85, '#84cc16', // 85-95: Lime (good)
95, '#22c55e' // > 95: Green (excellent)
];
```
**Rationale:**
- Red signals danger (below herd immunity, endemic transmission)
- Yellow indicates transition zone (outbreaks likely)
- Green shows success (herd immunity achieved)
- Matches epidemiological thresholds for measles control
### Size Scale (Population-Based)
**Expression:**
```javascript
const populationSizeExpression = [
'interpolate',
['exponential', 0.5], // Square root scaling for better visual distribution
['get', 'population'],
1000000, 8, // 1M people → 8px radius
50000000, 18, // 50M people → 18px radius
200000000, 30, // 200M people → 30px radius
1000000000, 45 // 1B people → 45px radius
];
```
**Rationale:**
- Exponential 0.5 (square root) prevents India/China from overwhelming map
- Larger circles = more people affected by coverage gaps
- Emphasizes importance of high-population countries
### Layer Stack
**1. Glow Layer (bottom):**
```javascript
{
id: 'vaccine-glow',
type: 'circle',
paint: {
'circle-radius': ['+', populationSizeExpression, 8],
'circle-opacity': 0.2,
'circle-blur': 1
}
}
```
- Creates soft halo effect
- Enhances visibility against dark background
- Radius = main circle + 8px
**2. Main Circle Layer (top):**
```javascript
{
id: 'vaccine-circles',
type: 'circle',
paint: {
'circle-radius': populationSizeExpression,
'circle-color': coverageColorExpression,
'circle-opacity': 0.8,
'circle-stroke-width': 1.5,
'circle-stroke-color': '#ffffff',
'circle-stroke-opacity': 0.4
}
}
```
- Primary visual element
- White stroke provides definition
- 0.8 opacity allows slight overlap visibility
## Timeline Control Implementation
### HTML5 Range Input
```html
<input type="range" min="2000" max="2023" value="2015"
class="timeline-slider" id="timelineSlider">
```
**Attributes:**
- min/max: Year range bounds
- value: Initial year (2015 - midpoint)
- Step: Default 1 (whole years)
### Filter Update Logic
```javascript
slider.addEventListener('input', (e) => {
const selectedYear = parseInt(e.target.value);
yearDisplay.textContent = selectedYear;
// Update both layers to show only selected year
map.setFilter('vaccine-circles', ['==', ['get', 'year'], selectedYear]);
map.setFilter('vaccine-glow', ['==', ['get', 'year'], selectedYear]);
});
```
**Key Techniques:**
- `input` event fires continuously during drag (vs. `change` which fires on release)
- setFilter() is highly performant (GPU-accelerated)
- Both layers must be filtered to maintain visual consistency
### Custom Slider Styling
**WebKit/Blink Browsers:**
```css
.timeline-slider::-webkit-slider-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #60a5fa, #a78bfa);
box-shadow: 0 2px 8px rgba(96, 165, 250, 0.4);
}
```
**Firefox:**
```css
.timeline-slider::-moz-range-thumb {
/* Same properties */
}
```
**Hover Effect:**
```css
::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
```
## Globe Configuration
### Projection & Initial View
```javascript
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/dark-v11',
projection: 'globe', // 3D globe (vs. mercator)
center: [20, 20], // Africa-centered
zoom: 1.5 // Show full globe
});
```
### Atmosphere (Dark Theme)
```javascript
map.setFog({
color: 'rgb(10, 14, 39)', // Atmosphere base color
'high-color': 'rgb(30, 41, 82)', // Color at horizon
'horizon-blend': 0.02, // Blend amount
'space-color': 'rgb(5, 7, 20)', // Background space color
'star-intensity': 0.6 // Star brightness (0-1)
});
```
**Visual Effect:**
- Deep blue-black space background
- Subtle atmosphere glow around globe limb
- Visible stars for depth perception
- Matches dark UI theme throughout
### Globe Rotation Animation
```javascript
let userInteracting = false;
const spinGlobe = () => {
if (!userInteracting) {
map.easeTo({
center: [map.getCenter().lng + 0.1, map.getCenter().lat],
duration: 1000,
easing: (t) => t // Linear easing
});
}
};
// Pause rotation during user interaction
map.on('mousedown', () => { userInteracting = true; });
map.on('mouseup', () => { userInteracting = false; });
map.on('dragend', () => { userInteracting = false; });
map.on('pitchend', () => { userInteracting = false; });
map.on('rotateend', () => { userInteracting = false; });
setInterval(spinGlobe, 1000); // Rotate every second
```
**Behavior:**
- Gentle eastward rotation when idle
- Stops immediately when user interacts
- Resumes after interaction completes
- Creates "living" visualization feel
## Popup Styling
### Dark Theme Integration
```css
.mapboxgl-popup-content {
background: rgba(15, 23, 42, 0.98);
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(96, 165, 250, 0.3);
}
```
**Design Choices:**
- Near-opaque background (0.98 alpha) for readability
- Blue border matches UI accent color
- Consistent border radius (8px) with control panels
- Deep shadow for depth against dark background
### Content Structure
```html
<div class="popup-title">Nigeria</div>
<div class="popup-stat">
<span class="popup-stat-label">Year:</span>
<span class="popup-stat-value">2015</span>
</div>
<div class="popup-divider"></div>
<!-- More stats -->
```
**Typography:**
- Title: 16px bold, blue accent (#60a5fa)
- Labels: 13px, muted gray (#94a3b8)
- Values: 13px bold, bright white (#e0e6ed)
- Dividers separate logical groups
## Performance Optimizations
### 1. Filter-Based Rendering
- Only 60 features rendered at a time (out of 1,440)
- GPU-accelerated filtering via Mapbox expressions
- No JavaScript loops for visibility control
### 2. Single Popup Instance
- One popup created, reused for all hovers
- Avoids repeated DOM creation/destruction
- Prevents memory leaks from orphaned popup elements
### 3. Embedded Data
- All 1,440 features in client-side GeoJSON
- No server requests after initial load
- Instant year transitions
### 4. Event Debouncing
- Globe rotation uses 1-second interval (not requestAnimationFrame)
- Reduces CPU usage when idle
- Sufficient for gentle ambient motion
### 5. Vector Rendering
- Mapbox GL uses WebGL for all rendering
- Scales smoothly at any zoom level
- Efficient on retina displays
## Browser Compatibility
**Supported:**
- Chrome/Edge 79+ (Chromium)
- Firefox 78+
- Safari 14+
- Opera 66+
**Requirements:**
- WebGL support
- ES6 modules (import/export)
- CSS Grid and Flexbox
- HTML5 range input
**Not Supported:**
- Internet Explorer (no WebGL 2.0)
- Very old mobile browsers
## Code Quality
### ES6 Module Usage
```javascript
import { MAPBOX_CONFIG } from '../../mapbox_test/shared/mapbox-config.js';
import { LayerFactory, COLOR_SCALES } from '../../mapbox_test/shared/layer-factory.js';
```
**Benefits:**
- Shared configuration across all visualizations
- Centralized token management
- Reusable layer factory patterns
- Easier maintenance
### Commenting Strategy
**Pattern Attribution:**
```javascript
// Pattern from Mapbox popup-on-hover example:
// Create single popup instance before interactions to prevent flickering
const popup = new mapboxgl.Popup({...});
```
**Complex Logic:**
```javascript
// Ensure popup appears at correct location if coordinates cross antimeridian
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
```
**Section Headers:**
```javascript
// ===== TIMELINE CONTROL =====
// ===== GLOBE CONFIGURATION =====
```
### Variable Naming
**Descriptive Names:**
- `coverageColorExpression` (not `colors`)
- `populationSizeExpression` (not `sizes`)
- `selectedYear` (not `year` or `y`)
**Consistent Prefixes:**
- `map*` for Mapbox objects (mapLayer, mapSource)
- `popup*` for popup-related variables
- `vaccine*` for data objects
## Testing Recommendations
### Manual Testing Checklist
1. **Timeline Slider:**
- [ ] Drag slider smoothly through all years
- [ ] Year display updates correctly
- [ ] Circles appear/disappear as expected
- [ ] No console errors during rapid sliding
2. **Hover Popups:**
- [ ] Popup appears on mouseenter
- [ ] Popup disappears on mouseleave
- [ ] Cursor changes to pointer on hover
- [ ] No flickering when moving between circles
- [ ] Popup content matches hovered country
- [ ] Popup positioned correctly (no cutoff)
3. **Globe Interaction:**
- [ ] Globe rotates when idle
- [ ] Rotation stops on mousedown
- [ ] Can drag/rotate/zoom smoothly
- [ ] Rotation resumes after interaction
- [ ] No lag or stuttering
4. **Data Accuracy:**
- [ ] Countries appear in correct locations
- [ ] Coverage colors match legend
- [ ] Population sizes appear proportional
- [ ] All 60 countries visible in each year
- [ ] No duplicate circles in same location
5. **Responsive Behavior:**
- [ ] Layout works on desktop (1920×1080)
- [ ] Controls readable on laptop (1366×768)
- [ ] Map fills entire viewport
- [ ] Legend and controls don't overlap
### Edge Cases to Test
1. **Antimeridian Crossing:**
- Hover countries near ±180° longitude (Fiji, Russia)
- Verify popup appears on correct side
2. **Overlapping Circles:**
- Hover countries with similar coordinates
- Verify correct country data shown
3. **Rapid Interaction:**
- Quickly move mouse across many circles
- Verify no orphaned popups or stuck cursors
4. **Year Extremes:**
- Test year 2000 (earliest)
- Test year 2023 (latest)
- Verify data exists for all countries
## Future Enhancement Roadmap
### Iteration 2 (Intermediate)
**Auto-Play Feature:**
- Play/pause button
- Configurable speed (1-5 seconds per year)
- Loop option
**Trend Indicators:**
- Up/down arrows showing coverage change vs. previous year
- Color-coded arrows (green = improvement, red = decline)
**Regional Filtering:**
- Buttons to filter by WHO region (AFRO, EURO, etc.)
- "Show All" option to reset
### Iteration 3 (Advanced)
**Country Detail Panels:**
- Click country to open side panel
- Chart.js time series graph using stored arrays
- Show MCV1/MCV2 coverage trends
- Overlay cases/deaths on secondary axis
**Outbreak Event Markers:**
- Special markers for major outbreaks (>1000 cases)
- Animated pulse effect
- Popup shows outbreak details
**Comparison Mode:**
- Split screen: two years side-by-side
- Difference view: color by coverage change
- Highlight countries with largest shifts
### Iteration 4 (Expert)
**Herd Immunity Overlay:**
- Threshold line at 95% coverage
- Countries below threshold highlighted with warning indicator
- Vulnerability score calculation
**Predictive Modeling:**
- Machine learning model for outbreak prediction
- Risk heatmap based on coverage gaps + recent trends
- Scenario planning: "What if coverage drops 5%?"
**Live Data Integration:**
- WHO/UNICEF API connection
- Real-time updates (quarterly)
- Data refresh button
- Version/timestamp display
**Advanced Interactions:**
- 3D bar charts rising from country locations (Deck.gl)
- Animated transitions between years (smooth morphing)
- VR mode for immersive exploration
- Export to video (year-by-year animation)
## Lessons Learned
### What Worked Well
1. **Flattened Data Structure:**
- Simple filtering logic
- Fast rendering
- Easy to debug
2. **Single Popup Pattern:**
- No flickering issues
- Smooth user experience
- Learned directly from Mapbox example
3. **Dark Theme:**
- Reduces eye strain
- Looks professional
- Matches scientific visualization conventions
4. **Shared Architecture:**
- Reusing MAPBOX_CONFIG saved time
- Consistent patterns across project
### Challenges Encountered
1. **Antimeridian Handling:**
- Initially forgot coordinate correction logic
- Mapbox example showed the solution
- Now implemented correctly
2. **Color Scale Calibration:**
- Needed multiple iterations to find readable colors
- Final palette balances visibility with semantic meaning
3. **Population Sizing:**
- Linear scaling made India/China too dominant
- Exponential 0.5 (square root) provided better balance
### Key Takeaways
1. **Web research is invaluable:**
- Official examples show best practices
- Following established patterns prevents bugs
- Documentation is key to future maintenance
2. **Data quality matters:**
- Realistic data generation creates credible visualization
- Epidemiological parameters add authenticity
- Time investment in data pays off in insights
3. **Progressive complexity:**
- Foundation iteration should be simple but complete
- Each feature should work perfectly before adding more
- Future iterations can build on solid base
## Maintenance Notes
### Updating Mapbox Version
If updating to newer Mapbox GL JS:
1. Check changelog for breaking changes
2. Update CDN links in HTML
3. Test popup API (may change)
4. Verify globe projection compatibility
5. Test setFilter() behavior
### Updating Data
To refresh with real WHO data:
1. Download WUENIC estimates (Excel format)
2. Parse to extract MCV1/MCV2 coverage by country/year
3. Join with WHO measles surveillance data for cases/deaths
4. Replace `generateMeaslesData()` function with parser
5. Maintain same property schema
### Adding Countries
To add more countries:
1. Look up lat/lng coordinates
2. Determine WHO region and income level
3. Find baseline coverage from WUENIC
4. Add to countries array in `generateMeaslesData()`
5. Test for overlap with existing circles
## Attribution
**Web Learning:**
- Source: Mapbox GL JS Examples
- URL: https://docs.mapbox.com/mapbox-gl-js/example/popup-on-hover/
- Pattern: Hover popup with mouseenter/mouseleave events
- Applied: Country information display on hover
**Data Sources:**
- WHO/UNICEF Estimates of National Immunization Coverage (WUENIC)
- WHO Measles & Rubella Surveillance Data
- World Bank Development Indicators
**Technologies:**
- Mapbox GL JS v3.0.1
- Shared architecture from /mapbox_test/shared/
**License:**
This visualization is created for educational and demonstration purposes. Data is synthetically generated based on real-world parameters. For production use, please obtain authoritative data from WHO and national health agencies.
---
**Created:** 2025-11-08
**Iteration:** 1 (Foundation)
**Status:** Complete and functional
**Next Iteration:** Add auto-play timeline animation