Matplotlib 科研绘图教程
环境:Windows 11 + Miniconda,Python 3.14.4,Matplotlib 3.10.9,VS Code
1. 简介与定位
Matplotlib 是 Python 生态里最底层、最通用的 2D 绘图库。对Python科研绘图而言,它几乎是所有科研出图(能带、DOS、吸附能、催化性能对比、势能面)的最终兜底工具。
需要先摆正一个观念:
- Matplotlib 是"命令式 + 面向对象"的绘图库,不是"声明式"的(不像 Excel / Origin 点几下)。
你写的每一句代码,本质都是在操纵一个Figure对象和它内部的若干Axes对象。 - 它在数值处理上依赖 NumPy,所以本文所有示例都默认:
import numpy as np
import matplotlib.pyplot as plt
- Matplotlib 的 3.x 系列 API 非常稳定,本文所有函数名 / 参数都以 3.10.9 为准,
不会出现你复制粘贴却报AttributeError的"伪代码"。
什么时候用 Matplotlib 而不是别的?
- 需要精确控制坐标轴、刻度、图例、字体、dpi 的投稿级图片 → Matplotlib(或它的高层封装)。
- 快速探索性统计(箱线图、小提琴图、热力图)→ Seaborn(第 12 节)。
- 交互式三维结构 / 晶体 → 请用 ASE / VESTA / OVITO,不要用 Matplotlib 硬画 3D。
2. 安装与版本检查
Miniconda 环境下推荐单独建一个绘图环境,避免污染你的 DFT / ML 环境:
conda create -n plot python=3.12 -y
conda activate plot
conda install -c conda-forge matplotlib numpy pandas scipy -y
版本检查(写脚本前先确认,避免"文档里的新参数在你环境里没有"):
import matplotlib
import numpy as np
print("Matplotlib:", matplotlib.__version__) # 期望 3.10.x
print("NumPy:", np.__version__)
print("Backend:", matplotlib.get_backend()) # VS Code 交互式通常是 TkAgg / QtAgg / module://...
小贴士:
matplotlib.get_backend()显示的是当前渲染后端。VS Code 里启用
"Interactive Window / Jupyter" 会切换到内联后端;纯.py脚本plt.show()
则会弹出一个独立窗口。若偏好.py脚本,那么最终保存图片请用fig.savefig()
而不是截图,保证分辨率与矢量格式(见第 10 节)。
3. pyplot 快速绘图 vs 面向对象 API
3.1 两种写法长什么样
pyplot(函数式)写法 —— 隐式管理"当前图"和"当前坐标轴",适合快速试探:
import matplotlib.pyplot as plt
plt.plot([1, 2, 3], [1, 4, 9])
plt.xlabel("x")
plt.ylabel("y")
plt.title("pyplot 风格")
plt.show()
面向对象(Figure/Axes)写法 —— 显式拿到 fig 和 ax,再对 ax 操作:
import matplotlib.pyplot as plt
fig, ax = plt.subplots() # fig 是整个画布,ax 是一套坐标轴
ax.plot([1, 2, 3], [1, 4, 9])
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title("面向对象风格")
fig.savefig("oo_style.png", dpi=300)
3.2 为什么科研出图强烈推荐后者
- 可读性 / 可维护性:
ax.set_xlabel(...)明确知道改的是哪个子图;plt.xlabel(...)
在多个子图时经常"改错对象"。 - 多子图必须 OO:
plt.subplots(2, 2)返回一个Axes数组,你必然要拿到每个
ax去画,pyplot 的"隐式当前图"在这里只会添乱。 - 复用性:把"画一张催化性能对比图"封装成
def plot_faradaic(df, ax),只接收
ax参数,想放哪个子图就传哪个,这是脚本化出图的核心套路。 - 避免状态污染:pyplot 会保留全局状态(当前 figure/axes),循环出图时容易串图。
结论:本文从第 4 节起全部使用 OO 风格。
plt只用来干三件事:plt.subplots、
plt.show、以及plt.rcParams全局设置。
4. Figure / Axes 概念
Figure:整张画布(可以理解成"一页纸"),可以有多个Axes。Axes:画布上的一个"坐标系区域",是真正画画的地方(线、点、柱、色块都画在这里)。
注意Axes≠axis;axis是坐标轴(x 轴 / y 轴),Axes是包含坐标轴的整块绘图区。- 一个
Axes通常包含两个Axis(x、y),还可以有twinx得到的第二个 yAxis。
4.1 单图
fig, ax = plt.subplots(figsize=(6, 4), dpi=150)
ax.plot(np.linspace(0, 10, 100), np.sin(np.linspace(0, 10, 100)))
fig.tight_layout()
4.2 多子图 subplots
fig, axes = plt.subplots(2, 2, figsize=(8, 6), sharex=True, sharey=True)
# axes 是一个 shape=(2,2) 的 ndarray,可用 axes[0,1] 索引
axes = axes.flatten() # 拍平成一维,遍历方便
for i, ax in enumerate(axes):
x = np.linspace(0, 4 * np.pi, 200)
ax.plot(x, np.sin(x + i * np.pi / 4))
ax.set_title(f"子图 {i + 1}")
fig.suptitle("subplots 示例", fontsize=14)
fig.tight_layout()
4.3 subplot_mosaic(推荐,语义化布局)
用字符串描述布局,一行一个子图区,同一字符表示跨行 / 跨列合并。这是画"组合图"最直观的方式:
fig, axd = plt.subplot_mosaic(
[["A", "B"],
["A", "C"]],
figsize=(8, 6),
layout="constrained", # 自动避免子图重叠
)
axd["A"].plot(np.linspace(0, 1, 100), np.random.rand(100))
axd["A"].set_title("A(跨两行)")
axd["B"].plot(np.linspace(0, 1, 100), np.random.rand(100))
axd["C"].plot(np.linspace(0, 1, 100), np.random.rand(100))
axd是一个字典,键就是字符串里的字符。第 11 节的"能带 + DOS 组合图"会用它。
4.4 GridSpec(精细控制尺寸比例)
当你需要"某个子图占 3 份宽、另一个占 1 份宽"时:
fig = plt.figure(figsize=(10, 4))
gs = fig.add_gridspec(1, 3, width_ratios=[3, 1, 1], wspace=0.15)
ax_main = fig.add_subplot(gs[0, 0]) # 主图,占 3 份
ax_dos = fig.add_subplot(gs[0, 1], sharey=ax_main) # 右侧小图,共享 y 轴
ax_other= fig.add_subplot(gs[0, 2], sharey=ax_main)
ax_main.plot([1, 2, 3], [1, 4, 9])
ax_dos.plot([0, 1], [0, 10])
ax_other.plot([0, 1], [0, 10])
5. 常见图类型(每种配代码)
先准备一套反复用到的小数据:
import numpy as np
rng = np.random.default_rng(42) # 固定随机种子,保证可复现
x = np.linspace(0, 10, 50)
y = 2.0 * x + rng.normal(0, 1.5, x.size) # 带噪声的线性关系
5.1 折线 plot
折线 / 曲线图是"吸附能随覆盖度变化""反应坐标能量"等场景的基本款。最常用参数:
color(颜色)、linestyle/ls(线型 '-' '--' '-.' ':')、linewidth/lw、
marker('o' 's' '^' 'D')、markersize/ms、label(配合图例)。
fig, ax = plt.subplots(figsize=(6, 4))
x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), color="tab:blue", linestyle="-", linewidth=1.5,
marker="o", markersize=4, markevery=10, label="sin")
ax.plot(x, np.cos(x), color="tab:red", linestyle="--", linewidth=1.5,
label="cos")
ax.legend()
markevery=10表示"每隔 10 个点画一个 marker",能避免数据点密集时 marker 糊成一团。
5.2 散点 scatter
适合"机器学习势函数的能量 vs DFT 能量对比(parity plot)"这类图。核心参数:s(点大小,
可以是数组)、c(颜色,可以是数组 + cmap)、alpha、edgecolors。
fig, ax = plt.subplots(figsize=(6, 6))
n = 200
x = rng.uniform(-5, 5, n)
y = x + rng.normal(0, 0.4, n) # 模拟"预测值 vs 真值"接近 y=x
err = np.abs(y - x)
sc = ax.scatter(x, y, c=err, cmap="viridis", s=30, alpha=0.8, edgecolors="k", linewidths=0.3)
ax.plot([-6, 6], [-6, 6], "r--", lw=1, label="y = x")
ax.set_xlabel("DFT 能量 / eV")
ax.set_ylabel("ML 势预测能量 / eV")
ax.set_aspect("equal")
ax.legend()
fig.colorbar(sc, ax=ax, label="绝对误差 / eV")
5.3 柱状 bar
柱状图用于"不同催化剂 / 不同活性位点的性能对比"。注意 bar 的 height 是"柱高",
x 是柱的横坐标(可以传浮点数精确控制位置,实现分组柱)。
fig, ax = plt.subplots(figsize=(7, 4))
sites = ["Fe-N4", "Co-N4", "Ni-N4", "Cu-N4"]
overpotential = [0.42, 0.35, 0.61, 0.88] # CO2RR 过电位 / V,越小越好
bars = ax.bar(sites, overpotential, width=0.6, color="tab:blue",
edgecolor="k", linewidth=0.5)
ax.set_ylabel("过电位 $\eta$ / V")
ax.set_ylim(0, 1.0)
ax.axhline(0.35, color="r", linestyle="--", label="Co-N4 参考") # 加一条参考线
ax.legend()
# 在每根柱顶标注数值
for rect in bars:
h = rect.get_height()
ax.text(rect.get_x() + rect.get_width() / 2, h + 0.01, f"{h:.2f}",
ha="center", va="bottom", fontsize=9)
使用 LaTeX 风格数学文本(
$\eta$)需要启用 mathtext,Matplotlib 内置支持
(无需安装 LaTeX),书写见第 9 节。
5.4 误差棒 errorbar
"吸附能 / 反应能" 常伴随多次计算或模型误差。errorbar 核心参数:yerr(可标量、
数组、或 [low, high] 上下不对称误差)、xerr、fmt(点 + 线样式,如 'o')、
capsize(误差棒两端横线长度)、elinewidth、ecolor。
fig, ax = plt.subplots(figsize=(7, 4))
catalysts = ["Cu(111)", "Cu(100)", "Cu(211)", "Zn-Cu"]
E_ads = [-0.85, -0.72, -0.93, -1.05] # CO 吸附能 / eV
err = [0.08, 0.06, 0.10, 0.07] # 标准差
ax.errorbar(catalysts, E_ads, yerr=err, fmt="o", markersize=7,
capsize=4, capthick=1.5, elinewidth=1.2, ecolor="tab:gray",
color="tab:red", label="CO 吸附能")
ax.axhline(0, color="k", lw=0.8)
ax.set_ylabel("吸附能 $E_{ads}$ / eV")
ax.legend()
5.5 直方图 hist
分析"大量结构的吸附能分布""MD 模拟中键长的分布"时用。核心参数:bins、density
(True 时归一化成概率密度,面积和为 1)、alpha、histtype。
fig, ax = plt.subplots(figsize=(6, 4))
energies = rng.normal(-0.5, 0.2, 5000) # 模拟 5000 个构型的吸附能
ax.hist(energies, bins=40, density=True, alpha=0.7,
color="tab:green", edgecolor="w", histtype="stepfilled")
ax.set_xlabel("吸附能 / eV")
ax.set_ylabel("概率密度")
5.6 箱线图 boxplot
比较"多组样本的分布(中位数、四分位、离群点)",比"只画平均值 + 误差棒"信息更丰富。
注意 3.10 中推荐用 tick_labels 指定 x 轴标签(旧的 labels 参数已弃用但仍可用)。
fig, ax = plt.subplots(figsize=(7, 4))
rng = np.random.default_rng(7)
# 四组催化剂的 CO2RR 法拉第效率(%) 分布
data = [rng.normal(78, 5, 50),
rng.normal(65, 8, 50),
rng.normal(52, 6, 50),
rng.normal(88, 4, 50)]
bp = ax.boxplot(data, tick_labels=["Cat-A", "Cat-B", "Cat-C", "Cat-D"],
patch_artist=True, showmeans=True, showfliers=True)
colors = ["tab:blue", "tab:orange", "tab:green", "tab:red"]
for patch, c in zip(bp["boxes"], colors):
patch.set_facecolor(c)
patch.set_alpha(0.6)
ax.set_ylabel("法拉第效率 / %")
5.7 等高线 contour / contourf
画二维势能面(PES)切片、反应坐标扫描。contourf 是填充色块版,contour 是线条版,
两者常叠加使用(色块 + 等值线)。
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
# 构造一个双势阱势能面,模拟两个反应坐标 r1, r2 上的自由能
r1 = np.linspace(-2, 2, 200)
r2 = np.linspace(-2, 2, 200)
R1, R2 = np.meshgrid(r1, r2)
Z = (R1**2 - 1)**2 + R2**2 + 0.5 * R1 * R2 # 一个形如"双谷"的势能面
cf = ax1.contourf(R1, R2, Z, levels=20, cmap="viridis")
fig.colorbar(cf, ax=ax1, label="势能")
ax1.set_title("contourf")
cs = ax2.contour(R1, R2, Z, levels=np.arange(0, 8, 0.5), cmap="viridis")
ax2.clabel(cs, inline=True, fontsize=7, fmt="%.1f") # 在线上标数值
ax2.set_title("contour(等值线)")
5.8 热力图 imshow / pcolormesh
imshow 画"规则网格的矩阵"(如 ML 势的力常数矩阵、相似度矩阵、混淆矩阵),
pcolormesh 画"非均匀网格 / 真实坐标的四边形",前者快、后者坐标更灵活。
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
# imshow:规则矩阵,origin 决定行方向
M = rng.normal(0, 1, (20, 20))
im = ax1.imshow(M, cmap="coolwarm", origin="lower", aspect="auto")
fig.colorbar(im, ax=ax1, label="值")
ax1.set_title("imshow(矩阵)")
# pcolormesh:非均匀坐标,shading='auto' 是 3.x 推荐写法
x = np.linspace(0, 5, 30)
y = np.linspace(0, 3, 20)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) * np.cos(Y)
pc = ax2.pcolormesh(X, Y, Z, cmap="viridis", shading="auto")
fig.colorbar(pc, ax=ax2, label="sin(x)cos(y)")
ax2.set_title("pcolormesh(真实坐标)")
imshow的坑:矩阵第一行默认画在上方(像图像)。物理坐标通常希望第一行在下方,
所以记得origin="lower";配合extent=[xmin,xmax,ymin,ymax]设置真实坐标范围。
6. 坐标轴:标签、范围、刻度、双 y 轴、对数坐标
6.1 标签与范围
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, np.sin(x))
ax.set_xlabel("反应坐标 r / Å", fontsize=13) # 推荐带单位
ax.set_ylabel("能量 E / eV", fontsize=13)
ax.set_xlim(0, 10) # 手动设范围;省略则自动
ax.set_ylim(-1.2, 1.2)
6.2 刻度控制
能带图要把 x 轴刻度换成高对称点符号(Γ、X、M...),靠 set_xticks + set_xticklabels:
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(np.linspace(0, 3, 50), np.sin(np.linspace(0, 3, 50)))
high_sym = [0, 1, 2, 3] # 高对称点的位置(k 路径累计长度)
labels = ["Γ", "X", "M", "Γ"] # 对应符号
ax.set_xticks(high_sym)
ax.set_xticklabels(labels, fontsize=12)
# 在高对称点处画竖直虚线
for k in high_sym:
ax.axvline(k, color="k", linestyle="--", linewidth=0.7, alpha=0.6)
ax.set_ylabel("E - E$_F$ / eV")
刻度细节统一交给 tick_params(方向、长度、字号、次要刻度):
ax.tick_params(axis="both", direction="in", length=4, width=1.2,
labelsize=11, top=True, right=True)
ax.minorticks_on() # 打开次要刻度
6.3 双 y 轴 twinx
典型的场景:一张图里同时画"电流密度 vs 电位"和"法拉第效率 vs 电位",量纲不同。
fig, ax1 = plt.subplots(figsize=(7, 4))
potential = np.linspace(-0.2, -1.0, 50)
current = 50 * np.exp(potential / 0.3) # 电流密度随过电位指数增长
faradaic = 100 / (1 + np.exp(-(potential + 0.6) / 0.08)) # FE 的 S 曲线
ax1.plot(potential, current, "tab:blue", label="电流密度")
ax1.set_xlabel("电位 vs. RHE / V")
ax1.set_ylabel("电流密度 / mA cm$^{-2}$", color="tab:blue")
ax1.tick_params(axis="y", labelcolor="tab:blue")
ax2 = ax1.twinx() # 共享 x 轴,独立右 y 轴
ax2.plot(potential, faradaic, "tab:red", label="法拉第效率")
ax2.set_ylabel("法拉第效率 / %", color="tab:red")
ax2.tick_params(axis="y", labelcolor="tab:red")
# 两条曲线合并到一个图例
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc="best")
6.4 对数坐标
吸附等温线、反应速率常数的 Arrhenius 图经常要对数:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
x = np.logspace(0, 5, 50) # 10^0 ~ 10^5
ax1.plot(x, x**2)
ax1.set_xscale("log") # 只 x 轴取 log
ax1.set_yscale("log") # x、y 都 log(双对数图)
ax1.set_title("log-log")
# Arrhenius:ln k = ln A - Ea/(RT) = f(1/T)
T = np.linspace(300, 600, 100)
k = 1e10 * np.exp(-50000 / (8.314 * T)) # Ea = 50 kJ/mol
ax2.plot(1000 / T, np.log(k))
ax2.set_xlabel("1000 / T (K$^{-1}$)")
ax2.set_ylabel("ln k")
ax2.set_title("Arrhenius 图")
需要更精细时还有
symlog(对称对数,可跨越 0 和负数)、logit。化学出图 99% 用
set_xscale("log")/set_yscale("log")就够了。
7. 标题、图例 legend、注释 annotate
7.1 标题
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, np.sin(x))
ax.set_title("(a) 反应势能曲线", fontsize=13, pad=10) # pad 控制标题与图间距
fig.suptitle("图 1", fontsize=14) # 画布总标题(多子图时用)
7.2 图例 legend
ax.plot(x, np.sin(x), label="exp. data")
ax.plot(x, np.cos(x), label="DFT calc.")
ax.legend(loc="best", # 自动挑不挡数据的位置
frameon=True, # 是否画边框
framealpha=0.8, # 边框透明度
ncol=2, # 分两列
fontsize=10,
handlelength=2.0) # 图例中线的长度
loc 常用取值:'best'、'upper right'、'upper left'、'lower left'、
'outside'(放图外)。也可以用 bbox_to_anchor=(1.02, 1) 精确放图外:
ax.legend(loc="upper left", bbox_to_anchor=(1.02, 1.0), borderaxespad=0)
7.3 注释 annotate / text
annotate 带箭头,适合标注"过渡态""限速步骤";text 就是纯文字。
fig, ax = plt.subplots(figsize=(6, 4))
x = np.linspace(0, 3, 100)
ax.plot(x, (x - 1.2)**2)
ax.annotate("过渡态 TS",
xy=(1.2, 0.0), # 箭头指向的点
xytext=(1.6, 1.5), # 文字位置
fontsize=11,
arrowprops=dict(arrowstyle="->", color="k", lw=1.2))
ax.text(0.1, 2.0, "势垒 $E_a$", fontsize=11) # 纯文字,无箭头
xy 支持坐标转换:默认是"数据坐标",可改成 xycoords="axes fraction" 用"轴内比例坐标"。
8. 配色与 colormap
8.1 colormap 的分类(先理解再选)
| 类型 | 用途 | 推荐 | 慎用 |
|---|---|---|---|
| 顺序 sequential | 单一方向单调变化(吸附能大小、浓度) | viridis plasma cividis Blues Greys | jet |
| 发散 diverging | 有正负、有"零点"(电荷差分、结合能偏差) | RdBu_r coolwarm seismic bwr | — |
| 定性 qualitative | 离散分类(不同元素 / 不同催化剂) | tab10 Set1 Set2 Paired | jet |
| 循环 cyclic | 周期性(角度、相) | twilight hsv | — |
色盲友好:
viridis(3.x 默认)、cividis、plasma都是感知均匀 + 色盲友好的。
尽量避免jet:它亮度不均匀,会在数据中制造"不存在的边界",是科研配色的头号坑。
8.2 使用 colormap
3.10 推荐用 matplotlib.colormaps 注册表获取 colormap 对象(旧 matplotlib.cm.get_cmap
已在 3.9 移除):
import matplotlib
cmap = matplotlib.colormaps["viridis"] # 推荐:从全局注册表取 colormap
cmap = matplotlib.colormaps["RdBu_r"]
print(list(matplotlib.colormaps)[:10]) # 打印前 10 个可用名称
说明:
matplotlib.colormaps是一个全局的ColormapRegistry实例,["名字"]取对象。
不要在脚本里matplotlib.colormaps.get_cmap(...)与旧教程的plt.cm.get_cmap混用,
后者已在 3.9 移除。
8.3 连续 colormap + 居中归一化 TwoSlopeNorm
画"电荷密度差 / 结合能偏差"这类有正有负、且希望 0 对应白色的图,用 TwoSlopeNorm:
import matplotlib.colors as mcolors
data = rng.normal(0, 0.5, (30, 30)) # 有正有负
norm = mcolors.TwoSlopeNorm(vmin=-1.5, vcenter=0, vmax=1.5)
fig, ax = plt.subplots(figsize=(5, 5))
im = ax.imshow(data, cmap="RdBu_r", norm=norm, origin="lower")
fig.colorbar(im, ax=ax, label="Δρ (e/Å$^3$)")
8.4 离散 colormap ListedColormap + BoundaryNorm
把连续数据"分档"着色(比如吸附强度分档:弱 / 中 / 强):
import matplotlib.colors as mcolors
bounds = [0, 0.3, 0.6, 1.0] # 边界
cmap = mcolors.ListedColormap(["tab:green", "tab:orange", "tab:red"])
norm = mcolors.BoundaryNorm(bounds, cmap.N)
Z = rng.uniform(0, 1, (20, 20))
fig, ax = plt.subplots(figsize=(5, 5))
im = ax.imshow(Z, cmap=cmap, norm=norm, origin="lower")
fig.colorbar(im, ax=ax, ticks=[0.15, 0.45, 0.8]) # 刻度放在每个档位中间
8.5 论文常用配色
- 单色系 + 黑白打印友好:
tab:bluetab:redtab:gray系列(Tableau 色,区分度高)。 - Paul Tol 色板(色盲友好):常用
#332288(蓝)#117733(绿)#44AA99(青)
#CC6677(红)#DDCC77(黄)#882255(紫)。 - 多曲线推荐循环:直接传
color列表手动指定,别依赖默认循环色。
9. 样式与字体:rcParams、中文与负号、字号统一
9.1 全局 rcParams(脚本开头统一设置)
把下面这段放在每个绘图脚本最前面,一次设置全局生效,避免每个图都重复调:
import matplotlib
import matplotlib.pyplot as plt
matplotlib.rcParams.update({
# ---- 中文 + 负号(Windows 下关键!)----
"font.sans-serif": ["Microsoft YaHei", "SimHei", "SimSun", "KaiTi"],
"axes.unicode_minus": False, # 用 ASCII 减号,否则负号显示成方块
# ---- 字号统一(投稿常用 7~10 pt,这里给 11 便于屏幕看)----
"font.size": 11,
"axes.labelsize": 12,
"axes.titlesize": 13,
"xtick.labelsize": 10,
"ytick.labelsize": 10,
"legend.fontsize": 10,
# ---- 线条与边框 ----
"axes.linewidth": 1.2,
"lines.linewidth": 1.5,
"xtick.direction": "in",
"ytick.direction": "in",
# ---- 分辨率 ----
"figure.dpi": 150,
"savefig.dpi": 300,
})
9.2 中文显示的准确做法(Windows)
Windows 自带的可用中文字体:SimHei(黑体,.ttf)、Microsoft YaHei(微软雅黑,.ttc)、
SimSun(宋体)、KaiTi(楷体)、FangSong(仿宋)。
第 9.1 节把 font.sans-serif 设成列表后,Matplotlib 会按顺序找第一个已安装的字体。
但有两个常见坑:
- 字体缓存没刷新:装好字体后 Matplotlib 可能还没把它登记进缓存。先检查:
from matplotlib import font_manager
names = {f.name for f in font_manager.fontManager.ttflist}
print("SimHei" in names, "Microsoft YaHei" in names)
若返回 False,手动注册并重建缓存:
from matplotlib import font_manager
font_manager.fontManager.addfont(r"C:\Windows\Fonts\simhei.ttf") # 直接指定字体文件路径
font_manager._rebuild()
.ttc字体(微软雅黑)偶尔识别失败:此时把"SimHei"排在列表最前面更稳妥,
因为 SimHei 是单字体.ttf,兼容性最好。
结论:
font.sans-serif用列表 +axes.unicode_minus=False,优先 SimHei,
这基本能解决 99% 的 Windows 中文乱码和负号问题。
9.3 数学符号(mathtext)
Matplotlib 内置 mathtext,用 $...$ 写数学表达式,无需安装 LaTeX。化学常用:
ax.set_ylabel(r"能量 $E - E_F$ / eV")
ax.set_xlabel(r"反应坐标 $\xi$ / Å")
ax.set_title(r"势垒 $E_a$ = 0.45 eV")
# 下标:$E_{ads}$;上标:$^{-2}$;希腊字母:$\Delta G^{\ddagger}$
注意:r"..." 原始字符串可避免 \d 等被当作转义。若想用真 LaTeX 渲染,
设置 matplotlib.rcParams["text.usetex"] = True(需额外装 TeX,一般没必要)。
9.4 已有样式表
Matplotlib 自带多种样式,也可叠加自定义:
plt.style.use("seaborn-v0_8-whitegrid") # 类似 seaborn 的网格样式
# 或叠加列表:plt.style.use(["seaborn-v0_8", "我的自定义.mplstyle"])
"seaborn"等旧样式名在 3.6+ 已弃用,3.10 里用"seaborn-v0_8-*"前缀。
10. 保存导出:dpi、矢量格式、bbox_inches
10.1 核心结论
- 投稿一律用矢量格式:
pdf、svg(eps也可,但建议优先pdf/svg)。
矢量图可无限放大不失真,期刊编辑 / LaTeX 需要矢量。 - dpi 只影响栅格内容:
png/tiff是位图,必须高 dpi(≥300);矢量图中若
嵌入了imshow等栅格元素,也要设 dpi 保证那些像素块清晰。 bbox_inches="tight":自动裁掉四周多余留白,几乎必加。
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, np.sin(x))
fig.savefig("fig.pdf", bbox_inches="tight") # 投稿首选:矢量 PDF
fig.savefig("fig.svg", bbox_inches="tight") # 矢量 SVG,可导入 Inkscape / PPT
fig.savefig("fig.png", dpi=300, bbox_inches="tight") # 报告 / 网页:高清位图
fig.savefig("fig.tif", dpi=600, bbox_inches="tight",
pil_kwargs={"compression": "tiff_lzw"}) # 部分期刊要 TIFF
常用
figsize(英寸):单栏 ≈(3.5, 2.6);双栏 ≈(7.0, 2.6)。配合
dpi=300,单栏图实际像素约 1050×780,足够清晰。
10.2 常用保存参数速查
| 参数 | 作用 | 推荐值 |
|---|---|---|
dpi | 栅格分辨率 | 300(投稿)/ 150(预览) |
bbox_inches | 裁边 | "tight" |
transparent | 透明背景 | True(做 PPT 时) |
pad_inches | 裁边后额外留白 | 0.05 |
format | 显式指定格式 | "pdf" "svg" "png" |
fig.savefig("fig.svg", bbox_inches="tight", transparent=True, pad_inches=0.05)
11. 科研实战示例(重点)
以下每个示例都是"可直接运行 + 换成你的真实数据就能用"的模板。数据用 NumPy 模拟,
注释里说明如何接真实数据。
11.1 能带结构图
真实数据通常来自 VASP 的 EIGENVAL / vasprun.xml(推荐用 pymatgen 的
BandStructure 或 vaspkit 导出)。这里演示通用画法:x 轴是 k 路径累计长度,
y 轴是相对费米能级的能量。
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(0)
# ---- 模拟数据:4 段 k 路径,每段 60 个点,5 条能带 ----
segments = [(0, 1), (1, 2), (2, 3), (3, 4)] # 各段端点
n_per = 60
n_bands = 5
k_path = []
for a, b in segments:
k_path.append(np.linspace(a, b, n_per))
k_path = np.concatenate(k_path)
# 用一个"正弦 + 微扰"模拟 5 条能带(价带向下、导带向上)
bands = np.zeros((n_bands, k_path.size))
for b in range(n_bands):
bands[b] = (b - 2) * 1.5 + 0.8 * np.sin(2 * np.pi * k_path / 4 + b)
fig, ax = plt.subplots(figsize=(7, 5))
for b in range(n_bands):
ax.plot(k_path, bands[b], color="tab:blue", lw=1.2)
ax.axhline(0, color="k", lw=0.8, ls="--") # 费米能级 E_F = 0
# 高对称点:用竖直虚线分隔 + 下方标注符号
k_labels_pos = [0, 1, 2, 3, 4]
k_labels = ["Γ", "X", "M", "Y", "Γ"]
for k in k_labels_pos:
ax.axvline(k, color="gray", lw=0.7, ls="--", alpha=0.6)
ax.set_xticks(k_labels_pos)
ax.set_xticklabels(k_labels, fontsize=13)
ax.set_xlim(0, 4)
ax.set_ylim(-4, 4)
ax.set_ylabel(r"$E - E_F$ / eV", fontsize=13)
ax.tick_params(direction="in", top=True, right=True)
ax.set_title("能带结构(示意)", fontsize=13)
fig.savefig("band_structure.pdf", bbox_inches="tight")
11.2 态密度 DOS 图
自旋极化计算通常画"自旋向上(正值)/ 自旋向下(负值)",用 fill_between 填充:
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(1)
E = np.linspace(-6, 6, 400) # 能量轴
def gaussian(x, mu, sigma):
return np.exp(-(x - mu)**2 / (2 * sigma**2)) / (sigma * np.sqrt(2 * np.pi))
# 用几个高斯峰模拟 DOS(spin up 为正,spin down 为负)
dos_up = (2.0 * gaussian(E, -1.0, 0.6) + 1.2 * gaussian(E, 0.8, 0.4)
+ 0.8 * gaussian(E, 2.5, 0.5))
dos_dn = (1.8 * gaussian(E, -0.6, 0.7) + 1.0 * gaussian(E, 1.5, 0.5)
+ 0.6 * gaussian(E, 3.0, 0.4))
fig, ax = plt.subplots(figsize=(6, 6))
ax.fill_between(E, dos_up, 0, color="tab:blue", alpha=0.5, label="spin up")
ax.fill_between(E, -dos_dn, 0, color="tab:red", alpha=0.5, label="spin down")
ax.axvline(0, color="k", lw=0.8, ls="--") # 费米能级
ax.set_xlabel(r"$E - E_F$ / eV", fontsize=13)
ax.set_ylabel("DOS / states eV$^{-1}$", fontsize=13)
ax.legend()
ax.set_title("态密度 DOS(自旋极化,示意)", fontsize=13)
fig.savefig("dos.pdf", bbox_inches="tight")
真实数据:VASP 的 DOSCAR / vasprun.xml 可用
pymatgen的DOSPlotter或
vaspkit导出.dat后np.loadtxt读取,再套用上面的填充画法。
11.3 能带 + DOS 组合图(subplot_mosaic,共享 y 轴)
这是凝聚态 / 催化论文最常见的"组合图"。DOS 转 90° 放在右侧,与能带共享能量轴:
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(2)
k = np.linspace(0, 4, 200)
E = np.linspace(-6, 6, 400)
# 模拟能带
bands = np.array([(b - 2) * 1.5 + 0.8 * np.sin(2 * np.pi * k / 4 + b)
for b in range(5)])
# 模拟 DOS
dos = (2.0 * np.exp(-(E - 0.5)**2 / 0.5) + 1.0 * np.exp(-(E + 1.5)**2 / 0.6))
fig, axd = plt.subplot_mosaic(
[["band", "dos"]],
figsize=(9, 5),
width_ratios=[3, 1],
layout="constrained",
)
ax_band = axd["band"]
ax_dos = axd["dos"]
# 左:能带
for b in range(5):
ax_band.plot(k, bands[b], color="tab:blue", lw=1.2)
ax_band.axhline(0, color="k", lw=0.8, ls="--")
for kp in [0, 1, 2, 3, 4]:
ax_band.axvline(kp, color="gray", lw=0.7, ls="--", alpha=0.6)
ax_band.set_xticks([0, 1, 2, 3, 4])
ax_band.set_xticklabels(["Γ", "X", "M", "Y", "Γ"], fontsize=12)
ax_band.set_ylabel(r"$E - E_F$ / eV", fontsize=13)
ax_band.set_xlim(0, 4)
ax_band.set_ylim(-6, 6)
# 右:DOS(横着画,与能带共享 y 轴)
ax_dos.plot(dos, E, color="tab:red", lw=1.5)
ax_dos.fill_betweenx(E, dos, 0, color="tab:red", alpha=0.4)
ax_dos.axhline(0, color="k", lw=0.8, ls="--")
ax_dos.set_ylim(-6, 6)
ax_dos.set_xlabel("DOS", fontsize=12)
ax_dos.set_yticks([]) # 隐藏右侧 DOS 图的 y 轴刻度,避免重复
ax_band.set_title("能带结构 + 态密度(示意)", fontsize=13)
fig.savefig("band_dos.pdf", bbox_inches="tight")
技巧:
width_ratios=[3, 1]让能带占 3 份宽、DOS 占 1 份;fill_betweenx是
"沿 y 轴方向填充"版本。若要真正共享同一能量轴刻度,可给subplot_mosaic
传sharey=True(但这里 DOS 有自己的刻度语义,故手动控制)。
11.4 催化性能对比柱状图(分组柱)
比较"不同催化剂在不同电位下的法拉第效率 / 产物选择性",用分组柱 + 图例:
import numpy as np
import matplotlib.pyplot as plt
catalysts = ["Cu", "Ag", "Au", "Zn"]
fe_co = [12, 5, 8, 3] # CO 法拉第效率 / %
fe_h2 = [30, 45, 25, 60] # H2 法拉第效率 / %
fe_hcooh = [45, 20, 55, 15] # HCOOH 法拉第效率 / %
x = np.arange(len(catalysts))
width = 0.25
fig, ax = plt.subplots(figsize=(7, 5))
r1 = ax.bar(x - width, fe_co, width, label="CO", color="tab:blue")
r2 = ax.bar(x, fe_h2, width, label="H$_2$", color="tab:gray")
r3 = ax.bar(x + width, fe_hcooh, width, label="HCOOH", color="tab:orange")
ax.set_ylabel("法拉第效率 / %", fontsize=12)
ax.set_xticks(x)
ax.set_xticklabels(catalysts, fontsize=12)
ax.set_ylim(0, 100)
ax.legend(ncol=3)
ax.set_title("CO$_2$RR 产物选择性对比(示意)", fontsize=13)
fig.savefig("faradaic_bar.pdf", bbox_inches="tight")
11.5 吸附能误差棒图
不同金属 / 晶面对同一吸附质的吸附能,含误差棒,并加"火山曲线"参考线:
import numpy as np
import matplotlib.pyplot as plt
surfaces = ["Cu(111)", "Cu(211)", "Zn(101)", "Pd(111)", "Pt(111)"]
E_CO = [-0.51, -0.78, -0.42, -1.20, -1.55] # CO 吸附能 / eV
err = [0.05, 0.09, 0.04, 0.07, 0.06]
fig, ax = plt.subplots(figsize=(8, 5))
ax.errorbar(surfaces, E_CO, yerr=err, fmt="s", markersize=9,
capsize=5, capthick=1.5, elinewidth=1.3,
color="tab:red", ecolor="tab:gray", label=r"$E_{ads}$(CO)")
ax.axhline(0, color="k", lw=0.8)
# 标注"弱吸附"和"强吸附"区
ax.axhspan(-1.4, -0.9, color="tab:blue", alpha=0.12, label="强吸附区")
ax.axhspan(-0.3, 0.0, color="tab:green", alpha=0.12, label="弱吸附区")
ax.set_ylabel(r"吸附能 $E_{ads}$ / eV", fontsize=13)
ax.tick_params(direction="in", top=True, right=True)
ax.legend()
ax.set_title("CO 吸附能对比(示意)", fontsize=13)
fig.savefig("adsorption_errorbar.pdf", bbox_inches="tight")
11.6 反应势能曲线(反应坐标图)
标注反应物 / 过渡态 / 产物,以及活化能 Ea 和反应能 ΔG:
import numpy as np
import matplotlib.pyplot as plt
# 反应坐标(离散的几个关键点)
rc = [0.0, 0.4, 1.0, 1.6, 2.0] # 反应物 -> TS -> 中间体 -> 产物
E = [0.0, 0.85, 0.30, 1.10, -0.45] # 相对能量 / eV(TS 是鞍点)
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(rc, E, marker="o", markersize=8, color="tab:blue", lw=2.0)
# 标注关键点
ax.annotate("反应物", xy=(0.0, 0.0), xytext=(0.05, 0.35), fontsize=11,
arrowprops=dict(arrowstyle="->", lw=1.0))
ax.annotate("TS1", xy=(0.4, 0.85), xytext=(0.5, 1.15), fontsize=11,
arrowprops=dict(arrowstyle="->", lw=1.0))
ax.annotate("TS2", xy=(1.6, 1.10), xytext=(1.25, 1.45), fontsize=11,
arrowprops=dict(arrowstyle="->", lw=1.0))
ax.annotate("产物", xy=(2.0, -0.45), xytext=(1.6, -1.0), fontsize=11,
arrowprops=dict(arrowstyle="->", lw=1.0))
# 用双箭头标注能垒 / 反应能
ax.annotate("", xy=(0.4, 0.0), xytext=(0.4, 0.85),
arrowprops=dict(arrowstyle="<->", color="r", lw=1.5))
ax.text(0.43, 0.4, r"$E_a$ = 0.85 eV", color="r", fontsize=11)
ax.annotate("", xy=(2.0, -0.45), xytext=(2.0, 0.0),
arrowprops=dict(arrowstyle="<->", color="g", lw=1.5))
ax.text(2.03, -0.25, r"$\Delta G$ = -0.45 eV", color="g", fontsize=11)
ax.set_xlabel("反应坐标", fontsize=13)
ax.set_ylabel("相对能量 / eV", fontsize=13)
ax.set_xticks([]) # 反应坐标没有物理刻度,隐藏数字
ax.set_ylim(-1.3, 1.6)
ax.set_title("反应势能曲线(示意)", fontsize=13)
fig.savefig("reaction_path.pdf", bbox_inches="tight")
12. seaborn 简要提及(何时用)
Seaborn 是构建在 Matplotlib 之上的高层统计绘图库,底层仍是 Matplotlib 对象。
原则:
- 探索数据 / 快速统计图:用 seaborn(
boxplotviolinplotkdeplotheatmap
pairplot一行出图,比手写 Matplotlib 快得多)。 - 投稿精修图:用 seaborn 出初步图后,拿到它返回的
ax,再用 Matplotlib 精调。
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt
sns.set_theme(style="ticks", context="paper", font="SimHei") # 全局样式,font 可设中文
rng = np.random.default_rng(3)
data = {"cat": np.repeat(["A", "B", "C", "D"], 50),
"value": rng.normal(0, 1, 200)}
fig, ax = plt.subplots(figsize=(6, 4))
sns.boxplot(data=data, x="cat", y="value", ax=ax) # 显式传入 ax,保持 OO 风格
ax.set_ylabel("吸附能 / eV")
ax.set_title("seaborn 箱线图(底层是 Matplotlib)")
fig.savefig("sns_box.pdf", bbox_inches="tight")
关键点:
sns.boxplot(..., ax=ax)里显式传ax,seaborn 就会画到你的Axes上,
后续继续用ax.set_*精调,两个库完全兼容。
13. 常见坑(务必先读)
-
中文字符显示成方框(tofu):
- 根因:Matplotlib 默认字体(DejaVu Sans)不含中文字形。
- 解决:设
font.sans-serif列表(优先 SimHei / Microsoft YaHei)+ 确认字体已
登记(第 9.2 节的检查代码)。必要时font_manager.fontManager.addfont(...)。
-
负号 / 减号显示成方块:
- 根因:默认用 Unicode 减号
−,中文字体里缺这个字形。 - 解决:
matplotlib.rcParams["axes.unicode_minus"] = False。
- 根因:默认用 Unicode 减号
-
子图重叠 / 标题被裁掉:
- 解决:
plt.subplots(..., layout="constrained")或fig.tight_layout()。
二选一即可,别同时用(会冲突)。subplot_mosaic里用layout="constrained"。
- 解决:
-
保存的 png 模糊 / 马赛克:
- 根因:
dpi太低(默认savefig.dpi可能 < 150)。 - 解决:
fig.savefig("x.png", dpi=300)。投稿用pdf/svg矢量格式彻底规避。
- 根因:
-
用
jetcolormap 画出"假边界":- 根因:
jet亮度不均匀,人为制造数据里不存在的结构。 - 解决:换成
viridis/plasma/cividis(顺序),RdBu_r/coolwarm(发散)。
- 根因:
-
plt.plot与ax.plot混用导致画错子图:- 根因:pyplot 操作的是"隐式当前 Axes",多子图时状态错乱。
- 解决:全程显式用
ax.xxx(...),plt只用于subplots/show/rcParams。
-
图例盖住数据:
- 解决:
ax.legend(loc="best")或bbox_to_anchor=(1.02, 1)放到图外。
- 解决:
-
imshow图像上下颠倒:- 解决:物理数据加
origin="lower";再配extent=[xmin,xmax,ymin,ymax]设真实坐标。
- 解决:物理数据加
-
颜色条
colorbar尺寸/位置不对:- 解决:用
fig.colorbar(im, ax=ax)显式绑定ax,颜色条会自动对齐该 Axes 高度。
- 解决:用
-
旧教程里的已删除 API:
matplotlib.cm.get_cmap(3.9 移除)→ 用matplotlib.colormaps["viridis"]。plt.style.use("seaborn")(3.6 弃用)→ 用"seaborn-v0_8"系列。boxplot(..., labels=...)(3.9 弃用)→ 用tick_labels=...。
14. 延伸资源
- 官方画廊(Gallery):https://matplotlib.org/stable/gallery/ —— 每个图都有源码,
直接搜图型 + 改数据。这是最高效的学习入口。 - 官方 API 参考:https://matplotlib.org/stable/api/index.html —— 查参数只看这里。
- Cheat Sheet:Matplotlib 官方提供官方速查表 PDF,打印贴桌上。
- pymatgen 出图:https://pymatgen.org ——
BSPlotter(能带)、DOSPlotter(态密度)、
PhaseDiagram(相图)可直接出图,且返回 Matplotlib 对象可继续精修。 - ASE 出图:https://wiki.fysik.dtu.dk/ase/ —— 原子结构 / 动画可视化,配合
Matplotlib 保存。 - "Scientific Visualization: Python + Matplotlib"(Nicolas P. Rougier,免费开源书)
—— 进阶配色、排版、多面板布局的权威参考。 - Paul Tol 配色:https://personal.sron.nl/~pault/ —— 色盲友好的科研配色标准。
附:一个可直接套用的"绘图脚本骨架"
# -*- coding: utf-8 -*-
"""科研出图脚本骨架:把 rcParams 设置 + 你的绘图函数组织起来。"""
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
# ---------- 1. 全局样式(中文 + 负号 + 字号 + 分辨率)----------
matplotlib.rcParams.update({
"font.sans-serif": ["Microsoft YaHei", "SimHei", "SimSun"],
"axes.unicode_minus": False,
"font.size": 11,
"axes.labelsize": 12,
"axes.titlesize": 13,
"xtick.labelsize": 10,
"ytick.labelsize": 10,
"legend.fontsize": 10,
"axes.linewidth": 1.2,
"lines.linewidth": 1.5,
"xtick.direction": "in",
"ytick.direction": "in",
"figure.dpi": 150,
"savefig.dpi": 300,
})
# ---------- 2. 你的绘图函数(只接收 ax,便于复用 / 组合)----------
def plot_my_result(data, ax):
"""在这里写具体绘图逻辑,data 换成你的 np.loadtxt / pandas 结果。"""
ax.plot(data[:, 0], data[:, 1], label="my data")
ax.set_xlabel("x / unit")
ax.set_ylabel("y / unit")
ax.legend()
return ax
# ---------- 3. 主流程 ----------
if __name__ == "__main__":
x = np.linspace(0, 10, 100)
y = np.sin(x)
fig, ax = plt.subplots(figsize=(6, 4))
plot_my_result(np.column_stack([x, y]), ax)
fig.savefig("result.pdf", bbox_inches="tight") # 投稿矢量图
fig.savefig("result.png", dpi=300, bbox_inches="tight") # 预览位图
plt.show()
评论交流
欢迎留下你的想法