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

378 lines
12 KiB
R
Executable File
Raw Permalink 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.
#!/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 = "")