2

Matplotlib automatically scales all the content in a figure window when you resize the figure. Typically this is what users will want, but I frequently want to increase the size of the window to make more room for something else. In this case, I would like the pre-existing content to remain the same size as I change the window size. Does anyone know a clean way to do this?

The only idea I have is to just resize the figure window, allow the figure content to be scaled, and then manually go scale each piece of content back to it's original size. This seems like a pain, so I was hoping there was a better way.

Stretch
  • 1,581
  • 3
  • 17
  • 31

3 Answers3

1

I looked at the AxesDivider module, but it didn't appear to be very well suited to my problem. I also considered using the transform stack, but I didn't see a significant advantage to using it instead of just scaling things manually.

Here is what I came up with:

import matplotlib.pyplot as plt
import numpy as np
from copy import deepcopy

#Create the original figure with a plot in it.
x1 = [1,2,3]
y1 = [1,2,3]
fig = plt.figure(figsize = [5,5], facecolor = [0.9,0.9,0.9])
data_ax = fig.add_axes([0.1,0.1,0.8,0.8])
data_ax.plot(x1a, y1a)
plt.savefig('old_fig.png', facecolor = [0.9,0.9,0.9])

Here is the old figure:

enter image description here

#Set the desired scale factor for the figure window
desired_sf = [2.0, 1.5]
#Get the current figure size using deepcopy() so that it will not be updated when the 
#figure size gets changed 
old_fig_size = deepcopy(fig.get_size_inches())
#Change the figure size.  The forward = True option is needed to make the figure window 
#size update prior to saving.
fig.set_size_inches([old_fig_size[0] * desired_sf[0], old_fig_size[1] * desired_sf[1]], forward = True)
#For some reason, the new figure size does not perfectly match what I specified, so I    
#simply query the figure size after resizing.
fig.canvas.draw()
new_fig_size = fig.get_size_inches()
#Get the actual scaling factor
sf = new_fig_size / old_fig_size

#Go through the figure content and scale appropriately
for ax in fig.axes:
    pos = ax.get_position()
    ax.set_position([pos.x0 / sf[0], pos.y0 / sf[1], pos.width / sf[0], pos.height / sf[1]])

for text in fig.texts:
    pos = np.array(text.get_position())
    text.set_position(pos / sf)

for line in fig.lines:
    x = line.get_xdata()
    y = line.get_ydata()
    line.set_xdata(x / sf[0])
    line.set_ydata(y / sf[1])

for patch in fig.patches:
    xy = patch.get_xy()
    patch.set_xy(xy / sf)

fig.canvas.draw()
plt.savefig('new_fig.png', facecolor = [0.9,0.9,0.9])

Here is the new figure (the plot shows up smaller because the image hosting service scales the total image size):

enter image description here

Tricky Parts:

One tricky part was the forward = True option in fig.set_size_inches(new_size, forward = True).

The second tricky part was realizing that the figure size changes when fig.canvas.draw() is called, meaning the actual scale factor (sf) did not necessarily match the desired scale factor (desired_sf). Maybe if I had used the transformation stack instead, it would have automatically compensated for the figure size changing when fig.canvas.draw() was called...

Stretch
  • 1,581
  • 3
  • 17
  • 31
0

Read this tutorial for a through introduction to the transform stack.

The short answer is that this behavior is inherent to the way that matplotlib views the world. Everything is position/defined in relative units (data-units, axes fraction, and figure fraction) which is only converted to screen-units during rendering, thus the only place any part of the library knows how 'big' it is in screen units is the figure size (controlled with fig.set_size_inches). This allows things like the figure to be resized at all.

One tool that might be of help to you is the AxesDivider module, but I have very little experience with it.

tacaswell
  • 84,579
  • 22
  • 210
  • 199
  • Ok. Thanks for the info. I will try out the `AxesDivider` module, but I will probably end up just resizing/transforming things one by one. – Stretch Aug 21 '14 at 02:56
0

There seems not to be an easy way in Matplotlib to freeze axis (or canvas) size while changing figure size. There might be a way through "Transforms" as there seems to be a frozen method for BBoxBase that could be reminiscent of a MATLAB function (on MEX) but no documentation is currently provided on the Matplotlib site.

In general, I would advise to build the figure structure separately, then resize the axes/canvas before the actual rendering to the new figure size.

For example, in the following, figure_3x1 builds a generic figure with 3x1 subplots, using a figure size specified in the option dictionary opts as opts['figsize'] of 6.5-by-5.5 inches:

def varargin(pars,**kwargs):
"""
varargin-like option for user-defined parameters in any function/module
Use:
pars = varargin(pars,**kwargs)

Input:
- pars     : the dictionary of parameters of the calling function
- **kwargs : a dictionary of user-defined parameters

Output:
- pars     : modified dictionary of parameters to be used inside the calling
             (parent) function
"""
    for key,val in kwargs.iteritems():
        if key in pars:
            pars[key] = val
    return pars

def figure_3x1(**kwargs):
'''
figure_3x1(**kwargs) : Create grid plot for a 1-column complex figure composed of (top-to-bottom):
                     3 equal plots | custom space

Input:
- left,right,top,bottom
- vs      : [val] vertical space between plots
- figsize : (width,height) 2x1 tuple for figure size

Output:
- fig     : fig handler
- ax      : a list of ax handles (top-to-bottom)

'''

    opts = {'left'   : 0.1,
            'right'  : 0.05,
            'bottom' : 0.1,
            'top'    : 0.02,
            'vs'     : [0.03],
            'figsize': (6.5,5.5), # This is the figure size with respect
                                  # to axes will be sized
            }

    # User-defined parameters
    opts = varargin(opts,**kwargs)

    nrow = 3

    # Axis specification
    AxisWidth = 1.0-opts['left']-opts['right']
    AxisHeight = [(1.0-opts['bottom']-opts['top']-2*opts['vs'][0])/nrow]

    # Axis Grid
    # x position
    xpos = opts['left']
    # y position
    ypos = list()
    ypos.append(opts['bottom'])
    ypos.append(ypos[0]+AxisHeight[0]+opts['vs'][0])
    ypos.append(ypos[1]+AxisHeight[0]+opts['vs'][0])

    # Axis boxes (bottom-up)
    axBoxes = list()
    axBoxes.append([xpos,ypos[0],AxisWidth,AxisHeight[0]])
    axBoxes.append([xpos,ypos[1],AxisWidth,AxisHeight[0]])
    axBoxes.append([xpos,ypos[2],AxisWidth,AxisHeight[0]])

    fig = plt.figure(1, figsize=opts['figsize'])

    ax = list()
    for i in xrange(shape(axBoxes)[0]-1,-1,-1):
        ax_aux = fig.add_axes(axBoxes[i])
        ax.append(ax_aux)

    return fig, ax

enter image description here

Now, I want to build a similar figure, keeping the axes of the same size and in the same positions (with respect to the left/bottom corner), but with more room within the figure. This is for example useful if I want to add a colorbar to one of the plots or multiple y-axes, and then I have multiple 3x1 figures that I need to show together for a publication purpose. The idea is then to figure out the actual size for the new figure size should be to accomodate all the new elements - let's say we need a figure of size 7.5x5.5 inches - and yet resize the axis boxes and their x-y coordinates with respect to this new figure size, in order to achieve their original size in figure of 6.5x5.5 inches. We introduce for this purpose a further option axref_size in opts of figure_3x1(**kwargs), which is a tuple of the type (width, height) that describes the figure size (in inches) with respect to we want the axes to be build (and sized) to in the new larger figure. After these width (AxisWidth) and height (AxisHeight) of these axes are first calculated with respect to the larger figure, we resize them along with the left and bottom coordinates, and the vertical/horizontal spaces between different axes, i.e. hs and vs to achieve the size of the original axref_size figure.

The actual resizing is achieved by the method:

def freeze_canvas(opts, AxisWidth, AxisHeight):
'''
Resize axis to ref_size (keeping left and bottom margins fixed)
Useful to plot figures in a larger or smaller figure box but with canvas of the same canvas

Inputs :

opts : Dictionary
 Options for figure plotting. Must contain keywords: 'left', 'bottom', 'hs', 'vs', 'figsize', 'axref_size'.
AxisWidth : Value / List
AxisHeight: Value / List

Return:
opts, AxisWidth, AxisHeight
'''
    # Basic function to proper resize
    resize = lambda obj, dx : [val+dx*val for val in obj] if type(obj)==type(list()) else obj+dx*obj
    if opts['axref_size'] != None :
        # Compute size differences
        dw = (opts['axref_size'][0]-opts['figsize'][0])/opts['figsize'][0]
        dh = (opts['axref_size'][1]-opts['figsize'][1])/opts['figsize'][1]
        for k,v in opts.iteritems():
            if k=='left' or k=='hs' : opts[k] = resize(v,dw)
            if k=='bottom' or k=='vs' : opts[k] = resize(v,dh)
        AxisWidth = resize(AxisWidth,dw)
        AxisHeight = resize(AxisHeight,dh)
    return opts, AxisWidth, AxisHeight

So that figure_3x1(**kwargs) is then edited as:

def figure_3x1(**kwargs):
'''
figure_3x1(**kwargs) : Create grid plot for a 1-column complex figure composed of (top-to-bottom):
                     3 equal plots | custom space
Include also the option of specifying axes size according to a reference figure size
'''

    opts = {'left'   : 0.1,
            'right'  : 0.05,
            'bottom' : 0.1,
            'top'    : 0.02,
            'vs'     : [0.03],
            'figsize': (6.5,5.5), # This is the figure size with respect
                                  # to axes will be sized
            'axref_size': None}

    ...
    ...

    # Axis specification
    AxisWidth = 1.0-opts['left']-opts['right']
    AxisHeight = [(1.0-opts['bottom']-opts['top']-2*opts['vs'][0])/nrow]

    # Optional resizing
    opts, AxisWidth, AxisHeight = freeze_canvas(opts, AxisWidth, AxisHeight)

    # Axis Grid
    # x position

    ...
    ...

    return fig, ax

In this fashion a call like

figure_3x1(figsize=(7.5,5.5),axref_size=(6.5,5.5))

generates the following larger figure (i.e. of 7.5x5.5 inches) but with the axes for subplots of the same size as in the above figure of 6.5x5.5 inches. Again, as pointed out in the other reply, the plots shows up smaller because the image hosting service scales the total image size.

enter image description here

maurizio
  • 745
  • 1
  • 7
  • 25