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.
Try it yourself
- 1
Empty the data
In
startStackedreplacerows.map(...)by[]. The box showsNo rides to show.in place of the chart (the empty chart is hidden, because its axis would show meaningless ticks), and the legend stays. InstartHeatmapreplaceawait loadHourlyJune()by[]and the heatmap box does the same. - 2
Send a bad row
Add
{ month: "Jan", district: "Harbor", rides: 100 }(a different spelling) to the stack's rows. The console warns1 row(s) were left outand the bars are unchanged.
Code & diagrams
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
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.
// 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().
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
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
How it plugs into what exists. You start from the Phase 6 project:
rides-line.js(now with D3 axes) andstations-bar.jsstay. Each new chart is a function that takes a container and rows and returns{ update(rows) }, likedrawStationsBar.src/parts/legend.jsis new:drawLegendfor colour names anddrawColorBarfor a gradient.main.jsadds two boxes with their own loading and error lines, and loadshourly-june.csvas text columns (day, hour, rides) because its datetimes have no time zone. - 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 inupdate. - 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
Why does drawLegend take a colour scale and not a list of colours?
Why does loadHourlyJune not use d3.autoType?
Practice
Make drawStationsStacked accept a keys option so it can stack other groups. Which lines use KEYS?
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.