2021-12-20
很多人用 `matplotlib`,停留在“能画图”这一层:`plot` 一下、`bar` 一下、`savefig` 一下,任务就算结束。 但一旦你开始做报告图、科研图、监控面板、批量导图,或者需要把风格、布局、导出过程统一起来,`matplotlib` 的真正价值才会慢慢露出来。
Figure、Axes、Artist官方文档里对 Artist 的定义非常关键:几乎所有你在图上接触到的对象,都是 Artist。
Figure 是整张画布,Axes 是真正承载坐标系和数据的绘图区,而线条、文字、图例、矩形块、注释、刻度,统统都属于 Artist。
这套模型的好处是:你不需要把每张图都写成一串“即时命令”,而是可以先拿到对象,再慢慢调。
来看一段非常典型的对象式写法:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 2 * np.pi, 300)
y = np.sin(x)
fig, ax = plt.subplots(figsize=(8, 4), constrained_layout=True)
line, = ax.plot(x, y, color="#f97316", linewidth=2.5, label="sin(x)")
title = ax.set_title("Object-Oriented Matplotlib")
text = ax.text(0.02, 0.95, "peak-ready", transform=ax.transAxes, va="top")
ax.legend()
ax.grid(alpha=0.25)
# 后续可以继续精修 Artist
line.set_linestyle("--")
title.set_color("#7c2d12")
text.set_fontsize(11)
plt.show()
这段代码里:
fig 是整张图的容器ax 是那块绘图区域line、title、text 都是后续可继续修改的 Artist很多“高阶感”,其实就来自这件事:你不再是“调用一个函数就结束”,而是在管理一组可编辑对象。
GridSpec 和 subplot_mosaic当图只剩一个坐标轴时,plt.subplots() 已经足够顺手。
但如果你要做主图 + 辅图、主图 + 残差图、左大右小、上宽下窄这类结构,仅靠普通二维数组布局就会开始别扭。
这时更推荐两套工具:
GridSpec:控制力强,适合精确切分subplot_mosaic:语义清晰,适合用“布局草图”来搭图import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(10, 6), layout="constrained")
gs = fig.add_gridspec(2, 3)
ax_main = fig.add_subplot(gs[:, :2])
ax_top = fig.add_subplot(gs[0, 2])
ax_bottom = fig.add_subplot(gs[1, 2])
x = np.linspace(0, 10, 200)
ax_main.plot(x, np.sin(x), color="#ea580c")
ax_top.hist(np.random.randn(800), bins=30, color="#fdba74")
ax_bottom.scatter(np.random.randn(80), np.random.randn(80), s=18, alpha=0.7)
plt.show()
如果你更喜欢“看着结构写代码”,subplot_mosaic 会很舒服:
import matplotlib.pyplot as plt
fig, axd = plt.subplot_mosaic(
[
["main", "stat"],
["main", "mini"],
],
figsize=(10, 6),
layout="constrained",
width_ratios=[2.2, 1],
)
axd["main"].set_title("Main View")
axd["stat"].set_title("Distribution")
axd["mini"].set_title("Zoom")
plt.show()
这类写法的优势,不只是“更优雅”,而是后面维护时脑子更清楚。
当一张图开始承担解释任务时,布局本身就是信息结构的一部分。
transforms很多人第一次看到 transform=ax.transAxes,会觉得它像个辅助参数。
其实这套坐标变换系统,是 matplotlib 最值得认真吃透的一部分。
常见的几种坐标语义可以简单理解成:
(0, 0),右上角是 (1, 1)Figure最常见的高阶用法,是把“横向依赖数据、纵向固定在轴区域”的元素绑在一起。比如强调一个时间区间,不希望阴影跟着 y 值范围变化。
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
x = np.linspace(0, 20, 400)
y = np.sin(x) + 0.15 * np.random.randn(len(x))
fig, ax = plt.subplots(figsize=(9, 4), constrained_layout=True)
ax.plot(x, y, color="#f97316", linewidth=2)
mask = (x >= 6) & (x <= 10)
trans = mtransforms.blended_transform_factory(ax.transData, ax.transAxes)
ax.fill_between(x, 0, 1, where=mask, transform=trans, color="#fb923c", alpha=0.18)
ax.text(0.02, 0.95, "用轴坐标固定说明文字", transform=ax.transAxes, va="top")
ax.text(8, 0.92, "重点区间", transform=trans, ha="center", va="top")
plt.show()
这类技巧特别适合:
一旦明白不同坐标系分别负责什么,你就不容易写出“缩放一下,注释全跑偏”的图。
如果你常常写这种代码:
plt.plot(...)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.title(..., fontsize=18)
plt.grid(alpha=0.2)
那说明你正在手工重复“样式定义”。
更稳的方式,是把样式收进 rcParams、style.use() 或 style.context() 里。
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
custom = {
"axes.facecolor": "#fff7ed",
"figure.facecolor": "#fffbf5",
"axes.edgecolor": "#9a3412",
"axes.labelcolor": "#7c2d12",
"xtick.color": "#7c2d12",
"ytick.color": "#7c2d12",
"grid.color": "#fdba74",
"grid.alpha": 0.25,
"axes.grid": True,
"font.size": 11,
}
with mpl.rc_context(custom):
x = np.linspace(0, 6, 200)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, np.cos(x), color="#ea580c", linewidth=2.5)
ax.set_title("Scoped style context")
plt.show()
这个思路很适合做:
如果你还想让文字压在复杂背景上更清楚,可以再加一点 path effects:
import matplotlib.patheffects as pe
label = ax.text(0.5, 0.5, "signal", transform=ax.transAxes,
ha="center", va="center", color="white", fontsize=16)
label.set_path_effects([
pe.withStroke(linewidth=3, foreground="#7c2d12")
])
别小看这一步。很多“图看着精致”的原因,并不是数据更复杂,而是样式控制足够一致。
高阶图里很容易出现一个误区:功能一多,就开始乱用双轴。
其实更稳的策略是:
secondary_xaxis / secondary_yaxis下面这个例子就比“强行 twinx”更克制一些:
import numpy as np
import matplotlib.pyplot as plt
def c_to_f(x):
return x * 9 / 5 + 32
def f_to_c(x):
return (x - 32) * 5 / 9
x = np.arange(0, 12)
y = np.linspace(18, 30, len(x))
fig, ax = plt.subplots(figsize=(8, 4), constrained_layout=True)
ax.plot(x, y, color="#f97316", linewidth=2.5)
ax.set_ylabel("temperature (C)")
secax = ax.secondary_yaxis("right", functions=(c_to_f, f_to_c))
secax.set_ylabel("temperature (F)")
plt.show()
这种写法很适合“只是换算单位,并不是另一套业务指标”的场景。
matplotlib.animation 经常被当成锦上添花,但它在很多场景下都很实用:
核心思路并不复杂:
Artist 保存下来set_* 方法FuncAnimationimport numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
x = np.linspace(0, 4 * np.pi, 300)
y = np.sin(x)
fig, ax = plt.subplots(figsize=(8, 4), constrained_layout=True)
ax.set_xlim(x.min(), x.max())
ax.set_ylim(-1.2, 1.2)
line, = ax.plot([], [], color="#ea580c", linewidth=2.5)
def update(frame):
line.set_data(x[:frame], y[:frame])
return (line,)
anim = FuncAnimation(fig, update, frames=len(x), interval=20, blit=True)
# 展示
plt.show()
# 导出时可按需要开启
# anim.save("wave.mp4", fps=30)
这里有个很容易被忽略的小点:动画对象最好保留在变量里,比如上面的 anim。
否则你以为自己“已经创建了动画”,实际上它可能还没来得及正确显示或导出。
很多图在 notebook 里看着挺好,一导出就开始出问题:
可以先记住这几条:
SVG 或适中 PNGPDF 很顺手bbox_inches="tight" 往往能减少边缘空白dpi 影响位图清晰度,不影响矢量路径本身fig.savefig("chart.svg", bbox_inches="tight")
fig.savefig("chart.png", dpi=220, bbox_inches="tight", facecolor=fig.get_facecolor())
fig.savefig("chart.pdf", bbox_inches="tight")
如果你的程序跑在无界面环境,比如任务调度、服务端批处理,记得关注后端问题。
matplotlib 的交互式后端负责屏幕显示,非交互式后端更偏向直接写文件;在纯导图场景里,后者往往更省心。
下面这段小示例,把布局、样式、变换、导出串在一起:
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
mpl.rcParams.update({
"figure.facecolor": "#fffaf5",
"axes.facecolor": "#fff7ed",
"axes.grid": True,
"grid.alpha": 0.18,
"axes.spines.top": False,
"axes.spines.right": False,
})
rng = np.random.default_rng(7)
x = np.linspace(0, 30, 300)
y = np.sin(x / 2) + rng.normal(0, 0.12, len(x))
fig = plt.figure(figsize=(10, 6), layout="constrained")
axd = fig.subplot_mosaic(
[
["main", "hist"],
["main", "mini"],
],
width_ratios=[2.4, 1],
)
ax_main = axd["main"]
ax_hist = axd["hist"]
ax_mini = axd["mini"]
line, = ax_main.plot(x, y, color="#ea580c", linewidth=2.2)
ax_main.set_title("Matplotlib advanced workflow demo")
ax_main.set_xlabel("step")
ax_main.set_ylabel("value")
trans = mtransforms.blended_transform_factory(ax_main.transData, ax_main.transAxes)
focus = (x >= 10) & (x <= 16)
ax_main.fill_between(x, 0, 1, where=focus, transform=trans, color="#fb923c", alpha=0.18)
ax_main.text(13, 0.92, "focus zone", transform=trans, ha="center", va="top")
ax_hist.hist(y, bins=24, orientation="horizontal", color="#fdba74", edgecolor="#9a3412")
ax_hist.set_title("distribution")
ax_mini.plot(x, np.gradient(y), color="#c2410c")
ax_mini.axhline(0, color="#9a3412", linewidth=1, alpha=0.5)
ax_mini.set_title("gradient")
fig.savefig("matplotlib-advanced-demo.svg", bbox_inches="tight")
plt.show()
这类结构很适合封成你自己的绘图模板。
以后换数据,不换骨架,效率会明显提高。