Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~28 min· topic 6 of 6

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.

the arithmetic for each barwhole filetext
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                    142

03Draw 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. 1

    Show six bars

    In the first example, change lowStations(rows, 5) to lowStations(rows, 6) in the const low line. Chapel appears at 41.7% with width 100 and y 170. In the page you would need a taller viewBox (the sixth row ends at y = 188), which is why the plan says "lowest five".

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

From 25 stations to five bars New tab

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: true
The dashboard after Phase 2 New tab

This 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

Press Run to see the page.

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: 7

Your project files

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

index.htmlhtml

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

terminal
$ Preview
── what you'll see ──
The label "6 of 30" now starts at x = 110, on top of the bar: dark text on orange, touching the axis line, and no longer right after the bar's end like the other four. The Console still prints the five normal lines and no error.

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.sort has 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. 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. 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-bar box 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-bar box's heading changes from "Busiest stations" to match. A new #chart-plan box, spanning the full width, is a table of Who, Question, Chart and Phase.

  3. 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. 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. 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. 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.json with 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

01

Why is the five-station chart sorted and measured as bikes ÷ docks rather than listed by station number with raw bike counts?

02

How is each bar's width worked out, and why is Clock Tower's 48?

03

What did the plan decide for Meera's question, and why isn't it a chart?

Practice

01

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

02

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.