Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~34 min· topic 6 of 6

Topic 3.6

Build: Rides This Year, Hand-Drawn

In one line

Draw all 365 days of Lumen's 2025 rides as a line chart with hand-made axes inside the dashboard's index.html. It answers Arjun's request: the team now knows what a chart library does, because they did it by hand.

Before you start

You will learn

  • Embed a year of data in index.html and draw it as a line with axes, gridlines and a note
  • Handle an empty array and a missing day without a broken picture
  • Say which parts of this chart a library would do for you

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 buying a power tool, you build one thing with hand tools. You feel what is slow and what is quick, and when you finally pick up the tool you know what you are paying for. Today the team draws a year by hand.

Words you'll meet

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

embedded data
Data typed into the page's own code, instead of loaded from a separate file.
empty state
What the page shows when there is nothing to draw: a clear message, not a blank box.
viewBox
An SVG attribute that sets the drawing's own coordinate system, so the picture scales to any width (Topic 1.4).
annotation
A note drawn on the chart that points at one thing, such as the day the app was down.
build topic
The last topic of a phase: it adds the phase's result to the dashboard.

Step by step

01Start from your file

Open your lumen-dashboard/index.html. Replace its contents with the file in the Files section (or the live example below): it is the Phase 2 page with the year's line added, so nothing you built earlier is lost.

02Paste the data

RIDES_2025 holds 365 numbers, 20 per line. START is 1 January 2025 in UTC milliseconds, so index i is the day START + i × DAY. NOTES marks index 225, which is 14 August.

03Paste the helpers

el() creates an SVG element with attributes (Topic 3.3), niceTicks() is the 1-2-5 rule from Topic 3.5, and dayName() turns an index into a date such as 14 August.

04Read drawRidesLine

It handles empty data first, then makes the margin, the y scale from the last tick, the title text, the gridlines, the month ticks, the path with the pen-up rule, lone dots and the note. Each part is a piece you wrote in an earlier topic.

05Check it

Console: Days: 365 | total rides: 933,651, Week bars: 7, Line segments: 364. The total equals the sum of the whole daily.csv. In the picture, find the July peak, the weekly wiggle and the orange note on 14 August.

06List what you wished for

Write down every place where you did something a library would do. That list is the reason for Phases 5 to 7.

Try it yourself

  1. 1

    Remove a day

    Change one number in RIDES_2025 (for example the 100th) to null. The line breaks at that day and the Console says Line segments: 362: the pen lifts at the gap, so one L becomes M and the gap removes another. The total also drops, to 932,065, because the quick self-check adds a null as 0 (sum + null).

  2. 2

    Draw an empty chart

    Change the last call to drawRidesLine(document.querySelector("#rides-line"), []). The box shows No rides to show yet. The last Console line becomes Uncaught TypeError: Cannot read properties of null (reading 'getAttribute'), because the self-check looks for a path that was never drawn: guard it too.

Code & diagrams

The Lumen Bikes dashboard after Phase 3 New tab

This is the whole lumen-dashboard/index.html after Phase 3 (the Phase 2 page plus the year's line), exactly as in the Files section. The data is embedded, so it runs without fetch.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Days: 365 | total rides: 933,651
Week bars: 7
Line segments: 364
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; }
    .wide { grid-column: 1 / -1; }
    .soon, .empty { 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 wide" id="rides-line" aria-labelledby="line-heading">
      <h2 id="line-heading">Rides through the year (2025)</h2>
      <!-- Phase 3: drawRidesLine() at the end of the script draws the chart here -->
    </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>
    // ---- 1. The data, embedded (a file opened by double-click cannot fetch the CSV) ----
    // Rides per day, 1 January to 31 December 2025: the "rides" column of /data/lumen/daily.csv.
    // If a day were missing, its entry would be null, never 0.
    const RIDES_2025 = [
      1193, 1584, 1592, 1063, 1243, 1149, 1479, 1447, 1550, 1602, 1009, 1001, 1540, 1477, 1427, 1345, 1514, 1270, 1332, 1413,
      1628, 1422, 1477, 1554, 1272, 1406, 1494, 1432, 1451, 1522, 1551, 1227, 1392, 1076, 1353, 1242, 1536, 1534, 1099, 1508,
      1675, 1723, 1739, 1601, 1560, 1502, 1452, 1775, 1719, 1702, 1792, 1824, 1531, 1455, 1952, 1596, 1866, 1901, 2011, 1539,
      1519, 1950, 1957, 1915, 1701, 1897, 1712, 1682, 2093, 2016, 2163, 1800, 2119, 1710, 1914, 2237, 2361, 2124, 2106, 2460,
      2120, 750, 2349, 2041, 2464, 1960, 2484, 2062, 2118, 1605, 2373, 2523, 2648, 1854, 2174, 2338, 2166, 2705, 2709, 1586,
      2928, 2260, 1803, 2914, 2986, 2999, 2714, 3102, 2443, 2614, 3086, 2759, 2883, 2810, 3004, 2554, 2475, 3107, 3067, 3386,
      3164, 3293, 2803, 2889, 2675, 3306, 2214, 1726, 3385, 2722, 2899, 3537, 3552, 3365, 3234, 3524, 2901, 3200, 3653, 3664,
      2866, 3852, 3522, 3242, 2911, 3758, 3906, 3945, 3039, 3881, 3318, 3166, 2979, 3992, 3759, 3742, 3648, 3013, 3451, 3630,
      3987, 3827, 3987, 4113, 3459, 2658, 4154, 3822, 4143, 3971, 4056, 3572, 3605, 3957, 3791, 1931, 4265, 3614, 2077, 3213,
      4130, 4115, 3446, 4219, 3764, 3666, 2748, 4277, 4054, 4027, 3077, 4132, 3420, 3315, 3859, 4302, 4064, 3101, 4380, 3540,
      3655, 3853, 3465, 3830, 4272, 4202, 3209, 3463, 4203, 4049, 4232, 3864, 3822, 3456, 3265, 3833, 4273, 4309, 4074, 3952,
      3285, 3302, 4132, 3640, 4226, 439, 4138, 3180, 3288, 4090, 3578, 3033, 2256, 4076, 3010, 3211, 3728, 3536, 3856, 3657,
      3987, 3240, 2934, 3709, 2688, 3493, 3873, 3441, 2321, 2875, 3506, 3425, 3512, 3456, 3244, 2650, 2800, 2735, 3354, 3511,
      3341, 3359, 1951, 2687, 3485, 3261, 2903, 3091, 3089, 2705, 2634, 2860, 2395, 3144, 3076, 3086, 2525, 2545, 2805, 3090,
      1233, 2780, 2944, 2283, 2285, 2641, 2655, 2589, 2564, 2563, 2132, 2340, 1748, 2459, 2558, 2506, 2320, 2282, 2122, 2570,
      2641, 2281, 2298, 1420, 1905, 1992, 1717, 2273, 2447, 2231, 2305, 1827, 1995, 1919, 2282, 1684, 1574, 1980, 1873, 1883,
      2156, 2028, 1997, 2138, 2101, 1770, 1061, 1846, 2065, 1820, 1873, 1712, 1533, 1502, 1790, 1433, 1853, 1853, 1692, 1578,
      1468, 1694, 1597, 1631, 1579, 1552, 1364, 1319, 1466, 1629, 767, 1574, 1613, 1328, 1272, 1283, 1159, 1658, 1535, 1601,
      1028, 1211, 1493, 1446, 910,
    ];
    const START = Date.UTC(2025, 0, 1); // day 0 of the array, in UTC milliseconds
    const DAY = 86400000;               // milliseconds in a day
    const NOTES = { 225: "App down" };  // index 225 is 14 August: a note to draw next to that day

    // ---- 2. Small helpers from Topics 3.2 to 3.5 ----
    const NS = "http://www.w3.org/2000/svg";
    function el(name, attrs, parent) {
      const node = document.createElementNS(NS, name);
      for (const key in attrs) node.setAttribute(key, attrs[key]);
      if (parent) parent.appendChild(node);
      return node;
    }
    function niceTicks(lo, hi, count) {
      if (!Number.isFinite(lo) || !Number.isFinite(hi)) return [];
      if (lo === hi) { lo -= 1; hi += 1; }
      const rough = (hi - lo) / count;
      const power = Math.pow(10, Math.floor(Math.log10(rough)));
      const f = rough / power;
      const step = (f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10) * power;
      const ticks = [];
      for (let k = Math.floor(lo / step); k <= Math.ceil(hi / step); k++) ticks.push(Number((k * step).toFixed(10)));
      return ticks;
    }
    function dayName(i) { // 225 -> "14 August"
      return new Date(START + i * DAY).toLocaleDateString("en-GB", { day: "numeric", month: "long", timeZone: "UTC" });
    }

    // ---- 3. The chart: drawRidesLine(box, rides) ----
    function drawRidesLine(box, rides) {
      const known = rides.filter((v) => v !== null);
      if (known.length === 0) { // empty state: say so, never a blank box
        el("p", { class: "empty" }, box).textContent = "No rides to show yet.";
        return;
      }
      const W = 720, H = 300;
      const margin = { top: 16, right: 16, bottom: 30, left: 48 };
      const w = W - margin.left - margin.right;
      const h = H - margin.top - margin.bottom;

      const yTicks = niceTicks(0, Math.max(...known), 5);
      const yMax = yTicks[yTicks.length - 1];
      const x = (i) => (i / Math.max(rides.length - 1, 1)) * w; // Math.max: one value must not divide by 0
      const y = (v) => h - (v / yMax) * h;

      const busiest = rides.indexOf(Math.max(...known));
      const quietest = rides.indexOf(Math.min(...known));
      const label = "Line chart of rides per day, " + rides.length + " days from 1 January 2025. Busiest day: " +
        dayName(busiest) + ", " + rides[busiest].toLocaleString("en-US") + " rides. Quietest day: " +
        dayName(quietest) + ", " + rides[quietest].toLocaleString("en-US") + " rides" +
        (NOTES[quietest] ? " (" + NOTES[quietest].toLowerCase() + ")" : "") + ".";
      const svg = el("svg", { viewBox: "0 0 " + W + " " + H, width: "100%", role: "img", "aria-label": label }, box);
      const g = el("g", { transform: "translate(" + margin.left + "," + margin.top + ")", "font-size": 12 }, svg);

      for (const t of yTicks) { // gridlines and y labels
        el("line", { x1: 0, x2: w, y1: y(t), y2: y(t), stroke: "#e3e8ec" }, g);
        el("text", { x: -8, y: y(t) + 4, "text-anchor": "end", fill: "#5b6b78" }, g).textContent = t.toLocaleString("en-US");
      }
      const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
      months.forEach((name, m) => { // one tick at the first day of each month
        const i = (Date.UTC(2025, m, 1) - START) / DAY;
        if (i >= rides.length) return;
        el("line", { x1: x(i), x2: x(i), y1: h, y2: h + 5, stroke: "#9aa8b4" }, g);
        el("text", { x: x(i) + 3, y: h + 20, fill: "#5b6b78" }, g).textContent = name;
      });
      el("line", { x1: 0, x2: w, y1: h, y2: h, stroke: "#9aa8b4" }, g);

      let d = "";
      let penDown = false; // a null lifts the pen, so a missing day leaves a gap
      rides.forEach((v, i) => {
        if (v === null) { penDown = false; return; }
        d += (penDown ? "L" : "M") + x(i).toFixed(1) + "," + y(v).toFixed(1);
        penDown = true;
      });
      el("path", { d: d, fill: "none", stroke: "#264653", "stroke-width": 1.5, "stroke-linejoin": "round" }, g);
      rides.forEach((v, i) => { // a value with no neighbours has no line: give it a dot
        if (v !== null && rides[i - 1] == null && rides[i + 1] == null) el("circle", { cx: x(i), cy: y(v), r: 3, fill: "#264653" }, g);
      });
      if (NOTES[quietest]) { // the note for the quietest day, drawn next to its dot
        el("circle", { cx: x(quietest), cy: y(rides[quietest]), r: 3.5, fill: "#e76f51" }, g);
        el("text", { x: x(quietest) - 8, y: y(rides[quietest]) + 4, "text-anchor": "end", fill: "#e76f51" }, g).textContent =
          dayName(quietest) + ": " + NOTES[quietest].toLowerCase() + ", " + rides[quietest] + " rides";
      }
    }

    drawRidesLine(document.querySelector("#rides-line"), RIDES_2025);

    // A quick self-check
    const total = RIDES_2025.reduce((sum, v) => sum + v, 0);
    console.log("Days:", RIDES_2025.length, "| total rides:", total.toLocaleString("en-US"));
    console.log("Week bars:", document.querySelectorAll("#this-week rect").length);
    console.log("Line segments:", document.querySelector("#rides-line path").getAttribute("d").split("L").length - 1);
    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

Use 0 for a missing day

Change the 100th number in RIDES_2025 to 0 instead of null.

terminal
$ Console and preview
── what you'll see ──
No error. Console: `Line segments: 364`. The preview shows a thin spike down to the baseline at one day, as if the bike share lost every customer for a day.

Myth vs fact

Myth

A hand-made chart is a toy; real projects always use a library.

Fact

The library does the same steps. Knowing them tells you what to check when it fails, and when a small hand-made chart is all you need.

Myth

Embedding 365 numbers in a page is bad practice.

Fact

It is fine for a first version and for pages opened without a server. From Phase 4 the data is loaded from a file.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The path text for 365 points is about 4 KB and cheap to draw. The cost is per update: rebuilding the whole SVG each time. The data join (Phase 5) updates only what changed.

  • ▸

    The aria-label is built from the data, so it stays true if the data changes. Phase 15 adds a keyboard way through the points and a data table.

Remember this

  1. 1

    The problem this solves: Arjun wanted the team to understand what a chart library does before they trust one. You now have every piece: margins and scales (Topic 3.2), a line path with gaps (3.4) and nice ticks (3.5). The build puts them together in the dashboard's #rides-line box, and Arjun's request is met.

  2. 2

    Starting point. Your index.html after Phase 2 has the bike header, the scalable week chart in #this-week, the "Stations running low" bars, the chart plan, and a #rides-line box that still says "coming soon". This build fills that box and makes it full width (.wide).

  3. 3

    The data is embedded. A page opened by double-click cannot fetch a CSV file (Phase 4 explains why and fixes it). So the 365 daily ride counts are written into the file as one array, RIDES_2025, copied from the rides column of daily.csv. The other choice, 12 monthly totals, would be shorter but would hide the weekly wiggles and the 14 August drop. A missing day would be null, never 0.

  4. 4

    **What drawRidesLine(box, rides) does.** It shows a message if there is no data. Otherwise it picks nice y ticks, draws gridlines and labels, month ticks at the first day of each month, one path (the pen lifts at a null), a dot for any lone value, and an orange note on the quietest day (14 August, 439 rides: the app outage). The <svg> has a viewBox, so it scales to the box, and its aria-label states the finding: busiest and quietest days.

  5. 5

    What the dashboard can do now that it could not before: it draws a year of real numbers with readable axes, from data, with code that survives empty and missing data. That answers Tomás's question: a July peak (4,380 rides on 18 July), a quiet winter, and one bad day.

  6. 6

    What hurt, and what a library does for us. We wished for: nice ticks and axes in one line (D3 scales and axes, Phase 6); dates as real dates with month ticks and time zones (Phases 4 and 6); redrawing when the data changes without rebuilding everything (the data join, Phase 5); curves and gaps through one function (Phase 7); tooltips and hover (Phase 8); resizing and themes (Phase 10). Still missing too: fetching the data (Phase 4) and the stations chart (Phase 5).

Explain it without notes

01

Which pieces from Topics 3.2 to 3.5 does drawRidesLine use?

02

Why is the data embedded in this phase?

03

Name three things you wished a library did for you.

Practice

01

Add the busiest day (18 July, 4,380 rides) as a second note, in the same style as the outage.

02

What makes an all-null array show the empty message?

Trade-offs

  • ↔

    A hand-made chart costs code and care but has no dependency and no surprises. A library saves time on axes, updates and interaction, and adds a dependency and its own rules.

Done when you can

  • My index.html shows the week bars and the year line, and the Console says Days: 365.

  • I can say what each part of drawRidesLine does and which topic taught it.

  • I can show what happens for an empty array and for a missing day.

  • I can list what I wished a library did, and which phase teaches it.