feat(bioflow): P0 内核移植与 8 环境镜像——副本 100 处适配零硬编码/8 镜像验证清单全过/金标 24 ID diff 空/TL 复查 4 项修正(gfpy 储备裁决+镜像备份排除+冒烟零硬编码+金标固化)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+215
@@ -0,0 +1,215 @@
|
||||
#!/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")
|
||||
Reference in New Issue
Block a user