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.

Embedded live on the home page

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)=(1f)Tk(x)+fTk+1(x),f=ttk3hT(\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 (w1+w0+w1+w2=1w_{-1} + w_0 + w_1 + w_2 = 1):

w(s)=12[s3+2s2s3s35s2+23s3+4s2+ss3s2],T(u)=n=12wnTi+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.44cos ⁣(360365(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.