Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing aspect ratio of subplots in matplotlib

I have created a series of simple greyscale images which I have plotted in a grid (unfortunately, can't upload an image because I don't have a high enough reputation :( ).

The pseudo-code is

# Define matplotlib PyPlot object

nrow =  8
ncol = 12

fig, axes = plt.subplots(nrow, ncol, subplot_kw={'xticks': [], 'yticks': []})
fig.subplots_adjust(hspace=0.05, wspace=0.05)

# Sample the fine scale model at random well locations

for ax in axes.flat:

    plot_data = # some Python code here to create 2D grey scale array...

    # ... create sub-plot

    img = ax.imshow(plot_data, interpolation='none')
    img.set_cmap('gray')

# Display the plot

plt.show()        

I want to change the aspect ratio so that the plots are squashed vertically and stretched horizontally. I have tried using ax.set_aspect and passing 'aspect' as a subplot_kw argument but to no avail. I also switched 'autoscale' off but I can then only see a handful of pixels. All suggestions welcome!

Thanks in advance!!

@JoeKington - thank you! That was a great reply!! Still trying to get my head around it all. Thanks also to the other posters for their suggestions. So, the original plot looked like this: http://imgur.com/Wi6v4cs When I set' aspect='auto'' the plot looks like this: http://imgur.com/eRBO6MZ which is a big improvement. All I need to do now is adjust the subplot size so that sub-plots are plotted in a portrait aspect ratio of eg 2:1, but with the plot filling the entire sub-plot. I guess 'colspan' would do this?

like image 977
PetGriffin Avatar asked Sep 17 '15 14:09

PetGriffin


1 Answers

The Short Answer

You're probably wanting to call:

ax.imshow(..., aspect='auto')

imshow will set the aspect ratio of the axes to 1 when it is called, by default. This will override any aspect you specify when you create the axes.

However, this is a common source of confusion in matplotlib. Let me back up and explain what's going on in detail.


Matplotlib's Layout Model


aspect in matplotlib refers to the ratio of the xscale and yscale in data coordinates. It doesn't directly control the ratio of the width and height of the axes.

There are three things that control the size and shape of the "outside box" of a matplotlib axes:

  1. The size/shape of the Figure (shown in red in figures below)
  2. The specified extent of the Axes in figure coordinates (e.g. the subplot location, shown in green in figures below)
  3. The mechanism that the Axes uses to accommodate a fixed aspect ratio (the adjustable parameter).

Axes are always placed in figure coordinates in other words, their shape/size is always a ratio of the figure's shape/size. (Note: Some things such as axes_grid will change this at draw time to get around this limitation.)

However, the extent the axes is given (either from its subplot location or explicitly set extent) isn't necessarily the size it will take up. Depending on the aspect and adjustable parameters, the Axes will shrink inside of its given extent.


To understand how everything interacts, let's plot a circle in lots of different cases.

No Fixed Aspect

In the basic case (no fixed aspect ratio set for the axes), the axes will fill up the entire space allocated to it in figure coordinates (shown by the green box).

The x and y scales (as set by aspect) will be free to change independently, distorting the circle:

enter image description here

When we resize the figure (interactively or at figure creation), the axes will "squish" with it:

enter image description here


Fixed Aspect Ratio, adjustable='box'

However, if the aspect ratio of the plot is set (imshow will force the aspect ratio to 1, by default), the Axes will adjust the size of the outside of the axes to keep the x and y data ratios at the specified aspect.

A key point to understand here, though, is that the aspect of the plot is the aspect of the x and y data scales. It's not the aspect of the width and height of the plot. Therefore, if the aspect is 1, the circle will always be a circle.

As an example, let's say we had done something like:

fig, ax = plt.subplots()
# Plot circle, etc, then:
ax.set(xlim=[0, 10], ylim=[0, 20], aspect=1)

By default, adjustable will be "box". Let's see what happens:

enter image description here

The maximum space the Axes can take up is shown by the green box. However, it has to maintain the same x and y scales. There are two ways this could be accomplished: Change the x and y limits or change the shape/size of the Axes bounding box. Because the adjustable parameter of the Axes is set to the default "box", the Axes shrinks inside of its maximum space.

And as we resize the figure, it will keep shrinking, but maintain the x and y scales by making the Axes use up less of the maximum space allocated to the axes (green box):

enter image description here

Two quick side-notes:

  1. If you're using shared axes, and want to have adjustable="box", use adjustable="box-forced" instead.
  2. If you'd like to control where the axes is positioned inside of the "green box" set the anchor of the axes. E.g. ax.set_anchor('NE') to have it remain "pinned" to the upper right corner of the "green box" as it adjusts its size to maintain the aspect ratio.

Fixed Aspect, adjustable="datalim"

The other main option for adjustable is "datalim".

In this case, matplotlib will keep the x and y scales in data space by changing one of the axes limits. The Axes will fill up the entire space allocated to it. However, if you manually set the x or y limits, they may be overridden to allow the axes to both fill up the full space allocated to it and keep the x/y scale ratio to the specified aspect.

In this case, the x limits were set to 0-10 and the y-limits to 0-20, with aspect=1, adjustable='datalim'. Note that the y-limit was not honored:

enter image description here

And as we resize the figure, the aspect ratio says the same, but the data limits change (in this case, the x-limit is not honored).

enter image description here


On a side note, the code to generate all of the above figures is at: https://gist.github.com/joferkington/4fe0d9164b5e4fe1e247


What does this have to do with imshow?


When imshow is called, it calls ax.set_aspect(1.0), by default. Because adjustable="box" by default, any plot with imshow will behave like the 3rd/4th images above.

For example:

enter image description here

However, if we specify imshow(..., aspect='auto'), the aspect ratio of the plot won't be overridden, and the image will "squish" to take up the full space allocated to the Axes:

enter image description here

On the other hand, if you wanted the pixels to remain "square" (note: they may not be square depending on what's specified by the extent kwarg), you can leave out the aspect='auto' and set the adjustable parameter of the axes to "datalim" instead.

E.g.

ax.imshow(data, cmap='gist_earth', interpolation='none')
ax.set(adjustable="datalim")

enter image description here

Axes Shape is Controlled by Figure Shape

The final part to remember is that the axes shape/size is defined as a percentage of the figure's shape/size.

Therefore, if you want to preserve the aspect ratio of the axes and have a fixed spacing between adjacent subplots, you'll need to define the shape of the figure to match. plt.figaspect is extremely handy for this. It simply generates a tuple of width, height based on a specified aspect ratio or a 2D array (it will take the aspect ratio from the array's shape, not contents).

For your example of a grid of subplots, each with a constant 2x1 aspect ratio, you might consider something like the following (note that I'm not using aspect="auto" here, as we want the pixels in the images to remain square):

import numpy as np
import matplotlib.pyplot as plt

nrows, ncols = 8, 12
dx, dy = 1, 2
figsize = plt.figaspect(float(dy * nrows) / float(dx * ncols))

fig, axes = plt.subplots(nrows, ncols, figsize=figsize)
for ax in axes.flat:
    data = np.random.random((10*dy, 10*dx))
    ax.imshow(data, interpolation='none', cmap='gray')
    ax.set(xticks=[], yticks=[])

pad = 0.05 # Padding around the edge of the figure
xpad, ypad = dx * pad, dy * pad
fig.subplots_adjust(left=xpad, right=1-xpad, top=1-ypad, bottom=ypad)

plt.show()

enter image description here

like image 188
Joe Kington Avatar answered Nov 15 '22 03:11

Joe Kington