diff --git a/docs/index.html b/docs/index.html index 009f5cd..e42b540 100644 --- a/docs/index.html +++ b/docs/index.html @@ -40,6 +40,10 @@ --low: #56B4E9; /* sky blue */ --mid: #E69F00; /* orange */ --high: #D55E00; /* vermilion */ + + --risk-low: #56B4E9; + --risk-moderate: #F2C94C; + --risk-high: #D7191C; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } @@ -261,6 +265,15 @@ font-size: 0.63rem; color: var(--muted); } + @media (max-width: 560px) { + .map-legend { + left: 0.75rem; + right: 0.75rem; + transform: none; + max-width: calc(100% - 1.5rem); + overflow-x: auto; + } + } /* ── Selected circle pulse ───────────────────────────────────── */ @keyframes circle-pulse { @@ -356,19 +369,30 @@ .panel-stat-label { font-size: 0.67rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); } .panel-stat-value { font-size: 1.05rem; font-weight: 700; color: var(--text); } - /* Panel — probability bars */ + /* Panel — risk badges */ .panel-section-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); margin-bottom: 0.4rem; } - .panel-prob-row { margin-bottom: 0.8rem; } - .panel-prob-label { - display: flex; justify-content: space-between; - font-size: 0.82rem; color: var(--text); margin-bottom: 0.3rem; + .panel-risk-list { + display: flex; + flex-direction: column; + gap: 0.45rem; + margin-bottom: 1.2rem; + } + .panel-risk-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.55rem 0; + border-bottom: 1px solid var(--border); + } + .panel-risk-row:last-child { border-bottom: none; } + .panel-risk-label { + font-size: 0.82rem; + color: var(--text); } - .panel-prob-label span:last-child { font-weight: 600; } - .panel-prob-track { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; } - .panel-prob-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease; } /* Panel — footer hint */ .panel-hint { @@ -410,6 +434,52 @@ margin-bottom: 0.75rem; } + .risk-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 5.8rem; + padding: 0.28rem 0.62rem; + border: 1px solid var(--risk-border); + border-radius: 999px; + background: var(--risk-bg); + color: var(--risk-text); + font-size: 0.72rem; + font-weight: 700; + line-height: 1; + white-space: nowrap; + } + .risk-low { + --risk-bg: rgba(86, 180, 233, 0.16); + --risk-border: rgba(86, 180, 233, 0.48); + --risk-text: #7DD3FC; + } + .risk-moderate { + --risk-bg: rgba(242, 201, 76, 0.18); + --risk-border: rgba(242, 201, 76, 0.58); + --risk-text: #FFE08A; + } + .risk-high { + --risk-bg: rgba(215, 25, 28, 0.18); + --risk-border: rgba(215, 25, 28, 0.58); + --risk-text: #FF7B72; + } + [data-theme="light"] .risk-low { + --risk-bg: rgba(86, 180, 233, 0.20); + --risk-border: rgba(9, 105, 218, 0.38); + --risk-text: #0969DA; + } + [data-theme="light"] .risk-moderate { + --risk-bg: rgba(242, 201, 76, 0.35); + --risk-border: rgba(143, 99, 0, 0.34); + --risk-text: #6F4E00; + } + [data-theme="light"] .risk-high { + --risk-bg: rgba(215, 25, 28, 0.14); + --risk-border: rgba(207, 34, 46, 0.34); + --risk-text: #CF222E; + } + h2 { font-size: 1.75rem; font-weight: 800; @@ -498,22 +568,7 @@ tr:last-child td { border-bottom: none; } tr:hover td { background: rgba(255,255,255,0.025); } - .prob-bar { - display: flex; - align-items: center; - gap: 0.5rem; - } - .prob-bar-track { - flex: 1; - height: 6px; - background: var(--border); - border-radius: 3px; - overflow: hidden; - } - .prob-bar-fill { - height: 100%; - border-radius: 3px; - } + .risk-cell .risk-badge { min-width: 6.2rem; } /* About section */ .about-grid { @@ -607,7 +662,7 @@
- Circle size = P(≥10 cases, 60-day sim). Color = MMR vaccination rate. Select a city to explore its risk. + Circle size = relative simulated chance of observing ≥10 cases. Color = MMR vaccination rate. Select a city to compare outcomes.
@@ -617,12 +672,12 @@
- Size → P(≥10 cases) + Size → chance of ≥10 cases
- 10% + Lower - 70% + Higher
@@ -721,7 +776,7 @@

Vaccination Data

Data

City-Level Risk Summary

- Outbreak probability estimated from the final day (day 60) of 200 independent simulations per city. + Risk labels summarize the probability of observing at least the listed number of cases by day 60.

@@ -732,9 +787,9 @@

City-Level Risk Summary

MMR coverage Population Seed cases † - Prob(≥10 cases) - Prob(≥20 cases) - Prob(≥50 cases) + Risk (≥10 cases) + Risk (≥20 cases) + Risk (≥50 cases) @@ -811,15 +866,39 @@

How to get in touch

/* CITIES is defined in data.js – auto-generated by docs/generate_data.py from scenarios/*_outbreak_size.csv, data/mmr.csv, data/population.csv */ +/* ── Risk labels for observing cases ────────────────────────── */ +function riskLevel(prob) { + if (prob < 0.10) return { + label: 'Low', + className: 'risk-low', + color: getComputedStyle(document.documentElement).getPropertyValue('--risk-low').trim() + }; + if (prob <= 0.40) return { + label: 'Moderate', + className: 'risk-moderate', + color: getComputedStyle(document.documentElement).getPropertyValue('--risk-moderate').trim() + }; + return { + label: 'High', + className: 'risk-high', + color: getComputedStyle(document.documentElement).getPropertyValue('--risk-high').trim() + }; +} + +function riskBadgeHtml(prob) { + const risk = riskLevel(prob); + return `${risk.label}`; +} + /* ── Colour fill by vaccination rate ────────────────────────── */ /* ColorBrewer RdYlBu 4-class diverging scale (Brewer 1994). - Centred at the ~95 % measles herd-immunity threshold (R₀ ≈12–18). + Centred at the ~95 % measles herd-immunity threshold (R0 approx. 12-18). Red = low coverage / higher risk; Blue = high coverage / lower risk. */ function vaccColor(vacc) { - if (vacc >= 95) return '#2c7bb6'; // blue — at/above herd immunity - if (vacc >= 92) return '#abd9e9'; // lt blue — approaching threshold - if (vacc >= 90) return '#fdae61'; // orange — below threshold - return '#d7191c'; // red — well below threshold + if (vacc >= 95) return '#2c7bb6'; // blue + if (vacc >= 92) return '#abd9e9'; // light blue + if (vacc >= 90) return '#fdae61'; // orange + return '#d7191c'; // red } /* ── Radius scaled by prob_10 (min 6 px, max 35 px total) ────── */ @@ -945,12 +1024,11 @@

How to get in touch

new RecenterControl().addTo(map); /* ── Theme-aware popup (WCAG 2.1: ≥4.5:1 contrast in both modes) ── */ -function buildPopupHtml(city, color) { +function buildPopupHtml(city) { const dark = document.documentElement.getAttribute('data-theme') !== 'light'; const tc = dark ? '#e6edf3' : '#1f2328'; // text colour const mc = dark ? '#8b949e' : '#57606a'; // muted label const hr = dark ? '#30363d' : '#d0d7de'; // divider - const pct = v => (v * 100).toFixed(1) + '%'; return `
${city.name} @@ -960,13 +1038,13 @@

How to get in touch

${city.vacc}% Population ${city.pop.toLocaleString()} - Outbreak probability + Risk of observing cases ≥ 10 cases - ${pct(city.prob_10)} + ${riskBadgeHtml(city.prob_10)} ≥ 20 cases - ${pct(city.prob_20)} + ${riskBadgeHtml(city.prob_20)} ≥ 50 cases - ${pct(city.prob_50)} + ${riskBadgeHtml(city.prob_50)}
`; } @@ -980,14 +1058,11 @@

How to get in touch

function isPanelVisible() { return window.innerWidth >= PANEL_BP; } function buildPanelHtml(city, color) { - const pct = v => (v * 100).toFixed(1) + '%'; - function probBar(label, v) { + function riskRow(label, v) { return ` -
-
${label}${pct(v)}
-
-
-
+
+ ${label} + ${riskBadgeHtml(v)}
`; } return ` @@ -1005,11 +1080,12 @@

${city.name}

${(city.pop / 1e6).toFixed(2)}M
-

Outbreak Probability

-

200 simulations · 60-day window · single seed case

- ${probBar('≥ 10 cases', city.prob_10)} - ${probBar('≥ 20 cases', city.prob_20)} - ${probBar('≥ 50 cases', city.prob_50)} +

Risk of Observing Cases

+
+ ${riskRow('≥ 10 cases', city.prob_10)} + ${riskRow('≥ 20 cases', city.prob_20)} + ${riskRow('≥ 50 cases', city.prob_50)} +

Click a different city on the map to compare.

`; } @@ -1151,25 +1227,15 @@

${city.name}

/* ── Populate city table ─────────────────────────────────────── */ const tbody = document.getElementById('city-tbody'); [...CITIES].sort((a, b) => b.prob_10 - a.prob_10).forEach(city => { - const color = vaccColor(city.vacc); - const pct = v => (v * 100).toFixed(1) + '%'; - const bar = (v, c) => ` -
-
-
-
- ${pct(v)} -
`; - const tr = document.createElement('tr'); tr.innerHTML = ` ${city.name} ${city.vacc}% ${(city.pop / 1e6).toFixed(2)}M ${city.seed_cases} - ${bar(city.prob_10, color)} - ${bar(city.prob_20, color)} - ${bar(city.prob_50, color)}`; + ${riskBadgeHtml(city.prob_10)} + ${riskBadgeHtml(city.prob_20)} + ${riskBadgeHtml(city.prob_50)}`; tbody.appendChild(tr); });