278

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?

7
  • 4
    With the update to the question, and the comments left in the answers below this is a duplicate of stackoverflow.com/questions/6963035/… Commented Apr 22, 2013 at 19:55
  • 2
    Not really, since my question is for plt.subplots(), and the question you link to uses add_subplot - I can't use that method unless I switch to add_subplot, which I would like to avoid. I could use the plt.text solution which is given as an alternative solution in your link, but it is not the most elegant solution. Commented Apr 23, 2013 at 7:06
  • To elaborate, as far as I understand, plt.subplots cannot generate a set of subplots within an existing axis environment, but always creates a new figure. Right? Commented Apr 23, 2013 at 7:15
  • A most elegant solution can be found here: stackoverflow.com/questions/6963035/… Commented Dec 8, 2017 at 2:42
  • 1
    Your link was provided by user Hooked more than 4 years ago (just a few comments above yours). As I said previously, that solution pertains to add_subplot, and not plt.subplots(). Commented Dec 8, 2017 at 12:32

8 Answers 8

351

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')

Multiple plots with common axes label

Sign up to request clarification or add additional context in comments.

5 Comments

note that 0.5 for the x-coordinate of the x-label doesn't put the label at the center of the center subplot. you'd need to go slightly larger than that to account for the yticklabels.
Have a look at this answer for a method that doesn't use plt.text. You create your subplots, but then add one bit plot, make it invisible, and label its x and y.
Thanks, worked in general. Any solution to breakage when using tight_layout?
@serv-inc with tight_layout replacing 0.04 with 0 seems to work.
Using fig.text is not a good idea. This messes up things like plt.tight_layout()
191

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()

enter image description here enter image description here enter image description here

see more

5 Comments

Neat, but seems to have problems with plt.tight_layout() where things overlap. When I try to change the x argument for f.supxlabel(), it screws up the margins too.
Make sure to use Python 3.7 or above in order to be able to install v 3.4 of matplotlib
You'd still need to use a text element to add multiple supylabels for different locations
If things overlap with tight_layout, try to supply the rect parameter, e.g. to increase the left margin: fig.tight_layout(rect=(0.025,0,1,1))
It's the best canonical solution.
177

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):

5 rows and 2 columns subplots with common x and y axis labels

8 Comments

Due to simplicity this should be the accepted answer. Very straightforward. Still relevant for matplotlib v3.x.
I would like to know how could it be used with multiple figure objects? fig.xlabel("foo") does not work.
FYI: Now that people use dark themes in StackOverflow, the labels can barely be read so its better to export your png's with white background
@xyzzyqed I didn't know there was such a thing as "themes" in stackoverflow, and I don't even remember how I exported the figure. How can I control the background when exporting?
The only problem of this solution is that it does not work when using constrained_layout=True because it creates overlapping labels. In this case you have to manually adjust the borders of the subplots.
|
46

Without sharex=True, sharey=True you get:

enter image description here

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()

enter image description here

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()

enter image description here

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

1 Comment

This is a lot harder if, for example, the number of plots is unknown (e.g. you've got a generalise plot function that works for any number of subplots).
19

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.

8 Comments

I'm sorry I was a bit unclear above. With "common" I meant one single x label below all the plots, and one single y label to the left of the plots, I have updated the question to reflect this.
@JohanLindberg: Concerning your comments here and above: Indeed plt.subplots() will create a new figure instance. If you want to stick with this command, you can easily add a big_ax = fig.add_subplot(111), since you already have a figure and can add another axis. After that, you can manipulate big_ax the way it is shown in the link from Hooked.
Thanks for your suggestions, but if I do that, I have to add the big_ax after plt.subplots(), and I get that subplot on top of everything else - can I make it transparent or send it to the back somehow? Even if I set all the colors to none as in Hooked's link, it is still a white box covering all my subplots.
@JohanLindberg, you're right, I hadn't checked that. But you can easily set the background color of the big axis to none by doing: big_ax.set_axis_bgcolor('none') You should also make the labelcolor none (as opposed to the example linked by Hooked): big_ax.tick_params(labelcolor='none', top='off', bottom='off', left='off', right='off')
I get an error: AttributeError: 'numpy.ndarray' object has no attribute 'set_xlabel' in the statement ax.set_xlabel('Common x-label'). Can you figure it out?
|
8

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'])

enter image description here enter image description here

Comments

6

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.

Comments

3

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.

1 Comment

Hey, I figured out a way to do this very much in the vein of your answer, but might solve some of these issues; see stackoverflow.com/a/44020303/4970632 (below)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.