Draw arrows on matplotlib figure directly - python

I created a plot containing multiple subplots on a grid. The plots differ in two parameters, so I would like it to look like they ordered in a coordinate system.
I managed to plot lines using matplotlib.lines.Line2D() next to the subplots directly on the figure.
But I would prefer to have an arrow instead of a line to make it more clear.
(I can add the specific parameter values using fig.text().)
I'd like the blue lines to be arrows
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from itertools import product
fig = plt.figure()
plotGrid = mpl.gridspec.GridSpec(2, 2)
x = np.linspace(0,10,10000)
y = [j* np.sin(x + i) for i,j in product(range(2), range(1,3))]
for i in range(4):
ax = plt.Subplot(fig, plotGrid[i])
for sp in ax.spines.values():
sp.set_visible(False)
ax.plot(x,y[i], color = 'r')
ax.set_xticks([])
ax.set_yticks([])
fig.add_subplot(ax)
all_axes = fig.get_axes()
#I would like these lines to be arrows
blcorPosn = 0.08 #bottom corner position
l1 = mpl.lines.Line2D([blcorPosn,blcorPosn], [1, blcorPosn],
transform=fig.transFigure, fig)
l2 = mpl.lines.Line2D([blcorPosn, 1], [blcorPosn, blcorPosn],
transform=fig.transFigure, fig)
fig.lines.extend([l1, l2])
I'm not sure if this is the way to go. But I spend like a day on this by now and the only way I see so far to draw arrows is drawing them directly on an axes but thats not an option for me as far as I can see.
Also this is my first post here so advice on how to ask questions is highly appreciated.
Thanks

You can replace the Line2D along each axis with a slightly modified call to FancyArrow patch. The main difference is that that origin and destination x,y coords are replaced with origin x,y and a x,y distance to draw. The values are also passed as parameters directly, not as lists:
l1 = mpl.patches.FancyArrow(blcorPosn, blcorPosn, 1, 0,
transform=fig.transFigure, figure=fig)
l2 = mpl.patches.FancyArrow(blcorPosn, blcorPosn, 0, 1,
transform=fig.transFigure, figure=fig)
The FancyArrow patch accepts a few other parameters to allow you to customise the appearance of the arrow including width (for line width), head_width and head_length.

Related

Set arrow size based on figure units instead of axis data units?

In matplotlib, is there a way to specify arrow head sizes in figure units rather than in data units?
The use case is: I am making a multi-panel figure in which each panel has a different axis size (e.g., one goes from 0 to 1 on the X-axis, and the next goes from 0 to 10). I'd like the arrows to appear the same in each panel. I'd also like the arrows to appear the same independent of direction.
For axes with an aspect ratio not equal to 1, the width of the tail (and therefore the size of the head) varies with direction.
The closest I've come is, after drawing on the canvas:
dx = ax.get_xlim()[1] - ax.get_xlim()[0]
for arrow in ax.patches:
arrow.set_data(width=dx/50)
but this does not work; it results in images like this:
Just use ax.annotate() instead of ax.arrow():
import matplotlib.pyplot as plt
import numpy as np
xlim, ylim = (-.3, .8), (0, 5.8)
arrow_start, arrow_end = np.asarray([.1, 3]), np.asarray([.5, 5])
fig = plt.figure(figsize=(3, 2))
ax = plt.gca()
ax.set_title('using ax.arrow()')
ax.set_xlim(xlim)
ax.set_ylim(ylim)
ax.arrow(*arrow_start, *(arrow_end - arrow_start), width=1/50)
fig = plt.figure(figsize=(3, 2))
ax = plt.gca()
ax.set_title('using ax.annotate()')
ax.set_xlim(xlim)
ax.set_ylim(ylim)
ax.annotate('', arrow_end, arrow_start, arrowprops=dict(width=5, headwidth=10, headlength=5))

Aligning annotated text with colorbar label text

I'd like to find a way to make an annotation that automatically aligns with the label text of a colorbar. Take this example:
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(figsize=(5,10))
data = np.arange(1000, 0, -10).reshape(10, 10)
im = ax.imshow(data, cmap='Blues')
clb = plt.colorbar(im, shrink=0.4)
clb.ax.annotate('text', xy=(1, -0.075), xycoords='axes fraction')
I want to have the last t of "text" to be on the same x coordinate as the last 0 of 1000 in the colorbar label. I can do so manually by adjusting the xy parameter in annotate, but I have to do this for many graphs and would like to find a way to get the parameter from somewhere automatically.
How can I get the maximum x coordinate of the text labes and annotate in a way where the annotation ends on that coordinate? Could someone point me in the right direction? Thanks a lot!
Since the labels are left-aligned, but you want to align your additional text according to the end of that label, I fear there is no other choice than to find out the coordinates from the drawn figure and place the label accordingly.
import matplotlib.pyplot as plt
from matplotlib import transforms
import numpy as np
fig, ax = plt.subplots(figsize=(5,4))
data = np.arange(1000, 0, -10).reshape(10, 10)
im = ax.imshow(data, cmap='Blues')
cbar = plt.colorbar(im)
# draw figure first to be able to retrieve coordinates
fig.canvas.draw()
# get the bounding box of the last label
bbox = cbar.ax.get_yticklabels()[-1].get_window_extent()
# calculate pixels back to axes coords
labx,_ = cbar.ax.transAxes.inverted().transform([bbox.x1,0])
ax.annotate('text', xy=(labx, -0.075), xycoords=cbar.ax.transAxes,
ha = "right")
plt.show()
Note that this approach will fail once you change the figure size afterwards or change the layout in any other way. It should hence always come last in your code.

Polar grid on left hand side of rectangular plot

I am trying to reproduce a plot like this:
So the requirements are actually that the grid (that is to be present just on the left side) behaves just like a grid, that is, if we zoom in and out, it is always there present and not dependent on specific x-y limits for the actual data.
Unfortunately there is no diagonal version of axhline/axvline (open issue here) so I was thinking about using the grid from polar plots.
So for that I have two problems:
This answer shows how to overlay a polar axis on top of a rectangular one, but it does not match the origins and x-y values. How can I do that?
I also tried the suggestion from this answer for having polar plots using ax.set_thetamin/max but I get an AttributeError: 'AxesSubplot' object has no attribute 'set_thetamin' How can I use these functions?
This is the code I used to try to add a polar grid to an already existing rectangular plot on ax axis:
ax_polar = fig.add_axes(ax, polar=True, frameon=False)
ax_polar.set_thetamin(90)
ax_polar.set_thetamax(270)
ax_polar.grid(True)
I was hoping I could get some help from you guys. Thanks!
The mpl_toolkits.axisartist has the option to plot a plot similar to the desired one. The following is a slightly modified version of the example from the mpl_toolkits.axisartist tutorial:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook
from mpl_toolkits.axisartist import SubplotHost, ParasiteAxesAuxTrans
from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear
import mpl_toolkits.axisartist.angle_helper as angle_helper
from matplotlib.projections import PolarAxes
from matplotlib.transforms import Affine2D
# PolarAxes.PolarTransform takes radian. However, we want our coordinate
# system in degree
tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform()
# polar projection, which involves cycle, and also has limits in
# its coordinates, needs a special method to find the extremes
# (min, max of the coordinate within the view).
# 20, 20 : number of sampling points along x, y direction
extreme_finder = angle_helper.ExtremeFinderCycle(20, 20,
lon_cycle=360,
lat_cycle=None,
lon_minmax=None,
lat_minmax=(0, np.inf),)
grid_locator1 = angle_helper.LocatorDMS(36)
tick_formatter1 = angle_helper.FormatterDMS()
grid_helper = GridHelperCurveLinear(tr,
extreme_finder=extreme_finder,
grid_locator1=grid_locator1,
tick_formatter1=tick_formatter1
)
fig = plt.figure(1, figsize=(7, 4))
fig.clf()
ax = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
# make ticklabels of right invisible, and top axis visible.
ax.axis["right"].major_ticklabels.set_visible(False)
ax.axis["right"].major_ticks.set_visible(False)
ax.axis["top"].major_ticklabels.set_visible(True)
# let left axis shows ticklabels for 1st coordinate (angle)
ax.axis["left"].get_helper().nth_coord_ticks = 0
# let bottom axis shows ticklabels for 2nd coordinate (radius)
ax.axis["bottom"].get_helper().nth_coord_ticks = 1
fig.add_subplot(ax)
## A parasite axes with given transform
## This is the axes to plot the data to.
ax2 = ParasiteAxesAuxTrans(ax, tr)
## note that ax2.transData == tr + ax1.transData
## Anything you draw in ax2 will match the ticks and grids of ax1.
ax.parasites.append(ax2)
intp = cbook.simple_linear_interpolation
ax2.plot(intp(np.array([150, 230]), 50),
intp(np.array([9., 3]), 50),
linewidth=2.0)
ax.set_aspect(1.)
ax.set_xlim(-12, 1)
ax.set_ylim(-5, 5)
ax.grid(True, zorder=0)
wp = plt.Rectangle((0,-5),width=1,height=10, facecolor="w", edgecolor="none")
ax.add_patch(wp)
ax.axvline(0, color="grey", lw=1)
plt.show()

How to pick a point in a subplot and highlight it in adjacent subplots in matplotlib(extension to region of points)

I want to create a scatter plot matrix which will be composed by some subplots. I have extracted from a .txt file my data and created an array of shape (x,y,z,p1,p2,p3). The first three columns of the array represent the x,y,z coordinates from the original image that these data come from and the last three columns(p1, p2, p3) some other parameters. Consequently, in each row of the array the parameters p1, p2, p3 have the same coordinates(x,y,z).In the scatter plot, I want to visualize the p1 parameter against the p2, p3 parameters in a first stage. For every point I pick, I would like its (x,y,z) parameters from the first three columns of my array to be annotated and the point with the same coordinates in the adjacent subplot to be highlighted or its color to be modified.
In my code, two subplots are created and in the terminal are printed the (p1,p2 or p3) values that are acquired by picking a point, the respective values of the same point in the adjacent subplot and the (x,y,z) parameters of this point.
Moreover, when I pick a point in the first subplot, the color of the corresponding point in the second subplot changes but not vice versa. This color modification is recognizable only if I resize manually the figure. How could I add interactivity for both subplots without having to tweak the figure in order to notice any changes? What kind of modifications should I make in order this interactivity to be feasible in a reduced scatter plot matrix like in this question "Is there a function to make scatterplot matrices in matplotlib?" . I am not an experienced python, matplotlib user, so any kind of help will be appreciated
import numpy as np
import matplotlib.pyplot as plt
import pylab as pl
def main():
#load data from file
data = np.loadtxt(r"data.txt")
plt.close("all")
x = data[:, 3]
y = data[:, 4]
y1 = data[:, 5]
fig1 = plt.figure(1)
#subplot p1 vs p2
plt.subplot(121)
subplot1, = plt.plot(x, y, 'bo', picker=3)
plt.xlabel('p1')
plt.ylabel('p2')
#subplot p1 vs p3
plt.subplot(122)
subplot2, = plt.plot(x, y1, 'bo', picker=3)
plt.xlabel('p1')
plt.ylabel('p3')
plt.subplots_adjust(left=0.1, right=0.95, wspace=0.3, hspace=0.45)
# art.getp(fig1.patch)
def onpick(event):
thisevent = event.artist
valx = thisevent.get_xdata()
valy = thisevent.get_ydata()
ind = event.ind
print 'index', ind
print 'selected point:', zip(valx[ind], valy[ind])
print 'point in the adjacent subplot', x[ind], y1[ind]
print '(x,y,z):', data[:, 0][ind], data[:, 1][ind], data[:, 2][ind]
for xcord,ycord in zip(valx[ind], valy[ind]):
plt.annotate("(x,y,z):", xy = (x[ind], y1[ind]), xycoords = ('data' ),
xytext=(x[ind] - .5, y1[ind]- .5), textcoords='data',
arrowprops=dict(arrowstyle="->",
connectionstyle="arc3"),
)
subplot2, = plt.plot(x[ind], y[ind], 'ro', picker=3)
subplot1 = plt.plot(x[ind], y[ind], 'ro', picker=3)
fig1.canvas.mpl_connect('pick_event', onpick)
plt.show()
main()
In conclusion, information are printed in the terminal, independently of the subplot, when I pick a point. But, the color is modified only in the points of the right subplot, when I pick a point in the left subplot and not vice versa. Moreover, the change of the color is not noticeable until I tweak the figure(e.g. move it or resize it) and when I choose a second point, the previous one remains colored.
Any kind of contribution will be appreciated. Thank you in advance.
You're already on the right track with your current code. You're basically just missing a call to plt.draw() in your onpick function.
However, in our discussion in the comments, mpldatacursor came up, and you asked about an example of doing things that way.
The current HighlightingDataCursor in mpldatacursor is set up around the idea of highlighting an entire Line2D artist, not just a particular index of it. (It's deliberately a bit limited, as there's no good way to draw an arbitrary highlight for any artist in matplotlib, so I kept the highlighting parts small.)
However, you could subclass things similar to this (assumes you're using plot and want the first thing you plot in each axes to be used). I'm also illustrating using point_labels, in case you want to have different labels for each point shown.:
import numpy as np
import matplotlib.pyplot as plt
from mpldatacursor import HighlightingDataCursor, DataCursor
def main():
fig, axes = plt.subplots(nrows=2, ncols=2)
for ax, marker in zip(axes.flat, ['o', '^', 's', '*']):
x, y = np.random.random((2,20))
ax.plot(x, y, ls='', marker=marker)
IndexedHighlight(axes.flat, point_labels=[str(i) for i in range(20)])
plt.show()
class IndexedHighlight(HighlightingDataCursor):
def __init__(self, axes, **kwargs):
# Use the first plotted Line2D in each axes
artists = [ax.lines[0] for ax in axes]
kwargs['display'] = 'single'
HighlightingDataCursor.__init__(self, artists, **kwargs)
self.highlights = [self.create_highlight(artist) for artist in artists]
plt.setp(self.highlights, visible=False)
def update(self, event, annotation):
# Hide all other annotations
plt.setp(self.highlights, visible=False)
# Highlight everything with the same index.
artist, ind = event.artist, event.ind
for original, highlight in zip(self.artists, self.highlights):
x, y = original.get_data()
highlight.set(visible=True, xdata=x[ind], ydata=y[ind])
DataCursor.update(self, event, annotation)
main()
Again, this assumes you're using plot and not, say, scatter. It is possible to do this with scatter, but you need to change an annoyingly large amount of details. (There's no general way to highlight an arbitrary matplotlib artist, so you have to have a lot of very verbose code to deal with each type of artist individually.)
Hope it's useful, at any rate.

Instead of grid lines on a plot, can matplotlib print grid crosses?

I want to have some grid lines on a plot, but actually full-length lines are too much/distracting, even dashed light grey lines. I went and manually did some editing of the SVG output to get the effect I was looking for. Can this be done with matplotlib? I had a look at the pyplot api for grid, and the only thing I can see that might be able to get near it are the xdata and ydata Line2D kwargs.
This cannot be done through the basic API, because the grid lines are created using only two points. The grid lines would need a 'data' point at every tick mark for there to be a marker drawn. This is shown in the following example:
import matplotlib.pyplot as plt
ax = plt.subplot(111)
ax.grid(clip_on=False, marker='o', markersize=10)
plt.savefig('crosses.png')
plt.show()
This results in:
Notice how the 'o' markers are only at the beginning and the end of the Axes edges, because the grid lines only involve two points.
You could write a method to emulate what you want, creating the cross marks using a series of Artists, but it's quicker to just leverage the basic plotting capabilities to draw the cross pattern.
This is what I do in the following example:
import matplotlib.pyplot as plt
import numpy as np
NPOINTS=100
def set_grid_cross(ax, in_back=True):
xticks = ax.get_xticks()
yticks = ax.get_yticks()
xgrid, ygrid = np.meshgrid(xticks, yticks)
kywds = dict()
if in_back:
kywds['zorder'] = 0
grid_lines = ax.plot(xgrid, ygrid, 'k+', **kywds)
xvals = np.arange(NPOINTS)
yvals = np.random.random(NPOINTS) * NPOINTS
ax1 = plt.subplot(121)
ax2 = plt.subplot(122)
ax1.plot(xvals, yvals, linewidth=4)
ax1.plot(xvals, xvals, linewidth=7)
set_grid_cross(ax1)
ax2.plot(xvals, yvals, linewidth=4)
ax2.plot(xvals, xvals, linewidth=7)
set_grid_cross(ax2, in_back=False)
plt.savefig('gridpoints.png')
plt.show()
This results in the following figure:
As you can see, I take the tick marks in x and y to define a series of points where I want grid marks ('+'). I use meshgrid to take two 1D arrays and make 2 2D arrays corresponding to the double loop over each grid point. I plot this with the mark style as '+', and I'm done... almost. This plots the crosses on top, and I added an extra keyword to reorder the list of lines associated with the plot. I adjust the zorder of the grid marks if they are to be drawn behind everything.*****
The example shows the left subplot where by default the grid is placed in back, and the right subplot disables this option. You can notice the difference if you follow the green line in each plot.
If you are bothered by having grid crosses on the boarder, you can remove the first and last tick marks for both x and y before you define the grid in set_grid_cross, like so:
xticks = ax.get_xticks()[1:-1] #< notice the slicing
yticks = ax.get_yticks()[1:-1] #< notice the slicing
xgrid, ygrid = np.meshgrid(xticks, yticks)
I do this in the following example, using a larger, different marker to make my point:
***** Thanks to the answer by #fraxel for pointing this out.
You can draw on line segments at every intersection of the tickpoints. Its pretty easy to do, just grab the tick locations get_ticklocs() for both axis, then loop through all combinations, drawing short line segments using axhline and axvline, thus creating a cross hair at every intersection. I've set zorder=0 so the cross-hairs are drawn first, so that they are behind the plot data. Its easy to control the color/alpha and cross-hair size. Couple of slight 'gotchas'... do the plot before you get the tick locations.. and also the xmin and xmax parameters seem to require normalisation.
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot((0,2,3,5,5,5,6,7,8,6,6,4,3,32,7,99), 'r-',linewidth=4)
x_ticks = ax.xaxis.get_ticklocs()
y_ticks = ax.yaxis.get_ticklocs()
for yy in y_ticks[1:-1]:
for xx in x_ticks[1:-1]:
plt.axhline(y=yy, xmin=xx / max(x_ticks) - 0.02,
xmax=xx / max(x_ticks) + 0.02, color='gray', alpha=0.5, zorder=0)
plt.axvline(x=xx, ymin=yy / max(y_ticks) - 0.02,
ymax=yy / max(y_ticks) + 0.02, color='gray', alpha=0.5, zorder=0)
plt.show()

Categories