怎么用matplotlib3.10的subfigures画复杂组合图?

Luna
Luna
发布时间:2026-11-13 | 5人浏览过
原创

介绍如何使用 Matplotlib 3.10 的 subfigures、add_subfigure 与 GridSpec 绘制多区域、不对称及嵌套的复杂组合图,并解决局部 colorbar、共享图例与排版冲突问题。

在 Matplotlib 3.10 中绘制包含多组坐标系、独立区域标题和局部色条(colorbar)的复杂组合图,核心做法是在创建画布时启用 layout="constrained",再通过 fig.subfigures() 或 GridSpec 搭配 fig.add_subfigure() 把整张画布切分为若干互不干扰的 SubFigure 子画布。每个 SubFigure 内部可以单独调用 subplots()、suptitle()、legend() 和 colorbar(),自动避让边距而不会挤压其他区域的坐标系对齐。

先确认环境与 subfigures 解决的具体问题

很多多子图图形会遇到这样的问题:如果整张图只用 plt.subplots() 或单一的 GridSpec,所有子图都共享同一个 Figure。一旦你想给左侧两张图加一个共同小标题、给右侧热力图单独放一条色条、或者把两组数据用浅灰底色框起来区分,色条和标签往往会打乱其他列子图的宽度对齐。

subfigures 把“区域分组”从 Axes 层面提升到了虚拟子画布层面。在开始写代码前,先在终端确认当前 Python 环境中的 Matplotlib 版本是否为 3.10.x(该接口自 3.4 引入并在后续版本持续完善,与 layout="constrained" 配合最稳定):

python -c "import matplotlib; print(matplotlib.__version__)"
示例输出:运行后终端若显示 3.10.0 或 3.10.x,说明可以直接使用本文的所有写法;若低于 3.5,建议将 layout="constrained" 写作 constrained_layout=True 或先升级包版本。

理解 Figure、SubFigure 与 Axes 的三层结构

用 subfigures 组织复杂组合图时,整张图不再是“画布直接装坐标系”的两层结构,而是分成三层:最外层是唯一的 Figure,中间一层是多个 SubFigure 区域,最内层才是实际画曲线、散点或热力图的 Axes。

这种三层结构带来的最大变化是“作用域隔离”。调用 subfig.colorbar()、subfig.suptitle() 或 subfig.set_facecolor() 时,影响范围只局限在该 SubFigure 内部,不会越界去压缩相邻子图区的空间。

怎么用matplotlib3.10的subfigures画复杂组合图?
图 1:Matplotlib 中 Figure、SubFigure 与 Axes 的三层容器模型及局部作用域隔离示意图。

规则分组:用 subfigures() 拆分网格与设置独立标题

如果组合图可以按规则的行列划分成几个模块——例如左边一列展示“信号与残差”,右边一列展示“频谱分布”——直接使用 fig.subfigures(nrows, ncols) 是最快的方式。它支持 width_ratios 和 height_ratios 控制各区块的宽高比例。

在代码里需要注意一个容易踩坑的参数:squeeze。默认情况下 squeeze=True,当 nrows=1, ncols=1 时会直接返回一个 SubFigure 对象;当其中一维是 1 时返回一维数组;两维都大于 1 时才返回二维数组。如果希望代码索引方式固定,建议显式传入 squeeze=False,统一用 subfigs[row, col] 取值。

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(7)
x = np.linspace(0, 10, 120)
y_obs = np.sin(x) + rng.normal(0, 0.15, size=x.size)
y_fit = np.sin(x)
residual = y_obs - y_fit

fig = plt.figure(figsize=(10, 4.8), layout="constrained")
subfigs = fig.subfigures(1, 2, width_ratios=[1.3, 1.0], wspace=0.06)

# 左侧 SubFigure:上下两张共享 x 轴的子图
subfigs[0].set_facecolor("#F8F9FA")
axs_left = subfigs[0].subplots(2, 1, sharex=True, height_ratios=[2, 1])
axs_left[0].plot(x, y_fit, color="#477DAD", linewidth=2, label="拟合曲线")
axs_left[0].scatter(x, y_obs, color="#4F6272", s=16, alpha=0.75, label="观测样本")
axs_left[0].set_ylabel("信号幅值")
axs_left[0].legend(loc="upper right", frameon=False)

axs_left[1].bar(x, residual, width=0.07, color="#4F6272", alpha=0.8)
axs_left[1].axhline(0, color="#477DAD", linewidth=1, linestyle="--")
axs_left[1].set_ylabel("残差")
axs_left[1].set_xlabel("时间 (s)")
subfigs[0].suptitle("A. 时域拟合与残差对比", fontsize=12, fontweight="bold")

# 右侧 SubFigure:独立的直方分布图
subfigs[1].set_facecolor("#F1F5F9")
ax_right = subfigs[1].subplots(1, 1)
ax_right.hist(residual, bins=16, color="#477DAD", edgecolor="#FFFFFF", alpha=0.88)
ax_right.set_xlabel("残差区间")
ax_right.set_ylabel("频次")
subfigs[1].suptitle("B. 残差分布统计", fontsize=12, fontweight="bold")

fig.suptitle("实验测量与误差分析总览", fontsize=14, fontweight="bold")
plt.show()
示例运行效果说明:执行后将生成左右两栏画布(宽比 1.3:1)。左侧浅灰底色区域内包含上下对齐的折线图与残差柱状图,顶部居中显示“A. 时域拟合与残差对比”;右侧略深底色区域显示直方图及“B. 残差分布统计”;整张图最上方居中显示总标题“实验测量与误差分析总览”。

不对称与嵌套布局:GridSpec 搭配 add_subfigure()

实际报告和论文插图往往不是均匀的行列网格。比如左侧一整列需要放一张跨两行的高大主图,右侧上下拆成两张小图;或者上方是一条横跨全宽的概览曲线,下方拆成三列对比。这类不对称布局有两种实现办法:

  • 方法一:先建 GridSpec 再用 fig.add_subfigure(gs[...])。适合跨行、跨列的不规则区域划分,切片语法与普通 GridSpec 完全一致(如 gs[:, 0] 表示占据所有行的第 0 列)。
  • 方法二:在 SubFigure 内部继续调用 subfig.subfigures() 进行嵌套。适合先按大区块切分,再在某个大区块内部做更细的独立分组和局部着色。
怎么用matplotlib3.10的subfigures画复杂组合图?
图 2:从画布初始化、分区切分到局部元素填充与最终导出的四步操作路径。

下面这段代码演示如何用 GridSpec 搭配 fig.add_subfigure() 搭建“左一右二”的不对称结构,并在右侧上方区块中继续嵌套 subfigures():

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(10, 6), layout="constrained")
gs = fig.add_gridspec(2, 2, width_ratios=[1.2, 1.4], height_ratios=[1, 1.1])

# 左侧跨两行的主区
sf_left = fig.add_subfigure(gs[:, 0])
ax_main = sf_left.subplots(1, 1)
ax_main.set_title("跨两行主坐标系")

# 右上区块:继续拆成左右两个子 subfigure
sf_top_right = fig.add_subfigure(gs[0, 1])
nested = sf_top_right.subfigures(1, 2, wspace=0.08)
nested[0].subplots().set_title("嵌套区 1")
nested[1].subplots().set_title("嵌套区 2")
sf_top_right.suptitle("右上嵌套分组", fontsize=11, fontweight="bold")

# 右下区块
sf_bottom_right = fig.add_subfigure(gs[1, 1])
ax_br = sf_bottom_right.subplots(1, 1)
ax_br.set_title("右下独立坐标系")

plt.show()

局部细节处理:独立 colorbar、共享图例与面板编号

组合图最容易在收尾阶段变乱的两个元素是色条(colorbar)和图例(legend)。在旧写法中,把色条直接挂在 fig.colorbar() 上,往往需要手工传递精确的 ax 列表或单独预留一列网格,稍有改动就会导致旁边不带色条的子图高度或宽度不齐。

使用 subfigures 时,直接调用目标子画布自己的方法即可:

  • 局部色条:在持有热力图或等高线图的子画布上调用 subfig.colorbar(im, ax=axs, location="right")。如果该 SubFigure 内部有多个共享同一色标的 Axes(比如 2 张热力图),只需把 ax=axs(轴列表)传进去,色条就会自动对齐这几张子图的总高度,而不会波及画布另一侧的折线图。
  • 区域共享图例:当一个 SubFigure 里的多张子图使用相同的线型和颜色含义时,先在第一张轴上获取句柄 handles, labels = ax.get_legend_handles_labels(),再调用 subfig.legend(handles, labels, loc="outside upper center", ncols=3)。配合 layout="constrained",带有 outside 前缀的位置关键字会自动在子画布内部预留图例空间,避免遮挡标题。
  • 规范添加 (a)、(b)、(c) 编号:如果每块子图只需要一个居中分组标题,可以直接用 subfig.suptitle("(a) 对照组", x=0.05, ha="left");若需要贴近坐标系左上角且多组严格齐平,可以在每个区块第一张 Axes 上使用 ax.set_title("(a)", loc="left", fontweight="bold")。
怎么用matplotlib3.10的subfigures画复杂组合图?
图 3:常规多子图直接加色条与使用 SubFigure 隔离色条、分组背景的效果对比。

完整实战:三区组合数据图代码与结构解析

下面把前面的写法整合成一份可直接运行的完整脚本。整张图包含三个功能区:左侧跨两行的多组折线对比区(带区域级共享图例)、右上方的双热力图对比区(带区域共享色条),以及右下方的分类汇总柱状图区。

为了保证中文正常显示且跨平台一致,代码内设定了常用中文字体回退列表;实际运行时可根据本机已安装字体(如 SimHei、PingFang SC 或 Noto Sans CJK SC)做对应调整。

import numpy as np
import matplotlib.pyplot as plt

# 1. 设置字体与全局风格(按本机可用字体顺序回退)
plt.rcParams["font.sans-serif"] = ["PingFang SC", "Microsoft YaHei", "SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False

# 2. 构造确定性示例数据
rng = np.random.default_rng(42)
steps = np.arange(0, 50)
curve_a = 1.0 - np.exp(-steps / 14.0) + rng.normal(0, 0.025, size=steps.size)
curve_b = 0.85 * (1.0 - np.exp(-steps / 18.0)) + rng.normal(0, 0.025, size=steps.size)
mat_1 = rng.normal(0.6, 0.25, size=(8, 8))
mat_2 = rng.normal(0.3, 0.25, size=(8, 8))
categories = ["方案甲", "方案乙", "方案丙", "基线"]
scores = [0.89, 0.82, 0.76, 0.64]

# 3. 初始化 Figure 与外层 GridSpec
fig = plt.figure(figsize=(11.5, 6.8), layout="constrained")
gs = fig.add_gridspec(2, 2, width_ratios=[1.15, 1.35], height_ratios=[1.05, 1.0])

# 区域 A:左侧跨两行,包含上下两张折线图和区域级共享图例
sf_a = fig.add_subfigure(gs[:, 0])
sf_a.set_facecolor("#F8F9FA")
axs_a = sf_a.subplots(2, 1, sharex=True)

l1, = axs_a[0].plot(steps, curve_a, color="#477DAD", linewidth=2.2, label="策略 A")
l2, = axs_a[0].plot(steps, curve_b, color="#4F6272", linewidth=2.0, linestyle="--", label="策略 B")
axs_a[0].set_ylabel("收敛得分")
axs_a[0].grid(True, linestyle=":", alpha=0.5)

axs_a[1].plot(steps, np.cumsum(curve_a - curve_b), color="#477DAD", linewidth=2.0)
axs_a[1].axhline(0, color="#4F6272", linewidth=1, linestyle="-.")
axs_a[1].set_ylabel("累计差值")
axs_a[1].set_xlabel("迭代步数")
axs_a[1].grid(True, linestyle=":", alpha=0.5)

sf_a.legend([l1, l2], ["策略 A", "策略 B"], loc="outside upper center", ncols=2, frameon=False)
sf_a.suptitle("(a) 迭代过程与差值累积", fontsize=12, fontweight="bold")

# 区域 B:右上角双热力图 + 局部共享 colorbar
sf_b = fig.add_subfigure(gs[0, 1])
sf_b.set_facecolor("#F1F5F9")
axs_b = sf_b.subplots(1, 2, sharey=True)

im1 = axs_b[0].imshow(mat_1, cmap="Blues", vmin=-0.2, vmax=1.2)
axs_b[0].set_title("批次 1 响应矩阵", fontsize=10.5)
im2 = axs_b[1].imshow(mat_2, cmap="Blues", vmin=-0.2, vmax=1.2)
axs_b[1].set_title("批次 2 响应矩阵", fontsize=10.5)

cbar = sf_b.colorbar(im1, ax=axs_b, location="right", shrink=0.92, pad=0.03)
cbar.set_label("归一化强度")
sf_b.suptitle("(b) 双批次响应矩阵对比", fontsize=12, fontweight="bold")

# 区域 C:右下角横向指标对比
sf_c = fig.add_subfigure(gs[1, 1])
sf_c.set_facecolor("#F8F9FA")
ax_c = sf_c.subplots(1, 1)
bars = ax_c.barh(categories, scores, color=["#477DAD", "#668FB5", "#4F6272", "#8A9AA8"], height=0.55)
ax_c.set_xlim(0, 1.05)
ax_c.set_xlabel("综合评分")
ax_c.invert_yaxis()
ax_c.bar_label(bars, fmt="%.2f", padding=4, fontsize=9.5)
sf_c.suptitle("(c) 综合指标对比", fontsize=12, fontweight="bold")

fig.suptitle("多维度实验对比组合图示例", fontsize=14.5, fontweight="bold")
fig.savefig("composite_subfigures_demo.png", dpi=200)
plt.close(fig)
示例输出文件说明:运行脚本后会在当前工作目录生成 composite_subfigures_demo.png。图像整体分为左、右上、右下三大模块:左侧区域顶部有“策略 A / 策略 B”水平图例,不会遮挡折线;右上区域两张热力图共享右侧唯一一条色条且高度一致;右下区域横向条形图自动避让左侧标签与右侧数值标签。

常见排版报错与排查方法

在使用 subfigures 拼装复杂版面时,大多数异常来自布局引擎混用、索引维度不匹配或画布物理尺寸过小。遇到问题可以按以下对照表快速定位:

常见现象或报错 主要原因 解决与验证方法
UserWarning: This figure includes Axes that are not compatible with tight_layout 在已经划分了 subfigures 的画布上调用了 plt.tight_layout() 或 fig.tight_layout()。 删除 tight_layout(),在创建画布时统一使用 plt.figure(..., layout="constrained")。
TypeError: 'SubFigure' object is not subscriptable 调用 subfigures(1, 1) 时默认 squeeze=True,返回的是单个对象而非数组。 改用 subfigures(1, 1, squeeze=False) 并按 subfigs[0, 0] 访问,或直接用单变量接收。
热力图或子图高度被压缩得很扁,终端提示边距不足 多层嵌套 subfigures 叠加了多层 suptitle 和轴标签,而 figsize 偏小。 先按比例增大 figsize(如宽度 10~14 英寸),再检查是否有多余的空白标题占高。
colorbar 挤歪了邻近区域的子图 误把色条挂在了顶层 fig.colorbar() 上,而未限定在所属 SubFigure 内。 改用 subfig.colorbar(mappable, ax=...),确保只调整该 SubFigure 内部的坐标轴。
关于外边距与间距微调:当开启 layout="constrained" 后,subplots_adjust() 会被忽略。如果希望统一收紧或放宽各子画布之间的间隙,可以通过 fig.get_layout_engine().set(w_pad=0.05, h_pad=0.05, wspace=0.04, hspace=0.04) 精细调整(具体数值以英寸或相对比例为准,按实际出图微调即可)。
怎么用matplotlib3.10的subfigures画复杂组合图?
图 4:三种 subfigures 切分方案的适用条件总结与出图前核对清单。
相关专题:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表