Skillversitas

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 caseCoordinates
Pointing at a data valuexy in data coords (default)
Fixed corner / margin texttransform=ax.transAxes, fractions 0-1
Annotation text that follows the pointdata coords for both, offset in data units
Text that stays put when limits changeaxes 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.