{"id":50882302,"url":"https://github.com/flowm/meteocompare","last_synced_at":"2026-06-15T14:01:21.439Z","repository":{"id":362548059,"uuid":"1244170702","full_name":"Flowm/meteocompare","owner":"Flowm","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-14T19:28:28.000Z","size":851,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T21:21:34.482Z","etag":null,"topics":["open-meteo","weather"],"latest_commit_sha":null,"homepage":"https://meteocompare.frcy.workers.dev/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Flowm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-20T03:00:13.000Z","updated_at":"2026-06-14T19:28:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Flowm/meteocompare","commit_stats":null,"previous_names":["flowm/meteocompare"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Flowm/meteocompare","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flowm%2Fmeteocompare","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flowm%2Fmeteocompare/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flowm%2Fmeteocompare/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flowm%2Fmeteocompare/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Flowm","download_url":"https://codeload.github.com/Flowm/meteocompare/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flowm%2Fmeteocompare/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34365597,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["open-meteo","weather"],"created_at":"2026-06-15T14:01:19.547Z","updated_at":"2026-06-15T14:01:21.433Z","avatar_url":"https://github.com/Flowm.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MeteoCompare\n\nMulti-model weather forecast comparison with a weighted aggregate and a per-timestep confidence score.\n\nFrontend-only (Vue 3 + Vite). Forecasts come straight from [open-meteo.com](https://open-meteo.com) — no MeteoCompare backend, no API key.\n\n## Features\n\n- **21 forecast models/products**, automatically dropped in/out based on geographic coverage and forecast horizon.\n- **Aggregate-first UI**: temperature + ±1σ confidence band, precipitation bars, daily strip with weather icon / high / low / precip prob / wind.\n- **Confidence score** per timestep — derived from inter-model spread normalised against typical seasonal spread, a model-count penalty, and lead-time decay encoded in the model weights.\n- **Per-model overlay** (opt-in) — one line per contributing model drawn over the aggregate, with per-model toggles, switchable between temperature, precipitation, precipitation probability, wind speed, and cloud cover.\n- **Window toggle** — 24 h / 3 d / 7 d on both charts.\n- **Locations** — open-meteo geocoding search, browser geolocation, URL-shareable state, favourites and recent-search in localStorage.\n- **Units** — °C ⇄ °F, mm ⇄ in, km/h ⇄ mph; persisted.\n\n## How the aggregation works\n\nPer timestep and per variable:\n\n1. **Pick the contributing models.** Each model has a home region (rough bbox) and a max useful lead time. Models that don't cover the location, or whose horizon has been exceeded, are filtered out.\n2. **Weight them.**\n   - Base weight = 1.\n   - Region bonus of +0.2 (mid-resolution) or +0.3 (convection-allowing) when the location is inside the model's home region.\n   - Lead-time decay per model class: convection-allowing models fade out by 60 h, mid-resolution regionals by 120 h, globals decay gently from 72 h → 0.4× by 240 h, and AI plus ensemble-mean products follow global decay with a smaller vote.\n   - Variable boost: CAMs get ×1.3 for precipitation and precipitation probability, since they explicitly resolve convection.\n3. **Aggregate**:\n   - **Temperature / precip / cloud cover / wind speed** → weighted mean + weighted standard deviation.\n   - **Wind direction** → weighted circular mean via unit-vector sum (so 350° + 10° averages to 0°, not 180°). Angular standard deviation via Mardia's formula on the mean resultant length.\n   - **Weather code** → severity-weighted modal class: bin WMO codes into severity groups (clear / mostly_clear / cloudy / fog / drizzle / rain / snow / storm), pick the group with the highest summed weight, then within that group pick the most-weighted code.\n\n## How the confidence score works\n\nFor each numeric variable:\n\n```\nspreadScore  = clamp(1 − stdDev / typicalSpread, 0, 1)\n               typicalSpread ramps with lead time; daily accumulated variables\n               (precipitation_sum) use a day-scale typical spread (mm/day) rather\n               than the hourly rate scale (mm/h).\n\nmodelFactor  = min(1, n / 3)   where n = number of contributing models\n               1 model → ⅓,  2 models → ⅔,  3+ models → 1\n\nconfidence   = clamp(spreadScore × modelFactor, 0, 1)\n```\n\nWind direction uses the same formula with circular standard deviation in degrees.\nWeather codes have no meaningful stdDev, so they use severity-group agreement instead:\n`confidence = clamp(weightShare(same severity group) × modelFactor, 0, 1)`.\n\nLead-time decay is handled entirely in the model weighting layer (not as a separate\nmultiplier here): CAMs fade out by 60 h, regionals by 120 h, globals decay past 72 h,\nand AI plus ensemble-mean products follow global decay with a smaller vote.\n\nThe badge maps the result to one of three tiers — high (≥70 %, emerald), mid (≥40 %, amber), low (rose).\n\n## Models\n\n| Open-meteo id                 | Provider           | Resolution / scope                 | Class         | Max lead |\n| ----------------------------- | ------------------ | ---------------------------------- | ------------- | -------- |\n| `ecmwf_ifs`                   | ECMWF              | 9 km HRES global                   | global        | 240 h    |\n| `gfs_seamless`                | NOAA               | seamless NOAA global/U.S. coverage | global        | 384 h    |\n| `gem_seamless`                | Environment Canada | 2.5–15 km, NA focus                | regional-mid  | 240 h    |\n| `ukmo_seamless`               | UK Met Office      | 2 km UKV / 10 km global            | regional-mid  | 168 h    |\n| `meteofrance_seamless`        | Météo-France       | 1.3 km AROME / 25 km ARPEGE        | regional-cam  | 102 h    |\n| `cma_grapes_global`           | CMA                | 15 km global, East Asia focus      | global        | 240 h    |\n| `bom_access_global`           | BOM                | 15 km global, Aus. focus           | global        | 240 h    |\n| `jma_seamless`                | JMA                | 5 km Japan / 55 km global          | regional-mid  | 264 h    |\n| `kma_seamless`                | KMA                | 1.5–13 km, Korea focus             | regional-mid  | 288 h    |\n| `icon_global`                 | DWD                | 11 km global                       | global        | 180 h    |\n| `icon_eu`                     | DWD                | 7 km Europe                        | regional-mid  | 120 h    |\n| `icon_d2`                     | DWD                | 2 km central Europe CAM            | regional-cam  | 48 h     |\n| `knmi_harmonie_arome_europe`  | KNMI               | 2 km Harmonie AROME Europe         | regional-cam  | 60 h     |\n| `dmi_harmonie_arome_europe`   | DMI                | 2 km Harmonie AROME Europe         | regional-cam  | 60 h     |\n| `metno_nordic`                | MET Norway         | 2.5 km Nordics                     | regional-cam  | 60 h     |\n| `meteoswiss_icon_seamless`    | MeteoSwiss         | 1–2 km ICON Switzerland seamless   | regional-cam  | 120 h    |\n| `geosphere_arome_austria`     | GeoSphere Austria  | AROME Austria                      | regional-cam  | 60 h     |\n| `ecmwf_aifs025_single`        | ECMWF              | 0.25° AI forecast                  | ai            | 360 h    |\n| `gfs_graphcast025`            | NOAA               | 0.25° GraphCast forecast           | ai            | 384 h    |\n| `ncep_aigfs025`               | NOAA               | 0.25° AI-enhanced GFS              | ai            | 384 h    |\n| `ncep_hgefs025_ensemble_mean` | NOAA               | 0.25° ensemble mean                | ensemble-mean | 384 h    |\n\n## Tech\n\n- **Vue 3** (`\u003cscript setup\u003e`, Composition API) + **Vite** + **TypeScript** (strict)\n- **Tailwind CSS v4** via `@tailwindcss/vite`\n- **vue-echarts** (ECharts 6) for the charts\n- **vue-router** for URL state, **@vueuse/core** for localStorage / debounce\n- **Erik Flowers' [weather-icons](https://github.com/erikflowers/weather-icons)** for the icon set\n- **Vitest** for unit tests\n- **oxlint** (Rust-based linter) + **oxfmt** for formatting\n- **wrangler** for deploys to Cloudflare Workers static assets\n\n## Architecture\n\n```\n        UI (Vue components)               Composables                Domain layer (pure TS)\n┌──────────────────────────────────┐  ┌──────────────────┐  ┌────────────────────────────────┐\n│  LocationBar                     │  │  useLocation     │  │  models.ts                     │\n│  AggregateSummary                │  │   ─ URL sync     │  │   ─ registry + bboxes          │\n│  HourlySeriesChart  (shared)     │  │   ─ favourites   │  │  weighting.ts                  │\n│  DailyStrip / DayCard            │  │  useForecast     │  │   ─ region bonus + decay       │\n│  VerificationDayCard            ►│◄─┤   ─ fetch+aggreg.│◄─┤  aggregate.ts                  │\n│  HitMissStrip                    │  │  useVerification │  │  aggregateVariables.ts (triad) │\n│  WeatherIcon / ConfidenceBadge   │  │   ─ fetch+score  │  │  confidence.ts                 │\n│                                  │  │  useUnits        │  │  verification.ts (bias/MAE …)  │\n│  ForecastView · VerificationView │  │   ─ formatters   │  │  weatherCodes.ts               │\n└──────────────────────────────────┘  └──────────────────┘  └────────────────────────────────┘\n                                                                            ▲\n                                                                            │\n                                            ┌───────────────────────────────┴──────────────────┐\n                                            │  api/omForecast.ts      ─ live forecast client    │\n                                            │  api/omSingleRuns.ts    ─ historical model runs   │\n                                            │  api/omHistoricalWeather.ts ─ ERA5-Seamless truth │\n                                            │  api/geocoding.ts       ─ location search         │\n                                            │  (HTTP caching via the service worker, SWR)       │\n                                            └────────────────────────────────────────────────────┘\n```\n\nThe **domain layer** is pure TS, unit-tested with Vitest. The UI sits on top of it via the\ncomposables. There is no global store — the URL is the source of truth for the location, and\nlocalStorage holds units, favourites, and recent searches.\n\n## Develop\n\n```bash\nnpm install\nnpm run dev          # http://localhost:5173\n```\n\n### Scripts\n\n```bash\nnpm run dev          # Vite dev server\nnpm run build        # type-check + production build to ./dist\nnpm run preview      # serve ./dist locally\n\nnpm test             # Vitest unit tests\nnpm run test:watch   # interactive\n\nnpm run lint         # oxlint + oxfmt --check + vue-tsc (CI gate)\nnpm run lint:fix     # oxlint --fix + oxfmt --write (local autofix)\n\nnpm run deploy       # build + wrangler deploy (Cloudflare Workers)\n```\n\nThe lint script is the single quality gate — it runs the linter, asserts formatting, and type-checks in one command.\n\n## Deploy\n\nThe app is shipped as static assets via Cloudflare Workers. Configuration lives in `wrangler.jsonc`:\n\n- `assets.directory: ./dist` — the Vite build output\n- `assets.not_found_handling: \"single-page-application\"` — Cloudflare serves `index.html` for any unmatched path, which is exactly what `vue-router`'s history mode needs\n\n```bash\nnpx wrangler login   # one-time\nnpm run deploy\n```\n\n## Limitations\n\n- **No bias correction.** Weights are static — no weight calibration against ERA5 reanalysis. Some models systematically run cold/warm or under/over-predict precipitation in some regions; that bias passes through to the aggregate.\n- **No ensemble members.** We pull deterministic runs only, not full ensemble distributions. Confidence is derived from inter-model spread, not from individual ensemble forecasts.\n- **Verification covers temperature and precipitation only.** ERA5-Seamless also provides wind and cloud-cover truth, but the verification page does not yet score them.\n\n## Acknowledgements\n\n- **[open-meteo.com](https://open-meteo.com)** — free, generous, CORS-friendly weather API that makes the whole frontend-only design possible. Forecasts are CC BY 4.0.\n- **Erik Flowers' [weather-icons](https://github.com/erikflowers/weather-icons)** — SIL OFL 1.1 font + MIT CSS.\n- The numeric weather prediction community at ECMWF, NOAA, DWD, Météo-France, UK Met Office, KNMI, MET Norway, JMA, KMA, BOM, Environment Canada, and others — open-meteo aggregates their public model outputs.\n\nThe multi-model aggregate is informational and not a forecast of record. For severe weather decisions, consult your local meteorological service.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflowm%2Fmeteocompare","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflowm%2Fmeteocompare","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflowm%2Fmeteocompare/lists"}