I'm trying to have a rotated text in matplotlib. unfortunately the rotation seems to be in the display coordinate system, and not in the data coordinate system. that is:
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_axes([0.15, 0.1, 0.8, 0.8])
t = np.arange(0.0, 1.0, 0.01)
line, = ax.plot(t, t, color='blue', lw=2)
ax.text (0.51,0.51,"test label", rotation=45)
plt.show()
will give a line that will be in a 45 deg in the data coordinate system, but the accompanied text will be in a 45 deg in the display coordinate system.
I'd like to have the text and data to be aligned even when resizing the figure.
I saw here that I can transform the rotation, but this will works only as long as the plot is not resized.
I tried writing ax.text (0.51,0.51,"test label", transform=ax.transData, rotation=45)
, but it seems to be the default anyway, and doesn't help for the rotation
Is there a way to have the rotation in the data coordinate system ?
EDIT:
I'm interested in being able to resize the figure after I draw it - this is because I usually draw something and then play with the figure before saving it
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.
Ok, starting off with code similar to your example:
%pylab inline
import numpy as np
fig = plt.figure()
ax = fig.add_axes([0.15, 0.1, 0.8, 0.8])
t = np.arange(0.0, 1.0, 0.01)
line, = ax.plot(t, t, color='blue', lw=2)
ax.text(0.51,0.51,"test label", rotation=45)
plt.show()
As you indicated, the text label is not rotated properly to be parallel with the line.
The dissociation in coordinate systems for the text object rotation relative to the line has been explained at this link as you indicated. The solution is to transform the text rotation angle from the plot to the screen coordinate system, and let's see if resizing the plot causes issues as you suggest:
for fig_size in [(3.0,3.0),(9.0,3.0),(3.0,9.0)]: #use different sizes, in inches
fig2 = plt.figure(figsize=fig_size)
ax = fig2.add_axes([0.15, 0.1, 0.8, 0.8])
text_plot_location = np.array([0.51,0.51]) #I'm using the same location for plotting text as you did above
trans_angle = gca().transData.transform_angles(np.array((45,)),text_plot_location.reshape((1,2)))[0]
line, = ax.plot(t, t, color='blue', lw=2)
ax.text(0.51,0.51,"test label", rotation=trans_angle)
plt.show()
Looks good to me, even with resizing. Now, if you make the line longer and the axis limits longer, then of course you'd have to adjust the text drawing to occur at the new center of the plot.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With