约束布局指南#

使用约束布局可以干净地将绘图拟合到您的图形中。

约束布局会自动调整子图,使得刻度标签、图例和颜色条等装饰不会重叠,同时仍然保留用户请求的逻辑布局。

约束布局类似于紧密布局,但更加灵活。它可以处理放置在多个坐标轴上的颜色条(放置颜色条),嵌套布局(子图)以及跨行或列的坐标轴(子图镶嵌),努力对齐同一行或列中的坐标轴的脊柱。此外,压缩布局将尝试将固定纵横比的坐标轴更紧密地移动到一起。本文档中描述了这些功能,以及最后讨论的一些实现细节

约束布局通常需要在将任何坐标轴添加到图形之前激活。有两种方法可以做到这一点:

这些在以下各节中详细描述。

警告

调用 tight_layout 将关闭约束布局

简单示例#

使用默认的坐标轴定位,坐标轴标题、坐标轴标签或刻度标签有时会超出图形区域,因此会被裁剪。

import matplotlib.pyplot as plt
import numpy as np

import matplotlib.colors as mcolors
import matplotlib.gridspec as gridspec

plt.rcParams['savefig.facecolor'] = "0.8"
plt.rcParams['figure.figsize'] = 4.5, 4.
plt.rcParams['figure.max_open_warning'] = 50


def example_plot(ax, fontsize=12, hide_labels=False):
    ax.plot([1, 2])

    ax.locator_params(nbins=3)
    if hide_labels:
        ax.set_xticklabels([])
        ax.set_yticklabels([])
    else:
        ax.set_xlabel('x-label', fontsize=fontsize)
        ax.set_ylabel('y-label', fontsize=fontsize)
        ax.set_title('Title', fontsize=fontsize)

fig, ax = plt.subplots(layout=None)
example_plot(ax, fontsize=24)
Title

为了防止这种情况,需要调整坐标轴的位置。对于子图,可以通过使用 Figure.subplots_adjust 调整子图参数来手动完成。但是,使用 layout="constrained" 关键字参数指定您的图形将自动进行调整。

fig, ax = plt.subplots(layout="constrained")
example_plot(ax, fontsize=24)
Title

当您有多个子图时,通常会看到不同坐标轴的标签相互重叠。

fig, axs = plt.subplots(2, 2, layout=None)
for ax in axs.flat:
    example_plot(ax)
Title, Title, Title, Title

在调用 plt.subplots 时指定 layout="constrained" 会使布局受到适当的约束。

fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
    example_plot(ax)
Title, Title, Title, Title

颜色条#

如果使用 Figure.colorbar 创建颜色条,则需要为其留出空间。约束布局会自动执行此操作。请注意,如果您指定 use_gridspec=True,它将被忽略,因为此选项旨在通过 tight_layout 改进布局。

注意

对于 pcolormesh 关键字参数(pc_kwargs),我们使用字典来保持本文档中的调用一致。

arr = np.arange(100).reshape((10, 10))
norm = mcolors.Normalize(vmin=0., vmax=100.)
# see note above: this makes all pcolormesh calls consistent:
pc_kwargs = {'rasterized': True, 'cmap': 'viridis', 'norm': norm}
fig, ax = plt.subplots(figsize=(4, 4), layout="constrained")
im = ax.pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=ax, shrink=0.6)
constrainedlayout guide

如果您将坐标轴列表(或其他可迭代容器)指定给 colorbarax 参数,则约束布局将从指定的坐标轴中获取空间。

fig, axs = plt.subplots(2, 2, figsize=(4, 4), layout="constrained")
for ax in axs.flat:
    im = ax.pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=axs, shrink=0.6)
constrainedlayout guide

如果您从坐标轴网格内部指定坐标轴列表,则颜色条将适当窃取空间,并留下间隙,但所有子图的大小仍将相同。

fig, axs = plt.subplots(3, 3, figsize=(4, 4), layout="constrained")
for ax in axs.flat:
    im = ax.pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=axs[1:, 1], shrink=0.8)
fig.colorbar(im, ax=axs[:, -1], shrink=0.6)
constrainedlayout guide

Suptitle#

约束布局还可以为 suptitle 腾出空间。

fig, axs = plt.subplots(2, 2, figsize=(4, 4), layout="constrained")
for ax in axs.flat:
    im = ax.pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=axs, shrink=0.6)
fig.suptitle('Big Suptitle')
Big Suptitle

图例#

图例可以放置在其父坐标轴之外。约束布局旨在为 Axes.legend() 处理此问题。但是,约束布局处理通过 Figure.legend() 创建的图例(尚未)。

fig, ax = plt.subplots(layout="constrained")
ax.plot(np.arange(10), label='This is a plot')
ax.legend(loc='center left', bbox_to_anchor=(0.8, 0.5))
constrainedlayout guide

但是,这将从子图布局中窃取空间

fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained")
axs[0].plot(np.arange(10))
axs[1].plot(np.arange(10), label='This is a plot')
axs[1].legend(loc='center left', bbox_to_anchor=(0.8, 0.5))
constrainedlayout guide

为了使图例或其他艺术家从子图布局中窃取空间,我们可以 leg.set_in_layout(False)。当然,这可能意味着图例最终会被裁剪,但如果随后使用 fig.savefig('outname.png', bbox_inches='tight') 调用绘图,则可能很有用。但是请注意,图例的 get_in_layout 状态必须再次切换才能使保存的文件正常工作,并且如果我们希望在打印之前让约束布局调整坐标轴的大小,则必须手动触发绘制。

fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained")

axs[0].plot(np.arange(10))
axs[1].plot(np.arange(10), label='This is a plot')
leg = axs[1].legend(loc='center left', bbox_to_anchor=(0.8, 0.5))
leg.set_in_layout(False)
# trigger a draw so that constrained layout is executed once
# before we turn it off when printing....
fig.canvas.draw()
# we want the legend included in the bbox_inches='tight' calcs.
leg.set_in_layout(True)
# we don't want the layout to change at this point.
fig.set_layout_engine('none')
try:
    fig.savefig('../../../doc/_static/constrained_layout_1b.png',
                bbox_inches='tight', dpi=100)
except FileNotFoundError:
    # this allows the script to keep going if run interactively and
    # the directory above doesn't exist
    pass
constrainedlayout guide

保存的文件如下所示:

../../../_images/constrained_layout_1b.png

解决这种尴尬的更好方法是简单地使用 Figure.legend 提供的方法

fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained")
axs[0].plot(np.arange(10))
lines = axs[1].plot(np.arange(10), label='This is a plot')
labels = [l.get_label() for l in lines]
leg = fig.legend(lines, labels, loc='center left',
                 bbox_to_anchor=(0.8, 0.5), bbox_transform=axs[1].transAxes)
try:
    fig.savefig('../../../doc/_static/constrained_layout_2b.png',
                bbox_inches='tight', dpi=100)
except FileNotFoundError:
    # this allows the script to keep going if run interactively and
    # the directory above doesn't exist
    pass
constrainedlayout guide

保存的文件如下所示:

../../../_images/constrained_layout_2b.png

填充和间距#

坐标轴之间的填充在水平方向由 w_padwspace 控制,在垂直方向由 h_padhspace 控制。这些可以通过 set 编辑。w/h_pad 是坐标轴周围的最小空间,单位为英寸

fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
    example_plot(ax, hide_labels=True)
fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0,
                            wspace=0)
constrainedlayout guide

子图之间的间距由 wspacehspace 进一步设置。这些被指定为整个子图组大小的一部分。如果这些值小于 w_padh_pad,则改用固定填充。请注意,在下面,边缘的空间与上面相比没有变化,但是子图之间的空间发生了变化。

fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
    example_plot(ax, hide_labels=True)
fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2,
                            wspace=0.2)
constrainedlayout guide

如果列数超过两列,则 wspace 在它们之间共享,因此此处 wspace 分为两部分,每列之间 wspace 为 0.1

fig, axs = plt.subplots(2, 3, layout="constrained")
for ax in axs.flat:
    example_plot(ax, hide_labels=True)
fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2,
                            wspace=0.2)
constrainedlayout guide

GridSpecs 也有可选的 hspacewspace 关键字参数,它们将代替 约束布局 设置的填充

fig, axs = plt.subplots(2, 2, layout="constrained",
                        gridspec_kw={'wspace': 0.3, 'hspace': 0.2})
for ax in axs.flat:
    example_plot(ax, hide_labels=True)
# this has no effect because the space set in the gridspec trumps the
# space set in *constrained layout*.
fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.0,
                            wspace=0.0)
constrainedlayout guide

颜色条的间距#

颜色条与其父级之间的距离为 pad,其中 pad 是父级宽度的某个比例。然后,到下一个子图的间距由 w/hspace 给出。

fig, axs = plt.subplots(2, 2, layout="constrained")
pads = [0, 0.05, 0.1, 0.2]
for pad, ax in zip(pads, axs.flat):
    pc = ax.pcolormesh(arr, **pc_kwargs)
    fig.colorbar(pc, ax=ax, shrink=0.6, pad=pad)
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.set_title(f'pad: {pad}')
fig.get_layout_engine().set(w_pad=2 / 72, h_pad=2 / 72, hspace=0.2,
                            wspace=0.2)
pad: 0, pad: 0.05, pad: 0.1, pad: 0.2

rcParams#

有五个 rcParams 可以设置,可以在脚本中或在 matplotlibrc 文件中设置。它们都带有前缀 figure.constrained_layout

  • use:是否使用约束布局。默认为 False

  • w_pad, h_pad:轴对象周围的填充。浮点数,表示英寸。默认值为 3./72 英寸(3 点)

  • wspace, hspace:子图组之间的空间。浮点数,表示被分隔的子图宽度的比例。默认值为 0.02。

plt.rcParams['figure.constrained_layout.use'] = True
fig, axs = plt.subplots(2, 2, figsize=(3, 3))
for ax in axs.flat:
    example_plot(ax)
Title, Title, Title, Title

与 GridSpec 一起使用#

约束布局 旨在与 subplots()subplot_mosaic()GridSpec()add_subplot() 一起使用。

请注意,在下文中 layout="constrained"

plt.rcParams['figure.constrained_layout.use'] = False
fig = plt.figure(layout="constrained")

gs1 = gridspec.GridSpec(2, 1, figure=fig)
ax1 = fig.add_subplot(gs1[0])
ax2 = fig.add_subplot(gs1[1])

example_plot(ax1)
example_plot(ax2)
Title, Title

可以实现更复杂的 gridspec 布局。请注意,这里我们使用了方便函数 add_gridspecsubgridspec

fig = plt.figure(layout="constrained")

gs0 = fig.add_gridspec(1, 2)

gs1 = gs0[0].subgridspec(2, 1)
ax1 = fig.add_subplot(gs1[0])
ax2 = fig.add_subplot(gs1[1])

example_plot(ax1)
example_plot(ax2)

gs2 = gs0[1].subgridspec(3, 1)

for ss in gs2:
    ax = fig.add_subplot(ss)
    example_plot(ax)
    ax.set_title("")
    ax.set_xlabel("")

ax.set_xlabel("x-label", fontsize=12)
Title, Title

请注意,在上面的示例中,左右列不具有相同的垂直范围。如果我们希望两个网格的顶部和底部对齐,则它们需要位于相同的 gridspec 中。我们也需要将此图形放大,以便轴不会塌陷为零高度

fig = plt.figure(figsize=(4, 6), layout="constrained")

gs0 = fig.add_gridspec(6, 2)

ax1 = fig.add_subplot(gs0[:3, 0])
ax2 = fig.add_subplot(gs0[3:, 0])

example_plot(ax1)
example_plot(ax2)

ax = fig.add_subplot(gs0[0:2, 1])
example_plot(ax, hide_labels=True)
ax = fig.add_subplot(gs0[2:4, 1])
example_plot(ax, hide_labels=True)
ax = fig.add_subplot(gs0[4:, 1])
example_plot(ax, hide_labels=True)
fig.suptitle('Overlapping Gridspecs')
Overlapping Gridspecs, Title, Title

此示例使用两个 gridspec,使颜色条仅与一组 pcolors 相关。请注意,由于此原因,左列比右侧的两列更宽。当然,如果您希望子图大小相同,则只需要一个 gridspec。请注意,使用 subfigures 也可以实现相同的效果。

fig = plt.figure(layout="constrained")
gs0 = fig.add_gridspec(1, 2, figure=fig, width_ratios=[1, 2])
gs_left = gs0[0].subgridspec(2, 1)
gs_right = gs0[1].subgridspec(2, 2)

for gs in gs_left:
    ax = fig.add_subplot(gs)
    example_plot(ax)
axs = []
for gs in gs_right:
    ax = fig.add_subplot(gs)
    pcm = ax.pcolormesh(arr, **pc_kwargs)
    ax.set_xlabel('x-label')
    ax.set_ylabel('y-label')
    ax.set_title('title')
    axs += [ax]
fig.suptitle('Nested plots using subgridspec')
fig.colorbar(pcm, ax=axs)
Nested plots using subgridspec, Title, Title, title, title, title, title

Matplotlib 现在提供了 subfigures,而不是使用子网格规格,它也可以与约束布局一起使用

fig = plt.figure(layout="constrained")
sfigs = fig.subfigures(1, 2, width_ratios=[1, 2])

axs_left = sfigs[0].subplots(2, 1)
for ax in axs_left.flat:
    example_plot(ax)

axs_right = sfigs[1].subplots(2, 2)
for ax in axs_right.flat:
    pcm = ax.pcolormesh(arr, **pc_kwargs)
    ax.set_xlabel('x-label')
    ax.set_ylabel('y-label')
    ax.set_title('title')
fig.colorbar(pcm, ax=axs_right)
fig.suptitle('Nested plots using subfigures')
Nested plots using subfigures, Title, Title, title, title, title, title

手动设置轴的位置#

可能存在手动设置轴位置的充分理由。手动调用 set_position 将设置轴,以便约束布局对其不再有任何影响。(请注意,约束布局仍然会为移动的轴保留空间)。

fig, axs = plt.subplots(1, 2, layout="constrained")
example_plot(axs[0], fontsize=12)
axs[1].set_position([0.2, 0.2, 0.4, 0.4])
Title

固定纵横比轴的网格:“压缩”布局#

约束布局在轴的“原始”位置网格上运行。但是,当轴具有固定的纵横比时,通常会使一侧变短,并在缩短的方向上留下较大的间隙。在以下示例中,轴是正方形的,但图形很宽,因此存在水平间隙

fig, axs = plt.subplots(2, 2, figsize=(5, 3),
                        sharex=True, sharey=True, layout="constrained")
for ax in axs.flat:
    ax.imshow(arr)
fig.suptitle("fixed-aspect plots, layout='constrained'")
fixed-aspect plots, layout='constrained'

解决此问题的一种明显方法是使图形大小更接近正方形,但是,要精确闭合间隙,需要反复试验。对于简单的轴网格,我们可以使用 layout="compressed" 来为我们完成这项工作

fig, axs = plt.subplots(2, 2, figsize=(5, 3),
                        sharex=True, sharey=True, layout='compressed')
for ax in axs.flat:
    ax.imshow(arr)
fig.suptitle("fixed-aspect plots, layout='compressed'")
fixed-aspect plots, layout='compressed'

手动关闭约束布局#

约束布局通常会在每次绘制图形时调整轴的位置。如果您想要获取约束布局提供的间距,但不希望它更新,则可以执行初始绘制,然后调用 fig.set_layout_engine('none')。这对于刻度标签可能会更改长度的动画可能很有用。

请注意,对于使用工具栏的后端,约束布局对于 ZOOMPAN GUI 事件已关闭。这可以防止轴在缩放和平移期间更改位置。

局限性#

不兼容的函数#

约束布局将与 pyplot.subplot 一起使用,但前提是每次调用时行数和列数都相同。原因是每次调用 pyplot.subplot 时,如果几何形状不同,将创建一个新的 GridSpec 实例,以及约束布局。因此,以下代码可以正常工作

fig = plt.figure(layout="constrained")

ax1 = plt.subplot(2, 2, 1)
ax2 = plt.subplot(2, 2, 3)
# third Axes that spans both rows in second column:
ax3 = plt.subplot(2, 2, (2, 4))

example_plot(ax1)
example_plot(ax2)
example_plot(ax3)
plt.suptitle('Homogenous nrows, ncols')
Homogenous nrows, ncols, Title, Title, Title

但是,以下代码会导致布局不佳

fig = plt.figure(layout="constrained")

ax1 = plt.subplot(2, 2, 1)
ax2 = plt.subplot(2, 2, 3)
ax3 = plt.subplot(1, 2, 2)

example_plot(ax1)
example_plot(ax2)
example_plot(ax3)
plt.suptitle('Mixed nrows, ncols')
Mixed nrows, ncols, Title, Title, Title

同样,subplot2grid 的工作方式也存在相同的限制,即 nrows 和 ncols 不能更改,布局才能看起来不错。

fig = plt.figure(layout="constrained")

ax1 = plt.subplot2grid((3, 3), (0, 0))
ax2 = plt.subplot2grid((3, 3), (0, 1), colspan=2)
ax3 = plt.subplot2grid((3, 3), (1, 0), colspan=2, rowspan=2)
ax4 = plt.subplot2grid((3, 3), (1, 2), rowspan=2)

example_plot(ax1)
example_plot(ax2)
example_plot(ax3)
example_plot(ax4)
fig.suptitle('subplot2grid')
subplot2grid, Title, Title, Title, Title

其他注意事项#

  • 约束布局仅考虑刻度标签、轴标签、标题和图例。因此,其他艺术家可能会被剪切,也可能会重叠。

  • 它假定刻度标签、轴标签和标题所需的额外空间与轴的原始位置无关。这通常是正确的,但在极少数情况下并非如此。

  • 后端处理渲染字体的方式略有不同,因此结果不会完全相同。

  • 使用延伸超出轴边界的轴坐标的艺术家,在添加到轴时会导致不寻常的布局。可以通过使用 add_artist() 将艺术家直接添加到 Figure 来避免这种情况。有关示例,请参见 ConnectionPatch

调试#

约束布局可能会以一些意想不到的方式失败。因为它使用约束求解器,所以求解器可以找到在数学上是正确的,但完全不是用户想要的解决方案。通常的失败模式是所有大小都塌陷为其允许的最小值。如果发生这种情况,则有两个原因之一

  1. 没有足够的空间来绘制您请求的元素。

  2. 存在错误 - 在这种情况下,请在 matplotlib/matplotlib#issues 上提交问题。

如果存在错误,请报告一个独立的示例,该示例不需要外部数据或依赖项(numpy 除外)。

有关算法的说明#

约束算法相对简单,但由于我们可以布局图形的方式复杂,因此具有一定的复杂性。

Matplotlib 中的布局通过 GridSpec 类使用网格规范进行。网格规范是将图形逻辑划分为行和列,并且这些行和列中轴的相对宽度由 width_ratiosheight_ratios 设置。

约束布局中,每个网格规范都获得一个与之关联的 layoutgridlayoutgrid 对于每列都有一系列的 leftright 变量,对于每行都有一系列的 bottomtop 变量,此外,它对于左、右、下和上都有一个边距。在每一行中,底/上边距会加宽,直到容纳该行中的所有装饰器为止。同样,对于列和左/右边距。

简单情况:一个轴#

对于单个坐标轴(Axes),布局非常直接。图形(figure)有一个父布局网格(layoutgrid),包含一列和一行,而网格规格(gridspec)有一个子布局网格,包含坐标轴,也包含一行和一列。坐标轴的每一侧都预留了“装饰”空间。在代码中,这是通过 do_constrained_layout() 中的条目来实现的,例如

gridspec._layoutgrid[0, 0].edit_margin_min('left',
      -bbox.x0 + pos.x0 + w_pad)

其中 bbox 是坐标轴的紧凑边界框,而 pos 是其位置。请注意,四个边距如何包围坐标轴的装饰。

from matplotlib._layoutgrid import plot_children

fig, ax = plt.subplots(layout="constrained")
example_plot(ax, fontsize=24)
plot_children(fig)
Title

简单示例:两个坐标轴#

当有多个坐标轴时,它们的布局以简单的方式绑定在一起。在此示例中,左侧坐标轴的装饰比右侧大得多,但它们共享一个底部边距,该边距被放大以容纳更大的 xlabel。顶部共享边距也是如此。左右边距不共享,因此允许不同。

fig, ax = plt.subplots(1, 2, layout="constrained")
example_plot(ax[0], fontsize=32)
example_plot(ax[1], fontsize=8)
plot_children(fig)
Title, Title

两个坐标轴和颜色条#

颜色条只是另一个扩展父布局网格单元格边距的项目

fig, ax = plt.subplots(1, 2, layout="constrained")
im = ax[0].pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=ax[0], shrink=0.6)
im = ax[1].pcolormesh(arr, **pc_kwargs)
plot_children(fig)
constrainedlayout guide

与网格规格关联的颜色条#

如果颜色条属于网格的多个单元格,则它会为每个单元格设置更大的边距

fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
    im = ax.pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=axs, shrink=0.6)
plot_children(fig)
constrainedlayout guide

大小不均匀的坐标轴#

有两种方法可以在网格规格布局中使坐标轴大小不均匀,一种是指定它们跨越网格规格的行或列,另一种是指定宽度和高度比。

这里使用了第一种方法。请注意,中间的 topbottom 边距不受左侧列的影响。这是该算法的有意决定,并导致右侧的两个坐标轴具有相同的高度,但它不是左侧坐标轴高度的 1/2。这与没有约束布局gridspec 的工作方式一致。

fig = plt.figure(layout="constrained")
gs = gridspec.GridSpec(2, 2, figure=fig)
ax = fig.add_subplot(gs[:, 0])
im = ax.pcolormesh(arr, **pc_kwargs)
ax = fig.add_subplot(gs[0, 1])
im = ax.pcolormesh(arr, **pc_kwargs)
ax = fig.add_subplot(gs[1, 1])
im = ax.pcolormesh(arr, **pc_kwargs)
plot_children(fig)
constrainedlayout guide

一种需要精细处理的情况是,如果边距没有任何艺术家约束其宽度。在下面的情况中,第 0 列的右边距和第 3 列的左边距没有边距艺术家来设置其宽度,因此我们采用确实有艺术家的边距宽度的最大值。这使得所有坐标轴都具有相同的大小

fig = plt.figure(layout="constrained")
gs = fig.add_gridspec(2, 4)
ax00 = fig.add_subplot(gs[0, 0:2])
ax01 = fig.add_subplot(gs[0, 2:])
ax10 = fig.add_subplot(gs[1, 1:3])
example_plot(ax10, fontsize=14)
plot_children(fig)
plt.show()
Title

脚本的总运行时间:(0 分 23.357 秒)

由 Sphinx-Gallery 生成的图库