Download index.html from FINAL-Bench/Weather-Model-Leaderboard: direct link, hf CLI and curl.
- Browser
- Download file 52.8 kB
-
https://huggingface.co/spaces/FINAL-Bench/Weather-Model-Leaderboard/resolve/main/index.html
- Command line
-
hf download hf://spaces/FINAL-Bench/Weather-Model-Leaderboard/index.html
-
curl -L -o index.html https://huggingface.co/spaces/FINAL-Bench/Weather-Model-Leaderboard/resolve/main/index.html
52.8 kB
| <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Global Weather Model Leaderboard: which weather model is most accurate?</title> | |
| <meta name="description" content="Daily accuracy ranking of global weather forecast models, AI and physics: Darwin-Prediction-Weather, ECMWF AIFS, ECMWF IFS, GFS, AIGFS, ICON, JMA, GEM and MET Norway, scored against ERA5 for 24 world cities since 2026-01-01, with a 10-day outlook."> | |
| <meta property="og:title" content="Global Weather Model Leaderboard"><meta property="og:description" content="Daily accuracy ranking of global weather forecast models, AI and physics: Darwin-Prediction-Weather, ECMWF AIFS, ECMWF IFS, GFS, AIGFS, ICON, JMA, GEM and MET Norway, scored against ERA5 for 24 world cities since 2026-01-01, with a 10-day outlook."><meta property="og:type" content="website"><meta property="og:url" content="https://huggingface.co/spaces/FINAL-Bench/Weather-Model-Leaderboard"> | |
| <link rel="canonical" href="https://huggingface.co/spaces/FINAL-Bench/Weather-Model-Leaderboard"> | |
| <script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"WebApplication","name":"Global Weather Model Leaderboard","url":"https://huggingface.co/spaces/FINAL-Bench/Weather-Model-Leaderboard","applicationCategory":"Weather","description":"Daily accuracy ranking of global weather forecast models, AI and physics: Darwin-Prediction-Weather, ECMWF AIFS, ECMWF IFS, GFS, AIGFS, ICON, JMA, GEM and MET Norway, scored against ERA5 for 24 world cities since 2026-01-01, with a 10-day outlook.","creator":{"@type":"Organization","name":"VIDRAFT","email":"vidraft@mail.vidraft.net"},"license":"https://www.apache.org/licenses/LICENSE-2.0"},{"@type":"Dataset","name":"Weather Model Forecast Archive 2026","url":"https://huggingface.co/datasets/FINAL-Bench/Weather-Model-Forecast-Archive","description":"Daily forecasts of global weather models and ERA5 observed daily maximum temperature for 24 cities since 2026-01-01.","creator":{"@type":"Organization","name":"VIDRAFT"},"license":"https://creativecommons.org/licenses/by-nc/4.0/"},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Which weather model is the most accurate in 2026?","acceptedAnswer":{"@type":"Answer","text":"For daily maximum temperature across 24 world cities, Darwin-Prediction-Weather has the lowest error at every lead from 1 to 7 days. Among weather-centre models, ECMWF AIFS ranks first."}},{"@type":"Question","name":"Are AI weather models more accurate than physics models?","acceptedAnswer":{"@type":"Answer","text":"In this record ECMWF AIFS (AI) has lower error than ECMWF IFS (physics) at every lead, and NOAA AIGFS (AI) has lower error than NOAA GFS (physics) at every lead."}},{"@type":"Question","name":"How often is the leaderboard updated?","acceptedAnswer":{"@type":"Answer","text":"Once a day: new forecasts are archived, newly published observations are scored, and the cumulative record and the 10-day outlook are refreshed."}}]}]}</script> | |
| <style> | |
| :root{--bg:#061226;--panel:#0d2247;--panel2:#11305f;--line:#1d3f78;--ink:#f2f7ff;--mut:#9db4d8;--cy:#35d0ff;--gold:#ffc83d;--red:#ff4d5e;--ai:#35d0ff;--ph:#b9c7e0} | |
| *{box-sizing:border-box}html{background:var(--bg)} | |
| 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} | |
| .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} | |
| .brand{font-weight:800;letter-spacing:.06em;font-size:17px;text-transform:uppercase}.brand b{color:var(--cy)} | |
| .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} | |
| .live i{width:7px;height:7px;border-radius:50%;background:#fff;animation:bl 1.2s infinite}@keyframes bl{50%{opacity:.25}} | |
| .le{position:relative;display:inline-flex;align-items:center;gap:10px;font:inherit;font-weight:900;font-size:14px;letter-spacing:.1em;color:#04121f;background:linear-gradient(180deg,#b5f0ff 0%,#35d0ff 45%,#1aa6e0 100%);border:1px solid #d9f7ff;border-radius:12px;padding:10px 18px 10px 12px;cursor:pointer;box-shadow:0 4px 0 #0b6f9c,0 8px 22px rgba(53,208,255,.55);transition:transform .08s,box-shadow .08s,filter .15s;animation:lep 2.2s ease-in-out infinite;overflow:hidden} | |
| .le:hover{filter:brightness(1.12);transform:translateY(-1px);box-shadow:0 5px 0 #0b6f9c,0 12px 28px rgba(53,208,255,.75)}.le:active{transform:translateY(3px);box-shadow:0 1px 0 #0b6f9c,0 3px 10px rgba(53,208,255,.5)}.le:focus-visible{outline:3px solid #fff;outline-offset:3px} | |
| .le i{width:24px;height:24px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#dff8ff,#2d8fe6 55%,#0a2a66);box-shadow:inset -3px -3px 6px rgba(0,0,0,.35),0 0 0 2px rgba(255,255,255,.7);animation:les 6s linear infinite} | |
| .le u{text-decoration:none;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:#04121f;color:#7fe3ff;font-size:11px;margin-left:2px} | |
| .le:after{content:"";position:absolute;top:0;left:-60%;width:40%;height:100%;background:linear-gradient(100deg,transparent,rgba(255,255,255,.65),transparent);animation:lesh 3.2s ease-in-out infinite} | |
| @keyframes lep{0%,100%{box-shadow:0 4px 0 #0b6f9c,0 8px 22px rgba(53,208,255,.45)}50%{box-shadow:0 4px 0 #0b6f9c,0 8px 34px rgba(53,208,255,.95),0 0 0 6px rgba(53,208,255,.18)}}@keyframes lesh{0%,55%{left:-60%}100%{left:130%}}@keyframes les{to{filter:hue-rotate(25deg)}} | |
| @media(prefers-reduced-motion:reduce){.le,.le:after,.le i{animation:none}} | |
| .mdl{position:fixed;inset:0;background:rgba(2,6,14,.82);z-index:50;display:flex;align-items:center;justify-content:center;padding:14px}.mdl[hidden]{display:none} | |
| .mbox{width:min(1200px,100%);height:min(780px,100%);background:#061226;border:1px solid var(--cy);border-radius:14px;display:flex;flex-direction:column;overflow:hidden} | |
| .mhd{display:flex;align-items:center;gap:12px;padding:10px 14px;background:linear-gradient(90deg,#0b2a5c,#0a1c3a);border-bottom:1px solid var(--line);flex-wrap:wrap}.mhd b{letter-spacing:.1em} | |
| .mtabs{display:flex;gap:6px;flex-wrap:wrap;margin-left:6px}.mt{font:inherit;font-size:12px;font-weight:700;color:var(--mut);background:rgba(4,11,24,.5);border:1px solid var(--line);border-radius:99px;padding:5px 12px;cursor:pointer}.mt.on{color:#061226;background:var(--cy);border-color:var(--cy)} | |
| .mx{margin-left:auto;font-size:24px;line-height:1;color:var(--ink);background:none;border:0;cursor:pointer;padding:0 6px} | |
| .mbody{flex:1;position:relative;min-height:0}#lmap,#wfr{position:absolute;inset:0;width:100%;height:100%;border:0;background:#030912}#lmap[hidden],#wfr[hidden]{display:none} | |
| .mft{padding:7px 14px;font-size:11px;color:var(--mut);border-top:1px solid var(--line)}.mft a{color:var(--cy)} | |
| .bm{filter:brightness(.62) saturate(.85)}.cpin2{position:absolute;transform:translate(-50%,-50%);background:rgba(6,18,38,.92);border:2px solid #35d0ff;border-radius:8px;padding:3px 7px;white-space:nowrap;font:700 11px/1.25 "Segoe UI",system-ui,sans-serif;color:#f2f7ff;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.6);cursor:pointer}.cpin2 b{display:block}.cpin2 span{display:block;font-size:10px}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#0d2247;color:#f2f7ff;border:1px solid #1d3f78}.leaflet-popup-content{margin:10px 12px;font:13px/1.4 "Segoe UI",system-ui,sans-serif}.leaflet-container{font:inherit} | |
| .stamp{margin-left:auto;color:var(--mut);font-size:13px;text-align:right}.stamp b{color:var(--ink)} | |
| .tick{background:#040b18;border-bottom:1px solid var(--line);overflow:hidden;white-space:nowrap;font-size:13px;font-weight:600} | |
| .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} | |
| @keyframes mq{from{transform:translateX(0)}to{transform:translateX(-50%)}} | |
| @media(prefers-reduced-motion:reduce){.tick span,.live i{animation:none}} | |
| main{max-width:1180px;margin:0 auto;padding:16px 16px 40px} | |
| .etabs{display:flex;gap:0;background:#07162f;border-bottom:1px solid var(--line);padding:0 12px;overflow-x:auto}.et{font:inherit;font-weight:800;font-size:13px;letter-spacing:.06em;text-transform:uppercase;color:var(--mut);background:none;border:0;border-bottom:3px solid transparent;padding:12px 18px;cursor:pointer;white-space:nowrap}.et:hover{color:var(--ink)}.et.on{color:var(--cy);border-bottom-color:var(--cy)}.et small{display:block;font-size:9px;letter-spacing:.08em;color:var(--mut);font-weight:700} | |
| .ctl{display:flex;flex-wrap:wrap;gap:10px;margin:4px 0 16px} | |
| .ctl label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--mut);display:flex;flex-direction:column;gap:4px;font-weight:700} | |
| 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%} | |
| .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:14px}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px} | |
| @media(max-width:860px){.grid,.grid2{grid-template-columns:1fr}} | |
| .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} | |
| .pn h2{margin:0 0 12px;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--cy);font-weight:800} | |
| .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)} | |
| .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} | |
| .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)} | |
| .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)} | |
| .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} | |
| .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} | |
| .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} | |
| .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} | |
| .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} | |
| .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} | |
| .bar.dwr{background:rgba(53,208,255,.07);border-radius:8px;border-bottom:0;margin-bottom:4px;padding:8px 6px} | |
| @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}} | |
| 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} | |
| .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)} | |
| .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} | |
| @media(max-width:700px){.ol{grid-template-columns:repeat(5,minmax(0,1fr))}} | |
| .tw{overflow-x:auto;scrollbar-color:#2a5596 #0a1c3a;scrollbar-width:thin}.tw::-webkit-scrollbar{height:8px}.tw::-webkit-scrollbar-track{background:#0a1c3a;border-radius:4px}.tw::-webkit-scrollbar-thumb{background:#2a5596;border-radius:4px}.ot{border-collapse:collapse;width:100%;min-width:1080px;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}.ot td .ty{margin-left:6px;font-size:9px}.ot tr.obr td{color:var(--gold);font-weight:800}.ot th.pa{color:#b9c7e0}.ot th.td,.ot td.td{background:rgba(255,200,61,.10)}.ot th.td{color:var(--gold)}.ot th.fu{color:var(--cy)}.ot .cu{border-right:2px solid var(--line);color:var(--ink)}.er{display:block;font-size:10px;font-weight:700}.wl{display:block;font-size:10px;font-weight:800}.ot td.fc{color:var(--cy);font-weight:700}.win{display:block;font-size:9px;font-weight:900;letter-spacing:.12em;color:#3a2a00;background:linear-gradient(180deg,#ffe08a,#ffc83d);border-radius:4px;padding:1px 5px;margin:0 0 3px auto;width:max-content}.ot td.wn1{background:rgba(255,200,61,.12);box-shadow:inset 0 0 0 1px rgba(255,200,61,.6);border-radius:6px}.er.g{color:#7be495}.er.y{color:#ffd166}.er.r{color:#ff6b7d} | |
| .h3{margin:0 0 8px;font-size:13px;font-weight:700;color:var(--ink)}.wt{border-collapse:separate;border-spacing:5px;width:100%;min-width:760px;font-size:12px}.wt th{color:var(--mut);font-size:11px;letter-spacing:.1em;text-transform:uppercase;padding:4px 6px;text-align:left;white-space:nowrap;font-weight:800}.wt th:first-child,.wt td:first-child{text-align:left;font-weight:700;color:var(--ink);white-space:nowrap;padding:5px 8px}.wt td.w{background:rgba(4,11,24,.5);border:1px solid var(--line);border-left:4px solid var(--cy);border-radius:8px;padding:7px 9px;white-space:nowrap}.wt td.w.dwn{background:linear-gradient(160deg,rgba(255,200,61,.16),rgba(4,11,24,.5) 70%);border-color:rgba(255,200,61,.55)}.wn{font-weight:800;font-size:13px;color:var(--ink)}.wm{display:flex;align-items:center;gap:8px;margin-top:4px}.wm b{font-size:13px;color:var(--ink)}.wt td.w.dwn .wm b{color:var(--gold)}.wt td.e{text-align:center;color:var(--mut)} | |
| .ml{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(255,255,255,.45)}.ml:hover{color:var(--cy);border-bottom-color:var(--cy)}.ml .ex{font-size:.7em;margin-left:3px;opacity:.7}.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} | |
| </style> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js" defer></script></head><body> | |
| <div class="top"><span class="live"><i></i>LIVE</span><span class="brand">Global Weather Model <b>Leaderboard</b></span><button class="le" id="lebtn" type="button" title="Open the live global weather map"><i></i>LIVE EARTH MAP<u>▶</u></button><span class="stamp" id="stamp">Loading…</span></div> | |
| <div class="tick"><span id="tick">Loading the latest verification…</span></div> | |
| <div class="mdl" id="mdl" hidden><div class="mbox" role="dialog" aria-modal="true" aria-label="Live Earth"> | |
| <div class="mhd"><span class="live"><i></i>LIVE</span><b>LIVE EARTH</b> | |
| <div class="mtabs"><button type="button" class="mt on" data-t="wind">Wind</button><button type="button" class="mt" data-t="satw">Clouds</button><button type="button" class="mt" data-t="rain">Rain radar</button><button type="button" class="mt" data-t="temp">Temperature</button><button type="button" class="mt" data-t="sat">City champions</button></div> | |
| <button type="button" class="mx" id="mclose" aria-label="Close">×</button></div> | |
| <div class="mbody"><div id="lmap"></div><iframe id="wfr" title="Live weather map" hidden></iframe></div> | |
| <div class="mft" id="mft"></div></div></div> | |
| <div class="etabs" id="etabs" role="tablist" aria-label="Weather element"> | |
| <button type="button" class="et on" data-e="tmax">Max temperature</button><button type="button" class="et" data-e="tmin">Min temperature</button><button type="button" class="et" data-e="wind">Wind</button><button type="button" class="et" data-e="precip">Precipitation</button></div> | |
| <main> | |
| <noscript><h1>Global Weather Model Leaderboard</h1><p>Daily accuracy ranking of global weather forecast models, AI and physics: Darwin-Prediction-Weather, ECMWF AIFS, ECMWF IFS, GFS, AIGFS, ICON, JMA, GEM and MET Norway, scored against ERA5 for 24 world cities since 2026-01-01, with a 10-day outlook. 2026 year to date, RMSE of daily maximum temperature in °C at 1, 3, 5 and 7 days ahead: Darwin-Prediction-Weather (VIDRAFT, AI) 0.84, 1.17, 1.59, 2.09. ECMWF AIFS (AI) 1.35, 1.59, 1.90, 2.42. ECMWF IFS (Physics) 1.44, 1.77, 2.21, 2.85. Data: <a href="https://huggingface.co/datasets/FINAL-Bench/Weather-Model-Forecast-Archive">Weather-Model-Forecast-Archive</a>. Contact: vidraft@mail.vidraft.net</p></noscript> | |
| <div class="ctl"> | |
| <label>Forecast lead<select id="lead"></select></label> | |
| <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">Last 30 days</option><option value="90">Last 90 days</option><option value="ytd" selected>2026 year to date</option></select></label> | |
| <label>Verification date<select id="date"></select></label> | |
| <label>City<select id="city"><option value="-1">All 24 cities</option></select></label> | |
| </div> | |
| <div id="status">Loading forecasts and observations…</div> | |
| <div class="grid"> | |
| <div class="pn"><h2 id="h1">Most accurate model</h2><div id="champ"></div></div> | |
| <div class="pn dw"><h2>AI vs Physics (Supercomputer)</h2><div id="dwc"></div></div> | |
| </div> | |
| <div class="pn" style="margin-top:14px"><h2 id="h2">Accuracy board</h2><div id="bars"></div></div> | |
| <div class="grid2"> | |
| <div class="pn"><h2>Error by forecast day</h2><div id="curve"></div><div class="lg" id="lg1"></div></div> | |
| <div class="pn"><h2>Most accurate model by city</h2><div id="map"></div><div class="lg" id="lg2"></div></div> | |
| </div> | |
| <div class="pn" style="margin-top:14px" id="opanel"><h2 id="h4">Results and 10-day outlook</h2> | |
| <div class="ctl" style="margin:0 0 12px"><label>Outlook city<select id="ocity"></select></label></div> | |
| <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> | |
| <div class="pn" style="margin-top:14px"><h2 id="h3">Monthly champions 2026</h2><div class="mo" id="mo"></div></div> | |
| <div class="pn" style="margin-top:14px"><h2>Champions at a glance: who is no. 1</h2> | |
| <h3 class="h3">By period and forecast day, 24 cities</h3><div class="tw"><table class="wt" id="wper"></table></div> | |
| <h3 class="h3" style="margin-top:16px">By city and forecast day, 2026 year to date</h3><div class="tw"><table class="wt" id="wcity"></table></div><div class="lg" id="lg3"></div></div> | |
| <div class="pn" style="margin-top:14px"><h2>How it is scored</h2> | |
| <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 value for the same UTC day at each city. Each element has its own tab and its own ranking: daily maximum temperature, daily minimum temperature,, daily maximum wind speed and daily precipitation total. 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: skill-weighted fusion of the public models, with local bias correction for temperature and wind, fitted only on days before the one being scored. It is ranked on exactly the same cases as the public models. In the 10-day outlook the fusion for days beyond the longest scored lead uses that lead's calibration.</p></div> | |
| <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>. Leaderboard application: Apache License 2.0. Compiled record and model: CC BY-NC 4.0. Contact: <a href="mailto:vidraft@mail.vidraft.net">vidraft@mail.vidraft.net</a></footer> | |
| </main><script> | |
| 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]}; | |
| const DW=["Darwin-Prediction-Weather","VIDRAFT","AI","kr","South Korea","#ffc83d"]; | |
| 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"]}; | |
| let LEADS=[1,3,5,7];const NAMES=Object.keys(CITIES),MK=Object.keys(MODELS),DAYS=50,LAG=6,MIN=31,DWC="#ffffff"; | |
| const flag=(cc,nm)=>`<img class="fl" src="https://flagcdn.com/w40/${cc}.png" alt="${nm}" title="${nm}" width="20" height="14">`; | |
| const URLS={darwin:"https://huggingface.co/FINAL-Bench/Darwin-Prediction-Weather",ecmwf_ifs025:"https://www.ecmwf.int/en/forecasts/documentation-and-support/medium-range-forecasts",ecmwf_aifs025_single:"https://huggingface.co/ecmwf/aifs-single-1.0",gfs_seamless:"https://www.emc.ncep.noaa.gov/emc/pages/numerical_forecast_systems/gfs.php",ncep_aigfs025:"https://www.emc.ncep.noaa.gov/emc.php",icon_seamless:"https://www.dwd.de/EN/research/weatherforecasting/num_modelling/01_num_weather_prediction_modells/icon_description.html",jma_seamless:"https://www.jma.go.jp/jma/en/Activities/nwp.html",gem_seamless:"https://eccc-msc.github.io/open-data/msc-data/nwp_gdps/readme_gdps_en/",metno_seamless:"https://www.met.no/en"}; | |
| const lk=(m,t)=>`<a class="ml" href="${URLS[m]}" target="_blank" rel="noopener" title="Open the ${inf(m)[0]} page">${t||inf(m)[0]}<span class="ex">↗</span></a>`; | |
| const inf=m=>m==="darwin"?DW:MODELS[m]; | |
| const tyb=m=>`<span class="ty ${inf(m)[2]==="AI"?"ai":"ph"}">${inf(m)[2]==="AI"?"AI":"Physics (Supercomputer)"}</span>`; | |
| const $=id=>document.getElementById(id),iso=d=>d.toISOString().slice(0,10); | |
| const end=new Date(Date.now()-LAG*864e5),start=new Date(end-(DAYS-1)*864e5); | |
| const lat=NAMES.map(c=>CITIES[c][0]).join(","),lon=NAMES.map(c=>CITIES[c][1]).join(","); | |
| 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")} | |
| 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} | |
| let O,F={},dates=[],gen="",OL=null,OLX=null,U="°C",EK="tmax"; | |
| const ELS={tmax:["data.json","daily maximum 2 m temperature","°C"],tmin:["data_tmin.json","daily minimum 2 m temperature","°C"],wind:["data_wind.json","daily maximum 10 m wind speed","km/h"],precip:["data_precip.json","daily precipitation total","mm"]},ECACHE={}; | |
| async function setEl(k){const b=[...document.querySelectorAll(".et")];let j=ECACHE[k]; | |
| if(!j){try{const r=await fetch(ELS[k][0]+"?d="+iso(new Date()));if(r.ok)j=await r.json()}catch(e){}if(j&&j.dates&&j.dates.length>MIN+1)ECACHE[k]=j;else j=null} | |
| if(!j){const e=b.find(x=>x.dataset.e===k);if(e&&!e.querySelector("small"))e.insertAdjacentHTML("beforeend","<small>record being built</small>");$("status").textContent=`The ${ELS[k][1]} record since 2026-01-01 is being built. Its ranking opens as soon as the record is complete.`;return} | |
| EK=k;U=ELS[k][2];O=j.O;F=j.F;dates=j.dates;gen=j.generated||"";if(j.leads)LEADS=j.leads;for(const x in memo)delete memo[x]; | |
| b.forEach(x=>x.classList.toggle("on",x.dataset.e===k));OLX=k==="tmax"?OL:(OL&&OL.el&&OL.el[k])||null;$("opanel").hidden=!OLX; | |
| const dsel=$("date");dsel.innerHTML=`<option value="latest" selected>Today · latest (${dates[dates.length-1]})</option>`;for(let i=dates.length-1;i>=MIN;i--)dsel.insertAdjacentHTML("beforeend",`<option>${dates[i]}</option>`); | |
| render();if(OLX)outlook()} | |
| async function load(){ | |
| 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();OLX=OL}}catch(e){}return}}}catch(e){} | |
| const q=`latitude=${lat}&longitude=${lon}&start_date=${iso(start)}&end_date=${iso(end)}&timezone=UTC`; | |
| const tr=await get(`https://archive-api.open-meteo.com/v1/archive?${q}&hourly=temperature_2m&models=era5`); | |
| 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))); | |
| const hv=LEADS.map(d=>`temperature_2m_previous_day${d}`).join(","); | |
| 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}`); | |
| F[m]=LEADS.map(d=>r.map(x=>dmax(x.hourly[`temperature_2m_previous_day${d}`])))}catch(e){}} | |
| } | |
| const num=v=>(v===null||v===undefined)?NaN:v,fin=Number.isFinite; | |
| // 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) | |
| function ocalc(c){const city=NAMES[c],days=OLX.dates.slice(1,11),ms=MK.filter(m=>OLX.models[m]&&OLX.models[m][city]); | |
| 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); | |
| const fz=(li,L,t,val)=>{const use=ms.filter(m=>F[m]&&F[m][li]&&fin(val(m)));if(use.length<3)return NaN;const {bias,mse}=fit(li,L,t,use);const good=use.filter(m=>fin(mse[m])&&mse[m]>0&&fin(bias[m][c]));if(good.length<3)return NaN;let ws=0,f=0;good.forEach(m=>ws+=wt(mse[m]));good.forEach(m=>f+=wt(mse[m])/ws*(val(m)-bias[m][c]));return clip(f)}; | |
| 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;return li<0?NaN:fz(li,LEADS[li],dates.length,m=>num(OLX.models[m][city][L]))}); | |
| const Rc=OLX.recent,Dl=+$("lead").value,Lp=Math.min(Dl,7),liH=LEADS.indexOf(Lp),pd=Rc?Rc.dates:[],ob=Rc?Rc.obs[city]||[]:[],idx=d=>Math.round((Date.parse(d)-Date.parse(dates[0]))/864e5); | |
| const pf=(m,j)=>Rc&&Rc.F[m]&&Rc.F[m][city]?num(Rc.F[m][city][Lp-1][j]):NaN,PF=pd.map((d,j)=>liH<0?NaN:fz(liH,Lp,idx(d),m=>pf(m,j))); | |
| const wins=pd.map((d,j)=>{const o=num(ob[j]);if(j===pd.length-1||!fin(o))return null;let bm=null,bv=1e9;[["darwin",PF[j]]].concat(ms.map(m=>[m,pf(m,j)])).forEach(([m,v])=>{if(fin(v)&&Math.abs(v-o)<bv-1e-9){bv=Math.abs(v-o);bm=m}});return bm}); | |
| return{city,days,ms,fus,Rc,Lp,liH,pd,ob,pf,PF,wins}} | |
| const SHN={darwin:"Darwin",ecmwf_ifs025:"ECMWF IFS",ecmwf_aifs025_single:"ECMWF AIFS",gfs_seamless:"GFS",ncep_aigfs025:"AIGFS",icon_seamless:"ICON",jma_seamless:"JMA",gem_seamless:"GEM",metno_seamless:"MET Nordic"}; | |
| // Results and 10-day outlook. All cities: one row per city. One city: one row per model. | |
| function outlook(){if(!OLX){$("otiles").innerHTML="";$("otab").innerHTML="";$("onote").textContent="The outlook appears after the next daily update.";return} | |
| const c=+$("ocity").value,dn=d=>new Date(d+"T12:00:00Z").toLocaleString("en",{weekday:"short",timeZone:"UTC"}).toUpperCase(),iss=OLX.issued.replace("T"," ").replace("Z"," UTC"); | |
| if(c<0){const all=NAMES.map((n,i)=>ocalc(i)),X=all[0],wc={};all.forEach(x=>x.wins.forEach(m=>{if(m)wc[m]=(wc[m]||0)+1}));const tot=Object.values(wc).reduce((a,b)=>a+b,0),ord=Object.entries(wc).sort((a,b)=>b[1]-a[1]); | |
| $("h4").textContent=`Results and 10-day outlook: all 24 cities, ${ELS[EK][1]}`; | |
| $("otiles").innerHTML=ord.slice(0,10).map(([m,n],i)=>`<div class="ot1" style="${i===0?"border-color:var(--gold)":""}"><div class="dn">${i===0?"MOST WINS":"NO. "+(i+1)}</div><div class="tv" style="font-size:22px">${n}</div><div class="dd" style="color:#fff;font-weight:700">${flag(inf(m)[3],inf(m)[4])}${SHN[m]}</div><div class="sp">${tot?(n/tot*100).toFixed(0):0}% of daily wins</div></div>`).join(""); | |
| let h=`<tr><th>City</th>`+X.pd.map((d,j)=>`<th class="${j===X.pd.length-1?"td":"pa"}">${j===X.pd.length-1?"TODAY":"−"+(X.pd.length-1-j)+"d"}<br>${d.slice(5)}</th>`).join("")+X.days.map((d,k)=>`<th class="fu">+${k+1}d<br>${d.slice(5)}</th>`).join("")+"</tr>"; | |
| all.forEach(x=>{h+=`<tr><td>${x.city}</td>`+x.pd.map((d,j)=>{const o=num(x.ob[j]),w=x.wins[j],last=j===x.pd.length-1;return `<td class="${last?"td":""} ${w==="darwin"?"wn1":""}">${fin(o)?o.toFixed(1)+(last?"*":""):"–"}${w?`<span class="wl" style="color:${inf(w)[5]}">${SHN[w]}</span>`:""}</td>`}).join("")+x.fus.map(v=>`<td class="fc">${fin(v)?v.toFixed(1):"–"}</td>`).join("")+"</tr>"}); | |
| $("otab").innerHTML=h; | |
| $("onote").textContent=`Past days: the actual value in ${U} and, under it, the model whose forecast (issued ${X.Lp} day${X.Lp>1?"s":""} earlier) came closest that day. Days ahead: the Darwin-Prediction-Weather forecast, issued ${iss}. Recent actual values are preliminary until ERA5 is published. * Today so far. The tiles count daily wins over the last ${X.pd.length-1} days in 24 cities. Choose a city to see every model's forecast.`;return} | |
| const x=ocalc(c),{city,days,ms,fus,Rc,Lp,liH,pd,ob,pf,PF,wins}=x,cum=liH>=0?run(liH,MIN,dates.length-1,c):null,cv=m=>{const r=cum&&cum.rows.find(q=>q.m===m);return r?r.r.toFixed(2):"–"},wcount={};wins.forEach(m=>{if(m)wcount[m]=(wcount[m]||0)+1}); | |
| $("h4").textContent=`Results and 10-day outlook: ${city}, ${ELS[EK][1]}`; | |
| $("otiles").innerHTML=days.map((d,k)=>{const vs=ms.map(m=>num(OLX.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(U==="°C"?0:1)+(U==="°C"?"°":""):"–"}</div><div class="sp">${fin(sp)?"±"+(sp/2).toFixed(1)+(U==="°C"?"°":" "+U)+" models":""}</div></div>`}).join(""); | |
| const cell=(v,o,w)=>{if(!fin(v))return "<td>–</td>";if(!fin(o))return `<td>${v.toFixed(1)}</td>`;const e=v-o,k=Math.abs(e)<1?"g":Math.abs(e)<2?"y":"r";return `<td class="${w?"wn1":""}">${w?'<span class="win">WIN</span>':""}${v.toFixed(1)}<span class="er ${k}">${e>=0?"+":""}${e.toFixed(1)}</span></td>`}; | |
| let h=`<tr><th>Model</th><th class="cu">RMSE to ${dates[dates.length-1].slice(5)}<br>${Lp}-day lead</th>`+pd.map((d,j)=>`<th class="${j===pd.length-1?"td":"pa"}">${j===pd.length-1?"TODAY":"−"+(pd.length-1-j)+"d"}<br>${d.slice(5)}</th>`).join("")+days.map((d,k)=>`<th class="fu">+${k+1}d<br>${d.slice(5)}</th>`).join("")+"</tr>"; | |
| if(Rc)h+=`<tr class="obr"><td>Actual value</td><td class="cu"></td>${ob.map((v,j)=>`<td class="${j===pd.length-1?"td":""}">${fin(num(v))?v.toFixed(1)+(j===pd.length-1?"*":""):"–"}</td>`).join("")}${days.map(()=>"<td></td>").join("")}</tr>`; | |
| h+=`<tr class="dwo"><td>${flag("kr","South Korea")}${lk("darwin")} ${tyb("darwin")}</td><td class="cu">${cv("darwin")}</td>${pd.map((d,j)=>cell(PF[j],j===pd.length-1?NaN:num(ob[j]),wins[j]==="darwin")).join("")}${fus.map(v=>`<td>${fin(v)?v.toFixed(1):"–"}</td>`).join("")}</tr>`; | |
| ms.forEach(m=>h+=`<tr><td>${flag(MODELS[m][3],MODELS[m][4])}${lk(m)} ${tyb(m)}</td><td class="cu">${cv(m)}</td>${pd.map((d,j)=>cell(pf(m,j),j===pd.length-1?NaN:num(ob[j]),wins[j]===m)).join("")}${days.map((d,k)=>{const v=num(OLX.models[m][city][k+1]);return `<td>${fin(v)?v.toFixed(1):"–"}</td>`}).join("")}</tr>`); | |
| $("otab").innerHTML=h; | |
| $("onote").textContent=`Forecasts issued ${iss}, in ${U}. The large numbers are the Darwin-Prediction-Weather forecast; "± models" is half the spread between the models, a guide to how settled the forecast is.${Rc?` "Actual value" is what really happened. Past days show each forecast issued ${Lp} day${Lp>1?"s":""} earlier and, in colour, how far it was from the actual value. Recent actual values are preliminary until ERA5 is published. * Today so far (${Rc.today_hours}:00 UTC). WIN marks the closest forecast of each day; days ahead receive their WIN when the actual value is in.${Object.keys(wcount).length?" Wins in the last "+wins.filter(Boolean).length+" days: "+Object.entries(wcount).sort((a,b)=>b[1]-a[1]).map(([m,n])=>inf(m)[0]+" "+n).join(", ")+".":""}`:""}`} | |
| // bias and weights use only days whose truth was known before scoring day t (t-30 .. t-lead) | |
| function fit(li,D,t,ms){const c0=CF(),w0=Math.max(0,t-c0.win),t1=t-D;const bias={},mse={}; | |
| for(const m of ms){bias[m]=[];let s2=0,n2=0; | |
| for(let c=0;c<NAMES.length;c++){if(!c0.bias){bias[m][c]=0;continue}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} | |
| 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++}} | |
| mse[m]=n2>=(c0.bias?1:100)?s2/n2:NaN} | |
| return{bias,mse}} | |
| const FCFG={tmax:{win:30,bias:1,p:1},tmin:{win:30,bias:1,p:1},wind:{win:30,bias:1,p:1},precip:{win:150,bias:0,p:8}},CF=()=>FCFG[EK],wt=v=>Math.pow(v,-CF().p),clip=v=>EK==="precip"?Math.max(0,v):v; | |
| const memo={}; | |
| // one pass: overall, per-city and per-month squared errors on the cases every listed model covers | |
| function run(li,t0,t1,ci){const key=[li,t0,t1,ci].join("|");if(memo[key])return memo[key];const D=LEADS[li]; | |
| 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} | |
| const EL=MK.filter(m=>(cov[m]||0)>=Math.max(1,0.6*tot)&&tot>0),OUT=MK.filter(m=>!EL.includes(m)); | |
| const mk=()=>{const A={};return{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]++}}}},P=mk(),Q=mk(); | |
| // P: every common case (public models only). Q: the cases where the fusion also exists, so it is ranked on identical cases. | |
| 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+=wt(mse[m]));const mon=dates[t].slice(0,7); | |
| 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; | |
| for(const m of EL)P.add(m,["c"+c,mon],num(F[m][li][c][t])-o); | |
| 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+=wt(mse[m])/ws*x} | |
| if(ok){for(const m of EL)Q.add(m,["c"+c,mon],num(F[m][li][c][t])-o);Q.add("darwin",["c"+c,mon],clip(fu)-o)}}} | |
| const A=Q.A.darwin?Q.A:P.A,ks=EL.filter(m=>A[m]).concat(A.darwin?["darwin"]:[]); | |
| const rows=ks.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}); | |
| const pub=rows.filter(x=>x.m!=="darwin"); | |
| return memo[key]={rows,pub,A,OUT,dw:A.darwin?Math.sqrt(A.darwin.s/A.darwin.n):NaN,dn:A.darwin?A.darwin.n:0}} | |
| 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; | |
| function render(){ | |
| const D=+$("lead").value,li=LEADS.indexOf(D),per=$("period").value,ci=+$("city").value,t1=$("date").value==="latest"?dates.length-1:dates.indexOf($("date").value),n=per==="day"?1:(per==="ytd"?9999:+per),t0=Math.max(MIN,t1-n+1); | |
| const R=run(li,t0,t1,ci),rows=R.rows,best=R.pub.length?R.pub[0].r:NaN,where=ci<0?"24 cities":NAMES[ci],span=t0===t1?dates[t1]:dates[t0]+" to "+dates[t1]; | |
| $("stamp").innerHTML=`Verified through <b>${dates[dates.length-1]}</b> (ERA5)${gen?"<br>Updated "+gen:""}`; | |
| $("h1").textContent=`Most accurate model: ${ELS[EK][1]}, ${D}-day forecast, ${where}`;$("h2").textContent=`Accuracy board: ${D}-day forecast, ${span}, ${where}`; | |
| 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.`} | |
| else{ | |
| $("status").textContent=`All 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.":""}`; | |
| const c=rows[0],M=inf(c.m); | |
| $("champ").innerHTML=`<div class="champ"><div class="medal">1</div><div><div class="cn">${lk(c.m)}</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>${U} RMSE</small></div>${rows[1]&&c.r<rows[1].r?`<div class="gain" style="margin:8px 0 6px">${((1-c.r/rows[1].r)*100).toFixed(1)}% lower error than no. 2</div>`:""}<div class="sub">${c.n.toLocaleString()} verified forecasts, ${span}</div></div></div>`+ | |
| `<div class="run">${rows.slice(1,3).map(x=>`<div class="rn"><div class="k">NO. ${x.rk}</div><div class="v">${lk(x.m)}</div><div class="sub">${flag(inf(x.m)[3],inf(x.m)[4])}${x.r.toFixed(2)} ${U}</div></div>`).join("")}</div>`; | |
| const bAI=rows.find(x=>inf(x.m)[2]==="AI"),bPH=rows.find(x=>inf(x.m)[2]!=="AI"),vs=(x,lab)=>x?`<div class="rn" style="margin-bottom:10px"><div class="k">BEST ${lab} · NO. ${x.rk} OVERALL</div><div class="v">${lk(x.m)}</div><div class="sub">${flag(inf(x.m)[3],inf(x.m)[4])}${inf(x.m)[1]} <b style="color:#fff;font-size:18px">${x.r.toFixed(2)} ${U}</b></div></div>`:""; | |
| $("dwc").innerHTML=vs(bAI,"AI")+vs(bPH,"PHYSICS (SUPERCOMPUTER)")+(bAI&&bPH?`<div class="gain">${bAI.r<bPH.r?"AI leads by "+((1-bAI.r/bPH.r)*100).toFixed(1)+"%":"Physics leads by "+((1-bPH.r/bAI.r)*100).toFixed(1)+"%"}</div><div class="sub" style="margin-top:10px">${rows.filter(x=>inf(x.m)[2]==="AI").length} AI models and ${rows.filter(x=>inf(x.m)[2]!=="AI").length} physics models on this board</div>`:""); | |
| const mx=Math.max(...rows.map(x=>x.r));let h=""; | |
| rows.forEach(x=>{const M=inf(x.m),d=x.m==="darwin";h+=`<div class="bar ${x.rk===1?"f":""} ${d?"dwr":""}"><div class="rk">${x.rk}</div><div class="nm">${lk(x.m)}<span class="d">${flag(M[3],M[4])}${M[1]} ${tyb(x.m)}${d?" fusion":""}</span></div><div class="trk"><i style="width:${x.r/mx*100}%;background:${d?"linear-gradient(90deg,#35d0ff,#9be9ff)":M[5]}"></i></div><div class="val">${x.r.toFixed(2)}</div></div>`}); | |
| $("bars").innerHTML=h; | |
| const tk=`<b>${D}-DAY FORECAST</b> · ${span} · ${where} `+rows.map(x=>x.m==="darwin"?`<em>#${x.rk}</em> <b>DARWIN-PREDICTION-WEATHER ${x.r.toFixed(2)} ${U}</b>`:`<em>#${x.rk}</em> ${MODELS[x.m][0]} ${x.r.toFixed(2)} ${U}`).join(" • ")+" "; | |
| $("tick").innerHTML=tk+tk; | |
| } | |
| // champions at a glance (independent of the period and city selectors) | |
| const last=dates.length-1,SH={darwin:"Darwin",ecmwf_ifs025:"ECMWF IFS",ecmwf_aifs025_single:"ECMWF AIFS",gfs_seamless:"GFS",ncep_aigfs025:"AIGFS",icon_seamless:"ICON",jma_seamless:"JMA",gem_seamless:"GEM",metno_seamless:"MET Nordic"},seen={}; | |
| const wc=(m,v)=>{if(!m)return '<td class="e">–</td>';seen[m]=1;const M=inf(m);return `<td class="w ${m==="darwin"?"dwn":""}" style="border-left-color:${M[5]}"><div class="wn">${flag(M[3],M[4])}${lk(m,SH[m])}</div><div class="wm"><span class="ty ${M[2]==="AI"?"ai":"ph"}">${M[2]==="AI"?"AI":"Physics"}</span><b>${v.toFixed(2)}${U==="°C"?"°":" "+U}</b></div></td>`},LA=LEADS.map((L,i)=>i).filter(i=>MK.some(m=>F[m]&&F[m][i]&&F[m][i][0].some(v=>v!==null&&v!==undefined))); | |
| const hd="<tr><th></th>"+LA.map(i=>`<th>${LEADS[i]}-day</th>`).join("")+"</tr>"; | |
| $("wper").innerHTML=hd+[["Latest day",1],["Last 7 days",7],["Last 30 days",30],["Last 90 days",90],["2026 year to date",9999]].map(([nm,n])=>`<tr><td>${nm}</td>${LA.map(i=>{const r=run(i,Math.max(MIN,last-n+1),last,-1).rows[0];return r?wc(r.m,r.r):wc(null)}).join("")}</tr>`).join(""); | |
| $("wcity").innerHTML=hd+NAMES.map((c,ci2)=>`<tr><td>${c}</td>${LA.map(i=>{const Y=run(i,MIN,last,-1);let bm=null,bv=1e9;Y.rows.forEach(x=>{const v=grp(Y.A,x.m,"c"+ci2,30);if(fin(v)&&v<bv){bv=v;bm=x.m}});return wc(bm,bv)}).join("")}</tr>`).join(""); | |
| $("lg3").innerHTML=["darwin"].concat(MK).filter(m=>seen[m]).map(m=>`<span><i style="background:${inf(m)[5]}"></i>${inf(m)[0]}</span>`).join(""); | |
| // error by forecast day | |
| 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; | |
| 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">`; | |
| 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>`} | |
| act.forEach(i=>s+=`<text x="${X(i)}" y="${H-8}" fill="#9db4d8" font-size="11" text-anchor="middle">${LEADS[i]}d</text>`); | |
| 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"/>`}; | |
| MK.forEach(m=>line(m,MODELS[m][5],2));line("darwin",DWC,3,true);$("curve").innerHTML=s+"</svg>"; | |
| $("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("")} | |
| else{$("curve").innerHTML="<p class='note'>Not enough verified lead times for this selection.</p>";$("lg1").innerHTML=""} | |
| // city map: best public model per city over the period (all cities) | |
| 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">`; | |
| 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"}"/>`; | |
| ms+=`<text x="4" y="${py(0)-4}" fill="#5f7fb3" font-size="9">EQUATOR</text>`;const used={}; | |
| 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; | |
| ms+=`<circle cx="${x}" cy="${y}" r="6.5" fill="${inf(bm)[5]}" stroke="#061226" stroke-width="1.5"><title>${c}: ${inf(bm)[0]} ${bv.toFixed(2)} ${U}</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>`}); | |
| $("map").innerHTML=ms+"</svg>";$("lg2").innerHTML=["darwin"].concat(MK).filter(m=>used[m]).map(m=>`<span><i style="background:${inf(m)[5]}"></i>${inf(m)[0]}</span>`).join("")||"No verified cases yet."; | |
| // monthly champions over the whole record for this lead | |
| 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`; | |
| $("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 nm=new Date(mo+"-15").toLocaleString("en",{month:"short"}).toUpperCase(); | |
| return bm?`<div class="mc" style="border-top-color:${inf(bm)[5]}"><div class="m">${nm}</div><div class="w">${inf(bm)[0]}</div><div class="e">${bv.toFixed(2)} ${U}</div></div>`:""}).join("")||"<p class='note'>No verified months yet for this lead.</p>"; | |
| } | |
| // LIVE EARTH: satellite (NASA GIBS) + rain radar (RainViewer) + city pins on our own map, and live wind / rain / temperature maps | |
| let LM=null;const G="https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/"; | |
| async function liveMap(){if(LM){setTimeout(()=>LM.invalidateSize(),60);$("mft").innerHTML=LM._ft;return}if(!window.L){$("mft").textContent="Map library is still loading. Please try again in a moment.";return} | |
| LM=L.map("lmap",{center:[25,125],zoom:3,minZoom:2,maxZoom:7,worldCopyJump:true,attributionControl:false}); | |
| L.tileLayer(G+"BlueMarble_NextGeneration/default/default/GoogleMapsCompatible_Level8/{z}/{y}/{x}.jpeg",{maxNativeZoom:8,className:"bm"}).addTo(LM); | |
| try{const r=await (await fetch("https://api.rainviewer.com/public/weather-maps.json")).json(),f=r.radar.past[r.radar.past.length-1];L.tileLayer(r.host+f.path+"/256/{z}/{x}/{y}/4/1_1.png",{maxNativeZoom:7,opacity:.8}).addTo(LM);LM._rv=new Date(f.time*1000).toISOString().slice(11,16)+" UTC"}catch(e){} | |
| const li=LEADS.indexOf(+$("lead").value),li2=li>=0&&li<7?li:0,Y=run(li2,MIN,dates.length-1,-1); | |
| NAMES.forEach((c,i)=>{let bm=null,bv=1e9;Y.rows.forEach(x=>{const v=grp(Y.A,x.m,"c"+i,30);if(fin(v)&&v<bv){bv=v;bm=x.m}});const o=OLX&&OLX.models?ocalc(i):null,col=bm?inf(bm)[5]:"#35d0ff",M=bm?inf(bm):null; | |
| const mk=L.marker(CITIES[c],{icon:L.divIcon({className:"",html:`<div class="cpin2" style="border-color:${col}"><b>${c}</b>${M?`<span style="color:${col}">${SHN[bm]}</span>`:""}</div>`,iconSize:[0,0]})}).addTo(LM); | |
| mk.bindPopup(`<b style="font-size:15px">${c}</b>${M?`<br>No. 1 in 2026: <b>${M[0]}</b> (${bv.toFixed(2)} ${U}, ${LEADS[li2]}-day forecast)`:""}${o?`<br>Darwin forecast, next days: ${o.fus.slice(0,5).map((v,k)=>fin(v)?`<b>${v.toFixed(0)}°</b>`:"–").join(" · ")}`:""}`)}); | |
| LM._ft=`The most accurate model in each city in 2026, over live rain radar${LM._rv?" ("+LM._rv+")":""}. Click a city for its forecast. We acknowledge the use of imagery provided by services from NASA's Global Imagery Browse Services (GIBS), part of NASA's Earth Science Data and Information System (ESDIS). Weather data by <a href="https://www.rainviewer.com/" target="_blank" rel="noopener">RainViewer</a>.`;$("mft").innerHTML=LM._ft} | |
| function liveTab(t){document.querySelectorAll(".mt").forEach(b=>b.classList.toggle("on",b.dataset.t===t));const f=$("wfr"); | |
| if(t==="sat"){f.hidden=true;$("lmap").hidden=false;liveMap()} | |
| else{$("lmap").hidden=true;f.hidden=false;const ov={wind:"wind",rain:"radar",temp:"temp",satw:"clouds"}[t];f.src=`https://embed.windy.com/embed.html?type=map&location=coordinates&metricRain=mm&metricTemp=%C2%B0C&metricWind=m%2Fs&zoom=3&overlay=${ov}&product=${t==="rain"?"radar":"ecmwf"}&level=surface&lat=25&lon=125`; | |
| $("mft").innerHTML='Live map by <a href="https://www.windy.com/" target="_blank" rel="noopener">Windy.com</a>.'}} | |
| function liveOpen(){$("mdl").hidden=false;document.body.style.overflow="hidden";liveTab("wind")} | |
| function liveClose(){$("mdl").hidden=true;document.body.style.overflow="";$("wfr").src="about:blank"} | |
| load().then(()=>{ECACHE.tmax={O,F,dates,generated:gen,leads:LEADS};document.querySelectorAll(".et").forEach(b=>b.onclick=()=>setEl(b.dataset.e));$("lebtn").onclick=liveOpen;$("mclose").onclick=liveClose;$("mdl").onclick=e=>{if(e.target===$("mdl"))liveClose()};document.addEventListener("keydown",e=>{if(e.key==="Escape"&&!$("mdl").hidden)liveClose()});document.querySelectorAll(".mt").forEach(b=>b.onclick=()=>liveTab(b.dataset.t));LEADS.forEach(d=>$("lead").insertAdjacentHTML("beforeend",`<option value="${d}"${d===7?" selected":""}>${d} day${d>1?"s":""} ahead</option>`));NAMES.forEach((c,i)=>$("city").insertAdjacentHTML("beforeend",`<option value="${i}">${c}</option>`)); | |
| $("date").insertAdjacentHTML("beforeend",`<option value="latest" selected>Today · latest (${dates[dates.length-1]})</option>`);for(let i=dates.length-1;i>=MIN;i--)$("date").insertAdjacentHTML("beforeend",`<option>${dates[i]}</option>`); | |
| $("ocity").insertAdjacentHTML("beforeend",`<option value="-1" selected>All 24 cities</option>`);NAMES.forEach((c,i)=>$("ocity").insertAdjacentHTML("beforeend",`<option value="${i}">${c}</option>`));$("ocity").onchange=outlook; | |
| ["lead","period","date","city"].forEach(id=>$(id).onchange=()=>{render();if(id==="city")$("ocity").value=$("city").value;outlook()});render();outlook(); | |
| // shareable links: #el=precip&lead=3&period=90&city=Seoul&live=sat | |
| const hp=new URLSearchParams(location.hash.slice(1));(async()=>{if(hp.get("el")&&ELS[hp.get("el")])await setEl(hp.get("el")); | |
| if(hp.get("lead")&&LEADS.includes(+hp.get("lead"))){$("lead").value=hp.get("lead")}if(hp.get("period")){$("period").value=hp.get("period")} | |
| if(hp.get("city")){const i=NAMES.indexOf(hp.get("city"));if(i>=0){$("city").value=i;$("ocity").value=i}} | |
| if(hp.get("lead")||hp.get("period")||hp.get("city")){render();if(OLX)outlook()} | |
| if(hp.get("live")){liveOpen();liveTab(hp.get("live"))}})()}).catch(e=>{$("status").textContent="Could not load data. Please reload in a moment.";console.error(e)}); | |
| </script></body></html> | |