Placing elements precisely, such as insets, labels at fixed offsets or lines spanning panels, requires choosing the right coordinate system.
Every artist has a transform mapping its coordinates to display pixels. The four you use most:
ax.transData: data coordinates, the default for plotted data.ax.transAxes: (0, 0) is the Axes' bottom-left, (1, 1) its top-right.fig.transFigure: the same for the whole figure.fig.dpi_scale_trans: inches from the figure's bottom-left.
Pass any of them as transform= to text, patches or lines. Blended transforms mix systems per axis: ax.get_xaxis_transform() uses data x and axes y, so a span from y=0 to 1 always covers the full height at a given x (this is how axvline works).
Transforms compose with +. transData itself is transScale + transLimits + transAxes, which is how log scales and limits feed into pixels. ScaledTranslation(dx, dy, fig.dpi_scale_trans) adds an offset in inches, so ax.transData + offset places something a fixed physical distance from a data point regardless of zoom.
transform.transform(points) converts to display pixels and transform.inverted() goes back, for example to convert a mouse position or to compute where a label landed.
Display coordinates depend on figure size, dpi and limits, so compute them only after the layout is final (after a draw), and never store pixel positions across resizes. ax.inset_axes([x, y, w, h]) uses axes coordinates by default, making insets resolution-independent.
Example
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
fig, ax = plt.subplots(figsize=(4, 3), dpi=100)
ax.set_xlim(0, 10); ax.set_ylim(0, 100)
# data -> display pixels and back
px = ax.transData.transform((5, 50))
print(ax.transData.inverted().transform(px)) # [ 5. 50.]
# axes coordinates: centre of the Axes, whatever the limits
print((ax.transAxes.transform((0.5, 0.5)) == px).all()) # True here
# blended: x in data, y in axes -> full-height marker at x=7
trans = ax.get_xaxis_transform()
ax.plot([7, 7], [0, 1], transform=trans, color="red")
# fixed 10-point offset to the right of a data point
offset = mtransforms.ScaledTranslation(10 / 72, 0, fig.dpi_scale_trans)
ax.text(5, 50, "label", transform=ax.transData + offset)
print(ax.transData.contains_branch(ax.transAxes)) # True
fig.savefig("transforms.png")Remember
- transData, transAxes, transFigure and dpi_scale_trans cover most needs
- Blended transforms mix data and axes coordinates per axis
- Compose with
+;inverted()maps pixels back to data
Watch out for
- Caching display coordinates before the final layout or dpi is applied.