Annotation
Where a chart stops showing data and starts making a claim
ax.textax.annotatetransAxesax.axvspanarrowpropsax.set_titleWatch it happen
Play it through, or step back and forth yourself.
ax.text(3, 60, "quiet")in DATA coordinatesha="center", va="bottom"which corner sits on the pointfontsize=11, color="#9a9a9a"notes should be quieterax.textsevery Text artist on the axesA correct, labelled, unobjectionable chart — and it doesn't say anything. The reader has to work out for themselves what they were meant to notice. Annotation is where you stop showing data and start making a claim.
The idea
You can build a correct, labelled, unobjectionable chart that says nothing at all — the reader is left to work out for themselves what they were meant to notice. Annotation is where you tell them.
ax.text
ax.text(3, 60, "midweek lull", ha="center", color="#9a9a9a")Words at a point in data coordinates — so they move when the limits change. ha and va decide which part of the text sits on that point, and getting them wrong is why annotations so often look slightly adrift.
Notes should be quieter than the data: smaller, greyer. They're support, not content.
ax.annotate
This is the one to actually learn:
i = int(np.argmax(delhi))
ax.annotate(f"best day: {delhi[i]}",
xy=(i, delhi[i]), # what you're POINTING AT
xytext=(i - 2, delhi[i] + 8), # where the WORDS go
arrowprops=dict(arrowstyle="->", color="#9a9a9a"))Two coordinates: xy is the target, xytext is the label. Everything else about annotate follows once those are straight. arrowprops draws the connector, and connectionstyle="arc3,rad=0.2" curves it around whatever's in the way.
Notice that the interesting point is computed — np.argmax — not typed in. Hardcoding xy=(5, 88) works today and points at empty space the moment the data changes.
Axes coordinates
ax.text(0.03, 0.95, "n = 120",
transform=ax.transAxes, va="top",
bbox=dict(facecolor="#161616", edgecolor="none", alpha=0.8))transform=ax.transAxes switches to fractions of the axes: (0, 0) is bottom-left, (1, 1) is top-right. That's how you pin a note to the corner and have it stay in the corner when the data underneath changes. fig.transFigure does the same for the whole canvas — sources and footnotes.
Label the lines directly
A legend makes the eye travel to a key, decode a colour, and travel back. Writing each series' name at the end of its own line removes that trip:
for name, line in zip(names, ax.lines):
ax.text(x[-1] + 0.1, line.get_ydata()[-1], name,
color=line.get_color(), va="center")
ax.margins(x=0.12) # make room on the rightTaking the colour from line.get_color() is the trick that makes it hold together — the label is the key. This is the change that most improves a multi-series chart, and almost nobody makes it.
Highlighting a period
ax.axvspan("Sat", "Sun", alpha=0.1, zorder=0)
ax.text(5.5, 95, "weekend", ha="center", fontsize=9)A shaded band for a closure, a campaign, a lockdown. Faint, and zorder=0 to keep it behind the data. It carries context the numbers can't.
The title is the claim
Last and most valuable. "Cups by day" describes the axes, and the axes already describe themselves. Promote the finding instead:
ax.set_title("Weekend trade is 40% above weekdays", loc="left", fontsize=13)Now the chart is evidence for a stated claim rather than a puzzle. loc="left" reads as a headline, and a smaller grey second line beneath it can carry the detail.
The discipline: write the sentence you want the reader to leave with, then check the chart supports it. If it doesn't, you've learned something more useful than a title.
See it run
The lesson's code, ready to run and to fiddle with.
Putting the kettle on…
Starting up…
Worked example
not gradedAlready written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.
tryreplacing the title with the finding you would actually say out loud.
Your turn
3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Before you can annotate the peak you have to find it. Return the name of the day Delhi sold the most — computed, not typed in.
Plot delhi and annotate its peak with the text "peak", pointing at the peak and putting the words two days to its left. Return [ax.texts[0].get_text(), list(ax.texts[0].xy)].
Put a note in the top-left corner of the axes at (0.03, 0.95) using axes coordinates rather than data coordinates. Return ax.texts[0].get_transform() is ax.transAxes.
