/**
* Measles Vaccination Coverage vs. Disease Outbreaks (2000-2023)
* Using shared architecture for reliability and best practices
*/
import { MAPBOX_CONFIG } from '../../shared/mapbox-config.js';
import { generateVaccineData } from '../../shared/data-generator.js';
import { LayerFactory, COLOR_SCALES } from '../../shared/layer-factory.js';
// Generate measles data (Point geometries with realistic metrics)
const measlesData = generateVaccineData('measles');
// Initialize map with validated configuration
const map = new mapboxgl.Map({
container: 'map',
...MAPBOX_CONFIG.getMapOptions({
style: 'mapbox://styles/mapbox/dark-v11',
center: [15, 25],
zoom: 1.5,
pitch: 0
})
});
// Map load event
map.on('load', () => {
const factory = new LayerFactory(map);
// Apply dark medical atmosphere
factory.applyGlobeAtmosphere({
theme: 'dark',
customConfig: {
color: 'rgb(10, 10, 15)',
'high-color': 'rgb(25, 35, 60)',
'horizon-blend': 0.02,
'space-color': 'rgb(5, 5, 10)',
'star-intensity': 0.6
}
});
// Add data source
map.addSource('measles-data', {
type: 'geojson',
data: measlesData
});
// Create coverage layer (first dose coverage)
const coverageLayer = factory.createCircleLayer({
id: 'coverage-layer',
source: 'measles-data',
sizeProperty: 'population',
colorProperty: 'coverage_dose1',
colorScale: 'coverageReverse', // Green (high) to Red (low)
sizeRange: [4, 25],
opacityRange: [0.7, 0.85]
});
map.addLayer(coverageLayer);
// Create outbreak circles layer (sized by cases_2023)
const outbreaksLayer = factory.createCircleLayer({
id: 'outbreaks-layer',
source: 'measles-data',
sizeProperty: 'cases_2023',
colorProperty: 'deaths_2023',
sizeRange: [6, 32],
opacityRange: [0.6, 0.8]
});
// Custom styling for outbreak layer (red circles)
outbreaksLayer.paint['circle-color'] = 'rgba(239, 83, 80, 0.7)';
outbreaksLayer.paint['circle-stroke-color'] = '#ef5350';
outbreaksLayer.paint['circle-stroke-width'] = 2;
// Only show outbreaks where cases_2023 > 100 (use coalesce to handle nulls)
outbreaksLayer.filter = ['>', ['coalesce', ['get', 'cases_2023'], 0], 100];
map.addLayer(outbreaksLayer);
// Create pulse effect for major outbreaks (>1000 cases)
const pulseLayer = factory.createPulseLayer('measles-data', {
id: 'outbreaks-pulse',
sizeMultiplier: 1.8,
color: 'rgba(239, 83, 80, 0.3)',
filter: ['>', ['coalesce', ['get', 'cases_2023'], 0], 1000]
});
map.addLayer(pulseLayer);
// Setup hover effects for coverage layer
map.on('mouseenter', 'coverage-layer', (e) => {
map.getCanvas().style.cursor = 'pointer';
if (e.features.length > 0) {
const feature = e.features[0];
const props = feature.properties;
const popupContent = `
${props.coverage_dose1 < 75 ? `
` : ''}
${props.coverage_dose1 >= 95 ? `
` : ''}
`;
new mapboxgl.Popup({ offset: 15 })
.setLngLat(feature.geometry.coordinates)
.setHTML(popupContent)
.addTo(map);
}
});
map.on('mouseleave', 'coverage-layer', () => {
map.getCanvas().style.cursor = '';
popup.remove();
});
// Setup hover for outbreak layer
map.on('mouseenter', 'outbreaks-layer', (e) => {
map.getCanvas().style.cursor = 'pointer';
if (e.features.length > 0) {
const feature = e.features[0];
const props = feature.properties;
const popupContent = `
`;
new mapboxgl.Popup({ offset: 15 })
.setLngLat(feature.geometry.coordinates)
.setHTML(popupContent)
.addTo(map);
}
});
map.on('mouseleave', 'outbreaks-layer', () => {
map.getCanvas().style.cursor = '';
popup.remove();
});
// Add custom legend for coverage
const legendDiv = document.createElement('div');
legendDiv.className = 'legend';
legendDiv.style.cssText = `
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(10, 14, 26, 0.95);
padding: 20px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
z-index: 1000;
min-width: 250px;
`;
legendDiv.innerHTML = `
Legend
Vaccination Coverage (1st Dose)
95%+
80%
60%
<60%
`;
map.getContainer().parentElement.appendChild(legendDiv);
// Setup layer toggles
document.getElementById('toggle-coverage').addEventListener('change', (e) => {
const visibility = e.target.checked ? 'visible' : 'none';
map.setLayoutProperty('coverage-layer', 'visibility', visibility);
});
document.getElementById('toggle-outbreaks').addEventListener('change', (e) => {
const visibility = e.target.checked ? 'visible' : 'none';
map.setLayoutProperty('outbreaks-layer', 'visibility', visibility);
map.setLayoutProperty('outbreaks-pulse', 'visibility', visibility);
});
document.getElementById('toggle-borders').addEventListener('change', (e) => {
const visibility = e.target.checked ? 'visible' : 'none';
// This would toggle borders if we had a borders layer
// For now, we can adjust stroke on coverage layer
if (e.target.checked) {
map.setPaintProperty('coverage-layer', 'circle-stroke-width',
map.getPaintProperty('coverage-layer', 'circle-stroke-width'));
} else {
map.setPaintProperty('coverage-layer', 'circle-stroke-width', 0);
}
});
// Globe auto-rotation
let userInteracting = false;
let spinEnabled = false;
const spinGlobe = () => {
if (spinEnabled && !userInteracting && map.isStyleLoaded()) {
map.easeTo({
center: [map.getCenter().lng + 0.15, map.getCenter().lat],
duration: 100,
easing: (n) => n
});
}
requestAnimationFrame(spinGlobe);
};
// spinGlobe(); // Auto-rotation disabled
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; });
});
// Handle window resize
window.addEventListener('resize', () => {
map.resize();
});