Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~33 min· topic 4 of 6

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 example M0,10L50,20.
M and L
Path commands. M x,y moves the pen without drawing. L x,y draws a straight line to that point.
stroke and fill
stroke is the colour of the outline or line; fill is the colour of the inside. A line chart wants fill="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 M instead of L, 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.

points to a pathwhole filejs
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).

Decide what a gap looks likediagram
Rendering diagram…

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

    Start the y axis at zero

    In the first example, change scale(2500, 4000, h, 0) to scale(0, 4000, h, 0). The path becomes M0.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. 2

    Join across the gap

    In the second example, change show("broken", rides, false); to show("broken", rides, true);. Both of the first two charts now log M10,44L57,10L103,18L243,43L290,28: with the pen kept down, nothing shows that Thursday and Friday are missing.

  3. 3

    Draw only January

    In the third example, change rows.map( in the line that creates data to rows.slice(0, 30).map(. The Console says Points: 30 | segments: 29, the path text is 317 characters, and the lowest day becomes 2025-01-12 with 1001 rides.

Code & diagrams

Points to a path: one week as a line New tab

The line is one element. The dots are only there to show where the data points are.

Preview

Press Run to see the page.

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.2
Gaps and a single point: break the line, or join across New tab

The 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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

broken  M10,44L57,10L103,18M243,43L290,28
joined  M10,44L57,10L103,18L243,43L290,28
single  M10,37
All 365 days of 2025 as one path New tab

The 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

Press Run to see the page.

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 190

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

Forget fill="none"

In the first example, delete the line path.setAttribute("fill", "none");.

terminal
$ Preview
── what you'll see ──
No error and the same path text in the Console. In the preview, a black shape fills the area between the line and a straight line from the last point back to the first. The line itself is still there, as a thin dark-blue outline.

Break #2

Let one value be undefined

In the first example, change { t: day(5), rides: 3742 } to { t: day(5), rides: undefined }.

terminal
$ Console
── what you'll see ──
M0.0,115.7L70.0,0.9L140.0,27.3L210.0,NaNL280.0,39.9L350.0,111.9L420.0,62.2
Error: <path> attribute d: Expected number, "M0.0,115.7L70.0,0.9L140.0,27.3L210.0,NaNL280.0,39.9L3…".
Error: <circle> attribute cy: Expected length, "NaN".

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 attribute vector-effect="non-scaling-stroke" keeps the stroke width in screen pixels. Phase 10 returns to this.

  • ▸

    Date.parse("2025-06-01") is UTC, but Date.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. 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. 2

    Points become one path string. For each point you work out x and y with your scales, then write M x,y for the first point and L x,y for every other one. Joined together, the week in the first example is M0.0,115.7L70.0,0.9L140.0,27.3…. You put that string in the d attribute of one <path>. A path is filled by default, so a line needs fill="none" and a stroke colour, or the browser paints the area inside the line.

  3. 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. 4

    A gap is a decision. If a value is missing (null), you can lift the pen (start a new M after 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. 5

    One point has no line. A path with only M x,y has 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 remember x = i ÷ (count − 1) divides by zero when count is 1: use Math.max(count − 1, 1).

  6. 6

    365 points are one element. The year in the third example is a single <path> whose d text 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

01

How do M and L turn a list of points into a line? What does the path text look like for three points?

02

Why does a line chart's path need fill="none"?

03

What are two ways to draw a series with missing values, and which one is honest?

04

Why is a path with a single M invisible, and what do you draw instead?

Practice

01

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?

02

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?

03

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 d text with M and L.

  • 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 NaN in a path from the error message.