Command Palette

Search for a command to run...

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

Topic 7.6

Legends, Symbols and Direct Labels

In one line

A chart must explain its own colours, shapes and sizes. d3.symbol draws marker shapes, legends are small charts built with the same scales, and direct labels put the names on the lines themselves so the reader needs no lookup.

Think of it like this

A map in a theme park. A legend at the corner says "red flag = toilets". You look at a flag, look away to the corner, then back. Now picture the park putting the word "Toilets" on the flag itself. That is a direct label: no trip to the corner.

Words you'll meet

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

legend
A key that says what each colour, shape or size means.
symbol
A small marker shape, such as a circle, a square or a star, drawn at a point.
direct label
A name written next to the thing it names, so no legend is needed.
leader line
A short line that joins a label to the point it belongs to, when the label had to be moved.
collision
Two labels that would be drawn on top of each other.

Step by step

01Print what d3.symbol writes

A circle of size 64 is M4.514,0A4.514,4.514,0,1,1,-4.514,0A4.514,4.514,0,1,1,4.514,0: two half-circle arcs. A square is M-4,-4h8v8h-8Z: 8 wide, area 64. A triangle is three points. Size 0 gives M0,0: nothing is drawn, and no error is shown.

02Draw 25 stations with shape and size

The first example plots each station at its longitude and latitude. The district is the shape and the colour, and the number of docks (12 to 30) is the area. Each district gathers in its own place: Harbour (circles) low right, Westpark (stars) lower left, University (triangles) upper left.

03Build the legends from the scales

The colour legend loops over the districts and draws each symbol with the same generator. The size legend draws circles for 12, 20 and 30 docks with the same size function as the marks. If you change the radius range in one place, both change.

04Put the names on the lines

The second example draws the six district lines and writes each name after its December point. The line ends are 1.3 pixels apart at the closest, so the labels would overlap. spread moves four of the labels down by 9 to 21 pixels, and a leader line joins each label to its line.

Try it yourself

  1. 1

    Pass a radius by mistake

    Add console.log(d3.symbol(d3.symbolCircle, 14)()). It prints a circle of radius 2.111, not 14. Pass 14 * 14 * Math.PI to get radius 14.

  2. 2

    Change the minimum gap

    In the second example change spread(ends, 13) to spread(ends, 20). The labels are now more spaced and Hillcrest and Westpark are further from their lines, so leader lines get longer and slanted.

Code & diagrams

Shape for the district, size for the docks, and two legends New tab

The first four lines are the strings `d3.symbol` writes. The legend rows use the same `shape` and `color` scales as the marks.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

circle   M4.514,0A4.514,4.514,0,1,1,-4.514,0A4.514,4.514,0,1,1,4.514,0
square   M-4,-4h8v8h-8Z
triangle M0,-7.019L6.079,3.51L-6.079,3.51Z
size 0   M0,0
districts: 6 | stations: 25 | docks range: 12 to 30
size legend values: 12, 20, 30
Direct labels that move apart, with leader lines New tab

Westpark's line starts in May because `defined` skips its zero months. Remove the `spread` call (use `ends` instead of `placed`) to see the labels overlap. Look at Westpark's name: spreading pushes it below the axis line, the problem the last Pro note warns about.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

smallest gap between line ends: 1.3 px | labels need 13
after spreading: 13.0 px
labels moved: Old Town by 9.1px, Riverside by 9.5px, Hillcrest by 21.2px, Westpark by 11.7px

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

Draw a symbol with a negative size

In the first example, run svg.append("path").attr("d", d3.symbol(d3.symbolCircle, -5)()).

terminal
$ Console
── what you'll see ──
Error: <path> attribute d: Expected number, "MNaN,0".

Myth vs fact

Myth

Every chart needs a legend box in a corner.

Fact

A legend makes the reader look away from the data. If the lines can carry their names, put the names there and drop the box.

Myth

A symbol's size argument is its radius.

Fact

It is the area in square pixels. Size 14 draws a circle of radius 2.1, and the tiny dot looks like a bug.

Pro corner

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

  • ▸

    d3.symbolsFill and d3.symbolsStroke are lists of types that look good filled and good as outlines. Both have seven entries, and d3.scaleOrdinal(domain, d3.symbolsFill) is the usual way to get a shape per category.

  • ▸

    An ordinal scale without .unknown(...) quietly adds every new name to its domain the first time it is asked for it. After color("Harbor") (a misspelling) the six-district scale has seven values, and a legend built from color.domain() grows a seventh swatch. That is why the stacked chart in Topic 7.7 filters rows by KEYS before it draws.

  • ▸

    Real label placement is a hard problem (it can be solved with force simulations, Topic 13.5). The one-pass spread above only moves labels down, so it can push the last label below the plot: check the bottom edge in production code.

Remember this

  1. 1

    The problem this solves: the district chart from Topic 7.2 has six colours and the reader must match each to a key. In the lines chart of this topic, the name of each district sits at the end of its line, and two lines that end close together would put two names on top of each other.

  2. 2

    **d3.symbol draws a marker.** d3.symbol(type, size)() returns the d text of a small shape. type is one of the built-in shapes, such as d3.symbolCircle, symbolSquare, symbolTriangle, symbolDiamond, symbolCross, symbolStar or symbolWye (D3 7 has a few more, such as symbolPlus and symbolTimes). size is the area in square pixels, not a radius: size 64 gives a circle of radius 4.514. The shape is centred on (0, 0), so move it with transform=translate(x, y).

  3. 3

    A legend is a tiny chart made from the same scales. For colour or shape, draw one mark for every value in the scale's domain, with its name next to it. For size, draw three marks at typical values (12, 20 and 30 docks), using the same size function as the chart. Never type colours into the legend by hand: when the scale changes, the legend must change with it.

  4. 4

    Size should follow area. The first example turns docks into a radius with d3.scaleSqrt and then into an area (π r²), so a station with 30 docks has about 2.5 times the area of one with 12. If you map the number to the radius itself, the area grows with the square and the biggest station looks far bigger than it is.

  5. 5

    Direct labels beat legends for lines. The second example puts each district's name at the right end of its line. The six line ends are only 1.3 pixels apart at the closest, but a label needs about 13, so a small spread function pushes labels down until each has 13 pixels, and draws a short leader line from the label to the true end. With many lines the labels still collide: then use a legend, or highlight one line and grey the rest.

Explain it without notes

01

What is the size argument of d3.symbol?

02

Why build a legend from the same scales as the chart?

03

What is a direct label and when does it stop working?

Practice

01

Make sizeLegend(values, sizeFn) that returns the d text for each value, using d3.symbolCircle.

02

Give every station symbol in the first example a hover title with the station name, its district and its docks.

Trade-offs

  • ↔

    A legend works for any number of categories and costs the reader a glance away. Direct labels save the glance and stop working when marks are crowded.

Done when you can

  • I can draw a marker with d3.symbol and say that the size is an area.

  • I can build a colour, shape and size legend from the chart's own scales.

  • I can put direct labels at line ends and spread them to avoid collisions.

  • I know when to use a legend instead.