matplotlib:调整图窗大小而不缩放图形内容
Matplotlib 是一个绘图工具,它会在你调整图形窗口大小的时候自动调整里面所有内容的大小。通常来说,这样的功能是用户所需要的,但我经常想要扩大窗口的大小,以便为其他东西留出更多空间。在这种情况下,我希望里面已经存在的内容在我改变窗口大小的时候能够保持原来的大小。有没有人知道一个简单的方法可以做到这一点?
我唯一想到的办法就是先调整图形窗口的大小,让里面的内容自动缩放,然后再手动把每个内容调整回原来的大小。这样做感觉很麻烦,所以我希望能有更好的解决办法。
3 个回答
在Matplotlib中,似乎没有简单的方法可以在改变图形大小的同时固定坐标轴(或画布)的大小。可能有一种方法是通过“变换”,因为似乎有一个frozen
方法用于BBoxBase
,这可能和MATLAB中的某个函数类似(在MEX中),但目前Matplotlib网站上没有相关的文档。
一般来说,我建议先单独构建图形结构,然后在实际渲染到新图形大小之前调整坐标轴/画布的大小。
例如,在下面的例子中,figure_3x1
构建了一个包含3x1子图的通用图形,使用在选项字典opts
中指定的图形大小opts['figsize']
,大小为6.5英寸乘5.5英寸:
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
现在,我想构建一个类似的图形,保持坐标轴的大小和位置(相对于左下角)不变,但在图形内留出更多空间。这在我想为某个图添加颜色条或多个y轴时非常有用,尤其是当我需要展示多个3x1的图形用于出版时。我的想法是确定新的图形大小,以便容纳所有新元素——假设我们需要一个7.5x5.5英寸的图形——同时根据这个新图形大小调整坐标轴的框和它们的x-y坐标,以便在7.5x5.5英寸的图形中实现它们在6.5x5.5英寸图形中的原始大小。为此,我们在figure_3x1(**kwargs)
的opts
中引入了一个新的选项axref_size
,它是一个类型为(宽度, 高度)
的元组,描述我们希望在新更大图形中构建(和调整大小)的坐标轴的图形大小(以英寸为单位)。在计算出这些坐标轴相对于更大图形的宽度(AxisWidth
)和高度(AxisHeight
)后,我们会调整它们以及left
和bottom
坐标,以及不同坐标轴之间的垂直/水平间距,即hs
和vs
,以达到原始axref_size
图形的大小。
实际的调整大小是通过以下方法实现的:
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
因此,figure_3x1(**kwargs)
会被编辑为:
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
这样调用类似于
figure_3x1(figsize=(7.5,5.5),axref_size=(6.5,5.5))
就会生成以下更大的图形(即7.5x5.5英寸),但子图的坐标轴大小与上面6.5x5.5英寸的图形相同。再次强调,如其他回复所指出的,图形看起来更小是因为图像托管服务缩放了整个图像的大小。
可以看看这个教程,它会详细介绍变换堆栈的内容。
简单来说,这种行为是因为matplotlib看待世界的方式。所有的东西都是用相对单位来定义的(比如数据单位、坐标轴的比例和图形的比例),只有在渲染的时候才会转换成屏幕单位。因此,库中的任何部分知道它在屏幕单位下有多大的地方,只有图形的大小(可以通过fig.set_size_inches
来控制)。这也让图形可以被调整大小。
一个可能对你有帮助的工具是AxesDivider
模块,不过我对它的经验不多。
我看了看 AxesDivider
模块,但感觉它不太适合我的问题。我也考虑过使用变换栈,但觉得手动缩放东西似乎没有太大优势。
这是我想到的解决方案:
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])
这是旧的图:
#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])
这是新的图(因为图片托管服务会缩放总图像大小,所以图看起来更小):
难点:
一个难点是 fig.set_size_inches(new_size, forward = True)
中的 forward = True
选项。
第二个难点是意识到当调用 fig.canvas.draw()
时,图的大小会发生变化,这意味着实际的缩放因子(sf
)不一定和期望的缩放因子(desired_sf
)一致。也许如果我使用变换栈,它会在调用 fig.canvas.draw()
时自动调整图的大小……