Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~21 min· topic 6 of 7

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.

a schedulerwhole filejs
let queued = false;
function requestDraw() {
  if (queued) return;      // a draw is already waiting
  queued = true;
  requestAnimationFrame(() => { queued = false; drawAll(); }); // drawAll reads the latest state
}
Draw once per burstdiagram
Rendering diagram…

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.

Many ways in, one way throughdiagram
Rendering diagram…

Try it yourself

  1. 1

    Add a fourth view

    In the first example add store.subscribe((s) => console.log("view 4 sees:", s.range ? s.range.length : null)); before report("start"). It draws at once (it prints view 4 sees: null) and again after every select. No other code changed.

  2. 2

    Check a different hash

    In the second example add "#from=2025-12-30&to=2026-02-01" to the list. It prints 2025-12-30 to 2025-12-31: the end is clamped to the last day of data.

Code & diagrams

One store, three views, four kinds of range New tab

A real brush would call select(from, to) from its end event (Topic 8.5).

Preview

Press Run to see the page.

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 selected
The selection in the URL: eight inputs and a real hashchange New tab

In 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: SecurityError

Break 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.

terminal
$ Console
── what you'll see ──
#from=2025-02-30&to=2025-03-10 -> 2025-03-02 to 2025-03-10

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 set means a view that calls set triggers itself. Guard with an equality check (if (same(range, state.range)) return;) when views can write.

  • ▸

    Writing the URL on every brush event floods the history if you use pushState. replaceState while dragging, and at most one pushState on end, keeps the back button useful.

Remember this

  1. 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. 2

    One state, many subscribers. The store in the first example is ten lines: get(), set(patch) and subscribe(view). A view is a function that draws from the state; set calls every view. A new view just subscribes. The line chart never mentions the scatter plot. In a project this store lives in src/main.js; EventTarget with CustomEvent, or a library, do the same job.

  3. 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. 4

    A burst of changes should draw once. A brush sends many brush events 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 use requestAnimationFrame (one draw per frame). Also keep expensive views on the end event and cheap ones on brush (Topic 8.5).

  5. 5

    Keep the selection in the URL. #from=2025-06-01&to=2025-06-30 makes a view shareable and works with reload and the back button. Write it on end, read it on load and on hashchange. The URL is user input, so check everything: a missing to, abc, a reversed, outside or empty range, and a date that is not real: d3.utcParse("%Y-%m-%d") accepts 2025-02-30 and returns 2 March, so format the result again and compare it with the text. In the live preview history.replaceState throws a SecurityError (an opaque-origin frame), so the example sets location.hash. In your project use history.replaceState in a try block: it adds no history entry per drag.

  6. 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

01

Why use a shared store instead of letting charts call each other?

02

Why does batching with a scheduler draw once for ten changes, and why is that still correct?

03

What must you check when you read a selection from the URL?

Practice

01

Write same(a, b) for two ranges (or null) and use it so store.set skips views when nothing changed.

02

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 brush event 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.