Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~22 min· topic 1 of 7

Topic 7.1

Lines, Areas and Curves

In one line

d3.line and d3.area are functions that turn data into the d text of an SVG path. You choose how a point becomes x and y, which points exist at all, and how the points are joined: straight, stepped or smoothed.

Think of it like this

Connect-the-dots in a puzzle book. You get numbered dots and a pencil. You join dot 1 to dot 2 to dot 3. If a dot is smudged, you lift the pencil and start again at the next clear one. A line generator is that pencil: it receives dots, writes the moves, and lifts when told a dot is missing.

Words you'll meet

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

generator
A D3 function that you configure and then call with data. d3.line() and d3.area() are generators.
accessor
A small function that says how to read one value from a data item, such as (d) => x(d.date).
defined
The accessor that says whether a data item exists. A false answer lifts the pen.
curve
The rule that decides how the line travels between two points: straight, stepped or smooth.
baseline
The edge of an area that does not move with the data, usually the value 0.

Step by step

01Print the string a line builder writes

The first example calls d3.line() on small arrays and prints the result. Three points give M0,10L20,40L40,25. That is the whole output: nothing is drawn until you set it on a path.

Three edge cases are worth knowing. An empty array gives null, not an empty string, so .attr("d", null) removes the attribute. One point gives M5,5Z, a path of length zero that shows nothing. A NaN value ("not a number": what a calculation gives when there is no sensible number) is written as the word NaN, and the browser rejects it.

02Cut a gap with defined

With defined((d) => !Number.isNaN(d[1])) the second point is skipped. The path becomes M0,10ZM40,25L60,5L80,9: two pieces in one element. The lonely first point draws nothing as a line, so you add a circle for it. A piece made of one point needs a marker (or a round line cap, stroke-linecap="round") to be seen at all.

03Draw the year, with the outage as a gap

The second example draws all 365 days. On 14 August the app was down and the count (439) is not a real count, so defined hides days under 600 rides. The path has two pieces and 4,460 characters. A band behind it joins the quietest and the busiest day of each of the 53 weeks.

One path or twelve? One <path> per month would be twelve elements, and any property you change (stroke, opacity) must be changed twelve times. One path with a gap is better when it is one story.

04Smooth with care

The third example draws the same nine days, 10 to 18 August, with six curves. The lowest day, 439, is the outage. curveBasis never reaches it: its lowest point is 1,687, so the dip looks mild. curveNatural rises to 4,536 although the busiest day was 4,226, a value that never happened. curveMonotoneX stays inside the data: 439 and 4,226.

Try it yourself

  1. 1

    Let a missing value through

    In the first example delete .defined(...) and run. The path becomes M0,10L20,NaNL40,25L60,5L80,9, the console prints Error: <path> attribute d: Expected number, "M0,10L20,NaNL40,25L60,5L8…". and the browser draws no line at all, not even between the good points.

  2. 2

    Draw the real outage

    In the second example change OUTAGE to 400. No day is hidden any more: the console prints pieces in the line path: 1 and undefined days: [], and the line plunges to 439 on 14 August. The data did not change, only the decision about it.

Code & diagrams

What d3.line writes: strings, edge cases and a gap New tab

A missing value is not a gap. Only `defined` makes one: NaN draws a broken path, and null quietly becomes 0. The picture shows the lonely first point (a piece of one point draws nothing, so circles mark every point) and the joined rest.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

three points     M0,10L20,40L40,25
empty array      null
one point        M5,5Z
NaN in the data  M0,10L20,NaNL40,25
null becomes 0   M0,10L20,0L40,25
default digits   M0.123,10L20.988,40
digits(1)        M0.1,10L21,40
defined() gap    M0,10ZM40,25L60,5L80,9
pieces in one path: 2
The year as a line with a gap, and a weekly band New tab

Treating the outage day as undefined is a decision, not a default: say so on the chart (a note under it) so nobody thinks the app had no data that day.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

days: 365 weeks: 53
pieces in the line path: 2
characters in the path: 4460
undefined days: [ '2025-08-14 439' ]
one path: 1 element; per month: 12 elements
Six curves through the same nine days New tab

Red dots are the data. `basis` is a smoothing curve: it is pulled towards the points and never touches them, which hides the outage almost completely.

Console

(nothing was printed)

Expected console output

rides: 3302 4132 3640 4226 439 4138 3180 3288 4090
linear     lowest 439 highest 4226
monotoneX  lowest 439 highest 4226
natural    lowest 439 highest 4536
basis      lowest 1687 highest 4090
cardinal   lowest 439 highest 4372
step       lowest 439 highest 4226
the data:  lowest 439 highest 4226

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 null for a missing day

In the first example, change the data to [[0, 10], [20, null], [40, 25]] and use d3.line() without defined.

terminal
$ Console
── what you'll see ──
null becomes 0 M0,10L20,0L40,25

Myth vs fact

Myth

A smoother line is a nicer, more accurate line.

Fact

Smoothing invents values between the points. In the third example natural shows 4,536 rides when the maximum was 4,226, and basis hides most of an outage. Use monotoneX when you must smooth, or straight lines.

Myth

d3.line draws the line on the page.

Fact

It returns text. You set the text on a <path> yourself. The text is the same one a hand-written path would hold (Topic 1.3).

Pro corner

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

  • ▸

    d3.line uses defined before it calls x and y, and the curve receives lineStart, point and lineEnd calls. In d3-shape/src/line.js the loop runs n + 1 times so that a final lineEnd is always sent. That is why a gap at the very end of the data still closes the last piece.

  • ▸

    A path with 100,000 points is one DOM node but a heavy one: the browser has to rebuild the whole shape whenever it changes. For many thousands of points, keep one point per pixel column (downsample) or move to Canvas (Phase 11).

Remember this

  1. 1

    The problem this solves: in Phase 3 you built the d string by joining text yourself. That works for one chart and breaks on the first empty array, missing day or smooth curve. d3.line writes the string for you and handles those cases the same way every time.

  2. 2

    A generator is a function that returns a function. d3.line() gives you a configured builder. .x(...) and .y(...) say how one data item becomes a position, .defined(...) says whether the item exists, .curve(...) says how to join. Call the builder with your array and it returns text such as M0,10L20,40L40,25: M moves the pen, each L draws a straight segment (Topic 1.3).

  3. 3

    **defined() makes gaps.** For each item, defined returns true or false. A false item ends the current piece of line, and the next defined item starts a new M. The result is still one <path>, with several pieces inside it. Without defined, a missing value is drawn as NaN or as zero (see the first example).

  4. 4

    **d3.area is a line with a second edge.** It has x, y0 (the bottom edge) and y1 (the top edge). Set y0 to a constant such as the axis baseline and you get the filled area under a line. Set y0 to another value per point and you get a band, such as each week's quietest and busiest day.

  5. 5

    A curve is a rule for the points between the dots. curveLinear joins them with straight lines. curveStep makes right-angled steps. curveMonotoneX smooths without ever going above or below the neighbouring points. curveNatural, curveCardinal and curveBasis are smoother, and they can lie: they can overshoot the data, or never touch it. The third example measures that on real rides.

Explain it without notes

01

What does d3.line()(data) return, and what do you do with it?

02

Why is a missing value that you set to null dangerous in a line chart, and what is the safe way to leave a gap?

03

How does d3.area differ from d3.line?

Practice

01

Write gapLine(rows, x, y) that draws rides as a line but leaves a gap for every row where rides is null or not a finite number. Test it on [{date: 1, rides: 5}, {date: 2, rides: null}, {date: 3, rides: 7}].

02

Make an area under the daily line: use d3.area() with y0 set to the bottom of the plot (y(0)) and y1 from the data.

Trade-offs

  • ↔

    Straight lines show exactly what you measured. Smooth curves look friendlier and can lie. For measurements, choose straight or monotoneX; for a model's fitted line, a smoother curve is fine if labelled.

Done when you can

  • I can print the string d3.line writes for a few points.

  • I can make a gap with defined and explain why null does not.

  • I can draw an area under a line and a band between two series.

  • I can say which curves stay inside the data and which can overshoot.