Files
PlantAI-dev/scripts/combo_tree_motif_structure_domain.R
T

960 lines
32 KiB
R
Raw Normal View History

2026-08-03 18:42:47 +08:00
#!/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 = "")