Topic 11.7
Build: The Trips Canvas
In one line
Add a canvas to the dashboard that draws all 10,000 June trips as dots around their start stations, sharp on every screen, with a text summary and a table beside it. It answers Arjun's wish to see every trip.
Before you start
You will learn
- Write a sharp-canvas helper,
setupCanvas, and use it in a chart file that returns{ update } - Draw 10,000 trips with one path and one fill, and explain what the dot positions mean
- Give the canvas a label with the finding, inner text and a table twin
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/canvas.js
- src/charts/trips-canvas.js
- src/main.js
Think of it like this
The operations room wall again. Until now every sheet showed a summary: months, districts, hours. Today a big sheet goes up where each of June's 10,000 trips is one small dot near the station it started from. Busy stations are thick clouds, quiet ones are thin. Beside the sheet hangs a typed list for anyone who reads instead of looks.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- setupCanvas
- Our helper in
src/utils/canvas.js: sizes the grid for the screen, sets the box, scales the context once and returns it. - jitter
- A small repeatable offset that spreads dots of the same station apart.
- table twin
- The same numbers as a real HTML table next to the canvas.
- empty state
- What the chart shows when there are no trips: a sentence, not a blank box.
Step by step
01Start from your dashboard
You have lumen-dashboard/ running with npm run dev, and trips-10k.csv in public/data/lumen/. This build starts from the Phase 10 dashboard (themes and responsive charts).
02Create the canvas helper, then the chart
Create src/utils/canvas.js, then src/charts/trips-canvas.js (it imports the helper). Then replace src/main.js.
03Run and look
A new box, Every June trip: 10,000 dots, loads and shows a cloud around each of the 25 stations (neighbouring stations overlap, so you count fewer separate blobs), the three busiest names (Quay, Lantern and Chapel; at a narrow width a name can sit on top of a neighbouring cloud) and a closed Show the numbers list. Open it: Quay 623 trips at the top, then Lantern 604 and Chapel 583.
04Break it on purpose
Rename trips-10k.csv. The box shows Could not load /data/lumen/trips-10k.csv (the server answered 404). in red and the other charts stay. Rename it back.
Try it yourself
- 1
Empty the data
In
startTrips, replacetripsby[]in thedrawTripsCanvascall. The canvas saysNo trips to show., its label says the same, and the table is empty. - 2
Send a trip from an unknown station
Add
{ id: "T0", start_station: "S99" }to the trips. The console warns1 trip(s) were left out: their start station is not in the stations list.and the picture is unchanged.
Code & diagrams
The preview cannot import your local files, so this example pastes canvas.js, trips-canvas.js and the Phase 11 part of main.js into one module (the loaders are one-line stand-ins for load.js). In your project they are separate files.
Preview
Console
Nothing logged yet.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.
// Makes a canvas sharp on every screen: the pixel grid is as big as the screen's real pixels, and the box on the page keeps its CSS size.
const MAX_RATIO = 3; // a ratio above 3 only costs memory: no phone screen needs more
/**
* Sizes a canvas to cssWidth x cssHeight CSS pixels and returns its 2D context, already scaled so you draw in CSS pixels.
* Setting width or height erases the canvas, so call this again after every resize, and draw again.
*/
export function setupCanvas(canvas, cssWidth, cssHeight, ratio = window.devicePixelRatio) {
if (!(cssWidth > 0) || !(cssHeight > 0)) {
throw new Error("setupCanvas needs a width and a height above 0, but got " + cssWidth + " x " + cssHeight + ".");
}
const r = Math.min(Math.max(Number.isFinite(ratio) ? ratio : 1, 1), MAX_RATIO);
canvas.width = Math.round(cssWidth * r); // the pixel grid, in device pixels
canvas.height = Math.round(cssHeight * r);
canvas.style.width = cssWidth + "px"; // the box on the page, in CSS pixels
canvas.style.height = cssHeight + "px";
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("This browser could not give a 2D drawing context for the canvas.");
// Use the real ratio of the rounded grid, so a fractional device ratio still fills the box exactly
ctx.setTransform(canvas.width / cssWidth, 0, 0, canvas.height / cssHeight, 0, 0);
return ctx;
}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
Forget the moveTo before each arc
In trips-canvas.js delete the line ctx.moveTo(x + jx * 24 + 2.2, y + jy * 24);.
Myth vs fact
Myth
Ten thousand dots must be slow.
Fact
As elements, yes. As one path painted once, it is one draw call. The cost is the number of path points, not the number of page elements.
Myth
The dot position is where the trip started.
Fact
Only the station is known. The jitter is made-up spread, so the cloud shows counts, not street positions.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The dots share one fill, so overlapping dots do not get darker. To show density, bin the trips (Topic 12.4) or paint in several passes with low alpha.
- ▸
A box with a canvas costs memory: 10,000 dots at ratio 2 in a 640 × 300 box is a 1,280 × 600 grid, about 3 MB.
- ▸
The station names are drawn straight on the dots, so on a narrow phone a name can overlap a neighbouring cloud. If you want them always readable, draw each name twice: first with
ctx.strokeTextin the page colour and a thicklineWidth(a halo), then withfillText.
Remember this
- 1
Arjun's need is met. All 10,000 trips are on the page as one picture, and it appears at once because the canvas holds one element, not 10,000. On a phone it is sharp, because
setupCanvasmakes the grid as big as the screen's pixels. - 2
Starting point: the Phase 10 dashboard. You have
lumen-dashboard/with everything from Phases 4 to 10: the loaders (loadStations,loadTrips), the crosshair, brush and presets, the keyed transitions, the themes (theme.css,charts.css, the theme button) andonResize. Three files change:src/utils/canvas.jsis new,src/charts/trips-canvas.jsis new, andsrc/main.jskeeps all of that and gains one box andstartTrips().index.htmldoes not change, because the box is added byaddBox. - 3
Themed and responsive. A canvas ignores
var(), sotrips-canvas.jsreads--chart-accent,--chart-inkand--chart-mutedwithgetComputedStyleat every draw.main.jsdraws again after every theme change andonResizecallsresize(width). - 4
The checks inside the chart. Trips whose station is not in the list are skipped with a console warning. With no trips the canvas says
No trips to show.in pixels and in its label. The summary names the busiest start station (Quay, 623 trips) and every station appears in the table twin.setupCanvasthrows a clear error for a zero size and for a missing context. - 5
What the dashboard can do now that it could not before: draw ten thousand marks without slowing the page, stay sharp on high-resolution screens, and describe a picture in words and in a table. Still missing: you cannot click or hover a dot (Phase 12 adds hit-testing), the cloud does not sit on a real map of Ardenvale (Phase 13), a full year of trips needs aggregation (Phase 12), and the table twin is basic (Phase 15).
Explain it without notes
Why does update call setupCanvas again, and not only clearRect?
Why is the jitter computed from the trip's id and not with Math.random()?
Practice
Make drawTripsCanvas accept options.color for the dots. Which line uses it?
Add a maxDots option that draws only the first maxDots trips and adds a sentence to the label saying how many were left out.
Done when you can
My project has canvas.js, trips-canvas.js and the new main.js.
The page shows the dot clouds, the three names and the table twin.
I made the trips file fail on purpose and saw only this box change.
I can say what the dashboard still lacks and which phase adds it.