Topic 2.6
Build: Pick and Sketch the Dashboard's Charts
In one line
Write a chart plan into your dashboard (which chart answers which team question), then replace the pie idea with a hand-made, sorted picture of the five emptiest stations. It answers Priya's question: which stations run out of bikes?
Before you start
You will learn
- Turn team questions into a chart plan you can keep in the page
- Work out five sorted bars from real numbers, with the ratio that answers the question
- Highlight one message and describe the chart in its accessible label
You write code in
In your own lumen-dashboard/index.html (replace its contents), and in the editor below
Files you touch
- index.html
Think of it like this
Before an architect draws a house, they list what the family needs: a kitchen the cook can see the garden from, a quiet room for work. The list comes first and the drawing follows. The chart plan is that list for the dashboard. Each row is one person's question and the chart that answers it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- chart plan
- A short table that lists each question the dashboard must answer and the chart that answers it.
- share (ratio)
- One number divided by another: here bikes ÷ docks, the part of a station that is stocked.
- stable sort
- A sort that keeps tied items in their original order, so the same input always gives the same picture.
- empty state
- What a chart box shows when there is nothing to draw, such as a message.
- direct label
- A label placed next to the mark it describes, instead of in a separate legend.
- accent colour
- The one bright colour used for the thing that matters, with everything else quiet.
Step by step
01Start from your Topic 0.7 file
Your lumen-dashboard/index.html has the header, three boxes and the week bars. Replace its contents with the file in the Files section below, save, and open it. The first three sections look the same, except the stations box and a new plan box at the bottom.
02Work out the five bars
The first example turns the 25 stations into the five bars: bikes ÷ docks, a stable sort from emptiest, the first five, and then share × 2.4 for each width and 30 + 28 × row for each top. Its output is the table of numbers you type into the SVG.
station bikes docks share width = share x 2.4 y = 30 + 28 x row
Clock Tower 6 30 20.0% 48 30
Rope Walk 7 20 35.0% 84 58
West Gate 7 20 35.0% 84 86
Station Road 11 30 36.7% 88 114
Hill 11 30 36.7% 88 14203Draw them in a viewBox
The SVG uses viewBox="0 0 380 170" and width="100%", as in Phase 1: the numbers above are in the drawing's own units and the picture scales to its box on any screen. The axis line at x = 110 is the zero baseline for every bar, and the station names are right-aligned just left of it.
04Label it for eyes and for screen readers
Each bar has its value written next to it. The aria-label on the <svg> lists all five stations with their numbers, so someone who cannot see the bars hears the same answer.
05Write the chart plan
The plan table lists seven questions from Priya, Tomás, Lina and Meera, each with a chart and the phase that builds it. Meera's row is not a chart: every chart will also come with its numbers as text (Phase 15). Open the page and check the Console: Bars: 7, Tallest bar: 160 (Tuesday), Low stations: 5, Emptiest first: true, Plan rows: 7.
Try it yourself
- 1
Show six bars
In the first example, change
lowStations(rows, 5)tolowStations(rows, 6)in theconst lowline. Chapel appears at 41.7% with width 100 and y 170. In the page you would need a tallerviewBox(the sixth row ends at y = 188), which is why the plan says "lowest five". - 2
Change the threshold
In the page, change the title to "Five stations are below 40% full" → "Five stations are below 30% full" and look at the bars. The title is now false, since four of the five are above 30%. A title is a claim; the picture must back it.
Code & diagrams
Rope Walk and West Gate tie at 35% (and Station Road and Hill at 36.7%): the stable sort keeps them in station-number order every time.
Console
(nothing was printed)Expected console output
Clock Tower 6 of 30 20.0% width 48 y 30
Rope Walk 7 of 20 35.0% width 84 y 58
West Gate 7 of 20 35.0% width 84 y 86
Station Road 11 of 30 36.7% width 88 y 114
Hill 11 of 30 36.7% width 88 y 142
sixth emptiest: Chapel 41.7%
100% full: Quay, Market, Mill, Harbour Gate, Ferry
stations below 20%: none: show a message, not an empty box
station with 0 docks is skipped: trueThis is the whole lumen-dashboard/index.html after Phase 2, exactly as in the Files section. Make the preview narrow to see the boxes stack and the plan table scroll.
Preview
Console
Nothing logged yet.Expected console output
Bars: 7
Tallest bar: 160 (Tuesday)
Chart shape (height / width): 0.553 at any size
Titled boxes: 4
Low stations: 5
Emptiest first: true
Plan rows: 7Your 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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lumen Bikes dashboard</title>
<style>
body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f6f7f9; color: #1d2b36; }
header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
header svg { flex: none; width: 56px; height: auto; color: #2a9d8f; }
header h1 { margin: 0; font-size: 22px; }
header p { margin: 2px 0 0; color: #5b6b78; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 12px; }
.box { background: white; border: 1px solid #dde3e8; border-radius: 8px; padding: 12px; }
.box h2 { margin: 0 0 8px; font-size: 15px; display: flex; align-items: center; gap: 6px; }
.box h2 svg { width: 18px; height: 18px; fill: #e76f51; }
.soon { color: #5b6b78; font-size: 14px; }
/* the week chart: one viewBox, so it scales with its box */
.chart { display: block; width: 100%; height: auto; }
.chart .bar { fill: #2a9d8f; }
.chart .bar.quiet { fill: #e76f51; }
.chart .baseline { stroke: #9aa8b4; }
.chart .labels { font-size: 12px; text-anchor: middle; fill: currentColor; }
.box p.note { margin: 6px 0 0; font-size: 13px; color: #5b6b78; }
#chart-plan { grid-column: 1 / -1; }
#chart-plan table { border-collapse: collapse; font-size: 13px; width: 100%; }
#chart-plan th, #chart-plan td { border: 1px solid #dde3e8; padding: 4px 8px; text-align: left; vertical-align: top; }
#chart-plan th { background: #eef2f5; }
</style>
</head>
<body>
<!-- Shapes defined once and reused with <use> -->
<svg width="0" height="0" style="position: absolute" aria-hidden="true">
<defs>
<symbol id="bike" viewBox="-2 -2 52 36">
<g fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M 4 24 a 8 8 0 1 0 16 0 a 8 8 0 1 0 -16 0"></path>
<path d="M 28 24 a 8 8 0 1 0 16 0 a 8 8 0 1 0 -16 0"></path>
<path d="M 12 24 L 19 10 L 32 10 L 24 24 Z"></path>
<path d="M 32 10 L 36 24 M 29 6 H 35 M 16 7 H 22"></path>
</g>
</symbol>
<symbol id="pin" viewBox="0 0 24 24">
<path d="M12 2 C8 2 5 5 5 9 C5 14 12 22 12 22 C12 22 19 14 19 9 C19 5 16 2 12 2 Z"></path>
<circle cx="12" cy="9" r="2.5" fill="#fff"></circle>
</symbol>
</defs>
</svg>
<header>
<svg viewBox="-2 -2 52 36" role="img" aria-label="Lumen Bikes: a bicycle">
<use href="#bike"></use>
</svg>
<div>
<h1>Lumen Bikes</h1>
<p>Ardenvale bike share, operations dashboard</p>
</div>
</header>
<main class="grid">
<section class="box" id="this-week" aria-labelledby="week-heading">
<h2 id="week-heading">Rides this week (2–8 June 2025)</h2>
<!-- 7 bars drawn by hand: height = rides ÷ 3,992 × 160 (the busiest day is 160 tall) -->
<svg class="chart" viewBox="0 0 380 210" role="img" aria-labelledby="week-title week-desc">
<title id="week-title">Rides per day, 2–8 June 2025</title>
<desc id="week-desc">Monday 2,979, Tuesday 3,992, Wednesday 3,759, Thursday 3,742, Friday 3,648, Saturday 3,013, Sunday 3,451. Monday, a rainy day, was the quietest.</desc>
<line class="baseline" x1="20" y1="180" x2="380" y2="180"></line>
<rect class="bar quiet" x="30" y="60.6" width="40" height="119.4"></rect>
<rect class="bar" x="80" y="20" width="40" height="160"></rect>
<rect class="bar" x="130" y="29.3" width="40" height="150.7"></rect>
<rect class="bar" x="180" y="30" width="40" height="150"></rect>
<rect class="bar" x="230" y="33.8" width="40" height="146.2"></rect>
<rect class="bar" x="280" y="59.2" width="40" height="120.8"></rect>
<rect class="bar" x="330" y="41.7" width="40" height="138.3"></rect>
<g class="labels">
<text x="50" y="198">Mon</text><text x="100" y="198">Tue</text><text x="150" y="198">Wed</text>
<text x="200" y="198">Thu</text><text x="250" y="198">Fri</text><text x="300" y="198">Sat</text>
<text x="350" y="198">Sun</text>
</g>
</svg>
</section>
<section class="box" id="rides-line" aria-labelledby="line-heading">
<h2 id="line-heading">Rides through the year</h2>
<p class="soon">Coming in Phase 3: a line of all 365 days.</p>
</section>
<section class="box" id="stations-bar" aria-labelledby="stations-heading">
<h2 id="stations-heading">
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#pin"></use></svg>
Stations running low (08:15, 18 June 2025)
</h2>
<!-- Share of docks holding a bike = bikes ÷ docks, emptiest first. 100% = 240 wide, so width = share × 2.4 -->
<svg viewBox="0 0 380 170" width="100%" role="img"
aria-label="The five emptiest stations, share of docks holding a bike: Clock Tower 6 of 30 (20 percent), Rope Walk 7 of 20 (35 percent), West Gate 7 of 20 (35 percent), Station Road 11 of 30 (37 percent), Hill 11 of 30 (37 percent).">
<text x="0" y="14" font-size="13" font-weight="700" fill="#1d2b36">Five stations are below 40% full</text>
<line x1="110" y1="24" x2="110" y2="164" stroke="#9aa8b4"></line>
<g font-size="12" fill="#1d2b36">
<text x="104" y="43" text-anchor="end" font-weight="700">Clock Tower</text>
<text x="104" y="71" text-anchor="end" font-weight="700">Rope Walk</text>
<text x="104" y="99" text-anchor="end" font-weight="700">West Gate</text>
<text x="104" y="127" text-anchor="end" font-weight="700">Station Road</text>
<text x="104" y="155" text-anchor="end" font-weight="700">Hill</text>
</g>
<rect x="110" y="30" width="48" height="18" fill="#e76f51"></rect>
<rect x="110" y="58" width="84" height="18" fill="#e76f51"></rect>
<rect x="110" y="86" width="84" height="18" fill="#e76f51"></rect>
<rect x="110" y="114" width="88" height="18" fill="#e76f51"></rect>
<rect x="110" y="142" width="88" height="18" fill="#e76f51"></rect>
<g font-size="12" fill="#1d2b36">
<text x="164" y="43">6 of 30</text>
<text x="200" y="71">7 of 20</text>
<text x="200" y="99">7 of 20</text>
<text x="204" y="127">11 of 30</text>
<text x="204" y="155">11 of 30</text>
</g>
</svg>
<p class="note">Quay, Market, Mill, Harbour Gate and Ferry are 100% full: no free dock to return a bike. Live data comes in Phase 5.</p>
</section>
<section class="box" id="chart-plan" aria-labelledby="plan-heading">
<h2 id="plan-heading">Chart plan: which chart answers which question</h2>
<table>
<thead><tr><th>Who</th><th>Question</th><th>Chart</th><th>Phase</th></tr></thead>
<tbody>
<tr><td>Priya</td><td>Which stations are running low right now?</td><td>Horizontal bars of bikes ÷ docks, emptiest first, lowest five highlighted</td><td>2 (by hand), 5 (data)</td></tr>
<tr><td>Priya</td><td>Where are the stations, and which are low?</td><td>Map with one dot per station</td><td>13</td></tr>
<tr><td>Tomás</td><td>How did the year go?</td><td>One line of 365 days, outage day marked</td><td>3, 7</td></tr>
<tr><td>Tomás</td><td>Which months were busiest?</td><td>12 bars from zero</td><td>5 (Topic 5.5), 7 (stacked)</td></tr>
<tr><td>Tomás</td><td>Do warm days bring more rides?</td><td>Scatter plot, temperature across, rides up</td><td>3 (by hand)</td></tr>
<tr><td>Lina</td><td>Which districts carried the most rides?</td><td>Sorted bars of totals (no pie)</td><td>2 (by hand), 7 (Topics 7.2, 7.3)</td></tr>
<tr><td>Meera</td><td>Can I get every chart's numbers without seeing it?</td><td>Every chart gets a text summary and a data table</td><td>15</td></tr>
</tbody>
</table>
</section>
</main>
<script>
// A quick self-check: bars, the chart's shape and the titled boxes
const bars = document.querySelectorAll("#this-week .bar");
const heights = Array.from(bars, (b) => Number(b.getAttribute("height")));
console.log("Bars:", bars.length);
console.log("Tallest bar:", Math.max(...heights), "(Tuesday)");
const view = document.querySelector("#this-week svg").viewBox.baseVal;
console.log("Chart shape (height / width):", (view.height / view.width).toFixed(3), "at any size");
console.log("Titled boxes:", document.querySelectorAll(".box[aria-labelledby]").length);
const low = document.querySelectorAll("#stations-bar rect");
const widths = Array.from(low, (r) => Number(r.getAttribute("width")));
console.log("Low stations:", low.length);
console.log("Emptiest first:", widths.every((w, i) => i === 0 || w >= widths[i - 1]));
console.log("Plan rows:", document.querySelectorAll("#chart-plan tbody tr").length);
</script>
</body>
</html>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
Put a label at the baseline
In the page, change the first bar's label <text x="164" ...>6 of 30</text> to x="110".
Myth vs fact
Myth
A chart plan is paperwork that slows the building down.
Fact
It takes ten minutes and stops you building a pie that nobody can read. Each row also says which phase will build the chart, so the team knows what is coming.
Myth
Hand-made bars are a waste because D3 will draw them later.
Fact
The numbers, the title, the label and the layout decisions stay. Only the typing is replaced. And you now know exactly what a scale does for you (Phase 6).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Sorting inside the data (
lowStations) rather than in the drawing code means the same list drives the chart, the accessible label and a data table. If the sort lived in the drawing, each of them could disagree. - ▸
Array.prototype.sorthas been required to be stable since ES2019, so ties keep their input order. In older engines it was not, and a chart could reorder tied bars between reloads. - ▸
A bikes ÷ docks ratio treats a 12-dock and a 30-dock station the same. For vans, Priya may care about absolute need (bikes short of half full). The chart plan is the place to record that choice: the question decides the measure.
Remember this
- 1
The problem this solves: Priya asked which stations run out of bikes, and the wall screen showed a 25-slice pie. Now the dashboard answers her with five bars she can read in two seconds. The plan also keeps the team from adding charts nobody asked for, and records what each later phase will build.
- 2
What the page has now. Phase 1's frame stays as it was: the bike icon, the scalable week chart and the titled boxes. The
#stations-barbox shows "Stations running low" for 08:15 on 18 June 2025: the five stations with the smallest share of docks holding a bike. The#stations-barbox's heading changes from "Busiest stations" to match. A new#chart-planbox, spanning the full width, is a table of Who, Question, Chart and Phase. - 3
The rule for the bars. The measure is bikes ÷ docks (Topic 2.3): Clock Tower has 6 bikes in 30 docks, 20%; Rope Walk and West Gate 7 in 20, 35%; Station Road and Hill 11 in 30, 36.7%. A full share of 100% is 240 pixels wide, so each width is the share × 2.4: 48, 84, 84, 88 and 88. Rows sit 28 pixels apart, sorted emptiest first, and each bar is labelled "6 of 30" so nobody has to guess the number behind the length.
- 4
One message. The five bars share one accent colour and the title says what they mean: "Five stations are below 40% full" (the sixth emptiest, Chapel, is at 41.7%). Because only five stations are drawn, there is no grey context here; a note under the chart mentions the five stations that are completely full, since a full station is a different kind of problem (no free dock to return a bike).
- 5
Edge cases the plan has to survive. If no station is below 40%, show a sentence ("No station is below 40% full") instead of an empty box. If two stations tie, keep a stable order (the first example does). A station with 0 docks would give
Infinity, so it must be filtered before the ratio. These checks go into code in Phase 5; for now the page is hand-made, and the numbers come from the table in the first example. - 6
What the dashboard can do now that it couldn't before: it names who asks what and which chart answers it, and it answers Priya's question for one moment in time with a chart the eye can rank. Still missing: the five bars are typed by hand and will be stale tomorrow (Phase 5 draws them from
status.jsonwith D3, Phase 16 refreshes them live); the week bars still have no axis or value labels (Phase 6); and the "Rides through the year" box is still empty (Phase 3 draws it by hand).
Explain it without notes
Why is the five-station chart sorted and measured as bikes ÷ docks rather than listed by station number with raw bike counts?
How is each bar's width worked out, and why is Clock Tower's 48?
What did the plan decide for Meera's question, and why isn't it a chart?
Practice
Write bar(x0, y, share, scale) that returns the SVG string for a bar and returns an empty string for a missing or negative share. Test (110, 30, 0.2, 240) and (110, 30, -0.1, 240).
Name two situations where a pie chart cannot show the data, and the chart you use instead.
Trade-offs
- ↔
Showing only the lowest five answers Priya fast but hides the rest. The page notes the five full stations, and Phase 5 can add "show all" as a table.
- ↔
Hand-made bars freeze a moment in time. They are right for 08:15 on 18 June 2025 and wrong an hour later, which is why Phase 5 draws them from data.
Done when you can
My index.html shows the plan table, the week bars and the five emptiest stations.
I can say why the chart measures bikes ÷ docks and sorts emptiest first.
I can work out any bar's width and y from its numbers.
The Console shows Low stations: 5, Emptiest first: true and no red lines.
I can say what the dashboard can do now and which phases add what's missing.