子目录
— 三分类火山图(顶刊风格,8x6cm):顶部图例带各类 n 计数 + 阈值虚线,蓝-灰-红配色,含通用复用脚本与完整踩坑记录。7.1_火山图1— 三组差异整合散点图(文献 Fig2I 复现):二维散点同时展示三个差异比较(x/y = 两组 log2FC,颜色+大小 = 第三组 log2FC),ggrepel 引线标签,附文献原始补充数据一键跑通。7.2_三组火山图
plot18 · 代码 2 · 图 3 · 笔记 3 · 代码库专题
火山图(Volcano Plot)是差异分析结果的标准可视化:横轴 log₂FC、纵轴 -log₁₀(padj),三分类 Up/Not/Down。
Fig2I7.2_三组火山图/Fig2I.pdf
共 2 张:点缩略图切换主图,下载按钮跟随当前主图。
图源:Fig2I.pdf
从专题 README 抽取;原文顺序保留,未跑过的部分照实标注。
火山图(Volcano Plot)是差异分析结果的标准可视化:横轴 log₂FC、纵轴 -log₁₀(padj),三分类 Up/Not/Down。
7.1_火山图1 — 三分类火山图(顶刊风格,8x6cm):顶部图例带各类 n 计数 + 阈值虚线,蓝-灰-红配色,含通用复用脚本与完整踩坑记录。7.2_三组火山图 — 三组差异整合散点图(文献 Fig2I 复现):二维散点同时展示三个差异比较(x/y = 两组 log2FC,颜色+大小 = 第三组 log2FC),ggrepel 引线标签,附文献原始补充数据一键跑通。ggrepel::geom_text_repel)2 个源码文件,按目录顺序;本机绝对路径已替换为 ~。 代码可复制,但需要按自己的数据改路径与参数。
# ============================================================
# 01_volcano_three_class.R — 三分类火山图(复现"顶刊风格")
#
# 说明:
# 参考某文献的火山图样式:顶部图例带各类 n 计数 +
# 两条竖直虚线(log2FC 阈值) + 一条水平虚线(padj 阈值)。
# 本脚本把输入路径参数化,可直接换到其他 DESeq2 结果复用。
#
# ⚠️ 本项目相关注意事项(踩坑记录):
# - 8x6cm 窄画布下,图例/轴标题/刻度字号必须相应缩小,
# 否则右侧文字贴边截断(详细见同级 README.md)
# - 点太小看不清 -> 增大 size 并注意层次(Up/Down 在 Not 之上)
# - y_lim / x_lim 要留 15% 呼吸空间,否则最高/最远点贴 panel 边
# - 52 个 padj=NA 基因(DESeq2 独立过滤后无 padj)会被 remove,
# 属正常,非 bug
#
# 输入: 一个 DESeq2 results 对象 .rds(list 内含 $tab,含
# gene_id / log2FC / padj 列)或直接传入 df
# 输出: fig_volcano_three_class.pdf(只 PDF,全局规则)
#
# 用法:
# Rscript 01_volcano_three_class.R --rds 02_results/deseq2_objects.rds \
# --tab deseq2_tab --out 03_figures/fig_volcano.pdf
# (无参数时用下方 DEFAULT 示教路径)
# ============================================================
suppressPackageStartupMessages({
library(data.table)
library(ggplot2)
})
# ---- 参数解析(命令行可覆盖默认值;支持 --k=v 和 --k v 两种写法) ----
args <- commandArgs(trailingOnly = TRUE)
arg_of <- function(k, default) {
i <- grep(paste0("^--", k, "(=|$)"), args)
if (!length(i)) return(default)
a <- args[i[1]]
if (grepl(paste0("^--", k, "="), a)) sub(paste0("^--", k, "="), "", a)
else if (i[1] < length(args)) args[i[1] + 1]
else default
}
rds_path <- arg_of("rds", "02_results/deseq2_objects.rds")
tab_name <- arg_of("tab", "tab") # rds 内保存表格的 list 成员名
out_pdf <- arg_of("out", "03_figures/fig_volcano_three_class.pdf")
out_w_cm <- as.numeric(arg_of("w", "8"))
out_h_cm <- as.numeric(arg_of("h", "6"))
# ---- 阈值(可覆盖) ----
padj_thr <- as.numeric(arg_of("padj_thr", "0.05"))
lfc_thr <- as.numeric(arg_of("lfc_thr", "0.585"))
# ---- 配色(用户指定,勿改) ----
cols <- c("Down" = "#2878B5", "Not" = "#CFCFCF", "Up" = "#EF3B35")
# ---- 读入数据(兼容 rds 对象或直接传 tab 的路径) ----
if (grepl("\\.rds$", rds_path)) {
obj <- readRDS(rds_path)
tab <- as.data.table(obj[[tab_name]])
} else {
tab <- as.data.table(readRDS(rds_path)) # 假设 rds 本身就是 data.frame
}
stopifnot(all(c("log2FC", "padj") %in% names(tab)))
# ---- 三分类 ----
tab[, cat := ifelse(!is.na(padj) & padj < padj_thr & log2FC > lfc_thr, "Up",
ifelse(!is.na(padj) & padj < padj_thr & log2FC < -lfc_thr, "Down", "Not"))]
tab$cat <- factor(tab$cat, levels = c("Down", "Not", "Up"))
n_down <- sum(tab$cat == "Down"); n_not <- sum(tab$cat == "Not"); n_up <- sum(tab$cat == "Up")
cat(sprintf("Down=%d Not=%d Up=%d\n", n_down, n_not, n_up))
# ---- 图例标签(窄画布下缩短为 n=%d 避免贴边) ----
lab <- c(sprintf("Down (n=%d)", n_down),
sprintf("Not (n=%d)", n_not),
sprintf("Up (n=%d)", n_up))
# ---- 选点(Up/Down 单独层,置于 Not 之上) ----
tab[, up_sel := !is.na(padj) & padj < padj_thr & log2FC > lfc_thr]
tab[, dn_sel := !is.na(padj) & padj < padj_thr & log2FC < -lfc_thr]
y_lim <- max(-log10(tab$padj), na.rm = TRUE) * 1.15 # 留顶部呼吸
x_min <- min(tab$log2FC, na.rm = TRUE) - 0.15 # 留左右呼吸
x_max <- max(tab$log2FC, na.rm = TRUE) + 0.15
# ---- 绘图 ----
p <- ggplot(tab[!is.na(padj)], aes(x = log2FC, y = -log10(padj))) +
geom_point(data = tab[cat == "Not"], aes(color = cat),
size = 1.2, alpha = 0.35) +
geom_point(data = tab[dn_sel == TRUE], aes(color = "Down"),
size = 2.0, alpha = 0.75) +
geom_point(data = tab[up_sel == TRUE], aes(color = "Up"),
size = 2.0, alpha = 0.75) +
geom_vline(xintercept = c(-lfc_thr, lfc_thr), linetype = 2,
color = "grey30", linewidth = 0.35) +
geom_hline(yintercept = -log10(padj_thr), linetype = 2,
color = "grey30", linewidth = 0.35) +
scale_color_manual(values = cols, labels = lab, breaks = c("Down", "Not", "Up")) +
scale_x_continuous(limits = c(x_min, x_max)) +
scale_y_continuous(limits = c(0, y_lim)) +
labs(x = expression(Log[2]("Fold Change")),
y = expression(-Log[10]("padj"))) +
theme_bw() +
theme(
legend.position = "top", legend.title = element_blank(),
legend.text = element_text(size = 6.5), legend.key = element_blank(),
legend.key.size = unit(0.2, "cm"), legend.spacing.x = unit(0.15, "cm"),
legend.margin = margin(b = 0, t = 0),
panel.grid = element_blank(),
panel.border = element_rect(color = "black", linewidth = 0.5),
axis.title = element_text(size = 8), axis.text = element_text(size = 6.5),
axis.ticks = element_line(linewidth = 0.4),
plot.margin = margin(t = 1, r = 4, b = 1, l = 3)
) +
guides(color = guide_legend(override.aes = list(size = 1.5, alpha = 1),
keywidth = unit(0.16, "cm"),
keyheight = unit(0.16, "cm")))
dir.create(dirname(out_pdf), showWarnings = FALSE, recursive = TRUE)
ggsave(out_pdf, p, width = out_w_cm, height = out_h_cm, units = "cm", device = "pdf")
cat("saved:", out_pdf, "\n")
cat("VOLCANO_OK\n")
# ============================================================================
# 三组差异比较整合散点图(文献 Fig2I 复现)
# 来源: 生信技能树公众号 https://mp.weixin.qq.com/s/IPKtCGbk7o9OFf5WsNAopg
# 文献: Dynamic remodelling of epithelial plasticity in colorectal cancer
# from single-cell and spatially resolved perspectives (2025, BMC)
# Fig2I: 二维散点图同时展示三个差异分组结果
# x = CRC vs Normal log2FC, y = LM vs Normal log2FC,
# 颜色/大小 = LM vs CRC log2FC
# 数据: 文献补充材料 MOESM2 (CRC vs Normal) + MOESM3 (LM vs CRC)
# 说明: 文献只公开了两组差异结果, 故第三维(y)借用 MOESM3 中的 LM vs CRC
# 映射为颜色/大小; 自己应用时换成真实三组数据即可
# 输出: Fig2I.pdf (矢量图)
# ============================================================================
rm(list = ls())
library(ggplot2)
library(ggrepel)
# ---- 1. 读取数据 -----------------------------------------------------------
# MOESM2: CRC vs Normal 差异结果(含 T 细胞与上皮两套, 取上皮)
diff1 <- read.csv("data/12967_2025_7380_MOESM2_ESM.csv", skip = 2)
table(diff1$cell.type)
diff1 <- diff1[diff1$cell.type == "Epi (CRC vs. Normal)", ]
colnames(diff1) <- paste0("CRCvsNormal.", colnames(diff1))
head(diff1)
# MOESM3: LM vs CRC 差异结果(取上皮)
diff2 <- read.csv("data/12967_2025_7380_MOESM3_ESM.csv", skip = 2)
table(diff2$cell.type)
diff2 <- diff2[diff2$cell.type == "Epi (LM vs. CRC)", ]
colnames(diff2) <- paste0("LMvsCRC.", colnames(diff2))
head(diff2)
# ---- 2. 合并两组差异并过滤 -------------------------------------------------
# 按基因名(inner join)取交集, 双侧 |log2FC| > 0.5
dt <- merge(diff1, diff2, by.x = "CRCvsNormal.X", by.y = "LMvsCRC.X")
dt <- dt[abs(dt$LMvsCRC.avg_log2FC) > 0.5, ]
dt <- dt[abs(dt$CRCvsNormal.avg_log2FC) > 0.5, ]
head(dt)
# 因缺少第三组独立差异结果, 交集基因偏多, 取前 114 个作图贴近原图
# (自己有完整三组数据时删掉这行, 用全部交集基因)
dt <- dt[1:114, ]
range(dt$CRCvsNormal.avg_log2FC)
range(dt$LMvsCRC.avg_log2FC)
# ---- 3. 基础散点图 (x/y 为两组 log2FC, 颜色+大小为第三组 log2FC) ----------
p <- ggplot(data = dt, aes(x = CRCvsNormal.avg_log2FC, y = LMvsCRC.avg_log2FC)) +
geom_point(aes(size = LMvsCRC.avg_log2FC, color = LMvsCRC.avg_log2FC),
shape = 19) +
scale_size_continuous(range = c(1, 4)) + # 点大小映射范围
scale_color_gradientn(
colors = c("#5961a3", "#54aca5", "#eff1b4", "#fdae61", "#cb4958"),
values = scales::rescale(c(-5, -4, -2, 0, 2.5)), # 渐变关键节点位置
limits = c(-5, 2.6))
p
# ---- 4. 添加基因标签 (ggrepel 防重叠 + 引线) -------------------------------
p1 <- p +
geom_text_repel(
data = dt,
aes(x = CRCvsNormal.avg_log2FC, y = LMvsCRC.avg_log2FC, label = CRCvsNormal.X),
size = 2.4,
force_pull = 2, # 标签向点回拉的力
point.padding = 0, # 标签与点的最小距离
box.padding = 0.01, # 标签框之间的距离
min.segment.length = 0.03, # >0 即显示引线
segment.color = "grey20",
segment.size = 0.5, # 引线粗细
segment.alpha = 0.8, # 引线透明度
max.overlaps = Inf, # 不因重叠隐藏任何标签
seed = 123, max.time = 1, max.iter = Inf)
p1
# ---- 5. 添加零线/标题/主题 -------------------------------------------------
p2 <- p1 +
geom_vline(xintercept = 0, linetype = "dashed", color = "gray50", linewidth = 0.8) +
geom_hline(yintercept = 0, linetype = "dashed", color = "gray50", linewidth = 0.8) +
scale_alpha_continuous(range = c(0.3, 1)) +
xlab(label = "CRC vs. Normal log2 Fold Change") +
ylab(label = "LM vs. Normal log2 Fold Change") +
ggtitle("DEG profiles across CRC vs. Normal, LM vs. Normal, \nand LM vs. CRC")
p2
p3 <- p2 +
theme_bw() +
theme(legend.position = "right",
legend.text = element_text(size = 12),
panel.border = element_rect(color = "black", fill = NA, linewidth = 1.2),
panel.grid.major = element_blank(), # 去掉主网格线
panel.grid.minor = element_blank(), # 去掉次网格线
plot.title = element_text(size = 16, face = "bold", hjust = 0.5),
axis.title.x = element_text(size = 16),
axis.title.y = element_text(size = 16),
axis.text.x = element_text(size = 12),
axis.text.y = element_text(size = 12))
p3
# ---- 6. 输出矢量图 (PDF) ---------------------------------------------------
ggsave(filename = "Fig2I.pdf", width = 8, height = 6, plot = p3)
cat("Done: Fig2I.pdf\n")
这个专题已经做成确定性工具,调用后直接返回图片, 不需要 AI 现写代码。