The Scroll You're Telling
Every chart below is built from one stream: you, reading this. The page samples your scroll position eight times a second and plots it with the same realtime machinery used for Wikipedia edits or market orders. As you read, the charts record where you pause, skim, or move backward.
A printed chart stays the same for every reader
Florence Nightingale’s diagram of deaths in the Crimean War gave readers the same printed view of the evidence. They could study it, skip it or return to it, but those choices left the chart unchanged.
Print keeps the author’s arrangement fixed while readers choose their own pace. This page adds a record of that pace: the line beside the article shows your position as you move through it.
Pause, scroll down or return to an earlier sentence to see the line respond.
Scroll depth · where you are on the page
Interactive charts let readers ask for another view
Interactive graphics gave readers ways to change a chart: filter a category, reveal a value or drag through time. Gapminder’s moving bubbles made it possible to follow changes in income and lifespan while choosing where to look.
Each action asks the chart for a different view of its data. The pointer measurements here record movement through the interface. They show activity, while your reason for moving remains yours.
Pointer activity · moving through the interface
Scrolling sets the pace of the story
The New York Times’s 2012 “Snow Fall” helped popularize stories that combine scrolling text with changing graphics. Authors prepare the sequence, and the reader’s position determines when each part appears.
This article uses a similar arrangement, with text beside a graphic that stays in view. But its charts also record your movement through that arrangement. The velocity plot distinguishes a quick pass from a pause or a return upward.
Scroll back to the previous paragraph. The velocity trace will dip below zero.
Velocity · direction and speed of scrolling
Your movements become a live record
A realtime chart receives observations as they arrive and keeps a recent window in view. Here, the observations are your scroll position and movement, sampled eight times a second.
The three charts read that stream differently. The line shows position, the dots show speed and direction, and the bars total the time each chapter spent in view.
Compare them before replaying your session. A return to an earlier section looks different from a long pause, even if both sessions finish at the same point on the page. The record preserves that difference; it cannot tell us what you understood or why you stopped.
The stream · position, direction and time
Your reading, as it happens
Read a little more, then replay your own session.
A stream preserves the order of reading
A single completion percentage leaves out returns to earlier sections and quick passes through later ones. The live charts show those movements as they happen.
Your scrolling supplies the data.
In “Snow Fall,” scrolling revealed a sequence prepared by the authors. Here each pause and movement adds a new observation to the charts. Position records where the page was; it leaves you to explain what you were doing there.
One reader, three coordinated views
A single rolling buffer of telemetry feeds a realtime line (position), a realtime swarm (signed velocity), and an ordinal bar fed through replace() (dwell per chapter). Semiotic owns the time windows, canvas rendering, axes, hit-testing, and per-beat styling.
// The reader is the stream. Sample scroll eight times a second. const lineRef = useRef(null) const swarmRef = useRef(null) useReadingTelemetry((sample) => { setBeats(current => [...current, sample].slice(-3600)) }) // { t, scroll, velocity, pointer, chapter, highlighting, kind } // kind = highlighting ? "highlight" : signOf(velocity) — a live text // selection recolors the beat, so highlighting reads apart from scrolling. // One library hook reconciles the rolling buffer into each chart. useSyncedPushData(lineRef, windowedBeats, { id: "id" }) useSyncedPushData(swarmRef, windowedBeats, { id: "id" }) <RealtimeLineChart ref={lineRef} timeAccessor="t" valueAccessor="scroll" valueExtent={[0, 1]} timeExtent={rollingTimeExtent(windowedBeats)} windowSize={720} pointIdAccessor="id" /> <RealtimeSwarmChart ref={swarmRef} timeAccessor="t" valueAccessor="velocity" yScaleType="symlog" categoryAccessor="kind" colors={beatColors} pointIdAccessor="id" annotations={latestRereadCallout} // the story never noticed. we did. /> // Attention pools where you dwell — an ordinal bar fed by replace(). barRef.current.replace(dwellByChapter(beats))No data leaves your browser. The “stream” is your own scroll, pointer, and dwell, sampled locally and discarded when you go. The seeded sample reading exists only so the replay has something to show before you’ve scrolled.