Realtime ECMWF Temperature Globe

A self-updating 3D planetary heatmap streaming live forecast data end-to-end: from raw GRIB2 bytes on AWS to a bicubic-interpolated WebGL globe.

Running live below, re-blended to the current time every 10 minutes.

Live Server Data |UTC: Loading UTC...
Loading 3D Context...
-60
-40
-20
0
20
40

Temperature last updated for —

Forecast data: ECMWF IFS Open Data © 2026 ECMWF, licensed under CC BY 4.0

Pipeline Architecture

Twice a day, ECMWF publishes its IFS 0.25° global forecast as multi-gigabyte GRIB2 files on a public S3 bucket. This pipeline surgically extracts just the 2-metre temperature field, decodes it on a DigitalOcean VPS, and serves it to the browser as raw binary — no weather API, no middleman.

  • Acquisition: Each forecast file ships with a JSON .index sidecar listing the byte offset and length of every meteorological parameter. A Bash pipeline greps the 2t entry and issues a ranged S3 GET — downloading ~1.7 MB instead of gigabytes per forecast step.
  • Decode: A custom C program linked against ECMWF's ecCodes library reads the GRIB message straight from stdin, converts Kelvin to Celsius, and writes a flat little-endian float32 frame: 1440 × 721 = 1,038,240 grid points.
  • Serve: nginx exposes the frames with CORS enabled, and a cron schedule refreshes all nine forecast steps (0–24h at 3h intervals) after each 00z/12z cycle publishes, alongside a manifest.json recording the run's init time.
  • Deliver: A Next.js rewrite proxies /weather_data/* to the VPS, so the Vercel-hosted frontend fetches same-origin — sidestepping mixed-content blocking and CORS preflights in one move.

Interpolation Mathematics

The server provides forecast frames 3 hours apart, so the client blends the two frames bracketing wall-clock time per gridpoint, re-evaluating every 10 minutes. The globe you see is always interpolated to now:

T(x, t)=(1−f) Tk(x)+f Tk+1(x),f=t−tk3 hT(\mathbf{x},\,t) = (1-f)\,T_k(\mathbf{x}) + f\,T_{k+1}(\mathbf{x}), \qquad f = \frac{t - t_k}{3\,\mathrm{h}}

Spatially, the native 0.25° grid would blur under magnification. When zooming in, the visible window is resampled to a 1920×1080 texture with a Catmull-Rom bicubic spline — a 16-tap separable kernel that passes exactly through every grid node (w−1+w0+w1+w2=1w_{-1} + w_0 + w_1 + w_2 = 1):

w(s)=12[−s3+2s2−s3s3−5s2+2−3s3+4s2+ss3−s2],T(u)=∑n=−12wn Ti+nw(s) = \tfrac{1}{2}\begin{bmatrix} -s^3+2s^2-s \\ 3s^3-5s^2+2 \\ -3s^3+4s^2+s \\ s^3-s^2 \end{bmatrix},\qquad T(u) = \sum_{n=-1}^{2} w_n\,T_{i+n}

Finally, the day/night terminator is baked into the texture from the solar zenith angle at each gridpoint, with the subsolar latitude following the seasonal declination:

Solar Zenith

cos⁡θz=sin⁡φsin⁡δ+cos⁡φcos⁡δcos⁡(λ−λ⊙)\cos\theta_z = \sin\varphi\sin\delta + \cos\varphi\cos\delta\cos(\lambda-\lambda_\odot)

Declination

δ≈−23.44∘cos⁡ ⁣(360∘365(N+10))\delta \approx -23.44^\circ \cos\!\left(\tfrac{360^\circ}{365}(N+10)\right)

Live Feed

  • • Source: ECMWF IFS Open Data, 0.25° global
  • • Grid: 1440 × 721 (1,038,240 points)
  • • Frame size: 4.15 MB float32 binary
  • • Cycles: 00z & 12z daily, 9 steps each
  • • Client refresh: temporal re-blend every 10 min
surfacetemp-bitstream.sh
# Locate the 2t field inside the multi-GB GRIB2
JSON_LINE=$(grep '"param": "2t"' temp.index)
OFFSET=$(echo "$JSON_LINE" | jq -r '._offset')
LENGTH=$(echo "$JSON_LINE" | jq -r '._length')

# Download ONLY those bytes and pipe them
# straight into the ecCodes decoder
aws s3api get-object \
  --bucket ecmwf-forecasts \
  --key "$GRIB_KEY" \
  --range "bytes=${OFFSET}-$((OFFSET+LENGTH-1))" \
  --no-sign-request \
  /dev/stdout | ./grib2bin "$OUT"

Rendering Stack

deck.gl GlobeView drapes the field over a WebGL sphere; the GPU handles all magnification between interpolated texels.

Base heatmap: full-globe BitmapLayer, bilinear GPU filtering
Zoom overlay: 1080p bicubic window, rebuilt on pan/zoom
96×192 tessellated sphere + Natural Earth 50m coastlines
Day/night terminator + sun position, updated in step

Forecast data: ECMWF IFS Open Data, © ECMWF, licensed under CC BY 4.0.