nicholas
nicholas

Reputation: 190

Set absolute size of subplots

I know how to set the relative size of subplots within a figure using gridspec or subplots_adjust, and I know how to set the size of a figure using figsize. My problem is setting the absolute size of the subplots.

Use case: I am making two separate plots which will be saved as pdfs for an academic paper. One has two subplots and one has three subplots (in both cases in 1 row). I need each of the 5 subplots to be the exact same size with the exact same font sizes (axis labels, tick labels, etc) in the resulting PDFs. In the example below the fonts are the same size but the subplots are not. If I make the height of the resulting PDFs the same (and thus the axes), the font on 3-subplots.pdf is smaller than that of 2-subplots.pdf.

MWE:

import matplotlib.pyplot as plt

subplots = [2, 3]
for i, cols in enumerate(subplots):

    fig, ax = plt.subplots(1, cols, sharey=True, subplot_kw=dict(box_aspect=1))

    for j in range(cols):
        ax[j].set_title(f'plot {j*cols}')
        ax[j].set_xlabel('My x label')
    ax[0].set_ylabel('My y label')

    plt.tight_layout()
    plt.savefig(f'{cols}-subplots.pdf', bbox_inches='tight', pad_inches=0)
    plt.show()

Output: output

Upvotes: 4

Views: 9711

Answers (3)

nick645378
nick645378

Reputation: 11

I created a function that creates axes with absolute sizes and acts in most ways like plt.subplots(...), for example by allowing shared y- or x-axes and returning the axes as a shaped numpy array. It centers the axes inside their grid areas, giving them as much space as possible between themselves and the edges of the figure, assuming you set figsize large enough.

The arguments include absolute height and width for the figure (see the matplotlib documentation for details) and absolute height and width for the axes, as requested in the original question.

from typing import Tuple
from matplotlib import pyplot as plt
import numpy as np

def subplots_with_absolute_sized_axes(
        nrows: int, ncols: int,
        figsize: Tuple[float, float],
        axis_width: float, axis_height: float,
        sharex: bool=False, sharey: bool=False) -> Tuple[plt.Figure, numpy.ndarray]:
    ''' Create axes with exact sizes.

    Spaces axes as far from each other and the figure edges as possible
    within the grid defined by nrows, ncols, and figsize.

    Allows you to share y and x axes, if desired.
    '''
    fig = plt.figure(figsize=figsize)
    figwidth, figheight = figsize
    # spacing on each left and right side of the figure
    h_margin = (figwidth - (ncols * axis_width)) / figwidth / ncols / 2
    # spacing on each top and bottom of the figure
    v_margin = (figheight - (nrows * axis_height)) / figheight / nrows / 2
    row_addend = 1 / nrows
    col_addend = 1 / ncols
    inner_ax_width = axis_width / figwidth
    inner_ax_height = axis_height / figheight
    axes = []
    sharex_ax = None
    sharey_ax = None
    for row in range(nrows):
        bottom = (row * row_addend) + v_margin
        for col in range(ncols):
            left = (col * col_addend) + h_margin
            if not axes:
                axes.append(fig.add_axes(
                    [left, bottom, inner_ax_width, inner_ax_height]))
                if sharex:
                    sharex_ax = axes[0]
                if sharey:
                    sharey_ax = axes[0]
            else:
                axes.append(fig.add_axes(
                    [left, bottom, inner_ax_width, inner_ax_height],
                    sharex=sharex_ax, sharey=sharey_ax))
    return fig, np.flip(np.asarray(list(axes)).reshape((nrows, ncols)), axis=0)

Upvotes: 1

nicholas
nicholas

Reputation: 190

I ended up solving this by:

  1. setting explicit absolute lengths for subplot width/height, the space between subplots and the space outside subplots,
  2. adding them up to get an absolute figure size,
  3. setting the subplot box_aspect to 1 to keep them square.
import matplotlib.pyplot as plt

num_subplots = [2, 3]

scale = 1 # scaling factor for the plot
subplot_abs_width = 2*scale # Both the width and height of each subplot
subplot_abs_spacing_width = 0.2*scale # The width of the spacing between subplots
subplot_abs_excess_width = 0.3*scale # The width of the excess space on the left and right of the subplots
subplot_abs_excess_height = 0.3*scale # The height of the excess space on the top and bottom of the subplots

for i, cols in enumerate(num_subplots):
    fig_width = (cols * subplot_abs_width) + ((cols-1) * subplot_abs_spacing_width) + subplot_abs_excess_width
    fig_height = subplot_abs_width+subplot_abs_excess_height

    fig, ax = plt.subplots(1, cols, sharey=True, figsize=(fig_width, fig_height), subplot_kw=dict(box_aspect=1))

    for j in range(cols):
        ax[j].set_title(f'plot {j}')
        ax[j].set_xlabel('My x label')
    ax[0].set_ylabel('My y label')

    plt.tight_layout()
    plt.savefig(f'{cols}-subplots.pdf', bbox_inches='tight', pad_inches=0)
    plt.show()

Picture of solution

Upvotes: 3

sunflower
sunflower

Reputation: 81

I prefer to use fig.add_axes([left, bottom, width, height]) which let you control the size and location of each subplot precisely. left and bottom decide the location of your subplots, while width and height decide the size. All quantities are in fractions of figure width and height, thus they are all float between 0 and 1.

An example:

fig = plt.figure(figsize=(8.3, 11.7))
axs = {
    "ax1": fig.add_axes([0.2, 0.7, 0.6, 0.2], xticklabels=[]),
    "ax2": fig.add_axes([0.2, 0.49, 0.6, 0.2], xticklabels=[]),
    "ax3": fig.add_axes([0.2, 0.28, 0.6, 0.2]),
}

With this I created 3 subplots in an A4 size figure, each of them are 0.6x8.3 width and 0.2x11.7 height. The spacing between them is 0.1x11.7. "ax1" and "ax2" do not show xticklabels so that I can set shared x ticks for them later.

You can see matplotlib API refenrence for more information https://matplotlib.org/stable/api/figure_api.html

Upvotes: 8

Related Questions