Files
PlantAI-dev/scripts/draw_rnaseq_expression.R
2026-08-03 18:42:47 +08:00

453 lines
26 KiB
R
Executable File

#!/usr/bin/env Rscript
suppressPackageStartupMessages({
library(ggplot2)
library(pheatmap)
library(grid)
})
args <- commandArgs(trailingOnly = TRUE)
kv <- list()
for (arg in args) {
if (grepl('=', arg, fixed = TRUE)) {
parts <- strsplit(arg, '=', fixed = TRUE)[[1]]
kv[[parts[1]]] <- paste(parts[-1], collapse = '=')
}
}
required <- c('fpkm', 'tpm', 'vst', 'sample_table', 'de_dir', 'prefix', 'out_dir')
missing <- required[!required %in% names(kv) | !nzchar(unlist(kv[required]))]
if (length(missing) > 0) {
stop('Missing required arguments: ', paste(missing, collapse = ', '))
}
dir.create(kv$out_dir, recursive = TRUE, showWarnings = FALSE)
read_matrix <- function(path) {
mat <- read.table(path, header = TRUE, row.names = 1, sep = '\t', check.names = FALSE,
stringsAsFactors = FALSE, comment.char = '', quote = '')
mat <- as.matrix(mat)
storage.mode(mat) <- 'numeric'
mat
}
read_samples <- function(path, sample_names) {
tab <- read.table(path, header = TRUE, sep = '\t', stringsAsFactors = FALSE,
check.names = FALSE, comment.char = '', quote = '')
names(tab) <- tolower(names(tab))
sample_col <- if ('sample' %in% names(tab)) 'sample' else names(tab)[1]
condition_col <- if ('condition' %in% names(tab)) 'condition' else names(tab)[min(2, ncol(tab))]
time_col <- if ('time' %in% names(tab)) 'time' else if ('timepoint' %in% names(tab)) 'timepoint' else if ('hour' %in% names(tab)) 'hour' else NA
out <- data.frame(sample = tab[[sample_col]], condition = tab[[condition_col]], stringsAsFactors = FALSE)
if (!is.na(time_col)) {
out$time <- tab[[time_col]]
}
out <- out[out$sample %in% sample_names, , drop = FALSE]
missing_samples <- setdiff(sample_names, out$sample)
if (length(missing_samples) > 0) {
missing_df <- data.frame(sample = missing_samples, condition = 'unknown', stringsAsFactors = FALSE)
if ('time' %in% names(out)) {
missing_df$time <- NA
}
out <- rbind(out, missing_df)
}
out <- out[match(sample_names, out$sample), , drop = FALSE]
rownames(out) <- out$sample
out$condition <- factor(out$condition)
out
}
save_plot <- function(plot, base, width = 8, height = 5) {
png_file <- paste0(base, '.png')
pdf_file <- paste0(base, '.pdf')
ggsave(png_file, plot, width = width, height = height, dpi = 300)
ggsave(pdf_file, plot, width = width, height = height)
c(png_file, pdf_file)
}
save_pheatmap <- function(ph, base, width = 8, height = 10) {
png_file <- paste0(base, '.png')
pdf_file <- paste0(base, '.pdf')
png(png_file, width = width * 300, height = height * 300, res = 300)
grid.newpage(); grid.draw(ph$gtable)
dev.off()
pdf(pdf_file, width = width, height = height)
grid.newpage(); grid.draw(ph$gtable)
dev.off()
c(png_file, pdf_file)
}
long_expr <- function(mat, label) {
data.frame(
gene_id = rep(rownames(mat), times = ncol(mat)),
sample = rep(colnames(mat), each = nrow(mat)),
value = as.numeric(mat),
matrix = label,
stringsAsFactors = FALSE
)
}
fpkm <- read_matrix(kv$fpkm)
tpm <- read_matrix(kv$tpm)
vst <- read_matrix(kv$vst)
common_samples <- Reduce(intersect, list(colnames(fpkm), colnames(tpm), colnames(vst)))
if (length(common_samples) < 2) stop('Fewer than 2 common samples across FPKM/TPM/VST matrices')
fpkm <- fpkm[, common_samples, drop = FALSE]
tpm <- tpm[, common_samples, drop = FALSE]
vst <- vst[, common_samples, drop = FALSE]
samples <- read_samples(kv$sample_table, common_samples)
annotation_col <- data.frame(condition = samples$condition)
rownames(annotation_col) <- samples$sample
artifacts <- data.frame(artifact = character(), path = character(), stringsAsFactors = FALSE)
add_artifacts <- function(names, paths) {
data.frame(artifact = names, path = paths, stringsAsFactors = FALSE)
}
tpm_source <- ifelse('tpm_source' %in% names(kv) && nzchar(kv$tpm_source), kv$tpm_source, 'unknown')
matrix_qc <- data.frame(
metric = c(
'fpkm_matrix', 'tpm_matrix', 'tpm_source', 'vst_matrix', 'sample_table',
'common_samples', 'genes_fpkm', 'genes_tpm', 'genes_vst',
'common_genes_fpkm_tpm', 'common_genes_tpm_vst'
),
value = c(
kv$fpkm, kv$tpm, tpm_source, kv$vst, kv$sample_table,
length(common_samples), nrow(fpkm), nrow(tpm), nrow(vst),
length(intersect(rownames(fpkm), rownames(tpm))),
length(intersect(rownames(tpm), rownames(vst)))
),
stringsAsFactors = FALSE
)
if ('matrix_bridge_qc' %in% names(kv) && file.exists(kv$matrix_bridge_qc)) {
bridge_qc <- read.table(kv$matrix_bridge_qc, header = TRUE, sep = '\t', stringsAsFactors = FALSE,
check.names = FALSE, comment.char = '', quote = '', fill = TRUE)
if (all(c('metric', 'value') %in% colnames(bridge_qc))) {
bridge_qc$metric <- paste0('bridge_', bridge_qc$metric)
matrix_qc <- rbind(matrix_qc, bridge_qc[, c('metric', 'value')])
}
}
matrix_qc_file <- file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.matrix_qc.tsv'))
write.table(matrix_qc, file = matrix_qc_file, sep = '\t', quote = FALSE, row.names = FALSE)
artifacts <- rbind(artifacts, add_artifacts('matrix_qc', matrix_qc_file))
# Full-matrix expression distribution plots. All genes are read and plotted as log2(x + 1).
for (entry in list(list(name = 'fpkm', mat = fpkm), list(name = 'tpm', mat = tpm))) {
label <- toupper(entry$name)
df <- long_expr(entry$mat, label)
df$log_value <- log2(df$value + 1)
df <- merge(df, data.frame(sample = samples$sample, condition = samples$condition), by = 'sample', all.x = TRUE)
p_box <- ggplot(df, aes(x = sample, y = log_value, fill = condition)) +
geom_boxplot(outlier.size = 0.15, linewidth = 0.25) +
labs(x = NULL, y = paste0('log2(', label, ' + 1)'), title = paste0(label, ' distribution per sample')) +
theme_bw(base_size = 10) +
theme(axis.text.x = element_text(angle = 45, hjust = 1), legend.position = 'right')
paths <- save_plot(p_box, file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', entry$name, '_boxplot')), 8.5, 5)
artifacts <- rbind(artifacts, add_artifacts(paste0(entry$name, '_boxplot_', c('png', 'pdf')), paths))
p_density <- ggplot(df, aes(x = log_value, color = sample, group = sample)) +
geom_density(linewidth = 0.5) +
labs(x = paste0('log2(', label, ' + 1)'), y = 'Density', title = paste0(label, ' density per sample')) +
theme_bw(base_size = 10) +
theme(legend.position = 'right')
paths <- save_plot(p_density, file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', entry$name, '_density')), 8.5, 5)
artifacts <- rbind(artifacts, add_artifacts(paste0(entry$name, '_density_', c('png', 'pdf')), paths))
}
# Top variable genes heatmap from VST matrix.
max_top <- as.integer(ifelse('top_n' %in% names(kv), kv$top_n, 100))
vars <- apply(vst, 1, var, na.rm = TRUE)
vars <- sort(vars[is.finite(vars)], decreasing = TRUE)
top_var_genes <- names(vars)[seq_len(min(max_top, length(vars)))]
top_var_mat <- vst[top_var_genes, , drop = FALSE]
write.table(data.frame(gene_id = top_var_genes, variance = vars[top_var_genes]),
file = file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.top_variable_genes.tsv')),
sep = '\t', quote = FALSE, row.names = FALSE)
ph <- pheatmap(top_var_mat, scale = 'row', annotation_col = annotation_col,
show_rownames = length(top_var_genes) <= 100,
main = paste0('Top ', length(top_var_genes), ' variable genes (VST)'),
silent = TRUE)
paths <- save_pheatmap(ph, file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.top_variable_vst_heatmap')), 8, 10)
artifacts <- rbind(artifacts, add_artifacts(paste0('top_variable_vst_heatmap_', c('png', 'pdf')), paths))
artifacts <- rbind(artifacts, add_artifacts('top_variable_gene_table', file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.top_variable_genes.tsv'))))
# Top DE genes heatmaps from all full DESeq2 result tables.
de_files <- list.files(kv$de_dir, pattern = paste0('^', kv$prefix, '\\.?rnaseq_de\\..*\\.DE_results\\.txt$'), full.names = TRUE)
if (length(de_files) == 0) {
de_files <- list.files(kv$de_dir, pattern = '\\.DE_results\\.txt$', full.names = TRUE)
}
summary_rows <- list()
consistency_rows <- list()
for (de_file in sort(de_files)) {
de <- read.table(de_file, header = TRUE, sep = '\t', stringsAsFactors = FALSE, check.names = FALSE,
comment.char = '', quote = '')
required_cols <- c('gene_id', 'padj', 'log2FoldChange')
if (!all(required_cols %in% colnames(de))) next
de <- de[!is.na(de$padj) & !is.na(de$log2FoldChange), , drop = FALSE]
de$abs_lfc <- abs(de$log2FoldChange)
sig <- de[de$padj < 0.05 & de$abs_lfc >= 1, , drop = FALSE]
sig <- sig[order(sig$padj, -sig$abs_lfc), , drop = FALSE]
genes <- sig$gene_id[sig$gene_id %in% rownames(vst)]
genes <- unique(genes)[seq_len(min(max_top, length(unique(genes))))]
comparison <- sub(paste0('^', kv$prefix, '\\.?rnaseq_de\\.'), '', basename(de_file))
comparison <- sub('\\.DE_results\\.txt$', '', comparison)
comparison <- gsub('[^A-Za-z0-9_]+', '_', comparison)
sig_gene_ids <- unique(sig$gene_id)
sig_in_tpm <- sig[sig$gene_id %in% rownames(tpm), , drop = FALSE]
concordance_pct <- NA_real_
comparison_parts <- strsplit(comparison, '_vs_', fixed = TRUE)[[1]]
if (length(comparison_parts) == 2 && nrow(sig_in_tpm) > 0) {
left_samples <- samples$sample[as.character(samples$condition) == comparison_parts[1]]
right_samples <- samples$sample[as.character(samples$condition) == comparison_parts[2]]
left_samples <- intersect(left_samples, colnames(tpm))
right_samples <- intersect(right_samples, colnames(tpm))
if (length(left_samples) > 0 && length(right_samples) > 0) {
mean_delta <- rowMeans(tpm[sig_in_tpm$gene_id, left_samples, drop = FALSE], na.rm = TRUE) -
rowMeans(tpm[sig_in_tpm$gene_id, right_samples, drop = FALSE], na.rm = TRUE)
concordant <- sign(mean_delta) == sign(sig_in_tpm$log2FoldChange)
concordance_pct <- round(sum(concordant, na.rm = TRUE) / length(concordant) * 100, 3)
}
}
consistency_rows[[comparison]] <- data.frame(
comparison = comparison,
tpm_source = tpm_source,
significant_genes = length(sig_gene_ids),
significant_genes_in_tpm = sum(sig_gene_ids %in% rownames(tpm)),
missing_de_genes_in_tpm = sum(!sig_gene_ids %in% rownames(tpm)),
significant_gene_tpm_overlap_pct = ifelse(length(sig_gene_ids) > 0, round(sum(sig_gene_ids %in% rownames(tpm)) / length(sig_gene_ids) * 100, 3), NA),
tpm_log2fc_direction_concordance_pct = concordance_pct,
stringsAsFactors = FALSE
)
summary_rows[[comparison]] <- data.frame(comparison = comparison, significant_genes = nrow(sig), plotted_genes = length(genes))
if (length(genes) == 0) next
out_table <- file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', comparison, '.top_de_genes.tsv'))
out_cols <- intersect(c('gene_id', 'baseMean', 'log2FoldChange', 'padj', 'regulation'), colnames(sig))
write.table(sig[sig$gene_id %in% genes, out_cols, drop = FALSE],
file = out_table, sep = '\t', quote = FALSE, row.names = FALSE)
ph <- pheatmap(vst[genes, , drop = FALSE], scale = 'row', annotation_col = annotation_col,
show_rownames = length(genes) <= 100,
main = paste0(comparison, ': top ', length(genes), ' DE genes (VST)'),
silent = TRUE)
paths <- save_pheatmap(ph, file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', comparison, '.top_de_vst_heatmap')), 8, 10)
artifacts <- rbind(artifacts, add_artifacts(paste0(comparison, '_top_de_vst_heatmap_', c('png', 'pdf')), paths))
artifacts <- rbind(artifacts, add_artifacts(paste0(comparison, '_top_de_gene_table'), out_table))
}
if (length(consistency_rows) > 0) {
consistency_df <- do.call(rbind, consistency_rows)
consistency_file <- file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.de_tpm_consistency.tsv'))
write.table(consistency_df, file = consistency_file, sep = '\t', quote = FALSE, row.names = FALSE)
artifacts <- rbind(artifacts, add_artifacts('de_tpm_consistency', consistency_file))
}
if (length(summary_rows) > 0) {
summary_df <- do.call(rbind, summary_rows)
write.table(summary_df, file = file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.de_heatmap_summary.tsv')),
sep = '\t', quote = FALSE, row.names = FALSE)
artifacts <- rbind(artifacts, add_artifacts('de_heatmap_summary', file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.de_heatmap_summary.tsv'))))
}
# Time-course normalized count trajectories. This follows the DESeq2 plotCounts
# idea: estimate size factors from raw counts, then plot normalized counts for
# genes selected from the time LRT result. It is only enabled when count_matrix
# is provided and sample conditions contain numeric time labels such as T0/T12/T24.
if ('count_matrix' %in% names(kv) && file.exists(kv$count_matrix)) {
if ('time' %in% names(samples)) {
time_values <- suppressWarnings(as.numeric(as.character(samples$time)))
} else {
time_values <- suppressWarnings(as.numeric(gsub('[^0-9.+-]', '', as.character(samples$condition))))
}
has_time <- all(is.finite(time_values)) && length(unique(time_values)) >= 3
time_lrt_files <- list.files(kv$de_dir, pattern = paste0('^', kv$prefix, '\\.?rnaseq_de\\.time_lrt\\.DE_results\\.txt$'), full.names = TRUE)
if (length(time_lrt_files) == 0) {
time_lrt_files <- list.files(kv$de_dir, pattern = 'time_lrt\\.DE_results\\.txt$', full.names = TRUE)
}
if (has_time && length(time_lrt_files) > 0) {
suppressPackageStartupMessages(library(DESeq2))
count_mat <- read_matrix(kv$count_matrix)
count_mat <- round(count_mat[, common_samples, drop = FALSE])
count_mat[count_mat < 0 | is.na(count_mat)] <- 0
coldata <- data.frame(row.names = common_samples, condition = samples$condition, time = time_values)
dds_counts <- DESeqDataSetFromMatrix(countData = count_mat, colData = coldata, design = ~ condition)
dds_counts <- estimateSizeFactors(dds_counts)
norm_counts <- counts(dds_counts, normalized = TRUE)
time_lrt <- read.table(time_lrt_files[1], header = TRUE, sep = '\t', stringsAsFactors = FALSE,
check.names = FALSE, comment.char = '', quote = '')
if (all(c('gene_id', 'padj') %in% colnames(time_lrt))) {
time_lrt <- time_lrt[!is.na(time_lrt$padj), , drop = FALSE]
time_lrt <- time_lrt[order(time_lrt$padj, -time_lrt$baseMean), , drop = FALSE]
max_time_genes <- as.integer(ifelse('time_top_n' %in% names(kv), kv$time_top_n, 12))
time_genes <- unique(time_lrt$gene_id[time_lrt$gene_id %in% rownames(norm_counts)])
time_genes <- time_genes[seq_len(min(max_time_genes, length(time_genes)))]
if (length(time_genes) > 0) {
norm_sub <- norm_counts[time_genes, , drop = FALSE]
norm_long <- data.frame(
gene_id = rep(rownames(norm_sub), times = ncol(norm_sub)),
sample = rep(colnames(norm_sub), each = nrow(norm_sub)),
normalized_count = as.numeric(norm_sub),
stringsAsFactors = FALSE
)
norm_long <- merge(norm_long, data.frame(sample = samples$sample, condition = samples$condition, time = time_values),
by = 'sample', all.x = TRUE)
norm_long$condition <- factor(norm_long$condition, levels = unique(samples$condition[order(time_values)]))
time_gene_table <- time_lrt[match(time_genes, time_lrt$gene_id), , drop = FALSE]
write.table(time_gene_table,
file = file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.time_lrt.top_timecourse_genes.tsv')),
sep = '\t', quote = FALSE, row.names = FALSE)
write.table(norm_long,
file = file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.time_lrt.normalized_counts_long.tsv')),
sep = '\t', quote = FALSE, row.names = FALSE)
p_time <- ggplot(norm_long, aes(x = time, y = normalized_count, color = condition)) +
geom_point(size = 1.8, alpha = 0.85) +
stat_summary(aes(group = gene_id), fun = mean, geom = 'line', color = 'grey25', linewidth = 0.45) +
scale_y_log10() +
facet_wrap(~ gene_id, scales = 'free_y', ncol = 4) +
labs(x = 'Time', y = 'Normalized counts (DESeq2 size-factor scaled, log10 axis)',
title = paste0('Top ', length(time_genes), ' time-LRT genes across time')) +
theme_bw(base_size = 10) +
theme(legend.position = 'none', strip.text = element_text(size = 7))
paths <- save_plot(p_time, file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.time_lrt.normalized_count_trajectories')), 10, 8)
artifacts <- rbind(artifacts, add_artifacts(paste0('time_lrt_normalized_count_trajectories_', c('png', 'pdf')), paths))
artifacts <- rbind(artifacts, add_artifacts('time_lrt_top_timecourse_gene_table', file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.time_lrt.top_timecourse_genes.tsv'))))
artifacts <- rbind(artifacts, add_artifacts('time_lrt_normalized_counts_long', file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.time_lrt.normalized_counts_long.tsv'))))
}
}
}
}
# Optional gene-family member expression plots. This branch is enabled by
# gene_family_members=*.final_members.id and is separate from the global plots.
if ('gene_family_members' %in% names(kv) && file.exists(kv$gene_family_members)) {
family_ids_raw <- read.table(kv$gene_family_members, header = FALSE, sep = '\t', stringsAsFactors = FALSE,
comment.char = '', quote = '', fill = TRUE)
family_ids <- unique(as.character(family_ids_raw[[1]]))
family_ids <- family_ids[nzchar(family_ids)]
family_label <- ifelse('family_label' %in% names(kv) && nzchar(kv$family_label), kv$family_label, 'family')
family_label_safe <- gsub('[^A-Za-z0-9_.-]+', '_', family_label)
family_genes <- unique(family_ids[family_ids %in% Reduce(union, list(rownames(fpkm), rownames(tpm), rownames(vst)))])
family_summary <- data.frame(
requested_family_genes = length(family_ids),
matched_fpkm = sum(family_ids %in% rownames(fpkm)),
matched_tpm = sum(family_ids %in% rownames(tpm)),
matched_vst = sum(family_ids %in% rownames(vst)),
stringsAsFactors = FALSE
)
write.table(family_summary,
file = file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', family_label_safe, '.family_summary.tsv')),
sep = '\t', quote = FALSE, row.names = FALSE)
artifacts <- rbind(artifacts, add_artifacts('family_summary', file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', family_label_safe, '.family_summary.tsv'))))
family_tables <- list()
for (entry in list(list(name = 'fpkm', mat = fpkm), list(name = 'tpm', mat = tpm), list(name = 'vst', mat = vst))) {
genes_here <- unique(family_ids[family_ids %in% rownames(entry$mat)])
if (length(genes_here) == 0) next
mat_here <- entry$mat[genes_here, , drop = FALSE]
if (entry$name %in% c('fpkm', 'tpm')) {
heat_mat <- log2(mat_here + 1)
heat_title <- paste0(family_label, ' ', toupper(entry$name), ' log2(value + 1)')
} else {
heat_mat <- mat_here
heat_title <- paste0(family_label, ' VST expression')
}
family_tables[[entry$name]] <- data.frame(
gene_id = rep(rownames(mat_here), times = ncol(mat_here)),
sample = rep(colnames(mat_here), each = nrow(mat_here)),
matrix = toupper(entry$name),
value = as.numeric(mat_here),
stringsAsFactors = FALSE
)
scale_mode <- ifelse(nrow(heat_mat) >= 2, 'row', 'none')
ph <- pheatmap(heat_mat, scale = scale_mode, annotation_col = annotation_col,
cluster_rows = nrow(heat_mat) >= 2,
cluster_cols = ncol(heat_mat) >= 2,
show_rownames = TRUE,
main = heat_title,
silent = TRUE)
paths <- save_pheatmap(ph, file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', family_label_safe, '.family_', entry$name, '_heatmap')), 8, 8)
artifacts <- rbind(artifacts, add_artifacts(paste0('family_', entry$name, '_heatmap_', c('png', 'pdf')), paths))
}
if (length(family_tables) > 0) {
family_long <- do.call(rbind, family_tables)
family_long <- merge(family_long, data.frame(sample = samples$sample, condition = samples$condition), by = 'sample', all.x = TRUE)
write.table(family_long,
file = file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', family_label_safe, '.family_expression_long.tsv')),
sep = '\t', quote = FALSE, row.names = FALSE)
artifacts <- rbind(artifacts, add_artifacts('family_expression_long', file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', family_label_safe, '.family_expression_long.tsv'))))
fpkm_long <- family_long[family_long$matrix == 'FPKM', , drop = FALSE]
if (nrow(fpkm_long) > 0) {
fpkm_stat <- aggregate(value ~ gene_id + condition, data = fpkm_long,
FUN = function(x) c(mean = mean(x, na.rm = TRUE), sd = sd(x, na.rm = TRUE)))
fpkm_stat <- do.call(data.frame, fpkm_stat)
names(fpkm_stat) <- c('gene_id', 'condition', 'mean', 'sd')
fpkm_stat$sd[is.na(fpkm_stat$sd)] <- 0
p_family_bar <- ggplot(fpkm_stat, aes(x = condition, y = mean, fill = condition)) +
geom_col(width = 0.65) +
geom_errorbar(aes(ymin = pmax(0, mean - sd), ymax = mean + sd), width = 0.2, linewidth = 0.25) +
facet_wrap(~ gene_id, scales = 'free_y', ncol = 4) +
labs(x = NULL, y = 'FPKM mean +/- SD', title = paste0(family_label, ' family FPKM by condition')) +
theme_bw(base_size = 10) +
theme(legend.position = 'none', strip.text = element_text(size = 7), axis.text.x = element_text(angle = 45, hjust = 1))
paths <- save_plot(p_family_bar, file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', family_label_safe, '.family_fpkm_barplot')), 10, 8)
artifacts <- rbind(artifacts, add_artifacts(paste0('family_fpkm_barplot_', c('png', 'pdf')), paths))
}
}
if ('count_matrix' %in% names(kv) && file.exists(kv$count_matrix)) {
if ('time' %in% names(samples)) {
time_values_family <- suppressWarnings(as.numeric(as.character(samples$time)))
} else {
time_values_family <- suppressWarnings(as.numeric(gsub('[^0-9.+-]', '', as.character(samples$condition))))
}
has_time_family <- all(is.finite(time_values_family)) && length(unique(time_values_family)) >= 3
if (has_time_family) {
suppressPackageStartupMessages(library(DESeq2))
count_mat_family <- read_matrix(kv$count_matrix)
count_mat_family <- round(count_mat_family[, common_samples, drop = FALSE])
count_mat_family[count_mat_family < 0 | is.na(count_mat_family)] <- 0
coldata_family <- data.frame(row.names = common_samples, condition = samples$condition, time = time_values_family)
dds_family <- DESeqDataSetFromMatrix(countData = count_mat_family, colData = coldata_family, design = ~ condition)
dds_family <- estimateSizeFactors(dds_family)
norm_counts_family <- counts(dds_family, normalized = TRUE)
family_time_genes <- unique(family_ids[family_ids %in% rownames(norm_counts_family)])
if (length(family_time_genes) > 0) {
norm_sub_family <- norm_counts_family[family_time_genes, , drop = FALSE]
norm_family_long <- data.frame(
gene_id = rep(rownames(norm_sub_family), times = ncol(norm_sub_family)),
sample = rep(colnames(norm_sub_family), each = nrow(norm_sub_family)),
normalized_count = as.numeric(norm_sub_family),
stringsAsFactors = FALSE
)
norm_family_long <- merge(norm_family_long, data.frame(sample = samples$sample, condition = samples$condition, time = time_values_family), by = 'sample', all.x = TRUE)
write.table(norm_family_long,
file = file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', family_label_safe, '.family_normalized_counts_long.tsv')),
sep = '\t', quote = FALSE, row.names = FALSE)
p_family_time <- ggplot(norm_family_long, aes(x = time, y = normalized_count, color = condition)) +
geom_point(size = 1.8, alpha = 0.85) +
stat_summary(aes(group = gene_id), fun = mean, geom = 'line', color = 'grey25', linewidth = 0.45) +
scale_y_log10() +
facet_wrap(~ gene_id, scales = 'free_y', ncol = 4) +
labs(x = 'Time', y = 'Normalized counts (DESeq2 size-factor scaled, log10 axis)',
title = paste0(family_label, ' family normalized counts across time')) +
theme_bw(base_size = 10) +
theme(legend.position = 'none', strip.text = element_text(size = 7))
paths <- save_plot(p_family_time, file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', family_label_safe, '.family_normalized_count_trajectories')), 10, 8)
artifacts <- rbind(artifacts, add_artifacts(paste0('family_normalized_count_trajectories_', c('png', 'pdf')), paths))
artifacts <- rbind(artifacts, add_artifacts('family_normalized_counts_long', file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.', family_label_safe, '.family_normalized_counts_long.tsv'))))
}
}
}
}
manifest <- data.frame(
key = c('fpkm_matrix', 'tpm_matrix', 'tpm_source', 'vst_matrix', 'sample_table', 'de_results_count', 'samples', 'genes_fpkm', 'genes_tpm', 'genes_vst', 'top_n'),
value = c(kv$fpkm, kv$tpm, tpm_source, kv$vst, kv$sample_table, length(de_files), length(common_samples), nrow(fpkm), nrow(tpm), nrow(vst), max_top),
stringsAsFactors = FALSE
)
write.table(manifest, file = file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.manifest.tsv')),
sep = '\t', quote = FALSE, row.names = FALSE)
write.table(artifacts, file = file.path(kv$out_dir, paste0(kv$prefix, '.rnaseq_expression_plot.artifacts.tsv')),
sep = '\t', quote = FALSE, row.names = FALSE)
cat('rnaseq_expression_plot complete\n')
cat('Output directory:', kv$out_dir, '\n')
cat('Artifacts:', nrow(artifacts), '\n')