Labels, Titles and Legends

The text that turns a plot into something someone else can read.

Overview

The minimum

A chart without labels is a picture. A chart with labels is information. The difference is whether a reader who was not in the room when you made it can understand what it shows.

Labels and legends are the text layer of a figure. They name the axes, identify the series, and state the units. Without them, the reader has to guess what the numbers mean, which scale applies to which line, and whether the y axis is dollars or percentages.

matplotlib gives you full control over every piece of text on a figure. Titles, axis labels, tick labels, legend entries, annotations and arbitrary text can all be placed, styled and positioned independently. That control is useful for publication-quality figures. For everyday work, a small set of defaults covers most needs.

This page starts with the minimum: a title, axis labels with units, and a legend. It then covers font sizing, placement, multiple legends, legend outside the axes, tick formatting, secondary labels and the cases where a legend is the wrong approach.

The most common omission is units. An axis labelled "Revenue" could mean rupees, dollars, thousands, millions or a percentage change. Adding the unit takes a few characters and removes all doubt. Get in the habit of including units on every axis.

Legend placement is worth thinking about. The default puts the legend inside the axes, which can cover data. Moving it to the edge or outside the axes keeps the data visible. On a line chart with many series, direct labels on the lines often work better than a legend at all. The eye stays on the data instead of jumping to a separate box.

Tick labels deserve attention too. Large numbers like 1,000,000 are hard to scan. Dividing by a thousand and labelling the axis "Revenue (thousands)" or using a formatter to add commas makes the chart easier to read.

each chart that leaves your screen needs a title, an x label and a y label. A legend if there's more than one series.

The axis labels carry the part people most often omit: units. "Sales" could be rupees, units, thousands or a percentage change. A reader who has to guess can't act on the chart. "Sales (₹. Thousands)" takes four extra characters and drops the question.

ax.set(title=..., xlabel=..., ylabel=...) does all three in one call. This Is convenient in a chain.

Worth knowing

Title, both axis labels and a legend are the minimum; units in the axis label are what make a number actionable.
legend() reads labels from the artists — without label= it warns and shows nothing. A leading underscore excludes an artist deliberately.
loc places a legend inside; bbox_to_anchor puts it outside. loc="best" is slow on busy plots and can move between runs.
Direct labels on the lines remove the colour-matching step a legend forces on the reader.
A title that states the finding is more useful than one that names the axes again; loc="left" makes it read as a headline.
fig.suptitle, supxlabel and supylabel label a whole grid, which beats repeating the same label on every panel.

Labels, Titles and Legends

The text that turns a plot into a thing someone else can read.

The four pieces of text

Title, both axis labels, and a legend when there is more than one series.

example_01.pymatplotlib
Output

legend needs labels to work with

It reads them from the artists; without them it has nothing to show.

example_02.pymatplotlib
Output

Placing it

loc for inside, bbox_to_anchor for outside.

example_03.pymatplotlib
Output

Legends cost the reader work

Labelling the lines directly is often better.

example_04.pymatplotlib
Output

Titles that say the finding

A descriptive title labels the chart; an assertive one states what it shows.

example_05.pymatplotlib
Output

Figure-level text

For a grid, the labels usually belong to the figure rather than each axes.

example_06.pymatplotlib
Output

With a grid of subplots sharing axes, repeating the same axis label on each panel is noise.

fig.supxlabel, fig.supylabel and fig.suptitle attach text to the figure instead. One label serves the whole grid.

fig.tight_layout() later makes room for them. Without it, a suptitle often shares the top row of panels.

legend

ax.legend() collects the artists on the axes that have a label and draws a key.

Artists without a label are skipped. If nothing has one, matplotlib warns and draws nothing. That warning. "No artists with labels found". Is one of the most common in matplotlib and always means the same thing.

A label beginning with an underscore is on purpose excluded. That is the method for keeping a helper artist. A threshold line, a shaded band. Out of the key without giving up labelling it in code.

You can also pass the handles and labels by name, ax.legend(handles, labels). This Is how you control the order or combine artists from other axes.

Placement

loc names a spot: "upper right", "lower left", "center", and the rest.

The default is "best". This Searches for the spot shared the least data. That is convenient and has two costs. It's slow on a plot with many points. It can put the legend one spot other when the data changes, which is unwelcome in a figure you're iterating on.

For a legend outside the axes:

ax.legend(loc="upper left", bbox_to_anchor=(1.02, 1), borderaxespad=0)

bbox_to_anchor gives a point in axes coords. (1.02, 1) is just past the right edge, at the top. And loc says which corner of the legend goes there. note to leave room, with fig.tight_layout() or a smaller axes, or the legend will be cut off when saved.

ncol=3 spreads a legend flatly. This Suits one placed above or below the axes.

frameon=False drops the box. This Often looks cleaner over a plot with white space.

Direct labelling

A legend asks the reader to match a colour to a name, hold it in memory. Find the matching line. That is real cognitive work, repeated for each series.

Labelling each line at its end drops the step:

ax.text(x[-1] + 0.2, y[-1], name, va="center")

It works well when the lines end at other heights. This Is common in time series. It fails when they settle. Then a legend is the honest choice.

This is one of the highest-value small gains on hand to a line chart. Matplotlib has no built-in for it. Two lines of text is the whole setup.

Titles that say something

The default habit is a title that names the vars: "Sales by month". The chart already shows that. The axis labels already say it.

A more handy title states the finding: "Sales peaked in August, then fell 22%". The reader then knows what they're looking for. The chart supports the claim rather than posing a question.

loc="left" left-aligns the title. This Makes it read like a headline rather than a caption. Mixed with a slightly larger font and a subtitle in smaller grey text, it's the layout most publications use. It's a few lines of matplotlib.

Annotating the thing the title mentions. A vertical line at the peak, a highlighted point. Closes the loop between the words and the picture.

Text properties

each text-making method takes the same styling args: fontsize, fontweight, color, family, style, alpha, rotation.

A small set of rules covers most charts:

Title — larger, bold, left-aligned. It's a headline.

Axis labels — default size, sentence case, with units.

Tick labels — smaller and lighter than the data. They're link rather than content.

Annotations — the colour of the thing they annotate. This Ties them together without an arrow.

plt.rcParams holds defaults for all of these. axes.titlesize, axes.labelsize, xtick.labelsize. So a house style sets them once rather than per chart.

Legend contents

ax.legend() takes args that solve most legend problems.

ncol=3 spreads it flatly. This Suits a legend above or below the axes and wastes far less vertical space than a single column.

title="Region" labels the legend itself. This Drops the need to explain the groups elsewhere.

frameon=False drops the box. framealpha=0.8 keeps it but lets the data show through.

fontsize="small" shrinks it, right because a legend is link material rather than content.

handles and labels given by name control both the order and the contents. How you put the series in the same vertical order they appear on the chart. This Makes the mapping obvious.

Mathematical text

Any text arg accepts mathtext between dollar signs:

ax.set_ylabel(r"Energy ($\mathrm{J\,m^{-2}}$)")

The r prefix matters, because backslashes are else read by Python before matplotlib sees them.

This is a built-in subset of LaTeX and needs no LaTeX installation. rcParams["text.usetex"] = True switches to a real LaTeX renderer for full support, at the cost of needing LaTeX to be present and slowing rendering a lot.

For units, superscripts and Greek letters. Which is most scientific labelling — mathtext is enough.

Multi-line and wrapped text

A long title can be broken with a newline. The alignment applies to the block:

ax.set_title("A longer headline that
runs to two lines", loc="left")

linespacing= adjusts the gap.

A common publication pattern is a bold headline and a lighter subtitle:

ax.set_title("Sales peaked in August", loc="left", fontsize=13, fontweight="bold")
ax.text(0, 1.02, "Monthly, 2024, thousands", transform=ax.transAxes,
        fontsize=9, color="0.4")

Two lines, and the chart reads like a thing published rather than a thing exported.

Labels that fix themselves

The most solid label is one found from the data rather than typed:

ax.set_title("Peak %s: %.0f" % (months[i], y[i]))
ax.legend(title="n = %d" % len(df))

A hard-coded number in a title is wrong the first time the data changes. Nothing will tell you. Deriving it means the chart can't disagree with itself. Which matters most for just the charts that get regenerated regularly.

Where the reader looks

Text placement isn't only about fitting. It's about the order things are read.

The title is read first, so it should carry the message.

The direct labels on the data are read next, if they exist. This Is why they beat a legend.

The axis labels are consulted when a value needs reading.

The legend is consulted again and again. This Is the cost that direct labels drop.

A caption or source note is read last, if at all. Belongs in small grey text at the bottom.

Designing in that order — message, then data labels, then link material — makes a chart that can be understood at a glance and interrogated later. This Is what a good chart does.

Common labelling errors

No units. The single most common omission, and the one that makes a chart unusable.

A title that repeats the axes. "Sales by month" over a chart with "Month" and "Sales" on the axes says nothing new.

A legend with no title. Here The groups need showing.

Labels rotated 45 degrees when barh would have avoided rotation fully.

Text that shares the data, with no background box.

A hard-coded number in the title that no longer matches the data.

**A legend in loc="best"** that moves between runs, making two versions of a figure hard to compare.

All are cheap to fix and all live into published charts regularly.

Labels, Titles and Legends, in summary

Title, both axis labels with units. A legend when there's more than one series. That is the min, and units are the part most often missing.

legend() reads labels from the artists, warns when there are none. Skips anything whose label starts with an underscore.

Direct labels on the lines drop the colour-matching a legend imposes. Are worth the two lines of text they cost.

A title that states the finding is more handy than one naming the vars. loc="left" makes it read as a headline.

For a grid, fig.suptitle and supxlabel replace repeating the same label on each panel.

And any number in a label should be found from the data. A hard-coded one is wrong the first time the data changes and nothing will say so.

Writing the title

The title is the most valuable text on a chart and the most often wasted.

Three levels, in growing usefulness:

Descriptive — "Sales by month". Names the vars the axes already name. Adds nothing.

exact — "Monthly sales, 2024, all regions". Adds scope. This The reader needs. Belongs in a subtitle rather than the headline.

Assertive — "Sales peaked in August, then fell 22%". States what the chart shows. The reader knows what to look for and can check the claim against the picture.

The assertive form has a discipline attached: having written it, you've to make sure the chart supports it. That is a handy constraint. It often changes the chart. Highlighting August, annotating the fall, dropping series that aren't part of the claim.

Where a chart truly has no single finding. An exploratory panel, a link figure. The exact form is right. The lack of a claim is itself helpful.

Subtitles and source notes

Two pieces of text that most charts want and matplotlib has no method for.

A subtitle carries the scope that the headline title leaves out. The period, the units, the group. Placed just under the title in axes coords, smaller and grey:

ax.text(0, 1.02, "Monthly, 2024, all regions", transform=ax.transAxes,
        fontsize=9, color="0.4", va="bottom")

A source note goes at the bottom of the figure, smaller still:

fig.text(0, 0, "Source: internal sales data, extracted 2024-09-01",
         fontsize=8, color="0.5", va="bottom")

Both are two lines and both are what makes a chart usable by someone who didn't make it. The source note in given is the gap between a chart that can be checked and one that has to be taken on trust.

One more thing

ax.legend(labelcolor="linecolor") colours each legend label to match its line. This Drops the need for the swatch fully and reads well with handlelength=0.

It's a compact treatment that sits between a full legend and direct labelling. Works when the lines settle so that end labels would share.

Labels, Titles and Legends: the short version

Text is what turns a picture of numbers into a thing someone else can act on. Units are the part most often left out.

A legend is link material that the reader consults again and again. A direct label is read once. Where the lines end apart, the direct label wins. It costs two lines of text.

Questions people ask about Labels, Titles and Legends

Why does ax.legend() show nothing? No artist has a label. Pass label= to each plot call, since legend only lists labelled artists.

Where should the legend go? Away from the data. Try loc='best' first, then move it outside with bbox_to_anchor, or label lines directly instead.

What should a title say? The finding, not the topic. 'Sales fell after March' tells readers what to look for. 'Monthly sales' does not.

How do I include units in axis labels? Put them in parentheses after the name, such as ax.set_xlabel('Distance (km)'). Label every axis that has units.

How do I write maths in a label? Wrap it in dollar signs, for example a Greek letter name between two dollar signs. matplotlib renders a subset of LaTeX called mathtext without needing LaTeX installed.

What is the difference between plt.title and ax.set_title? plt.title acts on the current axes. ax.set_title acts on the axes you name, which is safer with more than one panel.

Reading the code back

The text on a chart is written in a fixed order. The title says what was found, the axis labels say what the numbers are. The legend or the direct labels say which series is which. Written in that order, the chart is full. Written in any other, a thing is often missing. The most common omission is units. The most common redundancy is a title that repeats the axis names.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What does `ax.legend()` do when no artist has a label?

  2. How do you keep a helper line out of the legend?

  3. What is the drawback of `loc='best'`?

  4. Why is 'Sales peaked in August, then fell 22%' a better title than 'Sales by month'?

Cheat sheet

Labels, Titles and Legends

Add axis labels, a title and a legend to a matplotlib figure. Make a chart that someone else can read without asking what it shows.

MATPLOTLIB · vizlearn.in/matplotlib/labels_and_legends.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.