#======================================
# 原创代码无删减,编写不易,论文使用本代码绘图,请引用:www.tcmbiohub.com
# 祝大家投稿顺利!
## =======================================================================
### GO环状圈图 + 外圈“渐变长方形柱” + 叶子标签随柱长动态靠近
### 版本:方式A(手动缩小直径)——关闭自动 xy_lim_auto,并把 xy_lim 调大
# =======================================================================
.libPaths(c(Sys.getenv("R_LIBS_USER"), .libPaths()))
rm(list = ls())
# =======================================================================
suppressPackageStartupMessages({
library(tidyverse)
library(clusterProfiler)
library(org.Hs.eg.db)
library(igraph)
library(tidygraph)
library(ggraph)
library(cowplot)
library(scales)
})
# =========================
# 你只需要改这里
# =========================
gene_txt <- "PPDEGs.txt"
prefix <- "PP"
topN_each_onto <- 15
fdr_cutoff <- 1
leaf_label_mode <- "ID" # "ID" / "Description" / "Both"
rank_by <- "p.adjust" # "p.adjust" or "pvalue"
node_size_by <- "Count" # "Count" or "GeneRatio"
# 外圈柱长度代表什么:Count / GeneRatio / negLog10FDR
bar_by <- "Count"
out_pdf <- paste0(prefix, "_GO_graph_barRectGradient_labelFollow_smallDiameter.pdf")
out_all_csv <- paste0(prefix, "_GO_all.csv")
out_top_csv <- paste0(prefix, "_GO_TopN_forGraph.csv")
# =========================
# 外圈柱“渐变与丰富度”参数(可调)
# =========================
bar_slices <- 14 # 渐变分段数:10–20
bar_alpha <- 0.80 # 柱子透明度
bar_len_to <- c(0.03, 0.22) # 柱长映射区间
gap <- 0.05 # 柱子起点离叶子点距离
bar_width <- 0.035 # 柱子厚度(0.02–0.06)
# 渐变明暗控制(同一类内不同term深浅有区别)
start_light_to_white <- 0.86
end_min_strength <- 0.40
end_max_strength <- 1.00
# =========================
# 叶子标签(GO条码)随柱长靠近(关键参数)
# =========================
label_follow_bar <- TRUE
label_follow_k <- 0.75
label_follow_cap <- 0.20
label_offset <- 0.05
# =========================
# 方式A:手动缩小圆圈直径(关闭自动范围 + 调大xy_lim)
# =========================
xy_lim_auto <- FALSE
xy_lim <- 1.78 # ★ 这里控制“直径视觉大小”:越大 -> 图越小(白边更多);建议 1.65–1.95 试一下
# =========================
# 颜色
# =========================
pal <- c(BP = "#EE8C23", CC = "#7BB662", MF = "#B07CC6", GO = "#2BB7B3")
# =========================
# 工具函数:颜色混合(向量版)
# =========================
blend_hex <- function(col_from, col_to, t) {
t <- pmin(pmax(t, 0), 1)
rf <- grDevices::col2rgb(col_from) / 255
rt <- grDevices::col2rgb(col_to) / 255
r <- rf[1, ] * (1 - t) + rt[1, ] * t
g <- rf[2, ] * (1 - t) + rt[2, ] * t
b <- rf[3, ] * (1 - t) + rt[3, ] * t
grDevices::rgb(r, g, b)
}
# =========================
# 1) SYMBOL -> ENTREZID(含ALIAS补映射)
# =========================
genes_symbol0 <- scan(gene_txt, what = "character", sep = "\n", quiet = TRUE)
genes_symbol0 <- trimws(genes_symbol0)
genes_symbol0 <- unique(genes_symbol0[genes_symbol0 != "" & !is.na(genes_symbol0)])
genes_symbol <- genes_symbol0[nchar(genes_symbol0) > 1]
genes_symbol <- genes_symbol[!tolower(genes_symbol) %in% c("na", "null", "none")]
id_sym <- suppressWarnings(
bitr(genes_symbol, fromType = "SYMBOL", toType = "ENTREZID", OrgDb = org.Hs.eg.db)
) %>% distinct(ENTREZID, .keep_all = TRUE)
mapped_sym <- unique(id_sym$SYMBOL)
unmapped <- setdiff(genes_symbol, mapped_sym)
id_alias <- tibble()
if (length(unmapped) > 0) {
id_alias <- suppressWarnings(
bitr(unmapped, fromType = "ALIAS", toType = "ENTREZID", OrgDb = org.Hs.eg.db)
) %>% distinct(ENTREZID, .keep_all = TRUE)
}
de <- unique(c(id_sym$ENTREZID, id_alias$ENTREZID))
cat("Input SYMBOL:", length(genes_symbol), "\n")
cat("Mapped ENTREZID:", length(de), "\n")
if (length(de) < 10) stop("映射到的ENTREZID太少,无法稳定做GO富集。")
final_mapped_symbols <- unique(c(id_sym$SYMBOL, id_alias$ALIAS))
final_unmapped <- setdiff(genes_symbol, final_mapped_symbols)
writeLines(final_unmapped, paste0(prefix, "_unmapped_symbols.txt"))
# =========================
# 2) enrichGO(ALL)并导出全量
# =========================
ego <- enrichGO(
gene = de,
OrgDb = org.Hs.eg.db,
keyType = "ENTREZID",
ont = "ALL",
pvalueCutoff = 1,
qvalueCutoff = 1,
pAdjustMethod = "BH",
readable = FALSE
)
go_df <- tryCatch({
setReadable(ego, OrgDb = org.Hs.eg.db, keyType = "ENTREZID") %>% as.data.frame()
}, error = function(e) {
as.data.frame(ego)
})
if (nrow(go_df) == 0) stop("enrichGO 没有任何结果,请检查输入基因或映射。")
write.csv(go_df, out_all_csv, row.names = FALSE)
cat("Saved GO all -> ", out_all_csv, "\n", sep = "")
# =========================
# 3) TopN筛选并导出
# =========================
need_cols <- c("ONTOLOGY", "ID", "Description", "Count", "GeneRatio")
miss <- setdiff(need_cols, colnames(go_df))
if (length(miss) > 0) stop("GO结果缺少列:", paste(miss, collapse = ", "))
parse_ratio <- function(x) {
out <- rep(NA_real_, length(x))
for (i in seq_along(x)) {
s <- as.character(x[i])
if (is.na(s) || !grepl("/", s, fixed = TRUE)) next
sp <- strsplit(s, "/", fixed = TRUE)[[1]]
if (length(sp) != 2) next
num <- suppressWarnings(as.numeric(sp[1]))
den <- suppressWarnings(as.numeric(sp[2]))
if (is.na(num) || is.na(den) || den == 0) next
out[i] <- num / den
}
out
}
go_df2 <- go_df %>%
mutate(
Count = as.numeric(Count),
ratio = parse_ratio(GeneRatio),
p.adjust = if ("p.adjust" %in% colnames(.)) as.numeric(p.adjust) else NA_real_,
pvalue = if ("pvalue" %in% colnames(.)) as.numeric(pvalue) else NA_real_
)
if (!(rank_by %in% colnames(go_df2))) {
message("未检测到你指定的排序列:", rank_by, ",自动改用 p.adjust 或 pvalue。")
rank_by <- intersect(c("p.adjust", "pvalue"), colnames(go_df2))[1]
if (is.na(rank_by)) stop("GO结果里既没有 p.adjust 也没有 pvalue,无法按显著性排序。")
}
go_df2 <- go_df2 %>%
mutate(p_use = .data[[rank_by]]) %>%
mutate(p_use = ifelse(is.na(p_use), 1, p_use)) %>%
mutate(p_use = pmax(p_use, 1e-300))
if (rank_by == "p.adjust") {
go_df2_f <- go_df2 %>% filter(p_use < fdr_cutoff)
if (nrow(go_df2_f) == 0) {
message("FDR筛选后为0条,自动取消硬筛,只按TopN取。")
go_df2_f <- go_df2
}
} else {
go_df2_f <- go_df2
}
go_top <- go_df2_f %>%
filter(ONTOLOGY %in% c("BP", "CC", "MF")) %>%
group_by(ONTOLOGY) %>%
arrange(p_use, desc(Count), .by_group = TRUE) %>%
slice_head(n = topN_each_onto) %>%
ungroup() %>%
distinct(ONTOLOGY, ID, .keep_all = TRUE)
if (nrow(go_top) == 0) stop("筛选后没有任何 term 可画:请调大 topN 或放宽 fdr_cutoff。")
write.csv(go_top, out_top_csv, row.names = FALSE)
cat("Saved GO TopN -> ", out_top_csv, "\n", sep = "")
# =========================
# 4) 构建网络:GO -> (BP/CC/MF) -> term
# =========================
go_top <- go_df2_f %>%
filter(ONTOLOGY %in% c("BP", "CC", "MF")) %>%
group_by(ONTOLOGY) %>%
arrange(p_use, desc(Count), .by_group = TRUE) %>%
slice_head(n = topN_each_onto) %>%
ungroup() %>%
distinct(ONTOLOGY, ID, .keep_all = TRUE) %>%
mutate(
Description2 = str_remove(Description, ",.*"),
label_term = case_when(
leaf_label_mode == "ID" ~ ID,
leaf_label_mode == "Description" ~ str_wrap(Description2, width = 26),
leaf_label_mode == "Both" ~ str_wrap(paste0(ID, ": ", Description2), width = 30),
TRUE ~ ID
)
)
if (node_size_by == "GeneRatio") {
go_top <- go_top %>% mutate(node_size_term = ifelse(is.na(ratio), Count, ratio * 100))
} else {
go_top <- go_top %>% mutate(node_size_term = Count)
}
go_top <- go_top %>%
mutate(
bar_value = case_when(
bar_by == "Count" ~ as.numeric(Count),
bar_by == "GeneRatio" ~ as.numeric(ratio),
bar_by == "negLog10FDR" ~ -log10(pmax(p_use, 1e-300)),
TRUE ~ as.numeric(Count)
)
)
node_root <- tibble(
name = "GO",
node_size = sum(go_top$node_size_term, na.rm = TRUE),
node_level = "Type",
type = "GO",
label = "GO",
bar_value = NA_real_
)
node_onto <- go_top %>%
group_by(ONTOLOGY) %>%
summarise(node_size = sum(node_size_term, na.rm = TRUE), .groups = "drop") %>%
transmute(
name = ONTOLOGY,
node_size = node_size,
node_level = "ONTOLOGY",
type = ONTOLOGY,
label = ONTOLOGY,
bar_value = NA_real_
)
node_term <- go_top %>%
transmute(
name = ID,
node_size = node_size_term,
node_level = "ID",
type = ONTOLOGY,
label = label_term,
bar_value = as.numeric(bar_value)
)
node_file <- bind_rows(node_root, node_onto, node_term) %>%
distinct(name, .keep_all = TRUE)
edge_1 <- node_onto %>% transmute(from = "GO", to = name)
edge_2 <- go_top %>% transmute(from = ONTOLOGY, to = ID) %>% distinct()
edge_file <- bind_rows(edge_1, edge_2)
graph_go <- tbl_graph(nodes = node_file, edges = edge_file, directed = TRUE)
# =========================
# 5) 作图
# =========================
lay <- ggraph::create_layout(graph_go, layout = "dendrogram", circular = TRUE)
bar_df <- lay %>%
as_tibble() %>%
filter(node_level == "ID") %>%
mutate(
r = sqrt(x^2 + y^2),
r = ifelse(r == 0, 1e-6, r),
ux = x / r,
uy = y / r,
v = as.numeric(bar_value),
base_col = unname(pal[type])
)
if (all(is.na(bar_df$v))) stop("bar_value 全部为 NA:请检查 bar_by 或输入数据。")
bar_df$v[is.na(bar_df$v)] <- min(bar_df$v, na.rm = TRUE)
v_rng <- range(bar_df$v, na.rm = TRUE)
if (!all(is.finite(v_rng)) || v_rng[1] == v_rng[2]) {
bar_df <- bar_df %>% mutate(bar_len = mean(bar_len_to))
} else {
bar_df <- bar_df %>% mutate(bar_len = scales::rescale(v, to = bar_len_to, from = v_rng))
}
bar_df <- bar_df %>%
group_by(type) %>%
mutate(
v01 = {
rr <- range(v, na.rm = TRUE)
if (!all(is.finite(rr)) || rr[1] == rr[2]) rep(0.6, n())
else scales::rescale(v, to = c(0, 1), from = rr)
}
) %>%
ungroup()
w <- bar_width / 2
bar_df <- bar_df %>%
mutate(
px = -uy,
py = ux,
rin = r + gap
)
slice_df <- bar_df %>%
tidyr::expand_grid(slice = seq_len(bar_slices)) %>%
mutate(
t0 = (slice - 1) / bar_slices,
t1 = slice / bar_slices,
tmid = (t0 + t1) / 2,
rin_s = rin + bar_len * t0,
rout_s = rin + bar_len * t1,
x_in = rin_s * ux,
y_in = rin_s * uy,
x_out = rout_s * ux,
y_out = rout_s * uy
) %>%
mutate(
start_col = blend_hex(base_col, rep("#FFFFFF", n()), rep(start_light_to_white, n())),
end_strength = end_min_strength + (end_max_strength - end_min_strength) * v01,
end_col = blend_hex(rep("#FFFFFF", n()), base_col, end_strength),
slice_fill = blend_hex(start_col, end_col, tmid)
)
bar_poly <- bind_rows(
slice_df %>% transmute(id = name, slice = slice, fill = slice_fill, ord = 1,
x = x_in + w*px, y = y_in + w*py),
slice_df %>% transmute(id = name, slice = slice, fill = slice_fill, ord = 2,
x = x_out + w*px, y = y_out + w*py),
slice_df %>% transmute(id = name, slice = slice, fill = slice_fill, ord = 3,
x = x_out - w*px, y = y_out - w*py),
slice_df %>% transmute(id = name, slice = slice, fill = slice_fill, ord = 4,
x = x_in - w*px, y = y_in - w*py)
) %>% arrange(id, slice, ord)
label_df <- bar_df %>%
mutate(
follow = if (label_follow_bar) pmin(bar_len * label_follow_k, label_follow_cap) else 0,
r_lab = rin + follow + label_offset,
x_lab = r_lab * ux,
y_lab = r_lab * uy
) %>%
select(name, x_lab, y_lab, type) %>%
left_join(lay %>% as_tibble() %>% select(name, label), by = "name")
p <- ggraph(lay) +
geom_polygon(
data = bar_poly,
aes(x = x, y = y, group = interaction(id, slice), fill = fill),
alpha = bar_alpha,
color = NA,
show.legend = FALSE
) +
scale_fill_identity() +
geom_edge_diagonal(aes(color = node2.type), alpha = 0.35, linewidth = 0.6) +
geom_node_point(aes(size = node_size, color = type), alpha = 0.35) +
scale_size(range = c(2.5, 12)) +
scale_color_manual(values = pal, drop = FALSE) +
geom_text(
data = label_df,
aes(
x = x_lab,
y = y_lab,
label = label,
angle = -((-node_angle(x_lab, y_lab) + 90) %% 180) + 90,
color = type
),
size = 3.3,
hjust = "outward"
) +
geom_node_text(
aes(label = label, filter = !leaf, color = type),
fontface = "bold", size = 4, family = "sans"
) +
cowplot::theme_nothing() +
coord_fixed() +
coord_cartesian(xlim = c(-xy_lim, xy_lim), ylim = c(-xy_lim, xy_lim))
p
ggsave(out_pdf, plot = p, width = 7.8, height = 7.8, device = cairo_pdf)
message("Done. Saved plot -> ", out_pdf)
#======================================
# 原创代码无删减,编写不易,论文使用本代码绘图,请引用:www.tcmbiohub.com
# 祝大家投稿顺利!