- JavaScript 55.3%
- Python 23.7%
- Shell 11.3%
- CSS 7.2%
- HTML 2.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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). |
||
| css | ||
| js | ||
| scripts | ||
| .gitignore | ||
| .python-version | ||
| icon.svg | ||
| index.html | ||
| manifest.json | ||
| nginx.conf.example | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
| sw.js | ||
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
- Download the HDF5 and NetCDF files from the KNMI Data Platform (see links above).
Use the included
scripts/fetch-knmi.shfor automated cron-based fetching. - 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 - 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/ - 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) - 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.