640 lines
29 KiB
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>< 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>> 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>
|