infinite-agents-public/vaccine_timeseries/vaccine_timeseries_1_measles/index.html

640 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Measles Vaccination Coverage Timeline (2000-2023)</title>
<script src='https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.css' rel='stylesheet'/>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0a0e27;
color: #e0e6ed;
overflow: hidden;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
.controls {
position: absolute;
top: 20px;
left: 20px;
background: rgba(15, 23, 42, 0.95);
padding: 25px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
min-width: 320px;
}
.controls h1 {
font-size: 20px;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(135deg, #60a5fa, #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.controls .subtitle {
font-size: 13px;
color: #94a3b8;
margin-bottom: 20px;
font-weight: 400;
}
.timeline-control {
margin-bottom: 15px;
}
.timeline-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.timeline-label span {
font-size: 14px;
color: #cbd5e1;
font-weight: 500;
}
.year-display {
font-size: 28px;
font-weight: 700;
background: linear-gradient(135deg, #34d399, #60a5fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.timeline-slider {
width: 100%;
height: 8px;
border-radius: 4px;
background: linear-gradient(to right, #1e293b, #334155);
outline: none;
-webkit-appearance: none;
cursor: pointer;
}
.timeline-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #60a5fa, #a78bfa);
cursor: pointer;
box-shadow: 0 2px 8px rgba(96, 165, 250, 0.4);
transition: transform 0.2s;
}
.timeline-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.timeline-slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #60a5fa, #a78bfa);
cursor: pointer;
border: none;
box-shadow: 0 2px 8px rgba(96, 165, 250, 0.4);
transition: transform 0.2s;
}
.timeline-slider::-moz-range-thumb:hover {
transform: scale(1.2);
}
.timeline-range {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #64748b;
margin-top: 8px;
}
.legend {
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(15, 23, 42, 0.95);
padding: 20px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
min-width: 220px;
}
.legend h3 {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: #e0e6ed;
}
.legend-item {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 12px;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 10px;
border: 2px solid rgba(255, 255, 255, 0.2);
}
.legend-item span {
color: #cbd5e1;
}
/* Pattern from Mapbox popup-on-hover example */
.mapboxgl-popup {
max-width: 300px;
}
.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);
}
.mapboxgl-popup-close-button {
display: none;
}
.popup-title {
font-size: 16px;
font-weight: 700;
color: #60a5fa;
margin-bottom: 8px;
}
.popup-stat {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
font-size: 13px;
}
.popup-stat-label {
color: #94a3b8;
}
.popup-stat-value {
color: #e0e6ed;
font-weight: 600;
}
.popup-divider {
height: 1px;
background: rgba(255, 255, 255, 0.1);
margin: 10px 0;
}
.info-box {
margin-top: 20px;
padding: 15px;
background: rgba(96, 165, 250, 0.1);
border-radius: 8px;
border-left: 3px solid #60a5fa;
}
.info-box p {
font-size: 12px;
color: #cbd5e1;
line-height: 1.6;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="controls">
<h1>Measles Vaccination Coverage</h1>
<div class="subtitle">Global Timeline 2000-2023</div>
<div class="timeline-control">
<div class="timeline-label">
<span>Select Year</span>
<span class="year-display" id="yearDisplay">2015</span>
</div>
<input type="range" min="2000" max="2023" value="2015" class="timeline-slider" id="timelineSlider">
<div class="timeline-range">
<span>2000</span>
<span>2023</span>
</div>
</div>
<div class="info-box">
<p>Hover over countries to see detailed vaccination statistics. Circle size = population, color = coverage rate.</p>
</div>
</div>
<div class="legend">
<h3>MCV1 Coverage Rate</h3>
<div class="legend-item">
<div class="legend-color" style="background: #ef4444;"></div>
<span>&lt; 50% (Critical)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #f59e0b;"></div>
<span>50-70% (Low)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #eab308;"></div>
<span>70-85% (Medium)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #84cc16;"></div>
<span>85-95% (Good)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #22c55e;"></div>
<span>&gt; 95% (Excellent)</span>
</div>
</div>
<script type="module">
// Import shared configuration
import { MAPBOX_CONFIG } from '../../mapbox_test/shared/mapbox-config.js';
import { LayerFactory, COLOR_SCALES } from '../../mapbox_test/shared/layer-factory.js';
mapboxgl.accessToken = MAPBOX_CONFIG.accessToken;
// Generate realistic measles vaccination data for 60 countries across 24 years
function generateMeaslesData() {
const countries = [
// Africa (AFRO) - Lower coverage, higher disease burden
{ name: "Nigeria", iso3: "NGA", coords: [8.6753, 9.0820], region: "AFRO", income: "lower-middle", pop: 182202000, base_cov: 52 },
{ name: "Ethiopia", iso3: "ETH", coords: [40.4897, 9.1450], region: "AFRO", income: "low", pop: 99391000, base_cov: 58 },
{ name: "DR Congo", iso3: "COD", coords: [21.7587, -4.0383], region: "AFRO", income: "low", pop: 77267000, base_cov: 48 },
{ name: "South Africa", iso3: "ZAF", coords: [22.9375, -30.5595], region: "AFRO", income: "upper-middle", pop: 54490000, base_cov: 72 },
{ name: "Kenya", iso3: "KEN", coords: [37.9062, -0.0236], region: "AFRO", income: "lower-middle", pop: 46051000, base_cov: 64 },
{ name: "Uganda", iso3: "UGA", coords: [32.2903, 1.3733], region: "AFRO", income: "low", pop: 39032000, base_cov: 60 },
{ name: "Ghana", iso3: "GHA", coords: [-1.0232, 7.9465], region: "AFRO", income: "lower-middle", pop: 27410000, base_cov: 78 },
{ name: "Mozambique", iso3: "MOZ", coords: [35.5296, -18.6657], region: "AFRO", income: "low", pop: 27978000, base_cov: 55 },
{ name: "Madagascar", iso3: "MDG", coords: [46.8691, -18.7669], region: "AFRO", income: "low", pop: 24235000, base_cov: 62 },
{ name: "Cameroon", iso3: "CMR", coords: [12.3547, 7.3697], region: "AFRO", income: "lower-middle", pop: 23344000, base_cov: 66 },
// Asia (SEARO/WPRO) - Mixed coverage
{ name: "India", iso3: "IND", coords: [78.9629, 20.5937], region: "SEARO", income: "lower-middle", pop: 1311051000, base_cov: 74 },
{ name: "China", iso3: "CHN", coords: [104.1954, 35.8617], region: "WPRO", income: "upper-middle", pop: 1376049000, base_cov: 89 },
{ name: "Indonesia", iso3: "IDN", coords: [113.9213, -0.7893], region: "SEARO", income: "lower-middle", pop: 257564000, base_cov: 68 },
{ name: "Pakistan", iso3: "PAK", coords: [69.3451, 30.3753], region: "EMRO", income: "lower-middle", pop: 189381000, base_cov: 61 },
{ name: "Bangladesh", iso3: "BGD", coords: [90.3563, 23.6850], region: "SEARO", income: "lower-middle", pop: 160996000, base_cov: 82 },
{ name: "Philippines", iso3: "PHL", coords: [121.7740, 12.8797], region: "WPRO", income: "lower-middle", pop: 100699000, base_cov: 71 },
{ name: "Vietnam", iso3: "VNM", coords: [108.2772, 14.0583], region: "WPRO", income: "lower-middle", pop: 91680000, base_cov: 86 },
{ name: "Thailand", iso3: "THA", coords: [100.9925, 15.8700], region: "SEARO", income: "upper-middle", pop: 67959000, base_cov: 91 },
{ name: "Myanmar", iso3: "MMR", coords: [95.9560, 21.9162], region: "SEARO", income: "lower-middle", pop: 53370000, base_cov: 77 },
{ name: "South Korea", iso3: "KOR", coords: [127.7669, 35.9078], region: "WPRO", income: "high", pop: 50293000, base_cov: 96 },
// Europe (EURO) - High coverage
{ name: "Germany", iso3: "DEU", coords: [10.4515, 51.1657], region: "EURO", income: "high", pop: 80688000, base_cov: 93 },
{ name: "United Kingdom", iso3: "GBR", coords: [-3.4360, 55.3781], region: "EURO", income: "high", pop: 64716000, base_cov: 91 },
{ name: "France", iso3: "FRA", coords: [2.2137, 46.2276], region: "EURO", income: "high", pop: 64395000, base_cov: 88 },
{ name: "Italy", iso3: "ITA", coords: [12.5674, 41.8719], region: "EURO", income: "high", pop: 59798000, base_cov: 85 },
{ name: "Spain", iso3: "ESP", coords: [-3.7492, 40.4637], region: "EURO", income: "high", pop: 46122000, base_cov: 94 },
{ name: "Poland", iso3: "POL", coords: [19.1451, 51.9194], region: "EURO", income: "high", pop: 37948000, base_cov: 96 },
{ name: "Romania", iso3: "ROU", coords: [24.9668, 45.9432], region: "EURO", income: "upper-middle", pop: 19511000, base_cov: 82 },
{ name: "Netherlands", iso3: "NLD", coords: [5.2913, 52.1326], region: "EURO", income: "high", pop: 16925000, base_cov: 94 },
{ name: "Belgium", iso3: "BEL", coords: [4.4699, 50.5039], region: "EURO", income: "high", pop: 11299000, base_cov: 93 },
{ name: "Greece", iso3: "GRC", coords: [21.8243, 39.0742], region: "EURO", income: "high", pop: 10858000, base_cov: 90 },
// Americas (AMRO/PAHO)
{ name: "United States", iso3: "USA", coords: [-95.7129, 37.0902], region: "AMRO", income: "high", pop: 321774000, base_cov: 91 },
{ name: "Brazil", iso3: "BRA", coords: [-51.9253, -14.2350], region: "AMRO", income: "upper-middle", pop: 207848000, base_cov: 83 },
{ name: "Mexico", iso3: "MEX", coords: [-102.5528, 23.6345], region: "AMRO", income: "upper-middle", pop: 127017000, base_cov: 88 },
{ name: "Colombia", iso3: "COL", coords: [-74.2973, 4.5709], region: "AMRO", income: "upper-middle", pop: 48229000, base_cov: 86 },
{ name: "Argentina", iso3: "ARG", coords: [-63.6167, -38.4161], region: "AMRO", income: "upper-middle", pop: 43417000, base_cov: 92 },
{ name: "Canada", iso3: "CAN", coords: [-106.3468, 56.1304], region: "AMRO", income: "high", pop: 35940000, base_cov: 89 },
{ name: "Peru", iso3: "PER", coords: [-75.0152, -9.1900], region: "AMRO", income: "upper-middle", pop: 31377000, base_cov: 81 },
{ name: "Venezuela", iso3: "VEN", coords: [-66.5897, 6.4238], region: "AMRO", income: "upper-middle", pop: 31108000, base_cov: 79 },
{ name: "Chile", iso3: "CHL", coords: [-71.5430, -35.6751], region: "AMRO", income: "high", pop: 17948000, base_cov: 90 },
{ name: "Ecuador", iso3: "ECU", coords: [-78.1834, -1.8312], region: "AMRO", income: "upper-middle", pop: 16145000, base_cov: 84 },
// Middle East (EMRO)
{ name: "Iran", iso3: "IRN", coords: [53.6880, 32.4279], region: "EMRO", income: "upper-middle", pop: 79109000, base_cov: 88 },
{ name: "Turkey", iso3: "TUR", coords: [35.2433, 38.9637], region: "EURO", income: "upper-middle", pop: 78666000, base_cov: 87 },
{ name: "Iraq", iso3: "IRQ", coords: [43.6793, 33.2232], region: "EMRO", income: "upper-middle", pop: 36423000, base_cov: 64 },
{ name: "Saudi Arabia", iso3: "SAU", coords: [45.0792, 23.8859], region: "EMRO", income: "high", pop: 31540000, base_cov: 94 },
{ name: "Yemen", iso3: "YEM", coords: [48.5164, 15.5527], region: "EMRO", income: "low", pop: 26832000, base_cov: 45 },
{ name: "Syria", iso3: "SYR", coords: [38.9968, 34.8021], region: "EMRO", income: "low", pop: 18502000, base_cov: 52 },
{ name: "Jordan", iso3: "JOR", coords: [36.2384, 30.5852], region: "EMRO", income: "upper-middle", pop: 7595000, base_cov: 91 },
{ name: "United Arab Emirates", iso3: "ARE", coords: [53.8478, 23.4241], region: "EMRO", income: "high", pop: 9157000, base_cov: 92 },
{ name: "Lebanon", iso3: "LBN", coords: [35.8623, 33.8547], region: "EMRO", income: "upper-middle", pop: 5851000, base_cov: 75 },
{ name: "Oman", iso3: "OMN", coords: [55.9233, 21.4735], region: "EMRO", income: "high", pop: 4491000, base_cov: 93 },
// Oceania (WPRO)
{ name: "Australia", iso3: "AUS", coords: [133.7751, -25.2744], region: "WPRO", income: "high", pop: 23969000, base_cov: 92 },
{ name: "Papua New Guinea", iso3: "PNG", coords: [143.9555, -6.3150], region: "WPRO", income: "lower-middle", pop: 7619000, base_cov: 58 },
{ name: "New Zealand", iso3: "NZL", coords: [174.8860, -40.9006], region: "WPRO", income: "high", pop: 4529000, base_cov: 91 },
// Additional key countries
{ name: "Afghanistan", iso3: "AFG", coords: [67.7100, 33.9391], region: "EMRO", income: "low", pop: 32527000, base_cov: 42 },
{ name: "Ukraine", iso3: "UKR", coords: [31.1656, 48.3794], region: "EURO", income: "lower-middle", pop: 45154000, base_cov: 79 },
{ name: "Sudan", iso3: "SDN", coords: [30.2176, 12.8628], region: "EMRO", income: "lower-middle", pop: 39579000, base_cov: 51 },
{ name: "Algeria", iso3: "DZA", coords: [1.6596, 28.0339], region: "AFRO", income: "upper-middle", pop: 39667000, base_cov: 81 },
{ name: "Morocco", iso3: "MAR", coords: [-7.0926, 31.7917], region: "EMRO", income: "lower-middle", pop: 34378000, base_cov: 89 },
{ name: "Angola", iso3: "AGO", coords: [17.8739, -11.2027], region: "AFRO", income: "lower-middle", pop: 25022000, base_cov: 54 },
{ name: "Malaysia", iso3: "MYS", coords: [101.9758, 4.2105], region: "WPRO", income: "upper-middle", pop: 30331000, base_cov: 93 },
{ name: "Nepal", iso3: "NPL", coords: [84.1240, 28.3949], region: "SEARO", income: "low", pop: 28514000, base_cov: 85 }
];
const features = [];
const years = Array.from({ length: 24 }, (_, i) => 2000 + i);
countries.forEach(country => {
// Generate time series arrays
const yearlyData = years.map((year, idx) => {
// Base trend: improvement over time with COVID dip
let trend = 1 + (idx / 24) * 0.15; // 15% improvement over 24 years
if (year >= 2020 && year <= 2022) {
trend *= 0.92; // COVID-19 impact: 8% drop
}
// Regional variation
let regionalFactor = 1;
if (country.region === "AFRO") regionalFactor = 0.9;
if (country.region === "EURO") regionalFactor = 1.05;
if (country.region === "EMRO" && country.income === "low") regionalFactor = 0.85;
// Calculate coverage
let coverage1 = Math.min(98, Math.max(30, country.base_cov * trend * regionalFactor + (Math.random() - 0.5) * 5));
let coverage2 = coverage1 * 0.75; // Dose 2 typically 75% of dose 1
// Calculate cases (inverse relationship with coverage)
const herdImmunityThreshold = 95;
const coverageGap = Math.max(0, herdImmunityThreshold - coverage1);
const baseCases = (country.pop / 100000) * 100; // Base rate
const cases = Math.round(baseCases * (1 + coverageGap / 10) * (Math.random() * 0.5 + 0.75));
// Calculate deaths (case fatality rate ~1-3% in low-resource settings)
const cfr = country.income === "low" ? 0.03 : country.income === "lower-middle" ? 0.02 : 0.01;
const deaths = Math.round(cases * cfr * (Math.random() * 0.5 + 0.75));
return {
year,
coverage1: Math.round(coverage1),
coverage2: Math.round(coverage2),
cases,
deaths
};
});
// Create one feature per country-year
yearlyData.forEach(data => {
features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: country.coords
},
properties: {
name: country.name,
iso3: country.iso3,
year: data.year,
region: country.region,
income_level: country.income,
population: country.pop,
coverage_dose1: data.coverage1,
coverage_dose2: data.coverage2,
cases: data.cases,
deaths: data.deaths,
// Store complete time series for future chart use
years_array: JSON.stringify(yearlyData.map(d => d.year)),
coverage1_array: JSON.stringify(yearlyData.map(d => d.coverage1)),
coverage2_array: JSON.stringify(yearlyData.map(d => d.coverage2)),
cases_array: JSON.stringify(yearlyData.map(d => d.cases)),
deaths_array: JSON.stringify(yearlyData.map(d => d.deaths))
}
});
});
});
return {
type: "FeatureCollection",
features
};
}
const vaccineData = generateMeaslesData();
// Initialize map
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/dark-v11',
projection: 'globe',
center: [20, 20],
zoom: 1.5,
attributionControl: false
});
// Pattern from Mapbox popup-on-hover example:
// Create single popup instance before interactions to prevent flickering
const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false
});
map.on('load', () => {
// Configure atmosphere (dark theme)
map.setFog({
color: 'rgb(10, 14, 39)',
'high-color': 'rgb(30, 41, 82)',
'horizon-blend': 0.02,
'space-color': 'rgb(5, 7, 20)',
'star-intensity': 0.6
});
// Add data source
map.addSource('vaccine-data', {
type: 'geojson',
data: vaccineData,
generateId: true // Enable feature identification for hover
});
// Create coverage-based color 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)
];
// Population-based circle size
const populationSizeExpression = [
'interpolate',
['exponential', 0.5],
['get', 'population'],
1000000, 8, // 1M people
50000000, 18, // 50M people
200000000, 30, // 200M people
1000000000, 45 // 1B people
];
// Add circle layer
map.addLayer({
id: 'vaccine-circles',
type: 'circle',
source: 'vaccine-data',
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
},
// Initially filter to year 2015
filter: ['==', ['get', 'year'], 2015]
});
// Add glow layer for visual enhancement
map.addLayer({
id: 'vaccine-glow',
type: 'circle',
source: 'vaccine-data',
paint: {
'circle-radius': ['+', populationSizeExpression, 8],
'circle-color': coverageColorExpression,
'circle-opacity': 0.2,
'circle-blur': 1
},
filter: ['==', ['get', 'year'], 2015]
}, 'vaccine-circles');
// Pattern from Mapbox popup-on-hover example:
// Use mouseenter to show popup with country data
map.on('mouseenter', 'vaccine-circles', (e) => {
// Change cursor to pointer
map.getCanvas().style.cursor = 'pointer';
const coordinates = e.features[0].geometry.coordinates.slice();
const props = e.features[0].properties;
// Create popup HTML with dark theme styling
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>
<div class="popup-divider"></div>
<div class="popup-stat">
<span class="popup-stat-label">MCV1 Coverage:</span>
<span class="popup-stat-value">${props.coverage_dose1}%</span>
</div>
<div class="popup-stat">
<span class="popup-stat-label">MCV2 Coverage:</span>
<span class="popup-stat-value">${props.coverage_dose2}%</span>
</div>
<div class="popup-divider"></div>
<div class="popup-stat">
<span class="popup-stat-label">Measles Cases:</span>
<span class="popup-stat-value">${props.cases.toLocaleString()}</span>
</div>
<div class="popup-stat">
<span class="popup-stat-label">Deaths:</span>
<span class="popup-stat-value">${props.deaths.toLocaleString()}</span>
</div>
<div class="popup-divider"></div>
<div class="popup-stat">
<span class="popup-stat-label">Population:</span>
<span class="popup-stat-value">${(props.population / 1000000).toFixed(1)}M</span>
</div>
<div class="popup-stat">
<span class="popup-stat-label">Region:</span>
<span class="popup-stat-value">${props.region}</span>
</div>
`;
// 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;
}
// Display popup (reusing single instance)
popup.setLngLat(coordinates)
.setHTML(popupHTML)
.addTo(map);
});
// Pattern from Mapbox popup-on-hover example:
// Use mouseleave to remove popup and reset cursor
map.on('mouseleave', 'vaccine-circles', () => {
map.getCanvas().style.cursor = '';
popup.remove();
});
});
// Timeline slider control
const slider = document.getElementById('timelineSlider');
const yearDisplay = document.getElementById('yearDisplay');
slider.addEventListener('input', (e) => {
const selectedYear = parseInt(e.target.value);
yearDisplay.textContent = selectedYear;
// Use map.setFilter() to show only features for selected year
map.setFilter('vaccine-circles', ['==', ['get', 'year'], selectedYear]);
map.setFilter('vaccine-glow', ['==', ['get', 'year'], selectedYear]);
});
// Add navigation controls
map.addControl(new mapboxgl.NavigationControl(), 'top-right');
// Smooth globe rotation
let userInteracting = false;
const spinGlobe = () => {
if (!userInteracting) {
map.easeTo({
center: [map.getCenter().lng + 0.1, map.getCenter().lat],
duration: 1000,
easing: (t) => t
});
}
};
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);
</script>
</body>
</html>