Getting the coordinates of the arrow in a Matplotlib annotation - python

Following on from my previous question I have the coordinates of the text label box in figure fraction coordinates and attempted to get the coordinates of the arrow patch in the same way.
But the coordinates I get do not correspond to the arrow, because when I plot a line over the same coordinates it doesn't lie on top of it:
import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
def f(x):
return 10 * np.sin(3*x)**4
x = np.linspace(0, 2*np.pi, 100)
y = f(x)
fig, ax = plt.subplots()
ax.plot(x,y)
xpt = 1.75
ypt = f(xpt)
xy = ax.transData.transform([xpt, ypt])
xy = fig.transFigure.inverted().transform(xy)
xytext = xy + [0.1, -0.1]
rdx, rdy = 0, 1
ann = ax.annotate('A point', xy=xy, xycoords='figure fraction',
xytext=xytext, textcoords='figure fraction',
arrowprops=dict(arrowstyle='->', connectionstyle="arc3",
relpos=(rdx, rdy)),
bbox=dict(fc='gray', edgecolor='k', alpha=0.5),
ha='left', va='top'
)
fig.canvas.draw()
leader_line_box = ann.arrow_patch.get_extents()
print(leader_line_box)
leader_line_box = fig.transFigure.inverted().transform(leader_line_box)
print(leader_line_box)
from matplotlib.lines import Line2D
line = Line2D(leader_line_box.T[0], leader_line_box.T[1],transform=fig.transFigure, lw=2, color='m')
ax.add_line(line)
plt.savefig('test.png')
How can I get the ((x0,y0), (x1,y1)) coordinates of the annotation arrow in figure fraction units and what has gone wrong in my attempt here?

The easiest way in this very specific case is to just draw the x-coordinates in reverse
line = Line2D(leader_line_box.T[0][::-1], leader_line_box.T[1],transform=fig.transFigure, lw=2, color='m')
If you need a more general solution,
verts = ann.arrow_patch.get_path()._vertices
tverts= fig.transFigure.inverted().transform(verts)
index = [0,2]
line = Line2D([tverts[index[0],0],tverts[index[1],0]], [tverts[index[0],1],tverts[index[1],1]],
transform=fig.transFigure, lw=2, color='m')
ax.add_line(line)
This will work for any arrow direction (pointing upwards or downwards, east or west) but is specific to the arrowprops arguments arrowstyle='->' and connectionstyle="arc3". Using different arrowstyle or connection style will require to set index to different values which can be found by chosing the appropriate indices from the array stored in verts.
In a very general case one can also look at the following:
box = ann.arrow_patch._posA_posB
tbox = fig.transFigure.inverted().transform(leader_line_box)
print tbox
line = Line2D(tbox.T[0], tbox.T[1],transform=fig.transFigure)
However this will get you the line between the annotated point and the text itself. In general this line might be different from the actual arrow, depending in the arrow style in use.

You're almost there, you have the coordinates of the bounding box of the arrow, which is the box drawn using the arrow as the diagonal. From that, we can find the head / tail coordinates.
The bounding box coordinates are given in the order [[left, bottom], [right, top]]. Here, the arrow head is at the top left, and tail is bottom right. So we can draw two lines to visually mark these. Replacing that section in your code with this:
from matplotlib.lines import Line2D
dl = 0.01 # some arbitrary length for the marker line
head = [leader_line_box.T[0][0], leader_line_box.T[1][1]]
line_head = Line2D([head[0],head[0]+dl], [head[1],head[1]+dl],
transform=fig.transFigure, lw=2, color='r') # mark head with red
ax.add_line(line_head)
tail = [leader_line_box.T[0][1], leader_line_box.T[1][0]]
line_tail = Line2D([tail[0],tail[0]+dl], [tail[1],tail[1]+dl],
transform=fig.transFigure, lw=2, color='g') # mark tail with green
ax.add_line(line_tail)
results in the following plot:

Related

How to draw arrows and rectangles (for protein sec structure) outside main plot in matplotlib?

I'm trying to draw arrows and rectangles in matplotlib (to represent protein secondary structure) next to the y-axis of the plot, something like this:
From here I got the arrow part, but I can't figure out how to draw it outside the y-axis. Also, is there a way to draw rectangles in addition to arrows? Code and output below:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
x_tail = 0.0
y_tail = -0.1
x_head = 0.0
y_head = 0.9
dx = x_head - x_tail
dy = y_head - y_tail
fig, axs = plt.subplots(nrows=2)
arrow = mpatches.FancyArrowPatch((x_tail, y_tail), (dx, dy),
mutation_scale=50,
transform=axs[0].transAxes)
axs[0].add_patch(arrow)
arrow = mpatches.FancyArrowPatch((x_tail, y_tail), (dx, dy),
mutation_scale=100,
transform=axs[1].transAxes)
axs[1].add_patch(arrow)
axs[1].set_xlim(0, 1)
axs[1].set_ylim(0, 1)
It looks like the original approach is somewhat confusing.
Although you can draw rectangles via mpatch.Rectangle, I think it is easier to also draw the rectangles via FancyArrowPatch. That makes them behave and scale similarly, which is interesting for setting the width. Similarly, the vertical line is also drawn using a FancyArrowPatch.
For the positioning, it seems you can just give (tail_x, tail_y) and head_x, head_y. Via arrowstyle= the visual dimensions can be set. Leaving out head_length= from the style seems to allow an arrow that looks like a rectangle. For coloring, there are facecolor= and edgecolor=. And also color= which treats facecolor and edgecolor simultaneously.
arrow1.set_clip_on(False) allows to draw the arrows in the margin. Other functions can have a clip_on=False parameter. zorder= is needed to make the correct lines visible when one is drawn on top of the other.
Here is some example code. The rectangle is drawn twice so the vertical line doesn't show through the hatching. Now x is defined in 'axis coordinates' and y in the standard data coordinates. The 'axis' coordinates go from 0, the left border where usually y-axis is drawn to 1, the right border. Setting x to -0.1 means 10% to the left of the y-axis.
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.transforms as mtransforms
x0 = -0.1
arrow_style="simple,head_length=15,head_width=30,tail_width=10"
rect_style="simple,tail_width=25"
line_style="simple,tail_width=1"
fig, ax = plt.subplots()
# the x coords of this transformation are axes, and the y coord are data
trans = mtransforms.blended_transform_factory(ax.transAxes, ax.transData)
y_tail = 5
y_head = 15
arrow1 = mpatches.FancyArrowPatch((x0, y_tail), (x0, y_head), arrowstyle=arrow_style, transform=trans)
arrow1.set_clip_on(False)
ax.add_patch(arrow1)
y_tail = 40
y_head = 60
arrow2 = mpatches.FancyArrowPatch((x0, y_tail), (x0, y_head), arrowstyle=arrow_style, facecolor='gold', edgecolor='black', linewidth=1, transform=trans)
arrow2.set_clip_on(False)
ax.add_patch(arrow2)
y_tail = 20
y_head = 40
rect_backgr = mpatches.FancyArrowPatch((x0, y_tail), (x0, y_head), arrowstyle=rect_style, color='white', zorder=0, transform=trans)
rect_backgr.set_clip_on(False)
rect = mpatches.FancyArrowPatch((x0, y_tail), (x0, y_head), arrowstyle=rect_style, fill=False, color='orange', hatch='///', transform=trans)
rect.set_clip_on(False)
ax.add_patch(rect_backgr)
ax.add_patch(rect)
line = mpatches.FancyArrowPatch((x0, 0), (x0, 80), arrowstyle=line_style, color='orange', transform=trans, zorder=-1)
line.set_clip_on(False)
ax.add_patch(line)
ax.set_xlim(0, 30)
ax.set_ylim(0, 80)
plt.show()

How to colour circular lines in polar chart (matplotlib)

I'm trying to to colour the circular line that corresponds to the value of 0 in a polar chart. This is what I want to achieve:
On this related question (Shading a segment between two lines on polar axis (matplotlib)), ax.fill_between is used to fill the space between two values, but I'm looking for a way to colour just the circular line where the value for each variable is 0.
If anybody has any tips that would be most appreciated! I've inserted a minimal working example below if anybody fancies having a go.
import matplotlib.pyplot as plt
import pandas as pd
def make_spider(row, title, color):
import math
categories = list(df)
N = len(categories)
angles = [n / float(N) * 2 * math.pi for n in range(N)]
angles += angles[:1]
ax = plt.subplot(1, 5, row+1, polar=True)
plt.xticks(angles[:-1], categories, color='grey', size=8)
values = df.iloc[row].values.flatten().tolist()
values += values[:1]
ax.plot(angles, values, color=color, linewidth=2, linestyle='solid')
ax.fill(angles, values, color=color, alpha = .4)
plt.gca().set_rmax(.4)
my_dpi = 40
plt.figure(figsize=(1000/my_dpi, 1000/my_dpi), dpi=96)
my_palette = plt.cm.get_cmap('Set2', len(df.index)+1)
for row in range(0, len(df.index)):
make_spider( row = row, title='Cluster: ' + str(row), color=my_palette(row) )
Example dataframe here:
df = pd.DataFrame.from_dict({"no_rooms":{"0":-0.3470532925,"1":-0.082144001,"2":-0.082144001,"3":-0.3470532925,"4":-0.3470532925},"total_area":{"0":-0.1858487321,"1":-0.1685491141,"2":-0.1632483955,"3":-0.1769700284,"4":-0.0389887094},"car_park_spaces":{"0":-0.073703681,"1":-0.073703681,"2":-0.073703681,"3":-0.073703681,"4":-0.073703681},"house_price":{"0":-0.2416123064,"1":-0.2841806825,"2":-0.259622004,"3":-0.3529449824,"4":-0.3414842657},"pop_density":{"0":-0.1271390651,"1":-0.3105853643,"2":-0.2316607937,"3":-0.3297832328,"4":-0.4599021194},"business_rate":{"0":-0.1662745006,"1":-0.1426329043,"2":-0.1577528867,"3":-0.163560133,"4":-0.1099718326},"noqual_pc":{"0":-0.0251535462,"1":-0.1540641646,"2":-0.0204666924,"3":-0.0515740013,"4":-0.0445135996},"level4qual_pc":{"0":-0.0826103951,"1":-0.1777759951,"2":-0.114263357,"3":-0.1787044751,"4":-0.2709496389},"badhealth_pc":{"0":-0.105481688,"1":-0.1760349683,"2":-0.128215043,"3":-0.1560577648,"4":-0.1760349683}})
Probably a cheap hack based on the link you shared. The trick here is to simply use 360 degrees for fill_between and then use a very thin region around the circular line for 0 using margins such as -0.005 to 0.005. This way, you make sure the curve is centered around the 0 line. To make the line thicker/thinner you can increase/decrease this number. This can be straightforwardly extended to color all circular lines by putting it in a for loop.
ax.plot(angles, values, color=color, linewidth=2, linestyle='solid')
ax.fill(angles, values, color=color, alpha = .4)
ax.fill_between(np.linspace(0, 2*np.pi, 100), -0.005, 0.005, color='red', zorder=10) # <-- Added here
Other alternative could be to use a Circle patch as following
circle = plt.Circle((0, 0), 0.36, transform=ax.transData._b, fill=False, edgecolor='red', linewidth=2, zorder=10)
plt.gca().add_artist(circle)
but here I had to manually put 0.36 as the radius of the circle by playing around so as to put it exactly at the circular line for 0. If you know exactly the distance from the origin (center of the polar plot), you can put that number for exact position. At least for this case, 0.36 seems to be a good guess.
There is an easier option:
fig_radar.add_trace(go.Scatterpolar(
r = np.repeat(0, 360),
dtheta = 360,
mode = 'lines',
name = 'cirlce',
line_color = 'black',
line_shape="spline"
)
The addition of line_shape = "spline" makes it appear as a circle
dtheta divides the coordinates in so many parts (at least I understood it this way and it works)

Polar plot - Put one grid line in bold

I am trying to make use the polar plot projection to make a radar chart. I would like to know how to put only one grid line in bold (while the others should remain standard).
For my specific case, I would like to highlight the gridline associated to the ytick "0".
from matplotlib import pyplot as plt
import pandas as pd
import numpy as np
#Variables
sespi = pd.read_csv("country_progress.csv")
labels = sespi.country
progress = sespi.progress
angles=np.linspace(0, 2*np.pi, len(labels), endpoint=False)
#Concatenation to close the plots
progress=np.concatenate((progress,[progress[0]]))
angles=np.concatenate((angles,[angles[0]]))
#Polar plot
fig=plt.figure()
ax = fig.add_subplot(111, polar=True)
ax.plot(angles, progress, '.--', linewidth=1, c="g")
#ax.fill(angles, progress, alpha=0.25)
ax.set_thetagrids(angles * 180/np.pi, labels)
ax.set_yticklabels([-200,-150,-100,-50,0,50,100,150,200])
#ax.set_title()
ax.grid(True)
plt.show()
The gridlines of a plot are Line2D objects. Therefore you can't make it bold. What you can do (as shown, in part, in the other answer) is to increase the linewidth and change the colour but rather than plot a new line you can do this to the specified gridline.
You first need to find the index of the y tick labels which you want to change:
y_tick_labels = [-100,-10,0,10]
ind = y_tick_labels.index(0) # find index of value 0
You can then get a list of the gridlines using gridlines = ax.yaxis.get_gridlines(). Then use the index you found previously on this list to change the properties of the correct gridline.
Using the example from the gallery as a basis, a full example is shown below:
r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
ax = plt.subplot(111, projection='polar')
ax.set_rmax(2)
ax.set_rticks([0.5, 1, 1.5, 2]) # less radial ticks
ax.set_rlabel_position(-22.5) # get radial labels away from plotted line
ax.grid(True)
y_tick_labels = [-100, -10, 0, 10]
ax.set_yticklabels(y_tick_labels)
ind = y_tick_labels.index(0) # find index of value 0
gridlines = ax.yaxis.get_gridlines()
gridlines[ind].set_color("k")
gridlines[ind].set_linewidth(2.5)
plt.show()
Which gives:
It is just a trick, but I guess you could just plot a circle and change its linewidth and color to whatever could be bold for you.
For example:
import matplotlib.pyplot as plt
import numpy as np
Yline = 0
Npoints = 300
angles = np.linspace(0,360,Npoints)*np.pi/180
line = 0*angles + Yline
ax = plt.subplot(111, projection='polar')
plt.plot(angles, line, color = 'k', linewidth = 3)
plt.ylim([-1,1])
plt.grid(True)
plt.show()
In this piece of code, I plot a line using plt.plot between any point of the two vectors angles and line. The former is actually all the angles between 0 and 2*np.pi. The latter is constant, and equal to the 'height' you want to plot that line Yline.
I suggest you try to decrease and increase Npoints while having a look to the documentaion of np.linspace() in order to understand your problem with the roundness of the circle.

How can I get the coordinates of a Matplotlib patch and use it to add a new axis?

I created a figure and axis using fig = plt.figure() and ax = fig.add_subplot(my_arguments). Then I added a few patches using matplotlib.patches. I transformed each patch by using matplotlib.transforms.Affine2D() to translate and rotate in data coordinates and then convert the transformed coordinates in display coordinates by adding ax.transData() to the end of my Affine2D transformations.
This is a simplified version of the code:
import matplotlib as mpl
import matplotlib.patches as patches
from matplotlib.transforms import Bbox
fig = plt.figure()
ax = fig.add_subplot(111)
# plot anything here
ax.plot(range(10), 'ro')
my_patches = []
# in my code there many patches and therefore the line
# below is actually a list comprehension for each one
my_patches.append(
patches.Rectangle( (1, 2), 10, 20, transform=mpl.transforms.Affine2D() \
.translate(1, 1) \
.rotate_deg_around(1, 2, 35)
+ ax.transData, fill=False, color='blue')
)
# now add a new axis using the coordinates of the patch
patch = my_patches[0]
# get the coords of the lower left corner of the patch
left, bottom = patch.get_xy()
# get its width and height
width, height = patch.get_width(), patch.get_height()
# create a Bbox instance using the coords of the patch
bbox = Bbox.from_bounds(left, bottom, width, height)
# transform from data coords to display coords
disp_coords = ax.transData.transform(bbox)
# transform from display coords to figure coords
fig_coords = fig.transFigure.inverted().transform(disp_coords)
# new axis
ax2 = fig.add_axes(Bbox(fig_coords))
# plot anything else here
ax2.plot(range(10), 'bo')
However, the additional axis is not added to the figure at the same position as the transformed coordinates of the patch (they're close, though). Am I missing something?
I'm uncertain about what the purpose of this code is, so this might not be what you want. But in order for the axes box to appear at coordinates (1,2), you should probably draw the canvas first before working with coordinates obtained from patches.
...
fig.canvas.draw()
left, bottom = patch.get_xy()
...

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
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.

Categories