Annotations and Arrows
Annotations point at specific data locations and explain them: "peak here," "outlier: sensor failure," "policy change in March." They are the difference between a chart that shows a pattern and one that tells its reader what the pattern means. Matplotlib separates plain text (ax.text) from anchored, arrow-connected notes (ax.annotate), and both live in either data coordinates or axes-fraction coordinates.
annotate: Text plus Arrow
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 10, 200)
y = np.sin(x) * np.exp(-0.1 * x)
fig, ax = plt.subplots()
ax.plot(x, y)
peak_x, peak_y = x[np.argmax(y)], y.max()
ax.annotate("damped peak",
xy=(peak_x, peak_y), # the data point to point at
xytext=(peak_x + 1.5, peak_y - 0.5), # where the text goes
arrowprops=dict(arrowstyle="->", color="gray",
connectionstyle="arc3,rad=0.2"),
fontsize=10)
plt.show()
xy is the annotated point in data coordinates; xytext is the text position - also data coordinates by default, or axes fractions with textcoords="axes fraction". arrowprops controls the connector: arrowstyle (->, -[, fancy, wedge), color, and connectionstyle (straight, arc3, angle3) for curved arrows that clear the data.
Plain Text with ax.text
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.bar(["A", "B", "C"], [3, 7, 5])
ax.text(0.5, 7.4, "new record", ha="center", va="bottom",
fontsize=9, color="darkgreen")
ax.text(0.95, 0.95, "FY25", transform=ax.transAxes,
ha="right", va="top", fontsize=9, color="gray")
plt.show()
ax.text(x, y, s) places text at a fixed position: data coordinates by default, or transform=ax.transAxes for fractions of the axes (0.95, 0.95 = top-right corner, wherever the limits are). ha/va align the text box around the anchor point. This is the tool for corner notes, region labels, and captions that should not follow the data.
Marker plus Annotation
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(8)
x = np.arange(30)
y = rng.normal(50, 5, 30)
y[17] = 78 # the outlier
fig, ax = plt.subplots()
ax.plot(x, y, marker="o", markersize=4)
ax.plot([17], [78], "o", color="red", markersize=10, zorder=5)
ax.annotate("sensor spike\n(maintenance window)",
xy=(17, 78), xytext=(20, 60),
arrowprops=dict(arrowstyle="->", color="red"),
fontsize=9, color="red")
plt.show()
The pattern: re-plot the one interesting point with a distinct marker, then annotate it. The arrow connects text to the exact location, and the color ties the explanation to the mark.
Choosing Coordinate Systems
| Use case | Coordinates |
|---|---|
| Pointing at a data value | xy in data coords (default) |
| Fixed corner / margin text | transform=ax.transAxes, fractions 0-1 |
| Annotation text that follows the point | data coords for both, offset in data units |
| Text that stays put when limits change | axes fraction |
Summary
Use ax.annotate when the explanation belongs to a data point - xy targets it, xytext places the note, and arrowprops draws the connector - and ax.text with transAxes for fixed-position captions. Mark the annotated point distinctly, keep arrows short and curved clear of the data, and annotate at most a few points per chart: every annotation is a claim, and claims should be few and defensible.