Skip to content
Serhii Kuznetsov
All projects

03Data Visualization2026Solo — data engine, GPU renderer, interface, tests

Strata — Crossfilter over Millions of Rows

Linked charts over up to ten million rows, all in the browser: brush any chart and every other one follows within the frame.

Strata — Crossfilter over Millions of Rows screenshot

The problem

Linked charts, where brushing one filters the rest, are easy with a few thousand rows and a server to ask. I wanted to see how far a browser can take them alone, with every row in memory, every chart linked to every other, each frame of a drag answered within the frame, and a static host. The answer was ten million rows. Memory, aggregation, rendering and the tests each had to be built for that size.

Decisions

  1. 01

    Columns in memory every worker shares

    Ten million trips as JavaScript objects would take gigabytes and keep the garbage collector busy. As seven typed-array columns they take 19 bytes a row, held in SharedArrayBuffers that a pool of workers generates and scans in place, with nothing copied between threads. Shared memory needs a cross-origin isolated page, so the host sends the COOP and COEP headers. Without them the page still works, on one thread.

  2. 02

    A brush is a lookup, not a scan

    Rescanning every row on every frame of a drag cannot keep up at this size. I followed Falcon (Moritz, Howe and Heer, CHI 2019). When the pointer enters a chart, one pass counts rows by bin of that chart × bin of each other chart, and the counts become prefix sums, or summed-area tables for the scatter plot's rectangle. After that a frame costs about 0.02 ms, whether there are one million rows or ten. The pass itself runs while the pointer is still on its way.

  3. 03

    The GPU re-filters every point during a drag

    Positions go to the GPU once. The workers write one byte per row, which says whether the row passes every filter except the one being brushed. The live brush reaches the shader as a few uniforms. So a drag on another chart uploads nothing, and every point is tested again on every frame.

  4. 04

    Counted, not painted

    The first version blended points straight onto the screen, and every dense area burned out to the same white, because an 8-bit channel is full after a few hundred points. Now each point adds one to its pixel in a float texture. The GPU finds the maximum by shrinking that texture 4 × 4 at a time, and a last pass colours each pixel by the log of its count. The plot also had stripes. Fares rounded to ten cents landed 0.77 px apart, and rounding to the cent fixed it.

  5. 05

    Tests that count everything again

    The generator is seeded per chunk, so the data comes out the same however many workers produce it. The end-to-end tests regenerate it in Node and compare the page's number with a plain loop over every row. That covers the stories, keyboard brushing, category toggles and pointer drags. The filters live in the URL, which makes every view a shareable link and lets the tests read back exactly what a drag selected.

  6. 06

    A fair comparison with SVG

    A second page times Recharts, which draws one SVG element per point, against the WebGL renderer, in the visitor's own browser. Its first numbers were unfair to WebGL. A run straight after a big SVG chart also paid for the browser collecting those 60,000 nodes, and measured 400 ms instead of 12. Runs now start once that cleanup is over.

What I measured

10M

rows in the browser

generated in about a second on eight workers

0.02 ms

per brush frame

the same at one million rows and at ten

12 ms

20,000 points in WebGL

Recharts SVG: 1.4–2 s and 60,082 elements

17 + 7

unit + end-to-end tests

every end-to-end count is checked against a brute-force scan

Two million trips: a density heat map of distance against fare, next to the charts it filters
Airport runs selected: the flat fare draws a straight amber line across the scatter plot
The comparison page: SVG and WebGL timed in the browser

Built with

  • React
  • TypeScript
  • Web Workers
  • WebGL2
  • Cloudflare Workers