827 lines
22 KiB
Markdown
827 lines
22 KiB
Markdown
# 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
|