Command Palette

Search for a command to run...

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

Topic 7.7

Build: Rides by Station and Bike Type

In one line

Add two charts to the dashboard: monthly rides stacked by district, and a heatmap of June by day and hour. Both come with a shared legend part. It answers Priya's question: which months, which parts of the city, and which hours?

Before you start

You will learn

  • Add a stacked-bar chart and a heatmap as two chart files that return { update }
  • Share one legend part between them
  • Run the dashboard with two new boxes, each with its own loading and error text

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/charts/stations-stacked.js
  • src/charts/june-heatmap.js
  • src/parts/legend.js
  • src/main.js

Think of it like this

A wall in the operations room. Until now Priya had a few separate notes pinned up. Today two big sheets go up: one shows the year month by month with the city's districts stacked inside each month, and one shows every hour of June as a coloured grid. Each sheet has its own key, written once by one person.

Words you'll meet

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

chart function
A function such as drawStationsStacked(container, rows) that draws one chart and returns { update }.
part
A piece several charts share, such as the legend in src/parts/.
update
The function a chart returns. Call it with new rows and the chart changes in place.
empty state
What the page shows when there is nothing to draw: a sentence, not a blank box.

Step by step

01Start from the Phase 6 project

You have lumen-dashboard/ with the Phase 6 files, npm run dev running, and hourly-june.csv and station-month.csv in public/data/lumen/.

02Add the legend part, then the two charts

Create src/parts/legend.js, then src/charts/stations-stacked.js and src/charts/june-heatmap.js from the files below. They import the legend, so create it first.

03Replace main.js and look

Replace src/main.js. Two boxes appear under the existing ones: the stack by district and the June heatmap. Their loading lines show and disappear.

04Break it on purpose

Rename hourly-june.csv in public/data/lumen/. The heatmap box shows a red message and every other chart stays. A server that answers 404 gives Could not load /data/lumen/hourly-june.csv (the server answered 404). A dev server that answers a missing file with its home page (status 200) gives hourly-june.csv is missing the columns datetime, hour and rides. Is /data/lumen/hourly-june.csv really the data file?, from the column check in loadHourlyJune. Rename the file back.

Break it on purposediagram
Rendering diagram…

Try it yourself

  1. 1

    Empty the data

    In startStacked replace rows.map(...) by []. The box shows No rides to show. in place of the chart (the empty chart is hidden, because its axis would show meaningless ticks), and the legend stays. In startHeatmap replace await loadHourlyJune() by [] and the heatmap box does the same.

  2. 2

    Send a bad row

    Add { month: "Jan", district: "Harbor", rides: 100 } (a different spelling) to the stack's rows. The console warns 1 row(s) were left out and the bars are unchanged.

Code & diagrams

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

The preview cannot import your local files, so this example pastes legend.js, stations-stacked.js, june-heatmap.js and the Phase 7 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/parts/legend.jsjs

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

// Legends are built from the chart's own scales, so they can never disagree with the chart.
import * as d3 from "d3";

/** A row of swatches with names, one for every value of an ordinal colour scale. */
export function drawLegend(container, color, options = {}) {
  const { label = "Legend" } = options;
  const list = d3.select(container).append("ul").attr("aria-label", label)
    .style("display", "flex").style("flex-wrap", "wrap").style("gap", "4px 14px")
    .style("list-style", "none").style("margin", "6px 0 0").style("padding", "0").style("font-size", "12px");
  function update(names = color.domain()) {
    const item = list.selectAll("li").data(names, (d) => d).join("li").style("display", "flex").style("align-items", "center").style("gap", "5px");
    item.selectAll("i").data((d) => [d]).join("i").style("width", "10px").style("height", "10px").style("background", (d) => color(d));
    item.selectAll("span").data((d) => [d]).join("span").text((d) => d);
  }
  update();
  return { update };
}

/** A colour bar for a sequential scale: a gradient from the lowest to the highest value, with both ends named. */
export function drawColorBar(container, scale, options = {}) {
  const { width = 160, label = "rides" } = options;
  const wrap = d3.select(container).append("div").attr("role", "img")
    .style("display", "flex").style("align-items", "center").style("gap", "6px").style("font-size", "12px").style("margin-top", "6px");
  const low = wrap.append("span");
  const bar = wrap.append("span").style("width", width + "px").style("height", "10px");
  const high = wrap.append("span");
  function update() {
    const [lo, hi] = scale.domain();
    const stops = d3.range(0, 1.01, 0.1).map((t) => scale(lo + (hi - lo) * t));
    bar.style("background", "linear-gradient(to right, " + stops.join(", ") + ")");
    low.text(lo);
    high.text(hi + " " + label);
    wrap.attr("aria-label", "Colour scale from " + lo + " to " + hi + " " + label + ", light to dark");
  }
  update();
  return { update };
}

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

Read the date as a Date

In loadHourlyJune replace the day with new Date(d.datetime).getUTCDate().

terminal
$ In the heatmap
── what you'll see ──
On a computer set to UTC+5:30: six rows get day 31 and the heatmap warns "6 row(s) were left out". Hours 0 to 5 of every other day are filed under the day before, so 30 June has no cells for hours 0 to 5, and each day's early hours show the next day's rides.

Myth vs fact

Myth

Each chart should load its own data.

Fact

Loading in main.js (or load.js) and passing rows in keeps charts easy to test and to reuse with other data.

Remember this

  1. 1

    Priya's need is met. She can see the months and the districts in one chart, and the rush hours in another. The stack shows July at 117,803 rides and Westpark joining in May. The heatmap shows two weekday peaks (about 8 and 17 to 18 o'clock) and the weekend midday (busiest from 13 to 15 o'clock). The e-bike share by month is Topic 7.2's example, and bike types are still not on the dashboard.

  2. 2

    How it plugs into what exists. You start from the Phase 6 project: rides-line.js (now with D3 axes) and stations-bar.js stay. Each new chart is a function that takes a container and rows and returns { update(rows) }, like drawStationsBar. src/parts/legend.js is new: drawLegend for colour names and drawColorBar for a gradient. main.js adds two boxes with their own loading and error lines, and loads hourly-june.csv as text columns (day, hour, rides) because its datetimes have no time zone.

  3. 3

    The checks inside the charts. Both charts drop rows without a number (and warn), show "No rides to show." when nothing is left, and put a summary in aria-label (busiest month, busiest hour). The stack treats a missing district and month as 0 rides. The heatmap draws zero as grey. Both rebuild the colour domain from the new data in update.

  4. 4

    What the dashboard can do now that it could not before: compare months and districts in a stack, find rush hours in a heatmap, and explain colours with legends built from the same scales. Still missing: the charts do not react to pointer or touch (Phase 8), the colours and fonts do not follow the dark theme (Phase 10), the e-bike share has no chart of its own (Topic 7.2 shows it), and the heatmap has no text alternative beyond its summary (Phase 15).

Explain it without notes

01

Why does drawLegend take a colour scale and not a list of colours?

02

Why does loadHourlyJune not use d3.autoType?

Practice

01

Make drawStationsStacked accept a keys option so it can stack other groups. Which lines use KEYS?

02

Add a label option to drawJuneHeatmap that replaces the words after the colour bar's maximum. Which line in legend.js does it reach?

Done when you can

  • My project has stations-stacked.js, june-heatmap.js, legend.js and the new main.js.

  • The page shows the stack and the heatmap with their legends.

  • I made the heatmap fail on purpose and saw its box change, with the other charts intact.

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