You may use the following class to create the text along the line. Instead of an angle it takes two points (p
and pa
) as input. The connection between those two points define the angle in data coordinates. If pa
is not given, the connecting line between p
and xy
(the text coordinate) is used.
The angle is then updated automatically such that the text is always oriented along the line. This even works with logarithmic scales.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.text as mtext
import matplotlib.transforms as mtransforms
class RotationAwareAnnotation(mtext.Annotation):
def __init__(self, s, xy, p, pa=None, ax=None, **kwargs):
self.ax = ax or plt.gca()
self.p = p
if not pa:
self.pa = xy
self.calc_angle_data()
kwargs.update(rotation_mode=kwargs.get("rotation_mode", "anchor"))
mtext.Annotation.__init__(self, s, xy, **kwargs)
self.set_transform(mtransforms.IdentityTransform())
if 'clip_on' in kwargs:
self.set_clip_path(self.ax.patch)
self.ax._add_text(self)
def calc_angle_data(self):
ang = np.arctan2(self.p[1]-self.pa[1], self.p[0]-self.pa[0])
self.angle_data = np.rad2deg(ang)
def _get_rotation(self):
return self.ax.transData.transform_angles(np.array((self.angle_data,)),
np.array([self.pa[0], self.pa[1]]).reshape((1, 2)))[0]
def _set_rotation(self, rotation):
pass
_rotation = property(_get_rotation, _set_rotation)
Example usage:
fig, ax = plt.subplots()
t = np.arange(0.0, 1.0, 0.01)
line, = ax.plot(t, t, color='blue', lw=2)
ra = RotationAwareAnnotation("test label", xy=(.5,.5), p=(.6,.6), ax=ax,
xytext=(2,-1), textcoords="offset points", va="top")
plt.show()
Alternative for edge-cases
The above may fail in certain cases of text along a vertical line or on scales with highly dissimilar x- and y- units (example here). In that case, the following would be better suited. It calculates the angle in screen coordinates, instead of relying on an angle transformation.
class RotationAwareAnnotation2(mtext.Annotation):
def __init__(self, s, xy, p, pa=None, ax=None, **kwargs):
self.ax = ax or plt.gca()
self.p = p
if not pa:
self.pa = xy
kwargs.update(rotation_mode=kwargs.get("rotation_mode", "anchor"))
mtext.Annotation.__init__(self, s, xy, **kwargs)
self.set_transform(mtransforms.IdentityTransform())
if 'clip_on' in kwargs:
self.set_clip_path(self.ax.patch)
self.ax._add_text(self)
def calc_angle(self):
p = self.ax.transData.transform_point(self.p)
pa = self.ax.transData.transform_point(self.pa)
ang = np.arctan2(p[1]-pa[1], p[0]-pa[0])
return np.rad2deg(ang)
def _get_rotation(self):
return self.calc_angle()
def _set_rotation(self, rotation):
pass
_rotation = property(_get_rotation, _set_rotation)
For usual cases, both result in the same output. I'm not sure if the second class has any drawbacks, so I'll leave both in here, choose whichever you seem more suitable.