创建具有精确大小且无填充的图形(以及轴外的图例) [英] Creating figure with exact size and no padding (and legend outside the axes)

查看:63
本文介绍了创建具有精确大小且无填充的图形(以及轴外的图例)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试为科学文章制作一些数字,因此我希望我的数字具有特定的大小.我还看到默认情况下,Matplotlib在图形的边框上添加了很多填充,我不需要这些填充(因为图形无论如何都将在白色背景上).

I am trying to make some figures for a scientific article, so I want my figures to have a specific size. I also see that Matplotlib by default adds a lot of padding on the border of the figures, which I don't need (since the figures will be on a white background anyway).

要设置特定的图形尺寸,我只需使用plt.figure(figsize = [w, h]),然后添加参数tight_layout = {'pad': 0}即可删除填充.这可以完美地工作,甚至在我添加标题,y/x标签等时也可以工作.例如:

To set a specific figure size I simply use plt.figure(figsize = [w, h]), and I add the argument tight_layout = {'pad': 0} to remove the padding. This works perfectly, and even works if I add a title, y/x-labels etc. Example:

fig = plt.figure(
    figsize = [3,2],
    tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
plt.savefig('figure01.pdf')

这将创建一个大小为3x2(英寸)的pdf文件.

This creates a pdf file with exact size 3x2 (inches).

我遇到的问题是,例如当我在轴外添加文本框(通常是图例框)时,Matplotlib不会像添加标题/轴标签时那样为文本框腾出空间.通常,文本框会被截断,或者根本不会显示在保存的图形中.示例:

The issue I have is that when I for example add a text box outside the axis (typically a legend box), Matplotlib does not make room for the text box like it does when adding titles/axis labels. Typically the text box is cut off, or does not show in the saved figure at all. Example:

plt.close('all')
fig = plt.figure(
    figsize = [3,2],
    tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
t = ax.text(0.7, 1.1, 'my text here', bbox = dict(boxstyle = 'round'))
plt.savefig('figure02.pdf')

我在SO上其他地方找到的解决方案是将参数bbox_inches = 'tight'添加到savefig命令.现在包括文本框,如我所愿,但是pdf的大小现在错误.看起来Matplotlib只是使图形更大,而不是像添加标题和x/y-labels那样减小轴的大小.

A solution I found elsewhere on SO was to add the argument bbox_inches = 'tight' to the savefig command. The text box is now included like I wanted, but the pdf is now the wrong size. It seems like Matplotlib just makes the figure bigger, instead of reducing the size of the axes like it does when adding titles and x/y-labels.

示例:

plt.close('all')
fig = plt.figure(
    figsize = [3,2],
    tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
t = ax.text(0.7, 1.1, 'my text here', bbox = dict(boxstyle = 'round'))
plt.savefig('figure03.pdf', bbox_inches = 'tight')

(此数字为3.307x2.248)

(This figure is 3.307x2.248)

有没有解决方案可以解决大多数情况,即轴外有图例?

Is there any solution to this that covers most cases with a legend just outside the axes?

推荐答案

所以要求是:

  1. 具有固定的预定义图形大小
  2. 在轴外添加文本标签或图例
  3. 轴和文本不能重叠
  4. 轴与标题和轴标签一起紧紧地位于图形边框上.

所以tight_layoutpad = 0一起求解1.和4.,但与2.相矛盾.

So tight_layout with pad = 0, solves 1. and 4. but contradicts 2.

可以考虑将pad设置为更大的值.这样可以解决2.但是,由于它在所有方向上都是对称的,因此将与4相矛盾.

One could think on setting pad to a larger value. This would solve 2. However, since it's is symmetric in all directions, it would contradict 4.

使用bbox_inches = 'tight'更改图形尺寸.矛盾1.

Using bbox_inches = 'tight' changes the figure size. Contradicts 1.

因此,我认为没有通用的解决方案可以解决此问题.

So I think there is no generic solution to this problem.

我能想到的是以下内容:它在图形坐标中设置文本,然后在水平或垂直方向上调整轴的大小,以使轴和文本之间没有重叠.

Something I can come up with is the following: It sets the text in figure coordinates and then resizes the axes either in horizontal or in vertical direction such that there is no overlap between the axes and the text.

import matplotlib.pyplot as plt 
import matplotlib.transforms

fig = plt.figure(figsize = [3,2]) 
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')

def text_legend(ax, x0, y0, text, direction = "v", padpoints = 3, margin=1.,**kwargs):
    ha = kwargs.pop("ha", "right")
    va = kwargs.pop("va", "top")
    t = ax.figure.text(x0, y0, text, ha=ha, va=va, **kwargs) 
    otrans = ax.figure.transFigure

    plt.tight_layout(pad=0)
    ax.figure.canvas.draw()
    plt.tight_layout(pad=0)
    offs =  t._bbox_patch.get_boxstyle().pad * t.get_size() + margin # adding 1pt
    trans = otrans + \
            matplotlib.transforms.ScaledTranslation(-offs/72.,-offs/72.,fig.dpi_scale_trans)
    t.set_transform(trans)
    ax.figure.canvas.draw()

    ppar = [0,-padpoints/72.] if direction == "v" else [-padpoints/72.,0] 
    trans2 = matplotlib.transforms.ScaledTranslation(ppar[0],ppar[1],fig.dpi_scale_trans) + \
             ax.figure.transFigure.inverted() 
    tbox = trans2.transform(t._bbox_patch.get_window_extent())
    bbox = ax.get_position()
    if direction=="v":
        ax.set_position([bbox.x0, bbox.y0,bbox.width, tbox[0][1]-bbox.y0]) 
    else:
        ax.set_position([bbox.x0, bbox.y0,tbox[0][0]-bbox.x0, bbox.height]) 

# case 1: place text label at top right corner of figure (1,1). Adjust axes height.
#text_legend(ax, 1,1, 'my text here', bbox = dict(boxstyle = 'round'), )

# case 2: place text left of axes, (1, y), direction=="v"
text_legend(ax, 1., 0.8, 'my text here', margin=2., direction="h", bbox = dict(boxstyle = 'round') )

plt.savefig(__file__+'.pdf')
plt.show()

案例1(左)和案例2(右):

case 1 (left) and case 2 (right):


与图例进行相同操作稍微容易一些,因为我们可以直接使用bbox_to_anchor参数,而无需控制图例周围的花式框.


Doin the same with a legend is slightly easier, because we can directly use the bbox_to_anchor argument and don't need to control the fancy box around the legend.

import matplotlib.pyplot as plt 
import matplotlib.transforms

fig = plt.figure(figsize = [3.5,2]) 
ax = fig.add_subplot(111)
ax.set_title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
ax.plot([1,2,3], marker="o", label="quantity 1")
ax.plot([2,1.7,1.2], marker="s", label="quantity 2")

def legend(ax, x0=1,y0=1, direction = "v", padpoints = 3,**kwargs):
    otrans = ax.figure.transFigure
    t = ax.legend(bbox_to_anchor=(x0,y0), loc=1, bbox_transform=otrans,**kwargs)
    plt.tight_layout(pad=0)
    ax.figure.canvas.draw()
    plt.tight_layout(pad=0)
    ppar = [0,-padpoints/72.] if direction == "v" else [-padpoints/72.,0] 
    trans2=matplotlib.transforms.ScaledTranslation(ppar[0],ppar[1],fig.dpi_scale_trans)+\
             ax.figure.transFigure.inverted() 
    tbox = t.get_window_extent().transformed(trans2 )
    bbox = ax.get_position()
    if direction=="v":
        ax.set_position([bbox.x0, bbox.y0,bbox.width, tbox.y0-bbox.y0]) 
    else:
        ax.set_position([bbox.x0, bbox.y0,tbox.x0-bbox.x0, bbox.height]) 

# case 1: place text label at top right corner of figure (1,1). Adjust axes height.
#legend(ax, borderaxespad=0)
# case 2: place text left of axes, (1, y), direction=="h"
legend(ax,y0=0.8, direction="h", borderaxespad=0.2)

plt.savefig(__file__+'.pdf')
plt.show()


为什么72??72是每英寸的点数(ppi).这是一个固定的印刷单位,例如字体大小总是以磅为单位(例如12pt).由于matplotlib以相对于fontsize(即磅)为单位定义文本框的填充,因此我们需要使用72转换回英寸(然后显示坐标).此处未触摸默认的每英寸点数(dpi),但在fig.dpi_scale_trans中进行了说明.如果要更改dpi,则需要在创建图形和保存图形时确保已设置图形dpi(在plt.figure()plt.savefig()的调用中使用dpi=..).


Why 72? The 72 is the number of points per inch (ppi). This is a fixed typographic unit e.g. fontsizes are always given in points (like 12pt). Because matplotlib defines the padding of the text box in units relative to fontsize, which is points, we need to use 72 to transform back to inches (and then to display coordinates). The default dots per inch (dpi) is not touched here, but is accounted for in fig.dpi_scale_trans. If you want to change dpi you need to make sure the figure dpi is set when creating the figure as well as when saving it (use dpi=.. in the call to plt.figure() as well as plt.savefig()).

这篇关于创建具有精确大小且无填充的图形(以及轴外的图例)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆