No description
  • JavaScript 55.3%
  • Python 23.7%
  • Shell 11.3%
  • CSS 7.2%
  • HTML 2.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Chris Roscher c249beb300
fix temp: 2x downsample + block-center coords to fix 3-4°C bilinear artifacts
The 5× downsampling (390→78) plus edge-aligned lat/lon coordinates caused
the client-side bilinear interpolation to heavily weight the wrong grid
cells near coast/inland boundaries, producing errors up to 4.3°C.

Fixed two issues:
- Downsample factor 5→2 (195×195), still only 585 KB total (2.4×)
- Use block-center coordinates instead of block-edges so bilinear
  correctly weights the four cells surrounding the target point

Result: max error ≤0.58°C (within uint8 0.5°C quantization precision).
2026-05-14 14:38:46 +02:00
css feat: temperature data, 24h/60h toggle, fetch script, docs 2026-05-12 14:57:27 +02:00
js fix: prevent duplicate refresh requests, simplify stale-data handling 2026-05-14 14:10:38 +02:00
scripts fix temp: 2x downsample + block-center coords to fix 3-4°C bilinear artifacts 2026-05-14 14:38:46 +02:00
.gitignore Initial commit: KNMI radar nowcast viewer 2026-05-10 23:48:22 +02:00
.python-version Add dataset toggle, cache-busting, and NetCDF→H5 conversion 2026-05-11 00:01:16 +02:00
icon.svg Add PWA support: manifest, service worker, icon, theme-color 2026-05-12 12:29:48 +02:00
index.html feat: temperature data, 24h/60h toggle, fetch script, docs 2026-05-12 14:57:27 +02:00
manifest.json Add PWA support: manifest, service worker, icon, theme-color 2026-05-12 12:29:48 +02:00
nginx.conf.example fix: 24h bar scaling, nginx .bin.gz config, visYMax init 2026-05-12 15:17:14 +02:00
pyproject.toml fix: correct polar stereographic projection (EPSG:9829 variant B) 2026-05-12 20:37:27 +02:00
README.md fix temp: 2x downsample + block-center coords to fix 3-4°C bilinear artifacts 2026-05-14 14:38:46 +02:00
requirements.txt feat: support Alpine Linux — venv fallback, setup script 2026-05-11 21:10:52 +02:00
sw.js fix: prevent duplicate refresh requests, simplify stale-data handling 2026-05-14 14:10:38 +02:00

Weadar — KNMI weather viewer

Interactive map viewer for Dutch weather data, combining real-time radar nowcasts, UWCW model precipitation forecasts, and temperature forecasts in a single Leaflet-based interface.

Live demo: aitai.nl/weadar

Data sources

Dataset Source Resolution Timestep Horizon Format
Radar nowcast radar-forecast-2.0 1×1 km 5 min 2 hours HDF5
Precip forecast uwcw-ha-det-nl-s1-1-0 2×2 km 1 hour 60 hours NetCDF
Temperature uwcw-ha-det-nl-s1-1-0 2×2 km 1 hour 60 hours NetCDF

All datasets are produced by KNMI and licensed under CC BY 4.0.

  • Radar forecast — pySTEPS-based precipitation nowcast on a polar stereographic 700×765 grid over the Netherlands (bounds: 48.9°–55.97°N, 0.0°–10.86°E). Raw values × 12 = mm/h.
  • UWCW precipitation — HARMONIE-AROME atmospheric model from the United Weather Centres-West collaboration. Covers the Dutch domain at 2 km resolution with 60 hourly timesteps of total precipitation accumulation.
  • UWCW temperature — Same model, air temperature at 2m above ground level. Bilinearly downsampled 2× to a 195×195 lat/lon block-center grid, stored as uint8 (0.5°C precision), gzipped per frame (~10 KB each). The client does bilinear interpolation for the selected location.

How it works

Data pipeline

  1. Download the HDF5 and NetCDF files from the KNMI Data Platform (see links above). Use the included scripts/fetch-knmi.sh for automated cron-based fetching.
  2. Convert the model NetCDF to the radar's projection grid:
    uv run scripts/convert_nc_to_h5.py \
        data/uwcw-ha-det-nl-2km_*.nc \
        data/forecast.h5
    
  3. Convert temperature NetCDF to per-frame gzipped uint8:
    uv run scripts/convert_temp_to_frames.py \
        data/uwcw-ha-det-nl-2km_*_air-temperature-hagl.nc \
        data/temp/
    
  4. Split both H5 files into individual frames. WebP lossless is the default (no compression artifacts); lossy q=90 is available for size trade-offs:
    uv run scripts/h5_to_frames.py data/radar.h5    data/radar/    --lossless
    uv run scripts/h5_to_frames.py data/forecast.h5 data/forecast/ --lossless
    # or: --format png  (lossless)   --format webp  (lossy q=90)   --format gzip  (legacy)
    
  5. Serve with nginx (see nginx.conf.example) or any HTTP server

Frame encoding

Precipitation frames use an 8-bit sqrt palette: uint16 values are mapped to 256 grayscale indices with finer precision at low values (where weather data lives) and coarser at the top. The client reconstructs the original uint16 range using only maxValue from meta.json.

A noise-floor clip (--clip-mmh 0.05) zeros out values below the first visible colour band (≤ 0.05 mm/h) before encoding, producing cleaner backgrounds and improving compression.

Frames are stored as lossless WebP by default. Lossy WebP at any quality introduces grayscale shifts that cross the single-level gap between transparent and the first visible threshold, causing precipitation to flicker in and out when scrubbing the timeline. Lossless WebP eliminates this completely while keeping files small (the clip threshold makes zero-background regions compress efficiently).

Temperature frames use a simpler uint8 linear encoding with 0.5°C precision: value = round((°C + 50) × 2). The 390×390 model grid is bilinearly downsampled 2× to 195×195 with block-center coordinates, keeping interpolation error ≤0.6 °C. Each frame is gzipped individually (~10 KB) so the client can load frame 0 first and stream the rest.

Format Typical size (radar, 25 frames, 700×765)
WebP lossless ~0.4 MB total (−99% vs raw)
WebP lossy (q=90) ~0.5 MB total
PNG (optimized) ~0.6 MB total
gzip (legacy) ~8.1 MB total

Forecast (60 frames) is ~3.1 MB lossless WebP, ~1.1 MB lossy. Temperature (60 frames, 195×195) is ~0.6 MB gzipped uint8.

Client-side

The browser fetches a small meta.json first, then loads frame 0 (≈18 KB WebP for precip, ~10 KB gzip for temp), and renders the map immediately. Remaining frames stream in the background one at a time — the chart timeline fills in as data arrives.

meta.json (200 bytes)
  → frame 0000.webp (~18 KB)    ← instant render
  → frame 0001.webp             ← background stream
  → frame 0002.webp
  → ...

Toggle between Radar (2h nowcast) and Forecast (60h model). On the forecast view, a 24h/60h range toggle lets you zoom into the near term. Temperature is always shown: as a teal line on the forecast chart, as a standalone bubble on the radar view, and as color-coded stats in the panel.

Once both datasets are loaded, switching is instant (in-memory cache — no network). Click anywhere on the map to see precipitation and temperature stats at that location. The selected location persists across page loads via localStorage. Data auto-refreshes every 5 minutes and on PWA wake-up.

Projection

The radar nowcast uses KNMI's polar stereographic projection. The conversion script (scripts/convert_nc_to_h5.py) reprojects the UWCW model's regular lat/lon grid onto the same 700×765 stereographic grid, so both precipitation datasets share the identical rendering pipeline. Temperature is downsampled 2× to 195×195 with block-center coordinates — the client does bilinear interpolation (≤0.6 °C error).

Project structure

weadar/
├── index.html                  # Main page
├── css/
│   └── style.css               # Styles (glassmorphism panels, toggle, chart)
├── js/
│   └── app.js                  # Map, streaming loader, chart, stats, temp
├── scripts/
│   ├── fetch-knmi.sh           # Cron-ready KNMI download + split pipeline
│   ├── setup-alpine.sh         # One-shot Alpine Linux dependency installer
│   ├── convert_nc_to_h5.py     # NetCDF → 700×765 polar stereographic H5
│   ├── convert_temp_to_frames.py # Temp NetCDF → per-frame gzipped uint8
│   └── h5_to_frames.py         # H5 → individual frames (webp/png/gzip) + meta.json
├── data/                        # .gitignored — place H5/NC/frame files here
│   ├── radar/                  # split frames (0000.webp, 0001.webp, …)
│   ├── forecast/               # split frames
│   ├── temp/                   # temperature frames (0000.bin.gz, …)
│   ├── RAD_NL25_RAC_FM_*.h5    # source radar H5 (original KNMI filename)
│   ├── uwcw-*-precip-*.nc      # source forecast NetCDF
│   └── uwcw-*-temperature-*.nc # source temperature NetCDF
├── nginx.conf.example           # Nginx config with caching headers for streaming
├── pyproject.toml               # Python dependencies (uv or pip)
├── requirements.txt             # pip-compatible dependency list
└── .python-version

Setup

Python dependencies

Using uv (recommended):

uv sync

Or with pip:

pip install -r requirements.txt

This installs netCDF4, h5py, numpy, scipy, and Pillow for the conversion scripts.

Alpine Linux

For Alpine, run the one-shot setup script:

sudo sh scripts/setup-alpine.sh

This installs system packages (HDF5, NetCDF, build tools), creates a venv at .venv/, and installs all Python dependencies.

Automated fetching (cron)

scripts/fetch-knmi.sh handles the full pipeline — download, convert, split, and atomic swap — for all three datasets. Set up a cron job:

# Get a free API key from https://dataplatform.knmi.nl/
# Store it in WEADAR_API_KEY env var or /etc/weadar/api_key

# Radar: every 5 minutes, 1 min after upload (~ :03 past)
3-58/5 * * * * WEADAR_API_KEY="ey..." /path/to/weadar/scripts/fetch-knmi.sh radar >> /var/log/weadar-cron.log 2>&1

# Forecast + temperature: hourly, 1 min after upload (~ :33 past)
33 * * * * WEADAR_API_KEY="ey..." /path/to/weadar/scripts/fetch-knmi.sh forecast >> /var/log/weadar-cron.log 2>&1

# Or fetch all three at once:
# 33 * * * * WEADAR_API_KEY="ey..." /path/to/weadar/scripts/fetch-knmi.sh all >> /var/log/weadar-cron.log 2>&1

# Temperature-only (if you want it on a separate schedule):
# 33 * * * * WEADAR_API_KEY="ey..." /path/to/weadar/scripts/fetch-knmi.sh temp >> /var/log/weadar-cron.log 2>&1

The forecast command fetches both precipitation and temperature together (same dataset, same schedule). Use all to include radar too, or radar / forecast / temp for individual datasets.

Serving

Any HTTP server works — WebP images are decoded natively by the browser.

For production, use the provided nginx config which includes optimal caching headers (Cache-Control: no-cache on WebP/PNG frames for ETag-based revalidation, Cache-Control: no-store on meta.json):

# Adapt server_name in nginx.conf.example, then:
sudo ln -s $(pwd)/nginx.conf.example /etc/nginx/sites-enabled/weadar
sudo nginx -t && sudo systemctl reload nginx

For quick local development:

python3 -m http.server 8080

License

Code: MIT.
Data: CC BY 4.0 — © KNMI.