225 lines
8.3 KiB
Python
225 lines
8.3 KiB
Python
"""Publication-oriented plotting for PEP v2."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import subprocess
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
RSCRIPT_BIN = "Rscript"
|
||
|
|
|
||
|
|
|
||
|
|
def run_r_script(r_script_path: Path, timeout_sec: int = 180) -> None:
|
||
|
|
result = subprocess.run(
|
||
|
|
[RSCRIPT_BIN, str(r_script_path)],
|
||
|
|
capture_output=True,
|
||
|
|
text=True,
|
||
|
|
timeout=timeout_sec,
|
||
|
|
)
|
||
|
|
if result.returncode != 0:
|
||
|
|
raise RuntimeError(result.stderr or result.stdout or "R plotting failed")
|
||
|
|
|
||
|
|
|
||
|
|
def plot_protparam(data_tsv: Path, output_pdf: Path, output_png: Path, r_dir: Path) -> None:
|
||
|
|
r_script = r_dir / "plot_protparam_v2.R"
|
||
|
|
r_script.write_text(
|
||
|
|
f"""
|
||
|
|
library(ggplot2)
|
||
|
|
library(patchwork)
|
||
|
|
|
||
|
|
data <- read.delim("{data_tsv}", sep="\\t", header=TRUE, check.names=FALSE)
|
||
|
|
|
||
|
|
p1 <- ggplot(data, aes(x = mw / 1000)) +
|
||
|
|
geom_histogram(bins = min(15, max(5, nrow(data))), fill = "#4C72B0", color = "white", linewidth = 0.2) +
|
||
|
|
labs(title = "Molecular Weight", x = "MW (kDa)", y = "Count") +
|
||
|
|
theme_minimal(base_size = 10)
|
||
|
|
|
||
|
|
p2 <- ggplot(data, aes(x = pI)) +
|
||
|
|
geom_histogram(bins = min(15, max(5, nrow(data))), fill = "#DD8452", color = "white", linewidth = 0.2) +
|
||
|
|
labs(title = "Isoelectric Point", x = "pI", y = "Count") +
|
||
|
|
theme_minimal(base_size = 10)
|
||
|
|
|
||
|
|
p3 <- ggplot(data, aes(x = length)) +
|
||
|
|
geom_histogram(bins = min(15, max(5, nrow(data))), fill = "#55A868", color = "white", linewidth = 0.2) +
|
||
|
|
labs(title = "Sequence Length", x = "Length (aa)", y = "Count") +
|
||
|
|
theme_minimal(base_size = 10)
|
||
|
|
|
||
|
|
p4 <- ggplot(data, aes(x = gravy)) +
|
||
|
|
geom_histogram(bins = min(15, max(5, nrow(data))), fill = "#8172B3", color = "white", linewidth = 0.2) +
|
||
|
|
labs(title = "GRAVY", x = "GRAVY", y = "Count") +
|
||
|
|
theme_minimal(base_size = 10)
|
||
|
|
|
||
|
|
combined <- (p1 + p2) / (p3 + p4) +
|
||
|
|
plot_annotation(
|
||
|
|
title = "Physicochemical Properties (ExPASy ProtParam)",
|
||
|
|
caption = "Source: https://web.expasy.org/protparam/ ; values retrieved per sequence via official ProtParam endpoint."
|
||
|
|
)
|
||
|
|
|
||
|
|
ggsave("{output_pdf}", combined, width = 12, height = 9)
|
||
|
|
png("{output_png}", width = 1200, height = 900, res = 150)
|
||
|
|
print(combined)
|
||
|
|
dev.off()
|
||
|
|
""",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
run_r_script(r_script)
|
||
|
|
|
||
|
|
|
||
|
|
def plot_deeptmhmm(
|
||
|
|
segments_tsv: Path,
|
||
|
|
summary_tsv: Path,
|
||
|
|
strip_pdf: Path,
|
||
|
|
strip_png: Path,
|
||
|
|
summary_pdf: Path,
|
||
|
|
summary_png: Path,
|
||
|
|
r_dir: Path,
|
||
|
|
) -> None:
|
||
|
|
r_script = r_dir / "plot_deeptmhmm_v2.R"
|
||
|
|
r_script.write_text(
|
||
|
|
f"""
|
||
|
|
library(ggplot2)
|
||
|
|
library(patchwork)
|
||
|
|
|
||
|
|
segments <- read.delim("{segments_tsv}", sep="\\t", header=TRUE, stringsAsFactors=FALSE)
|
||
|
|
summary <- read.delim("{summary_tsv}", sep="\\t", header=TRUE, stringsAsFactors=FALSE)
|
||
|
|
|
||
|
|
short_id <- function(x) ifelse(nchar(x) > 28, paste0(substr(x, 1, 25), "..."), x)
|
||
|
|
|
||
|
|
summary$protein_label <- short_id(summary$protein_id)
|
||
|
|
label_levels <- rev(unique(summary$protein_label[order(summary$tm_count, summary$protein_id)]))
|
||
|
|
segments$protein_label <- factor(short_id(segments$protein_id), levels = label_levels)
|
||
|
|
segments$mid <- (segments$start + segments$end) / 2
|
||
|
|
segments$width <- segments$end - segments$start + 1
|
||
|
|
segments$state_label <- factor(
|
||
|
|
segments$state_label,
|
||
|
|
levels = c("Inside", "Outside", "TM helix", "Beta TM", "Signal", "Lumen")
|
||
|
|
)
|
||
|
|
|
||
|
|
state_colors <- c(
|
||
|
|
"Inside" = "#9ecae1", "Outside" = "#a1d99b", "TM helix" = "#ef6548",
|
||
|
|
"Beta TM" = "#bcbddc", "Signal" = "#fdd0a2", "Lumen" = "#fdae6b"
|
||
|
|
)
|
||
|
|
|
||
|
|
strip_h <- max(5, 0.24 * length(label_levels) + 1.8)
|
||
|
|
|
||
|
|
p_strip <- ggplot(segments, aes(x = mid, y = protein_label, width = width, fill = state_label)) +
|
||
|
|
geom_tile(height = 0.82, color = NA) +
|
||
|
|
scale_fill_manual(values = state_colors, name = "Topology") +
|
||
|
|
scale_x_continuous(expand = c(0.01, 0)) +
|
||
|
|
labs(
|
||
|
|
title = "DeepTMHMM Predicted Topology",
|
||
|
|
subtitle = "Topology parsed from DeepTMHMM predicted_topologies.3line (I=inside, O=outside, M=TM helix)",
|
||
|
|
caption = "Batch web submission to DTU DeepTMHMM; this is not the single-sequence official plot.png output.",
|
||
|
|
x = "Residue position",
|
||
|
|
y = NULL
|
||
|
|
) +
|
||
|
|
theme_minimal(base_size = 10) +
|
||
|
|
theme(
|
||
|
|
legend.position = "bottom",
|
||
|
|
panel.grid = element_blank(),
|
||
|
|
plot.caption = element_text(size = 8, hjust = 0, color = "#555555")
|
||
|
|
)
|
||
|
|
|
||
|
|
type_counts <- as.data.frame(table(summary$glob_tm_label))
|
||
|
|
colnames(type_counts) <- c("protein_type", "count")
|
||
|
|
type_counts$protein_type <- factor(type_counts$protein_type, levels = c("GLOB", "TM"))
|
||
|
|
max_count <- max(type_counts$count, na.rm = TRUE)
|
||
|
|
|
||
|
|
p_type <- ggplot(type_counts, aes(x = protein_type, y = count, fill = protein_type)) +
|
||
|
|
geom_col(width = 0.42, color = "white", linewidth = 0.2) +
|
||
|
|
geom_text(aes(label = count), vjust = -0.4, size = 3) +
|
||
|
|
scale_fill_manual(values = c("GLOB" = "#6baed6", "TM" = "#ef6548")) +
|
||
|
|
scale_y_continuous(expand = expansion(mult = c(0, 0.12)), limits = c(0, max_count * 1.15)) +
|
||
|
|
labs(title = "Protein Class", x = NULL, y = "Count") +
|
||
|
|
theme_minimal(base_size = 9) +
|
||
|
|
theme(legend.position = "none", panel.grid.major.x = element_blank())
|
||
|
|
|
||
|
|
summary$tm_factor <- factor(summary$tm_count, levels = sort(unique(summary$tm_count)))
|
||
|
|
tm_counts <- as.data.frame(table(summary$tm_factor))
|
||
|
|
colnames(tm_counts) <- c("tm_count", "count")
|
||
|
|
tm_counts$tm_count <- factor(tm_counts$tm_count, levels = levels(summary$tm_factor))
|
||
|
|
max_tm <- max(tm_counts$count, na.rm = TRUE)
|
||
|
|
|
||
|
|
p_tm <- ggplot(tm_counts, aes(x = tm_count, y = count)) +
|
||
|
|
geom_col(width = 0.42, fill = "#08519c", color = "white", linewidth = 0.2) +
|
||
|
|
geom_text(aes(label = count), vjust = -0.4, size = 3) +
|
||
|
|
scale_y_continuous(expand = expansion(mult = c(0, 0.12)), limits = c(0, max_tm * 1.15)) +
|
||
|
|
labs(
|
||
|
|
title = "TM Helix Count",
|
||
|
|
subtitle = "Counts from GFF3 when available, else contiguous M segments in topology",
|
||
|
|
x = "TM helices",
|
||
|
|
y = "Count"
|
||
|
|
) +
|
||
|
|
theme_minimal(base_size = 9) +
|
||
|
|
theme(panel.grid.major.x = element_blank())
|
||
|
|
|
||
|
|
p_summary <- p_type + p_tm + plot_layout(widths = c(1, 1.2))
|
||
|
|
|
||
|
|
ggsave("{strip_pdf}", p_strip, width = 11, height = strip_h)
|
||
|
|
ggsave("{summary_pdf}", p_summary, width = 8, height = 3.2)
|
||
|
|
|
||
|
|
png("{strip_png}", width = 1100, height = round(strip_h * 100), res = 150)
|
||
|
|
print(p_strip)
|
||
|
|
dev.off()
|
||
|
|
|
||
|
|
png("{summary_png}", width = 800, height = 320, res = 150)
|
||
|
|
print(p_summary)
|
||
|
|
dev.off()
|
||
|
|
""",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
run_r_script(r_script)
|
||
|
|
|
||
|
|
|
||
|
|
def plot_subcellular_heatmap(scores_tsv: Path, output_pdf: Path, output_png: Path, r_dir: Path) -> None:
|
||
|
|
r_script = r_dir / "plot_subcellular_v2.R"
|
||
|
|
r_script.write_text(
|
||
|
|
f"""
|
||
|
|
library(ggplot2)
|
||
|
|
library(tidyr)
|
||
|
|
library(dplyr)
|
||
|
|
|
||
|
|
raw <- read.delim("{scores_tsv}", sep="\\t", header=TRUE, check.names=FALSE, stringsAsFactors=FALSE)
|
||
|
|
meta_cols <- c("protein_id", "protein_label", "primary_location")
|
||
|
|
locations <- setdiff(names(raw), meta_cols)
|
||
|
|
|
||
|
|
long <- raw %>%
|
||
|
|
pivot_longer(cols = all_of(locations), names_to = "location", values_to = "score") %>%
|
||
|
|
mutate(
|
||
|
|
protein_label = factor(protein_label, levels = rev(unique(raw$protein_label))),
|
||
|
|
location = factor(location, levels = locations),
|
||
|
|
score = as.numeric(score),
|
||
|
|
score_label = ifelse(score > 0, sub("\\\\.0$", "", sprintf("%.1f", score)), ""),
|
||
|
|
text_color = ifelse(score >= 7, "white", "#222222")
|
||
|
|
)
|
||
|
|
|
||
|
|
plot_h <- max(5.5, 0.24 * length(unique(long$protein_label)) + 2.2)
|
||
|
|
plot_w <- max(10, 0.5 * length(locations) + 3.5)
|
||
|
|
|
||
|
|
p <- ggplot(long, aes(x = location, y = protein_label, fill = score)) +
|
||
|
|
geom_tile(color = "white", linewidth = 0.25) +
|
||
|
|
geom_text(aes(label = score_label, color = text_color), size = 1.8, show.legend = FALSE) +
|
||
|
|
scale_color_identity() +
|
||
|
|
scale_fill_gradient(low = "#f7fbff", high = "#08519c", name = "WoLF score") +
|
||
|
|
labs(
|
||
|
|
title = "Subcellular Localization (WoLF PSORT, Plant)",
|
||
|
|
subtitle = "Cell values = WoLF PSORT scores from wolfpsort.hgc.jp",
|
||
|
|
caption = "Note: scores are WoLF PSORT k-NN vote strengths, not experimental localization. score >= 10 is usually reliable; score 3-5 with similar values across columns suggests uncertainty.",
|
||
|
|
x = "Predicted compartment",
|
||
|
|
y = NULL
|
||
|
|
) +
|
||
|
|
theme_minimal(base_size = 10) +
|
||
|
|
theme(
|
||
|
|
axis.text.x = element_text(angle = 45, hjust = 1),
|
||
|
|
panel.grid = element_blank(),
|
||
|
|
plot.caption = element_text(hjust = 0, size = 8, color = "#555555", margin = margin(t = 6))
|
||
|
|
)
|
||
|
|
|
||
|
|
ggsave("{output_pdf}", p, width = plot_w, height = plot_h)
|
||
|
|
png("{output_png}", width = round(plot_w * 100), height = round(plot_h * 100), res = 150)
|
||
|
|
print(p)
|
||
|
|
dev.off()
|
||
|
|
""",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
run_r_script(r_script)
|