← Portfolio

Demo 03 · For site selection & geospatial SaaS

Trade-area simulation, live.

A Huff probabilistic trade-area model over 30 stores spread across the US. Each pixel computes the probability of choosing every store (gravity model: attractiveness ÷ distanceβ) and the overlay paints the dominant winner. Drag any store and the field recomputes at 60fps in WebAssembly. Click "Drop new store" then tap the map and watch the surrounding boundaries collapse.

Huff trade-area model

Compute— ms
Stores30
Cells / frame
Throughput

Each store is a colored sprite. Drag a sprite to relocate. Toggle "Drop new store" and click the map to add. "Animate β" sweeps decay from 0.8 to 3.0 so you can see boundaries tighten as distance matters more.

Loading WebAssembly…
Click anywhere on the map to drop a new store

If you build

A site-selection tool · demographic analytics SaaS · telecom or retail planning platform

Your users have a map and a panel of sliders: population density, drive-time, competitor proximity, zoning, demographics. Every change to a parameter is a backend round-trip, an eight-second spinner, and a small piece of their attention that doesn't come back.

The problem we solve

Analytical tools succeed when they feel like instruments, not databases. If a slider takes eight seconds to repaint, your power users learn to plan their queries and walk away while they run. The product becomes a batch job. The competitor with a sub-100ms map becomes the one their muscle memory prefers.

What we'd build for you

  1. Audit which computations belong client-side. Rule of thumb: anything driven by a slider, anything visited more than twice per session, anything that's pure-data-over-data.
  2. Port the kernel to Rust → WebAssembly (or hand-write WAT for small kernels like this demo). One module, one linear-memory buffer, one export.
  3. Wire the output into your existing map/chart layer. Demo uses MapLibre + canvas overlay; we can target Mapbox, Leaflet, deck.gl, or a Three.js scene equivalently.
  4. The backend stays for the things that genuinely need it: data fetch, persistence, multi-user state. Everything else feels instant.

Stakes

Without it: your analysts keep telling you the tool is "a little slow." Demos compress badly. Churn correlates with feature complexity, not with feature value.

With it: your power users tell you the tool changed how they work. The slider becomes a feedback loop, not a request.

Profile a slow analysis path →

How it's built

  • Model: Huff Probabilistic Trade Area. For each grid cell c and store i: P(c→i) = (Aᵢ ÷ dᵢβ) ÷ Σ(Aⱼ ÷ dⱼβ). The map paints the argmax store per cell with brightness modulated by max-probability.
  • WASM kernel: hand-written WAT compiled in-browser by wabt.js. Single export huff_compute(n_stores, w, h, lat0, lon0, dlat, dlon, beta) reads store data from linear memory and writes one packed u32 per cell (8 bits dominant index, 24 bits probability).
  • Drag interaction: pointer events on the pin-canvas layer hit-test against rendered store positions, then update lat/lon via map.unproject() on every move, triggering a fresh compute via requestAnimationFrame.
  • Paint pipeline: WASM output is sampled per pixel, converted to ImageData with an HSL palette (golden-ratio hue spacing for N ≤ 32), drawn to an offscreen canvas, then scale-blitted onto the overlay aligned to visible map bounds.
  • Performance budget: 120×72 grid × 30 stores ≈ 260,000 evaluations per frame. WASM holds 60fps on a mid-range laptop; the JS comparison drops to ~12fps for the same work. The "Bench" button runs both and reports the ratio.

The WAT source: