10

I would please like suggestions for how to override the default matplotlib behaviour when plotting images as subplots, whereby the subplot sizes don't seem to match the figure size. I would like to set my figure size (e.g. to match the width of an A4 page) and have the subplots automatically stretch to fill the space available. In the following example, the code below gives a figure with a lot of white space between the panels:

import numpy as np
import matplotlib.pyplot as plt

data=np.random.rand(10,4)

#creating a wide figure with 2 subplots in 1 row
fig,ax=plt.subplots(1,2, figsize=(9,3))  
ax=ax.reshape(1,len(ax))

for i in [0,1]:
    plt.sca(ax[0,i])
    plt.imshow(data,interpolation='nearest')
    plt.colorbar()

I would like the subplots to be stretched horizontally so that they fill the figure space. I will make many similar plots with different numbers of values along each axis, and the space between the plots appears to depend on the ratio of x values to y values, so I would please like to know if there is a good general way to set the subplot widths to fill the space. Can the physical size of subplots be specified somehow? I've been searching for solutions for a few hours, so thanks in advance for any help you can give.

PeterW
  • 311
  • 2
  • 5
  • 13

3 Answers3

15

You can adjust your axis area by using the ax.set_position method. It works with relative coordinates, so if you want to make an A4 image, then:

import numpy as np
import matplotlib.pyplot as plt

# figsize keyword talks some obscure units which need a conversion from standard units
plt.figure(figsize=np.array([210,297]) / 25.4)

x = np.linspace(0,2*np.pi, 100)
plt.plot(x, np.sin(x))

plt.gca().set_position([0, 0, 1, 1])

plt.show()

Now the axis area (plot area) fills the whole page.

The coordinates given to the set_position are relative coordinates [left, lower, width, height], where each direction is scaled by the page size.

As pointed out in the other answers, imshow and matshow try sometimes to keep the pixels in the picture square. There is a rather special interplay with the axis ratio and imshow.

  • if imshow is called without extent=[...] or aspect='auto' keyword arguments, it does what is instructed in the local defaults, usually tries to keep the pixels square
  • if this happens (or aspect='equal' is set), the axes act as if plt.axis('scaled') had been called, i.e. keeps X and Y coordinates equal length (pixels per unit) and change the axis size to match the extents
  • this can be overridden by setting plt.axis('tight') (which makes the x and y limits to fit the image exactly)

Th old trick is to use axis('auto') or axis('normal'), but these are nowadays deprecated (use scaled, equal, or tight).

Yes, it is a bit of a mess.

DrV
  • 22,637
  • 7
  • 60
  • 72
  • Thanks very much for the reply. That solution seems to work when using plt.plot(), but not when using plt.imshow(), which is what I am using. I am using this for plotting 2D histograms, with each square on the plot corresponding to a bin, and its colour corresponding to the number of values it contains. Do you know of a way to resize imshow() objects, or a better way to present 2D histograms where your method will work? – PeterW Jul 03 '14 at 10:28
  • @user3798292, you might try doing the set_position as specified here, but then instead of using the pyplot command, use the direct object interface (`ax.imshow(...)` rather than `plt.imshow(...)`) – Ajean Jul 03 '14 at 15:31
  • @user3798292: You have some solutions pointed out to you, but I added a few comments on this. `imshow` is a bit strange in this sense. – DrV Jul 03 '14 at 18:33
  • `plt.gca().set_position([0, 0, 1, 1])` was exactly what I needed to get plots of satellite imagery using `.imshow()` with Cartopy to fill the entire figure. For my use case, `plt.axis('tight')` made it show the entire projection of the globe, which wasn't what I was looking for. Thanks for the very helpful answer! – kevinmicke Feb 14 '18 at 21:12
  • 3
    I enjoyed the use of "obscure units" to refer to inches. Quite right, too. – Archimaredes Aug 22 '19 at 13:52
7

First, you're using calls to plt when you have Axes objects as your disposal. That road leads to pain. Second, imshow sets the aspect ratio of the axes scales to 1. That's why the axes are so narrow. Knowing all that, your example becomes:

import numpy as np
import matplotlib.pyplot as plt

data = np.random.rand(10,4)

#creating a wide figure with 2 subplots in 1 row
fig, axes = plt.subplots(1, 2, figsize=(9,3))  

for ax in axes.flatten():  # flatten in case you have a second row at some point
    img = ax.imshow(data, interpolation='nearest')
    ax.set_aspect('auto')

plt.colorbar(img)

On my system, that looks like this: enter image description here

Paul H
  • 65,268
  • 20
  • 159
  • 136
0

A friend came up with a solution that seems to work reasonably well, which I thought I would post for anybody with a similar problem - setting aspect='auto' in imshow seems to do the trick, for any choice of nx, figsize, nplots_hori and nplots_vert below.

import numpy as np
import matplotlib.pyplot as plt

nx=5
data=np.random.rand(10,nx)

figsize=[10,8]
nplots_hori=2
nplots_vert=2

fig,ax=plt.subplots(nplots_vert, nplots_hori, figsize=figsize)
if nplots_vert==1: ax=ax.reshape(1,len(ax))

plt.tight_layout()
for i in range(nplots_hori):
    for j in range(nplots_vert):
        plt.sca(ax[j,i])
        plt.imshow(data, aspect='auto')

plt.tight_layout()
plt.show()
PeterW
  • 311
  • 2
  • 5
  • 13