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:
truefor 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.
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
Slow it down
In
main.jspass{ duration: 1500 }todrawStationsBar. 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
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: truetoreduced: falsefor the right chart to see it move.
Code & diagrams
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
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: 21The 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
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.
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).
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)withreducedunset), not once at start. A person who switches it on while the dashboard is open gets the new behaviour on the next update. - ▸
updatereturns a promise only for callers that want it. The dashboard ignores it, a test awaits it, and a rejectedend()is turned intofalseinside, so no caller needs acatch.
Remember this
- 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
How it plugs into what exists. You start from the Phase 8 project (the tooltip, the brush and the linked bars).
src/utils/motion.jsis new and has three small functions:prefersReducedMotion(),ms(base)(the duration, or 0 for reduced motion) andstagger(count)(a step that keeps a group's start within 240 ms).stations-bar.jsandrides-line.jsimport it.main.jsand every other file stay as they are: the charts keep the same function names and signatures, andupdatenow returns a promise. - 3
The checks inside the charts.
updateis safe to call again and again: each call interrupts the previous movement and resolves its promise withfalse, and the last one resolves withtrue. 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
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
Why does update return false for some calls?
Where does the dashboard decide how much motion to use?
Why does the line reveal only on the first draw?
Practice
Make the line reveal last 300 ms. Which option, and which file?
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.jsand the newstations-bar.jsandrides-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.