# 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 = `
${props.name}
Year:${props.year}
`;
// 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
```
**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
Nigeria
Year:2015
```
**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