mahmood
mahmood

Reputation: 24705

Properly rotate and align annotation labels

The following code puts some points on the plane and draws a line from center to each point. For each point, there is a label and want to put the label after the point. Therefore, from center, we see a line, then a point and then a text. I want to put the label with the same slope of the line.

Currently, I have this code, but as you can see the rotated text is not properly aligned. How can I fix that?

import matplotlib.pyplot as plt
import numpy as np
from math import *
a = np.array([
[-0.108,0.414],
[0.755,-0.152],
[0.871,-0.039],
],)
labels = ["XXXXXXX", "YYYYYY", "ZZZZZZZ"]

x, y = a.T
plt.scatter(x, y)
plt.xlim(-1,1)
plt.ylim(-1,1)

ax = plt.axes()
for i in range(a.shape[0]):
   px = a[i,0]
   py = a[i,1]
   ax.arrow(0, 0, px, py, head_width=0, head_length=0.1, length_includes_head=True)
   angle = degrees(atan(py/px))
   ax.annotate(labels[i], (px, py), rotation=angle)

plt.grid(True)
plt.show()

enter image description here

UPDATE:

I used the solution proposed here and modified

text_plot_location = np.array([0.51,0.51])
trans_angle = plt.gca().transData.transform_angles(np.array((45,)),text_plot_location.reshape((1,2)))[0]
ax.annotate(labels[i], (px, py), rotation=text_plot_location)

However, I get this error TypeError: unhashable type: 'numpy.ndarray'

Upvotes: 0

Views: 612

Answers (3)

Stef
Stef

Reputation: 30579

Not ideal but a bit closer to what you want. The drawback is the arbitrary value of 30 points for the text offset that works for the given labels but needs to be adjusted for longer or shorter labels.

import matplotlib.pyplot as plt
import numpy as np
from math import *
a = np.array([[-0.108,0.414],[0.755,-0.152],[0.871,-0.039]])
labels = ["XXXXXXX", "YYYYYY", "ZZZZZZZ"]

x, y = a.T
plt.scatter(x, y)
plt.xlim(-1,1)
plt.ylim(-1,1)

ax = plt.axes()
for i in range(a.shape[0]):
   px = a[i,0]
   py = a[i,1]
   ax.arrow(0, 0, px, py, head_width=0, head_length=0.1, length_includes_head=True)
   angle = atan(py/px)
   d = (-1 if px < 0 else 1) * 30
   ax.annotate(labels[i], (px, py), rotation=degrees(angle), textcoords="offset points", 
               xytext=(d*cos(angle), d*sin(angle)), 
               verticalalignment='center', horizontalalignment='center')

plt.grid(True)
plt.show()

enter image description here

Upvotes: 1

mapf
mapf

Reputation: 2058

You made a simple mistake in your update. You need to pass trans_angle to the rotation key word instead of text_plot_location, however, I'm not sure if the result is what you are looking for.

import matplotlib.pyplot as plt
import numpy as np
from math import *
a = np.array([
[-0.108,0.414],
[0.755,-0.152],
[0.871,-0.039],
],)
labels = ["XXXXXXX", "YYYYYY", "ZZZZZZZ"]

x, y = a.T
plt.scatter(x, y)
plt.xlim(-1,1)
plt.ylim(-1,1)

ax = plt.axes()
for i in range(a.shape[0]):
    px = a[i, 0]
    py = a[i, 1]
    ax.arrow(0, 0, px, py, head_width=0, head_length=0.1,
             length_includes_head=True)
    text_plot_location = np.array([0.51, 0.51])
    angle = degrees(atan(py / px))
    trans_angle = plt.gca().transData.transform_angles(
        np.array((angle,)), text_plot_location.reshape((1, 2))
    )[0]
    ax.annotate(labels[i], (px, py), rotation=trans_angle)

plt.grid(True)
plt.show()

enter image description here

Upvotes: 0

cvanelteren
cvanelteren

Reputation: 1703

The link by @mapf is a bit cleaner, but this is what I came up with:

import matplotlib.pyplot as plt
import numpy as np
a = np.array([
[-0.108,0.414],
[0.755,-0.152],
[0.871,-0.039],
],)
labels = ["XXXXXXX", "YYYYYY", "ZZZZZZZ"]

x, y = a.T

fig, ax = plt.subplots()
ax.scatter(x, y)
ax.set_xlim(-1,1)
ax.set_ylim(-1,1)
line, = ax.plot(*a.T)
for jdx, (label, point) in enumerate(zip(labels, a)):
    # find closest point
    tmp = np.linalg.norm(a - point, axis = 1)
    idx = np.argsort(tmp)[1]
    other = a[idx]
    
    # compute angle
    deg = np.angle(complex(*(point - other)))
    deg = np.rad2deg(deg)
    ax.annotate(label, point, rotation = deg,
            ha = 'left', va = 'baseline',
            transform = ax.transData)
ax.grid(True)
fig.show()

enter image description here

I am not sure why the angle does not match the line exactly.

Upvotes: 1

Related Questions