0

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'

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
mahmood
  • 23,197
  • 49
  • 147
  • 242
  • 1
    Does this answer your question? [How to rotate matplotlib annotation to match a line?](https://stackoverflow.com/questions/18780198/how-to-rotate-matplotlib-annotation-to-match-a-line) – mapf Dec 21 '20 at 20:33
  • That puts the label above the line. In my final code, I have many points and if I put the labels above lines, then the representation will not be good for those line which have small angles. – mahmood Dec 21 '20 at 20:36
  • 1
    I understand, but you should be able to control the offset of the text relative to the line. – mapf Dec 21 '20 at 20:38
  • I updated the post. – mahmood Dec 21 '20 at 21:25

3 Answers3

1

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.

cvanelteren
  • 1,633
  • 9
  • 16
1

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

Stef
  • 28,728
  • 2
  • 24
  • 52
0

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

mapf
  • 1,906
  • 1
  • 14
  • 40