zcbot/skills/plot_pub/SKILL.md

236 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: plot_pub
description: 出版级 matplotlib 绘图(论文 / 报告 / 申报书用,中文字体 + viridis 配色 + dpi 设定一键到位)。✅ 触发:用户要画 XRD 谱、TG-DSC 曲线、应力-应变曲线、SEM 标注、多 panel 学术图、投稿级 / Nature 级复合图、要 SVG/PDF 矢量图给论文。⛔ 不触发:PPT 内嵌图(走 `ppt` skill 的 matplotlib 配图流程);只要快速看一眼数据(直接 `df.plot()` 即可,不用本 skill)。
---
# plot_pub
服务建材院的论文 / 申报书 / 调研报告出图。**核心是 `apply_pub_style()` 一键设置 rcParams**(中文字体、viridis 默认、字号、dpi、矢量优先),避免每次手动设置十几行还忘配中文字体。
## 何时用
- 写论文要投 *Cement and Concrete Research* / *J. Am. Ceram. Soc.* / *Construction and Building Materials* 等期刊
- 报告 / 申报书要中文标题 + 中文图例的高 dpi 图
- 多组样本数据对比(XRD 多谱叠图、强度发展曲线、温度场)
- 双 Y 轴(TG-DSC 是典型)
## 何时不用
- PPT 里要嵌图 → 走 `skills/ppt`(那边已经有配色 / 尺寸规范)
- 交互式探查数据 → 用 plotly / 直接 jupyter
- 只要看趋势不出版 → `df.plot()` 一行搞定,不用本 skill 的 ceremony
## 准备
```python
from skills.plot_pub.style import apply_pub_style
import matplotlib.pyplot as plt
import numpy as np
apply_pub_style() # 调一次,后面所有 plt 都按这个 style
```
## `apply_pub_style()` 参数
| 参数 | 默认 | 说明 |
|---|---|---|
| `chinese` | `True` | 加载 SimHei / Microsoft YaHei,中文不显示方块 |
| `dpi` | `150` | 屏幕显示 dpi;保存图用 `savefig(..., dpi=300)` 单独控 |
| `font_size` | `10` | 论文双栏图标准字号 |
| `linewidth` | `1.2` | 线粗,出版图细些显精致 |
| `cmap` | `"viridis"` | 默认 colormap,**审稿人不会喷**(jet 现在是雷区) |
## 典型工作流
### A. XRD pattern(多相叠图 + 标峰)
```python
from skills.plot_pub.style import apply_pub_style
import matplotlib.pyplot as plt
import numpy as np
apply_pub_style()
fig, ax = plt.subplots(figsize=(6, 4))
# 实测谱
ax.plot(two_theta_exp, intensity_exp, "k-", lw=1.0, label="实测")
# 理论谱(各物相,用 pymatgen XRDCalculator 算的)
for phase, pattern in pymatgen_patterns.items():
ax.vlines(pattern.x, 0, pattern.y / pattern.y.max() * intensity_exp.max() * 0.3,
label=phase, alpha=0.7)
ax.set_xlabel(r"$2\theta$ / °")
ax.set_ylabel("强度 / a.u.")
ax.set_xlim(5, 80)
ax.legend(frameon=False, loc="upper right")
fig.tight_layout()
fig.savefig("xrd_compare.pdf", bbox_inches="tight") # PDF 矢量,期刊偏好
fig.savefig("xrd_compare.png", dpi=300, bbox_inches="tight") # PNG 给 PPT
plt.close(fig) # 关键!run_python 多任务下不关会堆 figure 泄漏内存
```
### B. TG-DSC 双 Y 轴
```python
fig, ax_tg = plt.subplots(figsize=(6, 4))
ax_dsc = ax_tg.twinx()
ax_tg.plot(T, tg_mass, "b-", lw=1.2, label="TG")
ax_dsc.plot(T, dsc_flow, "r-", lw=1.2, label="DSC")
ax_tg.set_xlabel("温度 / °C")
ax_tg.set_ylabel("质量 / %", color="b")
ax_dsc.set_ylabel("热流 / mW·mg$^{-1}$", color="r")
ax_tg.tick_params(axis="y", labelcolor="b")
ax_dsc.tick_params(axis="y", labelcolor="r")
# 合并两个 ax 的 legend
lines1, labels1 = ax_tg.get_legend_handles_labels()
lines2, labels2 = ax_dsc.get_legend_handles_labels()
ax_tg.legend(lines1 + lines2, labels1 + labels2, loc="best", frameon=False)
fig.tight_layout()
fig.savefig("tg_dsc.pdf", bbox_inches="tight")
plt.close(fig)
```
### C. 多组样本对比(强度发展曲线)
```python
fig, ax = plt.subplots(figsize=(6, 4))
ages = [3, 7, 14, 28, 56, 90]
# 用 viridis 给多个样本上色,顺序色觉友好
import matplotlib.cm as cm
colors = cm.viridis(np.linspace(0.1, 0.9, len(samples)))
for (label, strengths), c in zip(samples.items(), colors):
ax.plot(ages, strengths, "o-", color=c, lw=1.2, ms=5, label=label)
ax.set_xlabel("龄期 / d")
ax.set_ylabel("抗压强度 / MPa")
ax.set_xscale("log") # 龄期用对数刻度更清楚
ax.legend(frameon=False)
ax.grid(True, ls="--", alpha=0.3)
fig.tight_layout()
fig.savefig("strength_dev.pdf", bbox_inches="tight")
plt.close(fig)
```
### D. 多 panel(2×2 figure,论文 figure 1 标配)
```python
fig, axes = plt.subplots(2, 2, figsize=(10, 7), constrained_layout=True)
axes[0, 0].plot(...) # (a) XRD
axes[0, 1].plot(...) # (b) FTIR
axes[1, 0].plot(...) # (c) TG
axes[1, 1].plot(...) # (d) 强度
# 子图编号(论文图 1 标准做法)
for ax, letter in zip(axes.flat, "abcd"):
ax.text(0.02, 0.95, f"({letter})", transform=ax.transAxes,
fontweight="bold", va="top")
fig.savefig("fig1_characterization.pdf", bbox_inches="tight")
plt.close(fig)
```
## 投稿级多 panel 复合图(Nature-grade composite)
要投高影响期刊、要一张"封面级"的多 panel figure 1 时,光排版正确不够 —— 还要**让图讲一个故事**。下面这套设计纪律移植自 `nature-figure` skill(MIT,[github.com/Yuan1z0825/nature-skills](https://github.com/Yuan1z0825/nature-skills)),砍掉其 R / 单细胞 / 在体生物那一套,只留可迁移的部分,适配建材领域。
### 动手前:五点 figure contract
画复合图前先在心里(或跟用户)把这五条对齐,**先想清楚再下笔**,避免画完六张图才发现讲不成一个故事:
1. **核心结论**:这张图要捍卫的**一句话**论断是什么?(例:"复合掺合料把 28d 强度提升 18% 且不牺牲流动度")
2. **证据链**:每个 panel 对应**唯一**一条证据,删掉冗余 panel —— 同一份数据别用两种图形再画一遍,同一组指标别排两次序
3. **图原型**:归类为 ① 定量网格(全是量化对比图)② schematic 主导(机理图 + 验证小图)③ 图像板 + 定量(SEM/光学 plate + 旁证曲线)④ 非对称混合;先定原型再排版
4. **后端**:本 skill 纯 matplotlib(Python),所有 panel / 预览 / 导出统一一套后端出
5. **期刊契约**:开画前定好尺寸(单栏 ~89mm / 双栏 ~183mm)、可编辑矢量(已由 `apply_pub_style``svg.fonttype='none'`)、source data、统计标注、导出格式
> 一句话原则:**图为科学逻辑服务**,美化 / 套模板 / 复杂排版都是次要的。
### 语义配色(比 viridis 更进一步)
单组数据用 viridis 没问题;但**多组对比 / 方法 vs 基线**时,颜色要承载语义,而不是随机区分。`style.py` 已内置一张语义色表:
```python
from skills.plot_pub.style import apply_pub_style, SEMANTIC_COLORS, clean_spines, ablation_alphas
apply_pub_style()
# 蓝=本方法/主角 绿=提升 红=baseline/对照 灰=参照 橙=少量强调
ax.bar(x0, y_base, color=SEMANTIC_COLORS["baseline"], label="基准配方")
ax.bar(x1, y_method, color=SEMANTIC_COLORS["method"], label="复合掺合料")
```
原则:**family consistency beats maximal hue separation** —— 相关的几条基线归一个色系,本方法的几个变体归另一个色系,别把颜色撒得到处都是。消融 / 梯度对比用**同色变 alpha**(0.25→1.0)而不是换色相:
```python
colors = ablation_alphas(len(掺量梯度)) # 同蓝色由浅到深
for (label, y), c in zip(掺量梯度.items(), colors):
ax.plot(x, y, color=c, label=label)
```
### 信息架构 + spine 纪律
- 多 panel 要**作为一个故事读**,不是六张互不相干的图拼一起
- 有 schematic / hero panel 时给它视觉主导,旁边的验证小图别喧宾夺主
- 每个 panel 调 `clean_spines(ax)` —— 只留左 + 下边框,去掉上 + 右,信噪比立刻上来
- legend 无框(`apply_pub_style` 已设)、网格抑制(只在需要时留稀疏 y 网格)
```python
fig, axes = plt.subplots(1, 3, figsize=(7.2, 2.6), constrained_layout=True) # 双栏宽 ~183mm
axes[0].bar(...) # (a) 概览:堆叠/分组柱
axes[1].imshow(...) # (b) 偏差:z-score 热图
axes[2].scatter(...) # (c) 关系:散点/气泡 —— 三级递进,各答一个不同的科学问题
for ax in axes:
clean_spines(ax)
for ax, letter in zip(axes, "abc"):
ax.text(-0.12, 1.02, f"({letter})", transform=ax.transAxes, fontweight="bold", va="bottom")
fig.savefig("fig1_composite.svg", bbox_inches="tight") # SVG 主格式,文字可编辑
fig.savefig("fig1_composite.png", dpi=300, bbox_inches="tight") # PNG 仅作 raster 预览
plt.close(fig)
```
### 导出纪律
- **SVG 为主格式**(文字可编辑,编辑部/作者能改),PDF 矢量并行,PNG 300dpi 仅作预览
- 别拿 PNG 当投稿正图(见反模式 #6)
## 中文字体配置(Windows 注意)
`apply_pub_style(chinese=True)` 默认按以下顺序找字体:
1. `SimHei`(Windows 自带,黑体)
2. `Microsoft YaHei`(Windows 自带,雅黑)
3. `WenQuanYi Micro Hei`(Linux 兜底)
都没有 → 退回 DejaVu Sans,中文显示方块。装字体方式:Windows 控制面板 → 字体,丢 .ttf 进去。
**LaTeX 公式渲染**:matplotlib 用 `$...$` 自动走 mathtext,中英文混排正常。要更精细的 LaTeX(自定义 macro)就 `plt.rcParams["text.usetex"] = True`,但要本地装 TeX Live。
## 反模式
1. **用 pyplot 状态机不用 OO 接口** —— 写 `plt.plot(); plt.xlabel(...)` 一时爽,多 figure 时变量乱;一律 `fig, ax = plt.subplots()`
2. **不 close figure** —— `run_python` 跑多次会堆 figure 内存涨,必须 `plt.close(fig)` 收尾
3. **用 jet colormap** —— 色觉障碍审稿人会喷,近年期刊都偏 viridis / plasma / cividis
4. **保存图不 `bbox_inches="tight"`** —— legend / 标题被裁掉,反复看图反复改尺寸
5. **同图 dpi 跟 figsize 不匹配** —— figsize=(10,7) dpi=300 给 PPT 嵌入太大,字会很小;论文图 figsize=(6,4) 双栏刚好
6. **PNG 给论文** —— 期刊要矢量,PDF / SVG / EPS 优先;PNG 只给 PPT / 网页
7. **中文显示方块还坚持跑** —— `apply_pub_style()` 失败会 warn,看到 warning 就停下查字体
8. **legend 框线 / 灰底** —— 出版图 `legend(frameon=False)`,干净
9. **轴标签没单位** —— `"温度"` 是错的,`"温度 / °C"` 才对(SI 推荐 `/` 分隔)
10. **`ax.set_xticks([1,2,3,...])` 手动写一堆** —— matplotlib 自动 tick 已经足够,手动覆盖只在特殊场景(对数轴 / 离散类别轴)
## 依赖
matplotlib 已经在 `requirements.txt`(>=3.8.0)。本 skill 无新增依赖。