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
Pass a radius by mistake
Add
console.log(d3.symbol(d3.symbolCircle, 14)()). It prints a circle of radius 2.111, not 14. Pass14 * 14 * Math.PIto get radius 14. - 2
Change the minimum gap
In the second example change
spread(ends, 13)tospread(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
The first four lines are the strings `d3.symbol` writes. The legend rows use the same `shape` and `color` scales as the marks.
Preview
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, 30Westpark'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
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.7pxBreak 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)()).
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.symbolsFillandd3.symbolsStrokeare lists of types that look good filled and good as outlines. Both have seven entries, andd3.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. Aftercolor("Harbor")(a misspelling) the six-district scale has seven values, and a legend built fromcolor.domain()grows a seventh swatch. That is why the stacked chart in Topic 7.7 filters rows byKEYSbefore it draws. - ▸
Real label placement is a hard problem (it can be solved with force simulations, Topic 13.5). The one-pass
spreadabove only moves labels down, so it can push the last label below the plot: check the bottom edge in production code.
Remember this
- 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
**
d3.symboldraws a marker.**d3.symbol(type, size)()returns thedtext of a small shape.typeis one of the built-in shapes, such asd3.symbolCircle,symbolSquare,symbolTriangle,symbolDiamond,symbolCross,symbolStarorsymbolWye(D3 7 has a few more, such assymbolPlusandsymbolTimes).sizeis 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 withtransform=translate(x, y). - 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
Size should follow area. The first example turns docks into a radius with
d3.scaleSqrtand 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
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
spreadfunction 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
What is the size argument of d3.symbol?
Why build a legend from the same scales as the chart?
What is a direct label and when does it stop working?
Practice
Make sizeLegend(values, sizeFn) that returns the d text for each value, using d3.symbolCircle.
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.symboland 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.