Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~26 min· topic 7 of 7

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.

Break it on purposediagram
Rendering diagram…

Try it yourself

  1. 1

    Empty the data

    In startTrips, replace trips by [] in the drawTripsCanvas call. The canvas says No trips to show., its label says the same, and the table is empty.

  2. 2

    Send a trip from an unknown station

    Add { id: "T0", start_station: "S99" } to the trips. The console warns 1 trip(s) were left out: their start station is not in the stations list. and the picture is unchanged.

Code & diagrams

The Phase 11 part of the dashboard (modules inlined) New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Your project files

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

src/utils/canvas.jsjs

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);.

terminal
$ In the picture
── what you'll see ──
No error. The clouds merge into one solid teal mass covering the whole map, with thin white streaks. The three names are still drawn, on top of the mass.

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.strokeText in the page colour and a thick lineWidth (a halo), then with fillText.

Remember this

  1. 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 setupCanvas makes the grid as big as the screen's pixels.

  2. 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) and onResize. Three files change: src/utils/canvas.js is new, src/charts/trips-canvas.js is new, and src/main.js keeps all of that and gains one box and startTrips(). index.html does not change, because the box is added by addBox.

  3. 3

    Themed and responsive. A canvas ignores var(), so trips-canvas.js reads --chart-accent, --chart-ink and --chart-muted with getComputedStyle at every draw. main.js draws again after every theme change and onResize calls resize(width).

  4. 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. setupCanvas throws a clear error for a zero size and for a missing context.

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

01

Why does update call setupCanvas again, and not only clearRect?

02

Why is the jitter computed from the trip's id and not with Math.random()?

Practice

01

Make drawTripsCanvas accept options.color for the dots. Which line uses it?

02

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.