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

960 lines
32 KiB
R
Executable File
Raw 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
## =========================================================
## combo_tree_motif_structure_domain.R
## Panel A:Phylogenetic tree(含基因 ID)
## Panel B:Subfamily 竖线
## Panel C:Motif(按 XML 实际长度比例)
## Panel D:Gene structure(按 GFF3 实际长度比例,只画 CDS + UTR)
## Panel E:Domain(按蛋白长度 qlen 比例;来自 hmmscan domtblout)
##
## 底部:为 Panel C/D/E 各画一条“坐标尺”(motif 位置 / gene 坐标 / aa 位置)
##
## 用法:
## Rscript combo_tree_motif_structure_domain.R \
## <treefile> <subfamily_file> <meme_xml> <gff3_file> <domtblout_file> \
## [evalue_cutoff] [coverage_percent] [output_pdf]
##
## 说明:
## evalue_cutoff 默认 1e-5(按 i-Evalue 或 full E 过滤)
## coverage_percent 为可选,只有你提供时才启用 coverage 过滤
## (覆盖度 = (ali_to - ali_from + 1) / qlen)
## =========================================================
args <- commandArgs(trailingOnly = TRUE)
if (length(args) < 5) {
stop(
"用法:\n",
" Rscript combo_tree_motif_structure_domain.R \\\n",
" <treefile> <subfamily_file> <meme_xml> <gff3_file> <domtblout_file> \\\n",
" [evalue_cutoff] [coverage_percent] [output_pdf]\n\n",
call. = FALSE
)
}
treefile <- args[1]
subfamily_file <- args[2]
meme_xml <- args[3]
gff3_file <- args[4]
domtblout_file <- args[5]
## E-value 阈值(默认 1e-5)
if (length(args) >= 6 && nzchar(args[6])) {
e_cutoff <- suppressWarnings(as.numeric(args[6]))
if (is.na(e_cutoff)) e_cutoff <- 1e-5
} else {
e_cutoff <- 1e-5
}
## 覆盖度阈值(可选):命令行用百分数,如 50 = 50%;不提供则不启用 coverage 过滤
if (length(args) >= 7 && nzchar(args[7])) {
tmp_cov <- suppressWarnings(as.numeric(args[7]))
if (is.na(tmp_cov) || tmp_cov <= 0) {
cov_cutoff <- NA_real_ # 非法或 <=0 表示不启用 coverage 过滤
} else {
cov_cutoff <- tmp_cov / 100
}
} else {
cov_cutoff <- NA_real_ # 默认不启用 coverage 过滤
}
## 输出 pdf 名称(可选)
if (length(args) >= 8 && nzchar(args[8])) {
output_pdf <- args[8]
} else {
prefix <- sub("\\.[^.]*$", "", treefile)
output_pdf <- paste0(prefix, ".tree_motif_structure_domain.pdf")
}
parse_panels <- function(panel_arg) {
allowed <- c("motif", "structure", "domain")
if (!nzchar(panel_arg)) return(allowed)
parts <- trimws(unlist(strsplit(panel_arg, "[,;]")))
parts <- parts[nzchar(parts)]
parts <- tolower(gsub(" ", "_", parts, fixed = TRUE))
parts[parts %in% c("motifs")] <- "motif"
parts[parts %in% c("gene_structure", "gene-structure", "genestructure")] <- "structure"
parts[parts %in% c("domains")] <- "domain"
if (any(parts == "all")) return(allowed)
bad <- setdiff(parts, allowed)
if (length(bad) > 0) {
stop("不支持的 combo panel: ", paste(bad, collapse = ", "), "; 可选: ", paste(allowed, collapse = ", "), call. = FALSE)
}
selected <- unique(parts)
if (length(selected) < 2) {
stop("combo 至少需要选择两个数据类型;单类型图请使用 motif/structure/domain 阶段已有结果。", call. = FALSE)
}
selected
}
selected_panels <- parse_panels(if (length(args) >= 9 && nzchar(args[9])) args[9] else "motif,structure,domain")
has_panel <- function(panel_name) panel_name %in% selected_panels
target_marker_species <- if (length(args) >= 10 && nzchar(args[10])) args[10] else ""
reference_marker_species <- if (length(args) >= 11 && nzchar(args[11])) args[11] else ""
classify_tip_role <- function(label) {
label <- as.character(label)
if (!nzchar(target_marker_species) && !nzchar(reference_marker_species)) return("gene")
if (nzchar(reference_marker_species)) {
# Support both normalized labels such as At|At1g01010 and native
# reference labels such as At1g01010 used in IQ-TREE outputs.
if (grepl(paste0("^", reference_marker_species, "\\|"), label) || grepl(paste0("^", reference_marker_species, "[0-9]"), label)) {
return("reference")
}
}
"target"
}
cat("树文件: ", treefile, "\n")
cat("亚家族文件: ", subfamily_file, "\n")
cat("MEME XML 文件: ", meme_xml, "\n")
cat("GFF3 文件: ", gff3_file, "\n")
cat("domtblout 文件: ", domtblout_file, "\n")
cat("E-value 阈值: ", e_cutoff, "\n")
if (is.na(cov_cutoff)) {
cat("覆盖度过滤: 未启用(仅按 E-value 过滤)\n")
} else {
cat("覆盖度阈值: ", cov_cutoff * 100, "%\n", sep = "")
}
cat("输出文件: ", output_pdf, "\n")
cat("展示 panel: ", paste(selected_panels, collapse = ", "), "\n\n")
## ===================== 2. 加载 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(xml2)
library(ggnewscale)
})
normalize_gene_id <- function(x) {
x <- as.character(x)
x <- sub("^[^|]+\\|", "", x)
repeat {
y <- sub("^(gene|transcript|CDS|mRNA)[:_]", "", x, ignore.case = TRUE)
if (identical(y, x)) break
x <- y
}
x <- sub("_[PT][0-9]+$", "", x, ignore.case = TRUE)
x <- sub("\\.[0-9]+$", "", x)
tolower(x)
}
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)
}
## ===================== 3. 读亚家族文件 + 深色配色 =====================
add_confidence_labels <- function(subfam_df, subfamily_file) {
add_subfamily_confidence_labels(subfam_df, subfamily_file)
}
# 检查亚家族文件是否为 "NONE" 或不存在
has_subfamily <- subfamily_file != "NONE" && file.exists(subfamily_file)
if (has_subfamily) {
subfam <- read_subfamily_plot_table(subfamily_file)
subfam <- add_confidence_labels(subfam, subfamily_file)
subfam_levels <- sort(unique(subfam$subfamily))
n_subfam <- length(subfam_levels)
if (n_subfam == 0) {
warning("在亚家族文件中没有检测到任何 subfamily,将不显示亚家族")
has_subfamily <- FALSE
}
} else {
cat("[INFO] 未提供亚家族文件,将不显示亚家族\n")
# 创建空的 subfam 数据框
subfam <- data.frame(label = character(), subfamily = character(), low_confidence = logical(), display_label = character(), stringsAsFactors = FALSE)
subfam_levels <- character()
n_subfam <- 0
}
## 跨图一致的亚家族配色
if (n_subfam > 0) {
subfam_cols <- make_subfamily_cols(subfam_levels)
} else {
subfam_cols <- character()
}
## ===================== 4. 树绘图函数 =====================
plot_iqtree_by_subfamily <- 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$tip_role <- ifelse(tree_df$isTip, vapply(tree_df$label, classify_tip_role, character(1)), NA_character_)
tree_df$support <- ifelse(
tree_df$isTip,
NA_real_,
suppressWarnings(as.numeric(tree_df$label))
)
p <- ggtree(tree, branch.length = "none") %<+% tree_df +
## bootstrap
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"),
guide = "legend",
name = "Bootstrap Percentage (BP)",
breaks = c("(75,100]", "(50,75]", "(0,50]"),
labels = expression(BP > 75,
50 < BP * "<=75",
BP <= 50)
) +
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
) +
## tip 颜色 = subfamily
geom_tippoint(
aes(color = subfamily, shape = tip_role),
size = 2,
na.rm = TRUE
) +
scale_color_manual(
values = subfam_cols,
name = "Subfamily"
) +
scale_shape_manual(
values = c(target = 17, reference = 16, gene = 16),
breaks = c("target", "reference"),
labels = c("Target", "Reference"),
name = "Species role",
na.translate = FALSE
) +
## 基因 ID 标签
geom_tiplab(aes(label = display_label), offset = label_offset, size = 3, hjust = 0) +
ggtitle(NULL) +
theme_tree() +
theme(
legend.background = element_rect(color = "#969696",
fill = "#d9d9d9")
)
attr(p, "label_offset") <- label_offset
return(p)
}
## ===================== 5. 解析 MEME XML =====================
parse_meme_xml <- function(meme_file) {
doc <- xml2::read_xml(meme_file)
## 所有序列的 id/name/length
seq_nodes <- xml2::xml_find_all(doc, ".//sequence")
seq_ids <- xml2::xml_attr(seq_nodes, "id")
seq_names <- xml2::xml_attr(seq_nodes, "name")
## 兼容 length / Length 大小写
seq_lengths <- xml2::xml_attr(seq_nodes, "length")
if (any(is.na(seq_lengths))) {
seq_lengths2 <- xml2::xml_attr(seq_nodes, "Length")
idx <- which(is.na(seq_lengths) & !is.na(seq_lengths2))
seq_lengths[idx] <- seq_lengths2[idx]
}
seq_lengths <- suppressWarnings(as.numeric(seq_lengths))
name_map <- setNames(seq_names, seq_ids) # id -> gene
length_map <- setNames(seq_lengths, seq_names) # gene -> seqlen
motif_nodes <- xml2::xml_find_all(doc, ".//motif")
site_list <- list()
motif_index <- 1L
for (m in motif_nodes) {
motif_id <- xml2::xml_attr(m, "id")
if (is.na(motif_id) || motif_id == "") {
motif_id <- xml2::xml_attr(m, "name")
}
if (is.na(motif_id) || motif_id == "") {
motif_id <- paste0("Motif_", motif_index)
}
motif_width <- xml2::xml_attr(m, "width")
motif_width <- suppressWarnings(as.numeric(motif_width))
if (is.na(motif_width)) motif_width <- 10
sites <- xml2::xml_find_all(m, ".//contributing_site")
if (length(sites) == 0) next
seq_id <- xml2::xml_attr(sites, "sequence_id")
start <- xml2::xml_attr(sites, "position")
start <- suppressWarnings(as.numeric(start))
width <- rep(motif_width, length(sites))
df <- data.frame(
seq_id = seq_id,
start = start,
width = width,
motif = motif_id,
stringsAsFactors = FALSE
)
site_list[[length(site_list) + 1L]] <- df
motif_index <- motif_index + 1L
}
if (length(site_list) == 0) {
stop("在 MEME XML 中没有找到任何 motif site,请检查文件或 XML 结构。", call. = FALSE)
}
sites <- dplyr::bind_rows(site_list)
## 序列名 & 序列长度来自 XML 中的 sequence 区
sites$gene <- unname(name_map[sites$seq_id])
sites$seqlen <- length_map[sites$gene]
## motif 终点坐标
sites$end <- sites$start + sites$width - 1
sites
}
## ===================== 6. 解析 GFF3(基因结构) =====================
parse_gff3_gene_structure <- function(gff3_file, tree_genes) {
tree_lookup <- setNames(tree_genes, normalize_gene_id(tree_genes))
gff <- read.table(gff3_file,
sep = "\t",
comment.char = "#",
quote = "",
stringsAsFactors = FALSE,
fill = TRUE)
if (ncol(gff) < 9) {
stop("GFF3 文件列数不足 9 列,请检查格式。", call. = FALSE)
}
colnames(gff)[1:9] <- c("seqid", "source", "type", "start",
"end", "score", "strand", "phase",
"attributes")
gff$start <- as.numeric(gff$start)
gff$end <- as.numeric(gff$end)
## 只保留关心的 feature 类型
feat_types <- c("exon", "CDS", "UTR",
"five_prime_UTR", "three_prime_UTR",
"5UTR", "3UTR")
feat_df <- gff[gff$type %in% feat_types, ]
if (nrow(feat_df) == 0) {
stop("在 GFF3 中没有找到 exon/CDS/UTR 等特征。", call. = FALSE)
}
## 解析 attributes 里的 Parent=...
get_attr <- function(attr_vec, key) {
sapply(strsplit(attr_vec, ";"), function(fields) {
kv <- strsplit(fields, "=")
res <- NA_character_
for (p in kv) {
if (length(p) == 2 && p[1] == key) {
res <- p[2]
break
}
}
res
})
}
## NCBI/Gnomon GFF3 often stores mRNA/CDS IDs as XP_* protein accessions,
## while the tree tips are XM_* transcript accessions in Name/transcript_id.
## Build an ID -> tree-tip alias map from mRNA rows before matching features.
mrna_df <- gff[tolower(gff$type) %in% c("mrna", "transcript"), ]
transcript_alias <- character(0)
if (nrow(mrna_df) > 0) {
mrna_id <- get_attr(mrna_df$attributes, "ID")
mrna_name <- get_attr(mrna_df$attributes, "Name")
mrna_tx <- get_attr(mrna_df$attributes, "transcript_id")
mrna_gene <- get_attr(mrna_df$attributes, "gene")
for (i in seq_len(nrow(mrna_df))) {
candidates <- c(mrna_name[i], mrna_tx[i], mrna_id[i], mrna_gene[i])
candidates <- candidates[!is.na(candidates) & nzchar(candidates)]
target <- NA_character_
for (cand in candidates) {
key <- normalize_gene_id(cand)
if (key %in% names(tree_lookup)) {
target <- unname(tree_lookup[key])
break
}
}
if (!is.na(target) && !is.na(mrna_id[i]) && nzchar(mrna_id[i])) {
transcript_alias[mrna_id[i]] <- target
}
}
}
feat_df$Parent <- get_attr(feat_df$attributes, "Parent")
feat_df$Parent_resolved <- ifelse(!is.na(feat_df$Parent) & feat_df$Parent %in% names(transcript_alias),
unname(transcript_alias[feat_df$Parent]),
feat_df$Parent)
feat_df$Parent_key <- normalize_gene_id(feat_df$Parent_resolved)
## 只保留 Parent/别名 出现在树 tip.label 的转录本
feat_df <- feat_df[!is.na(feat_df$Parent_resolved) &
feat_df$Parent_key %in% names(tree_lookup), ]
if (nrow(feat_df) == 0) {
stop("GFF3 中的 Parent 与树 tip.label 无交集,请检查 ID 是否一致。", call. = FALSE)
}
feat_df$gene <- unname(tree_lookup[feat_df$Parent_key])
## 每个 gene 的基因区域:真实 genomic 坐标范围
gene_ranges <- feat_df %>%
dplyr::group_by(gene) %>%
dplyr::summarise(
gene_start = min(start, end, na.rm = TRUE),
gene_end = max(start, end, na.rm = TRUE),
gene_len = gene_end - gene_start + 1,
.groups = "drop"
)
## 特征相对该基因起点的坐标
feat_df <- feat_df %>%
dplyr::left_join(gene_ranges, by = "gene") %>%
dplyr::mutate(
rel_start = pmin(start, end) - gene_start,
rel_end = pmax(start, end) - gene_start
) %>%
dplyr::mutate(
Type = dplyr::case_when(
type %in% c("five_prime_UTR", "three_prime_UTR",
"5UTR", "3UTR", "UTR") ~ "UTR",
TRUE ~ type
)
)
list(features = feat_df, ranges = gene_ranges)
}
## ===================== 7. 解析 domtblout(domain) =====================
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 输出是否为 --domtblout 格式。", call. = FALSE)
}
## HMMER3 domtblout 固定前 22 列
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"
)
## 余下列合并为 description
if (ncol(dom) > 22) {
desc_cols <- dom[, 23:ncol(dom), drop = FALSE]
dom$description <- apply(desc_cols, 1, function(x) paste(x, collapse = " "))
} else {
dom$description <- NA_character_
}
## 转成数值
num_cols <- c(
"tlen", "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"
)
for (cn in intersect(num_cols, colnames(dom))) {
dom[[cn]] <- suppressWarnings(as.numeric(dom[[cn]]))
}
## 只保留树上的基因(若指定)
if (!is.null(tree_genes)) {
tree_lookup <- setNames(tree_genes, normalize_gene_id(tree_genes))
dom$query_key <- normalize_gene_id(dom$query_name)
dom <- dom[dom$query_key %in% names(tree_lookup), , drop = FALSE]
if (nrow(dom) > 0) {
dom$query_name <- unname(tree_lookup[dom$query_key])
}
}
if (nrow(dom) == 0) {
warning("domtblout 中没有与树上的基因匹配的记录。")
return(dom)
}
## 计算 domain 覆盖度(按蛋白长度 qlen)
dom <- dom %>%
dplyr::mutate(
dom_len = ali_to - ali_from + 1,
cov = ifelse(is.na(qlen) | qlen <= 0, NA_real_, dom_len / qlen)
)
## 使用 i_Evalue 作为 domain 显著性(若不存在则退回 full E)
dom_e <- if ("i_Evalue" %in% colnames(dom)) dom$i_Evalue else dom$E_full
dom_filt <- dom %>%
dplyr::mutate(evalue = dom_e) %>%
dplyr::filter(!is.na(evalue), evalue <= e_cutoff)
## 只有在 cov_cutoff 非 NA 时才启用覆盖度过滤
if (!is.na(cov_cutoff)) {
dom_filt <- dom_filt %>%
dplyr::filter(is.na(cov) | cov >= cov_cutoff)
}
if (nrow(dom_filt) == 0) {
warning("在 domtblout 中按 E-value/coverage 筛选后没有 domain 命中。")
}
dom_filt
}
## ===================== 8. 底部坐标轴辅助函数 =====================
make_axis_df <- function(max_len, scale_val, x_left, axis_y, n_breaks = 5) {
empty_line <- data.frame(x = numeric(0), xend = numeric(0),
y = numeric(0), yend = numeric(0))
empty_labels <- data.frame(x = numeric(0), y = numeric(0),
label = character(0))
if (!is.finite(max_len) || max_len <= 0 ||
!is.finite(scale_val) || scale_val <= 0) {
return(list(line = empty_line, ticks = empty_line, labels = empty_labels))
}
brks <- pretty(c(0, max_len), n = n_breaks)
brks <- brks[brks >= 0 & brks <= max_len]
line_df <- data.frame(
x = x_left,
xend = x_left + max_len * scale_val,
y = axis_y,
yend = axis_y
)
ticks_df <- data.frame(
x = x_left + brks * scale_val,
xend = x_left + brks * scale_val,
y = axis_y,
yend = axis_y + 0.2
)
labels_df <- data.frame(
x = x_left + brks * scale_val,
y = axis_y - 0.3,
label = as.integer(brks)
)
list(line = line_df, ticks = ticks_df, labels = labels_df)
}
## ===================== 9. 画树 & 基本坐标信息 =====================
label_offset <- 0.4
p_tree <- plot_iqtree_by_subfamily(treefile, subfam, subfam_cols,
label_offset = label_offset)
tree_dat <- p_tree$data
tip_dat <- tree_dat %>%
dplyr::filter(isTip) %>%
dplyr::select(label, x, y, subfamily)
if (nrow(tip_dat) == 0) {
stop("树中没有 tip 节点,请检查 treefile。", 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)
min_y <- min(tree_dat$y, na.rm = TRUE)
axis_y <- min_y - 1.5
tree_core_width <- x_tip_max - x_tree_min
if (!is.finite(tree_core_width) || tree_core_width <= 0) {
stop("树的宽度计算异常,请检查 treefile。", call. = FALSE)
}
x_label_left <- x_tip_max + label_offset
label_panel_width <- max(1.5, tree_core_width * 0.6)
x_A_left <- x_tree_min
x_A_right <- x_label_left + label_panel_width
tip_lookup <- setNames(tip_dat$label, normalize_gene_id(tip_dat$label))
empty_axis <- make_axis_df(NA_real_, NA_real_, NA_real_, axis_y)
## ===================== 10. 按需解析 panel 数据 =====================
if (has_panel("motif")) {
if (meme_xml == "NONE" || !file.exists(meme_xml)) {
stop("已选择 motif panel,但 MEME XML 不存在。", call. = FALSE)
}
motif_sites <- parse_meme_xml(meme_xml)
motif_sites <- motif_sites %>%
dplyr::filter(!is.na(gene)) %>%
dplyr::mutate(gene_key = normalize_gene_id(gene)) %>%
dplyr::filter(gene_key %in% names(tip_lookup)) %>%
dplyr::mutate(gene = unname(tip_lookup[gene_key])) %>%
dplyr::left_join(tip_dat, by = c("gene" = "label")) %>%
dplyr::filter(!is.na(seqlen), !is.na(y))
if (nrow(motif_sites) == 0) {
stop("在 MEME 结果中没有找到与树 tip.label 匹配的基因。", call. = FALSE)
}
cat("motif 序列长度范围 (XML): ",
min(motif_sites$seqlen, na.rm = TRUE), " ~ ",
max(motif_sites$seqlen, na.rm = TRUE), "\n")
motif_levels <- sort(unique(motif_sites$motif))
motif_cols <- setNames(make_distinct_cols(length(motif_levels)), motif_levels)
motif_sites$motif <- factor(motif_sites$motif, levels = motif_levels)
} else {
motif_sites <- data.frame()
motif_cols <- character()
}
if (has_panel("structure")) {
if (gff3_file == "NONE" || !file.exists(gff3_file)) {
stop("已选择 structure panel,但 GFF3 文件不存在。", call. = FALSE)
}
gs <- parse_gff3_gene_structure(gff3_file, tip_dat$label)
gene_feat <- gs$features
gene_ranges <- gs$ranges
gene_ranges <- gene_ranges %>%
dplyr::left_join(tip_dat, by = c("gene" = "label")) %>%
dplyr::filter(!is.na(y))
gene_feat <- gene_feat %>%
dplyr::left_join(tip_dat, by = c("gene" = "label")) %>%
dplyr::filter(!is.na(y))
if (nrow(gene_feat) == 0) {
stop("GFF3 中匹配到树 tip.label 的基因结构为空。", call. = FALSE)
}
cat("基因长度范围 (GFF3): ",
min(gene_ranges$gene_len, na.rm = TRUE), " ~ ",
max(gene_ranges$gene_len, na.rm = TRUE), "\n")
} else {
gene_feat <- data.frame()
gene_ranges <- data.frame()
}
if (has_panel("domain")) {
if (domtblout_file == "NONE" || !file.exists(domtblout_file)) {
stop("已选择 domain panel,但 domtblout 文件不存在。", call. = FALSE)
}
domain_hits <- parse_domtblout(domtblout_file,
e_cutoff = e_cutoff,
cov_cutoff = cov_cutoff,
tree_genes = tip_dat$label)
domain_hits <- domain_hits %>%
dplyr::left_join(tip_dat, by = c("query_name" = "label")) %>%
dplyr::filter(!is.na(y))
if (nrow(domain_hits) > 0) {
cat("蛋白长度范围 (domtblout qlen): ",
min(domain_hits$qlen, na.rm = TRUE), " ~ ",
max(domain_hits$qlen, na.rm = TRUE), "\n")
} else {
cat("注意:筛选后没有任何 domain 命中,将不会绘制 domain 命中矩形。\n")
}
} else {
domain_hits <- data.frame()
}
## ===================== 11. 动态 X 布局 =====================
gap <- max(0.3, tree_core_width * 0.10)
r_B <- 0.6
panel_ratio <- c(motif = 2.0, structure = 2.8, domain = 2.0)
panel_label <- c(motif = "Motif", structure = "Gene structure", domain = "Domain")
x_B_left <- x_A_right + gap
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_layout <- data.frame(
panel = character(), x_left = numeric(), x_right = numeric(), width = numeric(), label = character(),
stringsAsFactors = FALSE
)
cursor <- x_B_right
for (panel_name in selected_panels) {
width <- tree_core_width * panel_ratio[[panel_name]]
x_left <- cursor + gap
x_right <- x_left + width
panel_layout <- rbind(panel_layout, data.frame(
panel = panel_name,
x_left = x_left,
x_right = x_right,
width = width,
label = panel_label[[panel_name]],
stringsAsFactors = FALSE
))
cursor <- x_right
}
panel_left <- function(panel_name) panel_layout$x_left[match(panel_name, panel_layout$panel)]
panel_width <- function(panel_name) panel_layout$width[match(panel_name, panel_layout$panel)]
panel_right <- function(panel_name) panel_layout$x_right[match(panel_name, panel_layout$panel)]
x_plot_right <- if (nrow(panel_layout) > 0) max(panel_layout$x_right) else x_B_right
## ===================== 12. panel 坐标计算 =====================
if (has_panel("motif")) {
x_C_left <- panel_left("motif")
width_C <- panel_width("motif")
max_len_motif <- max(motif_sites$seqlen, na.rm = TRUE)
scale_motif <- width_C / max_len_motif
cat("最长 motif 序列长度: ", max_len_motif,
";motif 面板宽度: ", width_C,
";scale_motif: ", scale_motif, "\n")
motif_gene_tracks <- motif_sites %>%
dplyr::distinct(gene, y, seqlen) %>%
dplyr::mutate(
x_gene_start = x_C_left,
x_gene_end = x_C_left + seqlen * scale_motif
)
motif_rect <- motif_sites %>%
dplyr::mutate(
xmin = x_C_left + (start - 1) * scale_motif,
xmax = x_C_left + end * scale_motif,
ymin = y - 0.25,
ymax = y + 0.25
)
motif_axis <- make_axis_df(max_len_motif, scale_motif, x_C_left, axis_y)
} else {
motif_gene_tracks <- data.frame()
motif_rect <- data.frame()
motif_axis <- empty_axis
}
if (has_panel("structure")) {
x_D_left <- panel_left("structure")
width_D <- panel_width("structure")
max_gene_len <- max(gene_ranges$gene_len, na.rm = TRUE)
scale_gene <- width_D / max_gene_len
cat("最长基因长度: ", max_gene_len,
";gene 面板宽度: ", width_D,
";scale_gene: ", scale_gene, "\n")
gene_tracks <- gene_ranges %>%
dplyr::mutate(
x_gene_start = x_D_left,
x_gene_end = x_D_left + gene_len * scale_gene
)
gene_feat_rect <- gene_feat %>%
dplyr::mutate(
xmin = x_D_left + rel_start * scale_gene,
xmax = x_D_left + rel_end * scale_gene,
ymin = y - 0.25,
ymax = y + 0.25
) %>%
dplyr::filter(Type %in% c("CDS", "UTR"))
gene_palette <- c(CDS = "#1b9e77", UTR = "#d95f02")
gene_types <- sort(unique(gene_feat_rect$Type))
gene_cols_vec <- gene_palette[gene_types]
names(gene_cols_vec) <- gene_types
gene_axis <- make_axis_df(max_gene_len, scale_gene, x_D_left, axis_y)
} else {
gene_tracks <- data.frame()
gene_feat_rect <- data.frame()
gene_cols_vec <- character()
gene_axis <- empty_axis
}
if (has_panel("domain")) {
x_E_left <- panel_left("domain")
width_E <- panel_width("domain")
max_qlen <- NA_real_
scale_domain <- NA_real_
if (nrow(domain_hits) > 0) {
max_qlen <- max(domain_hits$qlen, na.rm = TRUE)
scale_domain <- width_E / max_qlen
cat("最长蛋白长度: ", max_qlen,
";domain 面板宽度: ", width_E,
";scale_domain: ", scale_domain, "\n")
domain_gene_tracks <- domain_hits %>%
dplyr::distinct(query_name, y, qlen) %>%
dplyr::mutate(
x_gene_start = x_E_left,
x_gene_end = x_E_left + qlen * scale_domain
)
domain_rect <- domain_hits %>%
dplyr::mutate(
xmin = x_E_left + (ali_from - 1) * scale_domain,
xmax = x_E_left + ali_to * scale_domain,
ymin = y - 0.25,
ymax = y + 0.25
)
domain_levels <- sort(unique(domain_rect$target_name))
domain_cols <- setNames(make_distinct_cols(length(domain_levels)), domain_levels)
domain_rect$target_name <- factor(domain_rect$target_name, levels = domain_levels)
} else {
domain_gene_tracks <- data.frame()
domain_rect <- data.frame()
domain_cols <- character()
}
domain_axis <- make_axis_df(max_qlen, scale_domain, x_E_left, axis_y)
} else {
domain_gene_tracks <- data.frame()
domain_rect <- data.frame()
domain_cols <- character()
domain_axis <- empty_axis
}
subfam_ranges <- make_subfamily_ranges(tip_dat)
tree_header_x <- (x_A_left + x_A_right) / 2
header_y <- max_y + 1.2
## ===================== 13. 拼合整图 =====================
p_combo <- p_tree
if (has_panel("motif")) {
p_combo <- p_combo +
geom_segment(
data = motif_gene_tracks, inherit.aes = FALSE,
aes(x = x_gene_start, xend = x_gene_end, y = y, yend = y),
colour = "grey70", linewidth = 0.4
) +
ggnewscale::new_scale_fill() +
geom_rect(
data = motif_rect, inherit.aes = FALSE,
aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = motif),
color = NA
) +
scale_fill_manual(values = motif_cols, name = "Motif") +
geom_segment(data = motif_axis$line, inherit.aes = FALSE, aes(x = x, xend = xend, y = y, yend = yend), colour = "black", linewidth = 0.4) +
geom_segment(data = motif_axis$ticks, inherit.aes = FALSE, aes(x = x, xend = xend, y = y, yend = yend), colour = "black", linewidth = 0.3) +
geom_text(data = motif_axis$labels, inherit.aes = FALSE, aes(x = x, y = y, label = label), size = 2.5)
}
if (has_panel("structure")) {
p_combo <- p_combo +
geom_segment(
data = gene_tracks, inherit.aes = FALSE,
aes(x = x_gene_start, xend = x_gene_end, y = y, yend = y),
colour = "grey70", linewidth = 0.4
) +
ggnewscale::new_scale_fill() +
geom_rect(
data = gene_feat_rect, inherit.aes = FALSE,
aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = Type),
color = NA
) +
scale_fill_manual(values = gene_cols_vec, name = "Gene structure") +
geom_segment(data = gene_axis$line, inherit.aes = FALSE, aes(x = x, xend = xend, y = y, yend = yend), colour = "black", linewidth = 0.4) +
geom_segment(data = gene_axis$ticks, inherit.aes = FALSE, aes(x = x, xend = xend, y = y, yend = yend), colour = "black", linewidth = 0.3) +
geom_text(data = gene_axis$labels, inherit.aes = FALSE, aes(x = x, y = y, label = label), size = 2.5)
}
if (has_panel("domain") && nrow(domain_rect) > 0) {
p_combo <- p_combo +
geom_segment(
data = domain_gene_tracks, inherit.aes = FALSE,
aes(x = x_gene_start, xend = x_gene_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), colour = "black", linewidth = 0.4) +
geom_segment(data = domain_axis$ticks, inherit.aes = FALSE, aes(x = x, xend = xend, y = y, yend = yend), colour = "black", linewidth = 0.3) +
geom_text(data = domain_axis$labels, inherit.aes = FALSE, aes(x = x, y = y, label = label), size = 2.5)
}
p_combo <- p_combo +
geom_segment(
data = subfam_ranges, inherit.aes = FALSE,
aes(x = x_subfam, xend = x_subfam, y = ymin, yend = ymax, colour = subfamily),
linewidth = 1
) +
geom_text(
data = subfam_ranges, inherit.aes = FALSE,
aes(x = x_subfam + width_B * 0.05, y = ymid, label = subfamily, colour = subfamily),
hjust = 0, size = 3
) +
annotate("text", x = tree_header_x, y = header_y, label = "Phylogenetic tree", fontface = "bold", size = 4)
for (i in seq_len(nrow(panel_layout))) {
p_combo <- p_combo + annotate(
"text",
x = (panel_layout$x_left[i] + panel_layout$x_right[i]) / 2,
y = header_y,
label = panel_layout$label[i],
fontface = "bold",
size = 4
)
}
p_combo <- p_combo +
coord_cartesian(xlim = c(x_A_left, x_plot_right + tree_core_width * 0.1)) +
expand_limits(y = header_y + 0.5) +
theme(
legend.position = "right",
legend.background = element_rect(color = "#969696", fill = "#d9d9d9")
)
plot_width <- c(`1` = 12, `2` = 16, `3` = 20)[as.character(length(selected_panels))]
if (is.na(plot_width)) plot_width <- 20
ggsave(output_pdf, p_combo, width = plot_width, height = 10)
output_png <- sub("\\.pdf$", ".png", output_pdf)
ggsave(output_png, p_combo, width = plot_width, height = 10, dpi = 150, bg = "white")
cat("已输出 combo 图到: ", output_pdf, "\n")
cat("[OK] PNG: ", output_png, "\n", sep = "")