I've always wanted to see Pakistan's rivers as living data. Not a static chart, not a table of numbers — an actual map where you can watch the water move, where the Indus glows brighter on a high-flow day and the dams rise and fall with the real reservoir levels. So one day I sat down with Claude and built it.
This is the writeup: what it shows, where the data comes from, how it's put together, and the handful of problems that were genuinely interesting to solve.
What You're Looking At
The app is a 3D terrain scene of Pakistan. The country's six major rivers — the Indus, Kabul, Jhelum, Chenab, Ravi, and Sutlej — are drawn as glowing blue lines that run from the snowy mountains in the north down to the Arabian Sea.
But they're not just lines. Each river is animated with comet-like pulses of light that travel downstream, in the same direction the water actually flows. The brighter and faster a river pulses, the more water is moving through it right now. The Indus, carrying by far the most water, blazes; the Sutlej, running low, barely flickers.
Pakistan's big dams — Tarbela, Mangla, and Chashma — appear as luminous towers. A wireframe cage marks each reservoir's full capacity, and an amber column inside rises to the real fill level, with the exact percentage printed right beside it. Mangla sitting at 58% full has a visibly taller amber column than Tarbela at 43%.
You can orbit, tilt, and zoom the whole country like a video game, toggle between a dark basemap and a satellite view, and there's a slow auto-orbit camera for recording. Labels for each river and dam fade in as you zoom.
Where the Data Comes From
Everything you see is live, fetched server-side and refreshed daily:
- River discharge comes from GloFAS — the Global Flood Awareness System, part of Europe's Copernicus programme — served through the Open-Meteo Flood API. It gives daily river discharge in cubic metres per second at points along each river.
- Reservoir levels come from Pakistan's own water authorities, WAPDA and IRSA, which publish daily water-situation reports.
- 3D terrain is real elevation data from AWS Terrarium tiles, exaggerated 3× so the Karakoram and Himalaya genuinely stand up off the map.
- Basemaps are keyless tiles — CARTO Dark Matter for the dark view, Esri World Imagery for satellite.
- River and border geometry come from Natural Earth and HydroSHEDS.
A nice property: no Mapbox token, no API keys, nothing to configure. It deploys to Vercel as-is.
The Tech Stack
- Next.js (App Router) + TypeScript
- deck.gl for the WebGL 3D rendering —
TerrainLayer,TripsLayer,PathLayer,ColumnLayer,ScatterplotLayer,TextLayer - Tailwind CSS for the UI overlay
The browser calls a single /api/snapshot route, which fetches all the river
discharge and dam levels in parallel, normalizes them onto a log scale, maps each
river to a color, speed, and trail length, and returns one JSON snapshot. That
result is cached in memory for an hour. If a source is down, the last good
snapshot (or sensible seasonal fallbacks) is served — so the visualization never
renders blank.
The Interesting Problems
Making Light Actually Glow
A neon glow isn't one bright line — it's light adding up. The look comes from
stacking several additive-blended passes per river on a dark background: a wide,
soft halo at low opacity, then progressively tighter and brighter passes, down to
a thin white-hot core line. With additive blending (blendFunc of source-alpha
and one), overlapping layers literally sum their light, so the center blooms
exactly the way a real light source does against darkness. Five passes per river
gets you that portfolio-grade glow instead of a flat blue stroke.
The Indus Went Dark — and Why
Each river gets comet pulses via deck.gl's TripsLayer. Early on, every river had
a single pulse whose glowing trail covered a fixed fraction of its length. This
looked great on short rivers — a single comet fills most of the tiny Kabul — but
the Indus is roughly three times longer than any other river. One comet on the
Indus lit up maybe a fifth of it and left the rest dark, so the country's biggest
river read as having no flow at all, while trickles like the Ravi looked lively.
Backwards.
The fix was to emit multiple, evenly phase-shifted pulses per river, scaled to the river's length — about one pulse per three degrees of path, capped at six. The Indus now carries around five pulses spaced along its full length, so it reads as a continuous, flowing stream, while low-flow rivers keep their single sparse comet. Crucially, discharge still drives the speed and trail length of each pulse, so the physical meaning is intact — the change was purely about spatial density, not the data mapping.
Dams as Honest Water Vessels
The first version of the dams was a translucent glass tube with an oversized glowing base — it washed everything yellow and, worse, the fill height was hardcoded rather than driven by live data. The rebuild made each dam a wireframe cage (marking full capacity) with a solid column of water inside, colored teal-to-amber by fill level, topped with a bright white waterline rim. The column height is fill × capacity, pulled straight from the snapshot — so what you see is genuinely how full the reservoir is, between its dead level and its maximum.
A deck.gl v9 Typing Gotcha
The dev server rendered perfectly, but the Vercel production build failed. The
cause: deck.gl v9 moved to luma.gl v9, and the old parameter names I was using —
depthTest, blend, blendFunc, clearColor — still work at runtime through a
compatibility shim but are no longer in the strict TypeScript types. Turbopack's
dev server skips type-checking, so the errors only appeared when next build ran
the real tsc. The fix was to route those parameter objects through luma.gl's
Parameters type cast, keeping the exact runtime behavior while satisfying the
compiler. A good reminder that "it runs in dev" and "it builds for production" are
two different guarantees.
A Rotating GIF, Headless
For the README and social posts I wanted a looping GIF of the scene orbiting.
That's a small pipeline of its own: drive a headless Chrome (with software WebGL
via ANGLE/SwiftShader so it renders off-screen), let the terrain tiles and
snapshot load, capture 48 frames as the auto-orbit camera sweeps, then resize each
frame and quantize it into a palette to encode the GIF. The result is a
self-contained script — run the dev server, run the script, get docs/orbit.gif.
Built in a Day, With Claude
The honest headline is that this went from idea to deployed app in a single day. Claude did the heavy lifting — the deck.gl layer stacking, the data-fetching route with fallbacks, the animation math, the tricky bits like the long-river pulse fix and the v9 type casts — while I steered the look and caught the "wait, why is the Indus dark?" kind of problems.
What I like about the end result is that it's not a mockup. Every glow, every pulse speed, every amber water level is a real number from a real source, refreshed daily. It's the thing I always wanted to look at — Pakistan's rivers, alive.