216 lines
7.6 KiB
R
216 lines
7.6 KiB
R
#!/usr/bin/env Rscript
|
|||
|
|
|
||
|
|
suppressPackageStartupMessages({
|
||
|
|
library(ape)
|
||
|
|
library(ggtree)
|
||
|
|
library(tidytree)
|
||
|
|
library(ggplot2)
|
||
|
|
})
|
||
|
|
|
||
|
|
args <- commandArgs(trailingOnly = TRUE)
|
||
|
|
|
||
|
|
if (length(args) < 1) {
|
||
|
|
stop("用法: Rscript plot_tree.R <treefile> [output_dir] [layout: rectangular|circular|both]", call. = FALSE)
|
||
|
|
}
|
||
|
|
|
||
|
|
treefile <- args[1]
|
||
|
|
output_dir <- if (length(args) >= 2 && args[2] != "") args[2] else dirname(treefile)
|
||
|
|
tree_layout <- if (length(args) >= 3 && args[3] != "") tolower(args[3]) else "both"
|
||
|
|
output_prefix <- if (length(args) >= 4 && args[4] != "") args[4] else ""
|
||
|
|
num_arg <- function(idx, default) {
|
||
|
|
if (length(args) >= idx && nzchar(args[idx])) {
|
||
|
|
val <- suppressWarnings(as.numeric(args[idx]))
|
||
|
|
if (is.finite(val)) return(val)
|
||
|
|
}
|
||
|
|
default
|
||
|
|
}
|
||
|
|
bool_arg <- function(idx, default) {
|
||
|
|
if (length(args) >= idx && nzchar(args[idx])) {
|
||
|
|
return(tolower(args[idx]) %in% c("1", "true", "yes", "y"))
|
||
|
|
}
|
||
|
|
default
|
||
|
|
}
|
||
|
|
str_arg <- function(idx, default) {
|
||
|
|
if (length(args) >= idx && nzchar(args[idx])) return(args[idx])
|
||
|
|
default
|
||
|
|
}
|
||
|
|
shape_code <- function(x, default) {
|
||
|
|
x <- tolower(as.character(x))
|
||
|
|
switch(x, circle = 16, triangle = 17, square = 15, diamond = 18, star = 8, default)
|
||
|
|
}
|
||
|
|
label_offset <- num_arg(5, 0.02)
|
||
|
|
tip_label_size <- num_arg(6, 2.8)
|
||
|
|
branch_color <- str_arg(7, "black")
|
||
|
|
branch_width <- num_arg(8, 0.5)
|
||
|
|
show_bootstrap <- bool_arg(9, TRUE)
|
||
|
|
bootstrap_size <- num_arg(10, 2)
|
||
|
|
tip_point_size <- num_arg(11, 0)
|
||
|
|
tip_shape <- shape_code(str_arg(12, "circle"), 16)
|
||
|
|
circular_label_offset <- num_arg(13, 0.8)
|
||
|
|
circular_label_size <- num_arg(14, 1.8)
|
||
|
|
marker_mode <- tolower(str_arg(15, "auto"))
|
||
|
|
if (!marker_mode %in% c("auto", "tip", "species", "role")) marker_mode <- "auto"
|
||
|
|
if (!tree_layout %in% c("rectangular", "circular", "both")) {
|
||
|
|
stop("layout 必须是 rectangular、circular 或 both", call. = FALSE)
|
||
|
|
}
|
||
|
|
dir.create(output_dir, showWarnings = FALSE, recursive = TRUE)
|
||
|
|
|
||
|
|
tree <- read.tree(treefile)
|
||
|
|
n_tips <- length(tree$tip.label)
|
||
|
|
max_id_len <- max(nchar(tree$tip.label))
|
||
|
|
|
||
|
|
cat("基因数:", n_tips, " 最长ID:", max_id_len, "字符\n")
|
||
|
|
|
||
|
|
|
||
|
|
infer_tip_species <- function(label) {
|
||
|
|
label <- as.character(label)
|
||
|
|
if (grepl("|", label, fixed = TRUE)) return(strsplit(label, "|", fixed = TRUE)[[1]][1])
|
||
|
|
if (grepl("^At[0-9]g", label, ignore.case = FALSE)) return("At")
|
||
|
|
if (grepl("^LITCHI", label, ignore.case = FALSE)) return("LITCHI")
|
||
|
|
if (grepl("^LOC_Os", label, ignore.case = FALSE)) return("Os")
|
||
|
|
m <- regmatches(label, regexpr("^[A-Za-z]+", label))
|
||
|
|
if (length(m) && nzchar(m)) return(m)
|
||
|
|
"Unknown"
|
||
|
|
}
|
||
|
|
|
||
|
|
species_shape_values <- function(species_levels) {
|
||
|
|
palette <- c(tip_shape, 16, 15, 18, 8, 3, 4, 7, 9)
|
||
|
|
vals <- rep(palette, length.out = length(species_levels))
|
||
|
|
names(vals) <- species_levels
|
||
|
|
vals
|
||
|
|
}
|
||
|
|
|
||
|
|
# 提取bootstrap
|
||
|
|
tree_df <- as_tibble(tree)
|
||
|
|
tree_df$isTip <- tree_df$node <= n_tips
|
||
|
|
tree_df$support <- ifelse(
|
||
|
|
tree_df$isTip,
|
||
|
|
NA_real_,
|
||
|
|
suppressWarnings(as.numeric(tree_df$label))
|
||
|
|
)
|
||
|
|
tree_df$tip_species <- ifelse(tree_df$isTip, vapply(tree_df$label, infer_tip_species, character(1)), NA_character_)
|
||
|
|
species_levels <- sort(unique(na.omit(tree_df$tip_species[tree_df$isTip])))
|
||
|
|
effective_marker_mode <- marker_mode
|
||
|
|
if (effective_marker_mode == "auto") effective_marker_mode <- ifelse(length(species_levels) > 1, "species", "tip")
|
||
|
|
if (effective_marker_mode == "species" && length(species_levels) < 2) effective_marker_mode <- "tip"
|
||
|
|
tree_df$tip_marker <- ifelse(effective_marker_mode == "species", tree_df$tip_species, "Gene tip")
|
||
|
|
shape_values <- if (effective_marker_mode == "species") species_shape_values(species_levels) else c("Gene tip" = tip_shape)
|
||
|
|
shape_title <- if (effective_marker_mode == "species") "Species" else "Tip marker"
|
||
|
|
|
||
|
|
# 基础图
|
||
|
|
p_base <- ggtree(tree, layout = "rectangular", branch.length = "none", color = branch_color, size = branch_width) %<+% tree_df
|
||
|
|
if (show_bootstrap) {
|
||
|
|
p_base <- p_base +
|
||
|
|
geom_nodepoint(
|
||
|
|
aes(fill = cut(support, c(0, 50, 75, 100))),
|
||
|
|
shape = 21, size = bootstrap_size, na.rm = TRUE
|
||
|
|
) +
|
||
|
|
scale_fill_manual(
|
||
|
|
values = c("black", "grey", "white"),
|
||
|
|
name = "Bootstrap",
|
||
|
|
breaks = c("(75,100]", "(50,75]", "(0,50]")
|
||
|
|
)
|
||
|
|
}
|
||
|
|
if (tip_point_size > 0) {
|
||
|
|
p_base <- p_base +
|
||
|
|
geom_tippoint(aes(shape = tip_marker), size = tip_point_size, color = branch_color, na.rm = TRUE) +
|
||
|
|
scale_shape_manual(values = shape_values, name = shape_title, na.translate = FALSE)
|
||
|
|
}
|
||
|
|
p_base <- p_base + geom_tiplab(size = tip_label_size, offset = label_offset, hjust = 0) + theme_tree2()
|
||
|
|
|
||
|
|
# 获取范围
|
||
|
|
pb <- ggplot_build(p_base)
|
||
|
|
panel_ranges <- pb$layout$panel_params[[1]]
|
||
|
|
|
||
|
|
# 计算x轴范围
|
||
|
|
id_width <- max_id_len * max(0.05, tip_label_size * 0.045)
|
||
|
|
x_max_tree <- max(panel_ranges$x.range)
|
||
|
|
|
||
|
|
# 关键:右侧留足够空间给图例(图例宽度 + ID末尾到图例的距离)
|
||
|
|
legend_space <- 3.5 # 图例占据约3.5个x单位
|
||
|
|
x_max_needed <- x_max_tree + id_width + legend_space
|
||
|
|
|
||
|
|
y_min <- min(panel_ranges$y.range)
|
||
|
|
y_max <- max(panel_ranges$y.range)
|
||
|
|
|
||
|
|
cat("需要x_max:", round(x_max_needed, 2), "\n")
|
||
|
|
|
||
|
|
# 最终图
|
||
|
|
p_final <- p_base +
|
||
|
|
coord_cartesian(
|
||
|
|
xlim = c(0, x_max_needed),
|
||
|
|
ylim = c(y_min, y_max),
|
||
|
|
clip = "off"
|
||
|
|
) +
|
||
|
|
# 图例放最右边,垂直居中
|
||
|
|
theme(
|
||
|
|
legend.position = c(0.98, 0.5), # x=0.98最右,y=0.5垂直居中
|
||
|
|
legend.justification = c("right", "center"), # 右对齐,垂直居中
|
||
|
|
legend.background = element_rect(color = "grey50", fill = "white", size = 0.3),
|
||
|
|
plot.margin = margin(5, 5, 5, 5)
|
||
|
|
)
|
||
|
|
|
||
|
|
stem <- if (nzchar(output_prefix)) output_prefix else tools::file_path_sans_ext(basename(treefile))
|
||
|
|
|
||
|
|
save_plot_pair <- function(plot_obj, png_file, pdf_file, width, height) {
|
||
|
|
png(png_file, width = width, height = height, units = "in", res = 300, bg = "white")
|
||
|
|
print(plot_obj)
|
||
|
|
dev.off()
|
||
|
|
cat("PNG saved:", png_file, "\n")
|
||
|
|
ggsave(pdf_file, plot_obj, width = width, height = height, limitsize = FALSE)
|
||
|
|
cat("PDF saved:", pdf_file, "\n")
|
||
|
|
}
|
||
|
|
|
||
|
|
# 输出矩形树:保留旧文件名,避免破坏既有前端和 manifest 识别。
|
||
|
|
rect_tip_spacing <- max(0.34, tip_label_size * 0.12)
|
||
|
|
min_height <- max(8, 2.5 + n_tips * rect_tip_spacing)
|
||
|
|
if (tree_layout %in% c("rectangular", "both")) {
|
||
|
|
save_plot_pair(
|
||
|
|
p_final,
|
||
|
|
file.path(output_dir, paste0(stem, ".png")),
|
||
|
|
file.path(output_dir, paste0(stem, ".pdf")),
|
||
|
|
width = max(8.5, 6.5 + max_id_len * tip_label_size * 0.04),
|
||
|
|
height = min_height
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (tree_layout %in% c("circular", "both")) {
|
||
|
|
p_circular <- ggtree(tree, layout = "circular", branch.length = "none", size = branch_width, color = branch_color) %<+% tree_df
|
||
|
|
if (show_bootstrap) {
|
||
|
|
p_circular <- p_circular +
|
||
|
|
geom_nodepoint(
|
||
|
|
aes(fill = cut(support, c(0, 50, 75, 100))),
|
||
|
|
shape = 21, size = bootstrap_size, na.rm = TRUE
|
||
|
|
) +
|
||
|
|
scale_fill_manual(
|
||
|
|
values = c("black", "grey", "white"),
|
||
|
|
name = "Bootstrap",
|
||
|
|
breaks = c("(75,100]", "(50,75]", "(0,50]")
|
||
|
|
)
|
||
|
|
}
|
||
|
|
if (tip_point_size > 0) {
|
||
|
|
p_circular <- p_circular +
|
||
|
|
geom_tippoint(aes(shape = tip_marker), size = tip_point_size, color = branch_color, na.rm = TRUE) +
|
||
|
|
scale_shape_manual(values = shape_values, name = shape_title, na.translate = FALSE)
|
||
|
|
}
|
||
|
|
p_circular <- p_circular +
|
||
|
|
geom_tiplab2(size = circular_label_size, offset = circular_label_offset, align = TRUE, linesize = 0.1) +
|
||
|
|
theme_tree() +
|
||
|
|
theme(
|
||
|
|
legend.position = "right",
|
||
|
|
legend.background = element_rect(color = "grey50", fill = "white", size = 0.3),
|
||
|
|
plot.margin = margin(5, 5, 5, 5)
|
||
|
|
)
|
||
|
|
|
||
|
|
circular_size <- ifelse(n_tips > 120, 12, 10)
|
||
|
|
save_plot_pair(
|
||
|
|
p_circular,
|
||
|
|
file.path(output_dir, paste0(stem, ".circular_tree.png")),
|
||
|
|
file.path(output_dir, paste0(stem, ".circular_tree.pdf")),
|
||
|
|
width = circular_size,
|
||
|
|
height = circular_size
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
cat("完成\n")
|