Add 10-day outlook panel
Browse files- index.html +170 -149
index.html
CHANGED
|
@@ -1,149 +1,170 @@
|
|
| 1 |
-
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 2 |
-
<title>Global Weather Model Leaderboard</title>
|
| 3 |
-
<style>
|
| 4 |
-
:root{--bg:#061226;--panel:#0d2247;--panel2:#11305f;--line:#1d3f78;--ink:#f2f7ff;--mut:#9db4d8;--cy:#35d0ff;--gold:#ffc83d;--red:#ff4d5e;--ai:#35d0ff;--ph:#b9c7e0}
|
| 5 |
-
*{box-sizing:border-box}html{background:var(--bg)}
|
| 6 |
-
body{margin:0;color:var(--ink);font:15px/1.45 "Segoe UI",system-ui,-apple-system,Roboto,sans-serif;background:radial-gradient(1200px 600px at 80% -10%,#16407f 0%,transparent 60%),linear-gradient(180deg,#07162f,#050d1c 70%);min-height:100vh;font-variant-numeric:tabular-nums}
|
| 7 |
-
.top{display:flex;align-items:center;gap:14px;padding:12px 18px;background:linear-gradient(90deg,#0b2a5c,#0a1c3a);border-bottom:2px solid var(--cy);flex-wrap:wrap}
|
| 8 |
-
.brand{font-weight:800;letter-spacing:.06em;font-size:17px;text-transform:uppercase}.brand b{color:var(--cy)}
|
| 9 |
-
.live{display:inline-flex;align-items:center;gap:6px;background:var(--red);color:#fff;font-weight:800;font-size:11px;letter-spacing:.1em;padding:3px 9px;border-radius:4px}
|
| 10 |
-
.live i{width:7px;height:7px;border-radius:50%;background:#fff;animation:bl 1.2s infinite}@keyframes bl{50%{opacity:.25}}
|
| 11 |
-
.stamp{margin-left:auto;color:var(--mut);font-size:13px;text-align:right}.stamp b{color:var(--ink)}
|
| 12 |
-
.tick{background:#040b18;border-bottom:1px solid var(--line);overflow:hidden;white-space:nowrap;font-size:13px;font-weight:600}
|
| 13 |
-
.tick span{display:inline-block;padding:7px 0;animation:mq 45s linear infinite}.tick b{color:var(--gold)}.tick em{color:var(--cy);font-style:normal}
|
| 14 |
-
@keyframes mq{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
| 15 |
-
@media(prefers-reduced-motion:reduce){.tick span,.live i{animation:none}}
|
| 16 |
-
main{max-width:1180px;margin:0 auto;padding:16px 16px 40px}
|
| 17 |
-
.ctl{display:flex;flex-wrap:wrap;gap:10px;margin:4px 0 16px}
|
| 18 |
-
.ctl label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--mut);display:flex;flex-direction:column;gap:4px;font-weight:700}
|
| 19 |
-
select{font:inherit;font-weight:600;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink);max-width:100%}
|
| 20 |
-
.grid{display:grid;grid-template-columns:1.25fr 1fr;gap:14px}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}
|
| 21 |
-
@media(max-width:860px){.grid,.grid2{grid-template-columns:1fr}}
|
| 22 |
-
.pn{background:linear-gradient(180deg,var(--panel2),var(--panel));border:1px solid var(--line);border-radius:14px;padding:16px;position:relative;overflow:hidden;min-width:0}
|
| 23 |
-
.pn h2{margin:0 0 12px;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--cy);font-weight:800}
|
| 24 |
-
.champ{display:flex;gap:16px;align-items:center}.medal{flex:0 0 auto;width:74px;height:74px;border-radius:50%;display:grid;place-items:center;font-size:34px;font-weight:900;color:#3a2a00;background:radial-gradient(circle at 30% 25%,#fff3b0,#ffc83d 55%,#c98a00);box-shadow:0 0 0 4px rgba(255,200,61,.18)}
|
| 25 |
-
.cn{font-size:30px;font-weight:800;line-height:1.1}.big{font-size:54px;font-weight:900;line-height:1;letter-spacing:-.02em}.big small{font-size:18px;font-weight:700;color:var(--mut);margin-left:4px}
|
| 26 |
-
.sub{color:var(--mut);font-size:13px}.fl{vertical-align:-2px;margin-right:7px;border-radius:2px;box-shadow:0 0 0 1px rgba(255,255,255,.25)}
|
| 27 |
-
.ty{font-size:10px;font-weight:800;letter-spacing:.06em;border-radius:5px;padding:2px 7px;text-transform:uppercase;white-space:nowrap}.ty.ai{background:rgba(53,208,255,.16);color:var(--ai);border:1px solid rgba(53,208,255,.5)}.ty.ph{background:rgba(185,199,224,.12);color:var(--ph);border:1px solid rgba(185,199,224,.35)}
|
| 28 |
-
.run{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}.rn{background:rgba(4,11,24,.45);border:1px solid var(--line);border-radius:10px;padding:10px 12px;min-width:0}.rn .k{font-size:11px;color:var(--mut);font-weight:800;letter-spacing:.1em}.rn .v{font-size:20px;font-weight:800}
|
| 29 |
-
.dw{border-color:var(--cy);background:linear-gradient(160deg,#0f3d7a,#0a2350 60%,#081a3a)}.dw:before{content:"";position:absolute;inset:-40% -20% auto auto;width:320px;height:320px;background:radial-gradient(circle,rgba(53,208,255,.28),transparent 65%);pointer-events:none}
|
| 30 |
-
.dw .big{color:var(--cy)}.gain{display:inline-block;margin-top:10px;background:rgba(53,208,255,.14);border:1px solid rgba(53,208,255,.5);color:#dff6ff;font-weight:800;padding:6px 12px;border-radius:99px;font-size:14px}
|
| 31 |
-
.bar{display:grid;grid-template-columns:26px minmax(0,1.3fr) minmax(0,2.6fr) 50px;gap:10px;align-items:center;padding:7px 0;border-bottom:1px solid rgba(29,63,120,.6)}.bar:last-child{border:0}
|
| 32 |
-
.rk{font-weight:900;color:var(--mut);text-align:center}.bar.f .rk{color:var(--gold)}.nm{font-weight:700;min-width:0}.nm .d{display:block;font-size:11px;color:var(--mut);font-weight:600;margin-top:2px}
|
| 33 |
-
.trk{height:16px;background:rgba(4,11,24,.6);border-radius:8px;overflow:hidden}.trk i{display:block;height:100%;border-radius:8px}.val{font-weight:800;text-align:right}
|
| 34 |
-
.bar.dwr{background:rgba(53,208,255,.07);border-radius:8px;border-bottom:0;margin-bottom:4px;padding:8px 6px}
|
| 35 |
-
@media(max-width:560px){.bar{grid-template-columns:22px minmax(0,1fr) 46px}.trk{grid-column:2/4;grid-row:2;height:10px}.big{font-size:44px}.cn{font-size:24px}.medal{width:58px;height:58px;font-size:26px}}
|
| 36 |
-
svg{width:100%;height:auto;display:block}.lg{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:10px;font-size:12px;color:var(--mut)}.lg i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:5px;vertical-align:-1px}
|
| 37 |
-
.mo{display:grid;grid-template-columns:repeat(auto-fill,minmax(98px,1fr));gap:8px}.mc{background:rgba(4,11,24,.45);border:1px solid var(--line);border-radius:10px;padding:9px 10px;border-top:3px solid var(--cy)}.mc .m{font-size:11px;font-weight:800;letter-spacing:.1em;color:var(--mut)}.mc .w{font-weight:800;font-size:13px;margin-top:3px}.mc .e{font-size:12px;color:var(--mut)}
|
| 38 |
-
.
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
<
|
| 43 |
-
<div class="
|
| 44 |
-
<
|
| 45 |
-
<
|
| 46 |
-
<
|
| 47 |
-
<label>
|
| 48 |
-
</
|
| 49 |
-
<
|
| 50 |
-
<
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
<
|
| 54 |
-
<div class="pn"
|
| 55 |
-
<div class="
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
<
|
| 59 |
-
<div class="pn"
|
| 60 |
-
<div class="pn"
|
| 61 |
-
<
|
| 62 |
-
<
|
| 63 |
-
</
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
const
|
| 71 |
-
const
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
}
|
| 84 |
-
const
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
for(const m of
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
const
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
function
|
| 107 |
-
const
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 2 |
+
<title>Global Weather Model Leaderboard</title>
|
| 3 |
+
<style>
|
| 4 |
+
:root{--bg:#061226;--panel:#0d2247;--panel2:#11305f;--line:#1d3f78;--ink:#f2f7ff;--mut:#9db4d8;--cy:#35d0ff;--gold:#ffc83d;--red:#ff4d5e;--ai:#35d0ff;--ph:#b9c7e0}
|
| 5 |
+
*{box-sizing:border-box}html{background:var(--bg)}
|
| 6 |
+
body{margin:0;color:var(--ink);font:15px/1.45 "Segoe UI",system-ui,-apple-system,Roboto,sans-serif;background:radial-gradient(1200px 600px at 80% -10%,#16407f 0%,transparent 60%),linear-gradient(180deg,#07162f,#050d1c 70%);min-height:100vh;font-variant-numeric:tabular-nums}
|
| 7 |
+
.top{display:flex;align-items:center;gap:14px;padding:12px 18px;background:linear-gradient(90deg,#0b2a5c,#0a1c3a);border-bottom:2px solid var(--cy);flex-wrap:wrap}
|
| 8 |
+
.brand{font-weight:800;letter-spacing:.06em;font-size:17px;text-transform:uppercase}.brand b{color:var(--cy)}
|
| 9 |
+
.live{display:inline-flex;align-items:center;gap:6px;background:var(--red);color:#fff;font-weight:800;font-size:11px;letter-spacing:.1em;padding:3px 9px;border-radius:4px}
|
| 10 |
+
.live i{width:7px;height:7px;border-radius:50%;background:#fff;animation:bl 1.2s infinite}@keyframes bl{50%{opacity:.25}}
|
| 11 |
+
.stamp{margin-left:auto;color:var(--mut);font-size:13px;text-align:right}.stamp b{color:var(--ink)}
|
| 12 |
+
.tick{background:#040b18;border-bottom:1px solid var(--line);overflow:hidden;white-space:nowrap;font-size:13px;font-weight:600}
|
| 13 |
+
.tick span{display:inline-block;padding:7px 0;animation:mq 45s linear infinite}.tick b{color:var(--gold)}.tick em{color:var(--cy);font-style:normal}
|
| 14 |
+
@keyframes mq{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
| 15 |
+
@media(prefers-reduced-motion:reduce){.tick span,.live i{animation:none}}
|
| 16 |
+
main{max-width:1180px;margin:0 auto;padding:16px 16px 40px}
|
| 17 |
+
.ctl{display:flex;flex-wrap:wrap;gap:10px;margin:4px 0 16px}
|
| 18 |
+
.ctl label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--mut);display:flex;flex-direction:column;gap:4px;font-weight:700}
|
| 19 |
+
select{font:inherit;font-weight:600;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink);max-width:100%}
|
| 20 |
+
.grid{display:grid;grid-template-columns:1.25fr 1fr;gap:14px}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}
|
| 21 |
+
@media(max-width:860px){.grid,.grid2{grid-template-columns:1fr}}
|
| 22 |
+
.pn{background:linear-gradient(180deg,var(--panel2),var(--panel));border:1px solid var(--line);border-radius:14px;padding:16px;position:relative;overflow:hidden;min-width:0}
|
| 23 |
+
.pn h2{margin:0 0 12px;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--cy);font-weight:800}
|
| 24 |
+
.champ{display:flex;gap:16px;align-items:center}.medal{flex:0 0 auto;width:74px;height:74px;border-radius:50%;display:grid;place-items:center;font-size:34px;font-weight:900;color:#3a2a00;background:radial-gradient(circle at 30% 25%,#fff3b0,#ffc83d 55%,#c98a00);box-shadow:0 0 0 4px rgba(255,200,61,.18)}
|
| 25 |
+
.cn{font-size:30px;font-weight:800;line-height:1.1}.big{font-size:54px;font-weight:900;line-height:1;letter-spacing:-.02em}.big small{font-size:18px;font-weight:700;color:var(--mut);margin-left:4px}
|
| 26 |
+
.sub{color:var(--mut);font-size:13px}.fl{vertical-align:-2px;margin-right:7px;border-radius:2px;box-shadow:0 0 0 1px rgba(255,255,255,.25)}
|
| 27 |
+
.ty{font-size:10px;font-weight:800;letter-spacing:.06em;border-radius:5px;padding:2px 7px;text-transform:uppercase;white-space:nowrap}.ty.ai{background:rgba(53,208,255,.16);color:var(--ai);border:1px solid rgba(53,208,255,.5)}.ty.ph{background:rgba(185,199,224,.12);color:var(--ph);border:1px solid rgba(185,199,224,.35)}
|
| 28 |
+
.run{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}.rn{background:rgba(4,11,24,.45);border:1px solid var(--line);border-radius:10px;padding:10px 12px;min-width:0}.rn .k{font-size:11px;color:var(--mut);font-weight:800;letter-spacing:.1em}.rn .v{font-size:20px;font-weight:800}
|
| 29 |
+
.dw{border-color:var(--cy);background:linear-gradient(160deg,#0f3d7a,#0a2350 60%,#081a3a)}.dw:before{content:"";position:absolute;inset:-40% -20% auto auto;width:320px;height:320px;background:radial-gradient(circle,rgba(53,208,255,.28),transparent 65%);pointer-events:none}
|
| 30 |
+
.dw .big{color:var(--cy)}.gain{display:inline-block;margin-top:10px;background:rgba(53,208,255,.14);border:1px solid rgba(53,208,255,.5);color:#dff6ff;font-weight:800;padding:6px 12px;border-radius:99px;font-size:14px}
|
| 31 |
+
.bar{display:grid;grid-template-columns:26px minmax(0,1.3fr) minmax(0,2.6fr) 50px;gap:10px;align-items:center;padding:7px 0;border-bottom:1px solid rgba(29,63,120,.6)}.bar:last-child{border:0}
|
| 32 |
+
.rk{font-weight:900;color:var(--mut);text-align:center}.bar.f .rk{color:var(--gold)}.nm{font-weight:700;min-width:0}.nm .d{display:block;font-size:11px;color:var(--mut);font-weight:600;margin-top:2px}
|
| 33 |
+
.trk{height:16px;background:rgba(4,11,24,.6);border-radius:8px;overflow:hidden}.trk i{display:block;height:100%;border-radius:8px}.val{font-weight:800;text-align:right}
|
| 34 |
+
.bar.dwr{background:rgba(53,208,255,.07);border-radius:8px;border-bottom:0;margin-bottom:4px;padding:8px 6px}
|
| 35 |
+
@media(max-width:560px){.bar{grid-template-columns:22px minmax(0,1fr) 46px}.trk{grid-column:2/4;grid-row:2;height:10px}.big{font-size:44px}.cn{font-size:24px}.medal{width:58px;height:58px;font-size:26px}}
|
| 36 |
+
svg{width:100%;height:auto;display:block}.lg{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:10px;font-size:12px;color:var(--mut)}.lg i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:5px;vertical-align:-1px}
|
| 37 |
+
.mo{display:grid;grid-template-columns:repeat(auto-fill,minmax(98px,1fr));gap:8px}.mc{background:rgba(4,11,24,.45);border:1px solid var(--line);border-radius:10px;padding:9px 10px;border-top:3px solid var(--cy)}.mc .m{font-size:11px;font-weight:800;letter-spacing:.1em;color:var(--mut)}.mc .w{font-weight:800;font-size:13px;margin-top:3px}.mc .e{font-size:12px;color:var(--mut)}
|
| 38 |
+
.ol{display:grid;grid-template-columns:repeat(10,minmax(0,1fr));gap:8px;margin-bottom:14px}.ot1{background:linear-gradient(180deg,#134a8f,#0b2a5c);border:1px solid #2a66b8;border-radius:10px;padding:8px 4px;text-align:center}.ot1 .dn{font-size:11px;font-weight:800;letter-spacing:.08em;color:var(--mut)}.ot1 .dd{font-size:11px;color:var(--mut)}.ot1 .tv{font-size:24px;font-weight:900;color:#fff;line-height:1.2}.ot1 .sp{font-size:10px;color:var(--cy);font-weight:700}
|
| 39 |
+
@media(max-width:700px){.ol{grid-template-columns:repeat(5,minmax(0,1fr))}}
|
| 40 |
+
.tw{overflow-x:auto}.ot{border-collapse:collapse;width:100%;min-width:640px;font-size:13px}.ot th,.ot td{padding:6px 8px;text-align:right;border-bottom:1px solid rgba(29,63,120,.6);white-space:nowrap}.ot th:first-child,.ot td:first-child{text-align:left;position:sticky;left:0;background:#0f2a55}.ot th{color:var(--mut);font-size:11px;letter-spacing:.06em}.ot tr.dwo td{color:var(--cy);font-weight:800}
|
| 41 |
+
.note{color:var(--mut);font-size:13px;margin:0}.note b{color:var(--ink)}footer{color:var(--mut);font-size:12px;margin-top:18px}a{color:var(--cy)}#status{color:var(--mut);font-size:13px;margin-bottom:12px}
|
| 42 |
+
</style></head><body>
|
| 43 |
+
<div class="top"><span class="live"><i></i>LIVE</span><span class="brand">Global Weather Model <b>Leaderboard</b></span><span class="stamp" id="stamp">Loading…</span></div>
|
| 44 |
+
<div class="tick"><span id="tick">Loading the latest verification…</span></div>
|
| 45 |
+
<main>
|
| 46 |
+
<div class="ctl">
|
| 47 |
+
<label>Forecast lead<select id="lead"></select></label>
|
| 48 |
+
<label>Period<select id="period"><option value="day">Single day</option><option value="7">Last 7 days</option><option value="14">Last 14 days</option><option value="30" selected>Last 30 days</option><option value="90">Last 90 days</option><option value="ytd">2026 year to date</option></select></label>
|
| 49 |
+
<label>Verification date<select id="date"></select></label>
|
| 50 |
+
<label>City<select id="city"><option value="-1">All 24 cities</option></select></label>
|
| 51 |
+
</div>
|
| 52 |
+
<div id="status">Loading forecasts and observations…</div>
|
| 53 |
+
<div class="grid">
|
| 54 |
+
<div class="pn"><h2 id="h1">Most accurate public model</h2><div id="champ"></div></div>
|
| 55 |
+
<div class="pn dw"><h2>Darwin-Prediction-Weather</h2><div id="dwc"></div></div>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="pn" style="margin-top:14px"><h2 id="h2">Accuracy board</h2><div id="bars"></div></div>
|
| 58 |
+
<div class="grid2">
|
| 59 |
+
<div class="pn"><h2>Error by forecast day</h2><div id="curve"></div><div class="lg" id="lg1"></div></div>
|
| 60 |
+
<div class="pn"><h2>Most accurate model by city</h2><div id="map"></div><div class="lg" id="lg2"></div></div>
|
| 61 |
+
</div>
|
| 62 |
+
<div class="pn" style="margin-top:14px"><h2 id="h4">10-day outlook</h2>
|
| 63 |
+
<div class="ctl" style="margin:0 0 12px"><label>Outlook city<select id="ocity"></select></label></div>
|
| 64 |
+
<div class="ol" id="otiles"></div><div class="tw"><table class="ot" id="otab"></table></div><p class="note" id="onote" style="margin-top:10px"></p></div>
|
| 65 |
+
<div class="pn" style="margin-top:14px"><h2 id="h3">Monthly champions 2026</h2><div class="mo" id="mo"></div></div>
|
| 66 |
+
<div class="pn" style="margin-top:14px"><h2>How it is scored</h2>
|
| 67 |
+
<p class="note">Every day each model's forecast is recorded for up to 10 days ahead. When the observed value for a day is published, every forecast made for that day is scored and the cumulative record is updated, once a day. A forecast issued N days earlier is compared with the ERA5 daily maximum 2 m temperature for the same UTC day at each city. Lower RMSE ranks higher. Models are compared on the same cases: days and cities where every listed model issued a forecast. Type shows how a model works: <b>AI</b> models learn the forecast from data, <b>Physics (Supercomputer)</b> models solve the equations of the atmosphere on supercomputers. ERA5 is published about five days after the fact, so the latest verification date trails today. Forecasts 1 to 7 days ahead are scored from 2026-01-01; forecasts 8 to 10 days ahead are archived from 2026-11-13 and enter the ranking as their target days are verified. <b>Darwin-Prediction-Weather</b> is VIDRAFT's fusion engine: local bias correction plus skill-weighted fusion of the public models, fitted only on days before the one being scored. It is shown alongside the ranking and is not ranked among the public models. In the 10-day outlook the fusion for days beyond the longest scored lead uses that lead's calibration.</p></div>
|
| 68 |
+
<footer>Full daily record since 2026-01-01 (every model forecast and the ERA5 value): <a href="https://huggingface.co/datasets/FINAL-Bench/Weather-Model-Forecast-Archive">Weather-Model-Forecast-Archive</a>. Weather data by <a href="https://open-meteo.com/">Open-Meteo.com</a> (CC BY 4.0). Verification: ERA5, Copernicus Climate Change Service. Model sources: ECMWF, NOAA NCEP, DWD, JMA, ECCC, MET Norway. Engine: <a href="https://huggingface.co/FINAL-Bench/Darwin-Prediction-Weather">Darwin-Prediction-Weather</a>.</footer>
|
| 69 |
+
</main><script>
|
| 70 |
+
const CITIES={"Seoul":[37.57,126.98],"Busan":[35.18,129.08],"Tokyo":[35.68,139.69],"Beijing":[39.90,116.40],"Singapore":[1.35,103.82],"Delhi":[28.61,77.21],"Dubai":[25.20,55.27],"Moscow":[55.76,37.62],"Berlin":[52.52,13.40],"London":[51.51,-0.13],"Paris":[48.86,2.35],"Madrid":[40.42,-3.70],"Cairo":[30.04,31.24],"Nairobi":[-1.29,36.82],"Johannesburg":[-26.20,28.05],"New York":[40.71,-74.01],"Chicago":[41.88,-87.63],"Los Angeles":[34.05,-118.24],"Mexico City":[19.43,-99.13],"Sao Paulo":[-23.55,-46.63],"Buenos Aires":[-34.60,-58.38],"Sydney":[-33.87,151.21],"Anchorage":[61.22,-149.90],"Reykjavik":[64.15,-21.94]};
|
| 71 |
+
const MODELS={"ecmwf_ifs025":["ECMWF IFS","ECMWF","Physics","eu","Europe","#5b8cff"],"ecmwf_aifs025_single":["ECMWF AIFS","ECMWF","AI","eu","Europe","#35d0ff"],"gfs_seamless":["GFS","NOAA NCEP","Physics","us","United States","#ff8a4c"],"ncep_aigfs025":["AIGFS","NOAA NCEP","AI","us","United States","#ffd166"],"icon_seamless":["ICON","DWD","Physics","de","Germany","#7be495"],"jma_seamless":["JMA GSM","JMA","Physics","jp","Japan","#ff6b9d"],"gem_seamless":["GEM","ECCC","Physics","ca","Canada","#c79bff"],"metno_seamless":["MET Nordic / IFS","MET Norway","Physics","no","Norway","#8fe3d0"]};
|
| 72 |
+
let LEADS=[1,3,5,7];const NAMES=Object.keys(CITIES),MK=Object.keys(MODELS),DAYS=50,LAG=6,MIN=31,DWC="#ffffff";
|
| 73 |
+
const flag=(cc,nm)=>`<img class="fl" src="https://flagcdn.com/w40/${cc}.png" alt="${nm}" title="${nm}" width="20" height="14">`;
|
| 74 |
+
const tyb=m=>`<span class="ty ${MODELS[m][2]==="AI"?"ai":"ph"}">${MODELS[m][2]==="AI"?"AI":"Physics (Supercomputer)"}</span>`;
|
| 75 |
+
const $=id=>document.getElementById(id),iso=d=>d.toISOString().slice(0,10);
|
| 76 |
+
const end=new Date(Date.now()-LAG*864e5),start=new Date(end-(DAYS-1)*864e5);
|
| 77 |
+
const lat=NAMES.map(c=>CITIES[c][0]).join(","),lon=NAMES.map(c=>CITIES[c][1]).join(",");
|
| 78 |
+
async function get(u){for(let k=0;k<3;k++){try{const r=await fetch(u);if(r.ok)return await r.json();}catch(e){}await new Promise(r=>setTimeout(r,1500*(k+1)));}throw new Error("fetch failed")}
|
| 79 |
+
function dmax(h){const o=[];for(let d=0;d+24<=h.length;d+=24){let m=-1e9,n=0;for(let i=d;i<d+24;i++){const v=h[i];if(v!==null&&v!==undefined){n++;if(v>m)m=v}}o.push(n>=20?m:null)}return o}
|
| 80 |
+
let O,F={},dates=[],gen="",OL=null;
|
| 81 |
+
async function load(){
|
| 82 |
+
try{const r=await fetch("data.json?d="+iso(new Date()));if(r.ok){const j=await r.json();if(j.dates&&j.dates.length>MIN+1){O=j.O;F=j.F;dates=j.dates;gen=j.generated||"";if(j.leads)LEADS=j.leads;try{const o=await fetch("outlook.json?d="+iso(new Date()));if(o.ok)OL=await o.json()}catch(e){}return}}}catch(e){}
|
| 83 |
+
const q=`latitude=${lat}&longitude=${lon}&start_date=${iso(start)}&end_date=${iso(end)}&timezone=UTC`;
|
| 84 |
+
const tr=await get(`https://archive-api.open-meteo.com/v1/archive?${q}&hourly=temperature_2m&models=era5`);
|
| 85 |
+
O=tr.map(x=>dmax(x.hourly.temperature_2m));dates=[];for(let i=0;i<O[0].length;i++)dates.push(iso(new Date(+start+i*864e5)));
|
| 86 |
+
const hv=LEADS.map(d=>`temperature_2m_previous_day${d}`).join(",");
|
| 87 |
+
for(const m of MK){await new Promise(r=>setTimeout(r,9000));try{const r=await get(`https://previous-runs-api.open-meteo.com/v1/forecast?${q}&hourly=${hv}&models=${m}`);
|
| 88 |
+
F[m]=LEADS.map(d=>r.map(x=>dmax(x.hourly[`temperature_2m_previous_day${d}`])))}catch(e){}}
|
| 89 |
+
}
|
| 90 |
+
const num=v=>(v===null||v===undefined)?NaN:v,fin=Number.isFinite;
|
| 91 |
+
// 10-day outlook: each model's latest forecast, and the fusion built with the calibration of the matching lead (leads beyond the scored range use the longest scored lead)
|
| 92 |
+
function outlook(){if(!OL){$("otiles").innerHTML="";$("otab").innerHTML="";$("onote").textContent="The outlook appears after the next daily update.";return}
|
| 93 |
+
const c=+$("ocity").value,city=NAMES[c],days=OL.dates.slice(1,11),ms=MK.filter(m=>OL.models[m]&&OL.models[m][city]);
|
| 94 |
+
const scored=LEADS.map((L,i)=>MK.some(m=>F[m]&&F[m][i]&&F[m][i][c].some(v=>v!==null&&v!==undefined))?i:-1).filter(i=>i>=0);
|
| 95 |
+
const fus=days.map((d,k)=>{const L=k+1;let li=LEADS.indexOf(L);if(!scored.includes(li))li=scored.length?scored[scored.length-1]:-1;if(li<0)return NaN;
|
| 96 |
+
const use=ms.filter(m=>F[m]&&F[m][li]&&fin(num(OL.models[m][city][L])));if(use.length<3)return NaN;const {bias,mse}=fit(li,LEADS[li],dates.length,use);const good=use.filter(m=>fin(mse[m])&&mse[m]>0&&fin(bias[m][c]));if(good.length<3)return NaN;
|
| 97 |
+
let ws=0,f=0;good.forEach(m=>ws+=1/mse[m]);good.forEach(m=>f+=(1/mse[m])/ws*(OL.models[m][city][L]-bias[m][c]));return f});
|
| 98 |
+
const dn=d=>new Date(d+"T12:00:00Z").toLocaleString("en",{weekday:"short",timeZone:"UTC"}).toUpperCase();
|
| 99 |
+
$("h4").textContent=`10-day outlook: ${city}, daily maximum temperature`;
|
| 100 |
+
$("otiles").innerHTML=days.map((d,k)=>{const vs=ms.map(m=>num(OL.models[m][city][k+1])).filter(fin),sp=vs.length>1?Math.max(...vs)-Math.min(...vs):NaN;return `<div class="ot1"><div class="dn">${dn(d)}</div><div class="dd">${d.slice(5)}</div><div class="tv">${fin(fus[k])?fus[k].toFixed(0)+"°":"–"}</div><div class="sp">${fin(sp)?"±"+(sp/2).toFixed(1)+"° models":""}</div></div>`}).join("");
|
| 101 |
+
let h="<tr><th>Model</th>"+days.map((d,k)=>`<th>+${k+1}d<br>${d.slice(5)}</th>`).join("")+"</tr>";
|
| 102 |
+
h+=`<tr class="dwo"><td>Darwin-Prediction-Weather</td>${fus.map(v=>`<td>${fin(v)?v.toFixed(1):"–"}</td>`).join("")}</tr>`;
|
| 103 |
+
ms.forEach(m=>h+=`<tr><td>${flag(MODELS[m][3],MODELS[m][4])}${MODELS[m][0]}</td>${days.map((d,k)=>{const v=num(OL.models[m][city][k+1]);return `<td>${fin(v)?v.toFixed(1):"–"}</td>`}).join("")}</tr>`);
|
| 104 |
+
$("otab").innerHTML=h;$("onote").textContent=`Forecasts issued ${OL.issued.replace("T"," ").replace("Z"," UTC")}, in °C. The large numbers are the Darwin-Prediction-Weather fusion; "± models" is half the spread between the public models, a guide to how settled the forecast is. Each of these forecasts is scored on the board above once the day is verified.`}
|
| 105 |
+
// bias and weights use only days whose truth was known before scoring day t (t-30 .. t-lead)
|
| 106 |
+
function fit(li,D,t,ms){const w0=Math.max(0,t-30),t1=t-D;const bias={},mse={};
|
| 107 |
+
for(const m of ms){bias[m]=[];let s2=0,n2=0;
|
| 108 |
+
for(let c=0;c<NAMES.length;c++){let s=0,n=0;for(let d=w0;d<=t1;d++){const e=num(F[m][li][c][d])-num(O[c][d]);if(fin(e)){s+=e;n++}}bias[m][c]=n>=7?s/n:NaN}
|
| 109 |
+
for(let c=0;c<NAMES.length;c++)for(let d=w0;d<=t1;d++){const e=num(F[m][li][c][d])-bias[m][c]-num(O[c][d]);if(fin(e)){s2+=e*e;n2++}}
|
| 110 |
+
mse[m]=n2?s2/n2:NaN}
|
| 111 |
+
return{bias,mse}}
|
| 112 |
+
const memo={};
|
| 113 |
+
// one pass: overall, per-city and per-month squared errors on the cases every listed model covers
|
| 114 |
+
function run(li,t0,t1,ci){const key=[li,t0,t1,ci].join("|");if(memo[key])return memo[key];const D=LEADS[li];
|
| 115 |
+
const cov={};let tot=0;for(let t=t0;t<=t1;t++)for(let c=0;c<NAMES.length;c++){if(ci>=0&&c!==ci)continue;if(!fin(num(O[c][t])))continue;tot++;for(const m of MK)if(F[m]&&F[m][li]&&fin(num(F[m][li][c][t])))cov[m]=(cov[m]||0)+1}
|
| 116 |
+
const EL=MK.filter(m=>(cov[m]||0)>=Math.max(1,0.6*tot)&&tot>0),OUT=MK.filter(m=>!EL.includes(m));
|
| 117 |
+
const A={},add=(k,gs,e)=>{if(!fin(e))return;const a=A[k]||(A[k]={s:0,n:0,g:{}});a.s+=e*e;a.n++;for(const g of gs){const b=a.g[g]||(a.g[g]=[0,0]);b[0]+=e*e;b[1]++}};
|
| 118 |
+
for(let t=t0;t<=t1;t++){const {bias,mse}=fit(li,D,t,EL);const good=EL.filter(m=>fin(mse[m])&&mse[m]>0);let ws=0;good.forEach(m=>ws+=1/mse[m]);const mon=dates[t].slice(0,7);
|
| 119 |
+
for(let c=0;c<NAMES.length;c++){if(ci>=0&&c!==ci)continue;const o=num(O[c][t]);if(!fin(o))continue;if(!EL.every(m=>fin(num(F[m][li][c][t]))))continue;
|
| 120 |
+
for(const m of EL)add(m,["c"+c,mon],num(F[m][li][c][t])-o);
|
| 121 |
+
let fu=0,ok=good.length>=3&&good.length===EL.length;for(const m of good){const x=num(F[m][li][c][t])-bias[m][c];if(!fin(x)){ok=false;break}fu+=(1/mse[m])/ws*x}
|
| 122 |
+
if(ok)add("darwin",["c"+c,mon],fu-o)}}
|
| 123 |
+
const rows=EL.filter(m=>A[m]).map(m=>({m,r:Math.sqrt(A[m].s/A[m].n),n:A[m].n})).sort((a,b)=>a.r-b.r);let rk=0,prev=null;rows.forEach((x,i)=>{if(prev===null||x.r.toFixed(2)!==prev){rk=i+1;prev=x.r.toFixed(2)}x.rk=rk});
|
| 124 |
+
return memo[key]={rows,A,OUT,dw:A.darwin?Math.sqrt(A.darwin.s/A.darwin.n):NaN,dn:A.darwin?A.darwin.n:0}}
|
| 125 |
+
const grp=(A,k,g,min)=>A[k]&&A[k].g[g]&&A[k].g[g][1]>=min?Math.sqrt(A[k].g[g][0]/A[k].g[g][1]):NaN;
|
| 126 |
+
function render(){
|
| 127 |
+
const D=+$("lead").value,li=LEADS.indexOf(D),per=$("period").value,ci=+$("city").value,t1=dates.indexOf($("date").value),n=per==="day"?1:(per==="ytd"?9999:+per),t0=Math.max(MIN,t1-n+1);
|
| 128 |
+
const R=run(li,t0,t1,ci),rows=R.rows,best=rows.length?rows[0].r:NaN,where=ci<0?"24 cities":NAMES[ci],span=t0===t1?dates[t1]:dates[t0]+" to "+dates[t1];
|
| 129 |
+
$("stamp").innerHTML=`Verified through <b>${dates[dates.length-1]}</b> (ERA5)${gen?"<br>Updated "+gen:""}`;
|
| 130 |
+
$("h1").textContent=`Most accurate public model: ${D}-day forecast, ${where}`;$("h2").textContent=`Accuracy board: ${D}-day forecast, ${span}, ${where}`;
|
| 131 |
+
if(!rows.length){const e="<p class='note'>No verified cases yet for this selection.</p>";$("status").textContent="";$("champ").innerHTML=e;$("dwc").innerHTML=e;$("bars").innerHTML=e;$("tick").textContent=`${D}-day forecasts are being archived and will appear here as their target days are verified.`}
|
| 132 |
+
else{
|
| 133 |
+
$("status").textContent=`Public models scored as issued, on the cases every listed model covers.${R.OUT.length?" Not listed for this selection (insufficient coverage): "+R.OUT.map(m=>MODELS[m][0]).join(", ")+".":""}${rows[0].n<30?" Small sample: results for a single day or city vary widely.":""}`;
|
| 134 |
+
const c=rows[0],M=MODELS[c.m];
|
| 135 |
+
$("champ").innerHTML=`<div class="champ"><div class="medal">1</div><div><div class="cn">${M[0]}</div><div class="sub" style="margin:6px 0">${flag(M[3],M[4])}${M[1]} ${tyb(c.m)}</div><div class="big">${c.r.toFixed(2)}<small>°C RMSE</small></div><div class="sub">${c.n.toLocaleString()} verified forecasts, ${span}</div></div></div>`+
|
| 136 |
+
`<div class="run">${rows.slice(1,3).map(x=>`<div class="rn"><div class="k">NO. ${x.rk}</div><div class="v">${MODELS[x.m][0]}</div><div class="sub">${flag(MODELS[x.m][3],MODELS[x.m][4])}${x.r.toFixed(2)} °C</div></div>`).join("")}</div>`;
|
| 137 |
+
$("dwc").innerHTML=fin(R.dw)?`<div class="sub" style="margin-bottom:6px">${flag("kr","South Korea")}VIDRAFT <span class="ty ai">AI</span> fusion engine</div><div class="big">${R.dw.toFixed(2)}<small>°C RMSE</small></div><div class="gain">${R.dw<best?((1-R.dw/best)*100).toFixed(1)+"% lower error than the best public model":"Level with the best public model"}</div><div class="sub" style="margin-top:10px">${R.dn.toLocaleString()} verified forecasts, ${D}-day lead</div>`:"<p class='note'>Fusion needs at least three models with 30 days of history for this selection.</p>";
|
| 138 |
+
const mx=Math.max(...rows.map(x=>x.r),fin(R.dw)?R.dw:0);let h="";
|
| 139 |
+
if(fin(R.dw))h+=`<div class="bar dwr"><div class="rk">★</div><div class="nm">Darwin-Prediction-Weather<span class="d">${flag("kr","South Korea")}VIDRAFT · AI fusion</span></div><div class="trk"><i style="width:${R.dw/mx*100}%;background:linear-gradient(90deg,#35d0ff,#9be9ff)"></i></div><div class="val">${R.dw.toFixed(2)}</div></div>`;
|
| 140 |
+
rows.forEach(x=>{const M=MODELS[x.m];h+=`<div class="bar ${x.rk===1?"f":""}"><div class="rk">${x.rk}</div><div class="nm">${M[0]}<span class="d">${flag(M[3],M[4])}${M[1]} ${tyb(x.m)}</span></div><div class="trk"><i style="width:${x.r/mx*100}%;background:${M[5]}"></i></div><div class="val">${x.r.toFixed(2)}</div></div>`});
|
| 141 |
+
$("bars").innerHTML=h;
|
| 142 |
+
const tk=`<b>${D}-DAY FORECAST</b> · ${span} · ${where} `+rows.map(x=>`<em>#${x.rk}</em> ${MODELS[x.m][0]} ${x.r.toFixed(2)}°C`).join(" • ")+(fin(R.dw)?` • <b>DARWIN-PREDICTION-WEATHER ${R.dw.toFixed(2)}°C</b>`:"")+" ";
|
| 143 |
+
$("tick").innerHTML=tk+tk;
|
| 144 |
+
}
|
| 145 |
+
// error by forecast day
|
| 146 |
+
const cur=LEADS.map((L,i)=>run(i,t0,t1,ci)),act=LEADS.map((L,i)=>cur[i].rows.length?i:-1).filter(i=>i>=0),W=520,H=260,pl=38,pb=28,pt=10,pr=10;
|
| 147 |
+
if(act.length>1){const ymax=Math.max(...act.flatMap(i=>cur[i].rows.map(x=>x.r)))*1.08,X=i=>pl+(W-pl-pr)*(act.indexOf(i)/(act.length-1)),Y=v=>pt+(H-pt-pb)*(1-v/ymax);let s=`<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="RMSE by forecast day">`;
|
| 148 |
+
for(let g=0;g<=4;g++){const v=ymax*g/4;s+=`<line x1="${pl}" x2="${W-pr}" y1="${Y(v)}" y2="${Y(v)}" stroke="#1d3f78"/><text x="${pl-6}" y="${Y(v)+4}" fill="#9db4d8" font-size="11" text-anchor="end">${v.toFixed(1)}</text>`}
|
| 149 |
+
act.forEach(i=>s+=`<text x="${X(i)}" y="${H-8}" fill="#9db4d8" font-size="11" text-anchor="middle">${LEADS[i]}d</text>`);
|
| 150 |
+
const line=(k,col,wd,dash)=>{const p=act.map(i=>{const v=k==="darwin"?cur[i].dw:(cur[i].rows.find(x=>x.m===k)||{}).r;return fin(v)?`${X(i)},${Y(v)}`:null}).filter(Boolean);if(p.length>1)s+=`<polyline points="${p.join(" ")}" fill="none" stroke="${col}" stroke-width="${wd}" ${dash?'stroke-dasharray="6 4"':""} stroke-linejoin="round"/>`};
|
| 151 |
+
MK.forEach(m=>line(m,MODELS[m][5],2));line("darwin",DWC,3,true);$("curve").innerHTML=s+"</svg>";
|
| 152 |
+
$("lg1").innerHTML=`<span><i style="background:${DWC}"></i>Darwin-Prediction-Weather</span>`+MK.map(m=>`<span><i style="background:${MODELS[m][5]}"></i>${MODELS[m][0]}</span>`).join("")}
|
| 153 |
+
else{$("curve").innerHTML="<p class='note'>Not enough verified lead times for this selection.</p>";$("lg1").innerHTML=""}
|
| 154 |
+
// city map: best public model per city over the period (all cities)
|
| 155 |
+
const RA=run(li,t0,t1,-1),mw=520,mh=280,px=l=>((l+180)/360)*mw,py=l=>((72-l)/(72+48))*mh;let ms=`<svg viewBox="0 0 ${mw} ${mh}" role="img" aria-label="Most accurate model by city">`;
|
| 156 |
+
for(let l=-150;l<=150;l+=30)ms+=`<line x1="${px(l)}" x2="${px(l)}" y1="0" y2="${mh}" stroke="#16335f"/>`;for(let l=60;l>=-30;l-=30)ms+=`<line x1="0" x2="${mw}" y1="${py(l)}" y2="${py(l)}" stroke="${l===0?"#2a5596":"#16335f"}"/>`;
|
| 157 |
+
ms+=`<text x="4" y="${py(0)-4}" fill="#5f7fb3" font-size="9">EQUATOR</text>`;const used={};
|
| 158 |
+
NAMES.forEach((c,i)=>{let bm=null,bv=1e9;RA.rows.forEach(x=>{const v=grp(RA.A,x.m,"c"+i,Math.min(5,t1-t0+1));if(fin(v)&&v<bv){bv=v;bm=x.m}});if(!bm)return;used[bm]=1;const x=px(CITIES[c][1]),y=py(CITIES[c][0]),left=x>mw-90;
|
| 159 |
+
ms+=`<circle cx="${x}" cy="${y}" r="6.5" fill="${MODELS[bm][5]}" stroke="#061226" stroke-width="1.5"><title>${c}: ${MODELS[bm][0]} ${bv.toFixed(2)} °C</title></circle><text x="${left?x-9:x+9}" y="${y+3.5}" fill="#dbe7fb" font-size="9.5" font-weight="600" text-anchor="${left?"end":"start"}">${c}</text>`});
|
| 160 |
+
$("map").innerHTML=ms+"</svg>";$("lg2").innerHTML=MK.filter(m=>used[m]).map(m=>`<span><i style="background:${MODELS[m][5]}"></i>${MODELS[m][0]}</span>`).join("")||"No verified cases yet.";
|
| 161 |
+
// monthly champions over the whole record for this lead
|
| 162 |
+
const RY=run(li,MIN,dates.length-1,-1),months=[...new Set(dates.slice(MIN).map(d=>d.slice(0,7)))];$("h3").textContent=`Monthly champions 2026: ${D}-day forecast, 24 cities`;
|
| 163 |
+
$("mo").innerHTML=months.map(mo=>{let bm=null,bv=1e9;RY.rows.forEach(x=>{const v=grp(RY.A,x.m,mo,100);if(fin(v)&&v<bv){bv=v;bm=x.m}});const d=grp(RY.A,"darwin",mo,100),nm=new Date(mo+"-15").toLocaleString("en",{month:"short"}).toUpperCase();
|
| 164 |
+
return bm?`<div class="mc" style="border-top-color:${MODELS[bm][5]}"><div class="m">${nm}</div><div class="w">${MODELS[bm][0]}</div><div class="e">${bv.toFixed(2)} °C${fin(d)?" · Darwin "+d.toFixed(2):""}</div></div>`:""}).join("")||"<p class='note'>No verified months yet for this lead.</p>";
|
| 165 |
+
}
|
| 166 |
+
load().then(()=>{LEADS.forEach(d=>$("lead").insertAdjacentHTML("beforeend",`<option value="${d}"${d===3?" selected":""}>${d} day${d>1?"s":""} ahead</option>`));NAMES.forEach((c,i)=>$("city").insertAdjacentHTML("beforeend",`<option value="${i}">${c}</option>`));
|
| 167 |
+
for(let i=dates.length-1;i>=MIN;i--)$("date").insertAdjacentHTML("beforeend",`<option>${dates[i]}</option>`);
|
| 168 |
+
NAMES.forEach((c,i)=>$("ocity").insertAdjacentHTML("beforeend",`<option value="${i}">${c}</option>`));$("ocity").onchange=outlook;
|
| 169 |
+
["lead","period","date","city"].forEach(id=>$(id).onchange=()=>{render();if(id==="city"&&+$("city").value>=0){$("ocity").value=$("city").value;outlook()}});render();outlook()}).catch(e=>{$("status").textContent="Could not load data. Please reload in a moment.";console.error(e)});
|
| 170 |
+
</script></body></html>
|