Topic 3.4
A Line Chart from Points
In one line
A line chart is one SVG path: a string of commands that moves a pen to the first point and draws to every next one. You build that string from your data, put time on the x axis, and decide what to do with gaps, a single point and 365 values.
Think of it like this
A connect-the-dots puzzle. The dots are numbered, you put the pencil on dot 1 and draw a line to dot 2, then dot 3, and so on. In an SVG path, M puts the pencil down and L draws a line to the next dot. A line chart is a connect-the-dots puzzle whose dots come from your data.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- path
- An SVG shape drawn by a string of commands (
d), such as "move here, then draw a line there". - d attribute
- The text on a
<path>that holds its commands, for exampleM0,10L50,20. - M and L
- Path commands.
M x,ymoves the pen without drawing.L x,ydraws a straight line to that point. - stroke and fill
strokeis the colour of the outline or line;fillis the colour of the inside. A line chart wantsfill="none".- timestamp
- A date as a number: the milliseconds since 1 January 1970 UTC. It lets a date go through a scale like any other number.
- UTC
- The world's reference time, with no time zone and no summer time. Using it keeps every day exactly 24 hours long (Topic 4.3 deals with local time).
- gap
- A place in the data where values are missing, such as two days when a counter failed.
- pen up
- In a path, starting a new
Minstead ofL, so that the line does not join across a gap.
Step by step
01Start with dates and numbers
The first example uses the week of 2 to 8 June 2025 again. Each point is { t, rides }: t is the day as a UTC timestamp from Date.UTC(2025, 5, d) (month 5 is June), and rides is the count.
02Build two scales
The x scale maps the first timestamp to 0 and the last to the plot width. The y scale maps 2,500 to the bottom and 4,000 to the top. The code uses one tiny helper, scale(d0, d1, r0, r1), the same m·x + b from Topic 3.2 written as a function that returns a function.
03Write the path text
points.map(...) turns each point into M x,y (first) or L x,y, rounded to one decimal, and .join("") joins them. The Console prints the result: M0.0,115.7L70.0,0.9L140.0,27.3L210.0,29.2L280.0,39.9L350.0,111.9L420.0,62.2. Seven points, seven commands.
const d = points
.map((p, i) => (i === 0 ? "M" : "L") + p[0].toFixed(1) + "," + p[1].toFixed(1))
.join("");04Switch the fill off
The path gets fill="none", stroke="#264653" and stroke-width="2". Small circles mark the real data points, so you can see where the line bends. Delete the fill line (Break it) and the browser closes the shape and paints it black.
05Decide what a gap looks like
The second example draws the same week with Thursday and Friday missing, three times: the pen lifts at the gap (M10,44L57,10L103,18M243,43L290,28), the line joins across it (M10,44L57,10L103,18L243,43L290,28), and a series with one point (M10,37, which draws nothing, so the dot is the only mark).
06Draw the whole year
The third example fetches daily.csv, parses 365 rows, turns each date into a timestamp, and writes one path with 364 L commands. The log shows the path is 4,067 characters long. The lowest day is 2025-08-14 with 439 rides, so it ends 171 of the 190 pixels down.
07Handle the empty case
If the data is empty, the code writes No data to draw. under the chart and stops. Without the check, data[0].t throws a TypeError (there is no first row) and the box stays blank.
Try it yourself
- 1
Start the y axis at zero
In the first example, change
scale(2500, 4000, h, 0)toscale(0, 4000, h, 0). The path becomesM0.0,43.4L70.0,0.3L140.0,10.2L210.0,11.0L280.0,15.0L350.0,41.9L420.0,23.3: the same shape, but squeezed into the top third of the chart. A line chart can start above zero to show the changes; the axis has to say so. - 2
Join across the gap
In the second example, change
show("broken", rides, false);toshow("broken", rides, true);. Both of the first two charts now logM10,44L57,10L103,18L243,43L290,28: with the pen kept down, nothing shows that Thursday and Friday are missing. - 3
Draw only January
In the third example, change
rows.map(in the line that createsdatatorows.slice(0, 30).map(. The Console saysPoints: 30 | segments: 29, the path text is 317 characters, and the lowest day becomes 2025-01-12 with 1001 rides.
Code & diagrams
The line is one element. The dots are only there to show where the data points are.
Preview
Console
Nothing logged yet.Expected console output
M0.0,115.7L70.0,0.9L140.0,27.3L210.0,29.2L280.0,39.9L350.0,111.9L420.0,62.2The gap is pretend (the real data has no missing days): a way to see both choices side by side. The one-point path M10,37 draws nothing; only its dot is visible, at the left edge.
Preview
Console
Nothing logged yet.Expected console output
broken M10,44L57,10L103,18M243,43L290,28
joined M10,44L57,10L103,18L243,43L290,28
single M10,37The code is inside an async function because this example has no import, so it is a classic script. One path draws all 365 days; the sharpest spike down is 14 August.
Preview
Console
Nothing logged yet.Expected console output
Points: 365 | segments: 364
Path text: 4067 characters, starts M0.0,138.2L1.5,121.3L3.0,120...
Lowest day: 2025-08-14 439 -> y = 171.0 of 190Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget fill="none"
In the first example, delete the line path.setAttribute("fill", "none");.
Break #2
Let one value be undefined
In the first example, change { t: day(5), rides: 3742 } to { t: day(5), rides: undefined }.
Myth vs fact
Myth
A line chart needs one SVG element for every segment.
Fact
One <path> draws the whole line. The year in the third example is one element with 364 line commands in about 4 KB of text.
Myth
The line should always connect all the points, so the chart looks complete.
Fact
A line across a gap draws numbers that were never measured. Showing a gap is the honest choice when data is missing, and the reader should see that it is missing.
Myth
Dates must be strings on the x axis.
Fact
A date is a number (milliseconds) for the maths. It only turns into text, such as Jun, when you label an axis (Topic 3.5, Topic 6.3).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
SVG renders a path up to the first syntax error and stops. That is why the broken line in Break it ends after Wednesday instead of vanishing. It is also why a single bad point near the end of a long dataset costs you only the tail, while one near the start costs you the whole line.
- ▸
When the chart is scaled with
viewBox(Topic 1.4), the stroke scales too: a 1.5-pixel line on a 720-wide viewBox shown in a 360-pixel box is 0.75 pixels wide. The attributevector-effect="non-scaling-stroke"keeps the stroke width in screen pixels. Phase 10 returns to this. - ▸
Date.parse("2025-06-01")is UTC, butDate.parse("2025-06-01T00:00")(a date and time without a zone) is read in the viewer's local time zone, by the language standard. A column of dates that is parsed one way on one machine and another way on a second machine produces lines that shift by a day. Topic 4.3 builds on this. - ▸
A path connects points in the order you give them, and nothing sorts them for you. If the rows arrive newest first, or two rows share a date, the line doubles back on itself or draws a vertical jump. Sort a copy by time first (
[...rows].sort((a, b) => a.t - b.t)) and decide what a repeated date means (sum it, average it or keep the last) before you draw. A series whose values are all equal draws a flat line only if the y scale survives a zero-width domain: guard it as in Topic 3.2.
Remember this
- 1
The problem this solves: a bar chart compares amounts, but 365 bars hide the shape of a year. A line follows change over time: a rise, a fall, a weekly wiggle, a sudden drop. You need to turn a list of (date, value) points into one string of drawing commands that SVG understands.
- 2
Points become one path string. For each point you work out
xandywith your scales, then writeM x,yfor the first point andL x,yfor every other one. Joined together, the week in the first example isM0.0,115.7L70.0,0.9L140.0,27.3…. You put that string in thedattribute of one<path>. A path is filled by default, so a line needsfill="none"and astrokecolour, or the browser paints the area inside the line. - 3
Time is a number on x. A date becomes a timestamp: the number of milliseconds since 1 January 1970 UTC.
Date.UTC(2025, 5, 2)is 2 June 2025 at midnight UTC (months count from 0, so 5 is June).Date.parse("2025-06-01")also reads a date-only text as midnight UTC. Then x is a linear scale on timestamps, exactly like y. Using the row number as x is only right when the days are evenly spaced; with a missing day, spacing by timestamp stays correct. - 4
A gap is a decision. If a value is missing (
null), you can lift the pen (start a newMafter the gap, so the line breaks) or keep drawing across it (the line joins the two sides). Joining is what a careless chart does by default: it draws a confident straight line through days nobody counted. Breaking the line tells the truth. The second example shows both. - 5
One point has no line. A path with only
M x,yhas no length, so it draws nothing at all. If a series can have a single point (a new station's first day), draw a small circle for every point that has no neighbour. Also rememberx = i ÷ (count − 1)divides by zero whencountis 1: useMath.max(count − 1, 1). - 6
365 points are one element. The year in the third example is a single
<path>whosedtext is 4,067 characters long. That is cheap for the browser, unlike 365 separate circles. A line also does not need to start at zero (Topic 2.5), but then the axis must show where it starts.<polyline points="…">is a shorter way to write a plain line, but it cannot lift the pen or draw curves, which is why charts use<path>.
Explain it without notes
How do M and L turn a list of points into a line? What does the path text look like for three points?
Why does a line chart's path need fill="none"?
What are two ways to draw a series with missing values, and which one is honest?
Why is a path with a single M invisible, and what do you draw instead?
Practice
Write linePath(points) for an array of [x, y] pairs. What does it return for [[0, 10], [50, 20], [100, 5]], and for an empty array?
Turn the line into an area: what do you add to the path text so that it closes down to the baseline at y = 160, and which attribute do you change?
The rows come with date strings like "2025-06-02". Write the one line that turns them into timestamps, and say what you must not use if you want every day to be 24 hours long.
Trade-offs
- ↔
Using the row index as x is simpler, but only correct when the data is evenly spaced. Using timestamps costs one subtraction and stays correct when days are missing.
- ↔
A broken line is honest but looks fragmented when many values are missing. In that case, show the points as dots with a light line, or say in the title how many days are missing.
Done when you can
I can turn an array of points into the
dtext withMandL.I can switch off the fill for a line and set a stroke.
I can put time on x as a timestamp and scale it like a number.
I can draw a gap by lifting the pen, and a lone point as a circle.
I can find a
NaNin a path from the error message.