Weather scalar rendering
The colored "wash" overlays for temperature, wind speed, and (internally) precipitation are server-rendered RGBA PNGs. They look like full-resolution satellite imagery; they're actually 36 × 17 source values upsampled by the browser.
The pipeline
Open-Meteo grid (36 × 17 floats)
↓
LANCZOS resample to 720 × 360 (Pillow)
↓
Per-pixel colormap (Pillow + numpy)
↓
Save as RGBA PNG
↓
Cache in process memory (30 min TTL)
↓
Serve as Leaflet L.imageOverlay over the bounds
Why server-render and not client-render
Three options were on the table:
- Send the raw grid as JSON, render in WebGL on the client. Beautiful, but every browser becomes responsible for shaders and fragment programs. Mobile Safari struggles; IE/old Edge users see nothing.
- Send the grid as JSON, render on a
<canvas>. Works everywhere, but the per-pixel colour math runs in JavaScript for ~260,000 pixels — ~50 ms cost on every frame. - Render once on the server, ship a tiny PNG. ~30 KB per layer. Browser-native scaling. Works on every device. (We chose this.)
LANCZOS resampling
The 36 × 17 source grid is too sparse for direct rendering — at 720 × 360 output, each source cell would cover 20 × 21 output pixels as a hard rectangle, and the result would look like giant Minecraft blocks.
Pillow's Image.LANCZOS resampler does sinc-windowed
reconstruction: each output pixel is a weighted sum of nearby
source values, with the weights derived from the sinc function
truncated to a 3-cell window. The math:
sinc(x) = sin(π * x) / (π * x)
lanczos3(x) = sinc(x) * sinc(x / 3), -3 ≤ x ≤ 3
The output is smooth — no visible source-grid boundaries — even at zoom levels where one source cell maps to most of the viewport.
Per-field colormaps
Each scalar field has its own colormap function:
Temperature (continental wash, -30 °C → 40 °C):
def temp_color(t):
norm = clamp((t + 30) / 70, 0, 1)
if norm < 0.20: return interp(BLUE_DEEP, BLUE, norm / 0.20)
elif norm < 0.45: return interp(BLUE, GREEN, (norm - 0.20) / 0.25)
elif norm < 0.55: return interp(GREEN, GOLD, (norm - 0.45) / 0.10)
elif norm < 0.80: return interp(GOLD, ORANGE, (norm - 0.55) / 0.25)
else: return interp(ORANGE, RED, (norm - 0.80) / 0.20)
The narrow gold band at 15-21 °C is intentional — that's the "comfortable" range, and a sharp visual distinction there makes the user instantly read whether a place is too hot, too cold, or fine.
Wind speed (fades from transparent at calm to amber at gale, no green to keep it distinct from temp):
def wind_color(speed_kmh):
norm = clamp(speed_kmh / 80, 0, 1)
return interp_alpha(GOLD_LOW, AMBER_HIGH, norm), alpha=norm
Per-pixel composition (numpy)
For the 720×360 output, a Python loop would be 260k iterations in pure Python — 5+ seconds. Numpy vectorises the colormap across the whole array:
img = np.zeros((H, W, 4), dtype=np.uint8)
norm = np.clip((values + 30) / 70, 0, 1)
# Vectorised piecewise interpolation:
band = np.searchsorted(STOPS, norm)
t = (norm - STOPS[band-1]) / (STOPS[band] - STOPS[band-1])
img[..., 0] = (1 - t) * R[band-1] + t * R[band]
img[..., 1] = (1 - t) * G[band-1] + t * G[band]
img[..., 2] = (1 - t) * B[band-1] + t * B[band]
img[..., 3] = 255
Cost: ~80 ms total per frame, dominated by the LANCZOS resample.
Caching
Each rendered PNG is stamped with a t={half-hour-bucket}
query parameter. The browser caches it for the default
Cache-Control: public, max-age=1800 window. The next half
hour the same URL serves; after that, a new bucket triggers a
re-render.
In-process the renderer also caches the last N=4 PNGs (one per field) keyed by half-hour bucket — so a wave of users in the same half-hour all hit the same cached bytes.
Why Open-Meteo and not GFS / ECMWF directly
- Open-Meteo is free, commercial-OK, no key needed.
- The data is already a 5°-grid forecast for the next ~7 days, which is plenty for live-tracking context.
- They aggregate from the major models (GFS, ICON, ECMWF where permitted) and republish as a uniform API — we get the best of all worlds without writing code per provider.
Related
- Wind layer — user-facing view.
- Temperature layer.
- Pressure isobars — contour computation, separate pipeline.
Need help? Contact support · Where Is Tereza?