在图形中排列多个轴#

通常情况下,一个图形中需要多个 Axes,通常组织成一个规则的网格。Matplotlib 提供了多种工具来处理 Axes 的网格布局,这些工具随着库的发展而不断演进。在这里,我们将讨论我们认为用户最常使用的工具,这些工具是如何组织 Axes 的基础,并提及一些较旧的工具。

备注

Matplotlib 使用 Axes 来指代包含数据、x 轴和 y 轴、刻度、标签、标题等的绘图区域。更多详情请参见 图形的部分。另一个常用的术语是“子图”,它指的是与其他 Axes 对象在网格中的一个 Axes。

概述#

创建网格形状的 Axes 组合#

subplots

用于创建图形和轴网格的主要函数。它一次性创建并放置图形上的所有轴,并返回一个包含网格中轴句柄的对象数组。参见 Figure.subplots

subplot_mosaic

创建图形和轴网格的一种简单方法,具有额外的灵活性,即轴也可以跨越行或列。轴以带标签的字典形式返回,而不是数组。另请参见 Figure.subplot_mosaic复杂且语义化的图形组合 (subplot_mosaic)

有时,自然会有多个不同的坐标轴网格组,在这种情况下,Matplotlib 引入了 SubFigure 的概念:

SubFigure

图中的虚拟图形。

底层工具#

这些概念的基础是 GridSpecSubplotSpec:

GridSpec

指定子图将放置的网格的几何形状。需要设置网格的行数和列数。可选地,可以调整子图布局参数(例如,左、右等)。

SubplotSpec

指定子图在给定 GridSpec 中的位置。

一次添加单个坐标轴#

上述函数通过一次函数调用创建所有 Axes。也可以一次添加一个 Axes,这在 Matplotlib 的早期版本中是常用的方式。这样做通常不那么优雅和灵活,但在交互式工作或需要在自定义位置放置 Axes 时有时很有用:

add_axes

在指定位置 [left, bottom, height, width] 添加一个 Axes,位置以图形宽度和高度的分数表示。

subplotFigure.add_subplot

在图形上添加一个子图,使用基于1的索引(继承自Matlab)。可以通过指定网格单元范围来跨越列和行。

subplot2grid

类似于 pyplot.subplot,但使用基于0的索引和二维Python切片来选择单元格。

作为一个手动添加 Axes ax 的简单示例,让我们在一个 4 英寸 x 3 英寸的图形中添加一个 3 英寸 x 2 英寸的 Axes。注意,子图的位置是以图形归一化单位定义的 [左, 底, 宽, 高]:

import matplotlib.pyplot as plt
import numpy as np

w, h = 4, 3
margin = 0.5
fig = plt.figure(figsize=(w, h), facecolor='lightblue')
ax = fig.add_axes([margin / w, margin / h, (w - 2 * margin) / w,
                      (h - 2 * margin) / h])
arranging axes

用于制作网格的高级方法#

基本 2x2 网格#

我们可以使用 subplots 创建一个基本的 2×2 的 Axes 网格。它返回一个 Figure 实例和一个 Axes 对象的数组。Axes 对象可以用来访问在 Axes 上放置艺术家的方法;这里我们使用 annotate,但其他例子可能是 plotpcolormesh 等。

fig, axs = plt.subplots(ncols=2, nrows=2, figsize=(5.5, 3.5),
                        layout="constrained")
# add an artist, in this case a nice label in the middle...
for row in range(2):
    for col in range(2):
        axs[row, col].annotate(f'axs[{row}, {col}]', (0.5, 0.5),
                               transform=axs[row, col].transAxes,
                               ha='center', va='center', fontsize=18,
                               color='darkgrey')
fig.suptitle('plt.subplots()')
plt.subplots()

我们将标注许多 Axes,因此让我们封装标注,而不是每次需要时都使用那一大段标注代码:

def annotate_axes(ax, text, fontsize=18):
    ax.text(0.5, 0.5, text, transform=ax.transAxes,
            ha="center", va="center", fontsize=fontsize, color="darkgrey")

同样的效果可以通过 subplot_mosaic 实现,但返回类型是一个字典而不是数组,用户可以为键赋予有意义的含义。这里我们提供了两个列表,每个列表代表一行,列表中的每个元素代表一列的键。

fig, axd = plt.subplot_mosaic([['upper left', 'upper right'],
                               ['lower left', 'lower right']],
                              figsize=(5.5, 3.5), layout="constrained")
for k, ax in axd.items():
    annotate_axes(ax, f'axd[{k!r}]', fontsize=14)
fig.suptitle('plt.subplot_mosaic()')
plt.subplot_mosaic()

固定纵横比的轴网格#

固定纵横比的坐标轴在图像或地图中很常见。然而,它们在布局上提出了挑战,因为有两组约束作用于坐标轴的大小——既要适应图形,又要保持设定的纵横比。这通常会导致坐标轴之间出现较大的间隙:

fig, axs = plt.subplots(2, 2, layout="constrained",
                        figsize=(5.5, 3.5), facecolor='lightblue')
for ax in axs.flat:
    ax.set_aspect(1)
fig.suptitle('Fixed aspect Axes')
Fixed aspect Axes

解决这个问题的一种方法是改变图形的长宽比,使其接近Axes的长宽比,但这需要反复试验。Matplotlib还提供了``layout="compressed"``,它将适用于简单的网格,以减少Axes之间的间隙。(``mpl_toolkits``还提供了`~.mpl_toolkits.axes_grid1.axes_grid.ImageGrid`来实现类似的效果,但使用的是非标准的Axes类)。

fig, axs = plt.subplots(2, 2, layout="compressed", figsize=(5.5, 3.5),
                        facecolor='lightblue')
for ax in axs.flat:
    ax.set_aspect(1)
fig.suptitle('Fixed aspect Axes: compressed')
Fixed aspect Axes: compressed

跨越网格中行或列的轴#

有时我们希望坐标轴跨越网格的行或列。实际上有多种方法可以实现这一点,但最方便的方法可能是通过重复其中一个键来使用 subplot_mosaic

fig, axd = plt.subplot_mosaic([['upper left', 'right'],
                               ['lower left', 'right']],
                              figsize=(5.5, 3.5), layout="constrained")
for k, ax in axd.items():
    annotate_axes(ax, f'axd[{k!r}]', fontsize=14)
fig.suptitle('plt.subplot_mosaic()')
plt.subplot_mosaic()

请参阅以下内容,了解如何使用 GridSpecsubplot2grid 实现相同的功能。

网格中的可变宽度或高度#

subplotssubplot_mosaic 允许网格中的行具有不同的高度,列具有不同的宽度,使用 gridspec_kw 关键字参数。GridSpec 接受的间距参数可以传递给 subplotssubplot_mosaic

gs_kw = dict(width_ratios=[1.4, 1], height_ratios=[1, 2])
fig, axd = plt.subplot_mosaic([['upper left', 'right'],
                               ['lower left', 'right']],
                              gridspec_kw=gs_kw, figsize=(5.5, 3.5),
                              layout="constrained")
for k, ax in axd.items():
    annotate_axes(ax, f'axd[{k!r}]', fontsize=14)
fig.suptitle('plt.subplot_mosaic()')
plt.subplot_mosaic()

嵌套轴布局#

有时,拥有两个或多个可能不需要相互关联的 Axes 网格是有帮助的。实现这一点的最简单方法是使用 Figure.subfigures。请注意,子图布局是独立的,因此每个子图中的 Axes 脊线不一定对齐。有关使用 GridSpecFromSubplotSpec 实现相同效果的更详细方法,请参见下文。

fig = plt.figure(layout="constrained")
subfigs = fig.subfigures(1, 2, wspace=0.07, width_ratios=[1.5, 1.])
axs0 = subfigs[0].subplots(2, 2)
subfigs[0].set_facecolor('lightblue')
subfigs[0].suptitle('subfigs[0]\nLeft side')
subfigs[0].supxlabel('xlabel for subfigs[0]')

axs1 = subfigs[1].subplots(3, 1)
subfigs[1].suptitle('subfigs[1]')
subfigs[1].supylabel('ylabel for subfigs[1]')
arranging axes

也可以使用嵌套列表通过 subplot_mosaic 来嵌套 Axes。这种方法不像上面那样使用子图,因此缺乏为每个子图添加 suptitlesupxlabel 等的能力。相反,它是一个围绕下面描述的 subgridspec 方法的便捷包装。

inner = [['innerA'],
         ['innerB']]
outer = [['upper left',  inner],
          ['lower left', 'lower right']]

fig, axd = plt.subplot_mosaic(outer, layout="constrained")
for k, ax in axd.items():
    annotate_axes(ax, f'axd[{k!r}]')
arranging axes

低级和高级网格方法#

在内部,Axes 网格的排列是通过创建 GridSpecSubplotSpec 的实例来控制的。GridSpec 定义了一个(可能不均匀的)单元格网格。对 GridSpec 进行索引会返回一个覆盖一个或多个网格单元的 SubplotSpec,并可用于指定 Axes 的位置。

以下示例展示了如何使用低级方法通过 GridSpec 对象来排列 Axes。

基本 2x2 网格#

我们可以以与 plt.subplots(2, 2) 相同的方式实现一个 2x2 的网格:

fig = plt.figure(figsize=(5.5, 3.5), layout="constrained")
spec = fig.add_gridspec(ncols=2, nrows=2)

ax0 = fig.add_subplot(spec[0, 0])
annotate_axes(ax0, 'ax0')

ax1 = fig.add_subplot(spec[0, 1])
annotate_axes(ax1, 'ax1')

ax2 = fig.add_subplot(spec[1, 0])
annotate_axes(ax2, 'ax2')

ax3 = fig.add_subplot(spec[1, 1])
annotate_axes(ax3, 'ax3')

fig.suptitle('Manually added subplots using add_gridspec')
Manually added subplots using add_gridspec

跨越行或网格的轴#

我们可以使用 NumPy 切片语法 来索引 spec 数组,新的 Axes 将跨越该切片。这等同于 fig, axd = plt.subplot_mosaic([['ax0', 'ax0'], ['ax1', 'ax2']], ...)

fig = plt.figure(figsize=(5.5, 3.5), layout="constrained")
spec = fig.add_gridspec(2, 2)

ax0 = fig.add_subplot(spec[0, :])
annotate_axes(ax0, 'ax0')

ax10 = fig.add_subplot(spec[1, 0])
annotate_axes(ax10, 'ax10')

ax11 = fig.add_subplot(spec[1, 1])
annotate_axes(ax11, 'ax11')

fig.suptitle('Manually added subplots, spanning a column')
Manually added subplots, spanning a column

GridSpec 布局的手动调整#

当显式使用 GridSpec 时,你可以调整从 GridSpec 创建的子图的布局参数。请注意,此选项与 constrained layoutFigure.tight_layout 不兼容,因为它们都会忽略 leftright,并调整子图大小以填充图形。通常,这种手动放置需要迭代以确保轴的刻度标签不会重叠。

这些间距参数也可以作为 gridspec_kw 参数传递给 subplotssubplot_mosaic

fig = plt.figure(layout=None, facecolor='lightblue')
gs = fig.add_gridspec(nrows=3, ncols=3, left=0.05, right=0.75,
                      hspace=0.1, wspace=0.05)
ax0 = fig.add_subplot(gs[:-1, :])
annotate_axes(ax0, 'ax0')
ax1 = fig.add_subplot(gs[-1, :-1])
annotate_axes(ax1, 'ax1')
ax2 = fig.add_subplot(gs[-1, -1])
annotate_axes(ax2, 'ax2')
fig.suptitle('Manual gridspec with right=0.75')
Manual gridspec with right=0.75

使用 SubplotSpec 的嵌套布局#

你可以使用 subgridspec 创建类似于 subfigures 的嵌套布局。这里,Axes 的脊线 对齐的。

注意,这也可以从更详细的 gridspec.GridSpecFromSubplotSpec 中获得。

fig = plt.figure(layout="constrained")
gs0 = fig.add_gridspec(1, 2)

gs00 = gs0[0].subgridspec(2, 2)
gs01 = gs0[1].subgridspec(3, 1)

for a in range(2):
    for b in range(2):
        ax = fig.add_subplot(gs00[a, b])
        annotate_axes(ax, f'axLeft[{a}, {b}]', fontsize=10)
        if a == 1 and b == 1:
            ax.set_xlabel('xlabel')
for a in range(3):
    ax = fig.add_subplot(gs01[a])
    annotate_axes(ax, f'axRight[{a}, {b}]')
    if a == 2:
        ax.set_ylabel('ylabel')

fig.suptitle('nested gridspecs')
nested gridspecs

这是一个更复杂的嵌套 GridSpec 示例:我们创建一个外部的 4x4 网格,每个单元格包含一个内部的 3x3 Axes 网格。我们通过隐藏每个内部 3x3 网格中的适当脊线来勾勒出外部的 4x4 网格。

def squiggle_xy(a, b, c, d, i=np.arange(0.0, 2*np.pi, 0.05)):
    return np.sin(i*a)*np.cos(i*b), np.sin(i*c)*np.cos(i*d)

fig = plt.figure(figsize=(8, 8), layout='constrained')
outer_grid = fig.add_gridspec(4, 4, wspace=0, hspace=0)

for a in range(4):
    for b in range(4):
        # gridspec inside gridspec
        inner_grid = outer_grid[a, b].subgridspec(3, 3, wspace=0, hspace=0)
        axs = inner_grid.subplots()  # Create all subplots for the inner grid.
        for (c, d), ax in np.ndenumerate(axs):
            ax.plot(*squiggle_xy(a + 1, b + 1, c + 1, d + 1))
            ax.set(xticks=[], yticks=[])

# show only the outside spines
for ax in fig.get_axes():
    ss = ax.get_subplotspec()
    ax.spines.top.set_visible(ss.is_first_row())
    ax.spines.bottom.set_visible(ss.is_last_row())
    ax.spines.left.set_visible(ss.is_first_col())
    ax.spines.right.set_visible(ss.is_last_col())

plt.show()
arranging axes

更多阅读#

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

由 Sphinx-Gallery 生成的图库