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

22 KiB
Raw Blame History

Technical Documentation: Measles Vaccination Timeline Visualization

Project Metadata

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:

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:

// 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:

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:

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:

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:

// 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:

// 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:

{
  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:

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:

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:

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:

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):

{
    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):

{
    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

<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

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:

.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:

.timeline-slider::-moz-range-thumb {
    /* Same properties */
}

Hover Effect:

::-webkit-slider-thumb:hover {
    transform: scale(1.2);
}

Globe Configuration

Projection & Initial View

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)

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

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

.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

<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

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:

// Pattern from Mapbox popup-on-hover example:
// Create single popup instance before interactions to prevent flickering
const popup = new mapboxgl.Popup({...});

Complex Logic:

// 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:

// ===== 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:

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