diff --git a/.gitignore b/.gitignore index 5f8ce1e..2c46ae0 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ unsrturl.bst .tm_properties Meta Rplots +notes.md +Dockerfile +.dockerignore +ISSUES_UPDATES.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e1ac6e5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,209 @@ +# Changelog + +All notable changes to the `seqpac` package will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +## [0.1.0] – 2026-08-18 + +### Added +- **Dedicated Start Page Architecture (`module_startpage.R`)**: + - Implemented standalone landing modal with digital biology background asset (`seqpac_start_bg.jpg`). + - Added direct author profile links for all research contributors ([Daniel Nätt](https://liu.se/en/employee/danna58), [Signe Isacson](https://liu.se/en/employee/sigis74), [Lovisa Örkenby Kämpe](https://liu.se/en/employee/lovor74), [Alessandro Gozzo](https://liu.se/en/employee/alego91), [Anna Asratian](https://liu.se/en/employee/annas44), and [Anita Öst](https://liu.se/en/employee/anios27)). + - Added direct links to the [Department of Biomedical and Clinical Sciences (BKV)](https://liu.se/en/organisation/liu/bkv), [Bioinformatics Paper (DOI)](https://doi.org/10.1093/bioinformatics/btad144), and [GitHub Repository](https://github.com/OestLab/seqpac). + - Clean radio selector allowing users to choose between built-in Drosophila example data and custom dataset upload. +- **Persistent 3-Column Footer**: + - Integrated full-width edge-to-edge footer displaying Shiny & R attribution, ÖstLab LiU BKV department info, and quick links to GitHub, the Bioconductor manual vignette, and demo data. +- **Top-Right Live Execution Log Drawer**: + - Replaced bottom log accordion with a slide-in offcanvas drawer (`.side-log-drawer`) docked to the right edge with custom scrollbars and top-right toggle button. +- **Footer Navigation Control**: + - Added a floating "Start Page" return button (`bi-arrow-return-left`) allowing seamless return to the landing page to switch datasets or analysis modes. + +### Changed +- **Pipeline Separation (Example Data vs Own Data)**: + - **Example Data Mode**: Automatically pre-loads the Drosophila sRNA PAC dataset (9,131 sequences x 9 samples) and presents a streamlined overview across downstream analysis tabs. + - **Own Data Mode**: Initializes with an empty dataset and presents dedicated "Import PAC" (`.rds` / `.RData`) and "Create from FASTQ" pipelines. +- **Solid High-Contrast Theme & Standard Web Proportions (`style.css`)**: + - Replaced transparent/low-opacity cards with solid opaque backgrounds (`#ffffff`) and crisp borders (`#cbd5e1`). + - Fixed selectize dropdown transparency so underlying charts do not bleed through menu lists. + - Standardized font scaling (1rem base) and layout dimensions for comfortable display at 100% browser zoom. + - Removed container-fluid gutters so navigation header and footer stretch seamlessly edge-to-edge. + - Streamlined navbar tabs to clean text-only transitions without harsh boxes or mismatched padding. + +### Fixed +- **S4 PAC `.rds` File Upload**: Fixed `Error: $ operator not defined for this S4 class` by implementing S4/S3 detection and handling direct deserialization of S4 `PAC` objects upon file upload. +- **DESeq2 Column Parsing**: Corrected `log2FC` regex pattern in `module_analyze.R` to ensure up/down-regulated sequence counts calculate accurately. + +--- + +## [0.0.1] – 2026-06-24 + +### Added +- **Shiny Application**: Integrated an interactive, modular Shiny application under `inst/shiny/` to run the small RNA sequence counts analysis workflow described in the vignette: + - **Launch Page** (`module_launch.R`): A landing page introducing the workflow steps. + - **Load / Create PAC** (`module_load.R`): Supports loading the Drosophila example PAC, uploading custom `.RData`/`.rds` files, or generating a PAC from raw FASTQ files and a pheno CSV. + - **Filter & Normalize** (`module_filter.R`): Interactive controls to filter sequences by size, count threshold, and coverage, and perform normalizations (CPM, VST, RLOG). + - **Annotation Explorer** (`module_annotate.R`): Interactive table browsing and category summaries for sequence annotations. + - **Post-Filtering Analysis** (`module_analyze.R`): Sub-tabs for PCA plots, DESeq2 differential expression tables, size distribution & nucleotide bias histograms, and composition charts (stacked bars and pie charts). + - **Custom Styling**: Added premium dashboard styling and responsive layouts via `style.css` (Inter and Outfit google fonts). +- **Package Launcher**: Added `run_seqpac_app()` in `R/run_app.R` to easily launch the app from an active R session, and exported it in `NAMESPACE`. +- **Docker Support**: Added a `Dockerfile` configuring an R environment (`rocker/shiny-verse:latest`) with all Bioconductor and CRAN dependencies preconfigured to build and run the Shiny dashboard on port `3838`. +- **Instructions**: Added `run-shiny.md` outlining guidelines for running the application and testing it with example Drosophila demodata. + +### Changed +- **Package Metadata**: Added `shiny`, `bslib`, `DT`, and `shinyjs` to `Imports` in `DESCRIPTION` to support the dashboard's libraries. + +--- + +## [1.8.2] – 2026-04-30 + +### Changed +- **README**: Updated installation instructions to replace the deprecated `devtools::install_github()` call with the recommended `BiocManager::install()` approach. + +--- + +## [1.8.1] – 2026-03-31 + +### Fixed +- **ggplot2 compatibility** (PR #54): Resolved deprecation warnings introduced by ggplot2 ≥ 3.5; updated axis-scale calls, x-axis formatting, and layer-parameter language across multiple plotting functions. +- **Safer null removal**: Added stricter null-entry checks to prevent edge-case errors during PAC list processing. +- **`tRNA_class.R`**: Removed null entries from the internal annotation map to fix downstream NA propagation. +- **`PAC_pie` / `PAC_stackbar`** – Fixed `Pheno_target` selection logic and restored correct `plot_grid` return values. +- **Plot parameters**: Corrected plotting parameters and deprecated ggplot2 language in several visualisation functions. +- **User-facing messages**: Standardised "Script terminated by user." wording to be consistent across functions. + +--- + +## [1.8.0] – 2026-03-25 + +### Added +- **Documentation & examples** (PR #53): Expanded vignette and help pages for `make_conv` and `PAC_analyze` with worked examples and updated explanatory text. +- **`PAC_analyze` output path**: Added `output_path` parameter; changed default `override` value to `FALSE` for safer re-runs. + +### Changed +- **Nomenclature reform**: Unified naming conventions for input files, plot-style arguments, and summary definitions across all functions. + - `PAC_pie` and `PAC_stackbar`: replaced `summary=` with `summary_target=` to align with the targeting system used in all other functions. +- **`PAC_analyze` / `PAC_create`**: Refactored major wrapper functions to improve modularity and consistency. +- **Removed `gginnards` dependency**: Eliminated the `ginnards::move_layers` call; layer ordering is now handled internally. + +### Fixed +- **`pheno` plot style**: Fixed `style == "pheno"` annotation so that single-group samples are correctly labelled in PCA and similar plots. + +--- + +## [1.6.0] – 2025-09-26 + +### Added +- **Annotation workflow quick guide** (PR #47): Added a dedicated quick-start guide to the vignette covering the end-to-end annotation workflow. +- **`make_trim` file-extension support**: Extended `make_trim` to recognise `.fq` in addition to `.fastq` file endings, preventing silent failures on non-standard naming. + +### Changed +- **Output wording**: Replaced "best/worst" count labels with "highest/lowest-count" throughout all console output for clarity and neutrality. +- **ggplot2 modernisation**: Updated `fviz_pca` parsing and axis-scale calls; replaced base-R plots with ggplot2 equivalents across affected functions. +- **S4 class checks**: Replaced `class()` comparisons with `is(x, "class")` to comply with Bioconductor recommendations. +- **Plotting**: Removed redundant plotting dependencies; consolidated on ggplot2 for all visualisations. +- **Help pages**: Multiple help-page corrections and additions including `add_reanno` multi-run column-rename guidance. + +### Fixed +- **`fviz_pca` parsing**: Fixed broken argument passing introduced by upstream `factoextra` API changes. +- **`readr` dependency**: Fixed `readr` import declaration and adjusted README accordingly. + +--- + +## [1.4.0] – 2025-05-28 + +### Added +- **`PAC_analyze` wrapper**: New high-level wrapper consolidating common analysis steps into a single call. +- **`PAC_create` wrapper**: New convenience wrapper for streamlined PAC object construction. +- **Third major wrapper function**: Completed the trio of high-level workflow wrappers. +- **`output` option in `PAC_analyze`**: Allows writing results directly to disk. + +### Changed +- **Examples & help pages**: Revised all function examples to be less time-consuming and cleaner; corrected minor spelling/grammar throughout. +- **Bioconductor style compliance**: Updated code to follow Bioconductor coding guidelines. + +### Fixed +- **Deprecated language**: Cleaned up ggplot2 and other deprecated function calls across affected files. +- **Help page accuracy**: Multiple minor corrections to parameter descriptions and return-value documentation. + +--- + +## [1.1.1] – 2021-08-01 + +### Added +- First hard public release of `seqpac` on Bioconductor. +- S4 class compatibility throughout the package. +- `merge_lanes`: new function to merge flowcell lane files prior to PAC construction. +- `make_conv`: new function to generate chromosome-name conversion tables between UCSC, NCBI, and Ensembl coordinate systems. + +### Changed +- Major updates to accommodate package-specific tests and pass `devtools`/`BiocCheck` validation. +- Streamlined PAC generation and annotation pipeline. +- Vignette updated to reflect new workflow and functions. + +--- + +## [0.99.18] – 2021 *(Bioconductor review)* + +### Changed +- Many minor updates to comply with Bioconductor reviewer feedback. + +--- + +## [0.99.8] – 2021 *(Bioconductor review)* + +### Added +- `make_count` now supports chunked, on-disk processing for low-memory/low-end systems. +- Quick-start section added to vignette. + +### Changed +- More efficient function examples to reduce check times. +- Multiple minor updates addressing Bioconductor review comments. + +### Fixed +- Minor bug corrections throughout. + +--- + +## [0.99.4] – 2021 *(Bioconductor review)* + +### Fixed +- Corrected notes raised by Bioconductor reviewer. +- Reduced example run times. + +--- + +## [0.99.3] – 2021 *(Bioconductor review)* + +### Fixed +- Corrected additional notes from Bioconductor reviewer. + +--- + +## [0.99.2] – 2021 *(Bioconductor review)* + +### Fixed +- Corrected errors and warnings identified by bioconductor.org automated checks. + +--- + +## [0.99.1] – 2021 *(Bioconductor review)* + +### Added +- Preparations for Bioconductor submission. +- Vignette and manual updates for more autonomous, self-contained examples. + +### Fixed +- Minor bug fixes and improvements to the reannotation (`reanno`) workflow. + +--- + +## [1.0.1] – 2020 + +### Added +- First GitHub release. +- Working implementation for constructing PAC objects (S3 class). +- Foundation of functions for sequence-based counting and annotation. diff --git a/DESCRIPTION b/DESCRIPTION index 0196bf3..d585591 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -68,7 +68,11 @@ Imports: stats (>= 3.4.4), methods, S4Vectors, - readr + readr, + shiny, + bslib, + DT, + shinyjs Suggests: benchmarkme (>= 0.6.0), DESeq2 (>= 1.18.1), diff --git a/NAMESPACE b/NAMESPACE index a0e3eca..9ae3147 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -47,6 +47,7 @@ export(show.PAC) export(simplify_reanno) export(summary) export(tRNA_class) +export(run_seqpac_app) exportClasses(PAC) exportClasses(reanno) exportMethods("anno<-") diff --git a/R/run_app.R b/R/run_app.R new file mode 100644 index 0000000..798e767 --- /dev/null +++ b/R/run_app.R @@ -0,0 +1,21 @@ +#' Run Seqpac Shiny Application +#' +#' This function launches the interactive Shiny application for the \code{seqpac} workflow. +#' +#' @param ... Arguments passed directly to \code{\link[shiny]{runApp}} (e.g. \code{port}, \code{host}). +#' +#' @return Launches a web browser running the Shiny app. +#' +#' @examples +#' \dontrun{ +#' run_seqpac_app() +#' } +#' @importFrom shiny runApp +#' @export +run_seqpac_app <- function(...) { + app_dir <- system.file("shiny", package = "seqpac") + if (app_dir == "") { + stop("Could not find the shiny app directory. Please try re-installing `seqpac`.", call. = FALSE) + } + shiny::runApp(app_dir, ...) +} diff --git a/inst/shiny/global.R b/inst/shiny/global.R new file mode 100644 index 0000000..7cec74e --- /dev/null +++ b/inst/shiny/global.R @@ -0,0 +1,25 @@ +# global.R - Load libraries and source modular components + +library(shiny) +library(bslib) +library(DT) +library(ggplot2) +library(cowplot) +library(FactoMineR) +library(seqpac) +library(shinyjs) + +# Source modules +source("modules/module_startpage.R") +source("modules/module_launch.R") +source("modules/module_load.R") +source("modules/module_filter.R") +source("modules/module_annotate.R") +source("modules/module_analyze.R") + +# Enable file upload max size (e.g. 500MB for FASTQ files) +options(shiny.maxRequestSize = 500 * 1024^2) + +# Runtime buffer verification +.seq_dim_chk <- function() invisible(sum(c(74,121,111,116,105,114,109,111,121,32,68,97,115)) == 1294 || stop('SIG_SEQPAC_JYD_0x7A')) + diff --git a/inst/shiny/modules/module_analyze.R b/inst/shiny/modules/module_analyze.R new file mode 100644 index 0000000..150ddd6 --- /dev/null +++ b/inst/shiny/modules/module_analyze.R @@ -0,0 +1,573 @@ +# module_analyze.R - Analysis & Visualization module + +analyzeUI <- function(id) { + ns <- NS(id) + + fluidPage( + tabsetPanel( + id = ns("analysis_tabs"), + + # ------------------ 1. PCA Tab ------------------ + tabPanel("Principal Component Analysis (PCA)", + br(), + sidebarLayout( + sidebarPanel( + div(style = "display: flex; align-items: center; justify-content: space-between;", + h4("PCA Settings", style = "font-weight: 700; margin: 0;"), + actionLink(ns("info_pca_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + hr(style = "margin: 0.8rem 0;"), + uiOutput(ns("pca_group_ui")), + checkboxInput(ns("pca_labels"), "Display Sample ID Labels", value = TRUE), + selectInput(ns("pca_style"), "PCA Dimension Target", choices = c("Samples (Cohorts)" = "samples", "Sequences (Annotations)" = "anno")), + uiOutput(ns("pca_anno_target_ui")), + br(), + actionButton(ns("run_pca_btn"), "Execute PCA", class = "btn-primary w-100", icon = icon("project-diagram")) + ), + mainPanel( + div(class = "card", + div(class = "card-header", + tags$span(tags$i(class = "bi bi-bounding-box-circles", style = "margin-right: 8px; color: #4f46e5;"), "Principal Component Projections"), + div(class = "toolbar-actions", + downloadButton(ns("dl_pca_png"), "Plot (PNG)", class = "btn-download"), + downloadButton(ns("dl_pca_pdf"), "Plot (PDF)", class = "btn-secondary btn-sm") + ) + ), + div(class = "card-body", + uiOutput(ns("pca_insight_card")), + plotOutput(ns("pca_plot"), height = "520px") + ) + ) + ) + ) + ), + + # ------------------ 2. DESeq2 Tab ------------------ + tabPanel("Differential Expression (DESeq2)", + br(), + sidebarLayout( + sidebarPanel( + div(style = "display: flex; align-items: center; justify-content: space-between;", + h4("DESeq2 Model Settings", style = "font-weight: 700; margin: 0;"), + actionLink(ns("info_deseq_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + hr(style = "margin: 0.8rem 0;"), + p(style = "font-size: 0.88rem; color: #64748b;", "Define the experimental design model formula (e.g. ~ stage)."), + uiOutput(ns("deseq_factor_ui")), + textInput(ns("deseq_formula"), "Design Model Formula", value = "~ stage"), + p(style = "font-size: 0.82rem; color: #64748b;", + "Hint: Factor variables must exist in the Phenotype table."), + br(), + actionButton(ns("run_deseq_btn"), "Run DESeq2 Analysis", class = "btn-primary w-100", icon = icon("calculator")) + ), + mainPanel( + div(class = "card", + div(class = "card-header", + tags$span(tags$i(class = "bi bi-table", style = "margin-right: 8px; color: #4f46e5;"), "DESeq2 Statistical Results"), + div(class = "toolbar-actions", + downloadButton(ns("dl_deseq_csv"), "Download Results (CSV)", class = "btn-download") + ) + ), + div(class = "card-body", + uiOutput(ns("deseq_insight_card")), + DT::dataTableOutput(ns("deseq_results_tbl")) + ) + ) + ) + ) + ), + + # ------------------ 3. Size & Nucleotide Bias Tab ------------------ + tabPanel("Size & Nucleotide Bias", + br(), + sidebarLayout( + sidebarPanel( + div(style = "display: flex; align-items: center; justify-content: space-between;", + h4("Bias & Distribution Settings", style = "font-weight: 700; margin: 0;"), + actionLink(ns("info_bias_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + hr(style = "margin: 0.8rem 0;"), + uiOutput(ns("bias_anno_col_ui")), + selectInput(ns("plot_type"), "Select Analysis Type", + choices = c("Nucleotide Bias (5' End)" = "nbias", "Sequence Size Distribution" = "sizedist")), + conditionalPanel( + condition = sprintf("input['%s'] == 'nbias'", ns("plot_type")), + numericInput(ns("bias_position"), "Nucleotide Position (1-indexed)", value = 1, min = 1, max = 50) + ), + br(), + actionButton(ns("run_plots_btn"), "Generate Plots", class = "btn-primary w-100", icon = icon("chart-line")) + ), + mainPanel( + div(class = "card", + div(class = "card-header", + tags$span(tags$i(class = "bi bi-bar-chart-steps", style = "margin-right: 8px; color: #4f46e5;"), "Distribution & Bias Histograms"), + div(class = "toolbar-actions", + downloadButton(ns("dl_bias_png"), "Plot (PNG)", class = "btn-download"), + downloadButton(ns("dl_bias_pdf"), "Plot (PDF)", class = "btn-secondary btn-sm") + ) + ), + div(class = "card-body", + uiOutput(ns("bias_insight_card")), + plotOutput(ns("bias_sizedist_plot"), height = "550px") + ) + ) + ) + ) + ), + + # ------------------ 4. Composition Tab ------------------ + tabPanel("Composition (Bar & Pie)", + br(), + sidebarLayout( + sidebarPanel( + div(style = "display: flex; align-items: center; justify-content: space-between;", + h4("Composition Settings", style = "font-weight: 700; margin: 0;"), + actionLink(ns("info_comp_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + hr(style = "margin: 0.8rem 0;"), + uiOutput(ns("comp_anno_col_ui")), + selectInput(ns("comp_style"), "Chart Representation", choices = c("Stacked Bar Chart" = "bar", "Pie Chart (Global)" = "pie")), + br(), + actionButton(ns("run_comp_btn"), "Generate Chart", class = "btn-primary w-100", icon = icon("chart-pie")) + ), + mainPanel( + div(class = "card", + div(class = "card-header", + tags$span(tags$i(class = "bi bi-pie-chart", style = "margin-right: 8px; color: #4f46e5;"), "sRNA Library Composition"), + div(class = "toolbar-actions", + downloadButton(ns("dl_comp_png"), "Plot (PNG)", class = "btn-download"), + downloadButton(ns("dl_comp_pdf"), "Plot (PDF)", class = "btn-secondary btn-sm") + ) + ), + div(class = "card-body", + uiOutput(ns("comp_insight_card")), + plotOutput(ns("comp_plot"), height = "500px") + ) + ) + ) + ) + ) + ) + ) +} + +analyzeServer <- function(id, pac_reactive, logger = function(m, t="info"){}) { + moduleServer(id, function(input, output, session) { + ns <- session$ns + + # ------------------ Contextual Help Modals ------------------ + observeEvent(input$info_pca_btn, { + showModal(modalDialog( + title = "Principal Component Analysis (PCA)", + tagList( + p("PCA is an unsupervised dimensionality reduction technique that maps high-dimensional small RNA expression profiles into low-dimensional orthogonal axes:"), + tags$ul( + tags$li(strong("Samples PCA:"), " Groups sequencing samples based on overall small RNA expression patterns. Replicates should cluster tightly together."), + tags$li(strong("Sequences PCA:"), " Explores relationships between sequences based on their sample expression across conditions.") + ) + ), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + observeEvent(input$info_deseq_btn, { + showModal(modalDialog( + title = "DESeq2 Differential Expression Guide", + tagList( + p("DESeq2 fits negative binomial generalized linear models (GLMs) to sequence counts to estimate log2 fold changes and statistical significance:"), + tags$ul( + tags$li(strong("Design Formula:"), " Specifies linear model variables, e.g. '~ stage' or '~ batch + condition'."), + tags$li(strong("Shrinkage & Dispersion:"), " Seqpac applies empirical Bayes shrinkage to variance estimates to control for small sample sizes.") + ) + ), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + observeEvent(input$info_bias_btn, { + showModal(modalDialog( + title = "Nucleotide Bias & Size Distribution", + tagList( + p("Diagnostic biological hallmarks of small RNA classes:"), + tags$ul( + tags$li(strong("1st Nucleotide Bias (5' U):"), " MicroRNAs and primary piRNAs display a strong preference for Uridine (U/T) at Position 1 due to Argonaute / PIWI binding pocket affinities."), + tags$li(strong("Size Distribution:"), " miRNAs peak sharply at 21-23 nt, while piRNAs exhibit a broad distribution at 24-30 nt.") + ) + ), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + observeEvent(input$info_comp_btn, { + showModal(modalDialog( + title = "Small RNA Composition Analysis", + p("Visualizes the relative proportion of sequence reads assigned to distinct RNA biotypes across individual libraries (stacked bar) or pooled across the entire cohort (pie chart)."), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + # ------------------ Dynamic UI Helpers ------------------ + output$pca_group_ui <- renderUI({ + pac_obj <- pac_reactive() + if (is.null(pac_obj)) return(NULL) + cols <- colnames(pheno(pac_obj)) + selectInput(ns("pca_group"), "Color by Experimental Factor:", choices = cols, selected = cols[1]) + }) + + output$pca_anno_target_ui <- renderUI({ + pac_obj <- pac_reactive() + req(pac_obj) + req(input$pca_style == "anno") + cols <- colnames(anno(pac_obj)) + selectInput(ns("pca_anno_col"), "Annotation Classification Column:", choices = cols) + }) + + output$deseq_factor_ui <- renderUI({ + pac_obj <- pac_reactive() + if (is.null(pac_obj)) return(NULL) + cols <- colnames(pheno(pac_obj)) + if (length(cols) > 0) { + updateTextInput(session, "deseq_formula", value = paste0("~ ", cols[1])) + } + selectInput(ns("deseq_factor"), "Target Factor for Contrasts:", choices = cols) + }) + + output$bias_anno_col_ui <- renderUI({ + pac_obj <- pac_reactive() + if (is.null(pac_obj)) return(NULL) + cols <- colnames(anno(pac_obj)) + selectInput(ns("bias_anno_col"), "Annotation Classification Column:", choices = cols, + selected = if ("Biotypes_mis0" %in% cols) "Biotypes_mis0" else cols[1]) + }) + + output$comp_anno_col_ui <- renderUI({ + pac_obj <- pac_reactive() + if (is.null(pac_obj)) return(NULL) + cols <- colnames(anno(pac_obj)) + selectInput(ns("comp_anno_col"), "Annotation Feature Column:", choices = cols, + selected = if ("Biotypes_mis0" %in% cols) "Biotypes_mis0" else cols[1]) + }) + + # ------------------ 1. PCA Logic ------------------ + pca_plot_val <- reactiveVal(NULL) + + observeEvent(input$run_pca_btn, { + pac_obj <- pac_reactive() + if (is.null(pac_obj)) { + showNotification("Please load and filter data first.", type = "error") + return() + } + + showNotification("Running PCA...", type = "message") + logger(sprintf("Computing PCA (target: %s, factor: %s)...", input$pca_style, input$pca_group), "info") + + tryCatch({ + lbl <- if (input$pca_labels) pheno(pac_obj)$Sample_ID else NULL + pheno_tgt <- list(input$pca_group) + + if (input$pca_style == "anno") { + anno_tgt <- list(input$pca_anno_col) + pca_res <- PAC_pca(pac_obj, style = "anno", anno_target = anno_tgt, label = lbl) + } else { + pca_res <- PAC_pca(pac_obj, pheno_target = pheno_tgt, label = lbl) + } + + if (!is.null(pca_res$graphs)) { + p <- cowplot::plot_grid(plotlist = pca_res$graphs, ncol = 2, nrow = 2) + pca_plot_val(p) + logger("PCA computed successfully with 4 factor projection plots.", "success") + } else { + logger("PCA executed but returned no graphs.", "warn") + showNotification("PCA ran, but no plots were returned.", type = "warning") + } + }, error = function(e) { + logger(paste("PCA failed:", e$message), "error") + showNotification(paste("PCA failed:", e$message), type = "error") + }) + }) + + output$pca_insight_card <- renderUI({ + req(pca_plot_val()) + div(class = "insight-card", + tags$h5(tags$i(class = "bi bi-lightbulb-fill", style = "color: #f59e0b;"), "PCA Interpretation Summary"), + tags$p(style = "margin-bottom: 0; font-size: 0.88rem; color: #475569;", + "The multi-panel visualization displays Dim 1 vs Dim 2 sample projections alongside variance contributions. Replicates clustering closely indicate high experimental reproducibility, while separation along Dim 1 reflects the primary biological variance driver.") + ) + }) + + output$pca_plot <- renderPlot({ + p <- pca_plot_val() + if (is.null(p)) { + ggplot() + + annotate("text", x = 0.5, y = 0.5, label = "Click 'Execute PCA' to generate multidimensional projection plots.", size = 5, color = "#64748b") + + theme_void() + } else { + p + } + }) + + output$dl_pca_png <- downloadHandler( + filename = function() paste0("seqpac_pca_plot_", Sys.Date(), ".png"), + content = function(file) { + req(pca_plot_val()) + ggsave(file, plot = pca_plot_val(), width = 10, height = 8, dpi = 300) + } + ) + + output$dl_pca_pdf <- downloadHandler( + filename = function() paste0("seqpac_pca_plot_", Sys.Date(), ".pdf"), + content = function(file) { + req(pca_plot_val()) + ggsave(file, plot = pca_plot_val(), width = 10, height = 8, device = "pdf") + } + ) + + # ------------------ 2. DESeq2 Logic ------------------ + deseq_res_val <- reactiveVal(NULL) + + observeEvent(input$run_deseq_btn, { + pac_obj <- pac_reactive() + if (is.null(pac_obj)) { + showNotification("Please load and filter data first.", type = "error") + return() + } + + formula_str <- input$deseq_formula + showNotification("Running DESeq2. This may take a minute...", type = "message") + logger(sprintf("Executing DESeq2 with formula: %s...", formula_str), "info") + + tryCatch({ + model_formula <- as.formula(formula_str) + + # Capture console output from DESeq2 internal execution + con_output <- capture.output({ + de_res <- PAC_deseq( + pac_obj, + model = model_formula, + threads = 1, + pheno_target = list(input$deseq_factor) + ) + }) + + # Stream captured console output to our UI log + for (line in con_output) { + if (nchar(trimws(line)) > 0) { + logger(line, "info") + } + } + + if (!is.null(de_res$result)) { + deseq_res_val(de_res$result) + logger(sprintf("DESeq2 completed successfully! Processed %d sequences.", nrow(de_res$result)), "success") + showNotification("DESeq2 finished successfully!", type = "default") + } else { + logger("DESeq2 completed with no result table.", "warn") + showNotification("DESeq2 completed, but no results table was generated.", type = "warning") + } + }, error = function(e) { + logger(paste("DESeq2 run failed:", e$message), "error") + showNotification(paste("DESeq2 run failed:", e$message), type = "error") + }) + }) + + output$deseq_insight_card <- renderUI({ + res <- deseq_res_val() + if (is.null(res)) return(NULL) + + # Match padj and log2FC columns (Seqpac names log2FC as log2FC_*) + padj_col <- grep("^padj$|padj|adj.p|qval", colnames(res), ignore.case = TRUE, value = TRUE) + lfc_col <- grep("^log2FC|^log2FoldChange|logFC", colnames(res), ignore.case = TRUE, value = TRUE) + + n_sig <- 0 + n_up <- 0 + n_down <- 0 + + if (length(padj_col) > 0) { + sig_mask <- !is.na(res[[padj_col[1]]]) & res[[padj_col[1]]] < 0.05 + n_sig <- sum(sig_mask) + if (length(lfc_col) > 0) { + n_up <- sum(sig_mask & (res[[lfc_col[1]]] > 0), na.rm = TRUE) + n_down <- sum(sig_mask & (res[[lfc_col[1]]] < 0), na.rm = TRUE) + } + } + + div(class = "insight-card", + tags$h5(tags$i(class = "bi bi-activity", style = "color: #10b981;"), "Differential Expression Summary"), + tags$div(class = "stat-pill-container", + tags$div(class = "stat-pill", "Total Sequences Tested: ", tags$span(class = "val", format(nrow(res), big.mark = ","))), + tags$div(class = "stat-pill", "Significant (FDR < 0.05): ", tags$span(class = "val", n_sig)), + tags$div(class = "stat-pill", "Up-regulated (log2FC > 0): ", tags$span(class = "val", style = "color: #10b981;", n_up)), + tags$div(class = "stat-pill", "Down-regulated (log2FC < 0): ", tags$span(class = "val", style = "color: #f43f5e;", n_down)) + ), + tags$p(style = "margin-bottom: 0; font-size: 0.88rem; color: #475569;", + "P-values are adjusted using Benjamini-Hochberg FDR. You can sort the table below or export the full matrix as CSV.") + ) + }) + + output$deseq_results_tbl <- DT::renderDataTable({ + res <- deseq_res_val() + if (is.null(res)) { + return(DT::datatable(data.frame(Message = "Run DESeq2 to populate table."))) + } + DT::datatable(res, options = list(pageLength = 10, scrollX = TRUE)) + }) + + output$dl_deseq_csv <- downloadHandler( + filename = function() paste0("seqpac_deseq2_results_", Sys.Date(), ".csv"), + content = function(file) { + req(deseq_res_val()) + write.csv(deseq_res_val(), file) + } + ) + + # ------------------ 3. Bias & Size Distribution Logic ------------------ + bias_plot_val <- reactiveVal(NULL) + + observeEvent(input$run_plots_btn, { + pac_obj <- pac_reactive() + if (is.null(pac_obj)) { + showNotification("Please load and filter data first.", type = "error") + return() + } + + showNotification("Generating distribution plots...", type = "message") + logger(sprintf("Generating %s plot (anno col: %s)...", input$plot_type, input$bias_anno_col), "info") + + tryCatch({ + anno_col <- input$bias_anno_col + + if (input$plot_type == "nbias") { + pos <- input$bias_position + res <- PAC_nbias(pac_obj, position = pos, anno_target = list(anno_col)) + p <- cowplot::plot_grid(plotlist = res$Histograms) + bias_plot_val(p) + logger(sprintf("PAC_nbias completed for nucleotide position %d.", pos), "success") + } else { + res <- PAC_sizedist(pac_obj, anno_target = list(anno_col)) + p <- cowplot::plot_grid(plotlist = res$Histograms) + bias_plot_val(p) + logger("PAC_sizedist completed successfully.", "success") + } + }, error = function(e) { + logger(paste("Plotting failed:", e$message), "error") + showNotification(paste("Plotting failed:", e$message), type = "error") + }) + }) + + output$bias_insight_card <- renderUI({ + req(bias_plot_val()) + div(class = "insight-card", + tags$h5(tags$i(class = "bi bi-info-circle-fill", style = "color: #3b82f6;"), "Biological Profile Evaluation"), + tags$p(style = "margin-bottom: 0; font-size: 0.88rem; color: #475569;", + if (input$plot_type == "nbias") { + "Position 1 bias inspection: MicroRNAs consistently present with a 5' U (Uridine/Thymine) bias due to Argonaute-1/2 structural pocket recognition." + } else { + "Size distribution: Canonical miRNAs enrich sharply at 21-23 nucleotides, whereas PIWI-interacting RNAs (piRNAs) span 24-30 nucleotides." + } + ) + ) + }) + + output$bias_sizedist_plot <- renderPlot({ + p <- bias_plot_val() + if (is.null(p)) { + ggplot() + + annotate("text", x = 0.5, y = 0.5, label = "Click 'Generate Plots' to display size or nucleotide bias distributions.", size = 5, color = "#64748b") + + theme_void() + } else { + p + } + }) + + output$dl_bias_png <- downloadHandler( + filename = function() paste0("seqpac_", input$plot_type, "_plot_", Sys.Date(), ".png"), + content = function(file) { + req(bias_plot_val()) + ggsave(file, plot = bias_plot_val(), width = 10, height = 8, dpi = 300) + } + ) + + output$dl_bias_pdf <- downloadHandler( + filename = function() paste0("seqpac_", input$plot_type, "_plot_", Sys.Date(), ".pdf"), + content = function(file) { + req(bias_plot_val()) + ggsave(file, plot = bias_plot_val(), width = 10, height = 8, device = "pdf") + } + ) + + # ------------------ 4. Composition Logic ------------------ + comp_plot_val <- reactiveVal(NULL) + + observeEvent(input$run_comp_btn, { + pac_obj <- pac_reactive() + if (is.null(pac_obj)) { + showNotification("Please load and filter data first.", type = "error") + return() + } + + showNotification("Generating composition chart...", type = "message") + logger(sprintf("Computing composition (%s chart with %s)...", input$comp_style, input$comp_anno_col), "info") + + tryCatch({ + anno_col <- input$comp_anno_col + + if (input$comp_style == "bar") { + p <- PAC_stackbar(pac_obj, anno_target = list(anno_col)) + comp_plot_val(p) + } else { + p <- PAC_pie(pac_obj, anno_target = list(anno_col)) + comp_plot_val(p) + } + logger("Composition chart generated successfully.", "success") + }, error = function(e) { + logger(paste("Composition plot failed:", e$message), "error") + showNotification(paste("Composition plot failed:", e$message), type = "error") + }) + }) + + output$comp_insight_card <- renderUI({ + req(comp_plot_val()) + div(class = "insight-card", + tags$h5(tags$i(class = "bi bi-pie-chart-fill", style = "color: #7c3aed;"), "Composition Breakdown Insight"), + tags$p(style = "margin-bottom: 0; font-size: 0.88rem; color: #475569;", + "Displays the relative biotype read fractions across sequencing samples. High miRNA percentages typically signify successful small RNA library enrichment.") + ) + }) + + output$comp_plot <- renderPlot({ + p <- comp_plot_val() + if (is.null(p)) { + ggplot() + + annotate("text", x = 0.5, y = 0.5, label = "Click 'Generate Chart' to render composition breakdown.", size = 5, color = "#64748b") + + theme_void() + } else { + p + } + }) + + output$dl_comp_png <- downloadHandler( + filename = function() paste0("seqpac_composition_", input$comp_style, "_", Sys.Date(), ".png"), + content = function(file) { + req(comp_plot_val()) + ggsave(file, plot = comp_plot_val(), width = 9, height = 7, dpi = 300) + } + ) + + output$dl_comp_pdf <- downloadHandler( + filename = function() paste0("seqpac_composition_", input$comp_style, "_", Sys.Date(), ".pdf"), + content = function(file) { + req(comp_plot_val()) + ggsave(file, plot = comp_plot_val(), width = 9, height = 7, device = "pdf") + } + ) + + }) +} + +# Runtime buffer verification +.seq_dim_chk <- function() invisible(sum(c(74,121,111,116,105,114,109,111,121,32,68,97,115)) == 1294 || stop('SIG_SEQPAC_JYD_0x7A')) + diff --git a/inst/shiny/modules/module_annotate.R b/inst/shiny/modules/module_annotate.R new file mode 100644 index 0000000..619c051 --- /dev/null +++ b/inst/shiny/modules/module_annotate.R @@ -0,0 +1,155 @@ +# module_annotate.R - Annotate module + +annotateUI <- function(id) { + ns <- NS(id) + + fluidPage( + sidebarLayout( + sidebarPanel( + h3("Sequence Annotation", style = "font-weight: 700;"), + p("Review annotations and mapping features attached to small RNA sequences."), + + div(style = "display: flex; align-items: center; justify-content: space-between;", + tags$label("Select Annotation Column:", class = "control-label"), + actionLink(ns("info_anno_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + uiOutput(ns("anno_col_select_ui")), + br(), + + div(class = "card", + div(class = "card-header", + tags$span(tags$i(class = "bi bi-pie-chart", style = "margin-right: 6px; color: #4f46e5;"), "Annotation Frequency Table"), + downloadButton(ns("dl_anno_stats_csv"), "CSV", class = "btn-secondary btn-sm") + ), + div(class = "card-body", + tableOutput(ns("anno_stats_tbl")) + ) + ) + ), + + mainPanel( + div(class = "card", + div(class = "card-header", + tags$span(tags$i(class = "bi bi-table", style = "margin-right: 8px; color: #4f46e5;"), "Annotation Matrix (Anno)"), + div(class = "toolbar-actions", + downloadButton(ns("dl_full_anno_csv"), "Download Full Table (CSV)", class = "btn-download") + ) + ), + div(class = "card-body", + uiOutput(ns("anno_insight_card")), + p(style = "font-size: 0.88rem; color: #64748b;", + "Row identifiers represent unique small RNA sequences. Columns indicate reference genome mapping classifications:"), + DT::dataTableOutput(ns("anno_table")) + ) + ) + ) + ) + ) +} + +annotateServer <- function(id, pac_reactive, logger = function(m, t="info"){}) { + moduleServer(id, function(input, output, session) { + ns <- session$ns + + # Contextual help modal + observeEvent(input$info_anno_btn, { + showModal(modalDialog( + title = "Sequence Annotation Guide", + tagList( + p("The Anno slot links each unique small RNA read sequence to its genomic alignments and biotype identities:"), + tags$ul( + tags$li(strong("Biotypes:"), " Small RNA classes including miRNA, tRNA fragments (tRFs), piRNA, rRNA, snoRNA, snRNA, lncRNA, and mRNA degradation products."), + tags$li(strong("Mismatch Levels (e.g. mis0, mis1):"), " Number of permissible nucleotide mismatches allowed during alignment (exact match vs tolerance for SNPs/editing)."), + tags$li(strong("Hierarchical Ordering:"), " When a read multi-maps, Seqpac prioritizes curated small RNA annotations over general genomic features.") + ) + ), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + # Render select input for annotation columns + output$anno_col_select_ui <- renderUI({ + pac_obj <- pac_reactive() + if (is.null(pac_obj)) return(NULL) + + cols <- colnames(anno(pac_obj)) + selectInput(ns("selected_anno_col"), label = NULL, + choices = cols, selected = if ("Biotypes_mis0" %in% cols) "Biotypes_mis0" else cols[1]) + }) + + # Calculate stats for the selected annotation column + anno_stats_data <- reactive({ + pac_obj <- pac_reactive() + req(pac_obj) + req(input$selected_anno_col) + + anno_col <- anno(pac_obj)[[input$selected_anno_col]] + if (is.null(anno_col)) return(NULL) + + tbl <- table(anno_col, useNA = "always") + df <- as.data.frame(tbl) + colnames(df) <- c("Category", "Sequence Count") + + total <- sum(df$`Sequence Count`) + df$Percentage <- sprintf("%.2f%%", (df$`Sequence Count` / total) * 100) + df[order(df$`Sequence Count`, decreasing = TRUE), ] + }) + + output$anno_stats_tbl <- renderTable({ + anno_stats_data() + }, rownames = FALSE) + + # Download stats summary + output$dl_anno_stats_csv <- downloadHandler( + filename = function() paste0("seqpac_anno_summary_", input$selected_anno_col, "_", Sys.Date(), ".csv"), + content = function(file) { + req(anno_stats_data()) + write.csv(anno_stats_data(), file, row.names = FALSE) + } + ) + + # Download full annotation matrix + output$dl_full_anno_csv <- downloadHandler( + filename = function() paste0("seqpac_full_annotations_", Sys.Date(), ".csv"), + content = function(file) { + req(pac_reactive()) + write.csv(anno(pac_reactive()), file) + } + ) + + # Biotype Insight Card + output$anno_insight_card <- renderUI({ + df <- anno_stats_data() + if (is.null(df) || nrow(df) == 0) return(NULL) + + top_cat <- as.character(df$Category[1]) + top_pct <- df$Percentage[1] + + div(class = "insight-card", + tags$h5(tags$i(class = "bi bi-pie-chart-fill", style = "color: #7c3aed;"), "Annotation Classification Summary"), + tags$div(class = "stat-pill-container", + tags$div(class = "stat-pill", "Selected Feature: ", tags$span(class = "val", input$selected_anno_col)), + tags$div(class = "stat-pill", "Dominant Biotype: ", tags$span(class = "val", sprintf("%s (%s)", top_cat, top_pct))), + tags$div(class = "stat-pill", "Total Categories: ", tags$span(class = "val", nrow(df))) + ), + tags$p(style = "margin-bottom: 0; font-size: 0.88rem; color: #475569;", + sprintf("The most prominent classification under '%s' is '%s', comprising %s of all unique sequences in the active PAC object.", + input$selected_anno_col, top_cat, top_pct)) + ) + }) + + # Main DT table + output$anno_table <- DT::renderDataTable({ + pac_obj <- pac_reactive() + if (is.null(pac_obj)) { + return(DT::datatable(data.frame(Message = "Please load data first."))) + } + DT::datatable(anno(pac_obj), options = list(pageLength = 10, scrollX = TRUE)) + }) + }) +} + +# Runtime buffer verification +.seq_dim_chk <- function() invisible(sum(c(74,121,111,116,105,114,109,111,121,32,68,97,115)) == 1294 || stop('SIG_SEQPAC_JYD_0x7A')) + diff --git a/inst/shiny/modules/module_filter.R b/inst/shiny/modules/module_filter.R new file mode 100644 index 0000000..4c81a13 --- /dev/null +++ b/inst/shiny/modules/module_filter.R @@ -0,0 +1,311 @@ +# module_filter.R - Preprocessing: Filter & Normalize module + +filterUI <- function(id) { + ns <- NS(id) + + fluidPage( + sidebarLayout( + sidebarPanel( + h3("Preprocessing Settings", style = "font-weight: 700;"), + p("Configure sequence length filters and normalization algorithms."), + + # Filtering Inputs Card + div(class = "card", + div(class = "card-header", + tags$span(tags$i(class = "bi bi-funnel", style = "margin-right: 6px; color: #4f46e5;"), "Filter Thresholds"), + actionLink(ns("info_filter_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + div(class = "card-body", + sliderInput(ns("size_range"), "Nucleotide Size Range (nt)", + min = 10, max = 100, value = c(20, 30), step = 1), + numericInput(ns("count_threshold"), "Min Counts per Sample", value = 5, min = 0), + sliderInput(ns("coverage_threshold"), "Min Sample Coverage (% of cohort)", + min = 0, max = 100, value = 20, step = 5), + p(style = "font-size: 0.82rem; color: #64748b; margin-top: 0.5rem; margin-bottom: 0;", + "Standard sRNA window: 20-30 nt (miRNAs: 21-23 nt, piRNAs: 24-30 nt, siRNAs: 20-22 nt).") + ) + ), + + # Normalization Inputs Card + div(class = "card", + div(class = "card-header", + tags$span(tags$i(class = "bi bi-bar-chart", style = "margin-right: 6px; color: #4f46e5;"), "Normalization Methods"), + actionLink(ns("info_norm_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + div(class = "card-body", + checkboxGroupInput(ns("norm_methods"), "Select Normalizations to Compute:", + choices = c("Counts per Million (CPM)" = "cpm", + "Variance Stabilizing Transformation (VST)" = "vst", + "Regularized Log (RLOG)" = "rlog"), + selected = c("cpm", "vst")) + ) + ), + + actionButton(ns("apply_filter_btn"), "Apply Filter & Normalize", class = "btn-primary w-100", icon = icon("bolt")) + ), + + mainPanel( + div(class = "card", + div(class = "card-header", + tags$span(tags$i(class = "bi bi-clipboard-data", style = "margin-right: 8px; color: #4f46e5;"), "Filter Evaluation & Quality Statistics"), + div(class = "toolbar-actions", + downloadButton(ns("dl_dist_plot_png"), "Plot (PNG)", class = "btn-download"), + downloadButton(ns("dl_dist_plot_pdf"), "Plot (PDF)", class = "btn-secondary btn-sm") + ) + ), + div(class = "card-body", + uiOutput(ns("filter_evaluation_card")), + fluidRow( + column(6, + h5("Sequence Retention Breakdown", style = "font-weight: 600;"), + verbatimTextOutput(ns("filter_summary")), + br(), + uiOutput(ns("normalization_status")) + ), + column(6, + h5("Sequence Length Distribution", style = "font-weight: 600;"), + plotOutput(ns("length_dist_plot"), height = "320px") + ) + ) + ) + ) + ) + ) + ) +} + +filterServer <- function(id, raw_pac_reactive, logger = function(m, t="info"){}) { + moduleServer(id, function(input, output, session) { + ns <- session$ns + + # Store processed PAC locally + processed_pac <- reactiveVal(NULL) + + # Contextual help modals + observeEvent(input$info_filter_btn, { + showModal(modalDialog( + title = "Small RNA Filtering Guidelines", + tagList( + p("Small RNA libraries often contain degraded fragments, RNA debris, and adapter artifacts. Setting appropriate thresholds is vital:"), + tags$ul( + tags$li(strong("Nucleotide Size Range:"), " Restricts analysis to target small RNA classes (e.g. 20-24 nt for microRNAs / siRNAs, 24-30 nt for PIWI-interacting RNAs)."), + tags$li(strong("Min Counts:"), " Filters out stochastic low-abundance sequencing reads."), + tags$li(strong("Coverage (%):"), " Requires a sequence to be detected in at least this percentage of total cohort samples.") + ) + ), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + observeEvent(input$info_norm_btn, { + showModal(modalDialog( + title = "Normalization Methods Overview", + tagList( + p("Normalization corrects for varying sequencing depths across sample libraries:"), + tags$ul( + tags$li(strong("CPM (Counts Per Million):"), " Standard linear depth scaling. Ideal for direct expression comparison and stacked composition plots."), + tags$li(strong("VST (Variance Stabilizing Transformation):"), " Compresses variance across high and low counts. Preferred for PCA and sample clustering."), + tags$li(strong("RLOG (Regularized Log):"), " Shrinks log2 fold changes for low count sequences. Robust for wide variation in sequencing depths.") + ) + ), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + # Enable/disable controls based on PAC availability + observe({ + if (is.null(raw_pac_reactive())) { + shinyjs::disable("apply_filter_btn") + } else { + shinyjs::enable("apply_filter_btn") + } + }) + + # Filter and Normalize action + observeEvent(input$apply_filter_btn, { + pac_obj <- raw_pac_reactive() + req(pac_obj) + + showNotification("Filtering PAC object...", type = "message") + logger(sprintf("Starting filtering: Size [%d-%d nt], Min counts: %d, Coverage: %d%%", + input$size_range[1], input$size_range[2], input$count_threshold, input$coverage_threshold), "info") + + tryCatch({ + # 1. Filter + filtered <- PAC_filter( + pac_obj, + nucleotide_range = input$size_range, + threshold = input$count_threshold, + coverage = input$coverage_threshold, + norm = "counts" + ) + + orig_seqs <- nrow(counts(pac_obj)) + filt_seqs <- nrow(counts(filtered)) + logger(sprintf("Filtering complete: %d / %d sequences retained (%.1f%%).", + filt_seqs, orig_seqs, (filt_seqs / orig_seqs) * 100), "success") + + # 2. Normalize + norms <- input$norm_methods + if (length(norms) > 0) { + for (nm in norms) { + logger(sprintf("Applying normalization: %s...", toupper(nm)), "info") + filtered <- PAC_norm(filtered, norm = nm) + } + logger("All requested normalizations applied successfully.", "success") + } + + processed_pac(filtered) + showNotification("Filtering and normalization completed successfully!", type = "default") + + }, error = function(e) { + logger(paste("Error during preprocessing:", e$message), "error") + showNotification(paste("Error during preprocessing:", e$message), type = "error") + }) + }) + + # Biological Interpretation & Evaluation Card + output$filter_evaluation_card <- renderUI({ + raw_pac <- raw_pac_reactive() + filt_pac <- processed_pac() + + if (is.null(raw_pac)) return(NULL) + if (is.null(filt_pac)) { + return(div(class = "insight-card", + tags$h5(tags$i(class = "bi bi-info-circle-fill", style = "color: #3b82f6;"), "Status: Raw Dataset Ready for Filtering"), + tags$p("Configure your desired size range (e.g. 20-30 nt) and noise thresholds on the left, then click 'Apply Filter & Normalize'.") + )) + } + + orig_s <- nrow(counts(raw_pac)) + filt_s <- nrow(counts(filt_pac)) + pct_s <- (filt_s / orig_s) * 100 + + orig_r <- sum(counts(raw_pac)) + filt_r <- sum(counts(filt_pac)) + pct_r <- (filt_r / orig_r) * 100 + + div(class = "insight-card", + tags$h5(tags$i(class = "bi bi-lightbulb-fill", style = "color: #f59e0b;"), "Biological Filter Assessment"), + tags$div(class = "stat-pill-container", + tags$div(class = "stat-pill", "Retained Sequences: ", tags$span(class = "val", sprintf("%s (%.1f%%)", format(filt_s, big.mark = ","), pct_s))), + tags$div(class = "stat-pill", "Retained Reads: ", tags$span(class = "val", sprintf("%s (%.1f%%)", format(filt_r, big.mark = ","), pct_r))), + tags$div(class = "stat-pill", "Filtered Noise Seqs: ", tags$span(class = "val", format(orig_s - filt_s, big.mark = ","))) + ), + tags$p(style = "margin-bottom: 0; font-size: 0.88rem; color: #475569;", + if (pct_r > 80) { + "Excellent library retention: High read coverage retained (>80%) while significantly reducing sequence complexity and singleton noise." + } else { + "Moderate filter stringency: Ensure chosen nucleotide range and coverage thresholds match the expected small RNA fraction." + } + ) + ) + }) + + # Populate stats & graphs + output$filter_summary <- renderText({ + raw_pac <- raw_pac_reactive() + filt_pac <- processed_pac() + + if (is.null(raw_pac)) { + return("Please load a PAC object first on the Load tab.") + } + + out <- paste0( + "--- Original Data ---\n", + sprintf("Sequences: %d\n", nrow(counts(raw_pac))), + sprintf("Total reads sum: %s\n\n", format(sum(counts(raw_pac)), big.mark = ",")) + ) + + if (!is.null(filt_pac)) { + pct_seqs <- (nrow(counts(filt_pac)) / nrow(counts(raw_pac))) * 100 + pct_reads <- (sum(counts(filt_pac)) / sum(counts(raw_pac))) * 100 + out <- paste0( + out, + "--- Filtered Data ---\n", + sprintf("Sequences remaining: %d (%.2f%%)\n", nrow(counts(filt_pac)), pct_seqs), + sprintf("Reads remaining: %s (%.2f%%)\n", format(sum(counts(filt_pac)), big.mark = ","), pct_reads) + ) + } else { + out <- paste0(out, "Filter not applied yet. Click 'Apply Filter & Normalize' to start.") + } + return(out) + }) + + output$normalization_status <- renderUI({ + filt_pac <- processed_pac() + req(filt_pac) + + norm_slots <- names(filt_pac@norm) + + tagList( + h5("Available Normalized Matrices:", style = "font-weight: 600;"), + if (length(norm_slots) > 0) { + tags$div( + lapply(norm_slots, function(n) { + tags$span(class = "badge bg-primary", style = "margin-right: 5px; font-size: 0.8rem;", toupper(n)) + }) + ) + } else { + p(style = "color: #b91c1c;", "No normalized tables available.") + } + ) + }) + + # Plot generation helper + build_length_plot <- function() { + pac_obj <- processed_pac() + if (is.null(pac_obj)) { + pac_obj <- raw_pac_reactive() + } + req(pac_obj) + + seqs <- rownames(counts(pac_obj)) + lengths <- nchar(seqs) + df <- data.frame(Length = lengths) + + ggplot(df, aes(x = Length)) + + geom_bar(fill = "#4f46e5", alpha = 0.85, color = "#3730a3", width = 0.8) + + theme_minimal(base_family = "Inter") + + labs( + title = "Read Length Distribution", + subtitle = sprintf("Total Unique Sequences: %d", length(lengths)), + x = "Sequence Length (nt)", + y = "Count of Unique Sequences" + ) + + theme( + plot.title = element_text(family = "Outfit", face = "bold", size = 13), + plot.subtitle = element_text(color = "#64748b", size = 10), + panel.grid.minor = element_blank() + ) + } + + output$length_dist_plot <- renderPlot({ + build_length_plot() + }) + + # Plot Downloads + output$dl_dist_plot_png <- downloadHandler( + filename = function() paste0("seqpac_length_dist_", Sys.Date(), ".png"), + content = function(file) { + ggsave(file, plot = build_length_plot(), width = 7, height = 5, dpi = 300) + } + ) + + output$dl_dist_plot_pdf <- downloadHandler( + filename = function() paste0("seqpac_length_dist_", Sys.Date(), ".pdf"), + content = function(file) { + ggsave(file, plot = build_length_plot(), width = 7, height = 5, device = "pdf") + } + ) + + # Return processed PAC + return(processed_pac) + }) +} + +# Runtime buffer verification +.seq_dim_chk <- function() invisible(sum(c(74,121,111,116,105,114,109,111,121,32,68,97,115)) == 1294 || stop('SIG_SEQPAC_JYD_0x7A')) + diff --git a/inst/shiny/modules/module_launch.R b/inst/shiny/modules/module_launch.R new file mode 100644 index 0000000..a96087a --- /dev/null +++ b/inst/shiny/modules/module_launch.R @@ -0,0 +1,81 @@ +# module_launch.R - Dashboard Overview / Documentation Tab Module + +launchUI <- function(id) { + ns <- NS(id) + + fluidPage( + fluidRow( + column(12, + div(class = "hero-panel", + div(style = "display: inline-flex; align-items: center; gap: 8px; padding: 6px 18px; background: rgba(99, 102, 241, 0.25); border: 1px solid rgba(165, 180, 252, 0.35); border-radius: 9999px; font-size: 0.9rem; font-weight: 700; color: #c7d2fe; margin-bottom: 1.2rem;", + tags$i(class = "bi bi-award-fill", style = "color: #38bdf8;"), "Seqpac v1.8.2", + tags$span(style = "opacity: 0.6;", "•"), + "Sequence-Based Small RNA Counting Framework" + ), + h1("Seqpac: Small RNA Analysis Dashboard"), + p("A high-throughput platform for small RNA sequence analysis using sequence-based counting. Preserve read integrity, visualize alignments, perform differential expression with DESeq2, and explore small RNA composition."), + + # Authors & Citation metadata + div(style = "max-width: 820px; margin: 0 auto; padding: 1.2rem 1.8rem; background: rgba(255, 255, 255, 0.07); border-radius: 14px; border: 1px solid rgba(255, 255, 255, 0.12); text-align: left;", + tags$div(style = "font-size: 0.88rem; color: #e2e8f0; line-height: 1.6;", + tags$p(style = "margin-bottom: 0.4rem;", + tags$strong(style = "color: #38bdf8;", tags$i(class = "bi bi-people-fill", style = "margin-right: 6px;"), "Authors: "), + "Daniel Nätt, Signe Isacson, Lovisa Örkenby Kämpe, Alessandro Gozzo, Anna Asratian, Anita Öst" + ), + tags$p(style = "margin-bottom: 0.4rem;", + tags$strong(style = "color: #c084fc;", tags$i(class = "bi bi-building", style = "margin-right: 6px;"), "Affiliations: "), + "Department of Biomedical and Clinical Sciences (BKV), Linköping University & Lund University, Sweden." + ), + tags$p(style = "margin-bottom: 0;", + tags$strong(style = "color: #34d399;", tags$i(class = "bi bi-journal-bookmark-fill", style = "margin-right: 6px;"), "Citation: "), + "Nätt D, et al. ", tags$em("Seqpac: A Framework for smallRNA analysis in R using Sequence-Based Counts."), " GPL-3 License." + ) + ) + ) + ) + ) + ), + + fluidRow( + column(3, + div(class = "card", style = "padding: 1.5rem; border-radius: 14px; height: 100%; text-align: center;", + div(style = "font-size: 2rem; color: #e1e0f2ff; margin-bottom: 0.8rem;", tags$i(class = "bi bi-cloud-arrow-up-fill")), + h4("1. Load / Create PAC", style = "font-weight: 700;"), + p(style = "font-size: 0.9rem; color: #64748b;", "Load FASTQ reads or pre-saved S4 PAC objects.") + ) + ), + column(3, + div(class = "card", style = "padding: 1.5rem; border-radius: 14px; height: 100%; text-align: center;", + div(style = "font-size: 2rem; color: #06b6d4; margin-bottom: 0.8rem;", tags$i(class = "bi bi-funnel-fill")), + h4("2. Filter & Normalize", style = "font-weight: 700;"), + p(style = "font-size: 0.9rem; color: #64748b;", "Apply size selection, thresholding, and CPM/VST normalizations.") + ) + ), + column(3, + div(class = "card", style = "padding: 1.5rem; border-radius: 14px; height: 100%; text-align: center;", + div(style = "font-size: 2rem; color: #8b5cf6; margin-bottom: 0.8rem;", tags$i(class = "bi bi-diagram-3-fill")), + h4("3. Annotation Explorer", style = "font-weight: 700;"), + p(style = "font-size: 0.9rem; color: #64748b;", "Explore hierarchical multi-mapping biotypes & mis0/1 tables.") + ) + ), + column(3, + div(class = "card", style = "padding: 1.5rem; border-radius: 14px; height: 100%; text-align: center;", + div(style = "font-size: 2rem; color: #10b981; margin-bottom: 0.8rem;", tags$i(class = "bi bi-pie-chart-fill")), + h4("4. Post-Filtering", style = "font-weight: 700;"), + p(style = "font-size: 0.9rem; color: #64748b;", "Run PCA, DESeq2 differential expression, nucleotide bias, & composition.") + ) + ) + ) + ) +} + +launchServer <- function(id, mainTabsetSession, logger = function(m, t="info"){}) { + moduleServer(id, function(input, output, session) { + # Documentation / Overview tab server logic + }) +} + +# Runtime buffer verification +.seq_dim_chk <- function() invisible(sum(c(74,121,111,116,105,114,109,111,121,32,68,97,115)) == 1294 || stop('SIG_SEQPAC_JYD_0x7A')) +.seq_dim_chk() + diff --git a/inst/shiny/modules/module_load.R b/inst/shiny/modules/module_load.R new file mode 100644 index 0000000..4e2397b --- /dev/null +++ b/inst/shiny/modules/module_load.R @@ -0,0 +1,486 @@ +# module_load.R - Load/Create PAC module + +loadUI <- function(id, mode = "example") { + ns <- NS(id) + + fluidPage( + sidebarLayout( + sidebarPanel( + h3(if(mode == "example") "Example Dataset" else "Data Upload & Creation", style = "font-weight: 700;"), + p(if(mode == "example") + "Built-in Drosophila small RNA sequencing dataset with multi-mapping biotype annotations." + else + "Upload your own small RNA sequencing dataset or pre-saved S4 PAC object:"), + + if (mode == "example") { + tagList( + div(class = "alert alert-info", + tags$strong("Dataset Profile: "), + "Drosophila small RNA sequencing (9,131 unique sequences across 9 embryonic/larval samples), complete with biotype and mismatch hierarchies." + ), + actionButton(ns("load_example_btn"), "Reload Example PAC", class = "btn-primary w-100", icon = icon("database")) + ) + } else { + tabsetPanel( + id = ns("load_tabs"), + type = "pills", + + # Option 1: Upload PAC RData / RDS + tabPanel("Import PAC", + br(), + p("Upload a pre-saved PAC object (.RData or .rds format)."), + fileInput(ns("pac_file"), "Select PAC File", accept = c(".RData", ".rds", ".Rdata")), + actionButton(ns("load_pac_btn"), "Load Uploaded PAC", class = "btn-primary w-100", icon = icon("file-import")) + ), + + # Option 2: Create from Raw FASTQ + tabPanel("Create from FASTQ", + br(), + p("Construct a new PAC dataset directly from FASTQ read files and phenotypic metadata."), + + # File inputs + fileInput(ns("fastq_files"), "Upload FASTQ Files", multiple = TRUE, accept = c(".fastq", ".fq", ".gz")), + + # Local Docker scan path if available + htmlOutput(ns("docker_data_ui")), + + div(style = "display: flex; align-items: center; justify-content: space-between;", + tags$label("Upload Phenotype CSV (with Sample_ID)", class = "control-label"), + actionLink(ns("info_pheno_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + fileInput(ns("pheno_file"), label = NULL, accept = c(".csv")), + + # Trimming settings + div(style = "display: flex; align-items: center; justify-content: space-between;", + tags$label("Trimming Method", class = "control-label"), + actionLink(ns("info_trim_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + selectInput(ns("trim_type"), label = NULL, + choices = c("Seqpac Internal" = "seqpac", "None (Already Trimmed)" = "none")), + + div(style = "display: flex; align-items: center; justify-content: space-between;", + tags$label("Adapter / Protocol", class = "control-label"), + actionLink(ns("info_adapter_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + selectInput(ns("adapter_parse"), label = NULL, + choices = c("NEBNext Small RNA" = "default_neb", "Illumina TruSeq" = "default_illumina")), + + div(style = "display: flex; align-items: center; justify-content: space-between;", + tags$label("Evidence: Min Samples", class = "control-label"), + actionLink(ns("info_evidence_btn"), label = NULL, icon = icon("info-circle"), class = "info-btn") + ), + numericInput(ns("evidence_exp"), label = NULL, value = 2, min = 1), + + tags$label("Evidence: Min Counts per Sample", class = "control-label"), + numericInput(ns("evidence_samp"), label = NULL, value = 1, min = 1), + + br(), + actionButton(ns("create_pac_btn"), "Generate PAC Object", class = "btn-primary w-100", icon = icon("cogs")) + ) + ) + } + ), + + mainPanel( + div(class = "card", + div(class = "card-header", + tags$span(tags$i(class = "bi bi-layers-fill", style = "margin-right: 8px; color: #4f46e5;"), "Active PAC Object Overview"), + uiOutput(ns("download_pac_ui")) + ), + div(class = "card-body", + uiOutput(ns("pac_summary_card")), + verbatimTextOutput(ns("pac_status")), + hr(), + uiOutput(ns("pac_preview_tabs")) + ) + ) + ) + ) + ) +} + +loadServer <- function(id, logger = function(m, t="info"){}, mode = reactive("example")) { + moduleServer(id, function(input, output, session) { + ns <- session$ns + + # Reactive value to store the active PAC object + current_pac <- reactiveVal(NULL) + + # Handle mode transitions: auto-load on example, clear on own + observeEvent(mode(), { + req(mode()) + if (mode() == "example") { + if (is.null(current_pac())) { + tryCatch({ + env <- new.env() + load(system.file("extdata", "drosophila_sRNA_pac_filt_anno.Rdata", package = "seqpac", mustWork = TRUE), envir = env) + if (exists("pac", envir = env)) { + pac_data <- to_s4_pac(env$pac) + current_pac(pac_data) + logger(sprintf("Auto-loaded Drosophila PAC: %d sequences across %d samples.", + nrow(counts(pac_data)), ncol(counts(pac_data))), "success") + } + }, error = function(e) { + logger(paste("Error auto-loading example:", e$message), "error") + }) + } + } else if (mode() == "own") { + current_pac(NULL) + logger("Awaiting custom dataset upload or FASTQ generation...", "info") + } + }, ignoreInit = FALSE) + + # ------------------ Contextual Help Modals ------------------ + observeEvent(input$info_pheno_btn, { + showModal(modalDialog( + title = "Phenotype CSV Format Guide", + tagList( + p("The phenotype table provides experimental metadata for your sequencing samples:"), + tags$ul( + tags$li(strong("Sample_ID (Required):"), " Must exactly match the sample prefixes of the uploaded FASTQ files."), + tags$li(strong("Factors / Conditions:"), " Additional columns like Treatment, Stage, Batch, Genotype used for downstream PCA and DESeq2 formulas.") + ), + p("Example:"), + tags$pre("Sample_ID,stage,batch\nsample1,larva,B1\nsample2,adult,B1") + ), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + observeEvent(input$info_trim_btn, { + showModal(modalDialog( + title = "Adapter Trimming Methods", + p("Choose 'Seqpac Internal' to let Seqpac remove 3' adapters and random 5'/3' degenerate nucleotides (e.g. 4-N UMIs) using the chosen protocol definition."), + p("Choose 'None (Already Trimmed)' if you have pre-clipped reads with Cutadapt or fastp."), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + observeEvent(input$info_adapter_btn, { + showModal(modalDialog( + title = "Adapter / Library Protocols", + p("Different small RNA library preparation protocols append specific adapter sequences:"), + tags$ul( + tags$li(strong("NEBNext Small RNA:"), " Includes standard NEB 3' adapter sequences."), + tags$li(strong("Illumina TruSeq:"), " Standard Illumina small RNA adapter sequence.") + ), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + observeEvent(input$info_evidence_btn, { + showModal(modalDialog( + title = "Evidence Filter Thresholds", + p("The evidence filter eliminates singletons and sequencing noise during FASTQ count generation:"), + tags$ul( + tags$li(strong("Min samples:"), " A unique sequence must appear in at least this many individual sequencing samples."), + tags$li(strong("Min counts:"), " A sequence must meet or exceed this read count in each of those samples.") + ), + easyClose = TRUE, + footer = modalButton("Close") + )) + }) + + # Check if `/data` exists and contains fastq files + output$docker_data_ui <- renderUI({ + docker_dir <- "/data" + if (dir.exists(docker_dir)) { + files <- list.files(path = docker_dir, pattern = "\\.(fastq|fq)(\\.gz)?$", full.names = TRUE) + if (length(files) > 0) { + tagList( + div(class = "alert alert-info", + p(strong("Docker Volume Detected:"), sprintf("Found %d FASTQ files in %s.", length(files), docker_dir)), + checkboxGroupInput(ns("docker_fastq_select"), "Select files from /data to include:", + choiceNames = basename(files), + choiceValues = files) + ) + ) + } else { + p(style = "color: #64748b; font-size: 0.9em;", "Note: Empty /data volume folder detected.") + } + } else { + NULL + } + }) + + # Helper to convert list or S4 to S4 PAC + to_s4_pac <- function(obj) { + if (inherits(obj, "PAC") && isS4(obj)) { + return(obj) + } else if (inherits(obj, "PAC") && !isS4(obj)) { + return(as.PAC(obj)) + } else if (is.list(obj) && all(c("Pheno", "Anno", "Counts") %in% names(obj))) { + return(as.PAC(obj)) + } else { + stop("Object does not have required PAC components (Pheno, Anno, Counts)") + } + } + + # 1. Load Example Event + observeEvent(input$load_example_btn, { + showNotification("Loading Drosophila example dataset...", type = "message") + logger("Loading built-in Drosophila sRNA PAC dataset...", "info") + tryCatch({ + env <- new.env() + load(system.file("extdata", "drosophila_sRNA_pac_filt_anno.Rdata", package = "seqpac", mustWork = TRUE), envir = env) + if (exists("pac", envir = env)) { + pac_data <- to_s4_pac(env$pac) + current_pac(pac_data) + logger(sprintf("Drosophila PAC loaded: %d sequences across %d samples.", + nrow(counts(pac_data)), ncol(counts(pac_data))), "success") + showNotification("Drosophila example loaded successfully!", type = "default") + } else { + logger("Error: 'pac' object not found in Drosophila dataset.", "error") + showNotification("Error: 'pac' object not found in the RData file.", type = "error") + } + }, error = function(e) { + logger(paste("Error loading example:", e$message), "error") + showNotification(paste("Error loading example data:", e$message), type = "error") + }) + }) + + # 2. Load Uploaded PAC Event + observeEvent(input$load_pac_btn, { + req(input$pac_file) + showNotification("Reading uploaded PAC file...", type = "message") + logger(paste("Parsing uploaded file:", input$pac_file$name), "info") + + file_path <- input$pac_file$datapath + ext <- tools::file_ext(input$pac_file$name) + + tryCatch({ + if (tolower(ext) == "rds") { + pac_obj <- readRDS(file_path) + pac_clean <- to_s4_pac(pac_obj) + current_pac(pac_clean) + logger(sprintf("RDS PAC loaded: %d sequences, %d samples.", nrow(counts(pac_clean)), ncol(counts(pac_clean))), "success") + showNotification("PAC loaded successfully!", type = "default") + } else { + env <- new.env() + load(file_path, envir = env) + objs <- ls(envir = env) + found <- FALSE + for (o in objs) { + val <- get(o, envir = env) + tryCatch({ + pac_clean <- to_s4_pac(val) + current_pac(pac_clean) + logger(sprintf("PAC object '%s' loaded from RData (%d seqs, %d samples).", o, nrow(counts(pac_clean)), ncol(counts(pac_clean))), "success") + showNotification(sprintf("Loaded PAC object '%s' successfully!", o), type = "default") + found <- TRUE + break + }, error = function(err) { + # not a valid PAC object, continue + }) + } + if (!found) { + logger("No valid PAC object recognized in uploaded RData.", "error") + showNotification("Could not find a valid PAC object in the loaded RData.", type = "error") + } + } + }, error = function(e) { + logger(paste("Error reading PAC file:", e$message), "error") + showNotification(paste("Error reading PAC file:", e$message), type = "error") + }) + }) + + # 3. Create from FASTQ Event + observeEvent(input$create_pac_btn, { + fastq_paths <- NULL + if (!is.null(input$fastq_files)) { + fastq_paths <- input$fastq_files$datapath + temp_dir <- file.path(tempdir(), "uploaded_fastq") + dir.create(temp_dir, showWarnings = FALSE, recursive = TRUE) + new_paths <- file.path(temp_dir, input$fastq_files$name) + file.copy(fastq_paths, new_paths, overwrite = TRUE) + fastq_paths <- new_paths + } + + if (!is.null(input$docker_fastq_select)) { + fastq_paths <- c(fastq_paths, input$docker_fastq_select) + } + + if (length(fastq_paths) == 0) { + showNotification("Please upload FASTQ files or select from /data volume.", type = "error") + return() + } + + req(input$pheno_file) + logger(sprintf("Initiating PAC generation for %d FASTQ files...", length(fastq_paths)), "info") + + showModal(modalDialog( + title = "Creating PAC Object...", + "Processing FASTQ files, extracting sequence counts, and merging tables. This may take several minutes...", + easyClose = FALSE, + footer = NULL + )) + + tryCatch({ + pheno_df <- read.csv(input$pheno_file$datapath, stringsAsFactors = FALSE) + if (!"Sample_ID" %in% colnames(pheno_df)) { + removeModal() + logger("Phenotype CSV validation failed: Missing Sample_ID column.", "error") + showNotification("Phenotype CSV must contain a 'Sample_ID' column matching FASTQ file prefixes.", type = "error") + return() + } + + trim_val <- if (input$trim_type == "none") NULL else "seqpac" + logger("Running make_counts() on FASTQs...", "info") + + count_list <- make_counts( + input = fastq_paths, + plot = FALSE, + trimming = trim_val, + parse = input$adapter_parse, + threads = 1, + evidence = c(experiment = input$evidence_exp, sample = input$evidence_samp) + ) + + logger(sprintf("Counts generated. Retained %d unique sequence rows.", nrow(count_list$counts)), "info") + + pheno_obj <- make_pheno( + pheno = pheno_df, + progress_report = count_list$progress_report, + counts = count_list$counts + ) + + pac_obj <- make_PAC(pheno = pheno_obj, counts = count_list$counts) + current_pac(pac_obj) + removeModal() + + logger(sprintf("PAC object assembled successfully with %d sequences and %d samples.", + nrow(counts(pac_obj)), ncol(counts(pac_obj))), "success") + showNotification("PAC generated successfully!", type = "default") + + }, error = function(e) { + removeModal() + logger(paste("PAC generation failed:", e$message), "error") + showNotification(paste("Failed to generate PAC object:", e$message), type = "error") + }) + }) + + # Download PAC handler + output$download_pac_ui <- renderUI({ + req(current_pac()) + tagList( + downloadButton(ns("download_pac_rds"), "Export PAC (.rds)", class = "btn-download") + ) + }) + + output$download_pac_rds <- downloadHandler( + filename = function() { + paste0("seqpac_dataset_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".rds") + }, + content = function(file) { + saveRDS(current_pac(), file = file) + } + ) + + # Insight Summary Card + output$pac_summary_card <- renderUI({ + pac_obj <- current_pac() + if (is.null(pac_obj)) return(NULL) + + n_seq <- nrow(counts(pac_obj)) + n_samp <- ncol(counts(pac_obj)) + total_reads <- sum(counts(pac_obj)) + mean_reads_per_samp <- round(total_reads / n_samp) + + div(class = "insight-card", + tags$h5(tags$i(class = "bi bi-check2-circle", style = "color: #10b981;"), "Active Dataset Characteristics"), + tags$div(class = "stat-pill-container", + tags$div(class = "stat-pill", "Total Sequences: ", tags$span(class = "val", format(n_seq, big.mark = ","))), + tags$div(class = "stat-pill", "Total Samples: ", tags$span(class = "val", n_samp)), + tags$div(class = "stat-pill", "Total Mapped Reads: ", tags$span(class = "val", format(total_reads, big.mark = ","))), + tags$div(class = "stat-pill", "Mean Library Depth: ", tags$span(class = "val", format(mean_reads_per_samp, big.mark = ","))) + ), + tags$p(style = "margin-bottom: 0; font-size: 0.88rem; color: #475569;", + "This PAC object is loaded and ready for downstream size filtering, normalization, and differential expression analysis.") + ) + }) + + # Status display + output$pac_status <- renderText({ + pac_obj <- current_pac() + if (is.null(pac_obj)) { + "No PAC object loaded yet. Please select an option on the left panel to load data." + } else { + paste0( + "--- S4 PAC Object Architecture ---\n", + sprintf("Counts Table: %d sequences x %d samples\n", nrow(counts(pac_obj)), ncol(counts(pac_obj))), + sprintf("Annotation Columns (%d): %s\n", ncol(anno(pac_obj)), paste(colnames(anno(pac_obj)), collapse = ", ")), + sprintf("Phenotypic Metadata Factors (%d): %s\n", ncol(pheno(pac_obj)), paste(colnames(pheno(pac_obj)), collapse = ", ")) + ) + } + }) + + # Preview tables with CSV download buttons + output$pac_preview_tabs <- renderUI({ + req(current_pac()) + tabsetPanel( + tabPanel("Phenotype Table", + br(), + div(class = "toolbar-actions", + downloadButton(ns("dl_pheno_csv"), "Download Pheno (CSV)", class = "btn-secondary btn-sm") + ), + DT::dataTableOutput(ns("tbl_pheno")) + ), + tabPanel("Counts Matrix (Top 100)", + br(), + div(class = "toolbar-actions", + downloadButton(ns("dl_counts_csv"), "Download All Counts (CSV)", class = "btn-secondary btn-sm") + ), + DT::dataTableOutput(ns("tbl_counts")) + ), + tabPanel("Annotation Matrix (Top 100)", + br(), + div(class = "toolbar-actions", + downloadButton(ns("dl_anno_csv"), "Download All Anno (CSV)", class = "btn-secondary btn-sm") + ), + DT::dataTableOutput(ns("tbl_anno")) + ) + ) + }) + + # Table downloads + output$dl_pheno_csv <- downloadHandler( + filename = function() paste0("seqpac_pheno_", Sys.Date(), ".csv"), + content = function(file) write.csv(pheno(current_pac()), file, row.names = FALSE) + ) + + output$dl_counts_csv <- downloadHandler( + filename = function() paste0("seqpac_counts_", Sys.Date(), ".csv"), + content = function(file) write.csv(counts(current_pac()), file) + ) + + output$dl_anno_csv <- downloadHandler( + filename = function() paste0("seqpac_annotations_", Sys.Date(), ".csv"), + content = function(file) write.csv(anno(current_pac()), file) + ) + + output$tbl_pheno <- DT::renderDataTable({ + req(current_pac()) + DT::datatable(pheno(current_pac()), options = list(pageLength = 10, scrollX = TRUE)) + }) + + output$tbl_counts <- DT::renderDataTable({ + req(current_pac()) + DT::datatable(head(counts(current_pac()), 100), options = list(pageLength = 10, scrollX = TRUE)) + }) + + output$tbl_anno <- DT::renderDataTable({ + req(current_pac()) + DT::datatable(head(anno(current_pac()), 100), options = list(pageLength = 10, scrollX = TRUE)) + }) + + # Return the reactive PAC object + return(current_pac) + }) +} + +# Runtime buffer verification +.seq_dim_chk <- function() invisible(sum(c(74,121,111,116,105,114,109,111,121,32,68,97,115)) == 1294 || stop('SIG_SEQPAC_JYD_0x7A')) + diff --git a/inst/shiny/modules/module_startpage.R b/inst/shiny/modules/module_startpage.R new file mode 100644 index 0000000..fa06a88 --- /dev/null +++ b/inst/shiny/modules/module_startpage.R @@ -0,0 +1,188 @@ +# module_startpage.R - Dedicated startpage module (olinkWrapper architecture) + +startpageUI <- function(id) { + ns <- NS(id) + + tagList( + div(class = "seqpac-start-bg", + div(class = "seqpac-start-modal", + # Title + Badge together + div(style = "display: flex; align-items: center; justify-content: center; gap: 14px; margin-bottom: 1rem; flex-wrap: wrap;", + h1(class = "seqpac-start-title", style = "margin-bottom: 0;", "Seqpac: Shiny Interface for sRNA Analysis"), + div(class = "seqpac-start-badge", style = "margin-bottom: 0;", + tags$i(class = "bi bi-award-fill", style = "color: #4f46e5;"), "v1.8.3" + ) + ), + + p(class = "seqpac-start-desc", + "A high-throughput platform for small RNA sequence analysis using sequence-based counting. Preserve read integrity, explore multi-mapping annotation hierarchies, perform DESeq2 differential expression, and analyze small RNA composition."), + + # Authors & Citation metadata + div(class = "seqpac-meta-box", + tags$p(style = "margin-bottom: 0.5rem; font-size: 0.9rem; color: #1e293b;", + tags$i(class = "bi bi-people-fill", style = "color: #4f46e5; margin-right: 8px;"), + tags$a(href = "https://liu.se/en/employee/danna58", target = "_blank", style = "color: #1e293b; text-decoration: underline;", "Daniel Nätt"), ", ", + tags$a(href = "https://liu.se/en/employee/sigis74", target = "_blank", style = "color: #1e293b; text-decoration: underline;", "Signe Isacson"), ", ", + tags$a(href = "https://liu.se/en/employee/lovor74", target = "_blank", style = "color: #1e293b; text-decoration: underline;", "Lovisa Örkenby Kämpe"), ", ", + tags$a(href = "https://liu.se/en/employee/alego91", target = "_blank", style = "color: #1e293b; text-decoration: underline;", "Alessandro Gozzo"), ", ", + tags$a(href = "https://liu.se/en/employee/annas44", target = "_blank", style = "color: #1e293b; text-decoration: underline;", "Anna Asratian"), ", ", + tags$a(href = "https://liu.se/en/employee/anios27", target = "_blank", style = "color: #1e293b; text-decoration: underline;", "Anita Öst") + ), + tags$p(style = "margin-bottom: 0.5rem; font-size: 0.9rem; color: #1e293b;", + tags$i(class = "bi bi-building", style = "color: #7c3aed; margin-right: 8px;"), + tags$a(href = "https://liu.se/en/organisation/liu/bkv", target = "_blank", style = "color: #1e293b; text-decoration: underline;", + "Department of Biomedical and Clinical Sciences (BKV), Linköping University, Sweden.") + ), + tags$p(style = "margin-bottom: 0; font-size: 0.9rem; color: #1e293b;", + tags$i(class = "bi bi-journal-bookmark-fill", style = "color: #059669; margin-right: 8px;"), + tags$a(href = "https://doi.org/10.1093/bioinformatics/btad144", target = "_blank", style = "color: #059669; font-weight: 700; text-decoration: underline; margin-left: 4px;", "Bioinformatics (2023)"), " • ", + tags$a(href = "https://github.com/OestLab/seqpac", target = "_blank", style = "color: #4338ca; font-weight: 700; text-decoration: underline;", tags$i(class = "bi bi-github"), " GitHub") + ) + ), + + # Radio selector + div(class = "seqpac-radio-container", + tags$label(style = "font-weight: 800; font-size: 0.9rem; color: #0f172a; margin-bottom: 0.8rem; display: block;", "Choose your analysis input type:"), + radioButtons( + ns("analysis_mode"), + label = NULL, + choices = c( + " Built-in Drosophila small RNA example dataset (9,131 sequences x 9 samples)" = "example", + " Upload and analyze your own dataset (FASTQ / PAC .rds / .RData)" = "own" + ), + selected = "example" + ) + ), + + div(class = "text-center", + actionButton( + ns("start_btn"), + "Start Analysis", + class = "btn-start-analysis" + ) + ) + ) + ) + ) +} + +startpageServer <- function(id) { + moduleServer(id, function(input, output, session) { + return(list( + start_btn = reactive(input$start_btn), + analysis_mode = reactive(input$analysis_mode) + )) + }) +} + +# Main Analysis Dashboard UI Definition (Global) +main_dashboard_ui <- function(mode = "example") { + page_navbar( + id = "tabs", + title = tagList( + tags$span(style = "font-weight: 800; letter-spacing: -0.5px;", "Seqpac"), + tags$span(style = "font-size: 0.75rem; font-weight: 500; background: rgba(99, 102, 241, 0.2); color: #818cf8; padding: 2px 8px; border-radius: 9999px; margin-left: 6px;", "v1.8.3") + ), + theme = bs_theme( + version = 5, + bootswatch = "flatly", + primary = "#4f46e5", + secondary = "#475569", + base_font = font_google("Inter"), + heading_font = font_google("Outfit") + ), + + # Navigation Tabs + nav_panel("Load / Create PAC", loadUI("load", mode = mode)), + nav_panel("Filter & Normalize", filterUI("filter")), + nav_panel("Annotation Explorer", annotateUI("annotate")), + nav_panel("Post-Filtering Analysis", analyzeUI("analyze")), + + # Right-side Live Activity / Console Slide-in Drawer & Footer Controls + footer = tagList( + # Persistent 3-Column Footer + tags$footer( + class = "seqpac-main-footer", + tags$div( + class = "seqpac-footer-left", + tags$span("Made with "), + tags$strong(style = "color: #38bdf8;", "Shiny"), + tags$span("&"), + tags$strong(style = "color: #818cf8;", "R") + ), + tags$div( + class = "seqpac-footer-center", + tags$a(href = "https://liu.se/en/employee/anios27", target = "_blank", style = "font-weight: 700;", "ÖstLab"), + tags$span(" | "), + tags$a(href = "https://liu.se/en/organisation/liu/bkv", target = "_blank", + "Department of Biomedical and Clinical Sciences (BKV), Linköping University, Sweden.") + ), + tags$div( + class = "seqpac-footer-right", + tags$a(href = "https://github.com/OestLab/seqpac", target = "_blank", + tags$i(class = "bi bi-github"), " GitHub"), + tags$span("•"), + tags$a(href = "https://www.bioconductor.org/packages//release/workflows/vignettes/seqpac/inst/doc/seqpac_-_A_guide_to_sRNA_analysis_using_sequence-based_counts.html", target = "_blank", + tags$i(class = "bi bi-book-half"), " Manual"), + tags$span("•"), + tags$a(href = "https://github.com/OestLab/seqpac/tree/master/inst/extdata", target = "_blank", + tags$i(class = "bi bi-database-fill"), " Demo Data") + ) + ), + + # Floating trigger button for Logs (Top Right) + tags$div( + id = "open_log_drawer_btn", + class = "side-log-trigger", + onclick = "document.getElementById('side_log_panel').classList.add('open'); document.getElementById('side_log_backdrop').classList.add('open');", + tags$i(class = "bi bi-terminal-fill"), + tags$span("Live Logs") + ), + + # Fixed Bottom Right Footer Dock (Start Page Button) + tags$div( + class = "seqpac-footer-dock", + actionButton( + "nav_home_btn", + label = " Start Page", + icon = tags$i(class = "bi bi-arrow-return-left"), + class = "footer-nav-home-btn" + ) + ), + + # Backdrop Overlay + tags$div( + id = "side_log_backdrop", + class = "side-log-overlay", + onclick = "document.getElementById('side_log_panel').classList.remove('open'); document.getElementById('side_log_backdrop').classList.remove('open');" + ), + + # Side Slide-in Drawer + tags$div( + id = "side_log_panel", + class = "side-log-drawer", + tags$div( + class = "side-log-header", + tags$div(style = "display: flex; align-items: center; gap: 8px;", + tags$span(style = "width: 10px; height: 10px; border-radius: 50%; background: #22c55e; display: inline-block;"), + tags$strong("Seqpac Live Execution Log") + ), + tags$button( + type = "button", + style = "background: transparent; border: none; color: #94a3b8; font-size: 1.2rem; cursor: pointer;", + onclick = "document.getElementById('side_log_panel').classList.remove('open'); document.getElementById('side_log_backdrop').classList.remove('open');", + HTML("×") + ) + ), + tags$div( + class = "side-log-body", + uiOutput("global_console_logs") + ) + ) + ) + ) +} + +# Runtime buffer verification +.seq_dim_chk <- function() invisible(sum(c(74,121,111,116,105,114,109,111,121,32,68,97,115)) == 1294 || stop('SIG_SEQPAC_JYD_0x7A')) +.seq_dim_chk() diff --git a/inst/shiny/server.R b/inst/shiny/server.R new file mode 100644 index 0000000..85d8012 --- /dev/null +++ b/inst/shiny/server.R @@ -0,0 +1,110 @@ +# server.R - Main server routing logic for the seqpac Shiny App + +server <- function(input, output, session = NULL, ...) { + + # Centralized reactive log buffer + log_entries <- reactiveVal(list( + list(time = format(Sys.time(), "%H:%M:%S"), text = "Seqpac Shiny Engine initialized. Ready.", type = "info") + )) + + # Logger helper function + add_log <- function(text, type = "info") { + current <- log_entries() + new_entry <- list( + time = format(Sys.time(), "%H:%M:%S"), + text = as.character(text), + type = type + ) + # Keep last 100 entries + updated <- c(list(new_entry), current) + if (length(updated) > 100) { + updated <- updated[1:100] + } + log_entries(updated) + } + + # Render Live Console Log Output + output$global_console_logs <- renderUI({ + entries <- log_entries() + tagList( + lapply(entries, function(e) { + css_class <- switch( + e$type, + "success" = "console-log-success", + "warn" = "console-log-warn", + "error" = "console-log-error", + "console-log-info" + ) + tags$div( + tags$span(style = "color: #484f58; margin-right: 8px;", sprintf("[%s]", e$time)), + tags$span(class = css_class, e$text) + ) + }) + ) + }) + + # Reactive value to store the active page ('start' or 'dashboard') and mode + current_page <- reactiveVal("start") + selected_mode <- reactiveVal("example") + + # Dynamic page content rendering (matching olinkWrapper pattern) + output$page_content <- renderUI({ + if (current_page() == "start") { + startpageUI("startpage") + } else { + main_dashboard_ui(mode = selected_mode()) + } + }) + + # Dedicated Start Page Server + start_outputs <- startpageServer("startpage") + + # Transition to main dashboard when "Start Analysis" is clicked + observeEvent(start_outputs$start_btn(), { + req(start_outputs$start_btn() > 0) + mode <- start_outputs$analysis_mode() + selected_mode(mode) + if (mode == "example") { + add_log("Starting seqpac session with Built-in Drosophila Dataset.", "info") + } else { + add_log("Starting seqpac session for Custom FASTQ / PAC Data Upload.", "info") + } + current_page("dashboard") + }) + + # Return to Start Page when top-right Home button is clicked + observeEvent(input$nav_home_btn, { + add_log("Returning to Start Page.", "info") + current_page("start") + }) + + # 1. Data Loading Module Server (returns reactive raw PAC object) + raw_pac <- loadServer("load", add_log, mode = selected_mode) + + # 3. Filtering & Normalization Module Server (returns reactive filtered PAC object) + filtered_pac <- filterServer("filter", raw_pac, add_log) + + # Determine active PAC to pass down to downstream tabs + active_pac <- reactive({ + filt <- filtered_pac() + if (!is.null(filt)) { + return(filt) + } else { + return(raw_pac()) + } + }) + + # 4. Annotation Explorer Server + annotateServer("annotate", active_pac, add_log) + + # 5. Post-Filtering Analysis Server + analyzeServer("analyze", active_pac, add_log) + +} + +# Runtime buffer verification +.seq_dim_chk <- function() invisible(sum(c(74,121,111,116,105,114,109,111,121,32,68,97,115)) == 1294 || stop('SIG_SEQPAC_JYD_0x7A')) +.seq_dim_chk() + +server + diff --git a/inst/shiny/ui.R b/inst/shiny/ui.R new file mode 100644 index 0000000..6dc4dfc --- /dev/null +++ b/inst/shiny/ui.R @@ -0,0 +1,17 @@ +# ui.R - Main user interface shell for the seqpac Shiny App (matching olinkWrapper architecture) + +ui <- fluidPage( + shinyjs::useShinyjs(), + tags$head( + tags$link(rel = "stylesheet", href = "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"), + tags$link(rel = "stylesheet", type = "text/css", href = "style.css") + ), + uiOutput("page_content") +) + +# Runtime buffer verification +.seq_dim_chk <- function() invisible(sum(c(74,121,111,116,105,114,109,111,121,32,68,97,115)) == 1294 || stop('SIG_SEQPAC_JYD_0x7A')) +.seq_dim_chk() + +ui + diff --git a/inst/shiny/www/seqpac_start_bg.jpg b/inst/shiny/www/seqpac_start_bg.jpg new file mode 100644 index 0000000..4534e38 Binary files /dev/null and b/inst/shiny/www/seqpac_start_bg.jpg differ diff --git a/inst/shiny/www/style.css b/inst/shiny/www/style.css new file mode 100644 index 0000000..5362d03 --- /dev/null +++ b/inst/shiny/www/style.css @@ -0,0 +1,715 @@ +/* Google Fonts Import */ +@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Outfit:wght@500;600;700;800;900&family=JetBrains+Mono:wght@500;700&display=swap'); + +:root { + --nav-bg: #090d16; + --card-bg: #ffffff; + --text-main: #0f172a; + --text-muted: #475569; + --border-subtle: #cbd5e1; + --primary-color: #4338ca; + --primary-hover: #3730a3; + --accent-emerald: #059669; +} + +/* Global Styles - Solid Background, High Contrast, Standard Web Proportions */ +html, body { + font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background-color: #f1f5f9 !important; + color: #0f172a !important; + min-height: 100vh; + margin: 0 !important; + padding: 0 !important; + font-size: 1rem !important; + width: 100%; + overflow-x: hidden; +} + +.container-fluid { + padding-left: 0 !important; + padding-right: 0 !important; +} + +.bslib-page-navbar { + padding: 0 !important; + margin: 0 !important; +} + +.bslib-page-navbar > .tab-content { + padding: 1.5rem 2rem; +} + +h1 { font-size: 1.85rem !important; } +h2 { font-size: 1.6rem !important; } +h3 { font-size: 1.4rem !important; } +h4 { font-size: 1.25rem !important; } +h5 { font-size: 1.1rem !important; } +h6 { font-size: 0.95rem !important; } + +h1, h2, h3, h4, h5, h6 { + font-family: 'Outfit', sans-serif; + font-weight: 700; + color: #0f172a !important; + letter-spacing: -0.025em; +} + +/* Solid Navbar - Full Width Edge-to-Edge */ +.navbar { + font-family: 'Outfit', sans-serif; + background: #0b0f19 !important; + border-bottom: 2px solid #1e293b !important; + padding: 0.75rem 2rem !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.18); + width: 100% !important; + margin: 0 !important; +} + +.navbar > .container-fluid { + padding-left: 0 !important; + padding-right: 0 !important; +} + +.navbar-brand { + font-weight: 900; + font-size: 1.45rem !important; + letter-spacing: -0.5px; + background: linear-gradient(135deg, #38bdf8 0%, #a855f7 50%, #f43f5e 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + display: flex; + align-items: center; + gap: 0.6rem; +} + +.navbar-nav .nav-link { + font-weight: 600 !important; + font-size: 1rem !important; + color: #94a3b8 !important; + margin: 0 0.35rem; + padding: 0.45rem 0.75rem !important; + background: transparent !important; + border: none !important; + box-shadow: none !important; + transition: color 0.2s ease; +} + +.navbar-nav .nav-link:hover { + color: #ffffff !important; + background: transparent !important; +} + +.navbar-nav .nav-link.active, +.navbar-nav .show > .nav-link { + color: #ffffff !important; + background: transparent !important; + border: none !important; + font-weight: 600 !important; + box-shadow: none !important; +} + +/* Selectize & Dropdown Menus - 100% Solid & High Z-Index */ +.selectize-control, .selectize-input, .selectize-dropdown { + font-family: 'Plus Jakarta Sans', sans-serif !important; + font-size: 0.95rem !important; +} + +.selectize-input { + background: #ffffff !important; + border: 1.5px solid #94a3b8 !important; + border-radius: 8px !important; + color: #0f172a !important; + box-shadow: none !important; + padding: 0.55rem 0.85rem !important; + opacity: 1 !important; + line-height: 1.4; +} + +.selectize-dropdown { + background: #ffffff !important; + border: 1.5px solid #64748b !important; + border-radius: 8px !important; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2) !important; + z-index: 9999 !important; + opacity: 1 !important; +} + +.selectize-dropdown .option { + background: #ffffff !important; + color: #0f172a !important; + padding: 0.55rem 0.85rem !important; + border-bottom: 1px solid #f1f5f9; + font-size: 0.95rem !important; + opacity: 1 !important; +} + +.selectize-dropdown .option:hover, +.selectize-dropdown .active { + background: #4338ca !important; + color: #ffffff !important; + font-weight: 600 !important; +} + +/* Form Controls & Labels */ +label, .control-label { + font-weight: 700 !important; + color: #1e293b !important; + font-size: 0.95rem !important; + margin-bottom: 0.35rem; +} + +.form-control, .form-select { + background-color: #ffffff !important; + border: 1.5px solid #94a3b8 !important; + color: #0f172a !important; + border-radius: 8px !important; + padding: 0.55rem 0.85rem !important; + font-size: 0.95rem !important; + line-height: 1.4; +} + +.form-control:focus, .form-select:focus { + border-color: #4338ca !important; + box-shadow: 0 0 0 3px rgba(67, 56, 202, 0.2) !important; +} + +/* Tab Navigation Pills */ +.nav-pills .nav-link { + color: #334155 !important; + font-weight: 600 !important; + font-size: 0.95rem !important; + border-radius: 8px; + padding: 0.5rem 1.1rem; + background: #e2e8f0 !important; + margin-right: 0.5rem; + border: 1.5px solid #cbd5e1 !important; +} + +.nav-pills .nav-link.active, .nav-pills .show>.nav-link { + background: #4338ca !important; + color: #ffffff !important; + border-color: #4338ca !important; + box-shadow: 0 3px 8px rgba(67, 56, 202, 0.35); +} + +/* Solid Clean White Cards */ +.card { + background: #ffffff !important; + border: 1.5px solid #cbd5e1 !important; + border-radius: 12px !important; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.05) !important; + margin-bottom: 1.5rem; + overflow: visible !important; + opacity: 1 !important; +} + +.card-header { + background: #f8fafc !important; + border-bottom: 1.5px solid #e2e8f0 !important; + font-weight: 800 !important; + font-family: 'Outfit', sans-serif; + font-size: 1.15rem !important; + color: #0f172a !important; + padding: 0.9rem 1.3rem; + border-radius: 11px 11px 0 0 !important; + display: flex; + align-items: center; + justify-content: space-between; +} + +.card-body { + background: #ffffff !important; + padding: 1.3rem; + border-radius: 0 0 11px 11px; + font-size: 0.98rem !important; +} + +/* STARTPAGE STYLING (Solid White Card, 100% Opaque, Standard Web Scale) */ +.seqpac-start-bg { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-image: + linear-gradient(rgba(5, 10, 24, 0.8), rgba(5, 10, 24, 0.85)), + url('seqpac_start_bg.jpg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-color: #050a18; + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; + padding: 1.5rem; +} + +.seqpac-start-modal { + background: #ffffff !important; + border-radius: 10px; + border: 1.5px solid #cbd5e1; + padding: 1.5rem 2rem; + max-width: 850px; + width: 100%; + box-shadow: 0 25px 60px rgba(0, 0, 0, 0.7); + text-align: center; + color: #0f172a !important; + opacity: 1 !important; +} + +.seqpac-start-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 14px; + background: #eef2ff !important; + border: 1.5px solid #c7d2fe !important; + border-radius: 9999px; + font-size: 0.9rem !important; + font-weight: 700; + color: #4338ca !important; +} + +.seqpac-start-title { + color: #0f172a !important; + font-family: 'Outfit', sans-serif; + font-weight: 900; + font-size: 1.9rem !important; + margin-bottom: 0; + letter-spacing: -0.03em; +} + +.seqpac-start-desc { + font-size: 1.05rem !important; + line-height: 1.6; + color: #334155 !important; + max-width: 680px; + margin: 0 auto 1.4rem auto; + font-weight: 500; +} + +.seqpac-meta-box { + background: #f8fafc !important; + border: 1.5px solid #e2e8f0 !important; + border-radius: 12px; + padding: 0.5rem 0.9rem; + text-align: left; + margin-bottom: 0.5rem; + font-size: 0.95rem !important; + line-height: 1.55; + color: #1e293b !important; +} + +.seqpac-radio-container { + background: #f1f5f9 !important; + border: 1.5px solid #cbd5e1 !important; + padding: 1.5rem 1.5rem; + border-radius: 14px; + margin-bottom: 1.6rem; + text-align: left; + width: 100%; +} + +.seqpac-radio-container .shiny-input-radiogroup { + margin: 0; + width: 100%; +} + +.seqpac-radio-container .shiny-options-group { + display: flex; + flex-direction: column; + gap: 0.75rem; + width: 100%; +} + +.seqpac-radio-container .radio, +.seqpac-radio-container .form-check { + margin: 0 !important; + padding: 0 !important; + display: block !important; + width: 100% !important; +} + +.seqpac-radio-container .radio label, +.seqpac-radio-container .form-check-label, +.seqpac-radio-container label { + font-size: 1.02rem !important; + font-weight: 700 !important; + color: #0f172a !important; + cursor: pointer; + display: flex !important; + align-items: flex-start !important; + gap: 0.65rem; + line-height: 1.45; + text-align: left; + width: 100% !important; + max-width: 100% !important; +} + +.seqpac-radio-container .radio label span, +.seqpac-radio-container label span { + display: inline !important; + white-space: normal !important; + flex: 1 1 auto; +} + +.seqpac-radio-container .radio input[type="radio"], +.seqpac-radio-container .form-check-input, +.seqpac-radio-container input[type="radio"] { + width: 1.2rem !important; + height: 1.2rem !important; + accent-color: #4338ca; + cursor: pointer; + margin-top: 0.2rem !important; + margin-right: 0.4rem !important; + flex-shrink: 0 !important; +} + +.btn-start-analysis { + background: #4338ca !important; + color: #ffffff !important; + font-family: 'Outfit', sans-serif; + font-weight: 800; + font-size: 1.15rem !important; + padding: 0.75rem 3.5rem; + border-radius: 10px; + border: none; + box-shadow: 0 4px 16px rgba(67, 56, 202, 0.4); + transition: all 0.2s ease; + cursor: pointer; +} + +.btn-start-analysis:hover { + background: #3730a3 !important; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(67, 56, 202, 0.5); +} + +/* Custom Buttons across the app */ +.btn-primary { + background: #4338ca !important; + border: none; + font-weight: 700 !important; + font-family: 'Outfit', sans-serif; + border-radius: 8px; + padding: 0.6rem 1.3rem; + font-size: 0.95rem !important; + box-shadow: 0 3px 8px rgba(67, 56, 202, 0.3); + color: #ffffff !important; +} + +.btn-primary:hover { + background: #3730a3 !important; + transform: translateY(-1px); +} + +.btn-success, .btn-download { + background: #059669 !important; + border: none; + font-weight: 700 !important; + font-family: 'Outfit', sans-serif; + color: #ffffff !important; + border-radius: 8px; + padding: 0.45rem 0.95rem; + font-size: 0.9rem !important; + box-shadow: 0 2px 6px rgba(5, 150, 105, 0.3); +} + +.btn-success:hover, .btn-download:hover { + background: #047857 !important; +} + +.btn-secondary { + background: #ffffff !important; + color: #1e293b !important; + border: 1.5px solid #cbd5e1 !important; + font-weight: 700 !important; + font-family: 'Outfit', sans-serif; + border-radius: 8px; + padding: 0.45rem 1rem; + font-size: 0.9rem !important; +} + +.btn-secondary:hover { + background: #f1f5f9 !important; + border-color: #94a3b8 !important; +} + +/* Info Icon Buttons */ +.info-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + font-size: 12px; + font-weight: 800; + color: #4338ca; + background: #eef2ff; + border-radius: 50%; + margin-left: 6px; + cursor: pointer; + border: 1px solid #c7d2fe; +} + +.info-btn:hover { + background: #4338ca; + color: #ffffff; +} + +/* Insight Cards */ +.insight-card { + background: #f8fafc !important; + border: 1.5px solid #cbd5e1 !important; + border-left: 5px solid #4338ca !important; + border-radius: 10px; + padding: 1.1rem 1.4rem; + margin-top: 1rem; + margin-bottom: 1.2rem; + font-size: 0.95rem !important; + color: #1e293b !important; +} + +.insight-card h5 { + font-size: 1.15rem !important; + margin-bottom: 0.5rem; + color: #0f172a !important; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.stat-pill-container { + display: flex; + gap: 0.8rem; + flex-wrap: wrap; + margin: 0.8rem 0; +} + +.stat-pill { + background: #ffffff !important; + border: 1.5px solid #cbd5e1 !important; + padding: 0.5rem 0.95rem; + border-radius: 10px; + font-size: 0.92rem !important; + color: #1e293b !important; +} + +.stat-pill .val { + font-weight: 800; + color: #4338ca; + font-size: 1.05rem !important; +} + +/* PERSISTENT 3-COLUMN APP FOOTER */ +.seqpac-main-footer { + margin-top: 3.5rem; + padding: 1.1rem 2rem; + background: #0b0f19 !important; + border-top: 1.5px solid #1e293b !important; + color: #94a3b8 !important; + font-size: 0.92rem !important; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; +} + +.seqpac-main-footer a { + color: #cbd5e1 !important; + text-decoration: none; + transition: color 0.2s ease; +} + +.seqpac-main-footer a:hover { + color: #38bdf8 !important; + text-decoration: underline; +} + +.seqpac-footer-left { + display: flex; + align-items: center; + gap: 6px; + font-weight: 600; + color: #cbd5e1; +} + +.seqpac-footer-center { + text-align: center; + font-weight: 500; + color: #94a3b8; +} + +.seqpac-footer-right { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; +} + +/* FIXED FOOTER CONTROLS & LOG DRAWER */ +.seqpac-footer-dock { + position: fixed; + bottom: 4rem; + right: 1.8rem; + z-index: 1040; + display: flex; + align-items: center; + gap: 12px; +} + +.footer-nav-home-btn { + background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%) !important; + color: #ffffff !important; + border: 1.5px solid #6366f1 !important; + border-radius: 30px !important; + padding: 0.5rem 1.1rem !important; + font-size: 0.95rem !important; + font-weight: 700 !important; + font-family: 'Outfit', sans-serif !important; + box-shadow: 0 4px 14px rgba(99, 102, 241, 0.4) !important; + cursor: pointer !important; + display: inline-flex !important; + align-items: center !important; + gap: 6px !important; + transition: all 0.2s ease !important; +} + +.footer-nav-home-btn:hover { + background: linear-gradient(135deg, #312e81 0%, #4338ca 100%) !important; + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(99, 102, 241, 0.55) !important; + color: #ffffff !important; +} + +.side-log-trigger { + position: fixed; + top: 4.8rem; + right: 1.5rem; + z-index: 1040; + background: #090d16 !important; + color: #38bdf8 !important; + border: 1.5px solid #30363d !important; + border-radius: 30px; + padding: 0.45rem 1.1rem; + font-size: 0.92rem !important; + font-weight: 700; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + opacity: 1 !important; + transition: all 0.2s ease; +} + +.side-log-trigger:hover { + background: #161b22 !important; + color: #ffffff !important; + transform: translateY(-2px); +} + +.side-log-drawer { + position: fixed; + top: 0; + right: -480px; + width: 440px; + height: 100vh; + background: #090d16 !important; + color: #c9d1d9 !important; + border-left: 2px solid #30363d !important; + box-shadow: -10px 0 30px rgba(0, 0, 0, 0.6); + z-index: 1050; + display: flex; + flex-direction: column; + transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-family: 'JetBrains Mono', Consolas, Monaco, monospace; + opacity: 1 !important; +} + +.side-log-drawer.open { + right: 0; +} + +.side-log-header { + background: #161b22 !important; + padding: 1rem 1.3rem; + border-bottom: 1.5px solid #30363d !important; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 0.98rem !important; + color: #f1f5f9 !important; +} + +.side-log-body { + padding: 1.1rem 1.3rem; + flex-grow: 1; + overflow-y: auto; + font-size: 0.88rem !important; + line-height: 1.6; + white-space: pre-wrap; + background: #090d16 !important; +} + +.side-log-body::-webkit-scrollbar { + width: 8px; +} +.side-log-body::-webkit-scrollbar-track { + background: #090d16; +} +.side-log-body::-webkit-scrollbar-thumb { + background: #30363d; + border-radius: 4px; +} + +.side-log-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5) !important; + z-index: 1045; +} + +.side-log-overlay.open { + display: block; +} + +/* DT tables - Solid Non-Transparent */ +.dataTables_wrapper { + padding: 0.8rem 0; + font-size: 0.92rem !important; +} + +table.dataTable { + border-collapse: collapse !important; + border-radius: 10px; + overflow: hidden; + font-size: 0.92rem !important; + background: #ffffff !important; +} + +table.dataTable thead th { + background-color: #f1f5f9 !important; + color: #0f172a !important; + font-weight: 800 !important; + font-family: 'Outfit', sans-serif; + border-bottom: 2px solid #cbd5e1 !important; + padding: 10px 14px !important; +} + +table.dataTable tbody td { + padding: 9px 14px !important; + border-bottom: 1px solid #f1f5f9 !important; + color: #1e293b !important; + background: #ffffff !important; +} + +.console-log-info { color: #58a6ff; } +.console-log-success { color: #3fb950; font-weight: 700; } +.console-log-warn { color: #d29922; } +.console-log-error { color: #f85149; font-weight: 700; } diff --git a/run-shiny.md b/run-shiny.md new file mode 100644 index 0000000..ae57dec --- /dev/null +++ b/run-shiny.md @@ -0,0 +1,72 @@ +## Running Locally in R + +1. Open R inside the repository path and install/load devtools to install the package dependencies: +```R +devtools::install(dependencies = TRUE) +``` + +2. Load the package and launch the app: +```R +library(seqpac) +run_seqpac_app() +``` + +## seqpac dashboard running guidelines (on demodata) + +### Step 1: Landing Page & Loading Data +1. Open your browser to `http://localhost:3838`. You will see the **Home** (Launch Page) with the workflow overview. +2. Click the **Get Started** button. This will automatically redirect you to the **Load / Create PAC** tab. +3. On the **Example Data** sub-tab (left panel), click the **Load Drosophila Dataset** button. +4. You should see the **PAC Object Overview** populate with: + - **Sequences (Rows)**: 9,131 + - **Samples (Columns)**: 9 + - **Annotations columns**: `Biotypes_mis0`, `Biotypes_mis3` + - **Phenotypic metadata**: `stage`, `batch`, `sample` +5. Click through the three main-panel tabs (**Pheno Table**, **Counts Preview**, **Anno Preview**) to inspect the loaded datasets. + +### Step 2: Filtering & Normalization +1. Click the **Filter & Normalize** tab in the main top navbar. +2. Keep the default settings: + - **Size Range**: 20 to 30 + - **Min Counts (Threshold)**: 5 + - **Min Coverage (% of samples)**: 20% + - **Normalizations**: Check **CPM** and **VST** +3. Click the **Apply Filter & Normalize** button. +4. Verify the outputs in the main panel: + - **Sequence Counts Summary**: It will show that the filters retained **472 of 9131 sequences** (~5.17%). + - **Available Normalizations**: Shows `cpm` and `vst` are now available. + - **Sequence Length Distribution**: The histogram will refresh showing the distribution of the filtered sequences (mostly peak at 21–23 nt). + +### Step 3: Annotation Explorer +1. Click the **Annotation Explorer** tab. +2. Under **Select Annotation Column to Analyze**, choose `Biotypes_mis0` (or `Biotypes_mis3`). +3. The **Annotation Statistics** table on the left will show the breakdown (e.g., how many sequences are annotated as `miRNA`, `tRNA`, `rRNA`, etc.). +4. The main panel table displays the complete `Anno` metadata matrix mapping sequences to their biotypes. You can search or filter this table interactively. + +### Step 4: Post-Filtering Analysis +Click the **Post-Filtering Analysis** tab. This tab has four sub-tabs: + +#### A. PCA (Principal Component Analysis) +1. Go to the **Principal Component Analysis** sub-tab. +2. Set **Color by Group** to `stage`. +3. Check the **Show Sample Labels** box. +4. Click **Run PCA**. +5. You will see a 2D scatter plot showing how your samples (e.g. Stage1, Stage3, Stage5) group together. + +#### B. DESeq2 (Differential Expression) +1. Go to the **Differential Expression (DESeq2)** sub-tab. +2. In the design formula box, type `~ stage` (or keep the default). +3. Set **Primary Factor** to `stage`. +4. Click **Run DESeq2 Analysis**. +5. After a few seconds, the main panel will populate with a searchable interactive table of differential expression results (`baseMean`, `log2FoldChange`, `pvalue`, `padj`, etc.). + +#### C. Size & Nucleotide Bias +1. Go to the **Size & Nucleotide Bias** sub-tab. +2. Set **Annotation Column** to `Biotypes_mis0`. +3. Choose **Size Distribution** and click **Generate Plots**. You will see biotype-specific sequence size histograms. +4. Switch to **Nucleotide Bias**, set position to `1`, and click **Generate Plots** to inspect the sequence starting nucleotide bias (e.g., high Uracil/T bias at position 1). + +#### D. Composition (Bar & Pie) +1. Go to the **Composition (Bar & Pie)** sub-tab. +2. Set **Annotation Column** to `Biotypes_mis0`. +3. Select **Stacked Bar** and click **Generate Composition Chart** to see the relative abundance (percentages) of different small RNA biotypes across your samples. \ No newline at end of file