Matplotlib 全面使用指南 -- 在图形中排列多个轴 Arranging multiple Axes in a Figure

文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。

Matplotlib

在图形中排列多个轴

通常,一个图形中需要多个轴(Axes),通常以规则网格的形式排列。Matplotlib 提供了多种用于处理轴网格的工具,这些工具随着库的发展而不断演变。在这里,我们将讨论我们认为用户最常使用的工具,这些工具是如何组织轴的基础,以及提及一些较旧的工具。

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

概览

创建网格形状的轴组合

subplots

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

或者

subplot_mosaic

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

有时,自然会有多组不同的轴网格,Matplotlib 提供了 SubFigure 的概念:

SubFigure

图形内的虚拟图形。

基础工具

这些工具的基础是 GridSpecSubplotSpec 的概念:

GridSpec

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

SubplotSpec

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

逐个添加单个轴

上述函数可以在一次函数调用中创建所有轴。也可以逐个添加轴,这实际上是 Matplotlib 最初的工作方式。这样做通常不够优雅和灵活,但在交互式工作或放置自定义位置的轴时有时很有用:

add_axes

在由 [left, bottom, width, height] 指定的位置添加一个单个轴,位置以图形宽度或高度的比例表示。

subplotFigure.add_subplot

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

subplot2grid

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

作为一个简单的手动添加轴 ax 的示例,让我们向一个 4 英寸 x 3 英寸的图形中添加一个 3 英寸 x 2 英寸的轴。注意,子图的位置定义为 [left, bottom, width, height],以图形归一化单位表示:

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

img

创建网格的高级方法

基本 2x2 网格

我们可以使用 subplots 创建一个基本的 2x2 轴网格。它返回一个 Figure 实例和一个 Axes 对象数组。轴对象可以用来访问方法,以在轴上放置图形元素;这里我们使用 annotate,但其他示例可以是 plotpcolormesh 等。

fig, axs = plt.subplots(ncols=2, nrows=2, figsize=(5.5, 3.5),
                        layout="constrained")
# 在这里添加一个图形元素,例如在中间添加一个漂亮的标签...
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()')

img

我们将多次注释轴,因此让我们封装注释功能,而不是每次需要时都重复那一大段注释代码:

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()')

img

固定纵横比的轴网格

固定纵横比的轴在图像或地图中很常见。然而,它们在布局上存在挑战,因为对轴的大小施加了两组约束——它们必须适应图形,并且具有固定的纵横比。这会导致轴之间默认出现较大的间隔:

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

img

一种解决方法是将图形的纵横比调整为接近轴的纵横比,但这需要反复试验。Matplotlib 还提供了 layout="compressed",它可以与简单网格配合使用,以减少轴之间的间隔。(mpl_toolkits 还提供了 ImageGrid 来实现类似的效果,但使用了非标准的轴类)。

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

img

跨越网格行或列的轴

有时我们希望轴跨越网格的行或列。实际上有多种方法可以实现这一点,但最方便的可能是使用 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()')

img

下面将描述如何使用 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()')

img

嵌套轴布局

有时,拥有两个或多个可能彼此无关的轴网格会很有帮助。最简单的方法是使用 Figure.subfigures。注意,子图形布局是独立的,因此每个子图形中的轴脊柱不一定对齐。下面将介绍一种更冗长的方法,使用 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]')

img

还可以通过使用嵌套列表的 subplot_mosaic 来嵌套轴,这种方法不使用子图形,因此缺乏为每个子图形添加 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}]')

img

低级和高级网格方法

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

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

基本 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')

img

跨越网格行或列的轴

我们可以使用 NumPy 切片语法对 spec 数组进行索引,新轴将跨越该切片。这与 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')

img

手动调整 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')

img

使用 SubplotSpec 的嵌套布局

可以使用 subgridspec 创建类似于 subfigures 的嵌套布局。在这里,轴脊柱 对齐的。

注意,这也可以从更冗长的 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')

img

这是一个更复杂的嵌套 GridSpec 的示例:我们创建一个外层 4x4 网格,每个单元格包含一个内层 3x3 的轴网格。我们通过在每个内层 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):
        # 在网格中嵌套网格
        inner_grid = outer_grid[a, b].subgridspec(3, 3, wspace=0, hspace=0)
        axs = inner_grid.subplots()  # 为内层网格创建所有子图。
        for (c, d), ax in np.ndenumerate(axs):
            ax.plot(*squiggle_xy(a + 1, b + 1, c + 1, d + 1))
            ax.set(xticks=[], yticks=[])

# 只显示外层脊柱
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()

img

更多阅读

Download Jupyter notebook: arranging_axes.ipynb

Download Python source code: arranging_axes.py

Download zipped: arranging_axes.zip

风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

船长Q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值