'How to rotate matplotlib annotation to match a line?
Have a plot with several diagonal lines with different slopes. I would like to annotate these lines with a text label that matches the slope of the lines.
Something like this:

Is there a robust way to do this?
I've tried both text's and annotate's rotation parameters, but those are in screen coordinates, not data coordinates (i.e. it's always x degrees on the screen no matter the xy ranges). My x and y ranges differ by orders of magnitude, and obviously the apparent slope is affected by viewport size among other variables, so a fixed-degree rotation doesn't do the trick. Any other ideas?
Solution 1:[1]
This is the exact same process and basic code as given by @Adam --- it's just restructured to be (hopefully) a little more convenient.
def label_line(line, label, x, y, color='0.5', size=12):
"""Add a label to a line, at the proper angle.
Arguments
---------
line : matplotlib.lines.Line2D object,
label : str
x : float
x-position to place center of text (in data coordinated
y : float
y-position to place center of text (in data coordinates)
color : str
size : float
"""
xdata, ydata = line.get_data()
x1 = xdata[0]
x2 = xdata[-1]
y1 = ydata[0]
y2 = ydata[-1]
ax = line.get_axes()
text = ax.annotate(label, xy=(x, y), xytext=(-10, 0),
textcoords='offset points',
size=size, color=color,
horizontalalignment='left',
verticalalignment='bottom')
sp1 = ax.transData.transform_point((x1, y1))
sp2 = ax.transData.transform_point((x2, y2))
rise = (sp2[1] - sp1[1])
run = (sp2[0] - sp1[0])
slope_degrees = np.degrees(np.arctan2(rise, run))
text.set_rotation(slope_degrees)
return text
Used like:
import numpy as np
import matplotlib.pyplot as plt
...
fig, axes = plt.subplots()
color = 'blue'
line, = axes.plot(xdata, ydata, '--', color=color)
...
label_line(line, "Some Label", x, y, color=color)
Edit: note that this method still needs to be called after the figure layout is finalized, otherwise things will be altered.
See: https://gist.github.com/lzkelley/0de9e8bf2a4fe96d2018f1b1bd5a0d3c
Solution 2:[2]
I came up with something that works for me. Note the grey dashed lines:

The rotation must be set manually, but this must be done AFTER draw() or layout. So my solution is to associate lines with annotations, then iterate through them and do this:
- get line's data transform (i.e. goes from data coordinates to display coordinates)
- transform two points along the line to display coordinates
- find slope of displayed line
- set text rotation to match this slope
This isn't perfect, because matplotlib's handling of rotated text is all wrong. It aligns by the bounding box and not by the text baseline.
Some font basics if you're interested about text rendering: http://docs.oracle.com/javase/tutorial/2d/text/fontconcepts.html
This example shows what matplotlib does: http://matplotlib.org/examples/pylab_examples/text_rotation.html
The only way I found to have a label properly next to the line is to align by center in both vertical and horizontal. I then offset the label by 10 points to the left to make it not overlap. Good enough for my application.
Here is my code. I draw the line however I want, then draw the annotation, then bind them with a helper function:
line, = fig.plot(xdata, ydata, '--', color=color)
# x,y appear on the midpoint of the line
t = fig.annotate("text", xy=(x, y), xytext=(-10, 0), textcoords='offset points', horizontalalignment='left', verticalalignment='bottom', color=color)
text_slope_match_line(t, x, y, line)
Then call another helper function after layout but before savefig (For interactive images I think you'll have to register for draw events and call update_text_slopes in the handler)
plt.tight_layout()
update_text_slopes()
The helpers:
rotated_labels = []
def text_slope_match_line(text, x, y, line):
global rotated_labels
# find the slope
xdata, ydata = line.get_data()
x1 = xdata[0]
x2 = xdata[-1]
y1 = ydata[0]
y2 = ydata[-1]
rotated_labels.append({"text":text, "line":line, "p1":numpy.array((x1, y1)), "p2":numpy.array((x2, y2))})
def update_text_slopes():
global rotated_labels
for label in rotated_labels:
# slope_degrees is in data coordinates, the text() and annotate() functions need it in screen coordinates
text, line = label["text"], label["line"]
p1, p2 = label["p1"], label["p2"]
# get the line's data transform
ax = line.get_axes()
sp1 = ax.transData.transform_point(p1)
sp2 = ax.transData.transform_point(p2)
rise = (sp2[1] - sp1[1])
run = (sp2[0] - sp1[0])
slope_degrees = math.degrees(math.atan(rise/run))
text.set_rotation(slope_degrees)
Solution 3:[3]
New in matplotlib 3.4.0
There is now a built-in parameter transform_rotates_text for rotating text relative to a line:
To rotate text with respect to a line, the correct angle won't be the angle of that line in the plot coordinate system, but the angle that the line appears in the screen coordinate system. This angle can be determined automatically by setting the new parameter
transform_rotates_text.
So now we can just pass the raw data angle to plt.text and let matplotlib automatically transform it to the correct visual angle by setting transform_rotates_text=True:
# plot line from (1, 4) to (6, 10)
x = [1, 6]
y = [4, 10]
plt.plot(x, y, 'r.-')
# compute angle in raw data coordinates (no manual transforms)
dy = y[1] - y[0]
dx = x[1] - x[0]
angle = np.rad2deg(np.arctan2(dy, dx))
# annotate with transform_rotates_text to align text and line
plt.text(x[0], y[0], f'rotation={angle:.2f}', ha='left', va='bottom',
transform_rotates_text=True, rotation=angle, rotation_mode='anchor')
This approach is robust against the figure and axes scales. Even if we modify the figsize or xlim after placing the text, the rotation stays properly aligned:
# resizing the figure won't mess up the rotation
plt.gcf().set_size_inches(9, 4)
# rescaling the axes won't mess up the rotation
plt.xlim(0, 12)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Adam |
| Solution 3 |



