注意
跳转至末尾 下载完整示例代码。
约束布局指南#
使用约束布局以整洁地将绘图适应到图中。
约束布局会自动调整子图,以确保刻度标签、图例和颜色条等装饰不重叠,同时仍保留用户请求的逻辑布局。
约束布局类似于紧凑布局,但灵活性大大增强。它支持放置在多个Axes上的颜色条(放置颜色条),嵌套布局(subfigures
)以及跨行或跨列的Axes(subplot_mosaic
),并力求使同一行或同一列中Axes的边框对齐。此外,压缩布局会尝试将固定纵横比的Axes靠得更近。这些功能都在本文档中描述,文末还会讨论一些实现细节。
约束布局通常需要在任何Axes添加到图之前激活。有两种方法可以做到:
使用其对应的参数:
subplots
、figure
、subplot_mosaic
,例如:plt.subplots(layout="constrained")
通过rcParams激活,例如:
plt.rcParams['figure.constrained_layout.use'] = True
这些将在以下各节中详细描述。
警告
调用tight_layout
会关闭约束布局!
简单示例#
在默认的Axes定位下,坐标轴标题、坐标轴标签或刻度标签有时可能会超出图的区域,从而被剪裁。
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)

为防止这种情况,需要调整Axes的位置。对于子图,可以通过使用Figure.subplots_adjust
手动调整子图参数来完成。然而,使用layout="constrained"
关键字参数指定图将自动进行调整。
fig, ax = plt.subplots(layout="constrained")
example_plot(ax, fontsize=24)

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

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

颜色条#
如果您使用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)

如果您将Axes列表(或其他可迭代容器)指定给colorbar
的ax
参数,约束布局将从指定的Axes中占用空间。
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)

如果您指定来自Axes网格内部的Axes列表,颜色条将适当地占用空间并留出间隙,但所有子图仍将保持相同大小。
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)

主标题#
约束布局也可以为主标题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')

图例#
图例可以放置在它们父轴的外部。约束布局旨在处理Axes.legend()
的这种情况。然而,约束布局(目前)不处理通过Figure.legend()
创建的图例。

然而,这会占用子图布局的空间

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

保存的文件如下所示

解决这种不便的更好方法是直接使用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

保存的文件如下所示

填充和间距#
Axes之间的填充在水平方向由w_pad和wspace控制,在垂直方向由h_pad和hspace控制。这些可以通过set
进行编辑。w/h_pad 是Axes周围的最小空间,单位为英寸
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)

子图之间的间距通过wspace和hspace进一步设置。这些值被指定为整个子图组大小的一部分。如果这些值小于w_pad或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.2,
wspace=0.2)

如果列数多于两列,wspace会在它们之间共享,因此在这里wspace被分成两部分,每列之间有0.1的wspace
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)

GridSpecs也有可选的hspace和wspace关键字参数,这些参数将代替约束布局设置的填充
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)

颜色条间距#
颜色条与其父轴相距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)

rcParams#
有五个rcParams可以设置,既可以在脚本中,也可以在matplotlibrc
文件中设置。它们都带有前缀figure.constrained_layout
use: 是否使用约束布局。默认值为False
w_pad, h_pad: Axes对象周围的填充。浮点数,单位为英寸。默认值为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)

与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)

更复杂的gridspec布局是可行的。请注意,这里我们使用了便利函数add_gridspec
和subgridspec
。
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)

请注意,在上面的示例中,左列和右列的垂直范围不同。如果我们希望两个网格的顶部和底部对齐,那么它们需要位于同一个gridspec中。我们还需要把这个图做得更大一些,以防止Axes收缩到零高度
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')

此示例使用两个gridspec,使颜色条仅与一组pcolor相关。请注意,由于这个原因,左列比右侧两列更宽。当然,如果您希望子图大小相同,则只需一个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)

Matplotlib现在提供了subfigures
,而不是使用subgridspecs,它们也适用于约束布局
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')

手动设置Axes位置#
手动设置Axes位置可能有一些很好的理由。手动调用set_position
将设置Axes,因此约束布局不再对其产生影响。(请注意,约束布局仍会为被移动的Axes留出空间)。
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])

固定纵横比Axes网格:“压缩”布局#
约束布局在Axes的“原始”位置网格上操作。然而,当Axes具有固定纵横比时,通常会使其中一边变短,并在缩短的方向上留下大间隙。在下面的例子中,Axes是方形的,但图相当宽,因此存在水平间隙
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'")

一个明显的解决方法是将图的大小调整得更接近方形,然而,精确地弥合这些间隙需要反复试验。对于简单的Axes网格,我们可以使用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'")

手动关闭约束布局#
约束布局通常在图的每次绘制时调整Axes的位置。如果您希望获得约束布局提供的间距但不希望它更新,那么请进行首次绘制,然后调用fig.set_layout_engine('none')
。这对于刻度标签长度可能变化的动画很有用。
请注意,对于使用工具栏的后端,约束布局在ZOOM
和PAN
GUI事件中会被关闭。这可以防止Axes在缩放和平移期间改变位置。
限制#
不兼容的函数#
约束布局可以与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('Homogeneous nrows, ncols')

但下面的例子会导致布局不佳
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')

类似地,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')

其他注意事项#
约束布局只考虑刻度标签、坐标轴标签、标题和图例。因此,其他艺术家可能会被裁剪或重叠。
它假设刻度标签、坐标轴标签和标题所需的额外空间与Axes的原始位置无关。这通常是正确的,但少数情况下并非如此。
不同的后端处理字体渲染的方式存在细微差异,因此结果不会是像素级别的完全相同。
当艺术家使用超出Axes边界的Axes坐标时,如果将其添加到Axes中,将导致不寻常的布局。这可以通过将艺术家直接添加到
Figure
使用add_artist()
来避免。请参阅ConnectionPatch
以获取示例。
调试#
约束布局可能会以某种意想不到的方式失败。因为它使用约束求解器,求解器可能会找到数学上正确的解决方案,但这可能并非用户所期望的。常见的失败模式是所有尺寸都收缩到其允许的最小值。如果发生这种情况,原因有以下两点:
您请求绘制的元素没有足够的空间。
存在一个错误——在这种情况下,请在matplotlib/matplotlib#issues提交问题。
如果存在错误,请提供一个自包含的示例进行报告,该示例不依赖外部数据或依赖项(除了numpy)。
算法说明#
约束算法相对简单,但由于我们可以布置图的复杂方式而具有一定的复杂性。
Matplotlib中的布局通过GridSpec
类以及gridspecs实现。gridspec是将图逻辑地划分为行和列,其中这些行和列中Axes的相对宽度由width_ratios和height_ratios设置。
在约束布局中,每个gridspec都会关联一个布局网格(layoutgrid)。*布局网格*包含一系列用于每列的left
和right
变量,以及用于每行的bottom
和top
变量,此外,它还有左、右、下、上四个方向的边距。在每一行中,底部/顶部边距会被加宽,直到该行中的所有装饰物都被容纳。对于列和左/右边距也是如此。
简单案例:一个Axes#
对于单个Axes,布局是直截了当的。图有一个包含一列一行的父布局网格,包含Axes的gridspec也有一个子布局网格,同样包含一行一列。为Axes每侧的“装饰物”留出了空间。在代码中,这通过do_constrained_layout()
中的条目实现,例如:
gridspec._layoutgrid[0, 0].edit_margin_min('left',
-bbox.x0 + pos.x0 + w_pad)
其中bbox
是Axes的紧密边界框,pos
是其位置。请注意,这四个边距如何包含Axes装饰物。
from matplotlib._layoutgrid import plot_children
fig, ax = plt.subplots(layout="constrained")
example_plot(ax, fontsize=24)
plot_children(fig)

简单案例:两个Axes#
当存在多个Axes时,它们的布局以简单的方式绑定。在此示例中,左侧Axes的装饰比右侧大得多,但它们共享一个底部边距,该边距被扩大以适应更长的xlabel。共享的顶部边距也是如此。左侧和右侧边距不共享,因此允许它们不同。

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

与Gridspec关联的颜色条#
如果颜色条属于网格的多个单元格,那么它会为每个单元格创建一个更大的边距
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)

不均匀大小的Axes#
在Gridspec布局中,有两种方法可以使Axes具有不均匀的大小,一是通过指定它们跨越Gridspec的行或列,二是通过指定宽度和高度比例。
此处使用第一种方法。请注意,中间的top
和bottom
边距不受左侧列的影响。这是算法的一个有意识的决定,导致右侧两个Axes高度相同,但并非左侧Axes高度的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)

一个需要精细处理的情况是,如果边距没有任何艺术家来限制其宽度。在下面的例子中,第0列的右边距和第3列的左边距没有边距艺术家来设置其宽度,因此我们取那些确实有艺术家的边距宽度的最大值。这使得所有Axes具有相同的大小
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()

脚本总运行时间: (0分钟22.366秒)