# 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 = ` `; // 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 ``` **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