Command Palette

Search for a command to run...

Hectal
PHASE 9Intermediate ~50 min· topic 6 of 6

Topic 9.6

Build: Smooth Month-to-Month Updates

In one line

Make the station bars slide when the month changes, draw the rides line on once, and give every chart one shared rule for reduced motion. It answers Tomás: he can now watch Lantern climb to first place.

Before you start

You will learn

  • Keep each station's bar as the same element, so it slides to its new rank
  • Reveal the rides line from left to right once, and leave later updates still
  • Put the reduced-motion rule in one file, src/utils/motion.js, used by both charts

You write code in

In your Vite project lumen-dashboard/ (run npm run dev), and in the editor on this page

Files you touch

  • src/utils/motion.js
  • src/charts/stations-bar.js
  • src/charts/rides-line.js

Think of it like this

The departures board at a station. When a train's platform changes, the row does not blink and rewrite: the whole row slides to its new line, so you still know it is your train. Today the dashboard's station board gets the same treatment.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

motion.js
The small file in src/utils/ that decides how much motion a chart may use: one place for the reduced-motion rule.
ready
The promise the chart returns for its first draw, so code can wait for the animation to finish.
reduced
A chart option: true for no motion, or the person's own setting when it is left out.
stagger
A delay that grows by a small step from one row to the next, so the list appears in order.
taken back
A leaving row whose station returns before it was removed: the join matches it again and its fade-out is interrupted.

Step by step

01Start from the Phase 8 project

You have lumen-dashboard/ with the Phase 8 files (tooltip, brush, stations stack, heatmap) and npm run dev running. Open the dashboard and switch the month: the bars jump.

02Add motion.js

Create src/utils/motion.js from the file below. It imports nothing, and nothing imports it yet.

03Replace the two chart files

Replace src/charts/stations-bar.js and src/charts/rides-line.js with the files below. Reload: the line draws itself once, and switching the month slides the bars. Nothing in main.js changes.

Replace the two chart filesdiagram
Rendering diagram…

04Test the other setting

Open the browser's developer tools, then Rendering, then "Emulate CSS media feature prefers-reduced-motion" and choose "reduce". Reload and switch the month: the bars take their new places at once, and the line is already complete. Choose "no emulation" again.

05Click the month fast

Switch the month five times in a second. The bars never snap: each click interrupts the last movement and the last month wins. The Console stays empty, because update handles the interruption itself.

Try it yourself

  1. 1

    Slow it down

    In main.js pass { duration: 1500 } to drawStationsBar. The bars take 1.5 seconds. Past about a second the motion starts to feel slow, which is the lesson of Topic 9.2.

  2. 2

    Switch the setting

    With the developer tools set to "prefers-reduced-motion: reduce", reload. The line has no reveal and the bars jump. In the first example change reduced: true to reduced: false for the right chart to see it move.

Code & diagrams

Station bars that slide: motion.js and stations-bar.js (modules inlined) New tab

The left chart moves (300 ms here), the right one gets the reduced-motion path of the same code. The preview inlines motion.js and stations-bar.js into one module; in your project they are separate files. Only the last of three quick updates reports `true`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

first draw finished: true true
rows on the page: 21
January to July, both charts: true true
rows now: 25 (four stations opened in May)
three updates in the same moment finished: false, false, true
back to January: true | rows left: 21
The Phase 9 part of the dashboard (modules inlined) New tab

The preview cannot import your project's files, so this example pastes motion.js, axis.js, a short stand-in for tooltip.js, rides-line.js and stations-bar.js into one module (the local imports are removed). In your project they are separate files, and main.js is the Phase 8 file, unchanged.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

while it draws: dasharray set: true
reveal finished: true | dasharray left: null
June, Lantern's row: translate(92,64)
right after the change event, Lantern's row: translate(92,64)
movement finished: true | Lantern's row now: translate(92, 4)

Your project files

Your lumen-dashboard project after this topic. Click a file to see it in full.

src/utils/motion.jsjs

The whole file after this topic. If yours differs, you can copy this one and carry on.

// Motion rules in one place, so every chart treats "reduce motion" the same way.

/** True when the person asked their device for less motion (an accessibility setting on every major system). */
export function prefersReducedMotion() {
  return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
}

/** The length of one movement in milliseconds: the base length, or 0 when motion should be reduced. */
export function ms(base, reduced = prefersReducedMotion()) {
  return reduced ? 0 : base;
}

/** The delay between neighbours so that a whole group has started within `total` ms, however many there are (never more than `cap`). */
export function stagger(count, total = 240, cap = 40) {
  return count > 1 ? Math.min(cap, total / (count - 1)) : 0;
}

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

Remove the key from the join

In stations-bar.js change .data(sorted, (d) => d.id) to .data(sorted).

terminal
$ In the first example
── what you'll see ──
The bars still change length when the month changes, but nothing slides: each row only rewrites its own name and number. Right after the change event Lantern's row is already at translate(92,4): it jumped to the top, it did not slide (the keyed version still reads translate(92,64) at that moment and arrives at translate(92, 4)).

Myth vs fact

Myth

Animation is polish you add at the end.

Fact

It decides how a change is read. Here it is what lets Tomás follow one station, which is the point of the chart.

Myth

Respecting reduced motion means writing a second chart.

Fact

One duration of 0 gives the same code and the same final picture. ms() is the only place that knows about the setting.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The charts read the setting each time they animate (ms(duration, reduced) with reduced unset), not once at start. A person who switches it on while the dashboard is open gets the new behaviour on the next update.

  • ▸

    update returns a promise only for callers that want it. The dashboard ignores it, a test awaits it, and a rejected end() is turned into false inside, so no caller needs a catch.

Remember this

  1. 1

    The problem this solves: when Tomás switches the month, the bars redraw in one frame and he cannot tell which station moved. Today that is fixed. When he switches the month from January to July, the 21 stations no longer rewrite themselves. Lantern's bar (fifth in January) slides up to first place, bars grow or shrink to their new length, the numbers count to their new values, and the four stations that opened in May fade in at the bottom. His need is met: he can say, out loud, "Lantern overtook Quay." Meera's cousin, and everyone else who has asked their device for less motion, gets the same final picture at once, with no sliding.

  2. 2

    How it plugs into what exists. You start from the Phase 8 project (the tooltip, the brush and the linked bars). src/utils/motion.js is new and has three small functions: prefersReducedMotion(), ms(base) (the duration, or 0 for reduced motion) and stagger(count) (a step that keeps a group's start within 240 ms). stations-bar.js and rides-line.js import it. main.js and every other file stay as they are: the charts keep the same function names and signatures, and update now returns a promise.

  3. 3

    The checks inside the charts. update is safe to call again and again: each call interrupts the previous movement and resolves its promise with false, and the last one resolves with true. A row that was still fading out when the month changed back is taken back by the join. The numbers count from the value on screen, not from the last finished one. The screen-reader text and the tooltips change at once, never half-way. The line reveals only on the first draw, clears its dashes when it ends or is interrupted, and does nothing for empty data or a single day (their path has length 0).

  4. 4

    What the dashboard can do now that it could not before: follow a station across months, see the line arrive, and respect the reader's motion setting in one place. Still missing: the bars use fixed colours that do not follow the dark theme or colour-blind-safe palettes, and the line's text does not adapt to small screens (Phase 10); Canvas for very large drawings (Phase 11); maps and layouts that animate (Phase 13); React versions of the charts (Phase 14); a full screen-reader story for the changing numbers (Phase 15).

Explain it without notes

01

Why does update return false for some calls?

02

Where does the dashboard decide how much motion to use?

03

Why does the line reveal only on the first draw?

Practice

01

Make the line reveal last 300 ms. Which option, and which file?

02

Add an onEnd option to drawStationsBar that is called when the movement is done. Which line of update would you extend?

Trade-offs

  • ↔

    Moving every row on each update costs a few hundred small writes, fine for 25 rows. For thousands of rows, animate only the first screen or switch to Canvas (Phase 11).

Done when you can

  • My project has src/utils/motion.js and the new stations-bar.js and rides-line.js.

  • Switching the month slides the bars, and the four May stations fade in.

  • With reduced motion emulated, the same switch is instant.

  • I clicked the month quickly and saw no snapping.

  • I can say what the dashboard still lacks and which phase adds it.