Precise control over subplot locations in matplotlib - python

I am currently producing a figure for a paper, which looks like this:
The above is pretty close to how I want it to look, but I have a strong feeling that I'm not doing this the "right way", since it was really fiddly to produce, and my code is full of all sorts of magic numbers where I fine-tuned the positioning by hand. Thus my question is, what is the right way to produce a plot like this?
Here are the important features of this plot that made it hard to produce:
The aspect ratios of the three subplots are fixed by the data, but the images are not all at the same resolution.
I wanted all three plots to take up the full height of the figure
I wanted (a) and (b) to be close together since they share their y axis, while (c) is further away
Ideally, I would like the top of the top colour bar to exactly match the top of the three images, and similarly with the bottom of the lower colour bar. (In fact they aren't quite aligned, because I did this by guessing numbers and re-compiling the image.)
In producing this figure, I first tried using GridSpec, but I wasn't able to control the relative spacing between the three main subplots. I then tried ImageGrid, which is part of the AxisGrid toolkit, but the differing resolutions between the three images caused that to behave strangely. Delving deeper into AxesGrid, I was able to position the three main subplots using the append_axes function, but I still had to position the three colourbars by hand. (I created the colourbars manually.)
I'd rather not post my existing code, because it's a horrible collection of hacks and magic numbers. Rather my question is, is there any way in MatPlotLib to just specify the logical layout of the figure (i.e. the content of the bullet points above) and have the layout calculated for me automatically?

Here is a possible solution. You'd start with the figure width (which makes sense when preparing a paper) and calculate your way through, using the aspects of the figures, some arbitrary spacings between the subplots and the margins. The formulas are similar to the ones I used in this answer. And the unequal aspects are taken care of by GridSpec's width_ratios argument.
You then end up with a figure height such that the subplots' are equal in height.
So you cannot avoid typing in some numbers, but they are not "magic". All are related to acessible quatities like fraction of figure size or fraction of mean subplots size. Since the system is closed, changing any number will simply produce a different figure height, but will not destroy the layout.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np; np.random.seed(42)
imgs = []
shapes = [(550,200), ( 550,205), (1100,274) ]
for shape in shapes:
imgs.append(np.random.random(shape))
# calculate inverse aspect(width/height) for all images
inva = np.array([ img.shape[1]/float(img.shape[0]) for img in imgs])
# set width of empty column used to stretch layout
emptycol = 0.02
r = np.array([inva[0],inva[1], emptycol, inva[2], 3*emptycol, emptycol])
# set a figure width in inch
figw = 8
# border, can be set independently of all other quantities
left = 0.1; right=1-left
bottom=0.1; top=1-bottom
# wspace (=average relative space between subplots)
wspace = 0.1
#calculate scale
s = figw*(right-left)/(len(r)+(len(r)-1)*wspace)
# mean aspect
masp = len(r)/np.sum(r)
#calculate figheight
figh = s*masp/float(top-bottom)
gs = gridspec.GridSpec(3,len(r), width_ratios=r)
fig = plt.figure(figsize=(figw,figh))
plt.subplots_adjust(left, bottom, right, top, wspace)
ax1 = plt.subplot(gs[:,0])
ax2 = plt.subplot(gs[:,1])
ax2.set_yticks([])
ax3 = plt.subplot(gs[:,3])
ax3.yaxis.tick_right()
ax3.yaxis.set_label_position("right")
cax1 = plt.subplot(gs[0,5])
cax2 = plt.subplot(gs[1,5])
cax3 = plt.subplot(gs[2,5])
im1 = ax1.imshow(imgs[0], cmap="viridis")
im2 = ax2.imshow(imgs[1], cmap="plasma")
im3 = ax3.imshow(imgs[2], cmap="RdBu")
fig.colorbar(im1, ax=ax1, cax=cax1)
fig.colorbar(im2, ax=ax2, cax=cax2)
fig.colorbar(im3, ax=ax3, cax=cax3)
ax1.set_title("image title")
ax1.set_xlabel("xlabel")
ax1.set_ylabel("ylabel")
plt.show()

Related

How to position suptitle

I'm trying to adjust a suptitle above a multi-panel figure and am having trouble figuring out how to adjust the figsize and subsequently position the suptitle.
The problem is that calling plt.suptitle("my title", y=...) to adjust the position of the suptitle also adjusts the figure dimensions. A few questions:
where does suptitle(..., y=1.1) actually put the title? As far as I can tell, the documentation for the y parameter of suptitle points to matplotlib.text.Text, but I don't know what figure coordinates mean when you have multiple subplots.
what is the effect on figure size when specifying y to suptitle?
how do I manually adjust figure size and spacing (subplots_adjust?) to add a figure title per panel and a suptitle for the entire figure, maintaining the size of each ax in the figure?
An example:
data = np.random.random(size=100)
f, a = plt.subplots(2, 2, figsize=(10, 5))
a[0,0].plot(data)
a[0,0].set_title("this is a really long title\n"*2)
a[0,1].plot(data)
a[1,1].plot(data)
plt.suptitle("a big long suptitle that runs into the title\n"*2, y=1.05);
Obviously I can tweak y each time I make a figure, but I need a solution that generally works without manual intervention. I've tried both constrained layout and tight layout; neither works reliably with figures of any complexity.
1. What do figure coordinates mean?
Figure coordinates go 0 to 1, where (0,0) is the lower left corner and (1,1) is the upper right corner. A coordinate of y=1.05 is hence slightly outside the figure.
2. what is the effect on figure size when specifying y to suptitle?
Specifying y to suptitle has no effect whatsoever on the figure size.
3a. How do I manually adjust figure size and spacing to add a figure title per panel and a suptitle for the entire figure?
First, one would not add an additional linebreak. I.e. if you want to have 2 lines, don't use 3 linebreaks (\n). Then one can adjust the subplot parameters as desired to leave space for the titles. E.g. fig.subplots_adjust(top=0.8) and use a y <= 1 for the title to be inside the figure.
import matplotlib.pyplot as plt
import numpy as np
data = np.random.random(size=100)
fig, axes = plt.subplots(2, 2, figsize=(10, 5))
fig.subplots_adjust(top=0.8)
axes[0,0].plot(data)
axes[0,0].set_title("\n".join(["this is a really long title"]*2))
axes[0,1].plot(data)
axes[1,1].plot(data)
fig.suptitle("\n".join(["a big long suptitle that runs into the title"]*2), y=0.98)
plt.show()
3b. ... while maintaining the size of each ax in the figure?
Maintaining the size of the axes and still have enough space for the titles is only possible by changing the overall figure size.
This could look as follows, where we define a function make_space_above which takes the array of axes as input, as well as the newly desired top margin in units of inches. So for example, you come to the conclusion that you need 1 inch of margin on top to host your titles:
import matplotlib.pyplot as plt
import numpy as np
data = np.random.random(size=100)
fig, axes = plt.subplots(2, 2, figsize=(10, 5), squeeze = False)
axes[0,0].plot(data)
axes[0,0].set_title("\n".join(["this is a really long title"]*2))
axes[0,1].plot(data)
axes[1,1].plot(data)
fig.suptitle("\n".join(["a big long suptitle that runs into the title"]*2), y=0.98)
def make_space_above(axes, topmargin=1):
""" increase figure size to make topmargin (in inches) space for
titles, without changing the axes sizes"""
fig = axes.flatten()[0].figure
s = fig.subplotpars
w, h = fig.get_size_inches()
figh = h - (1-s.top)*h + topmargin
fig.subplots_adjust(bottom=s.bottom*h/figh, top=1-topmargin/figh)
fig.set_figheight(figh)
make_space_above(axes, topmargin=1)
plt.show()
(left: without calling make_space_above; right: with call to make_space_above(axes, topmargin=1))
Short Answer
For those coming from Google for adjusting the title position on a scatter matrix, you can simply set the y parameter to a value slightly lower than 1:
plt.suptitle('My Title', y=0.92)
... or use constrained_layout:
import matplotlib.pyplot as plt
import numpy as np
data = np.random.random(size=100)
f, a = plt.subplots(2, 2, figsize=(10, 5), constrained_layout=True)
a[0,0].plot(data)
a[0,0].set_title("this is a really long title\n"*2)
a[0,1].plot(data)
a[1,1].plot(data)
plt.suptitle("a big long suptitle that runs into the title\n"*2);
A bit of a hacky solution, but if your plots only have 1 column, perhaps consider just add the main title to the title of the first plot, like so:
ax[0].set_title("Main Title\nFirst Plot")

How can I adjust Axes sizes in matplotlib polar plots? [duplicate]

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.

imshow() subplots generate unwanted white spaces

When plotting two (or more) subplots, there is a large areas of white spaces within the plots (on all four sides) as seen here:
Following is the code which I used to plot it.
from pylab import *
from matplotlib import rc, rcParams
import matplotlib.pyplot as plt
for kk in range(57,58):
fn_i=str(kk)
image_file_1='RedshiftOutput00'+fn_i+'_Slice_z_RadioPowerDSA.png'
image_file_2='RedshiftOutput00'+fn_i+'_Slice_z_RadioPowerTRA.png'
image_file_3='RedshiftOutput00'+fn_i+'_Slice_z_RadioPowerDSA+TRA.png'
image_1 = plt.imread(image_file_1)
image_2 = plt.imread(image_file_2)
image_3 = plt.imread(image_file_3)
ax1 = subplot(131)
plt.imshow(image_1)
plt.axis('off') # clear x- and y-axes
ax2 = subplot(132)
plt.imshow(image_2)
plt.axis('off') # clear x- and y-axes
ax3 = subplot(133)
plt.imshow(image_3)
plt.axis('off') # clear x- and y-axes
plt.savefig('RedshiftOutput00'+fn_i+'_all.png')
I am also uploading the 3 images used in this code to making the code a Minimal Working Example
1) https://drive.google.com/file/d/0B6l5iRWTUbHWSTF2R3E1THBGeVk/view?usp=sharing
2) https://drive.google.com/file/d/0B6l5iRWTUbHWaFI4dHAzcWpiOEU/view?usp=sharing
3) https://drive.google.com/file/d/0B6l5iRWTUbHWaG8xclFlcGJNaUk/view?usp=sharing
How we can remove this white space ? I tried by fixing the whole plot size, still white space is comming.
Mel's comment above (use plt.tight_layout()) works in many situations, but sometimes you need a little more control. To manipulate axes more finely (useful, e.g., when you have lots of colorbars or twin-ned axes), you can use plt.subplots_adjust() or a GridSpec object.
GridSpec objects allow you to specify the horizontal and vertical extents of individual axes, as well as their proportional width and height & spacing. subplots_adjust() moves your axes around after you've already plotted stuff on them. I prefer using the first option, but both are documented well.
It also may help to fool around with the size of your figure. If you have lots of whitespace width-wise, make the width of the figure smaller.
Here's some example code that I used to set up a recent plot:
gs = gridspec.GridSpec(
nrows=1, ncols=3, left=0.1, bottom=0.25, right=0.95, top=0.95,
wspace=0.05, hspace=0., width_ratios=[1, 1, 1])
NII_ax = plt.subplot(gs[0])
SII_ax = plt.subplot(gs[1])
OI_ax = plt.subplot(gs[2])
And the result:
Then, if you need a colorbar, adjust the right argument in GridSpec to something like 0.85, and use fig.add_axes() with a list [left_lim, bottom, width, height] and use that as the axis argument for a fig.colorbar()

Changing aspect ratio of subplots in matplotlib

I have created a series of simple greyscale images which I have plotted in a grid (unfortunately, can't upload an image because I don't have a high enough reputation :( ).
The pseudo-code is
# Define matplotlib PyPlot object
nrow = 8
ncol = 12
fig, axes = plt.subplots(nrow, ncol, subplot_kw={'xticks': [], 'yticks': []})
fig.subplots_adjust(hspace=0.05, wspace=0.05)
# Sample the fine scale model at random well locations
for ax in axes.flat:
plot_data = # some Python code here to create 2D grey scale array...
# ... create sub-plot
img = ax.imshow(plot_data, interpolation='none')
img.set_cmap('gray')
# Display the plot
plt.show()
I want to change the aspect ratio so that the plots are squashed vertically and stretched horizontally. I have tried using ax.set_aspect and passing 'aspect' as a subplot_kw argument but to no avail. I also switched 'autoscale' off but I can then only see a handful of pixels. All suggestions welcome!
Thanks in advance!!
#JoeKington - thank you! That was a great reply!! Still trying to get my head around it all. Thanks also to the other posters for their suggestions. So, the original plot looked like this: http://imgur.com/Wi6v4cs
When I set' aspect='auto'' the plot looks like this: http://imgur.com/eRBO6MZ
which is a big improvement. All I need to do now is adjust the subplot size so that sub-plots are plotted in a portrait aspect ratio of eg 2:1, but with the plot filling the entire sub-plot. I guess 'colspan' would do this?
The Short Answer
You're probably wanting to call:
ax.imshow(..., aspect='auto')
imshow will set the aspect ratio of the axes to 1 when it is called, by default. This will override any aspect you specify when you create the axes.
However, this is a common source of confusion in matplotlib. Let me back up and explain what's going on in detail.
Matplotlib's Layout Model
aspect in matplotlib refers to the ratio of the xscale and yscale in data coordinates. It doesn't directly control the ratio of the width and height of the axes.
There are three things that control the size and shape of the "outside box" of a matplotlib axes:
The size/shape of the Figure (shown in red in figures below)
The specified extent of the Axes in figure coordinates (e.g. the subplot location, shown in green in figures below)
The mechanism that the Axes uses to accommodate a fixed aspect ratio (the adjustable parameter).
Axes are always placed in figure coordinates in other words, their shape/size is always a ratio of the figure's shape/size. (Note: Some things such as axes_grid will change this at draw time to get around this limitation.)
However, the extent the axes is given (either from its subplot location or explicitly set extent) isn't necessarily the size it will take up. Depending on the aspect and adjustable parameters, the Axes will shrink inside of its given extent.
To understand how everything interacts, let's plot a circle in lots of different cases.
No Fixed Aspect
In the basic case (no fixed aspect ratio set for the axes), the axes will fill up the entire space allocated to it in figure coordinates (shown by the green box).
The x and y scales (as set by aspect) will be free to change independently, distorting the circle:
When we resize the figure (interactively or at figure creation), the axes will "squish" with it:
Fixed Aspect Ratio, adjustable='box'
However, if the aspect ratio of the plot is set (imshow will force the aspect ratio to 1, by default), the Axes will adjust the size of the outside of the axes to keep the x and y data ratios at the specified aspect.
A key point to understand here, though, is that the aspect of the plot is the aspect of the x and y data scales. It's not the aspect of the width and height of the plot. Therefore, if the aspect is 1, the circle will always be a circle.
As an example, let's say we had done something like:
fig, ax = plt.subplots()
# Plot circle, etc, then:
ax.set(xlim=[0, 10], ylim=[0, 20], aspect=1)
By default, adjustable will be "box". Let's see what happens:
The maximum space the Axes can take up is shown by the green box. However, it has to maintain the same x and y scales. There are two ways this could be accomplished: Change the x and y limits or change the shape/size of the Axes bounding box. Because the adjustable parameter of the Axes is set to the default "box", the Axes shrinks inside of its maximum space.
And as we resize the figure, it will keep shrinking, but maintain the x and y scales by making the Axes use up less of the maximum space allocated to the axes (green box):
Two quick side-notes:
If you're using shared axes, and want to have adjustable="box", use adjustable="box-forced" instead.
If you'd like to control where the axes is positioned inside of the "green box" set the anchor of the axes. E.g. ax.set_anchor('NE') to have it remain "pinned" to the upper right corner of the "green box" as it adjusts its size to maintain the aspect ratio.
Fixed Aspect, adjustable="datalim"
The other main option for adjustable is "datalim".
In this case, matplotlib will keep the x and y scales in data space by changing one of the axes limits. The Axes will fill up the entire space allocated to it. However, if you manually set the x or y limits, they may be overridden to allow the axes to both fill up the full space allocated to it and keep the x/y scale ratio to the specified aspect.
In this case, the x limits were set to 0-10 and the y-limits to 0-20, with aspect=1, adjustable='datalim'. Note that the y-limit was not honored:
And as we resize the figure, the aspect ratio says the same, but the data limits change (in this case, the x-limit is not honored).
On a side note, the code to generate all of the above figures is at: https://gist.github.com/joferkington/4fe0d9164b5e4fe1e247
What does this have to do with imshow?
When imshow is called, it calls ax.set_aspect(1.0), by default. Because adjustable="box" by default, any plot with imshow will behave like the 3rd/4th images above.
For example:
However, if we specify imshow(..., aspect='auto'), the aspect ratio of the plot won't be overridden, and the image will "squish" to take up the full space allocated to the Axes:
On the other hand, if you wanted the pixels to remain "square" (note: they may not be square depending on what's specified by the extent kwarg), you can leave out the aspect='auto' and set the adjustable parameter of the axes to "datalim" instead.
E.g.
ax.imshow(data, cmap='gist_earth', interpolation='none')
ax.set(adjustable="datalim")
Axes Shape is Controlled by Figure Shape
The final part to remember is that the axes shape/size is defined as a percentage of the figure's shape/size.
Therefore, if you want to preserve the aspect ratio of the axes and have a fixed spacing between adjacent subplots, you'll need to define the shape of the figure to match. plt.figaspect is extremely handy for this. It simply generates a tuple of width, height based on a specified aspect ratio or a 2D array (it will take the aspect ratio from the array's shape, not contents).
For your example of a grid of subplots, each with a constant 2x1 aspect ratio, you might consider something like the following (note that I'm not using aspect="auto" here, as we want the pixels in the images to remain square):
import numpy as np
import matplotlib.pyplot as plt
nrows, ncols = 8, 12
dx, dy = 1, 2
figsize = plt.figaspect(float(dy * nrows) / float(dx * ncols))
fig, axes = plt.subplots(nrows, ncols, figsize=figsize)
for ax in axes.flat:
data = np.random.random((10*dy, 10*dx))
ax.imshow(data, interpolation='none', cmap='gray')
ax.set(xticks=[], yticks=[])
pad = 0.05 # Padding around the edge of the figure
xpad, ypad = dx * pad, dy * pad
fig.subplots_adjust(left=xpad, right=1-xpad, top=1-ypad, bottom=ypad)
plt.show()

Subplots: tight_layout changes figure size

Changing the vertical distance between two subplot using tight_layout(h_pad=-1) changes the total figuresize. How can I define the figuresize using tight_layout?
Here is the code:
#define figure
pl.figure(figsize=(10, 6.25))
ax1=subplot(211)
img=pl.imshow(np.random.random((10,50)), interpolation='none')
ax1.set_xticklabels(()) #hides the tickslabels of the first plot
subplot(212)
x=linspace(0,50)
pl.plot(x,x,'k-')
xlim( ax1.get_xlim() ) #same x-axis for both plots
And here is the results:
If I write
pl.tight_layout(h_pad=-2)
in the last line, then I get this:
As you can see, the figure is bigger...
You can use a GridSpec object to control precisely width and height ratios, as answered on this thread and documented here.
Experimenting with your code, I could produce something like what you want, by using a height_ratio that assigns twice the space to the upper subplot, and increasing the h_pad parameter to the tight_layout call. This does not sound completely right, but maybe you can adjust this further ...
import numpy as np
from matplotlib.pyplot import *
import matplotlib.pyplot as pl
import matplotlib.gridspec as gridspec
#define figure
fig = pl.figure(figsize=(10, 6.25))
gs = gridspec.GridSpec(2, 1, height_ratios=[2,1])
ax1=subplot(gs[0])
img=pl.imshow(np.random.random((10,50)), interpolation='none')
ax1.set_xticklabels(()) #hides the tickslabels of the first plot
ax2=subplot(gs[1])
x=np.linspace(0,50)
ax2.plot(x,x,'k-')
xlim( ax1.get_xlim() ) #same x-axis for both plots
fig.tight_layout(h_pad=-5)
show()
There were other issues, like correcting the imports, adding numpy, and plotting to ax2 instead of directly with pl. The output I see is this:
This case is peculiar because of the fact that the default aspect ratios of images and plots are not the same. So it is worth noting for people looking to remove the spaces in a grid of subplots consisting of images only or of plots only that you may find an appropriate solution among the answers to this question (and those linked to it): How to remove the space between subplots in matplotlib.pyplot?.
The aspect ratios of the subplots in this particular example are as follows:
# Default aspect ratio of images:
ax1.get_aspect()
# 1.0
# Which is as it is expected based on the default settings in rcParams file:
matplotlib.rcParams['image.aspect']
# 'equal'
# Default aspect ratio of plots:
ax2.get_aspect()
# 'auto'
The size of ax1 and the space beneath it are adjusted automatically based on the number of pixels along the x-axis (i.e. width) so as to preserve the 'equal' aspect ratio while fitting both subplots within the figure. As you mentioned, using fig.tight_layout(h_pad=xxx) or the similar fig.set_constrained_layout_pads(hspace=xxx) is not a good option as this makes the figure larger.
To remove the gap while preserving the original figure size, you can use fig.subplots_adjust(hspace=xxx) or the equivalent plt.subplots(gridspec_kw=dict(hspace=xxx)), as shown in the following example:
import numpy as np # v 1.19.2
import matplotlib.pyplot as plt # v 3.3.2
np.random.seed(1)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6.25),
gridspec_kw=dict(hspace=-0.206))
# For those not using plt.subplots, you can use this instead:
# fig.subplots_adjust(hspace=-0.206)
size = 50
ax1.imshow(np.random.random((10, size)))
ax1.xaxis.set_visible(False)
# Create plot of a line that is aligned with the image above
x = np.arange(0, size)
ax2.plot(x, x, 'k-')
ax2.set_xlim(ax1.get_xlim())
plt.show()
I am not aware of any way to define the appropriate hspace automatically so that the gap can be removed for any image width. As stated in the docstring for fig.subplots_adjust(), it corresponds to the height of the padding between subplots, as a fraction of the average axes height. So I attempted to compute hspace by dividing the gap between the subplots by the average height of both subplots like this:
# Extract axes positions in figure coordinates
ax1_x0, ax1_y0, ax1_x1, ax1_y1 = np.ravel(ax1.get_position())
ax2_x0, ax2_y0, ax2_x1, ax2_y1 = np.ravel(ax2.get_position())
# Compute negative hspace to close the vertical gap between subplots
ax1_h = ax1_y1-ax1_y0
ax2_h = ax2_y1-ax2_y0
avg_h = (ax1_h+ax2_h)/2
gap = ax1_y0-ax2_y1
hspace=-(gap/avg_h) # this divided by 2 also does not work
fig.subplots_adjust(hspace=hspace)
Unfortunately, this does not work. Maybe someone else has a solution for this.
It is also worth mentioning that I tried removing the gap between subplots by editing the y positions like in this example:
# Extract axes positions in figure coordinates
ax1_x0, ax1_y0, ax1_x1, ax1_y1 = np.ravel(ax1.get_position())
ax2_x0, ax2_y0, ax2_x1, ax2_y1 = np.ravel(ax2.get_position())
# Set new y positions: shift ax1 down over gap
gap = ax1_y0-ax2_y1
ax1.set_position([ax1_x0, ax1_y0-gap, ax1_x1, ax1_y1-gap])
ax2.set_position([ax2_x0, ax2_y0, ax2_x1, ax2_y1])
Unfortunately, this (and variations of this) produces seemingly unpredictable results, including a figure resizing similar to when using fig.tight_layout(). Maybe someone else has an explanation for what is happening here behind the scenes.

Categories