I have created a plot in Python with Pyplot that have multiple subplots.
I would like to draw a line which is not on any of the plots. I know how to draw a line which is part of a plot, but I don't know how to do it on the white space between the plots.
Thank you.
Thank you for the link but I don't want vertical lines between the plots. It is in fact a horizontal line above one of the plots to denote a certain range. Is there not a way to draw an arbitrary line on top of a figure?
First off, a quick way to do this is jut to use axvspan with y-coordinates greater than 1 and clip_on=False. It draws a rectangle rather than a line, though.
As a simple example:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot(range(10))
ax.axvspan(2, 4, 1.05, 1.1, clip_on=False)
plt.show()
For drawing lines, you just specify the transform that you'd like to use as a kwarg to plot (the same applies to most other plotting commands, actually).
To draw in "axes" coordinates (e.g. 0,0 is the bottom left of the axes, 1,1 is the top right), use transform=ax.transAxes, and to draw in figure coordinates (e.g. 0,0 is the bottom left of the figure window, while 1,1 is the top right) use transform=fig.transFigure.
As #tcaswell mentioned, annotate makes this a bit simpler for placing text, and can be very useful for annotations, arrows, labels, etc. You could do this with annotate (by drawing a line between a point and a blank string), but if you just want to draw a line, it's simpler not to.
For what it sounds like you're wanting to do, though, you might want to do things a bit differently.
It's easy to create a transform where the x-coordinates use one transformation and the y-coordinates use a different one. This is what axhspan and axvspan do behind the scenes. It's very handy for something like what you want, where the y-coordinates are fixed in axes coords, and the x-coordinates reflect a particular position in data coords.
The following example illustrates the difference between just drawing in axes coordinates and using a "blended" transform instead. Try panning/zooming both subplots, and notice what happens.
import matplotlib.pyplot as plt
from matplotlib.transforms import blended_transform_factory
fig, (ax1, ax2) = plt.subplots(nrows=2)
# Plot a line starting at 30% of the width of the axes and ending at
# 70% of the width, placed 10% above the top of the axes.
ax1.plot([0.3, 0.7], [1.1, 1.1], transform=ax1.transAxes, clip_on=False)
# Now, we'll plot a line where the x-coordinates are in "data" coords and the
# y-coordinates are in "axes" coords.
# Try panning/zooming this plot and compare to what happens to the first plot.
trans = blended_transform_factory(ax2.transData, ax2.transAxes)
ax2.plot([0.3, 0.7], [1.1, 1.1], transform=trans, clip_on=False)
# Reset the limits of the second plot for easier comparison
ax2.axis([0, 1, 0, 1])
plt.show()
Before panning
After panning
Notice that with the bottom plot (which uses a "blended" transform), the line is in data coordinates and moves with the new axes extents, while the top line is in axes coordinates and stays fixed.
Related
I try to set the line style of matplotlib plot spines, but for some reason, it does not work. I can set them invisible or make them thinner, but I cannot change the line style.
My aim is to present one plot cut up into two to show outliers at the top. I would like to set the respective bottom/top spines to dotted so they clearly show that there is a break.
import numpy as np
import matplotlib.pyplot as plt
# Break ratio of the bottom/top plots respectively
ybreaks = [.25, .9]
figure, (ax1, ax2) = plt.subplots(
nrows=2, ncols=1,
sharex=True, figsize=(22, 10),
gridspec_kw = {'height_ratios':[1 - ybreaks[1], ybreaks[0]]}
)
d = np.random.random(100)
ax1.plot(d)
ax2.plot(d)
# Set the y axis limits
ori_ylim = ax1.get_ylim()
ax1.set_ylim(ori_ylim[1] * ybreaks[1], ori_ylim[1])
ax2.set_ylim(ori_ylim[0], ori_ylim[1] * ybreaks[0])
# Spine formatting
# ax1.spines['bottom'].set_visible(False) # This works
ax1.spines['bottom'].set_linewidth(.25) # This works
ax1.spines['bottom'].set_linestyle('dashed') # This does not work
ax2.spines['top'].set_linestyle('-') # Does not work
ax2.spines['top'].set_linewidth(.25) # Works
plt.subplots_adjust(hspace=0.05)
I would expect the above code to draw the top plot's bottom spine and the bottom plot's top spine dashed.
What do I miss?
First one should mention that if you do not change the linewidth, the dashed style shows fine.
ax1.spines['bottom'].set_linestyle("dashed")
However the spacing may be a bit too tight. This is due to the capstyle being set to "projecting" for spines by default.
One can hence set the capstyle to "butt" instead (which is also the default for normal lines in plots),
ax1.spines['bottom'].set_linestyle('dashed')
ax1.spines['bottom'].set_capstyle("butt")
Or, one can separate the dashes further. E.g.
ax1.spines['bottom'].set_linestyle((0,(4,4)))
Now, if you also set the linewidth so something smaller, then you would need proportionally more spacing. E.g.
ax1.spines['bottom'].set_linewidth(.2)
ax1.spines['bottom'].set_linestyle((0,(16,16)))
Note that the line does not actually become thinner on screen due to the antialiasing in use. It just washes out, such that it becomes lighter in color. So in total it may make sense to keep the lineswidth at some 0.72 points (0.72 points = 1 pixel at 100 dpi) and change the color to light gray instead.
I am starting to play around with creating polar plots in Matplotlib that do NOT encompass an entire circle - i.e. a "wedge" plot - by setting the thetamin and thetamax properties. This is something I was waiting for for a long time, and I am glad they have it done :)
However, I have noticed that the figure location inside the axes seem to change in a strange manner when using this feature; depending on the wedge angular aperture, it can be difficult to fine tune the figure so it looks nice.
Here's an example:
import numpy as np
import matplotlib.pyplot as plt
# get 4 polar axes in a row
fig, axes = plt.subplots(2, 2, subplot_kw={'projection': 'polar'},
figsize=(8, 8))
# set facecolor to better display the boundaries
# (as suggested by ImportanceOfBeingErnest)
fig.set_facecolor('paleturquoise')
for i, theta_max in enumerate([2*np.pi, np.pi, 2*np.pi/3, np.pi/3]):
# define theta vector with varying end point and some data to plot
theta = np.linspace(0, theta_max, 181)
data = (1/6)*np.abs(np.sin(3*theta)/np.sin(theta/2))
# set 'thetamin' and 'thetamax' according to data
axes[i//2, i%2].set_thetamin(0)
axes[i//2, i%2].set_thetamax(theta_max*180/np.pi)
# actually plot the data, fine tune radius limits and add labels
axes[i//2, i%2].plot(theta, data)
axes[i//2, i%2].set_ylim([0, 1])
axes[i//2, i%2].set_xlabel('Magnitude', fontsize=15)
axes[i//2, i%2].set_ylabel('Angles', fontsize=15)
fig.set_tight_layout(True)
#fig.savefig('fig.png', facecolor='skyblue')
The labels are in awkward locations and over the tick labels, but can be moved closer or further away from the axes by adding an extra labelpad parameter to set_xlabel, set_ylabel commands, so it's not a big issue.
Unfortunately, I have the impression that the plot is adjusted to fit inside the existing axes dimensions, which in turn lead to a very awkward white space above and below the half circle plot (which of course is the one I need to use).
It sounds like something that should be reasonably easy to get rid of - I mean, the wedge plots are doing it automatically - but I can't seem to figure it out how to do it for the half circle. Can anyone shed a light on this?
EDIT: Apologies, my question was not very clear; I want to create a half circle polar plot, but it seems that using set_thetamin() you end up with large amounts of white space around the image (especially above and below) which I would rather have removed, if possible.
It's the kind of stuff that normally tight_layout() takes care of, but it doesn't seem to be doing the trick here. I tried manually changing the figure window size after plotting, but the white space simply scales with the changes. Below is a minimum working example; I can get the xlabel closer to the image if I want to, but saved image file still contains tons of white space around it.
Does anyone knows how to remove this white space?
import numpy as np
import matplotlib.pyplot as plt
# get a half circle polar plot
fig1, ax1 = plt.subplots(1, 1, subplot_kw={'projection': 'polar'})
# set facecolor to better display the boundaries
# (as suggested by ImportanceOfBeingErnest)
fig1.set_facecolor('skyblue')
theta_min = 0
theta_max = np.pi
theta = np.linspace(theta_min, theta_max, 181)
data = (1/6)*np.abs(np.sin(3*theta)/np.sin(theta/2))
# set 'thetamin' and 'thetamax' according to data
ax1.set_thetamin(0)
ax1.set_thetamax(theta_max*180/np.pi)
# actually plot the data, fine tune radius limits and add labels
ax1.plot(theta, data)
ax1.set_ylim([0, 1])
ax1.set_xlabel('Magnitude', fontsize=15)
ax1.set_ylabel('Angles', fontsize=15)
fig1.set_tight_layout(True)
#fig1.savefig('fig1.png', facecolor='skyblue')
EDIT 2: Added background color to figures to better show the boundaries, as suggested in ImportanteOfBeingErnest's answer.
It seems the wedge of the "truncated" polar axes is placed such that it sits in the middle of the original axes. There seems so be some constructs called LockedBBox and _WedgeBbox in the game, which I have never seen before and do not fully understand. Those seem to be created at draw time, such that manipulating them from the outside seems somewhere between hard and impossible.
One hack can be to manipulate the original axes such that the resulting wedge turns up at the desired position. This is not really deterministic, but rather looking for some good values by trial and error.
The parameters to adjust in this case are the figure size (figsize), the padding of the labels (labelpad, as already pointed out in the question) and finally the axes' position (ax.set_position([left, bottom, width, height])).
The result could then look like
import numpy as np
import matplotlib.pyplot as plt
# get a half circle polar plot
fig1, ax1 = plt.subplots(1, 1, figsize=(6,3.4), subplot_kw={'projection': 'polar'})
theta_min = 1.e-9
theta_max = np.pi
theta = np.linspace(theta_min, theta_max, 181)
data = (1/6.)*np.abs(np.sin(3*theta)/np.sin(theta/2.))
# set 'thetamin' and 'thetamax' according to data
ax1.set_thetamin(0)
ax1.set_thetamax(theta_max*180./np.pi)
# actually plot the data, fine tune radius limits and add labels
ax1.plot(theta, data)
ax1.set_ylim([0, 1])
ax1.set_xlabel('Magnitude', fontsize=15, labelpad=-60)
ax1.set_ylabel('Angles', fontsize=15)
ax1.set_position( [0.1, -0.45, 0.8, 2])
plt.show()
Here I've set some color to the background of the figure to better see the boundary.
I'm doing a bunch of work with various spherical projection plots using the Astropy WCS package, and have run into some frustrations concerning grid lines. As grid lines do not always intersect with the image bounding box or multiple intersect at the same place, they can go unlabeled or have their labels rendered illegible. I would like to be able to insert grid line labels in each line, much akin to the matplotlib.pyplot.clabel() function applied to contour plots, as in this matplotlib example. I can't embed the image as I am a new user; my apologies.
I know I can place labels using text(), figtext(), or annotate(), but since clabel() works I figure the functionality already exists, even if it hasn't been applied to grid lines. Projection plotting aside, does anyone know a way that in-line grid line labels akin to clabel() can be applied to grid lines on a plain rectangular plot?
To annotate the gridlines, you may use the positions of the major ticks (as those are the positions at which the gridlines are created).
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0,10)
y = np.sin(x)*10
fig, ax = plt.subplots()
ax.plot(x,y)
ax.grid()
for xi in ax.xaxis.get_majorticklocs():
ax.text(xi,0.8, "{:.2f}".format(xi), clip_on=True, ha="center",
transform=ax.get_xaxis_transform(), rotation=90,
bbox={'facecolor':'w', 'pad':1, "edgecolor":"None"})
for yi in ax.yaxis.get_majorticklocs():
ax.text(0.86,yi, "{:.2f}".format(yi), clip_on=True, va="center",
transform=ax.get_yaxis_transform(),
bbox={'facecolor':'w', 'pad':1, "edgecolor":"None"})
plt.show()
This question already has answers here:
Is there a way to make a discontinuous axis in Matplotlib?
(7 answers)
Closed 5 years ago.
Best way to describe what I want to achieve is using my own image:
Now I have a lot of dead space in the spectra plot, especially between 5200 and 6300. My question is quite simple, how would I add in a nice little // break that looks something similar to this (image lifted from the net):
I'm using this setup for my plots:
nullfmt = pyplot.NullFormatter()
fig = pyplot.figure(figsize=(16,6))
gridspec_layout1= gridspec.GridSpec(2,1)
gridspec_layout1.update(left=0.05, right=0.97, hspace=0, wspace=0.018)
pyplot_top = fig.add_subplot(gridspec_layout1[0])
pyplot_bottom = fig.add_subplot(gridspec_layout1[1])
pyplot_top.xaxis.set_major_formatter(nullfmt)
I'm quite certain it is achievable with gridpsec but an advanced tutorial cover exactly how this is achieved would be greatly appreciated.
Apologies also if this question has been dealt with previously on stackoverflow but I have looked extensively for the correct procedure for gridSpec but found nothing as yet.
I have managed to go as far as this, pretty much there:
However, my break lines are not as steep as I would like them...how do I change them? (I have made use of the example answer below)
You could adapt the matplotlib example for a break in the x-axis directly:
"""
Broken axis example, where the x-axis will have a portion cut out.
"""
import matplotlib.pylab as plt
import numpy as np
x = np.linspace(0,10,100)
x[75:] = np.linspace(40,42.5,25)
y = np.sin(x)
f,(ax,ax2) = plt.subplots(1,2,sharey=True, facecolor='w')
# plot the same data on both axes
ax.plot(x, y)
ax2.plot(x, y)
ax.set_xlim(0,7.5)
ax2.set_xlim(40,42.5)
# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labelright='off')
ax2.yaxis.tick_right()
# This looks pretty good, and was fairly painless, but you can get that
# cut-out diagonal lines look with just a bit more work. The important
# thing to know here is that in axes coordinates, which are always
# between 0-1, spine endpoints are at these locations (0,0), (0,1),
# (1,0), and (1,1). Thus, we just need to put the diagonals in the
# appropriate corners of each of our axes, and so long as we use the
# right transform and disable clipping.
d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((1-d,1+d), (-d,+d), **kwargs)
ax.plot((1-d,1+d),(1-d,1+d), **kwargs)
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d,+d), (1-d,1+d), **kwargs)
ax2.plot((-d,+d), (-d,+d), **kwargs)
# What's cool about this is that now if we vary the distance between
# ax and ax2 via f.subplots_adjust(hspace=...) or plt.subplot_tool(),
# the diagonal lines will move accordingly, and stay right at the tips
# of the spines they are 'breaking'
plt.show()
For your purposes, just plot your data twice (once on each axis, ax and ax2 and set your xlims appropriately. The "break lines" should move to match the new break because they are plotted in relative axis coordinates rather than data coordinates.
The break lines are just unclipped plot lines drawn between a pair of points. E.g. ax.plot((1-d,1+d), (-d,+d), **kwargs) plots the break line between point (1-d,-d) and (1+d,+d) on the first axis: this is the bottom righthand one. If you want to change the graident, change these values appropriately. For example, to make this one steeper, try ax.plot((1-d/2,1+d/2), (-d,+d), **kwargs)
The solution provided by xnx is a good start, but there is a remaining issue that the scales of the x-axes are different between the plots. This is not a problem if the range in the left plot and the range in the right plot are the same, but if they are unequal, subplot will still give the two plots equal width, so the x-axis scale will be different between the two plots (as is the case with xnx's example). I made a package, brokenaxes to deal with this.
This question already has answers here:
How to put the legend outside the plot
(18 answers)
Closed 5 years ago.
I am trying to use the keyword bbox_to_anchor() in a matplotlib plot in Python.
Here is a very basic plot that I have produced based on this example. :
import matplotlib.pyplot as plt
x = [1,2,3]
plt.subplot(211)
plt.plot(x, label="test1")
plt.plot([3,2,1], label="test2")
plt.legend(bbox_to_anchor=(0, -0.15, 1, 0), loc=2, ncol=2, mode="expand", borderaxespad=0)
plt.show()
I am trying to automatically place the legend outside the plot using bbox_to_anchor(). In this example, bbox_to_anchor() has 4 arguments listed.
In this particular example (above), the legend is placed below the plot so the number -0.15 needs to be manually entered each time a plot is changed (font size, axis title removed, etc.).
Is it possible to automatically calculate these 4 numbers for the following scenarios?:
legend below plot
legend above plot
legend to right of plot
If not, is it possible to make good guesses about these numbers, in Python?
Also, in the example code above I have set the last 2 numbers in bbox_to_anchor() to 1 and 0 since I do not understand what they are or how they work. What do the last 2 numbers in bbox_to_anchor() mean?
EDIT:
I HIGHLY RECOMMEND USING THE ANSWER FROM ImportanceOfBeingErnest:
How to put the legend outside the plot
EDIT END
This one is easier to understand:
import matplotlib.pyplot as plt
x = [1,2,3]
plt.subplot(211)
plt.plot(x, label="test1")
plt.plot([3,2,1], label="test2")
plt.legend(bbox_to_anchor=(0, 1), loc='upper left', ncol=1)
plt.show()
now play with the to coordinates (x,y). For loc you can use:
valid locations are:
right
center left
upper right
lower right
best
center
lower left
center right
upper left
upper center
lower center
The argument to bbox_to_anchor is in Axes Coordinates. matplotlib uses different coordinate systems to ease placement of objects on the screen. When dealing with positioning legends, the critical coordinate systems to deal with are Axes coordinates, Figure coordinates, and Display coordinates (in pixels) as shown below:
matplotlib coordinate systems
As previously mentioned, bbox_to_anchor is in Axes coordinates and does not require all 4 tuple arguments for a rectangle. You can simply give it a two-argument tuple containing (xpos, ypos) in Axes coordinates. The loc argument in this case will define the anchor point for the legend. So to pin the legend to the outer right of the axes and aligned with the top edge, you would issue the following:
lgd = plt.legend(bbox_to_anchor=(1.01, 1), loc='upper left')
This however does not reposition the Axes with respect to the Figure and this will likely position the legend off of the Figure canvas. To automatically reposition the Figure canvas to align with the Axes and legend, I have used the following algorithm.
First, draw the legend on the canvas to assign it real pixel coordinates:
plt.gcf().canvas.draw()
Then define the transformation to go from pixel coordinates to Figure coordinates:
invFigure = plt.gcf().transFigure.inverted()
Next, get the legend extents in pixels and convert to Figure coordinates. Pull out the farthest extent in the x direction since that is the canvas direction we need to adjust:
lgd_pos = lgd.get_window_extent()
lgd_coord = invFigure.transform(lgd_pos)
lgd_xmax = lgd_coord[1, 0]
Do the same for the Axes:
ax_pos = plt.gca().get_window_extent()
ax_coord = invFigure.transform(ax_pos)
ax_xmax = ax_coord[1, 0]
Finally, adjust the Figure canvas using tight_layout for the proportion of the Axes that must move over to allow room for the legend to fit within the canvas:
shift = 1 - (lgd_xmax - ax_xmax)
plt.gcf().tight_layout(rect=(0, 0, shift, 1))
Note that the rect argument to tight_layout is in Figure coordinates and defines the lower left and upper right corners of a rectangle containing the tight_layout bounds of the Axes, which does not include the legend. So a simple tight_layout call is equivalent to setting rect bounds of (0, 0, 1, 1).