Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

keeps text rotated in data coordinate system after resizing?

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

like image 648
nopede11 Avatar asked Nov 11 '13 13:11

nopede11


2 Answers

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()

enter image description here

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.

like image 131
ImportanceOfBeingErnest Avatar answered Nov 10 '22 01:11

ImportanceOfBeingErnest


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()

enter image description here

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()

enter image description hereenter image description hereenter image description here

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.

like image 45
treddy Avatar answered Nov 10 '22 01:11

treddy