# 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
