diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml index 1d67cf3..4e7f93f 100644 --- a/.github/workflows/conda.yml +++ b/.github/workflows/conda.yml @@ -106,7 +106,7 @@ jobs: source "${HOME}/conda/etc/profile.d/conda.sh" conda activate ../env - Rscript -e "setRepositories(ind=1:5); remotes::install_github('NICHD-BSPC/carnation', upgrade='never')" + Rscript -e "setRepositories(ind=1:5); remotes::install_github('NICHD-BSPC/carnation@r4.3', upgrade='never')" Rscript -e "library(carnation)" diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index b2e578c..b3f8af7 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -61,7 +61,7 @@ jobs: source "${HOME}/conda/etc/profile.d/conda.sh" conda activate ../env - Rscript -e "setRepositories(ind=1:5); remotes::install_github('NICHD-BSPC/carnation', upgrade='never')" + Rscript -e "setRepositories(ind=1:5); remotes::install_github('NICHD-BSPC/carnation@r4.3', upgrade='never')" Rscript -e "pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)" - name: Deploy to GitHub pages 🚀 diff --git a/DESCRIPTION b/DESCRIPTION index fd45eb7..7198b0d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,23 +1,27 @@ Package: carnation -Title: Shiny App to Explore RNA-Seq Analysis -Version: 0.99.8 +Title: Interactive Exploration & Management of RNA-Seq Analyses +Version: 0.99.9 Authors@R: c( person("Apratim", "Mitra", , "apratim.mitra@nih.gov", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-3279-0054")), + person(c("Matthew", "Tyler"), "Menold", , "matthew.menold@gmail.com", + role = c("ctb"), comment = c(ORCID = "0009-0007-4728-2470")), person("Ryan", "Dale", , "ryan.dale@nih.gov", role = c("fnd"), comment = c(ORCID = "0000-0003-2664-3744")) ) -Description: Interactive Shiny dashboard app that can be used to - explore RNA-Seq analysis results including differential expression (DE), +Description: This package provides a highly interactive & modular shiny app to + explore three facets of RNA-Seq analysis: differential expression (DE), functional enrichment and pattern analysis. Several visualizations are implemented to provide a wide-ranging view of data sets. For DE analysis, we provide PCA plot, MA plot, Upset plot & heatmaps, in addition to a highly customizable gene plot. Seven different visualizations are available for functional enrichment analysis, - and we also support gene pattern analysis. In addition, the app - provides a platform to manage multiple projects and user groups - that can be run on a central server. + and we also support gene pattern analysis. Genes of interest can + be tracked across all modules using the gene scratchpad. In addition, + carnation provides an integrated platform to manage multiple projects + and user access that can be run on a central server to share with + collaborators. License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) diff --git a/NAMESPACE b/NAMESPACE index a638950..723b20e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -43,6 +43,7 @@ export(horizonUI) export(in_admin_group) export(install_carnation) export(is_site_admin) +export(is_valid_pattern_obj) export(loadDataServer) export(loadDataUI) export(maPlotServer) @@ -155,13 +156,17 @@ importFrom(htmltools,tags) importFrom(htmltools,withTags) importFrom(igraph,"V<-") importFrom(igraph,V) +importFrom(methods,.hasSlot) importFrom(methods,new) importFrom(plotly,add_markers) importFrom(plotly,add_trace) +importFrom(plotly,event_data) +importFrom(plotly,event_register) importFrom(plotly,ggplotly) importFrom(plotly,layout) importFrom(plotly,plot_ly) importFrom(plotly,plotlyOutput) +importFrom(plotly,plotlyProxy) importFrom(plotly,renderPlotly) importFrom(plotly,save_image) importFrom(plotly,toWebGL) diff --git a/NEWS.md b/NEWS.md index b4551c4..0552627 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,18 @@ # carnation +## v0.99.9 + +- carnation now supports `edgeR` and `limma` output in differential + expression analysis. +- The scatter plot now allows gene selection directly from the plot + and viewed in the table. Genes selected in the table can be added + to the gene scratchpad and tracked across the app. +- The main loading page now shows the currently loaded dataset. + This prevents accidentally reloading or replacing the current + data. +- Pattern analysis can now be added to a carnation object via + the `Load data` module, either in TSV or Rds format. + ## v0.99.8 - Add packages to `Suggests::` to address build issues. diff --git a/R/app.R b/R/app.R index 0e4323d..c90a509 100644 --- a/R/app.R +++ b/R/app.R @@ -453,7 +453,12 @@ run_carnation <- function(credentials=NULL, passphrase=NULL, enable_admin=TRUE, withSpinner( uiOutput('load_ui') - ) # withSpinner + ), # withSpinner + + br(), + fluidRow( + column(3, uiOutput('current_obj')) + ) # fluidRow ), # tabPanel tabPanel('DE analysis', @@ -647,6 +652,9 @@ run_carnation <- function(credentials=NULL, passphrase=NULL, enable_admin=TRUE, # list to hold original object and file path original <- reactiveValues(obj=NULL, path=NULL) + # reactiveValues to hold loaded project + current <- reactiveValues(proj=NULL, analysis=NULL) + # list to hold user details user_details <- reactiveValues(username=NULL, admin=FALSE) @@ -1073,6 +1081,46 @@ run_carnation <- function(credentials=NULL, passphrase=NULL, enable_admin=TRUE, x }) + # make sure 'padj' & 'log2FoldChange' columns exist in res objects + # or supported alternatives exist + sanitized_res_list <- obj[[ res.name ]] + + # supported column names + column_names <- config()$server$de_analysis$column_names + defaults <- names(column_names) + + # these res objects will be dropped + drop_res_names <- NULL + + for(name in names(sanitized_res_list)){ + res <- sanitized_res_list[[ name ]]$res + res <- as.data.frame(res) + + for(cname in defaults){ + idx <- colnames(res) %in% column_names[[ cname ]] + + # if matches exist + if(sum(idx) > 0){ + if(sum(idx) > 1){ + # only use the first match if multiple matches + # and show warning + message('Warning: Ambiguous ', cname, 'column for ', name, '.\nUsing ', colnames(res)[which(idx)[1]]) + } + + idx <- which(idx)[1] + colnames(res)[idx] <- cname + } else { + message('Unsupported res type for ', name, ':', cname, ' column not found, skipping') + drop_res_names <- c(drop_res_names, name) + } + } + sanitized_res_list[[ name ]]$res <- res + } + + # remove unsupported res objects + sanitized_res_list <- sanitized_res_list[ !names(sanitized_res_list) %in% drop_res_names ] + obj[[ res.name ]] <- sanitized_res_list + # add obj slots to reactive values obj <- make_final_object(obj) @@ -1237,8 +1285,28 @@ run_carnation <- function(credentials=NULL, passphrase=NULL, enable_admin=TRUE, selected='DE analysis') updateTabsetPanel(session, inputId='de_mode', selected='Summary') + + # update current project + current$proj <- input$dds + + al <- assay.list$l[[ input$dds ]] + idx <- which(unname(al) == input$assay) + current$analysis <- names(assay.list$l[[ input$dds ]])[idx] + }) # observeEvent load data + # show loaded dataset + output$current_obj <- renderUI({ + req(current$proj) + + tags$div( + class='div-stats-card', + tags$p('Currently loaded:'), + tags$p(' - Project: ', tags$i(current$proj)), + tags$p(' - Analysis: ', tags$i(current$analysis)) + ) + }) + # update comparison menus after load observeEvent(app_object$res, { validate( @@ -1604,7 +1672,25 @@ run_carnation <- function(credentials=NULL, passphrase=NULL, enable_admin=TRUE, gene.to.plot=gene_scratchpad()) }) - scatterPlotServer('scatterplot', app_object, scatterplot_args, config) + scatter_data <- scatterPlotServer('scatterplot', + app_object, + scatterplot_args, + gene_scratchpad, + reactive({ input$reset.genes }), + config) + + observeEvent(scatter_data(), { + g <- scatter_data()$genes + + # only update scratchpad if different genes returned + if(length(setdiff(g, input$gene.to.plot)) != 0){ + # update gene selector with clicked genes + updateSelectizeInput(session, 'gene.to.plot', + choices=gene.id$gene, + selected=g, + server=TRUE) + } + }) ##################### UpSet plot ######################### diff --git a/R/carnation-pkg.R b/R/carnation-pkg.R index 24cf3b7..874d4e3 100644 --- a/R/carnation-pkg.R +++ b/R/carnation-pkg.R @@ -12,5 +12,7 @@ #' - Manage local data in single-user mode or deploy on a server to share with #' collaborators using in-built user management system. #' +#' Main function to run the app: [run_carnation()] +#' #' @keywords internal '_PACKAGE' diff --git a/R/functions.R b/R/functions.R index edc6d7f..81724b1 100644 --- a/R/functions.R +++ b/R/functions.R @@ -448,6 +448,69 @@ get_y_init <- function(df, y_delta, pseudocount){ return( c(min.init, max.init) ) } +#' Validate Pattern Analysis Object Schema +#' +#' Validate the schema for a single `degpatterns` analysis element used by the +#' pattern analysis module. +#' +#' @param pattern_obj A single pattern analysis element. Must be either a +#' `data.frame` or a list containing a `normalized` `data.frame`. +#' @param require_symbol Logical, if `TRUE` require a `symbol` column in the +#' analysis table. +#' +#' @return Returns `TRUE` when validation succeeds, otherwise returns `FALSE` +#' after emitting a message describing the issue. +#' +#' @examples +#' data(degpatterns_dex, package = "carnation") +#' +#' is_valid_pattern_obj(degpatterns_dex) +#' +#' @export +is_valid_pattern_obj <- function(pattern_obj, require_symbol = FALSE){ + if(is.null(pattern_obj)){ + message('"pattern_obj" cannot be NULL') + return(FALSE) + } + + req_cols <- c("genes", "value") + if(require_symbol) req_cols <- c(req_cols, "symbol") + + if(is.data.frame(pattern_obj)){ + df <- pattern_obj + } else if(is.list(pattern_obj) && + "normalized" %in% names(pattern_obj) && + is.data.frame(pattern_obj$normalized)){ + df <- pattern_obj$normalized + } else { + message( + '"pattern_obj" must be a data.frame or a list containing a data.frame in "$normalized"' + ) + return(FALSE) + } + + missing_cols <- setdiff(req_cols, colnames(df)) + if(length(missing_cols) > 0){ + message( + '"pattern_obj" is missing required column(s): ', + paste(missing_cols, collapse = ", ") + ) + return(FALSE) + } + + # Pattern module expects at least one clustering column. + cluster_cols <- c("cluster", grep("^cutoff", colnames(df), value = TRUE)) + if(!any(cluster_cols %in% colnames(df))){ + message( + '"pattern_obj" must contain at least one cluster column: ', + '"cluster" or columns starting with "cutoff"' + ) + return(FALSE) + } + + TRUE +} + #' Make final object for internal use by the app #' #' This function takes an uploaded object and sanitizes @@ -508,6 +571,9 @@ make_final_object <- function(obj){ # get res.list names comp.names <- names(obj[[res.name]]) + # these dds objects are not linked to res_list + orphan_dds <- NULL + # if res.list contains 'res', 'dds', 'label' elements # build the following: # @@ -591,9 +657,8 @@ make_final_object <- function(obj){ } else if(name %in% names(obj[[res.name]])){ res <- obj[[res.name]][[name]] } else { - message('no matching dds object found for ', name, ', skipping\n') - obj[[dds.name]] <- obj[[dds.name]][!names(obj[[dds.name]]) %in% name] - obj[[rld.name]] <- obj[[rld.name]][!names(obj[[rld.name]]) %in% name] + message('no matching dds object found for ', name, '\n') + orphan_dds <- c(orphan_dds, name) next } @@ -658,6 +723,20 @@ make_final_object <- function(obj){ obj$dds_mapping <- dds_mapping } + # use full idmap for orphan dds objects + if(!is.null(orphan_dds)){ + for(name in orphan_dds){ + dds <- obj[[dds.name]][[name]] + rownames(dds) <- all_idmap[rownames(dds)] + + rld <- obj[[rld.name]][[name]] + rownames(rld) <- all_idmap[rownames(rld)] + + obj[[dds.name]][[name]] <- dds + obj[[rld.name]][[name]] <- rld + } + } + # if degpatterns element exists, add symbol column if(length(degpatterns.name) != 0){ obj[[ degpatterns.name ]] <- lapply(obj[[ degpatterns.name ]], @@ -2485,6 +2564,7 @@ plotScatter.label <- function(compare, #' @param alpha float, marker opacity (default=1). #' @param size float, marker size (default=4). #' @param show.grid string, can be 'yes' (default) or 'no'. +#' @param source name of source to return event_data from #' @return plotly handle #' @@ -2567,7 +2647,8 @@ plotScatter.label_ly <- function(compare, lines=c('yes', 'yes', 'yes'), alpha=1, size=4, - show.grid='yes') { + show.grid='yes', + source='A') { names(color.palette) <- c('None', label_x, label_y, 'Both - opposite LFC sign', 'Both - same LFC sign') @@ -2591,7 +2672,7 @@ plotScatter.label_ly <- function(compare, show.grid <- if (show.grid == 'yes') TRUE else FALSE - p <- plot_ly() + p <- plot_ly(source=source) # list of plotting characters pch <- c('in'='circle', @@ -2620,7 +2701,7 @@ plotScatter.label_ly <- function(compare, p <- p %>% add_trace(data = df_i[df_i$shape == sym, ], x = ~get(x), y = ~get(y), - type = 'scatter', + type = 'scattergl', mode = 'markers', text = ~get(name.col), hoverinfo = 'text', @@ -2639,14 +2720,16 @@ plotScatter.label_ly <- function(compare, lab.df <- df[df[[name.col]] %in% lab.genes, ] if(nrow(lab.df) > 0){ p <- p %>% - add_markers(x=lab.df[[x]], y=lab.df[[y]], - text=lab.df$name.col, - hoverinfo='none', - name='Gene scratchpad', - marker=list(color='black', - symbol='circle-open', - size=size*2, - line=list(width=2))) + add_trace(x=lab.df[[x]], y=lab.df[[y]], + text=lab.df$name.col, + hoverinfo='none', + type='scattergl', + mode='markers', + name='Gene scratchpad', + marker=list(color='black', + symbol='circle-open', + size=size*2, + line=list(width=2))) } } @@ -2670,6 +2753,7 @@ plotScatter.label_ly <- function(compare, orientation = 'v', xanchor = 'left', yanchor = 'middle', + itemsizing = 'constant', font = list(size = 14), title = list(text = 'Significant in:'))) @@ -2702,7 +2786,7 @@ plotScatter.label_ly <- function(compare, # Add all collected shapes at once if (length(shapes_to_add) > 0) { - p <- p %>% layout(shapes = shapes_to_add) + p <- p %>% layout(shapes = shapes_to_add, dragmode='select') } # Return the Plotly plot object diff --git a/R/imports.R b/R/imports.R index 155f2ae..53e91a9 100644 --- a/R/imports.R +++ b/R/imports.R @@ -19,8 +19,8 @@ #' @importFrom htmltools withTags tagAppendChild tagAppendChildren tags tagList #' @importFrom igraph V "V<-" #' @importFrom MatrixGenerics rowVars -#' @importFrom methods new -#' @importFrom plotly plotlyOutput renderPlotly layout plot_ly add_trace add_markers toWebGL save_image ggplotly +#' @importFrom methods new .hasSlot +#' @importFrom plotly plotlyOutput renderPlotly layout plot_ly add_trace add_markers toWebGL save_image ggplotly plotlyProxy event_data event_register #' @importFrom RColorBrewer brewer.pal #' @importFrom reticulate py_install use_virtualenv #' @importFrom rintrojs introjsUI introBox introjs diff --git a/R/load-new-data.R b/R/load-new-data.R index 4f3962e..a620271 100644 --- a/R/load-new-data.R +++ b/R/load-new-data.R @@ -42,7 +42,6 @@ NULL loadDataUI <- function(id){ ns <- NS(id) - # TODO: add multiple dds tagList( fluidRow( column(12, align='left', @@ -69,6 +68,14 @@ loadDataUI <- function(id){ textOutput(ns('func_summary')), br(), br(), + span('Step 4: ', + style='font-style: italic;'), + actionButton(ns('add_dp'), 'Add pattern analysis results'), + span('(Optional)', + style='font-style: italic;'), + textOutput(ns('dp_summary')), + br(), br(), + actionButton(ns('create_new'), 'Create data set', class='btn-primary') ) # tagList @@ -767,7 +774,7 @@ loadDataServer <- function(id, username, config, rds=NULL){ # GSEA columns cprof_gsea_cols <- c('ID', 'Description', 'core_enrichment', 'setSize', - 'pvalue', 'p,adjust', 'qvalue', 'NES', 'setSize') + 'pvalue', 'p.adjust', 'qvalue', 'NES', 'setSize') # check for clusterProfiler columns if(all(cprof_or_cols %in% colnames(eres))){ @@ -841,6 +848,211 @@ loadDataServer <- function(id, username, config, rds=NULL){ removeModal() }) + #################### Pattern analysis results #################### + + observeEvent(input$add_dp, { + if(is.null(new_obj$dds_list)){ + showNotification( + 'Must add at least one counts table before uploading pattern analysis results!', + type='error' + ) + } + + validate( + need(!is.null(new_obj$dds_list), 'Must have counts') + ) + + if(is.null(new_obj$res_list)){ + showNotification( + 'Must add at least one DE results table before uploading pattern analysis results!', + type='error' + ) + } + + validate( + need(!is.null(new_obj$res_list), 'Must have DE results') + ) + + showModal( + modalDialog( + fileInput(ns('dp_file'), + label='Pattern analysis result file(s)', + multiple=TRUE), + + tags$div( + span('*Can add multiple files here'), + br(), br(), + span('Supported formats:'), + tags$ul( + tags$li('RDS files containing a degPatterns-like object (data.frame or list with "normalized" data.frame).'), + tags$li('Tab-delimited text files (TSV) containing pattern analysis table columns.') + ), + style='font-style: italic;' + ), + + footer=tagList( + actionButton(ns('add_dp_files'), 'OK'), + modalButton('Cancel') + ), + easyClose=TRUE + ) + ) + }) + + observeEvent(input$add_dp_files, { + if(is.null(input$dp_file)){ + showNotification( + 'No pattern analysis results uploaded!', + type='error' + ) + } + + req(input$dp_file) + + tag <- tagList( + fluidRow( + column(6, strong('Analysis name')), + column(6, strong('File')) + ) + ) + + for(i in seq_len(nrow(input$dp_file))){ + tmp_id <- tools::file_path_sans_ext(basename(input$dp_file$name[i])) + + tag <- tagAppendChildren( + tag, + fluidRow( + column(6, + textInput(ns(paste0('dp_id', i)), + label=NULL, + value=tmp_id) + ), + column(6, + span(input$dp_file$name[i]) + ) + ) + ) + } + + tag <- tagAppendChildren( + tag, + tags$div( + tags$ul( + tags$li('Analysis name: Unique name without white-space or commas.'), + tags$li('Each uploaded object must match the expected pattern-analysis schema.') + ), + style='font-style: italic;' + ) + ) + + showModal( + modalDialog( + tag, + footer=tagList( + actionButton(ns('add_dp_do'), 'OK'), + modalButton('Cancel') + ), + easyClose=TRUE + ) + ) + }) + + output$dp_summary <- renderText({ + if(!is.null(new_obj$degpatterns)){ + nsets <- length(new_obj$degpatterns) + + if(nsets > 1){ + msg <- paste(nsets, 'pattern analysis results') + } else { + msg <- paste(nsets, 'pattern analysis result') + } + msg + } + }) + + observeEvent(input$add_dp_do, { + req(input$dp_file) + + ndp <- nrow(input$dp_file) + all_names <- paste0('dp_id', seq_len(ndp)) + + # check that analysis names are not empty + for(name in all_names){ + if(input[[ name ]] == ''){ + showNotification( + 'Pattern analysis name cannot be empty!', + type='warning' + ) + } + + validate( + need(input[[ name ]] != '', '') + ) + + if(grepl('[[:space:],]', input[[ name ]])){ + showNotification( + 'Pattern analysis name cannot contain white-space or commas!', + type='warning' + ) + } + + validate( + need(!grepl('[[:space:],]', input[[ name ]]), '') + ) + } + + for(i in seq_len(ndp)){ + dp_id <- input[[ paste0('dp_id', i) ]] + f <- input$dp_file$datapath[i] + ext <- tolower(tools::file_ext(input$dp_file$name[i])) + + if(ext == 'rds'){ + dp_obj <- readRDS(f) + } else if(ext %in% c('tsv', 'txt')){ + dp_obj <- read.table(f, sep='\t', header=TRUE) + } else { + showNotification( + paste0('Unsupported pattern analysis file type: "', ext, '". Use .rds, .tsv or .txt'), + type='error' + ) + + validate( + need(ext %in% c('rds', 'tsv', 'txt'), '') + ) + } + + is_valid <- is_valid_pattern_obj(dp_obj, require_symbol=FALSE) + if(!is_valid){ + showNotification( + paste0('Invalid pattern analysis object: "', input$dp_file$name[i], '"'), + type='error' + ) + + validate( + need(is_valid, '') + ) + } + + if(is.null(new_obj$degpatterns)){ + new_obj$degpatterns <- setNames(list(dp_obj), dp_id) + } else if(dp_id %in% names(new_obj$degpatterns)){ + showNotification( + paste0('Pattern analysis named "', dp_id, + '" already present in object. Please choose different name'), + type='error' + ) + + validate( + need(!dp_id %in% names(new_obj$degpatterns), '') + ) + } else { + new_obj$degpatterns[[ dp_id ]] <- dp_obj + } + } + + removeModal() + }) + #################### save object #################### observeEvent(input$create_new, { @@ -954,11 +1166,12 @@ loadDataServer <- function(id, username, config, rds=NULL){ # - this prevents saved object from becoming very large if another # object has been previously loaded combined_final$dds <- lapply(combined_final$dds, function(x){ - attr(x@design, '.Environment') <- NULL + if(.hasSlot(x, 'design')) attr(x@design, '.Environment') <- NULL x }) - attr(combined_final$all_dds@design, '.Environment') <- NULL + if(.hasSlot(combined_final$all_dds, 'design')) + attr(combined_final$all_dds@design, '.Environment') <- NULL saveRDS(combined_final, ofile, compress=as.logical(input$compress)) @@ -988,8 +1201,27 @@ loadDataServer <- function(id, username, config, rds=NULL){ ) } else { if(!input$dir_new %in% y$data_area[[ug]]){ - y$data_area[[ug]] <- c(y$data_area[[ug]], - input$dir_new) + # check if any parent path of new_dir exists in data_areas + # if not, add to list + parent <- FALSE + + path <- normalizePath(path.expand(input$dir_new), mustWork=FALSE) + current_areas <- normalizePath(path.expand(y$data_area[[ ug ]]), mustWork=FALSE) + + while(TRUE){ + if(path %in% current_areas){ + parent <- TRUE + break + } else { + path <- dirname(path) + if(path == '.' || path == '/') break + } + } + + if(!parent){ + y$data_area[[ug]] <- c(y$data_area[[ug]], + input$dir_new) + } } } save_access_yaml(y) diff --git a/R/pattern-plot.R b/R/pattern-plot.R index b58e7e2..684af84 100644 --- a/R/pattern-plot.R +++ b/R/pattern-plot.R @@ -495,6 +495,8 @@ patternPlotServer <- function(id, ) obj <- pattern_obj()[[input$dp_analysis]] + + # if DEGpatterns object, only use 'normalized' slot if(!is.data.frame(obj)) obj <- obj$normalized # get metadata and attach if necessary diff --git a/R/save_object.R b/R/save_object.R index aad8189..faea2a6 100644 --- a/R/save_object.R +++ b/R/save_object.R @@ -295,11 +295,12 @@ saveServer <- function(id, original, current, coldata, pattern, username, config # - this prevents saved object from becoming very large if another # object has been previously loaded obj$dds <- lapply(obj$dds, function(x){ - attr(x@design, '.Environment') <- NULL + if(.hasSlot(x, 'design')) attr(x@design, '.Environment') <- NULL x }) - attr(obj$all_dds@design, '.Environment') <- NULL + if(.hasSlot(obj$all_dds, 'design')) + attr(obj$all_dds@design, '.Environment') <- NULL saveRDS(obj, destpath, compress=as.logical(input$compress)) @@ -328,8 +329,27 @@ saveServer <- function(id, original, current, coldata, pattern, username, config ) } else { if(!input$rds_path %in% y$data_area[[ug]]){ - y$data_area[[ug]] <- c(y$data_area[[ug]], - input$rds_path) + # check if any parent path of RDS object exists in data_areas + # if not, add to list + parent <- FALSE + + path <- normalizePath(dirname(path.expand(input$rds_path)), mustWork=FALSE) + current_areas <- normalizePath(path.expand(y$data_area[[ ug ]]), mustWork=FALSE) + + while(TRUE){ + if(path %in% current_areas){ + parent <- TRUE + break + } else { + path <- dirname(path) + if(path == '.' || path == '/') break + } + } + + if(!parent){ + y$data_area[[ug]] <- c(y$data_area[[ug]], + input$rds_path) + } } } save_access_yaml(y) diff --git a/R/scatter-plot.R b/R/scatter-plot.R index 817490c..48c4990 100644 --- a/R/scatter-plot.R +++ b/R/scatter-plot.R @@ -6,8 +6,9 @@ #' @param id Module id #' @param panel string, can be 'sidebar' or 'main' passed to UI #' @param obj reactiveValues object containing carnation object passed to server -#' @param plot_args reactive containing 'fdr.thres' (padj threshold), 'fc.thres' (log2FC) & -#' 'gene.to.plot' (genes to be labeled) passed to server +#' @param plot_args reactive containing 'fdr.thres' (padj threshold), 'fc.thres' (log2FC) +#' @param gene_scratchpad reactive containing gene scratchpad genes +#' @param reset_genes reactive to reset gene scratchpad selection #' @param config reactive list with config settings passed to server #' #' @returns @@ -32,11 +33,13 @@ #' plot_args <- reactive({ #' list( #' fdr.thres=0.1, -#' fc.thres=0, -#' gene.to.plot=c('gene1', 'gene2') +#' fc.thres=0 #' ) #' }) #' +#' gene_scratchpad <- reactive({ c('gene1', 'gene2') }) +#' reset_genes <- reactiveVal() +#' #' config <- reactiveVal(get_config()) #' #' shinyApp( @@ -45,7 +48,8 @@ #' mainPanel(scatterPlotUI('p', 'sidebar')) #' ), #' server = function(input, output, session){ -#' scatterPlotServer('p', obj, plot_args, config) +#' scatter_data <- scatterPlotServer('p', obj, plot_args, +#' gene_scratchpad, reset_genes, config) #' } #' ) #' @@ -108,16 +112,6 @@ scatterPlotUI <- function(id, panel){ ) # column ), # fluidRow - fluidRow( - column(6, h5('Show table?')), - column(6, - selectInput(ns("show_table"), label=NULL, - choices=c('yes', 'no'), - selected='yes' - ) # selectInput - ) # column - ), # fluidRow - wellPanel( style='background: white', @@ -285,23 +279,75 @@ scatterPlotUI <- function(id, panel){ ) # column ), # fluidRow - withSpinner( - uiOutput(ns('scatterplot_out')) - ), # withSpinner + conditionalPanel(paste0('input["', ns('plot_interactive'), '"] == "yes"'), + withSpinner( + plotlyOutput(ns('plotly_out'), height='600px') + ) + ), # conditionalPanel + conditionalPanel(paste0('input["', ns('plot_interactive'), '"] == "no"'), + withSpinner( + plotOutput(ns('plot_out'), height='600px') + ) + ), # conditionalPanel - withSpinner( - DTOutput(ns('scatter_datatable_out')) - ) # withSpinner + fluidRow( + column(3, + + h4('Selection settings'), + + bsCollapse(open='Plot selection', + bsCollapsePanel('Plot selection', + fluidRow(style='margin-left: 2px;', + uiOutput(ns('pt_selected')), + actionButton(ns('filter_sel_do'), + label='Show/Hide in table'), + actionButton(ns('reset_plt_selection'), + 'Clear', + class='btn-primary') + ) # fluidRow + ) # bsCollapsePanel + ), # bsCollapse + + bsCollapse(open='Table selection', + bsCollapsePanel('Table selection', + fluidRow(style='margin-left: 2px;', + actionButton(ns('add_selected'), 'Add to scratchpad'), + actionButton(ns('reset_tbl'), + 'Clear selection', + class='btn-primary') + ) # fluidRow + ) # bsCollapsePanel + ), # bsCollapse + + h4('Filter table by significance'), + selectizeInput(ns('filter_tbl'), label=NULL, width='100%', + choices=NULL, selected=NULL, multiple=TRUE), + + # For 'Select all' and 'Select none' buttons + fluidRow(style='margin-bottom: 5px; margin-left: 2px;', + actionButton(ns('select_all'), 'Select all', class = "btn-secondary"), + actionButton(ns('select_none'), 'Select none', class = "btn-secondary"), + actionButton(ns('filter_tbl_do'), + label='Apply', + icon=icon('filter'), + class='btn-primary') + ) # fluidRow + + ), # column + column(9, style='margin-top: 10px;', + withSpinner( + DTOutput(ns('scatter_tbl')) + ) # withSpinner + ) # column + ) # fluidRow - # conditionalPanel("input.show_table == 'yes'", - # ) # conditionalPanel ) # tagList } # else if panel='main' } # scatterPlotUI #' @rdname scattermod #' @export -scatterPlotServer <- function(id, obj, plot_args, config){ +scatterPlotServer <- function(id, obj, plot_args, gene_scratchpad, reset_genes, config){ moduleServer( id, @@ -332,9 +378,21 @@ scatterPlotServer <- function(id, obj, plot_args, config){ df_react <- reactiveVal(NULL) df_full <- reactiveVal(NULL) + # reactive to hold plot source + plot_source <- reactiveVal(NULL) + # reactive values to keep track of axis limits axis_limits <- reactiveValues(lim.x=NULL, lim.y=NULL) + # reactive to hold labeled genes + genes_clicked <- reactiveValues(g=NULL) + + # reactive to hold selected genes + selected_genes <- reactiveValues(g=NULL) + + # reactive to toggle table selection by selected genes + filter_tbl_by_sel_genes <- reactiveVal(FALSE) + # Initialize comparison selection dropdowns when data is available observeEvent(comp_all(), { validate( @@ -351,7 +409,7 @@ scatterPlotServer <- function(id, obj, plot_args, config){ df_react(NULL) df_full(NULL) - }) + }) # -------------------------------------------------------------- # # --------------- Set FDR and FC thresholds ---------------- # @@ -369,6 +427,21 @@ scatterPlotServer <- function(id, obj, plot_args, config){ curr_thres$fdr.thres <- fdr.thres curr_thres$fc.thres <- fc.thres }) + + # gene scratchpad + + observeEvent(gene_scratchpad(), { + g <- gene_scratchpad() + if(any(g != '')){ + if(!all(g %in% genes_clicked$g)) + genes_clicked$g <- unique(c(genes_clicked$g, g)) + } + }) + + observeEvent(reset_genes(), { + genes_clicked$g <- NULL + }) + # ------------------------------------------------------------# # ------------- helper functions -----------------------------# @@ -424,7 +497,8 @@ scatterPlotServer <- function(id, obj, plot_args, config){ input$x_axis_comp, input$y_axis_comp, curr_thres$fc.thres, - curr_thres$fdr.thres) + curr_thres$fdr.thres, + genes_clicked$g) }, { req(app_object()$res) @@ -552,14 +626,33 @@ scatterPlotServer <- function(id, obj, plot_args, config){ } # Significance as factor, to reorder in the graph - df$significance <- factor(df$significance, levels = c('None', label_i, label_j, 'Both - opposite LFC sign', 'Both - same LFC sign')) + sig_levels <- c('None', label_i, label_j, + 'Both - opposite LFC sign', 'Both - same LFC sign') + df$significance <- factor(df$significance, levels = sig_levels) # Store the dataframe in the df_react reactiveVal + updateSelectizeInput(session, 'filter_tbl', choices=sig_levels, selected=sig_levels) + df_react(df) df_full(df_full) flags$data_loaded <- flags$data_loaded + 1 }) + + # observers for tbl filters + observeEvent(input$select_all, { + # Get all possible comparison options + all_sig <- levels(df_react()$significance) + # Update the select_none checkbox + updateSelectizeInput(session, 'filter_tbl', selected=all_sig) + }) + + # Observer for Select none checkbox + observeEvent(input$select_none, { + # Update comp_all with no selected comparisons + updateSelectizeInput(session, 'filter_tbl', selected=character(0)) + }) + # ---------------------------------------------------------- # # --------------- Swap comparisons button ----------------- # @@ -647,8 +740,7 @@ scatterPlotServer <- function(id, obj, plot_args, config){ } # filter rows with NA values - df <- df %>% filter(!is.na(.data[[ xcol ]])) - df <- df %>% filter(!is.na(.data[[ xcol ]])) + df <- df %>% filter(!is.na(.data[[ xcol ]]), !is.na(.data[[ ycol ]])) # Create column with plotting character based on lim.x # Change point values for those outside plot limits to values that are within the limits @@ -698,7 +790,7 @@ scatterPlotServer <- function(id, obj, plot_args, config){ ) # Get genes to label - genes <- plot_args()$gene.to.plot + genes <- genes_clicked$g if(is.null(genes) || all(genes %in% '')){ lab.genes <- NULL } else { @@ -764,7 +856,7 @@ scatterPlotServer <- function(id, obj, plot_args, config){ lab.genes <- params[['lab.genes']] color.palette <- params[['color.palette']] - plotScatter.label_ly( + p <- plotScatter.label_ly( compare=input$compare, df=df, label_x=input$x_axis_comp, @@ -777,93 +869,230 @@ scatterPlotServer <- function(id, obj, plot_args, config){ size=input$size, show.grid=input$show_grid, color.palette=color.palette, - lab.genes=lab.genes + lab.genes=lab.genes, + source='scatter' ) - }) # eventReactive scatterplot_ly + # save plot source to reactive + plot_source('scatter') + + event_register(p, 'plotly_selected') + + p + }) # eventReactive scatterplot_ly # ----------------------------------------------------- # - # ---------------------- renerUI ---------------------- # - output$scatterplot_out <- renderUI({ - if (input$plot_interactive == 'yes') { + output$plotly_out <- renderPlotly({ + scatterplot_ly() + }) - p <- scatterplot_ly() %>% toWebGL() + output$plot_out <- renderPlot({ + scatterplot() + theme(text=element_text(size=18)) + }) - output$plot1 <- renderPlotly({ p }) + #################### point selection #################### - withSpinner( - plotlyOutput(ns('plot1'), height='600px') - ) + plotProxy <- plotlyProxy('plotly_out', session) + + get_pt_selected <- reactive({ + req(plot_source()) + event_data('plotly_selected', source=plot_source()) + }) - } else if (input$plot_interactive == 'no') { + observeEvent(get_pt_selected(), { + df <- get_pt_selected() - p <- scatterplot() + theme(text=element_text(size=18)) + data_df <- df_full() + xcol <- paste0(input$compare, '.x') + ycol <- paste0(input$compare, '.y') - output$plot2 <- renderPlot({ p }) + # get points by matching coords & key + keys <- paste(df$x, df$y) + data_keys <- paste(data_df[, xcol], data_df[, ycol]) - withSpinner( - plotOutput(ns('plot2'), height='600px') + new <- data_df$geneid[data_keys %in% keys] + curr <- unique(unlist(selected_genes$g)) + + # only add new points + if(!all(new %in% curr)){ + new_idx <- which(!new %in% curr) + showNotification( + paste0('Adding ', length(new_idx), ' genes to selection') + ) + + selected_genes$g[[ length(selected_genes$g) + 1 ]] <- new[new_idx] + } else if(length(new) > 0){ + showNotification( + paste0('All selected genes already in selection'), + type='warning' ) } - }) # renderUI + }) + + output$pt_selected <- renderUI({ + np <- length(unique(unlist(selected_genes$g))) + + tagList( + fluidRow( + column(12, style='margin-bottom: 10px;', + + paste(np, 'genes selected') + ) + ) + ) + }) + + observeEvent(input$reset_plt_selection, { + selected_genes$g <- NULL + }) + + observeEvent(input$filter_sel_do, { + filter_tbl_by_sel_genes(!filter_tbl_by_sel_genes()) + }) + # ------------------------------------------------------- # # -------------------- renerDataTable ------------------ # - # Optionally display datatable for scatter plot input data - output$scatter_datatable_out <- renderDT({ - if (input$show_table == 'yes') { - validate( - need(!is.null(df_full()), '') - ) - df <- df_full() + scatter_dt <- eventReactive(c(df_react(), input$x_axis_comp, input$y_axis_comp, + input$filter_tbl_do, filter_tbl_by_sel_genes()), { + validate( + need(!is.null(df_full()), '') + ) + df <- df_full() - # move geneid to beginning to work with container - df <- df %>% relocate('geneid') + # add significance column + sig_df <- df_react() - # Define the columns to format to 3 sig figs - columns_to_format <- c("padj.x", "padj.y", "log2FoldChange.x", "log2FoldChange.y") - which_cols <- which(colnames(df) %in% columns_to_format) - border_cols <- c(1, grep('padj', colnames(df))) + # map geneid to significance + sigvec <- sig_df$significance + names(sigvec) <- sig_df$geneid + df$significance <- sigvec[df$geneid] - all_comps <- c(input$x_axis_comp, input$y_axis_comp) - validate( - need(!any(is.null(all_comps)), 'Waiting for selection') + if(filter_tbl_by_sel_genes()){ + idx <- df$geneid %in% unique(unlist(selected_genes$g)) + + if(sum(idx) == 0){ + showNotification( + 'No rows left in table after filtering', type='error' + ) + + validate(need(sum(idx) > 0, '')) + } + df <- df[idx,] + } + + # get current filters + curr_filters <- input$filter_tbl + idx <- df$significance %in% curr_filters + if(sum(idx) == 0){ + showNotification( + 'No rows left in table after filtering', type='error' ) - # build container for table - sketch <- htmltools::withTags(table( - class = 'display', - tags$thead( - tags$tr( - tags$th(rowspan=2, 'geneid'), - lapply(all_comps, - function(x) tags$th(class='dt-center', colspan=2, x)) - ), - tags$tr( - lapply(rep(c('log2FoldChange', 'padj'), 2), tags$th) - ) + validate(need(sum(idx) > 0, '')) + } + # move geneid & significance to beginning to work with container + df[which(idx),] %>% relocate('significance') %>% relocate('geneid') + + }) + + # Optionally display datatable for scatter plot input data + output$scatter_tbl <- renderDT({ + validate( + need(!is.null(scatter_dt()), '') + ) + df <- scatter_dt() + + # Define the columns to format to 3 sig figs + columns_to_format <- c("padj.x", "padj.y", "log2FoldChange.x", "log2FoldChange.y") + which_cols <- which(colnames(df) %in% columns_to_format) + border_cols <- c(1, 2, grep('padj', colnames(df))) + + all_comps <- c(input$x_axis_comp, input$y_axis_comp) + validate( + need(!any(is.null(all_comps)), 'Waiting for selection') + ) + + # build container for table + sketch <- htmltools::withTags(table( + class = 'display', + tags$thead( + tags$tr( + lapply(c('geneid', 'significance'), function(x) tags$th(rowspan=2, x)), + lapply(all_comps, + function(x) tags$th(class='dt-center', colspan=2, x)) + ), + tags$tr( + lapply(rep(c('log2FoldChange', 'padj'), 2), tags$th) ) - )) - - df %>% - datatable(rownames=FALSE, - selection='none', - container=sketch, - options=list(autoWidth=TRUE, - columnDefs=list(list(className='dt-center', - targets=seq_len((ncol(df)-1)))))) %>% - formatStyle(columns=border_cols, - 'border-right'='solid 1px') %>% - formatSignif(columns=which_cols, digits=5) - } # if show_table == 'yes' + ) + )) + + df %>% + datatable(rownames=FALSE, + selection=list(mode='multiple'), + container=sketch, + options=list(autoWidth=TRUE, + columnDefs=list(list(className='dt-center', + targets=seq_len((ncol(df)-1)))))) %>% + formatStyle(columns=border_cols, + 'border-right'='solid 1px') %>% + formatSignif(columns=which_cols, digits=5) }) # renderDT # ------------------------------------------------------- # + # table selection handling + + scatter_proxy <- dataTableProxy('scatter_tbl') + + observeEvent(input$reset_tbl, { + scatter_proxy %>% selectRows(NULL) + }) + + observeEvent(input$add_selected, { + tbl <- scatter_dt() + sel <- input$scatter_tbl_rows_selected + + # handle NAs in symbol + s <- tbl$geneid + #s[is.na(s)] <- tbl$gene[is.na(s)] + + if(is.null(sel)){ + showNotification( + 'Cannot add genes, no rows selected', type='warning' + ) + validate( + need(!is.null(sel), '') + ) + } else if(all(s[sel] %in% genes_clicked$g)){ + showNotification( + 'Selected genes already present in scratchpad, skipping', type='warning' + ) + } else { + if(is.null(genes_clicked$g)) selected <- s[sel] + else selected <- unique(c(genes_clicked$g, s[sel])) + + new_genes <- setdiff(selected, genes_clicked$g) + showNotification( + paste('Adding', length(new_genes), + 'new genes to scratchpad') + ) + + genes_clicked$g <- c(genes_clicked$g, new_genes) + } + }) # observeEvent + # ---------------- Help and download buttons ------------ # helpButtonServer('de_cmp_scatter_help', size='l') helpButtonServer('de_scatter_help', size='l') downloadButtonServer('scatterplot_download', scatterplot, 'scatterplot') # ----------------------------------------------------- # + + return( + reactive({ + list(genes=genes_clicked$g) + }) + ) } # Server function ) # moduleServer } # maPlotServer diff --git a/README.md b/README.md index 374c4b4..c5d2452 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,19 @@ # Carnation +[![](https://bioconductor.org/shields/availability/devel/carnation.svg)](https://bioconductor.org/packages/devel/bioc/html/carnation.html#archives) +[![](https://bioconductor.org/shields/lastcommit/devel/bioc/carnation.svg)](https://bioconductor.org/checkResults/devel/bioc-LATEST/carnation/) +[![](https://bioconductor.org/shields/build/devel/bioc/carnation.svg)](https://bioconductor.org/checkResults/devel/bioc-LATEST/carnation/) +[![](https://bioconductor.org/shields/years-in-bioc/carnation.svg)](https://bioconductor.org/packages/devel/bioc/html/carnation.html#since) + **Deeply explore your bulk RNA-Seq data with interactive visualizations** Carnation is an interactive Shiny dashboard that transforms complex bulk RNA-Seq data into beautiful, insightful visualizations. Designed for both computational and experimental biologists, Carnation makes exploring differential expression analysis, functional enrichment, and pattern analysis intuitive and exciting. +**Carnation is now on Bioconductor devel (Official release: April 2026)** + +Check out the official bioconductor page [here](https://bioconductor.org/packages/devel/bioc/html/carnation.html) +for more details. + ## ✨ Key Features - **Interactive Visualizations**: Explore your data through multiple perspectives @@ -23,6 +33,24 @@ Carnation is an interactive Shiny dashboard that transforms complex bulk RNA-Seq ## 🚀 Installation +Carnation can be installed using `BiocManager::install`. First, start R (version: 4.6) +and then run: + +```r +# first check to see if BiocManager is available +if(!requireNamespace('BiocManager', quietly=TRUE)){ + install.packages('BiocManager') +} + +BiocManager::install('carnation') +``` + +To install the 'devel' version + +```r +BiocManager::install('carnation', version='devel') +``` + ### remotes You can install the developmental version of carnation from github using the `remotes` @@ -46,11 +74,16 @@ conda activate ./env R ``` -Then install the package with the `remotes` package. Note, here we set `upgrade='never'` -to make sure the conda-installed package versions remain unchanged. +Then install the package with the `remotes` package. + +Note: + +- Conda packages for R >= 4.6.0 may not be available yet causing installation using the default + github branch to fail. To avoid this, use branch `r4.3` which pins R to a lower version. +- Here we set `upgrade='never'` to make sure the conda-installed package versions remain unchanged. ```r -remotes::install_github('NICHD-BSPC/carnation', upgrade='never') +remotes::install_github('NICHD-BSPC/carnation@r4.3', upgrade='never') ``` ## 🏁 Getting Started @@ -124,26 +157,3 @@ We welcome contributions to Carnation! Please feel free to submit issues or pull Carnation is available under the MIT license. -## 💻 Server Mode - -Carnation supports multi-user environments with authentication: - -```r -# Create user database -credentials <- data.frame( - user = c('shinymanager'), - password = c('12345'), - admin = c(TRUE), - stringsAsFactors = FALSE -) - -# Initialize the database -shinymanager::create_db( - credentials_data = credentials, - sqlite_path = 'credentials.sqlite', - passphrase = 'admin_passphrase' -) - -# Run with authentication -run_carnation(credentials='credentials.sqlite', passphrase='admin_passphrase') -``` diff --git a/inst/extdata/config.yaml b/inst/extdata/config.yaml index 545df0a..6cdffeb 100644 --- a/inst/extdata/config.yaml +++ b/inst/extdata/config.yaml @@ -37,6 +37,16 @@ style: margin-bottom: -5px } + .div-stats-card { + border-radius: 8px; + box-shadow: 3px 4px 5px 0px rgba(0, 0, 0, 0.3); + margin-bottom: 10px; + padding-left: 10px; + padding-top: 10px; + padding-bottom: 10px; + background-color: #ebecf0; + } + .rank-list-container.custom-sortable { background-color: #d0d0d0; width: fit-content; @@ -202,6 +212,25 @@ server: - sizeFactor de_analysis: + column_names: + # here we list alternative names of columns + # to support multiple tools + pvalue: + - pvalue #DESeq2 + - PValue # edgeR + - P.Value # limma + padj: + - padj # DESeq2 + - FDR # edgeR + - adj.P.Val # limma + log2FoldChange: + - log2FoldChange # DESeq2 + - logFC # edgeR/limma + baseMean: + - baseMean # DESeq2 + - logCPM # edgeR + - AveExpr # limma + pdf: width: 10 height: 10 diff --git a/man/carnation-package.Rd b/man/carnation-package.Rd index 832c6d8..58f4818 100644 --- a/man/carnation-package.Rd +++ b/man/carnation-package.Rd @@ -36,6 +36,7 @@ Useful links: Other contributors: \itemize{ + \item Matthew Tyler Menold \email{matthew.menold@gmail.com} (\href{https://orcid.org/0009-0007-4728-2470}{ORCID}) [contributor] \item Ryan Dale \email{ryan.dale@nih.gov} (\href{https://orcid.org/0000-0003-2664-3744}{ORCID}) [funder] } diff --git a/man/is_valid_pattern_obj.Rd b/man/is_valid_pattern_obj.Rd new file mode 100644 index 0000000..83c8ec3 --- /dev/null +++ b/man/is_valid_pattern_obj.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/functions.R +\name{is_valid_pattern_obj} +\alias{is_valid_pattern_obj} +\title{Validate Pattern Analysis Object Schema} +\usage{ +is_valid_pattern_obj(pattern_obj, require_symbol = FALSE) +} +\arguments{ +\item{pattern_obj}{A single pattern analysis element. Must be either a +\code{data.frame} or a list containing a \code{normalized} \code{data.frame}.} + +\item{require_symbol}{Logical, if \code{TRUE} require a \code{symbol} column in the +analysis table.} +} +\value{ +Returns \code{TRUE} when validation succeeds, otherwise returns \code{FALSE} +after emitting a message describing the issue. +} +\description{ +Validate the schema for a single \code{degpatterns} analysis element used by the +pattern analysis module. +} +\examples{ +data(degpatterns_dex, package = "carnation") + +is_valid_pattern_obj(degpatterns_dex) + +} diff --git a/man/plotScatter.label_ly.Rd b/man/plotScatter.label_ly.Rd index c51898f..07fb5b6 100644 --- a/man/plotScatter.label_ly.Rd +++ b/man/plotScatter.label_ly.Rd @@ -17,7 +17,8 @@ plotScatter.label_ly( lines = c("yes", "yes", "yes"), alpha = 1, size = 4, - show.grid = "yes" + show.grid = "yes", + source = "A" ) } \arguments{ @@ -49,6 +50,8 @@ x = 0 & y = 0, but not the x = y diagonal.} \item{size}{float, marker size (default=4).} \item{show.grid}{string, can be 'yes' (default) or 'no'.} + +\item{source}{name of source to return event_data from} } \value{ plotly handle diff --git a/man/scattermod.Rd b/man/scattermod.Rd index 202ec5d..05a785a 100644 --- a/man/scattermod.Rd +++ b/man/scattermod.Rd @@ -8,7 +8,7 @@ \usage{ scatterPlotUI(id, panel) -scatterPlotServer(id, obj, plot_args, config) +scatterPlotServer(id, obj, plot_args, gene_scratchpad, reset_genes, config) } \arguments{ \item{id}{Module id} @@ -17,8 +17,11 @@ scatterPlotServer(id, obj, plot_args, config) \item{obj}{reactiveValues object containing carnation object passed to server} -\item{plot_args}{reactive containing 'fdr.thres' (padj threshold), 'fc.thres' (log2FC) & -'gene.to.plot' (genes to be labeled) passed to server} +\item{plot_args}{reactive containing 'fdr.thres' (padj threshold), 'fc.thres' (log2FC)} + +\item{gene_scratchpad}{reactive containing gene scratchpad genes} + +\item{reset_genes}{reactive to reset gene scratchpad selection} \item{config}{reactive list with config settings passed to server} } @@ -48,11 +51,13 @@ obj <- reactiveValues( plot_args <- reactive({ list( fdr.thres=0.1, - fc.thres=0, - gene.to.plot=c('gene1', 'gene2') + fc.thres=0 ) }) +gene_scratchpad <- reactive({ c('gene1', 'gene2') }) +reset_genes <- reactiveVal() + config <- reactiveVal(get_config()) shinyApp( @@ -61,7 +66,8 @@ shinyApp( mainPanel(scatterPlotUI('p', 'sidebar')) ), server = function(input, output, session){ - scatterPlotServer('p', obj, plot_args, config) + scatter_data <- scatterPlotServer('p', obj, plot_args, + gene_scratchpad, reset_genes, config) } ) \dontshow{\}) # examplesIf} diff --git a/tests/testthat/test-scatter-plot-module.R b/tests/testthat/test-scatter-plot-module.R index b7ce10c..582991f 100644 --- a/tests/testthat/test-scatter-plot-module.R +++ b/tests/testthat/test-scatter-plot-module.R @@ -32,12 +32,16 @@ test_that("scatterPlotServer processes data correctly with log2FoldChange", { ) }) + gene_scratchpad <- reactive({ c('gene1', 'gene2') }) + reset_genes <- reactiveVal() config <- reactiveVal(get_config()) testServer(scatterPlotServer, args = list( id = "test_scatter", obj = obj, plot_args = plot_args, + gene_scratchpad = gene_scratchpad, + reset_genes = reset_genes, config = config ), { # Set inputs for scatter plot @@ -109,12 +113,16 @@ test_that("scatterPlotServer handles comparison swapping", { ) }) + gene_scratchpad <- reactive({ c('gene1', 'gene2') }) + reset_genes <- reactiveVal() config <- reactiveVal(get_config()) testServer(scatterPlotServer, args = list( id = "test_scatter_swap", obj = obj, plot_args = plot_args, + gene_scratchpad = gene_scratchpad, + reset_genes = reset_genes, config = config ), { # Set initial inputs @@ -174,12 +182,16 @@ test_that("scatterPlotServer handles autoscaling", { ) }) + gene_scratchpad <- reactive({ c('gene1', 'gene2') }) + reset_genes <- reactiveVal() config <- reactiveVal(get_config()) testServer(scatterPlotServer, args = list( id = "test_scatter_autoscale", obj = obj, plot_args = plot_args, + gene_scratchpad = gene_scratchpad, + reset_genes = reset_genes, config = config ), { # Set inputs @@ -245,12 +257,16 @@ test_that("scatterPlotServer handles gene labeling", { ) }) + gene_scratchpad <- reactive({ c('gene1', 'gene2') }) + reset_genes <- reactiveVal() config <- reactiveVal(get_config()) testServer(scatterPlotServer, args = list( id = "test_scatter_labels", obj = obj, plot_args = plot_args, + gene_scratchpad = gene_scratchpad, + reset_genes = reset_genes, config = config ), { # Set inputs