256

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?

jolindbe
  • 2,752
  • 2
  • 17
  • 18
  • 4
    With the update to the question, and the comments left in the answers below this is a duplicate of http://stackoverflow.com/questions/6963035/pyplot-axes-labels-for-subplots – Hooked Apr 22 '13 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. – jolindbe Apr 23 '13 at 07: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? – jolindbe Apr 23 '13 at 07:15
  • A most elegant solution can be found here: https://stackoverflow.com/questions/6963035/pyplot-axes-labels-for-subplots – Mr.H Dec 08 '17 at 02: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(). – jolindbe Dec 08 '17 at 12:32
  • I open a feature request in github: http://github.com/matplotlib/matplotlib/issues/11147 – Oren May 06 '18 at 13:08
  • For those who wonder why several comments above link to another slightly different question, it happens that one of the answers is actually relevant in the case where `plt.subplots` is used: https://stackoverflow.com/a/36542971/1878788 It involves adding a larger subplot using `add_subplots` (that will receive the labels), **after** creating the initial subplots. "Switching" to `add_subplot` to create the initial subplots is not required. – bli Nov 06 '18 at 12:25

8 Answers8

329

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

divenex
  • 15,176
  • 9
  • 55
  • 55
  • 4
    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. – abcd Apr 07 '16 at 23:07
  • 3
    Have a look at [this answer](http://stackoverflow.com/a/36542971/2550114) 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. – James Owers May 12 '17 at 09:31
  • 1
    Thanks, worked in general. Any solution to breakage when using `tight_layout`? – serv-inc Jul 13 '17 at 11:23
  • 6
    @serv-inc with `tight_layout` replacing `0.04` with `0` seems to work. – divenex Jul 14 '17 at 15:04
  • 19
    Using `fig.text` is not a good idea. This messes up things like `plt.tight_layout()` – Peaceful Aug 29 '18 at 06:12
164

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

Oren
  • 4,711
  • 4
  • 37
  • 63
bli
  • 7,549
  • 7
  • 48
  • 94
  • 23
    Due to simplicity this should be the accepted answer. Very straightforward. Still relevant for matplotlib v3.x. – Kyle Swanson Nov 28 '18 at 02:44
  • 1
    I would like to know how could it be used with multiple figure objects? fig.xlabel("foo") does not work. – Horror Vacui Nov 07 '19 at 09:46
  • 2
    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 Jun 17 '20 at 21:14
  • @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? – bli Jun 18 '20 at 14:01
  • By default in plt.savefig the keyword `transparent=False`. The other plots in this thread all have white backgrounds. Maybe what you did was `plt.show` and click the 'save' button? – xyzzyqed Jun 18 '20 at 14:21
  • 3
    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. – baccandr Jul 13 '20 at 14:08
  • Completely remove hidden axis use: `plt.axis('off')` – marou Feb 23 '22 at 08:31
  • You can also use the `loc` argument of `xlabel` to set it to either `center`, `left`, or `right`, e.g., `plt.xlabel("common X", loc="center")` – mostafa.elhoushi Jul 10 '23 at 14:40
147

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

Oren
  • 4,711
  • 4
  • 37
  • 63
45

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

Piotr Migdal
  • 11,864
  • 9
  • 64
  • 86
  • 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). – naught101 Jan 27 '17 at 07:27
16

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.

astromax
  • 6,001
  • 10
  • 36
  • 47
Marius
  • 523
  • 4
  • 14
  • 6
    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. – jolindbe Apr 22 '13 at 19:34
  • 2
    @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. – Marius Apr 23 '13 at 08:13
  • 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. – jolindbe Apr 23 '13 at 11:49
  • 2
    @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')` – Marius Apr 23 '13 at 13:21
  • Thanks, this actually seems to work. I had already fixed it by the plt.text solution discussed earlier, but now I can change to this more elegant solution. – jolindbe Apr 23 '13 at 17:25
  • 2
    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? – hengxin Jul 08 '14 at 02:44
  • Nice answer. Your first two code lines (ie repeating OP's) seem redundant for easy exposition. More importantly, "axes" is maybe best not used; it can be a function built in to matplotlib. Maybe you could replace "axes" and "ax" both by "axs"? Also, for me, axs turns out to be not a tuple but a multidimensional array (dimensions 5x2), which is a bit awkward. So I use axs=axs.reshape((5*2,1)). – CPBL Jun 29 '15 at 02:11
  • To add one missing element in @Marius 's solution, while initializing, use `big_ax = fig.sub_plot(111, frameon=False)` to suppress the frame. – Sid Aug 18 '15 at 15:37
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

EL_DON
  • 1,416
  • 1
  • 19
  • 34
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.

Luke Davis
  • 2,548
  • 2
  • 21
  • 43
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.

CPe
  • 31
  • 3
  • 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 http://stackoverflow.com/a/44020303/4970632 (below) – Luke Davis May 17 '17 at 09:11