创建具有精确大小且无填充的图形(以及轴外的图例)

Posted

技术标签:

【中文标题】创建具有精确大小且无填充的图形(以及轴外的图例)【英文标题】:Creating figure with exact size and no padding (and legend outside the axes) 【发布时间】:2017-08-17 02:05:32 【问题描述】:

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

要设置特定的图形大小,我只需使用plt.figure(figsize = [w, h]),并添加参数tight_layout = 'pad': 0 以删除填充。这工作得很好,即使我添加标题、y/x-labels 等也可以工作。示例:

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 文件。

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

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 时那样减小轴的大小。

例子:

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)

是否有任何解决方案可以涵盖大多数情况下在轴外带有图例的情况?

【问题讨论】:

【参考方案1】:

所以要求是:

    具有固定的预定义图形大小 在坐标区外添加文本标签或图例 轴和文本不能重叠 坐标轴连同标题和坐标轴标签紧紧地靠在图形边框上。

所以tight_layoutpad = 0 解决了 1. 和 4. 但与 2. 相矛盾。

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

使用bbox_inches = 'tight' 更改图形大小。矛盾 1.

所以我认为这个问题没有通用的解决方案。

我能想到的方法如下:它将文本设置为图形坐标,然后在水平或垂直方向调整轴的大小,以使轴和文本之间没有重叠。

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(右):


用图例做同样的事情稍微容易一些,因为我们可以直接使用bbox_to_anchor 参数,不需要控制图例周围的花哨的框。
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=..)。

【讨论】:

谢谢,这太棒了!您介意解释一下在某些地方出现的硬编码72 是什么吗?我猜这是默认 dpi,但我不确定。 我在答案中添加了一些解释。 再次感谢。有什么简单的方法可以让这个传奇人物工作吗? IE。在轴外放置图例时。 我在答案中添加了图例的案例。【参考方案2】:

截至matplotlib==3.1.3,您可以使用constrained_layout=True 来达到预期的效果。这目前是实验性的,但请参阅 docs 以获得非常有用的指南(以及专门针对图例的 section)。请注意,图例会从情节中窃取空间,但这是不可避免的。我发现只要图例相对于绘图的大小不占用太多空间,那么图形就会被保存而不会裁剪任何内容。

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(3, 2), constrained_layout=True)
ax.set_title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
ax.plot([0,1], [0,1], label='my text here')
ax.legend(loc='center left', bbox_to_anchor=(1.1, 0.5))
fig.savefig('figure03.pdf')

The saved figure.

【讨论】:

以上是关于创建具有精确大小且无填充的图形(以及轴外的图例)的主要内容,如果未能解决你的问题,请参考以下文章

Python以精确的像素大小保存matplotlib图形

图外的 JQPlot 图例

sklearn管道ValueError:除连接轴外的所有输入数组维度必须完全匹配

如何为控制多个散景图形添加一个图例?

ggplot 密度图 alpha 未在图例中呈现

ggplot图形上线条的动态形状和大小变化创建第二个图例[重复]