AI编程(六十七):子图布局 — 多图组合,一页纸讲完整个故事

上一篇刚给图表化完妆,新问题就来了:汇报时老板要同时看销售额趋势、各月对比、品类占比、单价关系——四张图,难道发四个文件、开四个窗口?今天学子图布局:把多张小图排进同一张画布,2×2 排排坐,一页纸讲完整个故事。只引入一个新概念:子图(subplot)——画布上的”格子间”,每个格子是一张独立的图。

一、真实场景:四张图 vs 一页仪表盘

月底数据汇报,老板要看四样东西:全年销售额趋势(折线图)、四个季度对比(柱状图)、品类占比(饼图)、销量和单价的关系(散点图)。

你的做法:画了四张 PNG,发进工作群四个文件。老板手机上点开一张、退出、再点开一张,看到第三张已经忘了第一张长什么样,最后问你:”能不能放一页里?”

同事的做法:一张 2×2 的”四格仪表盘”,左上折线、右上柱状、左下饼图、右下散点,四个小图共用清爽的风格,标题一句话点题。老板一眼扫完整页,指着右下角问了个关键问题,汇报直接进入正题。

你可能觉得”不就是把图拼一起吗,用修图软件拖一下也行”。拖是可以拖,但拖出来的图改不了数据、调不了字号、没法再来一次。让程序自己把四张图排进一张画布,数据一变重跑一次就更新——这才是自动化的意义。今天这一篇,就是把你前面学的四种图装配成一页。

二、新概念:子图 —— 画布上的”格子间”

到今天为止,你画图都是 plt.bar(...) 这样”直接往画布上画”。画布上只有一张图,画到哪算哪。子图(subplot)的思路换了个管理方式:先把画布划分成格子,再每个格子里画自己的图。

打个比方:以前是往一张白纸上画画,现在是先在纸上打好 2×2 的格子间,每个工位坐一张图,互不干扰。

代码长这样:

# subplot_basic.py — 子图的第一眼:先分格,再画画
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

# 第1步:把画布分成 2 行 2 列,得到画布 fig 和 4 个格子(坐标系)axs
fig, axs = plt.subplots(2, 2, figsize=(10, 7))

# axs 是一个 2×2 的"格子阵":axs[0][0] 左上,axs[0][1] 右上,以此类推
axs[0][0].bar(["Q1", "Q2", "Q3", "Q4"], [120, 135, 150, 180])
axs[0][0].set_title("季度销售额(万元)")

axs[0][1].plot([1, 2, 3, 4, 5, 6], [20, 25, 23, 30, 28, 35], marker="o")
axs[0][1].set_title("月度增长趋势")

axs[1][0].pie([45, 30, 25], labels=["线上", "门店", "批发"], autopct="%1.0f%%")
axs[1][0].set_title("渠道占比")

axs[1][1].scatter([10, 20, 30, 40], [95, 80, 70, 62])
axs[1][1].set_title("价格与销量")
axs[1][1].set_xlabel("单价(元)")
axs[1][1].set_ylabel("销量(件)")

fig.suptitle("2026 全年销售四格仪表盘", fontsize=15)
plt.savefig("dashboard_2x2.png", dpi=150, bbox_inches="tight")
plt.show()

三个关键点,看懂这段代码就入门了:

  1. fig, axs = plt.subplots(2, 2, figsize=(10, 7)) —— 一次要来两样东西:fig 是整张画布(管总标题、整体大小),axs 是 2×2 的格子阵(每格是一张独立小图);
  2. axs[0][0].bar(...)、axs[0][0].set_title(...) —— 画图、设标题都”对格子说话”:axs[0][0] 就是左上那个格子。注意是 set_title、set_xlabel 这种带 set_ 的方法,不再是 plt.title;
  3. fig.suptitle(...) —— 管整页的大标题,和每格的小标题分开、互不打架。

这就是今天唯一的新概念。剩下的全是前面学过的东西——四种图各坐一个格子,仅此而已。

三、动手:把四格仪表盘越排越顺

第 1 步:先跑通”素排版”

上面的 subplot_basic.py 跑起来就是一张能用的四格图。但先别急着满意——直接跑会发现两个别扭:格子边缘有点挤,小图的标题和大标题贴得近。先解决”排得开”。

# subplot_neat.py — 排得开:让每个格子有自己的呼吸空间
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

months = ["1月", "2月", "3月", "4月", "5月", "6月"]
sales = [42, 38, 45, 50, 48, 52]
cost = [30, 28, 32, 35, 33, 36]

fig, axs = plt.subplots(2, 2, figsize=(11, 8), constrained_layout=True)

axs[0][0].plot(months, sales, marker="o", color="#4878A8", label="销售额")
axs[0][0].plot(months, cost, marker="s", color="#EE854A", label="成本")
axs[0][0].set_title("销售额 vs 成本")
axs[0][0].legend()

axs[0][1].bar(months, sales, color="#4878A8")
axs[0][1].set_title("各月销售额")

axs[1][0].bar(months, [s - c for s, c in zip(sales, cost)], color="#6AAC90")
axs[1][0].set_title("各月毛利")

axs[1][1].scatter(sales, cost, color="#B279A2", s=80)
axs[1][1].set_title("销售额与成本的关系")
axs[1][1].set_xlabel("销售额(万元)")
axs[1][1].set_ylabel("成本(万元)")

fig.suptitle("上半年经营看板", fontsize=15)
plt.savefig("dashboard_neat.png", dpi=150, bbox_inches="tight")
plt.show()

这一版的升级只有一处:plt.subplots(..., constrained_layout=True)。它让 matplotlib 自动把每格的标题、坐标轴标签排到互不遮挡。以前你用 bbox_inches="tight" 只管”保存时别切边”,constrained_layout=True 是”摆放时就别挤”,双保险。

第 2 步:格子不用一般大——大的放主图

四格平均分是最常见的排法,但汇报常常有主次:主图占大格,副图占小格。matplotlib 分格子靠 gridspec(格子规格),逻辑很直白:先声明格子怎么切,再指定哪张图占哪块。

# subplot_main.py — 主次分明:大格放主图,小格放佐证
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

months = ["1月", "2月", "3月", "4月", "5月", "6月",
          "7月", "8月", "9月", "10月", "11月", "12月"]
sales = [42, 38, 45, 50, 48, 52, 55, 53, 58, 62, 85, 90]

fig = plt.figure(figsize=(11, 7), constrained_layout=True)
gs = fig.add_gridspec(2, 2)          # 还是 2×2 的格子……

# 主图:横跨上面一整行(占 [0, :],即第 0 行全部两列)
ax_main = fig.add_subplot(gs[0, :])
ax_main.plot(months, sales, marker="o", color="#4878A8")
ax_main.set_title("全年销售额趋势(万元)")

# 副图1:左下
ax1 = fig.add_subplot(gs[1, 0])
ax1.bar(["Q1", "Q2", "Q3", "Q4"], [125, 150, 166, 237], color="#EE854A")
ax1.set_title("季度对比")

# 副图2:右下
ax2 = fig.add_subplot(gs[1, 1])
ax2.pie([125, 150, 166, 237], labels=["Q1", "Q2", "Q3", "Q4"], autopct="%1.0f%%")
ax2.set_title("季度占比")

fig.suptitle("2026 销售年度报告", fontsize=15)
plt.savefig("dashboard_main.png", dpi=150, bbox_inches="tight")
plt.show()

gs[0, :] 的读法和列表切片一样:”第 0 行、所有列”——于是主图横跨整个上排。这就是仪表盘排版的全部秘密:格子是可拼的,gs[1, 0]、gs[1, 1] 各占一格,gs[0, :] 拼大格。

第 3 步:用循环批量画,告别复制粘贴

四张图逐个写没问题,要是十张呢?格子阵支持下标遍历,”多图同款”的场景用循环一次搞定:

# subplot_loop.py — 批量画:四个区域,一个循环
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

regions = ["华东", "华北", "华南", "西南"]
data = {
    "华东": [42, 45, 50, 55],
    "华北": [30, 33, 35, 40],
    "华南": [25, 28, 32, 38],
    "西南": [18, 20, 22, 28],
}
quarters = ["Q1", "Q2", "Q3", "Q4"]

fig, axs = plt.subplots(2, 2, figsize=(10, 7), constrained_layout=True)

for name, ax in zip(regions, axs.flat):   # axs.flat 把 2×2 格子阵摊平成一排
    ax.bar(quarters, data[name], color="#4878A8")
    ax.set_title(f"{name} 季度销售额(万元)")
    ax.set_ylim(0, 60)                    # 四格统一纵轴范围,高低才可比!

fig.suptitle("四大区域季度对比", fontsize=15)
plt.savefig("dashboard_loop.png", dpi=150, bbox_inches="tight")
plt.show()

axs.flat 是个小神器:把 2×2 的格子阵摊平成一条线,和区域列表一一配对。注意 ax.set_ylim(0, 60)——四个格子纵轴范围必须一致,否则 18 万的小柱子在自己格子里显得和 55 万的一样高,图会撒谎。

四、改进:跟AI一起升级

把 subplot_main.py 发给 AI,需求往”汇报级”上提:

“这是我的销售年报仪表盘代码。帮我升级成汇报级:1)主图跨上排、两个副图在下排的布局已经写好,帮我检查文字有没有互相挤;2)三个小图的配色统一成’一强调多安静’的风格;3)每格小标题写成结论式,你根据数据各给两个候选;4)整个页面 A4 横版能打印,输出 150dpi PNG。先说方案再给代码。”

它的方案里这几个点值得追问:

  • 让它解释格子坐标:gs[0, :] 到底占哪里?合格的回答会画出”田”字格示意。看懂格子坐标,你以后排三格、六格都不求人;
  • 统一风格要逐格检查:让它把每格的配色、字号、坐标轴样式列成表格对齐,四格各画各的是仪表盘最常见的”脏”;
  • 追问打印尺寸:figsize 英寸 × dpi = 像素,A4 横版约 11.7×8.3 英寸。让它按这个尺寸出图,打印不糊。

五、四个必踩的坑(先打疫苗)

  1. 画布大小没跟着格子数放大。 默认画布画一张图正好,切成四格后每格只有四分之一大,标题挤成一团、饼图小得看不见。疫苗:格子越多,figsize 越要放大,2×2 至少 figsize=(10, 7),3×3 往 (12, 9) 起步;导出 dpi 别低于 150。
  1. 在子图上用 plt.title 这类全局函数。 plt.bar、plt.title 是”往当前格子画”,格子一多你根本说不清”当前”是哪格,图就画乱了。疫苗:有了 axs 之后,只用 ax.xxx 和 ax.set_xxx,每个动作都点名格子,绝不含糊。
  1. 小图标签互相打架、或被 suptitle 压住。 手动 subplots_adjust 调间距是体力活,调好这格挤那格。疫苗:constrained_layout=True 一针见效;万一还不行再用 fig.tight_layout(rect=[0, 0, 1, 0.96]) 给大标题留出空间。保存后亲眼看一下 PNG,四格图最考验眼神。
  1. 各格纵轴范围不一致,图在撒谎。 四个格子自动缩放各自的纵轴,同一份量级的数据画出”一样高”的柱子,老板据此拍板就是灾难。疫苗:同一量级的图共用纵轴范围(sharey=True 或手动 set_ylim),数值差异才真实。这也是下一节跟 AI 协作时必查的一项。

六、跟AI协作的正确姿势(多图组合特供版)

  1. 先分配任务,再让它排版。 发需求时把”每格讲什么结论”列清楚:”左上讲趋势、右上讲对比、左下讲占比、右下讲关系”。AI 知道每格的使命,才能推荐合适的图型和布局——不说使命只说”排四张图”,它只能给你四格平均分。
  1. 要”布局图”,不要”直觉”。 让它把 gridspec 的切法用文字画出来(”主图占上排两个格子”),你确认了再写代码。格子坐标是最容易出错的地方,先把图纸对齐,施工才不返工。
  1. 让它当”整页三秒测试员”。 上一篇测的是单张图,这次测整页:”假装你是第一次看这页仪表盘的老板,三秒内你先看哪格、看懂了什么?哪格多余?”——四格图的失败往往不是哪格画得丑,而是有一格根本不该存在。

七、今日小练习

练习1:拼一页四格看板。 把你前面画过的四种图(第62-65篇的折线、柱状、饼、散点)排成 2×2 一页,加 constrained_layout=True 和 fig.suptitle,导出一张 PNG(提示:fig, axs = plt.subplots(2, 2, figsize=(10, 7));每格用 axs[0][0].set_title 点名画;四种图各占 axs[0][0]、axs[0][1]、axs[1][0]、axs[1][1])。

练习2:把主图放大。 在练习1的基础上改成”上排横跨一张主图 + 下排两张副图”的布局(提示:gs = fig.add_gridspec(2, 2),主图 fig.add_subplot(gs[0, :]) 横跨第 0 行;副图占 gs[1, 0] 和 gs[1, 1])。对比两种排版,体会”主次”带来的阅读顺序变化。

练习3:揪出撒谎的图。 做一页”四大区域季度对比”的 2×2 柱状图,故意一版用自动纵轴、一版统一 set_ylim(0, 60),问自己”哪版里华北和华东的差距是真的?”(提示:纵轴范围不一致时,矮柱子在自己格子里会显得和高柱子一样高;同一量级的对比图必须共用纵轴)。

下篇预告

下一篇(第68篇):数据+图表 — pandas+matplotlib,让程序自动出图。 前面画图的数据都是手敲的列表,真实工作里数据躺在 CSV 和 Excel 里。下一篇把第31篇的 pandas 和这几篇的 matplotlib 接上:读表、算数、画图一条龙,数据一变图就更新——从”画图的人”升级成”出图的流水线”。