Topic 8.6
Linked Views
In one line
Linked views react to one shared selection. A tiny store with get, set and subscribe is enough: views never talk to each other, bursts of changes are batched, and the selection can live in the URL if every incoming value is checked.
Think of it like this
The departure board and the platform signs in a railway station. Nobody walks from sign to sign to change them. The timetable is changed in one place and every sign shows the new time. A dashboard with a brush, a scatter plot and a summary works the same way: one selection, many signs.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- linked views
- Charts that react to the same selection, so choosing in one changes the others.
- store
- One object that holds the shared state and tells subscribers when it changes.
- batching
- Collecting many changes and drawing once for all of them.
- hashchange
- A browser event that fires when the part of the URL after # changes.
Step by step
01Three views, one store
The first example draws the year's line, a scatter plot of temperature against rides, and a summary line. Each is a function subscribed to the store. select(from, to) writes the state and nothing else. The log shows what each view did for a normal range, a reversed one, one that starts before the data and an empty one.
02The selection in the URL
The second example reads eight different hashes, including the dangerous ones, and writes one back. It sets location.hash for real and waits for the hashchange event.
03Draw once per burst
In a test run, ten rapid changes drew 30 times (10 changes times 3 views) without batching, and 3 times (one redraw) with a scheduler. The state is already up to date before the draw runs, which is why one draw is enough.
let queued = false;
function requestDraw() {
if (queued) return; // a draw is already waiting
queued = true;
requestAnimationFrame(() => { queued = false; drawAll(); }); // drawAll reads the latest state
}04Many ways in, one way through
The brush, the buttons and the URL all call the same select function, which validates once and tells every view.
Try it yourself
- 1
Add a fourth view
In the first example add
store.subscribe((s) => console.log("view 4 sees:", s.range ? s.range.length : null));beforereport("start"). It draws at once (it printsview 4 sees: null) and again after everyselect. No other code changed. - 2
Check a different hash
In the second example add
"#from=2025-12-30&to=2026-02-01"to the list. It prints2025-12-30 to 2025-12-31: the end is clamped to the last day of data.
Code & diagrams
A real brush would call select(from, to) from its end event (Topic 8.5).
Preview
Console
Nothing logged yet.Expected console output
start | shaded px: 0 | orange dots: 0 | No range selected
select June | shaded px: 25 | orange dots: 30 | 107,712 rides, 1 Jun to 1 Jul
same, reversed | shaded px: 25 | orange dots: 30 | 107,712 rides, 1 Jun to 1 Jul
starts before the data | shaded px: 26 | orange dots: 31 | 43,439 rides, 1 Jan to 1 Feb
empty range | shaded px: 0 | orange dots: 0 | No range selectedIn your own project, history.replaceState works (in a try block). The preview is a sandboxed frame, so it refuses.
Console
(nothing was printed)Expected console output
#from=2025-06-01&to=2025-06-30 -> 2025-06-01 to 2025-06-30
#to=2025-06-01&from=2025-06-30 -> 2025-06-01 to 2025-06-30
#from=2025-06-01 -> no selection
#from=2025-02-30&to=2025-03-10 -> no selection
#from=2024-01-01&to=2026-01-01 -> 2025-01-01 to 2025-12-31
#from=abc&to=def -> no selection
#from=2025-06-01&to=2025-06-01 -> no selection
(empty) -> no selection
write: #from=2025-06-01&to=2025-06-30 | none: ""
hashchange fired, state read back: 2025-03-01 to 2025-03-31
replaceState: SecurityErrorBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Trust the URL
In the second example, replace the real function by const real = (t) => parse(t ?? ""); and run it.
Myth vs fact
Myth
Linked charts need to call each other.
Fact
They only need the same state. The fewer charts know about each other, the easier any one is to replace.
Myth
A URL parameter is trusted input.
Fact
Anyone can edit it or paste an old link. Parse and validate it like any user input.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A subscriber list called synchronously inside
setmeans a view that callssettriggers itself. Guard with an equality check (if (same(range, state.range)) return;) when views can write. - ▸
Writing the URL on every
brushevent floods the history if you usepushState.replaceStatewhile dragging, and at most onepushStateonend, keeps the back button useful.
Remember this
- 1
The problem this solves: Priya brushes June on the line, and she expects the scatter plot to colour June's days and the numbers to update. If the line chart called the scatter plot and the summary directly, every new chart would need a call from every other chart: three charts need six links, five need twenty. Charts that know each other are hard to change.
- 2
One state, many subscribers. The store in the first example is ten lines:
get(),set(patch)andsubscribe(view). A view is a function that draws from the state;setcalls every view. A new view just subscribes. The line chart never mentions the scatter plot. In a project this store lives insrc/main.js;EventTargetwithCustomEvent, or a library, do the same job. - 3
One function decides what a selection means. The example's
select(from, to)swaps a reversed range, clamps a range that starts before the data, and turns an empty range into "no selection". Every view then receives a clean value and none of them repeats the checks. - 4
A burst of changes should draw once. A brush sends many
brushevents as the mouse moves. With three views, ten changes make 30 draws. Request a redraw instead of drawing: the first request schedules one draw, later requests before it runs add nothing, and the draw reads the latest state. A test run counted 30 draws for ten changes without batching and 3 with it. In a browser userequestAnimationFrame(one draw per frame). Also keep expensive views on theendevent and cheap ones onbrush(Topic 8.5). - 5
Keep the selection in the URL.
#from=2025-06-01&to=2025-06-30makes a view shareable and works with reload and the back button. Write it onend, read it on load and onhashchange. The URL is user input, so check everything: a missingto,abc, a reversed, outside or empty range, and a date that is not real:d3.utcParse("%Y-%m-%d")accepts2025-02-30and returns 2 March, so format the result again and compare it with the text. In the live previewhistory.replaceStatethrows aSecurityError(an opaque-origin frame), so the example setslocation.hash. In your project usehistory.replaceStatein atryblock: it adds no history entry per drag. - 6
Highlight or filter, and be honest about resolution. A linked view can keep all its marks and colour the selected ones (the scatter plot here) or show only the selected data. And not every view has the same resolution: station totals exist per month (
station-month.csv), not per day, so a view that follows a day range must say "months touched by the selection" instead of pretending.
Explain it without notes
Why use a shared store instead of letting charts call each other?
Why does batching with a scheduler draw once for ten changes, and why is that still correct?
What must you check when you read a selection from the URL?
Practice
Write same(a, b) for two ranges (or null) and use it so store.set skips views when nothing changed.
Write the code that updates the URL when a brush ends and restores the brush when the page loads with a hash.
Trade-offs
- ↔
A synchronous store is simple and predictable, but many subscribers on every
brushevent can be slow. Batching trades a little latency (up to one frame) for far fewer draws.
Done when you can
I can write a store with get, set and subscribe, and make a view that only draws from the state.
I can validate a range: reversed, outside the data, empty.
I can batch a burst of changes into one redraw and explain why it is still correct.
I can read a selection from the URL safely, including a date that a parser silently corrects.