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
Remove a day
Change one number in
RIDES_2025(for example the 100th) tonull. The line breaks at that day and the Console saysLine segments: 362: the pen lifts at the gap, so oneLbecomesMand the gap removes another. The total also drops, to 932,065, because the quick self-check adds anullas 0 (sum + null). - 2
Draw an empty chart
Change the last call to
drawRidesLine(document.querySelector("#rides-line"), []). The box showsNo rides to show yet.The last Console line becomesUncaught 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
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
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: 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; }
.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.
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-labelis 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
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-linebox, and Arjun's request is met. - 2
Starting point. Your
index.htmlafter Phase 2 has the bike header, the scalable week chart in#this-week, the "Stations running low" bars, the chart plan, and a#rides-linebox that still says "coming soon". This build fills that box and makes it full width (.wide). - 3
The data is embedded. A page opened by double-click cannot
fetcha 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 theridescolumn ofdaily.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 benull, never 0. - 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 anull), a dot for any lone value, and an orange note on the quietest day (14 August, 439 rides: the app outage). The<svg>has aviewBox, so it scales to the box, and itsaria-labelstates the finding: busiest and quietest days. - 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
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
Which pieces from Topics 3.2 to 3.5 does drawRidesLine use?
Why is the data embedded in this phase?
Name three things you wished a library did for you.
Practice
Add the busiest day (18 July, 4,380 rides) as a second note, in the same style as the outage.
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.