Files
PlantAI-rag-new/bioflow_kernel/scripts/tree_domain.R
T

378 lines
12 KiB
R
Raw Normal View History

#!/usr/bin/env Rscript
## =========================================================
## tree_domain.R
## Panel A:Phylogenetic tree(含基因 ID + Bootstrap)
## Panel B:Subfamily 竖线(可选,帮助视觉分组)
## Panel C:Domain(按蛋白长度 qlen 比例;来自 hmmscan domtblout)
## =========================================================
args <- commandArgs(trailingOnly = TRUE)
if (length(args) < 3) {
stop(
"用法:\n",
" Rscript tree_domain.R \\\n",
" <treefile> <subfamily_file> <domtblout_file> \\\n",
" [evalue_cutoff] [coverage_percent] [output_prefix]\n\n",
call. = FALSE
)
}
treefile <- args[1]
subfamily_file <- args[2]
domtblout_file <- args[3]
## E-value 阈值(默认 1e-5)
e_cutoff <- if (length(args) >= 4 && nzchar(args[4])) {
tmp <- suppressWarnings(as.numeric(args[4]))
if (is.na(tmp)) 1e-5 else tmp
} else 1e-5
## 覆盖度阈值(可选)
cov_cutoff <- if (length(args) >= 5 && nzchar(args[5])) {
tmp <- suppressWarnings(as.numeric(args[5]))
if (is.na(tmp) || tmp <= 0) NA_real_ else tmp / 100
} else NA_real_
## 输出前缀(用于生成 pdf 和 png)
output_prefix <- if (length(args) >= 6 && nzchar(args[6])) {
args[6]
} else {
prefix <- sub("\\.[^.]*$", "", treefile)
paste0(prefix, ".tree_domain")
}
## 输出文件路径
output_pdf <- paste0(output_prefix, ".pdf")
output_png <- paste0(output_prefix, ".png")
cat("树文件: ", treefile, "\n")
cat("亚家族文件: ", subfamily_file, "\n")
cat("domtblout 文件: ", domtblout_file, "\n")
cat("E-value 阈值: ", e_cutoff, "\n")
cat("覆盖度过滤: ", if(is.na(cov_cutoff)) "未启用" else paste0(cov_cutoff*100, "%"), "\n")
cat("输出前缀: ", output_prefix, "\n")
cat("PDF 输出: ", output_pdf, "\n")
cat("PNG 输出: ", output_png, "\n\n")
## ===================== 加载 R 包 =====================
script_args_all <- commandArgs(trailingOnly = FALSE)
script_file_arg <- grep("^--file=", script_args_all, value = TRUE)
script_dir <- if (length(script_file_arg) > 0) dirname(normalizePath(sub("^--file=", "", script_file_arg[1]))) else getwd()
source(file.path(script_dir, "subfamily_palette.R"))
suppressPackageStartupMessages({
library(ape)
library(ggtree)
library(tidytree)
library(dplyr)
library(RColorBrewer)
library(ggplot2)
library(ggnewscale)
})
make_distinct_cols <- function(n) {
if (n <= 0) return(character(0))
base <- c(
"#0072B2", "#D55E00", "#009E73", "#CC79A7", "#E69F00",
"#56B4E9", "#F0E442", "#000000", "#882255", "#44AA99",
"#AA4499", "#117733", "#332288", "#DDCC77", "#88CCEE"
)
if (n <= length(base)) return(base[seq_len(n)])
if (exists("hcl.colors", where = asNamespace("grDevices"), inherits = FALSE)) {
return(grDevices::hcl.colors(n, palette = "Dark 3"))
}
grDevices::colorRampPalette(base)(n)
}
## ===================== 读亚家族文件 =====================
add_confidence_labels <- function(subfam_df, subfamily_file) {
add_subfamily_confidence_labels(subfam_df, subfamily_file)
}
## 处理 "NONE" 情况(表示没有亚家族文件)
if (subfamily_file == "NONE" || !file.exists(subfamily_file)) {
cat("[INFO] 未找到亚家族文件,使用默认着色\n")
## 从树文件读取基因名作为默认亚家族
tree_tmp <- read.tree(treefile)
subfam <- data.frame(
label = tree_tmp$tip.label,
subfamily = "Default",
low_confidence = FALSE,
display_label = tree_tmp$tip.label,
stringsAsFactors = FALSE
)
} else {
subfam <- read.table(subfamily_file, header = FALSE, stringsAsFactors = FALSE)
colnames(subfam) <- c("label", "subfamily")
subfam <- add_confidence_labels(subfam, subfamily_file)
}
subfam_levels <- sort(unique(subfam$subfamily))
n_subfam <- length(subfam_levels)
subfam_cols <- make_subfamily_cols(subfam_levels)
## ===================== 树绘图函数 =====================
plot_tree <- function(treefile, subfam_df, subfam_cols, label_offset = 0.4) {
tree <- read.tree(treefile)
tree_df <- as_tibble(tree)
tree_df$isTip <- tree_df$node <= length(tree$tip.label)
tree_df <- dplyr::left_join(tree_df, subfam_df, by = "label")
tree_df$display_label <- ifelse(is.na(tree_df$display_label), tree_df$label, tree_df$display_label)
tree_df$support <- ifelse(tree_df$isTip, NA_real_,
suppressWarnings(as.numeric(tree_df$label)))
## 修复:使用 plotmath 兼容的表达式
p <- ggtree(tree, branch.length = "none") %<+% tree_df +
geom_nodepoint(
aes(fill = cut(support, c(0, 50, 75, 100))),
shape = 21, size = 2, na.rm = TRUE
) +
scale_fill_manual(
values = c("black", "grey40", "grey80"),
name = "Bootstrap",
breaks = c("(75,100]", "(50,75]", "(0,50]"),
labels = c("BP > 75", "50 < BP <= 75", "BP <= 50") ## 修复:使用字符串而非 expression
) +
geom_text2(
aes(label = ifelse(!isTip & !is.na(support), round(support), "")),
hjust = -0.2, vjust = -0.4, size = 2.2,
na.rm = TRUE, show.legend = FALSE
) +
geom_tippoint(aes(color = subfamily), size = 2, na.rm = TRUE) +
scale_color_manual(values = subfam_cols, name = "Subfamily") +
geom_tiplab(aes(label = display_label), offset = label_offset, size = 3, hjust = 0) +
theme_tree() +
theme(legend.background = element_rect(color = "#969696", fill = "#d9d9d9"))
attr(p, "label_offset") <- label_offset
return(p)
}
## ===================== 解析 domtblout =====================
parse_domtblout <- function(domtbl_file, e_cutoff = 1e-5,
cov_cutoff = NA_real_, tree_genes = NULL) {
dom <- read.table(domtbl_file, header = FALSE, comment.char = "#",
stringsAsFactors = FALSE, fill = TRUE)
if (ncol(dom) < 22) {
stop("domtblout 文件列数少于 22 列,请检查 hmmscan 输出格式。", call. = FALSE)
}
colnames(dom)[1:22] <- c(
"target_name", "t_accession", "tlen", "query_name", "q_accession", "qlen",
"E_full", "score_full", "bias_full", "dom_index", "dom_of",
"c_Evalue", "i_Evalue", "dom_score", "dom_bias",
"hmm_from", "hmm_to", "ali_from", "ali_to", "env_from", "env_to", "acc"
)
num_cols <- c("tlen", "qlen", "E_full", "score_full", "i_Evalue",
"ali_from", "ali_to", "dom_score")
for (cn in intersect(num_cols, colnames(dom))) {
dom[[cn]] <- suppressWarnings(as.numeric(dom[[cn]]))
}
if (!is.null(tree_genes)) {
dom <- dom[dom$query_name %in% tree_genes, , drop = FALSE]
}
dom <- dom %>%
mutate(
dom_len = ali_to - ali_from + 1,
cov = ifelse(is.na(qlen) | qlen <= 0, NA_real_, dom_len / qlen),
evalue = if("i_Evalue" %in% colnames(dom)) i_Evalue else E_full
) %>%
filter(!is.na(evalue), evalue <= e_cutoff)
if (!is.na(cov_cutoff)) {
dom <- dom %>% filter(is.na(cov) | cov >= cov_cutoff)
}
return(dom)
}
## ===================== 画树 & 获取坐标 =====================
label_offset <- 0.4
p_tree <- plot_tree(treefile, subfam, subfam_cols, label_offset = label_offset)
tree_dat <- p_tree$data
tip_dat <- tree_dat %>%
filter(isTip) %>%
select(label, x, y, subfamily)
if (nrow(tip_dat) == 0) stop("树中没有 tip 节点。", call. = FALSE)
x_tree_min <- min(tree_dat$x, na.rm = TRUE)
x_tip_max <- max(tip_dat$x, na.rm = TRUE)
max_y <- max(tree_dat$y, na.rm = TRUE)
## 计算 Panel A 宽度
tree_core_width <- x_tip_max - x_tree_min
label_panel_width <- max(1.5, tree_core_width * 0.6)
x_A_left <- x_tree_min
x_A_right <- x_tip_max + label_offset + label_panel_width
## ===================== 解析 Domain 数据 =====================
domain_hits <- parse_domtblout(domtblout_file, e_cutoff = e_cutoff,
cov_cutoff = cov_cutoff, tree_genes = tip_dat$label)
domain_hits <- domain_hits %>%
left_join(tip_dat, by = c("query_name" = "label")) %>%
filter(!is.na(y))
if (nrow(domain_hits) == 0) {
stop("筛选后没有任何 domain 命中。", call. = FALSE)
}
cat("蛋白长度范围: ", min(domain_hits$qlen), " ~ ", max(domain_hits$qlen), "\n")
## ===================== 布局计算 =====================
gap_AB <- max(0.3, tree_core_width * 0.15)
gap_BC <- max(0.3, tree_core_width * 0.15)
r_B <- 0.6 # Subfamily 宽度系数
r_C <- 2.5 # Domain 宽度系数(比原来更宽,突出显示)
## Panel B (Subfamily)
x_B_left <- x_A_right + gap_AB
width_B <- tree_core_width * r_B
x_B_right <- x_B_left + width_B
x_subfam <- (x_B_left + x_B_right) / 2
## Panel C (Domain)
x_C_left <- x_B_right + gap_BC
width_C <- tree_core_width * r_C
x_C_right <- x_C_left + width_C
## ===================== Domain 坐标计算 =====================
max_qlen <- max(domain_hits$qlen, na.rm = TRUE)
scale_domain <- width_C / max_qlen
cat("Domain scale: ", scale_domain, "\n")
## 蛋白主干线
domain_tracks <- domain_hits %>%
distinct(query_name, y, qlen) %>%
mutate(
x_start = x_C_left,
x_end = x_C_left + qlen * scale_domain
)
## Domain 矩形
domain_rect <- domain_hits %>%
mutate(
xmin = x_C_left + (ali_from - 1) * scale_domain,
xmax = x_C_left + ali_to * scale_domain,
ymin = y - 0.25,
ymax = y + 0.25
)
## Domain 配色
domain_levels <- sort(unique(domain_rect$target_name))
domain_cols <- setNames(make_distinct_cols(length(domain_levels)), domain_levels)
## ===================== 亚家族范围 =====================
subfam_ranges <- make_subfamily_ranges(tip_dat)
## ===================== 底部坐标轴 =====================
make_axis <- function(max_len, scale_val, x_left, axis_y, n_breaks = 5) {
if (!is.finite(max_len) || max_len <= 0) return(NULL)
brks <- pretty(c(0, max_len), n = n_breaks)
brks <- brks[brks >= 0 & brks <= max_len]
list(
line = data.frame(x = x_left, xend = x_left + max_len * scale_val,
y = axis_y, yend = axis_y),
ticks = data.frame(x = x_left + brks * scale_val,
xend = x_left + brks * scale_val,
y = axis_y, yend = axis_y + 0.2),
labels = data.frame(x = x_left + brks * scale_val,
y = axis_y - 0.3, label = as.integer(brks))
)
}
axis_y <- min(tree_dat$y) - 1.5
domain_axis <- make_axis(max_qlen, scale_domain, x_C_left, axis_y)
## ===================== 组合绘图 =====================
header_y <- max_y + 1.2
p_combo <- p_tree +
## Panel B: Subfamily 竖线
geom_segment(
data = subfam_ranges, inherit.aes = FALSE,
aes(x = x_subfam, xend = x_subfam, y = ymin, yend = ymax, color = subfamily),
linewidth = 1.5
) +
geom_text(
data = subfam_ranges, inherit.aes = FALSE,
aes(x = x_subfam + width_B * 0.08, y = ymid, label = subfamily, color = subfamily),
hjust = 0, size = 3.5
) +
## Panel C: Domain
geom_segment(
data = domain_tracks, inherit.aes = FALSE,
aes(x = x_start, xend = x_end, y = y, yend = y),
colour = "grey70", linewidth = 0.4
) +
ggnewscale::new_scale_fill() +
geom_rect(
data = domain_rect, inherit.aes = FALSE,
aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = target_name),
color = NA
) +
scale_fill_manual(values = domain_cols, name = "Domain") +
## 底部坐标轴
geom_segment(data = domain_axis$line, inherit.aes = FALSE,
aes(x = x, xend = xend, y = y, yend = yend), linewidth = 0.4) +
geom_segment(data = domain_axis$ticks, inherit.aes = FALSE,
aes(x = x, xend = xend, y = y, yend = yend), linewidth = 0.3) +
geom_text(data = domain_axis$labels, inherit.aes = FALSE,
aes(x = x, y = y, label = label), size = 2.5) +
## 顶部标题
annotate("text", x = (x_A_left + x_A_right)/2, y = header_y,
label = "Phylogenetic tree", fontface = "bold", size = 4) +
annotate("text", x = x_subfam, y = header_y,
label = "Subfamily", fontface = "bold", size = 4) +
annotate("text", x = (x_C_left + x_C_right)/2, y = header_y,
label = "Protein Domain", fontface = "bold", size = 4) +
coord_cartesian(xlim = c(x_A_left - 0.5, x_C_right + 1)) +
expand_limits(y = header_y + 0.5) +
theme(legend.position = "right",
legend.background = element_rect(color = "#969696", fill = "#d9d9d9"))
## ===================== 保存输出(PDF + PNG)=====================
# 保存 PDF(矢量图)
ggsave(output_pdf, p_combo, width = 14, height = 10)
cat("[OK] PDF: ", output_pdf, "\n", sep = "")
# 保存 PNG(使用png()函数避免conda R的API冲突)
png(filename = output_png, width = 4200, height = 3000, res = 300)
print(p_combo)
dev.off()
cat("[OK] PNG: ", output_png, "\n", sep = "")
cat("[OK] Subfamily used: ", subfamily_file, "\n", sep = "")