Python/Matplotlib: controlling the aspect ratio in gridspec - python

I am trying to control the aspect ratio of 4 x 1 grid using GridSpec. I want very wide plots in the horizontal direction, and compact in the vertical direction, but I can't change the aspect ratio predictably (see first pic). All three settings of the 'height_ratios' below are giving me the same aspect ratio (see second pic below):
One:
gs = gridspec.GridSpec(4, 1, width_ratios=[1], height_ratios=[0.1, 0.1, 0.1, 0.1])
gs.update(wspace = 0, hspace = 0)
ax1 = plt.subplot(gs[0])
ax2 = plt.subplot(gs[1])
ax3 = plt.subplot(gs[2])
ax4 = plt.subplot(gs[3])
plt.show()
Two:
gs = gridspec.GridSpec(4, 1, width_ratios=[1], height_ratios=[1, 1, 1, 1])
Three:
gs = gridspec.GridSpec(4, 1, width_ratios=[1], height_ratios=[0.5, 0.5, 0.5, 0.5])
I was able to get elongated plots, like I want, by doing this:
ax1.set_aspect(0.1)
ax2.set_aspect(0.1)
ax3.set_aspect(0.1)
ax4.set_aspect(0.1)
But this adds space between the subplots, which I don't want, and I removed using hspace = 0. How do I control the aspect ratio without adding space between the subplots?
This is what I want, but I can't seem to get it again and I'm not sure why:
Instead, all I get is this:
This is what I get using ax.set_aspect(0.1), which has the correct aspect ratio, but it introduces space between the plots, which I don't want:

You need to set the figure size so you'll get the aspect ratio you want, which is controlled through the figsize parameter of plt.figure. It's not clear that you actually need GridSpec in this example; I would do:
import matplotlib.pyplot as plt
f, axes = plt.subplots(4, 1, figsize=(10, 4))
f.subplots_adjust(hspace=0)
If you do need to use GridSpec for a more complicated layout, you can make the figure with plt.figure and then pass the grid slices to the add_subplot method of the object that is returned.

Related

Fixed ylabel space (aligned y-axis) across multiple figures

I'm using code much like:
import matplotlib.pyplot as plt
labels = ['AAA', 'BBBBBBBBBBBBB', 'CCCCCC', 'DDDDDDDDDD']
values = [0, 2, 2, 5]
fig, ax = plt.subplots(figsize=(8, 0.07 + 0.25 * len(values)))
bars = ax.barh(labels, values, color=colors)
to generate horizontal bar plots as separate figures, one after another:
How can I make the left spines (i.e. the black bars) align when the width of labels varies between plots? (Aside from just aligning the rendered images to the right.)
I think the left margin/padding/space should be fixed, or the bar width should be fixed, but I can't quite figure how to do it.
In these cases, I just add empty axes at the left edge of each figure. I'm sure there are more sophisticated ways, but I find this to be simplest:
fig1 with blank axes at left location
fig1, ax = plt.subplots(figsize=(8, 1))
ax.barh(['AAA', 'BBBBBBBBBBBBB', 'CCCCCC', 'DDDDDDDDDD'], [0, 2, 2, 5])
# add empty axes at `left` location (unit: fraction of figure width)
left = -0.05 # requires manual adjustment
fig1.add_axes([left, 0, 0, 0.01]).axis('off')
plt.show()
fig2 with blank axes at same left location as fig1
fig2, ax = plt.subplots(figsize=(8, 1))
ax.barh(['AAaaaA', 'BBBB', 'CCCCCC', 'DDDDD'], [2, 8, 7, 1])
# add empty axes at same `left` location as fig1
fig2.add_axes([left, 0, 0, 0.01]).axis('off')
plt.show()
Output of fig1 and fig2:
A similar approach would be to annotate a space character at the left of each figure:
ax.annotate(' ', (left, 0), xycoords='axes fraction', annotation_clip=False)

How to have a common y-label between two subplots? [duplicate]

I have the following plot:
fig,ax = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
and now I would like to give this plot common x-axis labels and y-axis labels. With "common", I mean that there should be one big x-axis label below the whole grid of subplots, and one big y-axis label to the right. I can't find anything about this in the documentation for plt.subplots, and my googlings suggest that I need to make a big plt.subplot(111) to start with - but how do I then put my 5*2 subplots into that using plt.subplots?
This looks like what you actually want. It applies the same approach of this answer to your specific case:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(nrows=3, ncols=3, sharex=True, sharey=True, figsize=(6, 6))
fig.text(0.5, 0.04, 'common X', ha='center')
fig.text(0.04, 0.5, 'common Y', va='center', rotation='vertical')
Since I consider it relevant and elegant enough (no need to specify coordinates to place text), I copy (with a slight adaptation) an answer to another related question.
import matplotlib.pyplot as plt
fig, axes = plt.subplots(5, 2, sharex=True, sharey=True, figsize=(6,15))
# add a big axis, hide frame
fig.add_subplot(111, frameon=False)
# hide tick and tick label of the big axis
plt.tick_params(labelcolor='none', which='both', top=False, bottom=False, left=False, right=False)
plt.xlabel("common X")
plt.ylabel("common Y")
This results in the following (with matplotlib version 2.2.0):
New in Matplotlib v3.4 (pip install matplotlib --upgrade)
supxlabel and supylabel
fig.supxlabel('common_x')
fig.supylabel('common_y')
See example:
import matplotlib.pyplot as plt
for tl, cl in zip([True, False, False], [False, False, True]):
fig = plt.figure(constrained_layout=cl, tight_layout=tl)
gs = fig.add_gridspec(2, 3)
ax = dict()
ax['A'] = fig.add_subplot(gs[0, 0:2])
ax['B'] = fig.add_subplot(gs[1, 0:2])
ax['C'] = fig.add_subplot(gs[:, 2])
ax['C'].set_xlabel('Booger')
ax['B'].set_xlabel('Booger')
ax['A'].set_ylabel('Booger Y')
fig.suptitle(f'TEST: tight_layout={tl} constrained_layout={cl}')
fig.supxlabel('XLAgg')
fig.supylabel('YLAgg')
plt.show()
see more
Without sharex=True, sharey=True you get:
With it you should get it nicer:
fig, axes2d = plt.subplots(nrows=3, ncols=3,
sharex=True, sharey=True,
figsize=(6,6))
for i, row in enumerate(axes2d):
for j, cell in enumerate(row):
cell.imshow(np.random.rand(32,32))
plt.tight_layout()
But if you want to add additional labels, you should add them only to the edge plots:
fig, axes2d = plt.subplots(nrows=3, ncols=3,
sharex=True, sharey=True,
figsize=(6,6))
for i, row in enumerate(axes2d):
for j, cell in enumerate(row):
cell.imshow(np.random.rand(32,32))
if i == len(axes2d) - 1:
cell.set_xlabel("noise column: {0:d}".format(j + 1))
if j == 0:
cell.set_ylabel("noise row: {0:d}".format(i + 1))
plt.tight_layout()
Adding label for each plot would spoil it (maybe there is a way to automatically detect repeated labels, but I am not aware of one).
Since the command:
fig,ax = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
you used returns a tuple consisting of the figure and a list of the axes instances, it is already sufficient to do something like (mind that I've changed fig,axto fig,axes):
fig,axes = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
for ax in axes:
ax.set_xlabel('Common x-label')
ax.set_ylabel('Common y-label')
If you happen to want to change some details on a specific subplot, you can access it via axes[i] where i iterates over your subplots.
It might also be very helpful to include a
fig.tight_layout()
at the end of the file, before the plt.show(), in order to avoid overlapping labels.
It will look better if you reserve space for the common labels by making invisible labels for the subplot in the bottom left corner. It is also good to pass in the fontsize from rcParams. This way, the common labels will change size with your rc setup, and the axes will also be adjusted to leave space for the common labels.
fig_size = [8, 6]
fig, ax = plt.subplots(5, 2, sharex=True, sharey=True, figsize=fig_size)
# Reserve space for axis labels
ax[-1, 0].set_xlabel('.', color=(0, 0, 0, 0))
ax[-1, 0].set_ylabel('.', color=(0, 0, 0, 0))
# Make common axis labels
fig.text(0.5, 0.04, 'common X', va='center', ha='center', fontsize=rcParams['axes.labelsize'])
fig.text(0.04, 0.5, 'common Y', va='center', ha='center', rotation='vertical', fontsize=rcParams['axes.labelsize'])
Update:
This feature is now part of the proplot matplotlib package that I recently released on pypi. By default, when you make figures, the labels are "shared" between subplots.
Original answer:
I discovered a more robust method:
If you know the bottom and top kwargs that went into a GridSpec initialization, or you otherwise know the edges positions of your axes in Figure coordinates, you can also specify the ylabel position in Figure coordinates with some fancy "transform" magic.
For example:
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
bottom, top = 0.1, 0.9
fig, axs = plt.subplots(nrows=2, ncols=1, bottom=bottom, top=top)
avepos = 0.5 * (bottom + top)
transform = mtransforms.blended_transform_factory(mtransforms.IdentityTransform(), fig.transFigure) # specify x, y transform
axs[0].yaxis.label.set_transform(transform) # changed from default blend (IdentityTransform(), axs[0].transAxes)
axs[0].yaxis.label.set_position((0, avepos))
axs[0].set_ylabel('Hello, world!')
...and you should see that the label still appropriately adjusts left-right to keep from overlapping with labels, just like normal, but will also position itself exactly between the desired subplots.
Notably, if you omit the set_position call, the ylabel will show up exactly halfway up the figure. I'm guessing this is because when the label is finally drawn, matplotlib uses 0.5 for the y-coordinate without checking whether the underlying coordinate transform has changed.
I ran into a similar problem while plotting a grid of graphs. The graphs consisted of two parts (top and bottom). The y-label was supposed to be centered over both parts.
I did not want to use a solution that depends on knowing the position in the outer figure (like fig.text()), so I manipulated the y-position of the set_ylabel() function. It is usually 0.5, the middle of the plot it is added to. As the padding between the parts (hspace) in my code was zero, I could calculate the middle of the two parts relative to the upper part.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
# Create outer and inner grid
outerGrid = gridspec.GridSpec(2, 3, width_ratios=[1,1,1], height_ratios=[1,1])
somePlot = gridspec.GridSpecFromSubplotSpec(2, 1,
subplot_spec=outerGrid[3], height_ratios=[1,3], hspace = 0)
# Add two partial plots
partA = plt.subplot(somePlot[0])
partB = plt.subplot(somePlot[1])
# No x-ticks for the upper plot
plt.setp(partA.get_xticklabels(), visible=False)
# The center is (height(top)-height(bottom))/(2*height(top))
# Simplified to 0.5 - height(bottom)/(2*height(top))
mid = 0.5-somePlot.get_height_ratios()[1]/(2.*somePlot.get_height_ratios()[0])
# Place the y-label
partA.set_ylabel('shared label', y = mid)
plt.show()
picture
Downsides:
The horizontal distance to the plot is based on the top part, the bottom ticks might extend into the label.
The formula does not take space between the parts into account.
Throws an exception when the height of the top part is 0.
There is probably a general solution that takes padding between figures into account.

Using up space left by a missing label when using subplots

I apologise for the titlegore, but I could not figure out how to phrase it in a different way. The problem is best illustrated by the picture below. As you can see, I made figure consisting of 5 subplots using matplotlibs gridspec, which are fit into 4 square panels. The three empty panels have their own sets of x coordinates, and require their own label. However, the data from the first two panels shares the X axis, and (given that the actual label will be lengthy) I'd rather have only a single label and a single set of ticks for both, as shown here.
But as you can see, this leaves a rather large gap of whitespace between the two panels where the label would have gone. And this is what I'd like to solve; I'd like to stretch the two panels in equal amounts to fill up this white space. At the same time the top of the top panel and the bottom of the bottom panel should still align with the subplot to the right, and the bottom of the two panels shouldn't interfere with the position of the row that comes below either. I looked into the documentation on adjusting the panels in the documentation but I couldn't figure it out.
As an aside I'd also like to have a single y-axis label for the two panels, but I think I can fudge that in with fig.text().
The code that generates the above plot:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec
xs = np.linspace(0,8*np.pi,101)
ys = np.cos(xs)
fig = plt.figure(figsize=(7.2,4.45*1.5))
gs1 = gridspec.GridSpec(4, 2, figure=fig)
#gs1.update(hspace=0.1)
ax1 = plt.subplot(gs1[0, 0])
ax1.plot(xs, ys)
#ax1.set_xlabel('X')
ax1.set_ylabel('Y1')
ax1.set_xticks([])
ax2 = plt.subplot(gs1[1, 0])
ax2.plot(xs, 0.5*ys)
ax2.set_xlabel('X')
ax2.set_ylabel('Y2')
ax2.set_ylim(-1,1)
gs2 = gridspec.GridSpec(4, 2)
ax3 = plt.subplot(gs2[0:2, 1])
ax3.set_xlabel('X3')
ax3.set_ylabel('Y3')
ax4 = plt.subplot(gs2[2:, 0])
ax4.set_xlabel('X4')
ax4.set_ylabel('Y4')
ax5 = plt.subplot(gs2[2:, 1])
ax5.set_xlabel('X5')
ax5.set_ylabel('Y5')
plt.tight_layout()
You can use a SubplotSpec in one of the quadrants of a 2x2 gridspec.
An example is found int gridspec-using-subplotspec.
Here it would look like
import numpy as np
from matplotlib import pyplot as plt
xs = np.linspace(0,8*np.pi,101)
ys = np.cos(xs)
fig = plt.figure(figsize=(7.2,4.45*1.5))
# 2x2 "outer" GridSpec
gs = fig.add_gridspec(2, 2)
# 2x1 "inner" GridSpec to be used
# in one cell of the outer grid
gs00 = gs[0,0].subgridspec(2, 1)
ax1 = fig.add_subplot(gs00[0])
ax1.plot(xs, ys)
ax1.set_ylabel('Y1')
ax1.set_xticks([])
ax2 = fig.add_subplot(gs00[1])
ax2.plot(xs, 0.5*ys)
ax2.set_xlabel('X')
ax2.set_ylabel('Y2')
ax2.set_ylim(-1,1)
ax3 = fig.add_subplot(gs[0,1])
ax3.set_xlabel('X3')
ax3.set_ylabel('Y3')
ax4 = fig.add_subplot(gs[1,0])
ax4.set_xlabel('X4')
ax4.set_ylabel('Y4')
ax5 = fig.add_subplot(gs[1,1])
ax5.set_xlabel('X5')
ax5.set_ylabel('Y5')
fig.tight_layout()
plt.show()

Multiple GridSpecs in one Figure

I was working through matplotlib's documentation (http://matplotlib.org/users/gridspec.html#adjust-gridspec-layout), and in this particular example I do not understand the logic behind the layout of two GridSpecs in one figure. The significant part of the code they use (leaving out text, titles and labels) is
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
f = plt.figure()
gs1 = GridSpec(3, 3)
gs1.update(left=0.05, right=0.48, wspace=0.05)
ax1 = plt.subplot(gs1[:-1, :])
ax2 = plt.subplot(gs1[-1, :-1])
ax3 = plt.subplot(gs1[-1, -1])
gs2 = GridSpec(3, 3)
gs2.update(left=0.55, right=0.98, hspace=0.05)
ax4 = plt.subplot(gs2[:, :-1])
ax5 = plt.subplot(gs2[:-1, -1])
ax6 = plt.subplot(gs2[-1, -1])
This gives the following result (http://matplotlib.org/_images/demo_gridspec03.png):
These two GridSpecs seem to be aligned next to each other by default. Do I miss something in the code, that does this explicitly?
I tried to add a third GridSpec, like so:
gs3 = gridspec.GridSpec(3, 3)
ax7 = plt.subplot(gs3[:, 0])
ax8 = plt.subplot(gs3[:, 1:])
but this just fills the whole figure and the first two GridSpecs are "overpainted".
To restate my question, is there some implicit logic for the layout of two GridSpecs in a figure (note that I know of the method GridSpecFromSubplotSpec, but here it is not being used)?
The GridSpec extent can be adjusted with the update command. With this line you limit the first GridSpec to the left side (48%) of the Figure.
gs1.update(left=0.05, right=0.48, wspace=0.05)
The second GridSpec is then limited to the right side of the Figure with
gs1.update(left=0.55, right=0.98, hspace=0.05)
Similarly you can limit the vertical extent with the keywords top and bottom.

How do I increase the spacing between subplots with subplot2grid?

I use subplot2grid to make a subplot like the following:
In order to make the ticks sufficiently large for publication, I need to increase the vertical and horizontal spacing between axes.
Normally, I would use something like subplot_adjust(hspace = 0.5), but that doesn't seem to work with subplot2grid.
Could anyone please recommend a solution?
Here is the code I use to plot things and create the axes:
import matplotlib.pyplot as plt
ax1 = plt.subplot2grid((2,2),(0,0), colspan = 2)
ax2 = plt.subplot2grid((2,2),(1,0), colspan = 1)
ax3 = plt.subplot2grid((2,2),(1,1), colspan = 1)
df.plot( ax = ax1)
plt.show()
I've found the solution here
The code is as follows:
AX = gridspec.GridSpec(2,2)
AX.update(wspace = 0.5, hspace = 0.5)
ax1 = plt.subplot(AX[0,:])
ax2 = plt.subplot(AX[1,0])
ax3 = plt.subplot(AX[1,1])
Which produces the same subplots with increased horizontal and vertical spacing.
You can add the following line:
plt.subplots_adjust(hspace=0.8)
under this one:
ax3 =plt.subplot2grid((2,2),(1,1), colspan = 1)
You can play with all sorts of parameters this way.

Categories