# 第 14 章 论文级图表与可视化

> 前置：已完成第 08–13 章的分析，手上有生态位、通讯、差异通讯的结果对象。本章教你把这些结果画成“能进论文”的图。

## 本章目标

1. 掌握论文级图表的通用规范：字号、配色、坐标轴、图注、分辨率与导出格式。
2. 学会 6 类关键图表的“何时用、怎么读、代码要点”：空间散点图、气泡图、通讯网络图、热图、堆叠条形图、Sankey 流量图。
3. 学会“一图一故事”的结果图编排策略，看懂 Figure 1–4 的组织逻辑，并以 `fig17_paper_figure.png` 为范例拆解 A/B/C/D 面板。
4. 掌握 R（showtext）与 Python（matplotlib 字体注册）的中文字体设置，解决中文乱码。
5. 能对照“常见图表错误表”自查并修正自己的图。

## 14.1 论文级图表通用规范

新手常犯的误区是“图能出就行”。论文审稿人看图的顺序是：先看整体是否整洁、再看字号是否可读、最后才看内容。下面 6 条规范是底线，请逐条对照自己的图。

**（1）字号：正文 8–10 pt，图内最小不小于 6 pt。** 期刊排版会把图缩到栏宽（约 8–9 cm），图内文字如果小于 6 pt，印刷后几乎不可读。建议：图内文字 7–8 pt，轴标题与图例标题 9 pt。在 R 里用 `base_size` 统一控制，在 Python 里用 `rcParams` 统一控制。

**（2）配色：色盲友好。** 全球约 8% 男性和 0.5% 女性有红绿色觉缺陷，红绿对比对他们是灾难。两个安全方案：

- 连续色（数值渐变）用 **viridis**：从深紫到亮黄，色觉缺陷下依然可区分明度变化。
- 分类色（离散组别）用 **Okabe-Ito** 8 色调色板：`#E69F00` 橙、`#56B4E9` 蓝、`#009E73` 绿、`#F0E442` 黄、`#0072B2` 深蓝、`#D55E00` 红、`#CC79A7` 粉、`#000000` 黑。本书教学图默认采用这一约定（各图配色见 `00-FIGURES` 清单）。

本书生态位图的四色约定以第 10 章 `fig10` 为准：肿瘤核心、侵袭前沿、免疫浸润区、基质区四种生态位使用四种可区分的分类色，全书统一，避免同一生态位在不同图里颜色不同。

**（3）坐标轴。** 轴标题写清楚“变量（单位）”；刻度线朝外；刻度标签字号不小于 6 pt；坐标范围必须覆盖全部数据点，不要截断造成误导；两幅并列的图使用相同坐标范围，方便对比。

**（4）图注要素（figure caption）。** 一句“结论性”标题 + 关键信息，通常包含：样本量 n、统计方法与校正（如“BH-FDR 校正，**P** < 0.05”）、色标含义、必要的缩写解释。本书所有模拟数据图注必须注明“（模拟数据演示）”。

**（5）分辨率：300 dpi。** 印刷需要 300 dpi，屏幕预览 150 dpi 足够。导出时直接设 300 dpi，宁可文件大一点。

**（6）导出格式：PNG 用于投稿系统与正文预览，PDF（矢量）用于审稿与后续编辑。** 矢量图放大不糊，且文字可选，方便编辑替换字体。

下面是一段可运行的 R 导出模板（假设你在 RStudio 里）：

```r
# 目的：统一设置 ggsave 导出参数，保证全书图风格一致
library(ggplot2)

# ggplot 默认主题微调：图内文字 8 pt、白色背景、细网格
theme_paper <- theme_classic(base_size = 8) +
  theme(axis.ticks.length = unit(1.5, "pt"),
        legend.key.size = unit(3, "mm"))

p <- ggplot(mtcars, aes(mpg, wt)) + geom_point() + theme_paper

# PNG：300 dpi；PDF：矢量导出
ggsave("figures/figXX_example.png", p,
       width = 9, height = 8, units = "cm", dpi = 300)
ggsave("figures/figXX_example.pdf", p, width = 9, height = 8, units = "cm")
```

**运行结果解读**：`figures/` 下出现同名 PNG 与 PDF 两个文件，PNG 的像素尺寸约为 `9/2.54*300 × 8/2.54*300`，即约 1063×945 像素，满足印刷要求。

Python 侧对应设置：

```python
import matplotlib.pyplot as plt

plt.rcParams.update({
    "font.size": 8,                       # 全局字号 8 pt
    "axes.titlesize": 9,
    "axes.labelsize": 8,
    "xtick.labelsize": 7,
    "ytick.labelsize": 7,
    "figure.dpi": 300,                    # 输出分辨率
    "savefig.dpi": 300,
    "axes.edgecolor": "black",
})
fig, ax = plt.subplots(figsize=(3.5, 3.2))   # 约 9×8 cm
ax.scatter(range(10), range(10), s=12, c="#0072B2")
fig.savefig("figures/figXX_example.png", bbox_inches="tight")
fig.savefig("figures/figXX_example.pdf", bbox_inches="tight")
```

**常见坑**：只导 PNG 不导 PDF；字号用默认值（R 默认 11 pt、matplotlib 默认 10 pt），被期刊缩小后糊成一团；用红绿配色表达“上调/下调”。

## 14.2 关键图表制作清单

本书分析主线会产出 6 类图。每类图都按“何时用 + 怎么读 + 代码要点”给出，请对照你自己的结果选择。

### 14.2.1 空间散点图（SpatialDimPlot / sc.pl.spatial_scatter）

- **何时用**：把任何“按 spot 的属性”（QC 指标、聚类、细胞类型、生态位、通讯活性）映射回组织切片，回答“这个属性在组织上长在哪”。
- **怎么读**：颜色即属性；同一生态位的 spot 是否连成片；边界是否清晰；异常 spot（如高线粒体）是否集中在特定区域。
- **代码要点**：R 用 Seurat 的 `SpatialDimPlot`；Python 用 `squidpy.pl.spatial_scatter`。

```r
# 目的：把生态位标签画回组织切片（第 10 章产物）
library(Seurat)
SpatialDimPlot(obj, group.by = "niche", label = FALSE, label.size = 3,
               cols = c("#E69F00", "#56B4E9", "#009E73", "#F0E442")) +
  theme(legend.position = "bottom")
ggsave("figures/fig10_niche.png", width = 9, height = 8, units = "cm", dpi = 300)
```

```python
import squidpy as sq
sq.pl.spatial_scatter(adata, color="niche", size=0.4, cmap=None,
                      figsize=(4, 4), dpi=300)
```

**运行结果解读**：切片的 spot 网格按生态位着色，四种颜色形成四个空间区域；如果颜色呈“椒盐状”（大量孤立单点），说明生态位边界不稳定，回第 10 章调 q 或平滑参数。

### 14.2.2 气泡图（netVisual_bubble）

- **何时用**：展示“配体-受体对 × 细胞类型对（或生态位对）”的通讯，一行能同时看到强度（气泡大小）与显著性（颜色）。
- **怎么读**：行是 L-R 对，列是发送细胞→接收细胞；气泡越大通讯概率越高，颜色越深越显著；一眼看出“谁在给谁发信号”。
- **代码要点**：`netVisual_bubble(cellchat, sources.use, targets.use, signaling)`，可用 `signaling` 过滤到目标通路。

```r
# 目的：展示侵袭前沿与免疫浸润区之间特定通路的 L-R 通讯（模拟演示）
library(CellChat)
netVisual_bubble(cellchat, sources.use = c("Tumor", "CAF"),
                 targets.use = c("CD8 T", "Macrophage"),
                 signaling = c("TGFb", "MIF", "PD-L1"),
                 font.size = 8, font.size.title = 9)
```

**运行结果解读**：图中每个气泡对应一对“发送细胞类型→接收细胞类型”；若某个 L-R 对整行空白，说明该通讯在当前分组里不显著，不要硬解读。

### 14.2.3 圆形通讯网络图（netVisual_circle）

- **何时用**：展示整体通讯网络的拓扑结构，或按生态位 split 后并列比较各生态位的“通讯图景”（对应本书 fig12）。
- **怎么读**：节点是细胞类型/生态位，连线粗细代表通讯强度，箭头方向代表发送→接收；节点越大说明参与通讯越活跃。
- **代码要点**：`netVisual_circle(net, weight.scale = TRUE)`；多生态位并列用 `netVisual_circle` 配合 `split.by` 或对每个生态位分别成图后用 `patchwork` 拼接。

```r
# 目的：三个生态位的通讯网络并列（模拟演示，图 12-1 同款）
library(patchwork)
p_list <- lapply(c("Tumor core", "Invasive front", "Immune region"), function(n) {
  netVisual_circle(net_list[[n]], weight.scale = TRUE,
                   title.name = n, label.edge = FALSE)
})
wrap_plots(p_list, ncol = 3)
ggsave("figures/fig12_comm_network.png",
       width = 20, height = 7, units = "cm", dpi = 300)
```

**运行结果解读**：对比三张子图，侵袭前沿的“肿瘤→免疫”边是否明显变粗、是否出现独特的免疫抑制边（如 PD-L1 相关）——这正是“重编程”叙事的图形起点。

### 14.2.4 热图（ComplexHeatmap / pheatmap / matplotlib）

- **何时用**：矩阵型结果，如“生态位 × 信号通路活性”（fig15）、“细胞类型 × 细胞类型共定位 z 分数”（fig11）、“cluster × marker 基因表达”。
- **怎么读**：行/列聚类揭示结构；颜色渐变（如蓝→白→红）表示数值高低；配合行注释与列注释（如生态位分组色带）可同时读三层信息。
- **代码要点**：R 推荐 `ComplexHeatmap`（注释能力强）；Python 用 `seaborn.clustermap` 或 `matplotlib`。

```r
# 目的：生态位 × 通路活性热图（模拟演示，图 15-1 同款）
library(ComplexHeatmap)
library(viridis)
Heatmap(pathway_mat,               # 行=通路，列=生态位
        name = "activity",
        col = viridis(100),
        row_names_gp = gpar(fontsize = 8),
        column_names_gp = gpar(fontsize = 8),
        show_row_dend = TRUE, show_column_dend = FALSE)
# 导出：见 14.4 的 PDF 嵌入示例，先设 showtext 再 pdf()
```

**运行结果解读**：若侵袭前沿一列在 TGFb、MIF、PD-L1 等行同时呈高值（亮色块成簇），即为“侵袭前沿免疫抑制性重编程”的直观证据。

### 14.2.5 堆叠条形图

- **何时用**：展示组成比例，如“各生态位内细胞类型占比”（fig09 右）、“各生态位通讯数量/强度对比”（fig13 可用堆叠条形展示通路构成）。
- **怎么读**：条的总高=总数（或 100%），分段长度=各类占比；注意比较的是“比例”还是“绝对数量”，图注必须写清。
- **代码要点**：R 用 `ggplot2` 的 `geom_bar(position = "fill")`（比例）或 `position = "stack"`（数量）。

```r
# 目的：各生态位细胞组成比例（模拟演示）
library(ggplot2)
ggplot(comp_df, aes(x = niche, y = fraction, fill = celltype)) +
  geom_bar(stat = "identity", position = "fill", width = 0.7) +
  scale_fill_manual(values = c("#E69F00", "#56B4E9", "#009E73",
                               "#F0E442", "#0072B2", "#D55E00", "#CC79A7")) +
  theme_classic(base_size = 8) +
  labs(x = "生态位", y = "细胞类型占比") +
  theme(legend.position = "right")
ggsave("figures/fig09_niche_composition.png", width = 10, height = 7,
       units = "cm", dpi = 300)
```

**运行结果解读**：横轴四个生态位、纵轴 0–100%，每个条内各细胞类型分段；免疫浸润区应有更高的 T/B 占比，基质区应有更高的 CAF 占比。

### 14.2.6 Sankey 流量图

- **何时用**：展示“来源生态位 → 重编程信号通路 → 靶细胞类型”的信号流（fig16），把“哪里的信号、通过什么通路、到达什么细胞”讲成一条可读的流。
- **怎么读**：从左到右三列节点，连线的宽度正比于通讯活性；流越粗的路径越是“重编程主轴”。
- **代码要点**：R 用 `ggsankey` 包（`remotes::install_github("daattali/ggsankey")`），数据整理为“来源-目标-权重”长表；Python 可用 `plotly` 的 `sankey`。

```r
# 目的：重编程信号流 Sankey（模拟演示，图 16-1 同款）
library(ggsankey)
# df 三列：source(来源生态位) / target(通路或靶细胞) / value(通讯活性)
ggplot(df, aes(x = stage, next_x = next_stage,
               node = node, next_node = next_node,
               fill = node, value = value, label = node)) +
  geom_sankey(flow_alpha = 0.5, node.color = "gray30") +
  geom_sankey_label(size = 2.5, color = "black", fill = "white") +
  scale_fill_viridis_d() +
  theme_void(base_size = 8) + theme(legend.position = "none")
ggsave("figures/fig16_reprogramming.png", width = 14, height = 8,
       units = "cm", dpi = 300)
```

**运行结果解读**：从“侵袭前沿”出发的流若显著粗于其他来源，且汇入“PD-L1 / TGF-β”通路后指向 CD8 T 细胞，这就是全书的中心故事：**侵袭前沿通过免疫抑制通路重编程了 CD8 T 的微环境信号**。

## 14.3 一图一故事：结果图编排策略

有了 6 类单图，下一步是把它们编排成论文的“结果图”。原则是：**每张主图回答一个问题，每个面板是答案的一个证据**。本书建议的编排策略如下（对应 outline 的 Figure 1–4）。

- **Figure 1 路线图**：全书/课题的技术路线图（fig01 风格），一张图交代“数据 → 生态位 → 通讯 → 重编程”的流程，审稿人 30 秒内看懂你的分析框架。
- **Figure 2 生态位与组成**：生态位空间图（fig10）+ 邻域共现富集热图（fig11）+ 各生态位细胞组成堆叠条形（fig09 右），回答“组织里有哪些生态位、它们由什么细胞构成”。
- **Figure 3 通讯差异**：分生态位网络图（fig12）+ 通讯数量/强度柱状（fig13）+ 差异通讯气泡图（fig14），回答“生态位之间的通讯到底哪里不一样”。
- **Figure 4 重编程通路**：通路活性热图（fig15）+ 重编程信号流 Sankey（fig16），回答“差异背后的通路主轴是什么、信号流向哪里”，是全篇的高潮。

下面以本书范例图 `fig17_paper_figure.png`（模拟数据演示）讲解单张主图的 A/B/C/D 面板组织逻辑：

![图 14-1 论文结果图版式示例（模拟数据演示）。A：生态位识别（空间散点图，四色对应四种生态位）；B：邻域共现富集热图（细胞类型 × 细胞类型 z 分数，红=共定位富集）；C：差异通讯气泡图（侵袭前沿 vs 肿瘤核心，实心=前沿、空心=核心）；D：信号通路活性热图（生态位 × 通路）。](figures/fig17_paper_figure.png){width=90%}

**面板组织逻辑（阅读顺序：从左到右、从上到下，每面板回答一个问题）**：

- **A 面板：先给空间背景。** “组织是什么样、生态位在哪”——用空间散点图建立空间直觉，是后续所有分析的空间锚点。
- **B 面板：给组成证据。** “生态位由什么细胞构成、哪些细胞喜欢待在一起”——用共现富集热图证明生态位不是拍脑袋分的，而是有细胞共定位依据的。
- **C 面板：给差异证据。** “生态位之间的通讯差在哪”——用差异气泡图把“重编程”落到具体的 L-R 对上。
- **D 面板：给机制整合。** “差异汇成哪些通路”——用通路热图把几十对 L-R 归纳成几条通路主轴，让故事可被一句话概括。

四面板合在一起回答完整问题：“在**哪里**（A），**谁和谁**在一起（B），**信号怎么变**（C），**归结为什么通路**（D）”。每个面板只承担一个任务，不要在一个面板里塞两件事。

**图注写作模板**：`图 X 标题（结论句）。A：……（n=…，方法）；B：……（统计方法，色标含义）；C：……；D：……。模拟数据演示。`

## 14.4 中文字体设置

R 的 ggplot 与 Python 的 matplotlib 默认字体都不含中文字形，直接输出中文会变成方块（乱码）。两个方案如下。

**R：showtext 包**（推荐，无需改系统字体）：

```r
# 目的：让 R 图正常显示中文，并嵌入 PDF
library(showtext)
font_add("SimHei", "simhei.ttf")        # Windows 自带黑体；Mac 可换 "PingFang SC"
showtext_auto()                         # 之后所有 ggplot 自动用 showtext 渲染
# 画图…然后导出：
pdf("figures/figXX_cn.pdf", width = 4, height = 3.5)
print(p)                                # p 为含中文的 ggplot 对象
dev.off()
showtext_end()
```

**运行结果解读**：`showtext_auto()` 之后，ggplot 中 `labs(x = "生态位")` 等中文即可正常渲染；`pdf()` + `dev.off()` 导出的 PDF 已内嵌字形，投稿系统打开不会缺字。

**Python：matplotlib 字体注册**：

```python
# 目的：注册中文字体并设为默认
import matplotlib
from matplotlib import font_manager

font_path = "C:/Windows/Fonts/simhei.ttf"   # Windows 黑体路径
font_manager.fontManager.addfont(font_path)
prop = font_manager.FontProperties(fname=font_path)
matplotlib.rcParams["font.family"] = prop.get_name()  # 设为默认字体
matplotlib.rcParams["axes.unicode_minus"] = False     # 修复负号显示为方块
```

**运行结果解读**：设置后，`ax.set_xlabel("生态位")` 等中文正常显示；`axes.unicode_minus = False` 是关键一行——不设置时负号会因字体回退而显示为方块。

**常见坑**：只注册不设默认（`rcParams["font.family"]`）；Linux 服务器上没有 SimHei（需先 `apt install fonts-wqy-zenhei` 或改用文泉驿/思源字体）；PDF 导出时未嵌入字体导致换机后乱码（R 用 showtext、Python 用 `savefig` 的 `fonttype="cmap"` 可缓解，最稳妥是导出时确认字体内嵌）。

## 14.5 常见图表错误与修正

| 错误 | 后果 | 修正 |
| --- | --- | --- |
| 图内文字小于 6 pt | 印刷后不可读 | 统一 `base_size = 8` / `rcParams["font.size"] = 8` |
| 红绿表达“上调/下调” | 色觉缺陷读者无法区分 | 改用蓝-黄或 viridis；或用形状/实心空心区分（见 fig14 的实心=前沿、空心=核心） |
| 坐标轴范围截断 | 夸大差异、误导读者 | 坐标范围覆盖全部数据；对比图统一坐标 |
| 图注缺样本量与统计方法 | 审稿人要求补，返工 | 按 14.1(4) 模板补全 |
| 分辨率低于 300 dpi | 印刷模糊 | `dpi = 300`；矢量图用 PDF |
| 中文乱码 | 图不可用 | 见 14.4 字体设置 |
| 一张面板塞两个问题 | 读者抓不住重点 | 拆成两张图，或改为一图一故事 |
| 两图并列但坐标/配色不一致 | 对比误导 | 并列图统一坐标范围、统一配色方案 |
| 忘了“模拟数据演示”标注 | 教学图被误当真实结果 | 图注统一加注（见 00-FIGURES） |
| 只出 PNG 不出 PDF | 编辑无法编辑文字 | 同时导出 PNG 与 PDF |

## 本章小结

本章把“分析结果”变成“论文级图表”。记住三个关键词：**规范**（字号 8–10 pt、色盲友好配色、300 dpi、图注四要素）、**清单**（6 类图各司其职：空间散点图给空间背景、气泡图给 L-R 细节、网络图给拓扑、热图给矩阵结构、堆叠条形给组成、Sankey 给信号流）、**故事**（Figure 1 路线 → Figure 2 生态位 → Figure 3 通讯差异 → Figure 4 重编程通路，每面板回答一个问题）。中文字体问题用 showtext / matplotlib 字体注册解决。最后对照 14.5 的错误表自查一遍，你的图就达到投稿门槛了。

## 练习与思考

1. （概念题）为什么论文图要用 300 dpi 导出？屏幕截图直接贴进稿件会有什么问题？
2. （动手题）打开第 10 章的生态位对象，用 `SpatialDimPlot` 把生态位画回切片，按 14.2.1 的规范调整字号与配色，导出 300 dpi PNG 与 PDF 各一张。
3. （动手题）用第 13 章的差异通讯结果画一张 `netVisual_bubble`，只保留 `signaling = c("TGFb", "PD-L1", "MIF")` 的 L-R 对，并写出它的图注（含统计方法与“模拟数据演示”标注）。
4. （思考题）对照 `fig17_paper_figure.png`，说明为什么 B 面板（共现富集）放在 C 面板（差异通讯）之前更合理。
5. （挑战题）把第 13 章的通讯结果整理成 Sankey 长表（来源生态位→通路→靶细胞），用 `ggsankey` 重画 fig16 同款图，比较它和热图在“讲故事”上的差别。
