#' @title Generate Pathway Plotting Code

#' @param GSEAlens_res GseaRes object

#' @param contrast_id Character. Contrast ID

#' @param target_pathways Character vector. Pathway IDs

#' @param user_genes Character vector. Genes of interest

#' @param pathway_genes Character vector. Pathway genes (for coloring)

#' @param expr_type Character. Expression type

#' @return A character string containing executable R code

#' @export



#' @examples

#' # Generate plotting code for the top enriched pathway in a pre-computed task

#' data(precomputed_gseares, package = "GSEAlens")
#'
#' gsea_res <- precomputed_gseares

#' task <- extract_gsea_task(gsea_res, contrast_id = "untrt_vs_trt")

#' res_df <- as.data.frame(task$gsea_res)

#' top_pathway <- res_df$ID[1]

#'

#' code <- generate_pathway_plot_code(

#'   GSEAlens_res   = gsea_res,

#'   contrast_id    = "untrt_vs_trt",

#'   target_pathways = top_pathway

#' )

#' cat(code)

generate_pathway_plot_code <- function(GSEAlens_res,

                                                                                contrast_id,

                                                                                target_pathways,

                                                                                user_genes = character(0),

                                                                                pathway_genes = character(0),

                                                                                expr_type = "logcpm") {

    # Extract necessary information

    GSEAlens_task <- tryCatch(

    {

            GSEAlens::extract_gsea_task(GSEAlens_res, contrast_id)

    },

    error = function(e) {

            stop("Failed to extract task for contrast '", contrast_id, "': ", e$message)

    }

    )



    left_group <- GSEAlens_task$meta$left_group

    right_group <- GSEAlens_task$meta$right_group



    # Color palette

    colors <- c(

    "#E41A1C", "#377EB8", "#4DAF4A", "#984EA3",

    "#FF7F00", "#A65628", "#F781BF", "#999999"

    )

    color_str <- paste(colors[seq_len(min(4, length(target_pathways)))], collapse = '", "')



    # Format pathway vector

    pathways_str <- paste(sprintf('    "%s"', target_pathways), collapse = ",\n")



    # Format user genes vector (no newline)

    if (length(user_genes) > 0) {

    genes_str <- paste0('GSEAlens_genes <- c("', paste(user_genes, collapse = '", "'), '")')

    } else {

    genes_str <- "GSEAlens_genes <- character(0)  # No genes of interest specified"

    }



    # Format pathway genes vector (no newline)

    if (length(pathway_genes) > 0) {

    pathway_str <- paste0('GSEAlens_pathway_genes <- c("', paste(pathway_genes, collapse = '", "'), '")')

    } else {

    pathway_str <- "GSEAlens_pathway_genes <- character(0)  # No pathway genes specified"

    }



    # Build code using paste0

    code <- paste0(

    '# =============================================================================

# GSEAlens Static Plotting Code

# Generated by GSEAlens Shiny App

# =============================================================================



# Step 1: Load your GseaRes object

GSEAlens_res <- YOUR_GSEA_RES_OBJECT



# Step 2: Extract task for the specified contrast

GSEAlens_task <- GSEAlens::extract_gsea_task(GSEAlens_res, contrast_id = "', contrast_id, '")

GSEAlens_left_group <- "', left_group, '"

GSEAlens_right_group <- "', right_group, '"



# Step 3: Define pathways to plot

GSEAlens_pathways <- c(

', pathways_str, "

)



# Step 4: Define genes of interest (optional)

", genes_str, "



# Step 5: Define pathway genes (for coloring)

", pathway_str, '



# Step 6: Load required libraries


library(ggplot2)

library(patchwork)

library(ggrepel)



# -----------------------------------------------------------------------------

# 6.1 Combined GSEA Enrichment Plot

# -----------------------------------------------------------------------------

GSEAlens_combined_plot <- plot_directional_gsea(

    directional_gsea_obj = GSEAlens_task,

    target_pathways = GSEAlens_pathways,

    subPlot = 3,

    curveCol = c("', color_str, '"),

    main_title = paste0("Combined: ", length(GSEAlens_pathways), " Pathways")

)

print(GSEAlens_combined_plot)



# -----------------------------------------------------------------------------

# 6.2 DE Volcano Plot with ggrepel Labels (Six-Color Aesthetics)

# -----------------------------------------------------------------------------

GSEAlens_de_df <- GSEAlens::get_de_table(GSEAlens_res, contrast_id = "', contrast_id, '")



# Thresholds

GSEAlens_logfc_thresh <- 1

GSEAlens_pval_thresh <- 0.05



# Color definitions

GSEAlens_color_map <- c(

    "both" = "#9C27B0",    # Purple: User & Pathway

    "user" = "#4DAF4A",    # Green: User only

    "pathway" = "#FF9800",  # Orange: Pathway only

    "up" = "#E41A1C",      # Red: Up-regulated

    "down" = "#377EB8",     # Blue: Down-regulated

    "ns" = "#C0C0C0"       # Gray: Not significant

)



# Classify genes

GSEAlens_user_upper <- toupper(GSEAlens_genes)

GSEAlens_pathway_upper <- toupper(GSEAlens_pathway_genes)

GSEAlens_de_df$GSEAlens_is_user <- toupper(GSEAlens_de_df$gene_symbol) %in% GSEAlens_user_upper

GSEAlens_de_df$GSEAlens_is_pathway <- toupper(GSEAlens_de_df$gene_symbol) %in% GSEAlens_pathway_upper

GSEAlens_de_df$GSEAlens_is_significant <- abs(GSEAlens_de_df$logFC) > GSEAlens_logfc_thresh & GSEAlens_de_df$pvalue < GSEAlens_pval_thresh



# Assign category colors

GSEAlens_de_df$GSEAlens_category <- dplyr::case_when(

    GSEAlens_de_df$GSEAlens_is_user & GSEAlens_de_df$GSEAlens_is_pathway ~ "both",

    GSEAlens_de_df$GSEAlens_is_user ~ "user",

    GSEAlens_de_df$GSEAlens_is_pathway ~ "pathway",

    GSEAlens_de_df$GSEAlens_is_significant & GSEAlens_de_df$logFC > 0 ~ "up",

    GSEAlens_de_df$GSEAlens_is_significant & GSEAlens_de_df$logFC < 0 ~ "down",

    TRUE ~ "ns"

)



# Assign plot order (higher = on top layer)

GSEAlens_de_df$GSEAlens_plot_order <- dplyr::case_when(

    GSEAlens_de_df$GSEAlens_is_user & GSEAlens_de_df$GSEAlens_is_pathway ~ 5,  # both - top layer

    GSEAlens_de_df$GSEAlens_is_user ~ 4,  # user

    GSEAlens_de_df$GSEAlens_is_pathway ~ 3,  # pathway

    GSEAlens_de_df$GSEAlens_is_significant ~ 2,  # up/down

    TRUE ~ 1  # ns - bottom layer

)



# Sort by plot order (draw bottom layer first, top layer last)

GSEAlens_de_df <- GSEAlens_de_df[order(GSEAlens_de_df$GSEAlens_plot_order), ]



# Statistics

GSEAlens_n_up <- sum(GSEAlens_de_df$GSEAlens_is_significant & GSEAlens_de_df$logFC > 0, na.rm = TRUE)

GSEAlens_n_down <- sum(GSEAlens_de_df$GSEAlens_is_significant & GSEAlens_de_df$logFC < 0, na.rm = TRUE)

GSEAlens_n_ns <- sum(!GSEAlens_de_df$GSEAlens_is_significant, na.rm = TRUE)

GSEAlens_n_user <- sum(GSEAlens_de_df$GSEAlens_is_user, na.rm = TRUE)

GSEAlens_n_pathway <- sum(GSEAlens_de_df$GSEAlens_is_pathway, na.rm = TRUE)

GSEAlens_n_both <- sum(GSEAlens_de_df$GSEAlens_is_user & GSEAlens_de_df$GSEAlens_is_pathway, na.rm = TRUE)



# Create volcano plot

GSEAlens_volcano_plot <- ggplot(GSEAlens_de_df, aes(x = logFC, y = -log10(pvalue))) +

    geom_point(aes(color = GSEAlens_category), alpha = 0.6, size = 3) +

    # Add ggrepel labels for both and user genes

    ggrepel::geom_text_repel(

    data = GSEAlens_de_df[GSEAlens_de_df$GSEAlens_is_user, ],

    aes(label = gene_symbol),

    size = 3,

    max.overlaps = 50,

    box.padding = 0.3,

    point.padding = 0.2,

    segment.alpha = 0.5,

    fontface = "bold",

    # Color based on is_pathway: both=purple, user=green

    color = ifelse(GSEAlens_de_df$GSEAlens_is_user[GSEAlens_de_df$GSEAlens_is_user] & GSEAlens_de_df$GSEAlens_is_pathway[GSEAlens_de_df$GSEAlens_is_user], "#9C27B0", "#4DAF4A")

    ) +

    scale_color_manual(

    values = GSEAlens_color_map,

    breaks = c("both", "user", "pathway", "up", "down", "ns"),

    labels = c("Both", "Selected", "Pathway", "Up", "Down", "NS")

    ) +

    labs(

    title = paste0(GSEAlens_left_group, " vs ", GSEAlens_right_group, " - DE Volcano"),

    subtitle = paste0(

    "Both: ", GSEAlens_n_both, " | Selected: ", GSEAlens_n_user, " | Pathway: ", GSEAlens_n_pathway, " | Up: ", GSEAlens_n_up, " | Down: ", GSEAlens_n_down, " | NS: ", GSEAlens_n_ns,

    "\n",

    tail(GSEAlens_pathways, 1)

),

    x = "log2 Fold Change",

    y = "-log10(P-value)"

    ) +

    theme_bw() +

    theme(

    legend.position = "bottom",

    legend.title = element_blank(),

    plot.title = element_text(hjust = 0.5),

    plot.subtitle = element_text(hjust = 0.5, color = "gray40", lineheight = 0.8)

    ) +

    geom_vline(xintercept = c(-GSEAlens_logfc_thresh, GSEAlens_logfc_thresh),

                            linetype = "dashed", color = "gray50", alpha = 0.7) +

    geom_hline(yintercept = -log10(GSEAlens_pval_thresh),

                            linetype = "dashed", color = "gray50", alpha = 0.7)



print(GSEAlens_volcano_plot)



# -----------------------------------------------------------------------------

# 6.3 Save plots to file (optional)

# --- Save to file (uncomment one) --------------------------------------------
# ggsave("GSEAlens_combined_plot.pdf", GSEAlens_combined_plot, width = 12, height = 8, device = cairo_pdf)
# ggsave("GSEAlens_combined_plot.png", GSEAlens_combined_plot, width = 12, height = 8, dpi = 300)
# ggsave("GSEAlens_combined_plot.svg", GSEAlens_combined_plot, width = 12, height = 8)
# ggsave("GSEAlens_volcano_plot.pdf", GSEAlens_volcano_plot, width = 10, height = 7, device = cairo_pdf)
# ggsave("GSEAlens_volcano_plot.png", GSEAlens_volcano_plot, width = 10, height = 7, dpi = 300)
# ggsave("GSEAlens_volcano_plot.svg", GSEAlens_volcano_plot, width = 10, height = 7)

'

    )



    return(code)

}





#' @title Generate Joint Canvas Code

#' @param GSEAlens_res GseaRes object

#' @param contrast_ids Character vector. Multiple contrast IDs

#' @param target_pathways Character vector. Pathway IDs

#' @param ncol Integer. Number of columns

#' @return A character string containing executable R code

#' @export



#' @examples
#' \donttest{
#' data(precomputed_gseares, package = "GSEAlens")
#' task <- extract_gsea_task(precomputed_gseares, contrast_id = "untrt_vs_trt")
#' target <- as.data.frame(task$gsea_res)$ID[1]
#' code <- generate_joint_canvas_code(GSEAlens_res = precomputed_gseares,
#'                                    contrast_ids = "untrt_vs_trt",
#'                                    target_pathways = target)
#' cat(code)
#' }
generate_joint_canvas_code <- function(GSEAlens_res,

                                                                                contrast_ids,

                                                                                target_pathways,

                                                                                ncol = 2) {

    # Format contrast IDs

    contrasts_str <- paste(sprintf('    "%s"', contrast_ids), collapse = ",\n")



    # Format pathways

    pathways_str <- paste(sprintf('    "%s"', target_pathways), collapse = ",\n")



    # Color palette

    colors <- c(

    "#E41A1C", "#377EB8", "#4DAF4A", "#984EA3",

    "#FF7F00", "#A65628", "#F781BF", "#999999"

    )

    color_str <- paste(colors[seq_len(min(4, length(target_pathways)))], collapse = '", "')



    # Build code

    code <- paste0(

    "# =============================================================================

# GSEAlens Joint Canvas Code

# Generated by GSEAlens Shiny App

# =============================================================================



# Step 1: Load your GseaRes object

GSEAlens_res <- YOUR_GSEA_RES_OBJECT



# Step 2: Define parameters

GSEAlens_contrasts <- c(

", contrasts_str, "

)



GSEAlens_pathways <- c(

", pathways_str, "

)



GSEAlens_ncol <- ", ncol, '



# Step 3: Load required libraries


library(ggplot2)

library(patchwork)



# Step 4: Generate plots for each contrast

GSEAlens_plot_list <- list()



for (GSEAlens_i in seq_along(GSEAlens_contrasts)) {

    GSEAlens_contrast_id <- GSEAlens_contrasts[GSEAlens_i]



    GSEAlens_task <- GSEAlens::extract_gsea_task(GSEAlens_res, contrast_id = GSEAlens_contrast_id)



    GSEAlens_p <- plot_directional_gsea(

    directional_gsea_obj = GSEAlens_task,

    target_pathways = GSEAlens_pathways,

    subPlot = 3,

    curveCol = c("', color_str, '"),

    main_title = paste0(GSEAlens_contrast_id, " [", length(GSEAlens_pathways), " pathways]"),

    add_pval = FALSE,

    show_contrast_in_axis = TRUE

    )



    GSEAlens_plot_list[[GSEAlens_i]] <- GSEAlens_p

}



# Step 5: Combine plots using patchwork

GSEAlens_combined <- patchwork::wrap_plots(

    GSEAlens_plot_list,

    ncol = GSEAlens_ncol,

    nrow = ceiling(length(GSEAlens_plot_list) / GSEAlens_ncol),

    byrow = TRUE,

    guides = "collect"

) + patchwork::plot_annotation(

    title = paste0("Joint GSEA Canvas: ", length(GSEAlens_contrasts), " contrasts x ", length(GSEAlens_pathways), " pathways"),

    subtitle = paste0("Arrangement: ", paste(GSEAlens_contrasts, collapse = " -> ")),

    theme = theme(

    plot.title = element_text(size = 14, face = "bold", hjust = 0.5),

    plot.subtitle = element_text(size = 10, color = "gray50", hjust = 0.5)

    )

)



# Step 6: Wrap the canvas so outer margin (canvas breathing room) does NOT

# leak into the per-contrast 3-panel GSEA plot. Setting plot.margin on the

# bare canvas via the and-operator would override the per-panel margin = 0

# configured in plot_directional_gsea and .gsea_nb_core, re-inflating the

# gaps between Panel 1 (RES curve), Panel 2 (hit strips) and Panel 3

# (ranked list). wrap_elements(full = ...) keeps the title/subtitle

# while confining the outer margin to the canvas border.

GSEAlens_combined <- patchwork::wrap_elements(full = GSEAlens_combined) +

    theme(plot.margin = margin(10, 10, 10, 10))



# Step 7: Display combined plot

print(GSEAlens_combined)



# Step 8: Save to file (uncomment one)

# ggsave("GSEAlens_joint_canvas.pdf", GSEAlens_combined, width = 14, height = 10, device = cairo_pdf)

# ggsave("GSEAlens_joint_canvas.png", GSEAlens_combined, width = 14, height = 10, dpi = 300)

# ggsave("GSEAlens_joint_canvas.svg", GSEAlens_combined, width = 14, height = 10)

'

    )



    return(code)
}






#' @title Generate Combined Pathway Plotting Code
#' @description Return an executable R script reproducing the Combined Pathway
#'   Plotting module (mod_multi_plot / module 12) as a static figure. Mirrors
#'   the interactive \code{plot_directional_gsea()} call the app performs when
#'   the user selects pathways in the main table's "Combined Display" column.
#' @param gsea_res_var Character. Name of the GseaRes variable the generated
#'   code should reference (default \code{"gsea_res"}). The user replaces the
#'   placeholder assignment with their own object.
#' @param contrast_id Character. Contrast ID passed to
#'   \code{extract_gsea_task()}.
#' @param target_pathways Character vector. Pathway IDs to plot.
#' @param subPlot Integer (1-3). \code{plot_directional_gsea} subPlot argument
#'   (1 = running score, 2 = rank position, 3 = both). Default 3.
#' @param curve_colors Character vector of color hex codes for pathway curves.
#'   If NULL, falls back to a sensible default palette.
#' @param left_group Character. Left group label for axis annotation.
#' @param right_group Character. Right group label.
#' @return Character scalar of executable R code.
#' @examples
#' code <- generate_combined_plot_code(
#'   contrast_id    = "untrt_vs_trt",
#'   target_pathways = c("HALLMARK_TNFA_SIGNALING_VIA_NFKB")
#' )
#' cat(code)
#' @export
generate_combined_plot_code <- function(gsea_res_var = "gsea_res",
                                                                                    contrast_id,
                                                                                    target_pathways,
                                                                                    subPlot = 3,
                                                                                    curve_colors = NULL,
                                                                                    left_group = "Left",
                                                                                    right_group = "Right") {
    if (length(target_pathways) == 0) {
    return("# No pathways selected; nothing to plot.")
    }

    pathways_str <- paste(sprintf('    "%s"', target_pathways), collapse = ",\n")

    if (is.null(curve_colors) || length(curve_colors) == 0) {
    curve_colors <- c("#E41A1C", "#377EB8", "#4DAF4A", "#984EA3",
                                            "#FF7F00", "#A65628", "#F781BF", "#999999")
    }
    color_str <- paste(curve_colors, collapse = '", "')

    sprintf(
'# =============================================================================
# GSEAlens Combined Pathway Plotting Code
# Generated by GSEAlens Shiny App
# =============================================================================

# Step 1: Load your GseaRes object
%s <- YOUR_GSEA_RES_OBJECT

# Step 2: Define parameters
GSEAlens_contrast_id <- "%s"
GSEAlens_pathways <- c(
%s
)
GSEAlens_subPlot   <- %d
GSEAlens_left_grp  <- "%s"
GSEAlens_right_grp <- "%s"

# Step 3: Load required libraries
library(ggplot2)

# Step 4: Extract the task for the specified contrast
# NOTE: target_collection = "ALL" merges cross-collection results when applicable.
GSEAlens_task <- GSEAlens::extract_gsea_task(%s, contrast_id = GSEAlens_contrast_id, target_collection = "ALL")

# Step 5: Build the combined directional GSEA plot
GSEAlens_p <- plot_directional_gsea(
    directional_gsea_obj = GSEAlens_task,
    target_pathways = GSEAlens_pathways,
    subPlot = GSEAlens_subPlot,
    curveCol = c("%s")
)

# Step 6: Display
print(GSEAlens_p)

# Step 7: Save to file (uncomment one)
# ggsave("GSEAlens_combined_plot.pdf", GSEAlens_p, width = 12, height = 10, device = cairo_pdf)
# ggsave("GSEAlens_combined_plot.png", GSEAlens_p, width = 12, height = 10, dpi = 300)
# ggsave("GSEAlens_combined_plot.svg", GSEAlens_p, width = 12, height = 10)
',
    gsea_res_var,
    contrast_id,
    pathways_str,
    as.integer(subPlot),
    left_group,
    right_group,
    gsea_res_var,
    color_str
    )
}




#' @title Generate Boxplot Expression-Data Extraction Code

#' @description Produce a self-contained, reproducible R script that extracts

#'   the per-sample expression values (Sample / Group / Expression) of a

#'   single gene from a \code{GseaRes} object. The returned data frame

#'   matches exactly what is shown in the Quadrant module's expression

#'   boxplot (panel 4), so the user can paste it into other software

#'   (e.g. GraphPad Prism, Excel, R) for downstream plotting.

#' @param GSEAlens_res A \code{GseaRes} object.

#' @param contrast_id Character. Contrast ID (e.g. "Treat_vs_Control").

#' @param gene_symbol Character. Gene symbol as shown in the boxplot title.

#' @param expr_type Character. Expression matrix type passed to

#'   \code{get_expr_matrix()} (e.g. "logcpm", "vst", "count"). Default "logcpm".

#' @param custom_order Character or NULL. Custom group ordering string

#'   (e.g. "GroupA -> GroupB") or "default" / NULL to use natural order.

#'   When non-default, the generated code will re-order the \code{Group}

#'   factor accordingly.

#' @return A character string of executable R code.

#' @export

#'
#' @examples
#' \donttest{
#' data(precomputed_gseares, package = "GSEAlens")
#' code <- generate_boxplot_data_code(
#'   GSEAlens_res = precomputed_gseares,
#'   contrast_id  = "untrt_vs_trt",
#'   gene_symbol  = "GAPDH",
#'   expr_type    = "logcpm"
#' )
#' cat(code)
#' }
generate_boxplot_data_code <- function(GSEAlens_res,

                                                                                contrast_id,

                                                                                gene_symbol,

                                                                                expr_type = "logcpm",

                                                                                custom_order = NULL) {

    # ---- 1. Pull side-info from the live GseaRes (so the generated code is

    #         informative even if the user just copy-pastes it). ----

    # Prefer GSEAlens::extract_gsea_task() (same source as generate_pathway_plot_code)

    # for consistency; fall back to a heuristic split of the contrast ID.

    left_group  <- NA_character_

    right_group <- NA_character_

    gtask <- tryCatch(GSEAlens::extract_gsea_task(GSEAlens_res, contrast_id), error = function(e) NULL)

    if (!is.null(gtask) && !is.null(gtask$meta)) {

    left_group  <- gtask$meta$left_group  %||% NA_character_

    right_group <- gtask$meta$right_group %||% NA_character_

    }

    if (is.na(left_group) || is.na(right_group)) {

    if (grepl("_vs_", contrast_id, fixed = TRUE)) {

            parts <- strsplit(contrast_id, "_vs_", fixed = TRUE)[[1]]

            left_group  <- parts[1]

            right_group <- parts[2]

    }

    }



    # ---- 2. Determine whether to include the optional ordering block. ----

    has_custom_order <- !is.null(custom_order) &&

                                            length(custom_order) == 1 &&

                                            nzchar(custom_order) &&

                                            !identical(custom_order, "default")



    # ---- 3. Build the R script as a single string. ----

    # Use single quotes around user-provided strings; escape any embedded '.

    safe_gene <- gsub("'", "''", gene_symbol %||% "")

    safe_contrast <- gsub("'", "''", contrast_id %||% "")



    ordering_block <- if (has_custom_order) {

    safe_order <- gsub("'", "''", custom_order)

    paste0(

            "\n",

            "# Optional: apply a custom group ordering (matches the Shiny boxplot)\n",

            "GSEAlens_custom_order <- '", safe_order, "'\n",

            "if (!is.null(GSEAlens_custom_order) &&\n",

            "    nzchar(GSEAlens_custom_order) &&\n",

            "    !identical(GSEAlens_custom_order, 'default')) {\n",

            "  GSEAlens_sep <- if (grepl('->', GSEAlens_custom_order, fixed = TRUE)) '->' else ','\n",

            "  GSEAlens_order_parts <- trimws(strsplit(GSEAlens_custom_order, GSEAlens_sep)[[1]])\n",

            "  GSEAlens_actual_groups <- unique(as.character(GSEAlens_box_data$Group))\n",

            "  GSEAlens_valid_parts <- GSEAlens_order_parts[GSEAlens_order_parts %in% GSEAlens_actual_groups]\n",

            "  GSEAlens_x_categories <- c(GSEAlens_valid_parts,\n",

            "                              setdiff(GSEAlens_actual_groups, GSEAlens_valid_parts))\n",

            "  GSEAlens_box_data <- GSEAlens_box_data[GSEAlens_box_data$Group %in% GSEAlens_x_categories, ]\n",

            "  GSEAlens_box_data$Group <- factor(GSEAlens_box_data$Group,\n",

            "                                     levels = GSEAlens_x_categories,\n",

            "                                     ordered = TRUE)\n",

            "} else {\n",

            "  GSEAlens_box_data$Group <- factor(GSEAlens_box_data$Group)\n",

            "}\n"

    )

    } else {

    paste0(

            "\n",

            "# Optional: order the Group factor (alphabetical by default)\n",

            "# GSEAlens_box_data$Group <- factor(GSEAlens_box_data$Group)\n"

    )

    }



    code <- paste0(

    '# =============================================================================

# GSEAlens Boxplot Data Extraction Code

# Generated by GSEAlens Shiny App

# =============================================================================

#

# What this script does

# ----------------------

# Extracts the per-sample expression values of a single gene from a GseaRes

# object and returns a tidy data frame (columns: Sample, Group, Expression)

# that exactly matches the values shown in the Quadrant module boxplot

# (panel 4 "Full Expression Distribution").

#

# You can then:

#   * write.csv(GSEAlens_box_data, "my_gene_box.csv", row.names = FALSE)

#   * copy GSEAlens_box_data to GraphPad Prism / Excel for further plotting

#   * ggplot2::ggplot(GSEAlens_box_data, aes(Group, Expression)) +

#       ggplot2::geom_boxplot() + ggplot2::geom_jitter(width = 0.2)



# Step 1: Load your GseaRes object

# --------------------------------

GSEAlens_res <- YOUR_GSEA_RES_OBJECT



GSEAlens_contrast_id <- "', safe_contrast, '"

GSEAlens_gene_symbol <- "', safe_gene, '"

GSEAlens_expr_type   <- "', expr_type, '"

', if (!is.na(left_group) && !is.na(right_group)) {

    paste0('GSEAlens_left_group  <- "', left_group, '"\n',

                    'GSEAlens_right_group <- "', right_group, '"\n')

} else {

    paste0('# (Left/right group labels could not be inferred from the GseaRes.)\n')

},



    '\n

# Step 2: Pull the expression matrix and sample metadata

# ------------------------------------------------------

GSEAlens_expr_mat    <- GSEAlens::get_expr_matrix(GSEAlens_res, type = GSEAlens_expr_type)

GSEAlens_sample_meta <- GSEAlens::get_sample_meta(GSEAlens_res)



# Step 3: Locate the gene (handles Ensembl IDs, SYMBOL lookup, case-insensitive)

# -------------------------------------------------------------------------------

GSEAlens_target_upper   <- toupper(GSEAlens_gene_symbol)

GSEAlens_rownames_upper <- toupper(rownames(GSEAlens_expr_mat))

GSEAlens_match_idx <- which(GSEAlens_rownames_upper == GSEAlens_target_upper)



if (length(GSEAlens_match_idx) == 0 &&

    !is.null(GSEAlens_res$expr_bundle$gene_meta)) {

    GSEAlens_gene_meta <- GSEAlens_res$expr_bundle$gene_meta

    if (is.null(rownames(GSEAlens_gene_meta))) {

    rownames(GSEAlens_gene_meta) <- rownames(GSEAlens_expr_mat)

    }

    GSEAlens_symbol_col <- intersect(

    c("SYMBOL", "symbol", "Gene", "gene_name", "gene_symbol"),

    colnames(GSEAlens_gene_meta)

    )[1]

    if (!is.na(GSEAlens_symbol_col)) {

    GSEAlens_meta_upper <- toupper(as.character(GSEAlens_gene_meta[[GSEAlens_symbol_col]]))

    GSEAlens_meta_idx   <- which(GSEAlens_meta_upper == GSEAlens_target_upper)

    if (length(GSEAlens_meta_idx) > 0) {

            GSEAlens_ensembl_id <- rownames(GSEAlens_gene_meta)[GSEAlens_meta_idx[1]]

            GSEAlens_match_idx  <- which(rownames(GSEAlens_expr_mat) == GSEAlens_ensembl_id)

    }

    }

}



if (length(GSEAlens_match_idx) == 0) {

    stop(sprintf("Gene \'%s\' was not found in the expression matrix.", GSEAlens_gene_symbol))

}



# Step 4: Build the per-sample data frame

# ---------------------------------------

GSEAlens_actual_gene  <- rownames(GSEAlens_expr_mat)[GSEAlens_match_idx[1]]

GSEAlens_expr_values  <- GSEAlens_expr_mat[GSEAlens_actual_gene, ]

GSEAlens_sample_names <- names(GSEAlens_expr_values)

GSEAlens_group_info   <- GSEAlens_sample_meta$group[

    match(GSEAlens_sample_names, rownames(GSEAlens_sample_meta))

]



GSEAlens_box_data <- data.frame(

    Sample     = GSEAlens_sample_names,

    Group      = as.character(GSEAlens_group_info),

    Expression = as.numeric(GSEAlens_expr_values),

    stringsAsFactors = FALSE

)

GSEAlens_box_data <- GSEAlens_box_data[!is.na(GSEAlens_box_data$Group), ]

', ordering_block, '

# Step 5: Preview / save

# ----------------------

print(GSEAlens_box_data)



# Uncomment to save to CSV (Excel / GraphPad friendly):

# write.csv(GSEAlens_box_data, "GSEAlens_', safe_gene, '_', safe_contrast, '_box.csv", row.names = FALSE)



# Optional: make a boxplot with ggplot2 (matches the Shiny look-and-feel)

# GSEAlens_p <- ggplot2::ggplot(GSEAlens_box_data,

#                               ggplot2::aes(x = Group, y = Expression, fill = Group)) +

#   ggplot2::geom_boxplot(alpha = 0.7, outlier.shape = NA) +

#   ggplot2::geom_jitter(width = 0.2, size = 3, alpha = 0.6) +

#   ggplot2::theme_bw(base_size = 12) +

#   ggplot2::labs(title = GSEAlens_gene_symbol, y = GSEAlens_expr_type, x = NULL) +

#   ggplot2::theme(legend.position = "none",

#                  axis.text.x = ggplot2::element_text(angle = 45, hjust = 1))

# print(GSEAlens_p)

'

    )



    return(code)

}







#' @title Extract Boxplot Data (Long and Wide)

#' @description Internal helper used by the Quadrant module's "Export Boxplot

#'   Data" button. Computes the exact per-sample expression data frame that

#'   the Shiny boxplot (panel 4) is built on, and returns it in two formats:

#' \describe{

#'   \item{long}{Tidy data frame with columns \code{Sample}, \code{Group},

#'     \code{Expression}. One row per sample. Suitable for GraphPad Prism

#'     ("Grouped" data table) and Excel.}

#'   \item{wide}{A one-row data frame. The first column holds the gene

#'     symbol (column name \code{Gene}); the remaining columns hold the

#'     expression value of that gene in each sample (column names are the

#'     sample IDs, in the expression matrix's original column order).

#'     Suitable for direct copy-paste into Prism ("Column" data table) or

#'     any spreadsheet.}

#' }

#' @param GSEAlens_res A \code{GseaRes} object.

#' @param contrast_id Character. Contrast ID (e.g. \code{"Treat_vs_Control"}).

#' @param gene_symbol Character. Gene symbol as shown in the boxplot title.

#' @param expr_type Character. Expression matrix type passed to

#'   \code{get_expr_matrix()}. Default \code{"logcpm"}.

#' @return A named list with elements \code{long}, \code{wide}, \code{gene},

#'   \code{contrast_id}, \code{expr_type}, \code{left_group},

#'   \code{right_group}, \code{sample_order}.

#' @keywords internal

extract_boxplot_data <- function(GSEAlens_res,

                                                                    contrast_id,

                                                                    gene_symbol,

                                                                    expr_type = "logcpm") {

    # ---- 1. Pull side-info (left/right group labels, with safe fallback) ----

    left_group  <- NA_character_

    right_group <- NA_character_

    gtask <- tryCatch(GSEAlens::extract_gsea_task(GSEAlens_res, contrast_id),

                    error = function(e) NULL)

    if (!is.null(gtask) && !is.null(gtask$meta)) {

    left_group  <- gtask$meta$left_group  %||% NA_character_

    right_group <- gtask$meta$right_group %||% NA_character_

    }

    if (is.na(left_group) || is.na(right_group)) {

    if (grepl("_vs_", contrast_id, fixed = TRUE)) {

            parts <- strsplit(contrast_id, "_vs_", fixed = TRUE)[[1]]

            left_group  <- parts[1]

            right_group <- parts[2]

    }

    }



    # ---- 2. Expression matrix and sample metadata ----

    expr_mat    <- get_expr_matrix(GSEAlens_res, type = expr_type)

    sample_meta <- get_sample_meta(GSEAlens_res)



    if (is.null(expr_mat)) {

    stop(sprintf("Expression matrix (type = '%s') is not available.", expr_type))

    }

    if (is.null(sample_meta)) {

    stop("Sample metadata is not available in the GseaRes object.")

    }



    # ---- 3. Locate the gene (case-insensitive, with SYMBOL/Ensembl fallback) ----

    target_upper   <- toupper(gene_symbol)

    rownames_upper <- toupper(rownames(expr_mat))

    match_idx <- which(rownames_upper == target_upper)



    if (length(match_idx) == 0 && !is.null(GSEAlens_res$expr_bundle$gene_meta)) {

    gene_meta <- GSEAlens_res$expr_bundle$gene_meta

    if (is.null(rownames(gene_meta))) {

            rownames(gene_meta) <- rownames(expr_mat)

    }

    symbol_col <- intersect(

            c("SYMBOL", "symbol", "Gene", "gene_name", "gene_symbol"),

            colnames(gene_meta)

    )[1]

    if (!is.na(symbol_col)) {

            meta_upper <- toupper(as.character(gene_meta[[symbol_col]]))

            meta_idx   <- which(meta_upper == target_upper)

            if (length(meta_idx) > 0) {

        ensembl_id <- rownames(gene_meta)[meta_idx[1]]

        match_idx  <- which(rownames(expr_mat) == ensembl_id)

            }

    }

    }



    if (length(match_idx) == 0) {

    stop(sprintf("Gene '%s' was not found in the expression matrix.", gene_symbol))

    }



    # ---- 4. Build the per-sample data frame (preserves expr_mat column order) ----

    actual_gene  <- rownames(expr_mat)[match_idx[1]]

    expr_values  <- expr_mat[actual_gene, ]

    sample_names <- names(expr_values)

    group_info   <- sample_meta$group[match(sample_names, rownames(sample_meta))]



    long_df <- data.frame(

    Sample     = sample_names,

    Group      = as.character(group_info),

    Expression = as.numeric(expr_values),

    stringsAsFactors = FALSE

    )

    # Drop samples without a group assignment

    long_df <- long_df[!is.na(long_df$Group), , drop = FALSE]



    # ---- 5. Build the wide data frame: first row = gene, columns = samples ----

    wide_df <- data.frame(

    Gene = gene_symbol,

    as.list(setNames(long_df$Expression, long_df$Sample)),

    check.names = FALSE,

    stringsAsFactors = FALSE

    )



    # ---- 6. Return ----

    list(

    long         = long_df,

    wide         = wide_df,

    gene         = gene_symbol,

    contrast_id  = contrast_id,

    expr_type    = expr_type,

    left_group   = left_group,

    right_group  = right_group,

    sample_order = long_df$Sample

    )

}





# =============================================================================

# Publication-ready static plot code generators

# =============================================================================

# These functions return self-contained R script strings that re-create the

# interactive plotly/visNetwork figures as static ggplot2 graphics for

# publication. The returned code is displayed in the Shiny app's export modal

# and can be copy-pasted into an R session.

# All generators accept a `data.frame` slice (the exact subset shown in the

# interactive view) plus user-tunable style parameters, so the generated code

# is both reproducible and customizable.

# =============================================================================



#' @title Generate Publication DotPlot Code
#' @description Return an executable ggplot2 script reproducing the Pathway
#'   Relationship module's dotplot as a static, publication-ready figure.
#'   The generated script assumes a \code{GseaRes} object is already loaded
#'   in the R environment and derives plot data via \code{extract_gsea_task()}
#'   + \code{get_core_genes_list()}, producing concise reproducible code
#'   without embedding raw data.
#' @param gsea_res_var Character. Name of the GseaRes variable in the user's
#'   environment (default \code{"gsea_res"}).
#' @param contrast_id Character. Contrast ID to extract.
#' @param pathways Character vector. Pathway IDs to display.
#' @param color_mode Character. One of "padj", "pval", "nes".
#' @param size_mode  Character. One of "core_size", "setsize".
#' @param left_group Character. Left contrast label.
#' @param right_group Character. Right contrast label.
#' @param palette Character. Viridis option letter ("A"-"H").
#' @param point_range Numeric length 2. Min/max point size in mm.
#' @param target_collection Character. Gene set collection(s) to extract
#'   (e.g. \code{"H"}, \code{c("C2", "C2:CP")}). Default \code{"ALL"}.
#' @param base_size Numeric. theme_bw base font size.
#' @param margin_top Numeric. Top plot margin (mm).
#' @param margin_bottom Numeric. Bottom plot margin (mm).
#' @param margin_left Numeric. Left plot margin (mm).
#' @param margin_right Numeric. Right plot margin (mm).
#' @param fig_width Numeric. Figure width in inches for ggsave.
#' @param fig_height Numeric. Figure height in inches for ggsave.
#' @param fig_dpi Numeric. DPI for PNG raster output (default 300).
#' @return Character scalar of executable R code.
#' @examples
#' code <- generate_dotplot_code(
#'   gsea_res_var = "gsea_res", contrast_id = "untrt_vs_trt",
#'   pathways = c("HALLMARK_TNFA_SIGNALING_VIA_NFKB"))
#' cat(code)
#' @export
generate_dotplot_code <- function(gsea_res_var = "gsea_res",
                                                                    contrast_id,
                                                                    pathways,
                                                                    color_mode = "padj",
                                                                    size_mode = "core_size",
                                                                    left_group = "Left",
                                                                    right_group = "Right",
                                                                    palette = "D",
                                                                    point_range = c(3, 8),
                                                                    target_collection = "ALL",
                                                                    base_size = 12,
                                                                    margin_top = 18,
                                                                    margin_bottom = 18,
                                                                    margin_left = 18,
                                                                    margin_right = 18,
                                                                    fig_width = 9,
                                                                    fig_height = 7,
                                                                    fig_dpi = 300) {

    if (length(pathways) == 0) {
    return("# No pathways selected; nothing to plot.")
    }

    pathways_str <- paste(sprintf('    "%s"', pathways), collapse = ",\n")

    # Format target_collection for R code output
    tc_str <- if (length(target_collection) == 1 && toupper(target_collection) == "ALL") {
    '"ALL"'
    } else {
    paste0('c(', paste0('"', target_collection, '"', collapse = ", "), ')')
    }

    color_field <- switch(color_mode,
    "padj" = "-log10(p.adjust)",
    "pval" = "-log10(pvalue)",
    "nes"  = "abs(NES)",
    "-log10(p.adjust)")
    color_title <- switch(color_mode,
    "padj" = "-log10(FDR)",
    "pval" = "-log10(P-value)",
    "nes"  = "|NES|",
    "-log10(FDR)")

    size_field <- switch(size_mode,
    "core_size" = "CoreCount",
    "setsize"   = "setSize",
    "CoreCount")
    size_title <- switch(size_mode,
    "core_size" = "Core Genes",
    "setsize"   = "Set Size",
    "Core Genes")

    sprintf(
'# =============================================================================
# GSEAlens Publication DotPlot Code
# =============================================================================

library(ggplot2)

# --- Assume gsea_res (GseaRes object) is loaded in your environment ---------
# Uncomment to load from RDS:
# %s <- readRDS("your_capsule.rds")

GSEAlens_contrast_id <- "%s"
GSEAlens_pathways <- c(
%s
)

# --- Extract task and build plot data ----------------------------------------
GSEAlens_task <- GSEAlens::extract_gsea_task(%s,
    contrast_id = GSEAlens_contrast_id, target_collection = %s)
df <- as.data.frame(GSEAlens_task$gsea_res@result)
df <- df[df$ID %%in%% GSEAlens_pathways, ]

# CoreCount via leading-edge (core) genes
GSEAlens_core <- GSEAlens::get_core_genes_list(GSEAlens_task, GSEAlens_pathways)
df$CoreCount <- vapply(GSEAlens_pathways, function(pid) {
    if (is.null(GSEAlens_core[[pid]])) 0L else length(GSEAlens_core[[pid]])
}, integer(1))

# --- Type coercion (defensive) -----------------------------------------------
df$NES       <- suppressWarnings(as.numeric(df$NES))
df$pvalue    <- suppressWarnings(as.numeric(df$pvalue))
df$p.adjust  <- suppressWarnings(as.numeric(df$p.adjust))
df$setSize   <- suppressWarnings(as.numeric(df$setSize))
df$CoreCount <- suppressWarnings(as.integer(df$CoreCount))
df$NES[is.na(df$NES)]             <- 0
df$pvalue[is.na(df$pvalue)]       <- 1
df$p.adjust[is.na(df$p.adjust)]   <- 1
df$setSize[is.na(df$setSize)]     <- 0
df$CoreCount[is.na(df$CoreCount)] <- 0L

# --- Aesthetic config --------------------------------------------------------
color_field <- "%s"
color_title <- "%s"
size_field  <- "%s"
size_title  <- "%s"

# Size column clamp (prevents ggplot2 censoring large values)
size_lim_hi <- if (size_field == "CoreCount") 50 else 500
df[[size_field]] <- pmin(pmax(df[[size_field]], 0), size_lim_hi)

p <- ggplot(df, aes(
    x = NES,
    y = reorder(ID, NES),
    size  = .data[[size_field]],
    fill  = if (color_field == "abs(NES)") abs(NES)
                    else if (color_field == "-log10(p.adjust)") -log10(p.adjust)
                    else -log10(pvalue)
)) +
    geom_point(shape = 21, color = "black", stroke = 0.3, alpha = 0.85) +
    scale_size_continuous(name = size_title, range = c(%g, %g)) +
    scale_fill_viridis_c(name = color_title, option = "%s", direction = -1) +
    geom_vline(xintercept = 0, linetype = "dashed", color = "grey50") +
    theme_bw(base_size = %g) +
    labs(
    title    = sprintf("Pathway DotPlot: %%s vs %%s", "%s", "%s"),
    subtitle = sprintf("Color: %%s  |  Size: %%s", color_title, size_title),
    x = "NES (Normalized Enrichment Score)",
    y = NULL
    ) +
    coord_cartesian(clip = "off") +
    theme(
    plot.title    = element_text(face = "bold", hjust = 0.5),
    plot.subtitle = element_text(color = "gray40", hjust = 0.5, margin = margin(b = 20)),
    axis.text.y   = element_text(size = rel(0.85)),
    legend.position = "right",
    plot.margin     = margin(%d, %d, %d, %d)
    )

# --- Direction annotation: colored enriched-in labels above the plot ---------
# Single-direction aware: if only one NES sign is present, show one label only.
# y=1.01 + just="bottom": text sits just outside the top border, growing upward.
has_neg <- any(df$NES < 0, na.rm = TRUE)
has_pos <- any(df$NES > 0, na.rm = TRUE)
if (has_neg) {
    p <- p + annotation_custom(
    grid::textGrob("Enriched in %s", x = 0.02, y = 1.01, just = c("left", "bottom"),
                                        gp = grid::gpar(col = "#3B6EA5", fontface = "bold", fontsize = %g)),
    xmin = -Inf, xmax = Inf, ymin = -Inf, ymax = Inf)
}
if (has_pos) {
    p <- p + annotation_custom(
    grid::textGrob("Enriched in %s", x = 0.98, y = 1.01, just = c("right", "bottom"),
                                        gp = grid::gpar(col = "#C0392B", fontface = "bold", fontsize = %g)),
    xmin = -Inf, xmax = Inf, ymin = -Inf, ymax = Inf)
}

print(p)

# --- Save to file (uncomment one) --------------------------------------------
# ggsave("dotplot.pdf", p, width = %g, height = %g, device = cairo_pdf)
# ggsave("dotplot.png", p, width = %g, height = %g, dpi = %g)
# ggsave("dotplot.svg", p, width = %g, height = %g)
',
    gsea_res_var,
    contrast_id,
    pathways_str,
    gsea_res_var,
    tc_str,
    color_field, color_title,
    size_field,  size_title,
    point_range[1], point_range[2],
    palette,
    base_size,
    left_group, right_group,
    margin_top, margin_right, margin_bottom, margin_left,
    right_group,
    base_size,
    left_group,
    base_size,
    fig_width, fig_height,
    fig_width, fig_height, fig_dpi,
    fig_width, fig_height
    )
}


#' @title Generate Publication Volcano Code
#' @description Return an executable ggplot2 script reproducing the Quadrant
#'   module's pathway volcano plot as a static, publication-ready figure.
#'   Assumes a \code{GseaRes} object is already loaded in the R environment.
#' @param gsea_res_var Character. Name of the GseaRes variable.
#' @param contrast_id Character. Contrast ID to extract.
#' @param left_group Character.
#' @param right_group Character.
#' @param base_size Numeric.
#' @return Character scalar of executable R code.
#' @param n_selected Integer. Number of selected pathways (subtitle).
#' @param selected_ids Character vector. Pathway IDs for ggrepel annotation.
#' @param show_annotations Logical. Add ggrepel labels for selected pathways.
#' @param target_collection Character. Gene set collection(s) to extract.
#'   Default \code{"ALL"}.
#' @param margin_top Numeric. Top plot margin (mm).
#' @param margin_bottom Numeric. Bottom plot margin (mm).
#' @param margin_left Numeric. Left plot margin (mm).
#' @param margin_right Numeric. Right plot margin (mm).
#' @param fig_width Numeric. Figure width in inches for ggsave.
#' @param fig_height Numeric. Figure height in inches for ggsave.
#' @param fig_dpi Numeric. DPI for PNG raster output (default 300).
#' @examples
#' code <- generate_volcano_code(
#'   gsea_res_var = "gsea_res", contrast_id = "untrt_vs_trt",
#'   left_group = "Ctrl", right_group = "Trt")
#' cat(code)
#' @export
generate_volcano_code <- function(gsea_res_var = "gsea_res",
                                                                    contrast_id,
                                                                    left_group = "Left",
                                                                    right_group = "Right",
                                                                    n_selected = 0L,
                                                                    selected_ids = character(0),
                                                                    show_annotations = FALSE,
                                                                    target_collection = "ALL",
                                                                    base_size = 12,
                                                                    margin_top = 15, margin_bottom = 15,
                                                                    margin_left = 18, margin_right = 18,
                                                                    fig_width = 8,
                                                                    fig_height = 6,
                                                                    fig_dpi = 300) {

    selected_str <- if (length(selected_ids) > 0) {
    paste0('c(', paste0('"', selected_ids, '"', collapse = ", "), ')')
    } else "character(0)"

    # Format target_collection for R code output
    tc_str <- if (length(target_collection) == 1 && toupper(target_collection) == "ALL") {
    '"ALL"'
    } else {
    paste0('c(', paste0('"', target_collection, '"', collapse = ", "), ')')
    }

    labs_block <- if (show_annotations) {
    paste0(
'  labs(\n',
'    title = sprintf("Pathway Volcano: %s vs %s", "', left_group, '", "', right_group, '"),\n',
'    subtitle = sprintf("%d pathways | %d selected | %d significant (P<0.25)",\n',
'                       nrow(df), ', n_selected, ', sum(df$p.adjust < 0.25, na.rm = TRUE)),\n',
'    caption = sprintf("Left (NES<0): Enriched in %s  |  Right (NES>0): Enriched in %s",\n',
'                      "', right_group, '", "', left_group, '"),\n',
'    x = "NES",\n',
'    y = "-log10(P-value)"\n',
'  ) +'
    )
    } else {
    paste0(
'  labs(\n',
'    title = sprintf("Pathway Volcano: %s vs %s", "', left_group, '", "', right_group, '"),\n',
'    caption = sprintf("Left (NES<0): Enriched in %s  |  Right (NES>0): Enriched in %s",\n',
'                      "', right_group, '", "', left_group, '"),\n',
'    x = "NES",\n',
'    y = "-log10(P-value)"\n',
'  ) +'
    )
    }

    sprintf(
'# =============================================================================
# GSEAlens Publication Pathway Volcano Code
# =============================================================================

library(ggplot2)

# --- Assume gsea_res (GseaRes object) is loaded in your environment ---------
# Uncomment to load from RDS:
# %s <- readRDS("your_capsule.rds")

GSEAlens_contrast_id <- "%s"

# --- Extract task and build plot data ----------------------------------------
GSEAlens_task <- GSEAlens::extract_gsea_task(%s,
    contrast_id = GSEAlens_contrast_id, target_collection = %s)
df <- as.data.frame(GSEAlens_task$gsea_res@result)

# --- Selected pathway IDs (for ggrepel labels) -------------------------------
selected_ids <- %s
n_selected   <- %d

# --- Type coercion (defensive) -----------------------------------------------
df$NES      <- suppressWarnings(as.numeric(df$NES))
df$pvalue   <- suppressWarnings(as.numeric(df$pvalue))
df$p.adjust <- suppressWarnings(as.numeric(df$p.adjust))
df$NES[is.na(df$NES)]             <- 0
df$pvalue[is.na(df$pvalue)]       <- 1
df$p.adjust[is.na(df$p.adjust)]   <- 1

# --- Direction: NES > 0 = enriched in left_group; NES < 0 = right_group ------
df$direction <- with(df, ifelse(NES > 0,
    "Enriched in %s", "Enriched in %s"))
df$sig_level <- with(df,
    cut(pvalue,
            breaks = c(-Inf, 1e-10, 1e-5, 1e-2, 0.05, Inf),
            labels = c("P<1e-10", "P<1e-5", "P<1e-2", "P<0.05", "NS"),
            right  = FALSE))

p <- ggplot(df, aes(x = NES, y = -log10(pvalue),
                    color = direction, alpha = sig_level)) +
    geom_point(size = 2.5) +
    scale_color_manual(
    values = c("Enriched in %s" = "#E41A1C", "Enriched in %s" = "#377EB8"),
    name = "Direction") +
    scale_alpha_manual(
    values = c("P<1e-10" = 1.0, "P<1e-5" = 0.9, "P<1e-2" = 0.7,
                                "P<0.05" = 0.5, "NS" = 0.25),
    name = "Significance", drop = FALSE) +
    geom_hline(yintercept = -log10(0.05), linetype = "dashed", color = "grey50") +
    geom_vline(xintercept = 0, linetype = "dashed", color = "grey50") +
    theme_bw(base_size = %g) +
%s
    theme(
    plot.title   = element_text(face = "bold", hjust = 0.5),
    plot.caption = element_text(color = "gray40", hjust = 0.5, size = rel(0.85)),
    legend.position = "right",
    plot.margin  = margin(%d, %d, %d, %d)
    )

# --- ggrepel labels for selected pathways ------------------------------------
if (length(selected_ids) > 0) {
    df_label <- df[df$ID %%in%% selected_ids, ]
    if (nrow(df_label) > 0) {
    library(ggrepel)
    p <- p + ggrepel::geom_label_repel(
            data = df_label,
            aes(label = ID),
            size = 3, fontface = "bold",
            fill = "white", color = "#222", alpha = 0.9,
            box.padding = 0.5, label.padding = 0.18, linewidth = 0.2,
            min.segment.length = 0,
            segment.color = "grey55", segment.size = 0.3, segment.alpha = 0.7,
            max.overlaps = 20, max.iter = 5000,
            nudge_y = 0.3, direction = "both", force = 2
    )
    }
}

print(p)

# --- Save to file (uncomment one) --------------------------------------------
# ggsave("volcano.pdf", p, width = %g, height = %g, device = cairo_pdf)
# ggsave("volcano.png", p, width = %g, height = %g, dpi = %g)
# ggsave("volcano.svg", p, width = %g, height = %g)
',
    gsea_res_var,
    contrast_id,
    gsea_res_var,
    tc_str,
    selected_str,
    n_selected,
    left_group, right_group,
    left_group, right_group,
    base_size,
    labs_block,
    margin_top, margin_right, margin_bottom, margin_left,
    fig_width, fig_height,
    fig_width, fig_height, fig_dpi,
    fig_width, fig_height
    )
}


#' @title Generate Publication DE Volcano Code
#' @description Return an executable ggplot2 script reproducing the Quadrant
#'   module's gene-level Differential Expression Volcano as a static figure.
#'   Assumes a \code{GseaRes} object is already loaded. Six-color classification
#'   (both / user / pathway / up / down / ns) mirrors the interactive view.
#' @param gsea_res_var Character. Name of the GseaRes variable.
#' @param contrast_id Character. Contrast ID for DE table extraction.
#' @param user_genes Character vector of user-selected genes (upper-cased).
#' @param pathway_genes Character vector of pathway genes (upper-cased).
#' @param logfc_thresh Numeric. Default 1.
#' @param pval_thresh Numeric. Default 0.05.
#' @param left_group Character.
#' @param right_group Character.
#' @param base_size Numeric.
#' @return Character scalar of executable R code.
#' @param show_annotations Logical. Add ggrepel labels.
#' @param margin_top Numeric. Top plot margin (mm).
#' @param margin_bottom Numeric. Bottom plot margin (mm).
#' @param margin_left Numeric. Left plot margin (mm).
#' @param margin_right Numeric. Right plot margin (mm).
#' @param fig_width Numeric. Figure width in inches for ggsave.
#' @param fig_height Numeric. Figure height in inches for ggsave.
#' @param fig_dpi Numeric. DPI for PNG raster output (default 300).
#' @examples
#' code <- generate_de_volcano_code(
#'   gsea_res_var = "gsea_res", contrast_id = "untrt_vs_trt",
#'   left_group = "Ctrl", right_group = "Trt")
#' cat(code)
#' @export
generate_de_volcano_code <- function(gsea_res_var = "gsea_res",
                                                                            contrast_id,
                                                                            user_genes = character(0),
                                                                            pathway_genes = character(0),
                                                                            logfc_thresh = 1,
                                                                            pval_thresh = 0.05,
                                                                            left_group = "Left",
                                                                            right_group = "Right",
                                                                            show_annotations = FALSE,
                                                                            base_size = 12,
                                                                            margin_top = 15, margin_bottom = 15,
                                                                            margin_left = 18, margin_right = 18,
                                                                            fig_width = 8,
                                                                            fig_height = 6,
                                                                            fig_dpi = 300) {

    user_str <- if (length(user_genes) > 0) {
    paste0('c(', paste0('"', user_genes, '"', collapse = ", "), ')')
    } else "character(0)"
    pathway_str <- if (length(pathway_genes) > 0) {
    paste0('c(', paste0('"', pathway_genes, '"', collapse = ", "), ')')
    } else "character(0)"

    labs_annot_block <- if (show_annotations) {
    paste0(
'  labs(\n',
'    title = sprintf("DE Volcano: %s vs %s", "', left_group, '", "', right_group, '"),\n',
'    subtitle = sprintf(\n',
'      "Up %d | Down %d | NS %d | Selected %d | Pway %d | Both %d",\n',
'      sum(de_df$category == "up"),   sum(de_df$category == "down"),\n',
'      sum(de_df$category == "ns"),   sum(de_df$category == "user"),\n',
'      sum(de_df$category == "pathway"), sum(de_df$category == "both")),\n',
'    x = "log2 Fold Change",\n',
'    y = "-log10(P-value)"\n',
'  ) +\n',
'  annotate("text", x = Inf, y = Inf, color = "#E41A1C",\n',
'           label = sprintf("High in %s", "', left_group, '"),\n',
'           hjust = 1.08, vjust = 1.6, fontface = "bold", size = 4.5) +\n',
'  annotate("text", x = -Inf, y = Inf, color = "#377EB8",\n',
'           label = sprintf("High in %s", "', right_group, '"),\n',
'           hjust = -0.08, vjust = 1.6, fontface = "bold", size = 4.5) +\n',
'  coord_cartesian(clip = "off") +\n',
'  theme(\n',
'    plot.title    = element_text(face = "bold", hjust = 0.5),\n',
'    plot.subtitle = element_text(color = "gray30", hjust = 0.5),\n',
    sprintf('    plot.margin   = margin(%d, %d, %d, %d),\n', margin_top, margin_right, margin_bottom, margin_left),
'    legend.position = "right"\n',
'  )'
    )
    } else {
    paste0(
'  labs(\n',
'    title = sprintf("DE Volcano: %s vs %s", "', left_group, '", "', right_group, '"),\n',
'    x = "log2 Fold Change",\n',
'    y = "-log10(P-value)"\n',
'  ) +\n',
'  theme(\n',
'    plot.title    = element_text(face = "bold", hjust = 0.5),\n',
'    legend.position = "right",\n',
    sprintf('    plot.margin = margin(%d, %d, %d, %d)\n', margin_top, margin_right, margin_bottom, margin_left),
'  )'
    )
    }

    sprintf(
'# =============================================================================
# GSEAlens Publication DE Volcano Code
# =============================================================================

library(ggplot2)

# --- Assume gsea_res (GseaRes object) is loaded in your environment ---------
# Uncomment to load from RDS:
# %s <- readRDS("your_capsule.rds")

GSEAlens_contrast_id <- "%s"

# --- Extract DE table from GseaRes -------------------------------------------
de_df <- GSEAlens::get_de_table(%s, GSEAlens_contrast_id)
if (!"logFC"  %%in%% names(de_df)) de_df$logFC  <- de_df$log2FoldChange
if (!"pvalue" %%in%% names(de_df)) de_df$pvalue <- de_df$p.value

user_genes    <- toupper(%s)
pathway_genes <- toupper(%s)

# --- Type coercion (defensive) -----------------------------------------------
de_df$logFC  <- suppressWarnings(as.numeric(de_df$logFC))
de_df$pvalue <- suppressWarnings(as.numeric(de_df$pvalue))
de_df$logFC[is.na(de_df$logFC)]   <- 0
de_df$pvalue[is.na(de_df$pvalue)] <- 1

# --- Thresholds --------------------------------------------------------------
logfc_thresh <- %g
pval_thresh  <- %g

# --- Classify ----------------------------------------------------------------
de_df$gene_upper <- toupper(de_df$gene_symbol)
de_df$is_user     <- de_df$gene_upper %%in%% user_genes
de_df$is_pathway  <- de_df$gene_upper %%in%% pathway_genes
de_df$is_sig      <- abs(de_df$logFC) > logfc_thresh & de_df$pvalue < pval_thresh

de_df$category <- dplyr::case_when(
    de_df$is_user & de_df$is_pathway ~ "both",
    de_df$is_user                    ~ "user",
    de_df$is_pathway                 ~ "pathway",
    de_df$is_sig & de_df$logFC > 0   ~ "up",
    de_df$is_sig & de_df$logFC < 0   ~ "down",
    TRUE                             ~ "ns"
)

color_map <- c(
    both = "#9C27B0", user = "#4DAF4A", pathway = "#FF9800",
    up = "#E41A1C",   down = "#377EB8", ns = "#C0C0C0"
)
de_df$category <- factor(de_df$category,
    levels = c("ns", "down", "up", "pathway", "user", "both"))

# --- Layer ordering (mirrors interactive plotly) -----------------------------
# Sort so special categories (pathway/user/both) are drawn LAST and appear
# on top of the significant and NS points, making them easy to spot.
de_df$plot_order <- dplyr::case_when(
    de_df$category == "both"    ~ 6,
    de_df$category == "user"    ~ 5,
    de_df$category == "pathway" ~ 4,
    de_df$category %%in%% c("up", "down") ~ 2,
    TRUE ~ 1
)
de_df <- de_df[order(de_df$plot_order), ]

p <- ggplot(de_df, aes(x = logFC, y = -log10(pvalue), color = category)) +
    geom_point(aes(size = category, alpha = category)) +
    scale_color_manual(values = color_map, name = "Category",
    labels = c(ns = "NS", down = "Down", up = "Up",
                                pathway = "Pathway", user = "Selected", both = "Both")) +
    scale_size_manual(values = c(ns = 1, down = 2, up = 2,
                                                                pathway = 3.5, user = 3.5, both = 4), guide = "none") +
    scale_alpha_manual(values = c(ns = 0.4, down = 0.7, up = 0.7,
                                pathway = 0.9, user = 0.9, both = 1.0), guide = "none") +
    geom_vline(xintercept = c(-logfc_thresh, logfc_thresh),
                            linetype = "dashed", color = "grey60") +
    geom_hline(yintercept = -log10(pval_thresh),
                            linetype = "dashed", color = "grey60") +
    theme_bw(base_size = %g) +
%s

# --- ggrepel labels for Confirmed Gene Markers ONLY --------------------------
de_df_label <- de_df[de_df$category %%in%% c("user", "both"), ]
if (nrow(de_df_label) > 0) {
    label_color_map <- c(user = "#4DAF4A", both = "#9C27B0")
    library(ggrepel)
    p <- p + ggrepel::geom_label_repel(
    data = de_df_label,
    aes(label = gene_symbol, fill = category),
    color = "white", size = 3, fontface = "bold",
    box.padding = 0.45, label.padding = 0.2,
    min.segment.length = 0, segment.color = "grey55",
    max.overlaps = Inf, max.iter = 5000,
    show.legend = FALSE
    ) +
    scale_fill_manual(values = label_color_map, guide = "none")
}

print(p)

# --- Save to file (uncomment one) --------------------------------------------
# ggsave("de_volcano.pdf", p, width = %g, height = %g, device = cairo_pdf)
# ggsave("de_volcano.png", p, width = %g, height = %g, dpi = %g)
# ggsave("de_volcano.svg", p, width = %g, height = %g)
',
    gsea_res_var,
    contrast_id,
    gsea_res_var,
    user_str, pathway_str,
    logfc_thresh, pval_thresh,
    base_size,
    labs_annot_block,
    fig_width, fig_height,
    fig_width, fig_height, fig_dpi,
    fig_width, fig_height
    )
}


#' @title Generate Publication Pathway Network Code
#' @description Return an executable R script reproducing the Pathway
#'   Relationship module's network view as a static ggplot2 figure.
#'   Assumes a \code{GseaRes} object is already loaded. Uses igraph layout +
#'   geom_segment edges + geom_point nodes (no ggraph dependency).
#' @param gsea_res_var Character. Name of the GseaRes variable.
#' @param contrast_id Character. Contrast ID to extract.
#' @param pathways Character vector. Pathway IDs to include in the network.
#' @param min_shared_genes Integer. Minimum shared core genes for an edge.
#' @param width_mode Character. "rank" or "weight".
#' @param seed Integer. Layout seed.
#' @param target_collection Character. Gene set collection(s) to extract.
#'   Default \code{"ALL"}.
#' @param base_size Numeric.
#' @return Character scalar of executable R code.
#' @param margin_top Numeric. Top plot margin (mm).
#' @param margin_bottom Numeric. Bottom plot margin (mm).
#' @param margin_left Numeric. Left plot margin (mm).
#' @param margin_right Numeric. Right plot margin (mm).
#' @param fig_width Numeric. Figure width in inches for ggsave.
#' @param fig_height Numeric. Figure height in inches for ggsave.
#' @param fig_dpi Numeric. DPI for PNG raster output (default 300).
#' @examples
#' code <- generate_network_code(
#'   gsea_res_var = "gsea_res", contrast_id = "untrt_vs_trt",
#'   pathways = c("HALLMARK_TNFA_SIGNALING_VIA_NFKB", "HALLMARK_HYPOXIA"))
#' cat(code)
#' @export
generate_network_code <- function(gsea_res_var = "gsea_res",
                                                                    contrast_id,
                                                                    pathways,
                                                                    min_shared_genes = 2,
                                                                    width_mode = "weight",
                                                                    seed = 42L,
                                                                    target_collection = "ALL",
                                                                    base_size = 12,
                                                                    margin_top = 18, margin_bottom = 18,
                                                                    margin_left = 18, margin_right = 18,
                                                                    fig_width = 10,
                                                                    fig_height = 8,
                                                                    fig_dpi = 300) {

    if (length(pathways) == 0) {
    return("# No pathways selected; nothing to plot.")
    }

    pathways_str <- paste(sprintf('    "%s"', pathways), collapse = ",\n")

    # Format target_collection for R code output
    tc_str <- if (length(target_collection) == 1 && toupper(target_collection) == "ALL") {
    '"ALL"'
    } else {
    paste0('c(', paste0('"', target_collection, '"', collapse = ", "), ')')
    }

    width_block <- if (width_mode == "rank") {
'  # rank-based: assign edge width by Jaccard rank (1..n -> 1..5 px)
    edge_df <- edge_df[order(edge_df$weight, decreasing = TRUE), ]
    n <- nrow(edge_df)
    edge_df$width <- 5 - (seq_len(n) - 1) * 4 / max(1, n - 1)
    edge_df$width <- pmax(1, pmin(5, edge_df$width))'
    } else {
'  # weight-based: edge width proportional to Jaccard value (emapplot style)
    edge_df$width <- 1 + edge_df$weight * 7   # Jaccard in [0,1] -> [1,8] px'
    }

    sprintf(
'# =============================================================================
# GSEAlens Publication Pathway Network Code
# =============================================================================

library(igraph)
library(ggplot2)
library(ggrepel)

# --- Assume gsea_res (GseaRes object) is loaded in your environment ---------
# Uncomment to load from RDS:
# %s <- readRDS("your_capsule.rds")

GSEAlens_contrast_id <- "%s"
GSEAlens_pathways <- c(
%s
)

# --- Extract task and build network data -------------------------------------
GSEAlens_task <- GSEAlens::extract_gsea_task(%s,
    contrast_id = GSEAlens_contrast_id, target_collection = %s)
df <- as.data.frame(GSEAlens_task$gsea_res@result)
df <- df[df$ID %%in%% GSEAlens_pathways, ]

# Build edge list from shared leading-edge (core) genes
GSEAlens_core <- GSEAlens::get_core_genes_list(GSEAlens_task, GSEAlens_pathways)
edge_df <- GSEAlens::build_edge_list_safely(GSEAlens_core, min_shared_genes = %d)
node_df <- df[, c("ID", "NES", "p.adjust")]
names(node_df)[1] <- "name"

# --- Type coercion (defensive) -----------------------------------------------
node_df$NES      <- suppressWarnings(as.numeric(node_df$NES))
node_df$p.adjust <- suppressWarnings(as.numeric(node_df$p.adjust))
edge_df$weight   <- suppressWarnings(as.numeric(edge_df$weight))
node_df$NES[is.na(node_df$NES)]           <- 0
node_df$p.adjust[is.na(node_df$p.adjust)] <- 1
edge_df$weight[is.na(edge_df$weight)]     <- 0

# --- Edge width mapping ------------------------------------------------------
%s

# --- Build graph and compute FR layout ---------------------------------------
g <- graph_from_data_frame(edge_df[, c("from", "to")],
                                                        directed = FALSE,
                                                        vertices = node_df$name)
set.seed(%d)
layout_xy <- layout_with_fr(g)
node_df$x <- layout_xy[, 1]
node_df$y <- layout_xy[, 2]

# Edge segments need explicit x/y endpoints
edge_plot <- merge(edge_df, node_df, by.x = "from", by.y = "name")
names(edge_plot)[names(edge_plot) == "x"] <- "x_from"
names(edge_plot)[names(edge_plot) == "y"] <- "y_from"
edge_plot <- merge(edge_plot, node_df, by.x = "to", by.y = "name")
names(edge_plot)[names(edge_plot) == "x"] <- "x_to"
names(edge_plot)[names(edge_plot) == "y"] <- "y_to"

node_df$direction <- ifelse(node_df$NES > 0, "Up", "Down")

p <- ggplot() +
    geom_segment(data = edge_plot,
                                aes(x = x_from, y = y_from, xend = x_to, yend = y_to,
                                        linewidth = width),
                                color = "grey70", alpha = 0.6) +
    scale_linewidth_continuous(range = c(0.3, 2.5), name = "Edge width") +
    geom_point(data = node_df,
                            aes(x = x, y = y, fill = direction, size = -log10(p.adjust)),
                            shape = 21, color = "black", stroke = 0.4, alpha = 0.85) +
    scale_fill_manual(values = c("Up" = "#F28F8F", "Down" = "#8FB7E8"),
                    name = "Direction") +
    scale_size_continuous(range = c(3, 9), name = "-log10(FDR)") +
    ggrepel::geom_label_repel(
    data = node_df,
    aes(x = x, y = y, label = sub("^[^_]*_", "", name)),
    size = 3.2, fontface = "bold",
    fill = "white", color = "#222", alpha = 0.95,
    box.padding = 0.45, label.padding = 0.18, linewidth = 0.2,
    min.segment.length = 0,
    segment.color = "grey55", segment.size = 0.3, segment.alpha = 0.7,
    max.overlaps = Inf, nudge_y = 0.18, direction = "both"
    ) +
    guides(
    fill = guide_legend(override.aes = list(size = 7, shape = 21, stroke = 1)),
    size = guide_legend(override.aes = list(shape = 21, fill = "grey50", stroke = 0.5))
    ) +
    theme_void(base_size = %g) +
    theme(legend.position = "right",
        plot.margin = margin(%d, %d, %d, %d)) +
    coord_cartesian(clip = "off") +
    labs(title = "Pathway Relationship Network")

print(p)

# --- Save to file (uncomment one) --------------------------------------------
# ggsave("network.pdf", p, width = %g, height = %g, device = cairo_pdf)
# ggsave("network.png", p, width = %g, height = %g, dpi = %g)
# ggsave("network.svg", p, width = %g, height = %g)
',
    gsea_res_var,
    contrast_id,
    pathways_str,
    gsea_res_var,
    tc_str,
    min_shared_genes,
    width_block,
    seed, base_size,
    margin_top, margin_right, margin_bottom, margin_left,
    fig_width, fig_height,
    fig_width, fig_height, fig_dpi,
    fig_width, fig_height
    )
}


#' @title Generate Publication HubGene Network Code
#' @description Return an executable R script reproducing the HubGene module's
#'   visNetwork view as a static ggplot2 figure. Assumes a \code{GseaRes}
#'   object is already loaded. Node size is controlled by the same three-mode
#'   logic (fixed / fdr / setsize) as the interactive view.
#' @param gsea_res_var Character. Name of the GseaRes variable.
#' @param contrast_id Character. Contrast ID to extract.
#' @param pathways Character vector. Pathway IDs to include.
#' @param min_hub_degree Integer. Minimum hub degree for gene inclusion.
#' @param pw_size_mode Character. One of "fixed", "fdr", "setsize".
#' @param seed Integer.
#' @param target_collection Character. Gene set collection(s) to extract.
#'   Default \code{"ALL"}.
#' @param base_size Numeric.
#' @return Character scalar of executable R code.
#' @param margin_top Numeric. Top plot margin (mm).
#' @param margin_bottom Numeric. Bottom plot margin (mm).
#' @param margin_left Numeric. Left plot margin (mm).
#' @param margin_right Numeric. Right plot margin (mm).
#' @param fig_width Numeric. Figure width in inches for ggsave.
#' @param fig_height Numeric. Figure height in inches for ggsave.
#' @param fig_dpi Numeric. DPI for PNG raster output (default 300).
#' @examples
#' code <- generate_hubgene_code(
#'   gsea_res_var = "gsea_res", contrast_id = "untrt_vs_trt",
#'   pathways = c("HALLMARK_TNFA_SIGNALING_VIA_NFKB"))
#' cat(code)
#' @export
generate_hubgene_code <- function(gsea_res_var = "gsea_res",
                                                                    contrast_id,
                                                                    pathways,
                                                                    min_hub_degree = 2,
                                                                    pw_size_mode = "setsize",
                                                                    seed = 42L,
                                                                    target_collection = "ALL",
                                                                    base_size = 12,
                                                                    margin_top = 18, margin_bottom = 18,
                                                                    margin_left = 18, margin_right = 18,
                                                                    fig_width = 10,
                                                                    fig_height = 8,
                                                                    fig_dpi = 300) {

    if (length(pathways) == 0) {
    return("# No pathways selected; nothing to plot.")
    }

    pathways_str <- paste(sprintf('    "%s"', pathways), collapse = ",\n")

    # Format target_collection for R code output
    tc_str <- if (length(target_collection) == 1 && toupper(target_collection) == "ALL") {
    '"ALL"'
    } else {
    paste0('c(', paste0('"', target_collection, '"', collapse = ", "), ')')
    }

    size_block <- switch(pw_size_mode,
    "fdr" =
'  # FDR-driven sizing
    pw$fdr_neglog <- -log10(pmax(pw$FDR, 1e-300))
    r <- range(pw$fdr_neglog); if (diff(r) < 1e-6) r <- c(0, 10)
    pw$size <- 30 * (0.6 + 0.8 * (pw$fdr_neglog - r[1]) / diff(r))',
    "setsize" =
'  # setSize-driven sizing (cnetplot classic)
    r <- range(pw$setSize); if (diff(r) < 1) r <- c(0, 200)
    pw$size <- 30 * (0.6 + 0.8 * (sqrt(pw$setSize) - sqrt(r[1])) /
                            (sqrt(r[2]) - sqrt(r[1])))',
'  # Fixed size (user-controlled constant)
    pw$size <- 30'
    )

    sprintf(
'# =============================================================================
# GSEAlens Publication HubGene Network Code
# =============================================================================

library(igraph)
library(ggplot2)
library(ggrepel)

# --- Assume gsea_res (GseaRes object) is loaded in your environment ---------
# Uncomment to load from RDS:
# %s <- readRDS("your_capsule.rds")

GSEAlens_contrast_id <- "%s"
GSEAlens_pathways <- c(
%s
)

# --- Extract task and build HubGene network ----------------------------------
GSEAlens_task <- GSEAlens::extract_gsea_task(%s,
    contrast_id = GSEAlens_contrast_id, target_collection = %s)

GSEAlens_net <- GSEAlens::build_hubgene_network(
    GSEAlens_task,
    pathway_ids    = GSEAlens_pathways,
    min_hub_degree = %d
)

pw    <- GSEAlens_net$nodes$pathway
genes <- GSEAlens_net$nodes$gene
edges_raw <- GSEAlens_net$edges

edges <- if (!is.null(edges_raw) && nrow(edges_raw) > 0) {
    data.frame(from = as.character(edges_raw$source),
                            to   = as.character(edges_raw$target),
                            stringsAsFactors = FALSE)
} else {
    data.frame(from = character(0), to = character(0), stringsAsFactors = FALSE)
}

# --- Type coercion (defensive) -----------------------------------------------
if (!"setSize" %%in%% names(pw))    pw$setSize <- pw$n_total
if (!"degree"  %%in%% names(genes)) genes$degree <- 1L
pw$NES     <- suppressWarnings(as.numeric(pw$NES))
pw$FDR     <- suppressWarnings(as.numeric(pw$FDR))
pw$setSize <- suppressWarnings(as.numeric(pw$setSize))
genes$stat   <- suppressWarnings(as.numeric(genes$stat))
genes$degree <- suppressWarnings(as.integer(genes$degree))
pw$NES[is.na(pw$NES)]         <- 0
pw$FDR[is.na(pw$FDR)]         <- 1
pw$setSize[is.na(pw$setSize)] <- 100
genes$stat[is.na(genes$stat)]       <- 0
genes$degree[is.na(genes$degree)]   <- 1

# --- Pathway node sizing -----------------------------------------------------
%s

# --- Graph + layout ----------------------------------------------------------
pw$vname    <- paste0("pw_",   pw$id)
genes$vname <- paste0("gene_", genes$id)
edges$from  <- paste0("gene_", edges$from)
edges$to    <- paste0("pw_",   edges$to)

all_verts <- data.frame(
    name = c(pw$vname, genes$vname), stringsAsFactors = FALSE)
g <- graph_from_data_frame(edges[, c("from", "to")],
                                                        directed = FALSE, vertices = all_verts)
set.seed(%d)
layout_xy <- layout_with_fr(g)
all_verts$x <- layout_xy[, 1]
all_verts$y <- layout_xy[, 2]
pw    <- cbind(pw,    all_verts[match(pw$vname,    all_verts$name), c("x", "y")])
genes <- cbind(genes, all_verts[match(genes$vname, all_verts$name), c("x", "y")])

pw$direction <- ifelse(pw$NES > 0, "Up", "Down")
genes$direction <- ifelse(genes$stat > 0, "Up",
                                        ifelse(genes$stat < 0, "Down", "Neutral"))

edge_plot <- merge(edges, all_verts, by.x = "from", by.y = "name")
names(edge_plot)[names(edge_plot) == "x"] <- "x_from"
names(edge_plot)[names(edge_plot) == "y"] <- "y_from"
edge_plot <- merge(edge_plot, all_verts, by.x = "to", by.y = "name")
names(edge_plot)[names(edge_plot) == "x"] <- "x_to"
names(edge_plot)[names(edge_plot) == "y"] <- "y_to"

p <- ggplot() +
    geom_segment(data = edge_plot,
                                aes(x = x_from, y = y_from, xend = x_to, yend = y_to),
                                color = "grey75", alpha = 0.5, linewidth = 0.4) +
    geom_point(data = genes,
                            aes(x = x, y = y, color = direction, size = degree),
                            alpha = 0.85) +
    scale_color_manual(
    values = c("Up" = "#F28F8F", "Down" = "#8FB7E8", "Neutral" = "#BDBDBD"),
    name = "Direction") +
    scale_size_continuous(range = c(2, 8), name = "Hub degree") +
    geom_point(data = pw,
                            aes(x = x, y = y, size = size),
                            shape = 23, fill = "#FFC107", color = "black", stroke = 0.5) +
    ggrepel::geom_label_repel(
    data = pw,
    aes(x = x, y = y, label = sub("^[^_]*_", "", id)),
    size = 3.2, fontface = "bold",
    fill = "white", color = "#222", alpha = 0.95,
    box.padding = 0.45, label.padding = 0.18, linewidth = 0.2,
    min.segment.length = 0,
    segment.color = "grey55", segment.size = 0.3, segment.alpha = 0.7,
    max.overlaps = Inf, nudge_y = 0.18, direction = "both"
    ) +
    guides(
    color = guide_legend(override.aes = list(size = 7)),
    size  = guide_legend()
    ) +
    theme_void(base_size = %g) +
    theme(legend.position = "right",
        plot.margin = margin(%d, %d, %d, %d)) +
    coord_cartesian(clip = "off") +
    labs(title = "HubGene Pathway-Gene Network")

print(p)

# --- Save to file (uncomment one) --------------------------------------------
# ggsave("hubgene.pdf", p, width = %g, height = %g, device = cairo_pdf)
# ggsave("hubgene.png", p, width = %g, height = %g, dpi = %g)
# ggsave("hubgene.svg", p, width = %g, height = %g)
',
    gsea_res_var,
    contrast_id,
    pathways_str,
    gsea_res_var,
    tc_str,
    min_hub_degree,
    size_block,
    seed, base_size,
    margin_top, margin_right, margin_bottom, margin_left,
    fig_width, fig_height,
    fig_width, fig_height, fig_dpi,
    fig_width, fig_height
    )
}


#' @title Serialize a Data Frame to TSV

#' @description Internal helper used by the Quadrant module's "Export Boxplot

#'   Data" modal. Writes a \code{data.frame} as tab-separated values with a

#'   header row and \code{NA} rendered as empty strings. Values are quoted

#'   only when they contain tab / newline / double-quote characters, so

#'   clean sample IDs stay unquoted.

#' @param df A \code{data.frame}.

#' @return A character scalar containing the TSV (no trailing newline).

#' @keywords internal

.serialize_df_tsv <- function(df) {

    if (!is.data.frame(df) || nrow(df) == 0) {

    return("")

    }



    needs_quote <- function(s) {

    !is.na(s) && nzchar(s) && grepl("[\t\n\"]", s)

    }



    encode_cell <- function(val) {

    if (is.na(val)) return("")

    if (is.numeric(val)) return(format(val, trim = TRUE, scientific = FALSE))

    s <- as.character(val)

    if (needs_quote(s)) shQuote(s, type = "sh") else s

    }



    header <- paste(vapply(colnames(df), encode_cell, character(1)), collapse = "\t")

    body_lines <- vapply(seq_len(nrow(df)), function(i) {

    paste(vapply(df, function(col) encode_cell(col[i]), character(1)),

                    collapse = "\t")

    }, character(1))



    paste(c(header, body_lines), collapse = "\n")

}





#' @title Generate Publication Boxplot Image Code

#' @description Return an executable ggplot2 script reproducing the Quadrant

#'   module's per-gene expression boxplot (panel 4) as a static,

#'   publication-ready figure. Mirrors the interactive plotly: boxplot +

#'   jittered points, group colors, optional zero baseline, optional custom

#'   group order.

#' @param box_data_long data.frame with columns: Sample, Group, Expression.

#'   (Same long format returned by \code{extract_boxplot_data()$long}.)

#' @param gene_symbol Character. Gene name for the plot title.

#' @param expr_type Character. Y-axis label (e.g. "logcpm", "log2FC").

#' @param left_group Character. First group name (gets red fill).

#' @param right_group Character. Second group name (gets blue fill).

#' @param use_zero_baseline Logical. Add dashed red line at y = 0.

#' @param custom_order Character scalar. Custom group ordering

#'   (e.g. "A->B,C") or "default".

#' @param base_size Numeric.

#' @param margin_top Numeric. Top plot margin (mm).

#' @param margin_bottom Numeric. Bottom plot margin (mm).

#' @param margin_left Numeric. Left plot margin (mm).

#' @param margin_right Numeric. Right plot margin (mm).

#' @param fig_width Numeric. Figure width in inches for ggsave.

#' @param fig_height Numeric. Figure height in inches for ggsave.

#' @param fig_dpi Numeric. DPI for PNG raster output (default 300).

#' @return Character scalar of executable R code.

#' @export

#'
#' @examples
#' \donttest{
#' # Build a minimal long-format expression data frame (self-contained)
#' box_long <- data.frame(
#'   Sample     = c("S1", "S2", "S3", "S4"),
#'   Group      = factor(c("untrt", "untrt", "trt", "trt"),
#'                       levels = c("untrt", "trt")),
#'   Expression = c(8.1, 8.3, 9.5, 9.7)
#' )
#' code <- generate_boxplot_image_code(
#'   box_data_long = box_long,
#'   gene_symbol   = "GAPDH",
#'   expr_type     = "logcpm",
#'   left_group    = "untrt",
#'   right_group   = "trt"
#' )
#' cat(code)
#' }
generate_boxplot_image_code <- function(box_data_long,

                                        gene_symbol = "Gene",

                                        expr_type = "Expression",

                                        left_group = NA_character_,

                                        right_group = NA_character_,

                                        use_zero_baseline = TRUE,

                                        custom_order = "default",

                                        base_size = 12,

                                        margin_top = 15, margin_bottom = 15,

                                        margin_left = 18, margin_right = 18, fig_width = 5, fig_height = 5, fig_dpi = 300) {

    # Subset to the three essential columns + drop NA groups, mirroring the

    # interactive plotly rendering path.

    keep_cols <- intersect(c("Sample", "Group", "Expression"),

                                                    colnames(box_data_long))

    box_subset <- box_data_long[, keep_cols, drop = FALSE]

    box_subset <- box_subset[!is.na(box_subset$Group), , drop = FALSE]

    data_literal <- paste(utils::capture.output(dput(box_subset)), collapse = "\n")



    safe_gene <- gsub("'", "''", gene_symbol %||% "")

    safe_expr <- gsub("'", "''", expr_type %||% "Expression")



    # Build the optional custom-ordering block (mirrors

    # generate_boxplot_data_code's logic, inlined for a self-contained script).

    has_custom_order <- !is.null(custom_order) &&

                                            length(custom_order) == 1 &&

                                            nzchar(custom_order) &&

                                            !identical(custom_order, "default")

    ordering_block <- if (has_custom_order) {

    safe_order <- gsub("'", "''", custom_order)

    paste0(

'# --- Custom group ordering (matches the Shiny boxplot) ----------------------\n',

'custom_order <- \'', safe_order, '\'\n',

'sep <- if (grepl("->", custom_order, fixed = TRUE)) "->" else ","\n',

'order_parts <- trimws(strsplit(custom_order, sep)[[1]])\n',

'actual_groups <- unique(as.character(df$Group))\n',

'valid_parts <- order_parts[order_parts %in% actual_groups]\n',

'x_categories <- c(valid_parts, setdiff(actual_groups, valid_parts))\n',

'df <- df[df$Group %in% x_categories, ]\n',

'df$Group <- factor(df$Group, levels = x_categories, ordered = TRUE)\n'

    )

    } else {

'# --- Group ordering (alphabetical by default) -------------------------------\n# df$Group <- factor(df$Group)\nx_categories <- unique(as.character(df$Group))\ndf$Group <- factor(df$Group, levels = x_categories)\n'

    }



    # Zero-baseline line block

    baseline_block <- if (isTRUE(use_zero_baseline)) {

'  geom_hline(yintercept = 0, linetype = "dashed",

                            color = "red", alpha = 0.7, linewidth = 0.8) +'

    } else {

'  # (zero baseline line disabled) #'

    }



    sprintf(

'# =============================================================================

# GSEAlens Publication Boxplot Code

# Generated by GSEAlens Shiny App

# =============================================================================

# Per-gene expression boxplot with jittered points, mirroring the

# interactive plotly view in the Quadrant module (panel 4).



library(ggplot2)



# --- Data --------------------------------------------------------------------

df <- %s



# --- Type coercion (defensive) -----------------------------------------------

if (!"Sample"     %%in%% names(df)) df$Sample     <- NA_character_

if (!"Group"      %%in%% names(df)) df$Group      <- NA_character_

if (!"Expression" %%in%% names(df)) df$Expression <- NA_real_

df$Sample     <- as.character(df$Sample)

df$Group      <- as.character(df$Group)

df$Expression <- suppressWarnings(as.numeric(df$Expression))

df <- df[!is.na(df$Group) & !is.na(df$Expression), ]



%s



# --- Group color palette -----------------------------------------------------

# Two-group case uses the same red/blue scheme as the DE volcano; otherwise

# a qualitative palette. Matches the interactive plotly fill mapping.

unique_groups <- levels(df$Group)

if (length(unique_groups) == 0) unique_groups <- unique(as.character(df$Group))

group_colors <- if (length(unique_groups) == 2) {

    setNames(c("#E41A1C", "#377EB8"), unique_groups[1:2])

} else {

    cols <- c("#E41A1C", "#377EB8", "#4DAF4A", "#984EA3",

            "#FF7F00", "#A65628", "#F781BF", "#999999")[seq_along(unique_groups)]

    setNames(cols, unique_groups)

}



# --- Y-axis range with headroom (mirrors interactive plotly) -----------------

y_min <- min(df$Expression, na.rm = TRUE)

y_max <- max(df$Expression, na.rm = TRUE)

%s

y_range <- y_max - y_min

y_min <- y_min - y_range * 0.1

y_max <- y_max + y_range * 0.1



p <- ggplot(df, aes(x = Group, y = Expression, fill = Group)) +

    geom_boxplot(alpha = 0.7, outlier.shape = NA) +

    geom_jitter(width = 0.2, size = 2, alpha = 0.6) +

    scale_fill_manual(values = group_colors) +

    scale_x_discrete(limits = levels(df$Group), drop = FALSE) +

%s

    coord_cartesian(ylim = c(y_min, y_max)) +

    theme_bw(base_size = %g) +

    labs(

    title = "%s",

    y = "%s",

    x = NULL

    ) +

    theme(

    legend.position = "none",

    axis.text.x = element_text(angle = 45, hjust = 1),

    plot.title = element_text(face = "bold", hjust = 0.5),

    plot.margin = margin(%d, %d, %d, %d)

    )



print(p)



# --- Save to file (uncomment one) --------------------------------------------

# ggsave("boxplot.pdf", p, width = %g, height = %g, device = cairo_pdf)

# ggsave("boxplot.png", p, width = %g, height = %g, dpi = %g)

# ggsave("boxplot.svg", p, width = %g, height = %g)

',

    data_literal,

    ordering_block,

    if (isTRUE(use_zero_baseline)) 'y_min <- min(y_min, 0)\ny_max <- max(y_max, 0)' else '# (zero baseline disabled)',

    baseline_block,

    base_size,

    safe_gene,

    safe_expr,

    margin_top, margin_right, margin_bottom, margin_left,

    fig_width, fig_height,

    fig_width, fig_height, fig_dpi,

    fig_width, fig_height

    )

}



