Matplotlib plot not adhering to figure dimensions

53 views Asked by At

I'm trying to create a figure with subplots using the GridSpec method of a specific size. The size I'm trying to specify is 4.205 x 2.2752 inches to fit as a panel in an academic figure. I'm using fig.subplots_adjust to have the subplots fill the entirety of the figure. However, the output figure has completely different dimensions. Here is my code:

# set up the figure
fig = plt.figure(figsize = (4.2055, 2.2752))

# number of columns in the Gridspec
fwid = 22

# set up the grid
grid = plt.GridSpec(1,fwid)

# set up the number of columns allocated to each subplot
h0 = int((fwid-1)/3)
h1 = int(h0*2)
ax = [
    fig.add_subplot(grid[:h0]),
    fig.add_subplot(grid[h0:h1]),
    fig.add_subplot(grid[h1]),
]


hm1 = np.random.randn(10,10)
hm2 = np.random.randn(10,10)

# plot the heatmaps
sns.heatmap(
    hm1, ax = ax[0],
    vmin = 0, vmax = 1,
    cbar = False
)
sns.heatmap(
    hm2, ax = ax[1],
    vmin = 0, vmax = 1,
    # put the colorbar in the 3rd subplot
    cbar_ax = ax[2]
)


ax[0].set_xticks(np.arange(0.5,10))
ax[0].set_xticklabels(np.arange(0.5,10))
ax[0].set_yticks(np.arange(0.5,10))
ax[1].set_yticks([])
ax[1].set_xticks(np.arange(0.5, 10))
ax[1].set_xticklabels(np.arange(0.5,10))

fig.subplots_adjust(left = 0, right = 1, bottom = 0, top = 1)

plt.savefig('example.png', facecolor = 'white', transparent = False,
            bbox_inches = 'tight', dpi = 300)

Here is what the output figure looks like: enter image description here

Instead of being 4.205 x 2.2752 inches, it is 2.96 x 2.3166 inches. Not sure what is going on here. Any help is appreciated!

1

There are 1 answers

4
John Collins On

Fine-tuning precisely customized figure image file output dimensions with Matplotlib GridSpec subplots of sns.heatmap (including a third axes object for the colorbar legend)

Key changes implemented below:

  • Explicitly set the DPI during the initial figure instantiation
  • Explicitly set the aspect ratio of the axes (using Axes.set_aspect)
  • Be aware of the functionality of the matplotlib.pyplot.savefig.bbox_inches parameter to ensure figure output dimensions in Matplotlib

A few additional changes had to also be implemented (see code below). Essentially, the approach taken here was the result of some trial and error in fine-tuning the figsize, DPI, and subplots margins (see: matplotlib.pyplot.subplots_adjust), followed by image cropping using PIL. The final result, though, as shown, is a matplotlib GridSpec-arrayed figure of two Seaborn heatmap subplots which neatly fill the given dimensions you specify.*

import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

from PIL import Image

# Define the figure dimensions in inches
w = 4.2055
h = 2.2752

# Set up the figure with a specific DPI
fig = plt.figure(figsize=(w * 1.5, h * 1.5), dpi=300)

# Number of columns in the GridSpec
fwid = 22

# Set up the grid
grid = gridspec.GridSpec(1, fwid)

# Set up the number of columns allocated to each subplot
h0 = int((fwid - 1) / 3)
h1 = int(h0 * 2)
ax = [
    fig.add_subplot(grid[:h0]),
    fig.add_subplot(grid[h0:h1]),
    fig.add_subplot(grid[h1]),
]

hm1 = np.random.randn(10, 10)
hm2 = np.random.randn(10, 10)

sns.heatmap(
    hm1, ax=ax[0], vmin=0, vmax=1, cbar=False,
)
sns.heatmap(hm2, ax=ax[1], vmin=0, vmax=1, cbar_ax=ax[2])

ax[0].set_title("Heatmap 1", size=8)
ax[1].set_title("Heatmap 2", size=8)
ax[0].set_ylabel("Y Axis Label", size=6)

# Move the x-axis label downwards and adjust its position
ax[1].set_xlabel("X Axis Label", labelpad=15, size=6)
ax[1].xaxis.set_label_coords(0, -0.15)

# Remove y-axis tick labels for the second heatmap
ax[1].set_yticks([])

# Decrease axes tick labels font size for better readability
for a in ax:
    a.tick_params(
        axis="both", which="major", labelsize=4, length=2, width=0.25
    )

# Explicitly alter the aspect ratios for the three axes in the grid
ax[0].set_aspect((h * 1.75) / w)
ax[1].set_aspect((h * 1.75) / w)
ax[2].set_aspect(10)

# Adjust the figure margins and size (add space in between the two subplots)
plt.subplots_adjust(wspace=2)

plt.savefig(
    "example.png",
    facecolor="white",
    transparent=False,
    dpi=300,  # , bbox_inches="tight"
)

# Crop high-resolution output figure
image = Image.open("example.png")

crop_box = (80, 200, 1340, 830)
cropped_image = image.crop(crop_box)
cropped_image.save(
    "cropped_fig.png", dpi=(image.info["dpi"][0], image.info["dpi"][1])
)

(See GridSpec docs regarding the implementation of subplots here.)

where the final output produced (cropped_fig.png) is:

GridSpec dual heatmaps with explicitly defined figure dimensions

and the direct output from Matplotlib was (the black dashed line bounding box was measured to precisely match the specified dimensions; this and the red dimension annotations were added in photoshop, but the image file size was not cropped - i.e., the raw dimensions output from matplotlib are preserved):

Photoshop-annotated direct figure output from matplotlib

In the solution provided, the aspect ratio of the Seaborn subplots axes objects is explicitly modified to effectively stretch / widen the heatmaps, thus filling out the area defined by the given dimensions specifications. Using the values for all the various customizable figure and axes parameters as shown above, however, results in the generation of substantial amounts of whitespace padding the output figure (see directly above screenshot viewing the raw matplotlib output figure file before cropping). To account for this, the figure is initialized by the required dimensions multiplied by a scalar factor (i.e., figsize=(w * 1.5, h * 1.5)) set such that the final resulting visualization matches to the desired size when subsequently cropped out as close as possible. The fine-tuned values of these parameters can of course be adjusted / tailored as needed.


*Note: It is the use of bbox_inches="tight" which results in the output file not having the exact dimensions as specified.