21
votes

In matplotlib, Is it possible to set a a separate title for each row of subplots in addition to the title set for the entire figure and the title set for each individual plot? This would correspond to the orange text in the figure below. enter image description here

If not, how would you get around this problem? Create a separate column of empty subplots to the left and fill them with the orange text?

I am aware that it is possible to manually position each single title using text() or annotate(), but that usually requires a lot of tweaking and I have many subplots. Is there a smoother solution?

4

4 Answers

2
votes

As of matplotlib 3.4.0, row titles can now be implemented as subfigure suptitles:

New Figure.subfigures and Figure.add_subfigure allow creating virtual figures within figures [with] localized figure artists (e.g., colorbars and suptitles) that only pertain to each subfigure.

The matplotlib gallery includes demos on how to plot subfigures.


To reproduce OP's reference figure:

  • using Figure.subfigures (more straightforward)

    Create fig.subfigures (3x1) where each subfig gets its own subfig.subplots (1x3) and subfig.suptitle:

    fig = plt.figure(constrained_layout=True)
    fig.suptitle('Figure title')
    
    # create 3x1 subfigs
    subfigs = fig.subfigures(nrows=3, ncols=1)
    for row, subfig in enumerate(subfigs):
        subfig.suptitle(f'Subplot row title {row}')
    
        # create 1x3 subplots per subfig
        axs = subfig.subplots(nrows=1, ncols=3)
        for col, ax in enumerate(axs):
            ax.plot()
            ax.set_title(f'Plot title {col}')
    
  • or using Figure.add_subfigure (onto existing pyplot.subplots)

    If you already have a plt.subplots figure (3x1), then add_subfigures into the underlying gridspec. Again each subfig will get its own subfig.subplots (1x3) and subfig.suptitle:

    # create 3x1 subplots
    fig, axs = plt.subplots(nrows=3, ncols=1, constrained_layout=True)
    fig.suptitle('Figure title')
    
    # clear subplots
    for ax in axs:
        ax.remove()
    
    # add subfigure per subplot
    gridspec = axs[0].get_subplotspec().get_gridspec()
    subfigs = [fig.add_subfigure(gs) for gs in gridspec]
    
    for row, subfig in enumerate(subfigs):
        subfig.suptitle(f'Subplot row title {row}')
    
        # create 1x3 subplots per subfig
        axs = subfig.subplots(nrows=1, ncols=3)
        for col, ax in enumerate(axs):
            ax.plot()
            ax.set_title(f'Plot title {col}')
    

Output of either example (after some styling/formatting):

subfigures with localized suptitles

19
votes

An idea is to create three "big subplots", to give each of them a title, and make them invisible. On the top of that you can create your matrix of smaller subplots.

This solution is entirely based on this post, except that more attention has been paid to actually removing the background subplot.

enter image description here

import matplotlib.pyplot as plt

fig, big_axes = plt.subplots( figsize=(15.0, 15.0) , nrows=3, ncols=1, sharey=True) 

for row, big_ax in enumerate(big_axes, start=1):
    big_ax.set_title("Subplot row %s \n" % row, fontsize=16)

    # Turn off axis lines and ticks of the big subplot 
    # obs alpha is 0 in RGBA string!
    big_ax.tick_params(labelcolor=(1.,1.,1., 0.0), top='off', bottom='off', left='off', right='off')
    # removes the white frame
    big_ax._frameon = False


for i in range(1,10):
    ax = fig.add_subplot(3,3,i)
    ax.set_title('Plot title ' + str(i))

fig.set_facecolor('w')
plt.tight_layout()
plt.show()
6
votes

Another easy cheat is to give the title of the middle column as subplot row XX\n\nPlot title No.YY

1
votes

It is better to firstly plot your real subplots and then plot empty subplots above them, thus you will have a more precise title align. And to do it precisely we need plt.GridSpec() (link).

It is best seen in columns subtitles:

# modified code of @snake_chrmer
fig, big_axes = plt.subplots(figsize=(9, 3) , nrows=1, ncols=3, sharey=True) 

for title, big_ax in zip(['First', 'Second', 'Third'], big_axes):
    big_ax.set_title(f'{title}\n', fontweight='semibold')
    big_ax.set_frame_on(False)
    big_ax.axis('off')


for i in range(1, 7):
    ax = fig.add_subplot(1,6,i)
    ax.set_title('Plot title ' + str(i))

fig.set_facecolor('w')
plt.tight_layout()
plt.show()

enter image description here

# my solition
import matplotlib.pyplot as plt
from matplotlib.gridspec import SubplotSpec

def create_subtitle(fig: plt.Figure, grid: SubplotSpec, title: str):
    "Sign sets of subplots with title"
    row = fig.add_subplot(grid)
    # the '\n' is important
    row.set_title(f'{title}\n', fontweight='semibold')
    # hide subplot
    row.set_frame_on(False)
    row.axis('off')


rows = 1 
cols = 6
fig, axs = plt.subplots(rows, cols, figsize=(9, 3))
for i, ax in enumerate(axs.flatten()):
    ax.set_title(f'Plot title {i}')
grid = plt.GridSpec(rows, cols)
create_subtitle(fig, grid[0, 0:2], 'First')
create_subtitle(fig, grid[0, 2:4], 'Second')
create_subtitle(fig, grid[0, 4:6], 'Third')
fig.tight_layout()
fig.set_facecolor('w')

enter image description here

# original problem
rows = 3
cols = 3
fig, axs = plt.subplots(rows, cols, figsize=(9, 9))
for i, ax in enumerate(axs.flatten()):
    ax.set_title(f'Plot title {i}')
grid = plt.GridSpec(rows, cols)
create_subtitle(fig, grid[0, ::], 'First')
create_subtitle(fig, grid[1, ::], 'Second')
create_subtitle(fig, grid[2, ::], 'Third')
fig.tight_layout()
fig.set_facecolor('w')

enter image description here

UPD

It is more logical and comprehensible to create subgrid for a set of subplots just to title them. The subgrig gives a wast space for modifications:

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

rows = 1
cols = 3

fig = plt.figure(figsize=(9, 3))
# grid for pairs of subplots
grid = plt.GridSpec(rows, cols)

for i in range(rows * cols):
    # create fake subplot just to title pair of subplots
    fake = fig.add_subplot(grid[i])
    #  '\n' is important
    fake.set_title(f'Fake #{i}\n', fontweight='semibold', size=14)
    fake.set_axis_off()

    # create subgrid for two subplots without space between them
    # <https://matplotlib.org/2.0.2/users/gridspec.html>
    gs = gridspec.GridSpecFromSubplotSpec(1, 2, subplot_spec=grid[i], wspace=0)

    # real subplot #1
    ax = fig.add_subplot(gs[0])
    ax.set_title(f'Real {i}1')
    # hide ticks and labels
    ax.tick_params(left=False, labelleft=False, labelbottom=False, bottom=False)

    # real subplot #2
    ax = fig.add_subplot(gs[1], sharey=ax)
    ax.set_title(f'Real {i}2')
    # hide ticks and labels
    ax.tick_params(left=False, labelleft=False, labelbottom=False, bottom=False)

fig.patch.set_facecolor('white')
fig.suptitle('SUPERTITLE', fontweight='bold', size=16)
fig.tight_layout()

enter image description here

Original problem:

rows = 3
cols = 1

fig = plt.figure(figsize=(9, 9))
# grid for pairs of subplots
grid = plt.GridSpec(rows, cols)

for i in range(rows * cols):
    # create fake subplot just to title set of subplots
    fake = fig.add_subplot(grid[i])
    # '\n' is important
    fake.set_title(f'Fake #{i}\n', fontweight='semibold', size=14)
    fake.set_axis_off()

    # create subgrid for two subplots without space between them
    # <https://matplotlib.org/2.0.2/users/gridspec.html>
    gs = gridspec.GridSpecFromSubplotSpec(1, 3, subplot_spec=grid[i])

    # real subplot #1
    ax = fig.add_subplot(gs[0])
    ax.set_title(f'Real {i}1')

    # real subplot #2
    ax = fig.add_subplot(gs[1], sharey=ax)
    ax.set_title(f'Real {i}2')
    
    # real subplot #3
    ax = fig.add_subplot(gs[2], sharey=ax)
    ax.set_title(f'Real {i}3')

fig.patch.set_facecolor('white')
fig.suptitle('SUPERTITLE', fontweight='bold', size=16)
fig.tight_layout()

enter image description here