diff --git a/.codeocean/app-panel.json b/.codeocean/app-panel.json index 75e7580..0aadcef 100644 --- a/.codeocean/app-panel.json +++ b/.codeocean/app-panel.json @@ -14,7 +14,8 @@ "categories": [ { "id": "EzTg1ivlFDHEy9PI", - "name": "Basic", + "name": "DEG Filtering & Contrasts", + "description": "Select DEG columns, strict thresholds, and contrasts included in the plot", "icon": "📂" }, { @@ -44,21 +45,51 @@ ], "parameters": [ { - "id": "contrasts_colname_id", + "id": "signif_colname_id", "category": "EzTg1ivlFDHEy9PI", - "name": "Contrasts column name", - "param_name": "contrasts_colname", - "description": "Name of the column in the summarized differential expression data that contains the contrast names.", + "name": "Significance column name", + "param_name": "signif_colname", + "description": "Significance column in each DEG contrast result. Default is adjpval.", "type": "text", "value_type": "string", - "default_value": "Contrast" + "default_value": "adjpval" + }, + { + "id": "signif_threshold_id", + "category": "EzTg1ivlFDHEy9PI", + "name": "Significance threshold", + "param_name": "signif_threshold", + "description": "Numeric significance threshold. Features are included when their p-value or adjusted p-value is strictly less than this cutoff.", + "type": "text", + "value_type": "string", + "default_value": "0.05" + }, + { + "id": "change_colname_id", + "category": "EzTg1ivlFDHEy9PI", + "name": "Fold change column name", + "param_name": "change_colname", + "description": "Log fold-change column in each DEG contrast result. Default is logFC.", + "type": "text", + "value_type": "string", + "default_value": "logFC" + }, + { + "id": "change_threshold_id", + "category": "EzTg1ivlFDHEy9PI", + "name": "Fold change threshold", + "param_name": "change_threshold", + "description": "Numeric absolute log fold-change threshold. Features are included when their absolute log fold-change is strictly greater than this cutoff.", + "type": "text", + "value_type": "string", + "default_value": "1.0" }, { "id": "select_contrasts_id", "category": "EzTg1ivlFDHEy9PI", "name": "Select contrasts", "param_name": "select_contrasts", - "description": "A vector of contrast names to select for the plot.", + "description": "Comma-separated contrast names to include. Leave blank to use all DEG contrasts available in the multiOmicDataSet.", "type": "text", "value_type": "string" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c64702..bc4efbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ ## Development version -- Use MOSuite v0.4.0. +- Build Venn and Intersection plots directly from DEG results, with configurable strict thresholds and optional comma-separated contrast selection. (#10, @TJoshMeyer) - Sync Code Ocean app panel parameter descriptions with MOSuite package documentation for MOSuite v0.4.1 (#6, @phoman14). - Improved the Code Ocean parameter UI for the plot venn diagram capsule. (#2, @phoman14) +- Use MOSuite v0.4.2. ## v2.0 diff --git a/code/MOSuite/R/data.R b/code/MOSuite/R/data.R index 23f2561..a420dd4 100644 --- a/code/MOSuite/R/data.R +++ b/code/MOSuite/R/data.R @@ -56,15 +56,6 @@ #' @keywords data "nidap_deg_gene_list" -#' Summarized differential expression analysis for input to venn diagram -#' @keywords data -"nidap_volcano_summary_dat" - -#' Output data from venn diagram. -#' The result of running `plot_venn_diagram()` on `nidap_volcano_summary_dat` -#' @keywords data -"nidap_venn_diagram_dat" - #' Default MOSuite color palette #' #' A character vector of 12 hex color codes used as the default palette throughout MOSuite plots. diff --git a/code/MOSuite/R/plot_venn_diagram.R b/code/MOSuite/R/plot_venn_diagram.R index 3a176fe..37b540c 100644 --- a/code/MOSuite/R/plot_venn_diagram.R +++ b/code/MOSuite/R/plot_venn_diagram.R @@ -4,9 +4,18 @@ #' tested contrasts). This Venn diagram is available for up to five sets; Intersection plot is available for any number #' of sets. Specific sets can be selected for the visualizations and the returned dataset may include all (default) or #' specified intersections. -#' An S7 generic with methods for `multiOmicDataSet` and `data.frame`. +#' An S7 generic for `multiOmicDataSet` inputs. #' -#' @param moo_diff_summary_dat multiOmicDataSet or summarized differential expression analysis data frame. +#' @param moo_diff_summary_dat multiOmicDataSet containing per-contrast differential expression results. +#' @param feature_id_colname Feature identifier column in each contrast result. Defaults to the first column. +#' @param signif_colname Significance column in each contrast result. Default: `"adjpval"`. +#' @param signif_threshold Features pass the significance threshold when their value is less than this cutoff +#' (exclusive). Default: `0.05`. +#' @param change_colname Log fold-change column in each contrast result. Default: `"logFC"`. +#' @param change_threshold Features pass the fold-change threshold when their absolute value is greater than this +#' cutoff (exclusive). Default: `1.0`. +#' @param select_contrasts Contrast names to include. If empty, all available contrasts are used. +#' @inheritParams plot_venn_sets #' #' @export plot_venn_diagram <- S7::new_generic( @@ -15,7 +24,10 @@ plot_venn_diagram <- S7::new_generic( function( moo_diff_summary_dat, feature_id_colname = NULL, - contrasts_colname = "Contrast", + signif_colname = "adjpval", + signif_threshold = 0.05, + change_colname = "logFC", + change_threshold = 1.0, select_contrasts = c(), plot_type = "Venn diagram", intersection_ids = c(), @@ -60,7 +72,10 @@ plot_venn_diagram <- S7::new_generic( S7::method(plot_venn_diagram, multiOmicDataSet) <- function( moo_diff_summary_dat, feature_id_colname = NULL, - contrasts_colname = "Contrast", + signif_colname = "adjpval", + signif_threshold = 0.05, + change_colname = "logFC", + change_threshold = 1.0, select_contrasts = c(), plot_type = "Venn diagram", intersection_ids = c(), @@ -97,55 +112,80 @@ S7::method(plot_venn_diagram, multiOmicDataSet) <- function( save_plots = options::opt("save_plots"), plots_subdir = "diff" ) { + diff_results <- moo_diff_summary_dat@analyses$diff + if (!is.list(diff_results) || length(diff_results) == 0) { + stop( + "No per-contrast differential expression results found in moo@analyses$diff" + ) + } + if (length(select_contrasts) > 0) { + missing_contrasts <- setdiff(select_contrasts, names(diff_results)) + if (length(missing_contrasts) > 0) { + stop(glue::glue( + "Selected contrasts not found: {paste(missing_contrasts, collapse = ', ')}" + )) + } + diff_results <- diff_results[select_contrasts] + } + if (is.null(feature_id_colname)) { + feature_id_colname <- names(diff_results[[1]])[1] + } + + venn_sets <- lapply(diff_results, function(diff_result) { + required_columns <- c(feature_id_colname, signif_colname, change_colname) + missing_columns <- setdiff(required_columns, names(diff_result)) + if (length(missing_columns) > 0) { + stop(glue::glue( + "Required DEG columns not found: {paste(missing_columns, collapse = ', ')}" + )) + } + is_significant <- !is.na(diff_result[[signif_colname]]) & + diff_result[[signif_colname]] < signif_threshold + has_change <- !is.na(diff_result[[change_colname]]) & + abs(diff_result[[change_colname]]) > change_threshold + return(unique(diff_result[[feature_id_colname]][ + is_significant & has_change + ])) + }) + return( - moo_diff_summary_dat@analyses$diff |> - join_dfs_wide() |> - plot_volcano_summary(print_plots = FALSE, save_plots = FALSE) |> - plot_venn_diagram( - feature_id_colname, - contrasts_colname, - select_contrasts, - plot_type, - intersection_ids, - venn_force_unique, - venn_numbers_format, - venn_significant_digits, - venn_fill_colors, - venn_fill_transparency, - venn_border_colors, - venn_font_size_for_category_names, - venn_category_names_distance, - venn_category_names_position, - venn_font_size_for_counts, - venn_outer_margin, - intersections_order, - display_empty_intersections, - intersection_bar_color, - intersection_point_size, - intersection_line_width, - table_font_size, - table_content, - graphics_device, - dpi, - image_width, - image_height, - plot_filename, - print_plots, - save_plots, - plots_subdir, - ) + plot_venn_sets( + venn_sets = venn_sets, + plot_type = plot_type, + intersection_ids = intersection_ids, + venn_force_unique = venn_force_unique, + venn_numbers_format = venn_numbers_format, + venn_significant_digits = venn_significant_digits, + venn_fill_colors = venn_fill_colors, + venn_fill_transparency = venn_fill_transparency, + venn_border_colors = venn_border_colors, + venn_font_size_for_category_names = venn_font_size_for_category_names, + venn_category_names_distance = venn_category_names_distance, + venn_category_names_position = venn_category_names_position, + venn_font_size_for_counts = venn_font_size_for_counts, + venn_outer_margin = venn_outer_margin, + intersections_order = intersections_order, + display_empty_intersections = display_empty_intersections, + intersection_bar_color = intersection_bar_color, + intersection_point_size = intersection_point_size, + intersection_line_width = intersection_line_width, + table_font_size = table_font_size, + table_content = table_content, + graphics_device = graphics_device, + dpi = dpi, + image_width = image_width, + image_height = image_height, + plot_filename = plot_filename, + print_plots = print_plots, + save_plots = save_plots, + plots_subdir = plots_subdir + ) ) } -#' @inheritParams option_params -#' @inheritParams filter_counts -#' @inheritParams plot_volcano_enhanced -#' @inheritParams plot_volcano_summary +#' Build Venn or UpSet intersections from feature sets #' -#' @param moo_diff_summary_dat Summarized differential expression analysis -#' @param contrasts_colname Name of the column in `moo_diff_summary_dat` that contains the contrast names (default: -#' "Contrast") -#' @param select_contrasts A vector of contrast names to select for the plot. If empty, all contrasts are used. +#' @param venn_sets Named list of feature identifier vectors, one per contrast. #' @param plot_type Type of plot to generate: "Venn diagram" or "Intersection plot". Default: "Venn diagram" #' @param intersection_ids A vector of intersection IDs to select for the plot. If empty, all intersections are used. #' @param venn_force_unique If TRUE, forces unique elements in the Venn diagram. Default: TRUE @@ -170,17 +210,10 @@ S7::method(plot_venn_diagram, multiOmicDataSet) <- function( #' @param table_font_size Font size for the table in the plot. Default: 3 #' @param table_content Content of the table in the plot. Default: NULL #' -#' @keywords plotters -#' -#' @examples -#' plot_venn_diagram(nidap_volcano_summary_dat, print_plots = TRUE) +#' @keywords internal #' -#' @rdname plot_venn_diagram -S7::method(plot_venn_diagram, S7::class_data.frame) <- function( - moo_diff_summary_dat, - feature_id_colname = NULL, - contrasts_colname = "Contrast", - select_contrasts = c(), +plot_venn_sets <- function( + venn_sets, plot_type = "Venn diagram", intersection_ids = c(), venn_force_unique = TRUE, @@ -224,34 +257,12 @@ S7::method(plot_venn_diagram, S7::class_data.frame) <- function( "UpSetR" )) - if (nrow(moo_diff_summary_dat) == 0) { - stop("Dataframe is empty") - } - - ### PH: - # Input - DEG table from Volcano Summary, I think we need to make this function more generic. - # The input should be the Limma DEG table and maybe be used with the DEG Gene List Template - # Output - Venn Diagram Figure + Venn table - # Purpose - compare DEGS from different Comparisons - - input_dataset <- as.data.frame(moo_diff_summary_dat) - if (is.null(feature_id_colname)) { - feature_id_colname <- colnames(moo_diff_summary_dat)[1] - } - - ### PH: Create venn Table from DEG table - - # SET INPUT ==== - - # select required columns - set_elements <- input_dataset[, feature_id_colname] - set_names <- input_dataset[, contrasts_colname] - - # prepare format - R list - vlist <- split(set_elements, set_names) - if (!is.null(select_contrasts)) { - vlist <- vlist[select_contrasts] + if ( + !is.list(venn_sets) || length(venn_sets) == 0 || is.null(names(venn_sets)) + ) { + stop("venn_sets must be a non-empty named list") } + vlist <- venn_sets num_categories <- length(vlist) if (num_categories == 0) { stop("Zero categories found") @@ -279,11 +290,7 @@ S7::method(plot_venn_diagram, S7::class_data.frame) <- function( if (num_categories > 1) { sets <- fromList(vlist) - if (!is.null(select_contrasts)) { - Intersection <- sets[, match(select_contrasts, colnames(sets))] - } else { - Intersection <- sets - } + Intersection <- sets # generate intersection frequency table and gene list (all intersections for the output dataset/table not the plot) intersection_matrix <- Intersection @@ -334,7 +341,7 @@ S7::method(plot_venn_diagram, S7::class_data.frame) <- function( # returned intersections - if (!is.null(intersection_ids)) { + if (length(intersection_ids) > 0) { intersection_ids <- sort(as.numeric(intersection_ids)) tabsel <- tab[tab$Id %in% intersection_ids, ] Intersectionsel <- Intersection[Intersection$Id %in% intersection_ids, ] @@ -397,7 +404,7 @@ S7::method(plot_venn_diagram, S7::class_data.frame) <- function( pSet <- UpSetR::upset( sets, nsets = num_categories, - sets = select_contrasts, + sets = names(vlist), order.by = intersections_order, nintersects = NA, text.scale = 2, diff --git a/code/MOSuite/data-raw/nidap.R b/code/MOSuite/data-raw/nidap.R index b5a96a7..319b617 100644 --- a/code/MOSuite/data-raw/nidap.R +++ b/code/MOSuite/data-raw/nidap.R @@ -118,21 +118,5 @@ usethis::use_data(nidap_deg_analysis_2, overwrite = TRUE) nidap_deg_gene_list <- moo@analyses$diff_filt usethis::use_data(nidap_deg_gene_list, overwrite = TRUE) -nidap_volcano_summary_dat <- readr::read_csv(system.file( - "extdata", - "nidap", - "Volcano_Summary.csv.gz", - package = "MOSuite" -)) -usethis::use_data(nidap_volcano_summary_dat, overwrite = TRUE) - -nidap_venn_diagram_dat <- readr::read_csv(system.file( - "extdata", - "nidap", - "Venn_Diagram.csv.gz", - package = "MOSuite" -)) -usethis::use_data(nidap_venn_diagram_dat, overwrite = TRUE) - ## re-compress Rda files # tools::resaveRdaFiles('data') diff --git a/code/MOSuite/data/nidap_venn_diagram_dat.rda b/code/MOSuite/data/nidap_venn_diagram_dat.rda deleted file mode 100644 index 9c01b50..0000000 Binary files a/code/MOSuite/data/nidap_venn_diagram_dat.rda and /dev/null differ diff --git a/code/MOSuite/data/nidap_volcano_summary_dat.rda b/code/MOSuite/data/nidap_volcano_summary_dat.rda deleted file mode 100644 index 4c3cd28..0000000 Binary files a/code/MOSuite/data/nidap_volcano_summary_dat.rda and /dev/null differ diff --git a/code/MOSuite/inst/extdata/nidap/Venn_Diagram.csv.gz b/code/MOSuite/inst/extdata/nidap/Venn_Diagram.csv.gz deleted file mode 100644 index c14dc31..0000000 Binary files a/code/MOSuite/inst/extdata/nidap/Venn_Diagram.csv.gz and /dev/null differ diff --git a/code/MOSuite/inst/extdata/nidap/Volcano_Summary.csv.gz b/code/MOSuite/inst/extdata/nidap/Volcano_Summary.csv.gz deleted file mode 100644 index fb3ec17..0000000 Binary files a/code/MOSuite/inst/extdata/nidap/Volcano_Summary.csv.gz and /dev/null differ diff --git a/code/MOSuite/inst/quarto/report.qmd b/code/MOSuite/inst/quarto/report.qmd index d5090ee..fbee239 100644 --- a/code/MOSuite/inst/quarto/report.qmd +++ b/code/MOSuite/inst/quarto/report.qmd @@ -160,6 +160,6 @@ dat_volcano_enhanced <- moo@analyses$diff |> ### Venn Diagram ```{r venn_diagram} -venn_dat <- dat_volcano_summary |> plot_venn_diagram() +venn_dat <- moo |> plot_venn_diagram() head(venn_dat) ``` diff --git a/code/MOSuite/man/nidap_venn_diagram_dat.Rd b/code/MOSuite/man/nidap_venn_diagram_dat.Rd deleted file mode 100644 index 6ab32bb..0000000 --- a/code/MOSuite/man/nidap_venn_diagram_dat.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/data.R -\docType{data} -\name{nidap_venn_diagram_dat} -\alias{nidap_venn_diagram_dat} -\title{Output data from venn diagram. -The result of running \code{plot_venn_diagram()} on \code{nidap_volcano_summary_dat}} -\format{ -An object of class \code{spec_tbl_df} (inherits from \code{tbl_df}, \code{tbl}, \code{data.frame}) with 3068 rows and 4 columns. -} -\usage{ -nidap_venn_diagram_dat -} -\description{ -Output data from venn diagram. -The result of running \code{plot_venn_diagram()} on \code{nidap_volcano_summary_dat} -} -\keyword{data} diff --git a/code/MOSuite/man/nidap_volcano_summary_dat.Rd b/code/MOSuite/man/nidap_volcano_summary_dat.Rd deleted file mode 100644 index 43bdf38..0000000 --- a/code/MOSuite/man/nidap_volcano_summary_dat.Rd +++ /dev/null @@ -1,16 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/data.R -\docType{data} -\name{nidap_volcano_summary_dat} -\alias{nidap_volcano_summary_dat} -\title{Summarized differential expression analysis for input to venn diagram} -\format{ -An object of class \code{spec_tbl_df} (inherits from \code{tbl_df}, \code{tbl}, \code{data.frame}) with 4929 rows and 7 columns. -} -\usage{ -nidap_volcano_summary_dat -} -\description{ -Summarized differential expression analysis for input to venn diagram -} -\keyword{data} diff --git a/code/MOSuite/man/plot_venn_diagram.Rd b/code/MOSuite/man/plot_venn_diagram.Rd index 80ab701..19b80d2 100644 --- a/code/MOSuite/man/plot_venn_diagram.Rd +++ b/code/MOSuite/man/plot_venn_diagram.Rd @@ -3,13 +3,15 @@ \name{plot_venn_diagram} \alias{plot_venn_diagram} \alias{plot_venn_diagram,MOSuite::multiOmicDataSet-method} -\alias{plot_venn_diagram,data.frame-method} \title{Plot a venn diagram, UpSet plot, or table of intersections} \usage{ plot_venn_diagram( moo_diff_summary_dat, feature_id_colname = NULL, - contrasts_colname = "Contrast", + signif_colname = "adjpval", + signif_threshold = 0.05, + change_colname = "logFC", + change_threshold = 1, select_contrasts = c(), plot_type = "Venn diagram", intersection_ids = c(), @@ -46,44 +48,10 @@ plot_venn_diagram( plot_venn_diagram( moo_diff_summary_dat, feature_id_colname = NULL, - contrasts_colname = "Contrast", - select_contrasts = c(), - plot_type = "Venn diagram", - intersection_ids = c(), - venn_force_unique = TRUE, - venn_numbers_format = "raw", - venn_significant_digits = 2, - venn_fill_colors = c("darkgoldenrod2", "darkolivegreen2", "mediumpurple3", - "darkorange2", "lightgreen"), - venn_fill_transparency = 0.2, - venn_border_colors = "fill colors", - venn_font_size_for_category_names = 3, - venn_category_names_distance = c(), - venn_category_names_position = c(), - venn_font_size_for_counts = 6, - venn_outer_margin = 0, - intersections_order = "degree", - display_empty_intersections = FALSE, - intersection_bar_color = "steelblue4", - intersection_point_size = 2.2, - intersection_line_width = 0.7, - table_font_size = 0.7, - table_content = "all intersections", - graphics_device = grDevices::png, - dpi = 300, - image_width = 4000, - image_height = 3000, - plot_filename = "venn_diagram.png", - print_plots = options::opt("print_plots"), - save_plots = options::opt("save_plots"), - plots_subdir = "diff" -) - -## S7 method for class -plot_venn_diagram( - moo_diff_summary_dat, - feature_id_colname = NULL, - contrasts_colname = "Contrast", + signif_colname = "adjpval", + signif_threshold = 0.05, + change_colname = "logFC", + change_threshold = 1, select_contrasts = c(), plot_type = "Venn diagram", intersection_ids = c(), @@ -117,17 +85,21 @@ plot_venn_diagram( ) } \arguments{ -\item{moo_diff_summary_dat}{Summarized differential expression analysis} +\item{moo_diff_summary_dat}{multiOmicDataSet containing per-contrast differential expression results.} + +\item{feature_id_colname}{Feature identifier column in each contrast result. Defaults to the first column.} -\item{feature_id_colname}{The column from the counts data containing the Feature IDs (Usually Gene or Protein ID). -This is usually the first column of your input Counts Matrix. Only columns of Text type from your input Counts -Matrix will be available to select for this parameter. (Default: \code{NULL} - first column in the counts matrix will be -used.)} +\item{signif_colname}{Significance column in each contrast result. Default: \code{"adjpval"}.} -\item{contrasts_colname}{Name of the column in \code{moo_diff_summary_dat} that contains the contrast names (default: -"Contrast")} +\item{signif_threshold}{Features pass the significance threshold when their value is less than this cutoff +(exclusive). Default: \code{0.05}.} -\item{select_contrasts}{A vector of contrast names to select for the plot. If empty, all contrasts are used.} +\item{change_colname}{Log fold-change column in each contrast result. Default: \code{"logFC"}.} + +\item{change_threshold}{Features pass the fold-change threshold when their absolute value is greater than this +cutoff (exclusive). Default: \code{1.0}.} + +\item{select_contrasts}{Contrast names to include. If empty, all available contrasts are used.} \item{plot_type}{Type of plot to generate: "Venn diagram" or "Intersection plot". Default: "Venn diagram"} @@ -171,33 +143,11 @@ same colors as \code{venn_fill_colors})} \item{table_font_size}{Font size for the table in the plot. Default: 3} \item{table_content}{Content of the table in the plot. Default: NULL} - -\item{graphics_device}{passed to \code{ggsave(device)}. Default: \code{grDevices::png}} - -\item{dpi}{dots-per-inch of the output image (see \code{ggsave()}) - only used if save_plots is TRUE} - -\item{image_width}{output image width in pixels - only used if save_plots is TRUE} - -\item{image_height}{output image height in pixels - only used if save_plots is TRUE} - -\item{plot_filename}{plot output filename - only used if save_plots is TRUE. When multiple comparisons are saved -separately, the comparison name is appended before the file extension.} - -\item{print_plots}{Whether to print plots during analysis (Defaults to \code{FALSE}, overwritable using option 'moo_print_plots' or environment variable 'MOO_PRINT_PLOTS')} - -\item{save_plots}{Whether to save plots to files during analysis (Defaults to \code{TRUE}, overwritable using option 'moo_save_plots' or environment variable 'MOO_SAVE_PLOTS')} - -\item{plots_subdir}{subdirectory in \verb{figures/} where plots will be saved if \code{save_plots} is \code{TRUE}} } \description{ Generates Venn diagram of intersections across a series of sets (e.g., intersections of significant genes across tested contrasts). This Venn diagram is available for up to five sets; Intersection plot is available for any number of sets. Specific sets can be selected for the visualizations and the returned dataset may include all (default) or specified intersections. -An S7 generic with methods for \code{multiOmicDataSet} and \code{data.frame}. -} -\examples{ -plot_venn_diagram(nidap_volcano_summary_dat, print_plots = TRUE) - +An S7 generic for \code{multiOmicDataSet} inputs. } -\keyword{plotters} diff --git a/code/MOSuite/man/plot_venn_sets.Rd b/code/MOSuite/man/plot_venn_sets.Rd new file mode 100644 index 0000000..f927ce3 --- /dev/null +++ b/code/MOSuite/man/plot_venn_sets.Rd @@ -0,0 +1,89 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot_venn_diagram.R +\name{plot_venn_sets} +\alias{plot_venn_sets} +\title{Build Venn or UpSet intersections from feature sets} +\usage{ +plot_venn_sets( + venn_sets, + plot_type = "Venn diagram", + intersection_ids = c(), + venn_force_unique = TRUE, + venn_numbers_format = "raw", + venn_significant_digits = 2, + venn_fill_colors = c("darkgoldenrod2", "darkolivegreen2", "mediumpurple3", + "darkorange2", "lightgreen"), + venn_fill_transparency = 0.2, + venn_border_colors = "fill colors", + venn_font_size_for_category_names = 3, + venn_category_names_distance = c(), + venn_category_names_position = c(), + venn_font_size_for_counts = 6, + venn_outer_margin = 0, + intersections_order = "degree", + display_empty_intersections = FALSE, + intersection_bar_color = "steelblue4", + intersection_point_size = 2.2, + intersection_line_width = 0.7, + table_font_size = 0.7, + table_content = "all intersections", + graphics_device = grDevices::png, + dpi = 300, + image_width = 4000, + image_height = 3000, + plot_filename = "venn_diagram.png", + print_plots = options::opt("print_plots"), + save_plots = options::opt("save_plots"), + plots_subdir = "diff" +) +} +\arguments{ +\item{venn_sets}{Named list of feature identifier vectors, one per contrast.} + +\item{plot_type}{Type of plot to generate: "Venn diagram" or "Intersection plot". Default: "Venn diagram"} + +\item{intersection_ids}{A vector of intersection IDs to select for the plot. If empty, all intersections are used.} + +\item{venn_force_unique}{If TRUE, forces unique elements in the Venn diagram. Default: TRUE} + +\item{venn_numbers_format}{Format for the numbers in the Venn diagram. Options: "raw", "percent", "raw-percent", +"percent-raw". Default: "raw"} + +\item{venn_significant_digits}{Number of significant digits for the Venn diagram numbers. Default: 2} + +\item{venn_fill_colors}{A vector of colors to fill the Venn diagram categories. Default: c("darkgoldenrod2", +"darkolivegreen2", "mediumpurple3", "darkorange2", "lightgreen")} + +\item{venn_fill_transparency}{Transparency level for the Venn diagram fill colors. Default: 0.2} + +\item{venn_border_colors}{Colors for the borders of the Venn diagram categories. Default: "fill colors" (uses the +same colors as \code{venn_fill_colors})} + +\item{venn_font_size_for_category_names}{Font size for the category names in the Venn diagram. Default: 3} + +\item{venn_category_names_distance}{Distance of the category names from the Venn diagram circles. Default: c()} + +\item{venn_category_names_position}{Position of the category names in the Venn diagram. Default: c()} + +\item{venn_font_size_for_counts}{Font size for the counts in the Venn diagram. Default: 6} + +\item{venn_outer_margin}{Outer margin for the Venn diagram. Default: 0} + +\item{intersections_order}{Order of the intersections in the plot. Default: "by size"} + +\item{display_empty_intersections}{If TRUE, displays empty intersections in the plot. Default: FALSE} + +\item{intersection_bar_color}{Color for the intersection bars in the plot. Default: "lightgray"} + +\item{intersection_point_size}{Size of the points in the intersection plot. Default: 2} + +\item{intersection_line_width}{Width of the lines in the intersection plot. Default: 0.5} + +\item{table_font_size}{Font size for the table in the plot. Default: 3} + +\item{table_content}{Content of the table in the plot. Default: NULL} +} +\description{ +Build Venn or UpSet intersections from feature sets +} +\keyword{internal} diff --git a/code/MOSuite/tests/testthat/_snaps/plot_venn_diagram.md b/code/MOSuite/tests/testthat/_snaps/plot_venn_diagram.md deleted file mode 100644 index b6e1eb0..0000000 --- a/code/MOSuite/tests/testthat/_snaps/plot_venn_diagram.md +++ /dev/null @@ -1,9 +0,0 @@ -# plot_venn_diagram works with defaults - - Code - p <- plot_venn_diagram(nidap_volcano_summary_dat, print_plots = FALSE, - save_plots = TRUE) - Message - All intersections: 1:7,c(1, 2, 3, 4, 5, 6, 7),c(163, 237, 518, 780, 225, 379, 766),c("Yes", "Yes", "Yes", "Yes", "Yes", "Yes", "Yes") - Intersections returned: 1:7,c(1, 2, 3, 4, 5, 6, 7),c(163, 237, 518, 780, 225, 379, 766) - diff --git a/code/MOSuite/tests/testthat/test-plot_venn_diagram.R b/code/MOSuite/tests/testthat/test-plot_venn_diagram.R index 9c63715..0136cb9 100644 --- a/code/MOSuite/tests/testthat/test-plot_venn_diagram.R +++ b/code/MOSuite/tests/testthat/test-plot_venn_diagram.R @@ -1,56 +1,89 @@ -test_that("plot_venn_diagram works with defaults", { - expect_snapshot( - p <- plot_venn_diagram( - nidap_volcano_summary_dat, - print_plots = FALSE, - save_plots = TRUE +make_venn_test_moo <- function() { + contrast_ba <- data.frame( + Gene = c("both", "ba_only", "p_boundary", "fc_boundary"), + logFC = c(2, 2, 2, 1), + adjpval = c(0.01, 0.01, 0.05, 0.01) + ) + contrast_ca <- data.frame( + Gene = c("both", "ca_only", "p_boundary", "fc_boundary"), + logFC = c(-2, -2, -2, -1), + adjpval = c(0.01, 0.01, 0.05, 0.01) + ) + return( + multiOmicDataSet( + sample_metadata = data.frame(Sample = "S1"), + anno_dat = data.frame(), + counts_lst = list(raw = data.frame(Gene = "seed", S1 = 1)), + analyses_lst = list(diff = list("B-A" = contrast_ba, "C-A" = contrast_ca)) ) ) - expect_equal( - plot_venn_diagram(nidap_volcano_summary_dat), - as.data.frame(nidap_venn_diagram_dat) +} + +test_that("plot_venn_diagram builds intersections directly from DEG MOO results", { + result <- plot_venn_diagram( + make_venn_test_moo(), + print_plots = FALSE, + save_plots = FALSE ) + + expect_s3_class(result, "data.frame") + expect_setequal(result$Gene, c("both", "ba_only", "ca_only")) + expect_false(any(result$Gene %in% c("p_boundary", "fc_boundary"))) + expect_true("(B-A ∩ C-A)" %in% result$Intersection) }) -test_that("plot_venn_diagram raises condition for empty df", { - expect_error( - plot_venn_diagram(structure( - list( - GeneName = character(0), - Contrast = character(0), - FC = numeric(0), - logFC = numeric(0), - tstat = numeric(0), - pval = numeric(0), - adjpval = numeric(0) - ), - class = "data.frame", - row.names = integer(0) - )), - "Dataframe is empty" + +test_that("plot_venn_diagram respects a subset of MOO contrasts", { + moo <- make_venn_test_moo() + moo@analyses$diff[["D-A"]] <- data.frame( + Gene = c("both", "da_only"), + logFC = c(2, 2), + adjpval = c(0.01, 0.01) + ) + + result <- plot_venn_diagram( + moo, + select_contrasts = c("B-A", "C-A"), + print_plots = FALSE, + save_plots = FALSE ) + + expect_setequal(result$Gene, c("both", "ba_only", "ca_only")) + expect_false("da_only" %in% result$Gene) + expect_false(any(grepl("D-A", result$Intersection, fixed = TRUE))) }) -test_that("intersection matrix assignment avoids recursive evaluation error", { - # This test demonstrates the fix for the recursive default argument reference error - # The error occurred with this pattern: - # Intersection <- sapply(colnames(Intersection), function(x) Intersection[, x]) - # The fix uses a temporary variable: - # intersection_matrix <- Intersection; - # Intersection <- sapply(colnames(intersection_matrix), ...) - - # Call plot_venn_diagram directly to ensure the fix works in practice - expect_no_error({ - result <- plot_venn_diagram( - nidap_volcano_summary_dat, +test_that("plot_venn_diagram errors for missing contrast DEG columns", { + expect_error( + plot_venn_diagram( + make_venn_test_moo(), + signif_colname = "missing_column", print_plots = FALSE, save_plots = FALSE + ), + "Required DEG columns not found" + ) +}) + +test_that("plot_venn_diagram uses an Intersection plot for more than five contrasts", { + moo <- make_venn_test_moo() + moo@analyses$diff <- rep(moo@analyses$diff["B-A"], 6) + names(moo@analyses$diff) <- paste0("contrast_", seq_len(6)) + for (contrast_name in names(moo@analyses$diff)) { + moo@analyses$diff[[contrast_name]] <- data.frame( + Gene = c("both", contrast_name), + logFC = c(2, 2), + adjpval = c(0.01, 0.01) ) - }) + } - # Verify the result has the expected structure - expect_s3_class(result, "data.frame") - expect_true("Gene" %in% colnames(result)) - expect_true("Intersection" %in% colnames(result)) - expect_true("Id" %in% colnames(result)) - expect_true("Size" %in% colnames(result)) + expect_no_error( + suppressWarnings( + plot_venn_diagram( + moo, + plot_type = "Venn diagram", + print_plots = FALSE, + save_plots = FALSE + ) + ) + ) }) diff --git a/code/MOSuite/vignettes/visualization.Rmd b/code/MOSuite/vignettes/visualization.Rmd index ffa1073..9352f0f 100644 --- a/code/MOSuite/vignettes/visualization.Rmd +++ b/code/MOSuite/vignettes/visualization.Rmd @@ -153,7 +153,7 @@ head(dat_volcano_summary) ### Venn Diagram ```{r venn_diagram, fig.width=7, fig.height=7} -venn_dat <- dat_volcano_summary |> plot_venn_diagram() +venn_dat <- moo |> plot_venn_diagram() head(venn_dat) ``` diff --git a/code/main.R b/code/main.R index 50a627e..50d4095 100644 --- a/code/main.R +++ b/code/main.R @@ -7,6 +7,19 @@ library(stringr) library(dplyr) devtools::load_all('/code/MOSuite') +# parse an optional comma-separated character vector +parse_character_vector <- function(x) { + if (is.null(x) || length(x) == 0) { + return(NULL) + } + values <- trimws(unlist(strsplit(x, ",", fixed = TRUE))) + values <- unique(values[nzchar(values)]) + if (length(values) == 0) { + return(NULL) + } + return(values) +} + # parse comma-separated numeric vectors parse_numeric_vector <- function(x) { if (is.null(x) || identical(x, "") || length(x) == 0) { @@ -37,16 +50,34 @@ parser$add_argument( help = "Column name for feature IDs" ) parser$add_argument( - "--contrasts_colname", + "--signif_colname", type = "character", - default = "Contrast", - help = "Column name for contrast names" + default = "adjpval", + help = "Significance column in each contrast result" +) +parser$add_argument( + "--signif_threshold", + type = "double", + default = 0.05, + help = "Significance cutoff for p-values" +) +parser$add_argument( + "--change_colname", + type = "character", + default = "logFC", + help = "Log fold-change column in each contrast result" +) +parser$add_argument( + "--change_threshold", + type = "double", + default = 1.0, + help = "Absolute log fold-change cutoff" ) parser$add_argument( "--select_contrasts", type = "character", default = "", - help = "Comma-separated contrast names to select" + help = "Comma-separated contrast names; leave blank to use all contrasts" ) parser$add_argument( "--plot_type", @@ -199,20 +230,14 @@ args <- parser$parse_args() # load multiOmicDataSet from data directory moo <- load_moo_from_data_dir() -# First, generate volcano summary to get the differential expression summary data -# (This is required input for the venn diagram function) -summary_dat <- plot_volcano_summary( - moo, - print_plots = FALSE, - save_plots = FALSE -) - -# Now generate the venn diagram from the summary data venn_result <- plot_venn_diagram( - summary_dat, + moo, feature_id_colname = args$feature_id_colname, - contrasts_colname = args$contrasts_colname, - select_contrasts = parse_optional_vector(args$select_contrasts), + signif_colname = args$signif_colname, + signif_threshold = args$signif_threshold, + change_colname = args$change_colname, + change_threshold = args$change_threshold, + select_contrasts = parse_character_vector(args$select_contrasts), plot_type = args$plot_type, intersection_ids = parse_numeric_vector(args$intersection_ids), venn_force_unique = args$venn_force_unique, diff --git a/tests/testthat/helper-cli.R b/tests/testthat/helper-cli.R index b149641..1c9592a 100644 --- a/tests/testthat/helper-cli.R +++ b/tests/testthat/helper-cli.R @@ -79,9 +79,15 @@ expect_outputs_created <- function(results_dir) { } common_cli_args <- c( - "--contrasts_colname=Contrast", + "--signif_colname=adjpval", + "--signif_threshold=0.05", + "--change_colname=logFC", + "--change_threshold=1.0", "--venn_numbers_format=raw", "--intersections_order=degree", "--display_empty_intersections=FALSE", + "--image_width=400", + "--image_height=300", + "--dpi=72", "--plot_filename=venn_diagram.png" ) diff --git a/tests/testthat/test-main.R b/tests/testthat/test-main.R index f173ab1..47c0e7e 100644 --- a/tests/testthat/test-main.R +++ b/tests/testthat/test-main.R @@ -4,7 +4,10 @@ test_that("Venn diagram capsule keeps expected CLI parameter contract", { expected_args <- c( "feature_id_colname", - "contrasts_colname", + "signif_colname", + "signif_threshold", + "change_colname", + "change_threshold", "select_contrasts", "plot_type", "intersection_ids", @@ -33,11 +36,20 @@ test_that("Venn diagram capsule keeps expected CLI parameter contract", { ) expect_same_values(extract_main_arguments(main_lines), expected_args) - expect_match(main_text, "plot_volcano_summary\\(") + expect_same_values( + extract_panel_param_names(read_repo_file(".codeocean", "app-panel.json")), + expected_args + ) + expect_false(grepl("plot_volcano_summary\\(", main_text)) expect_match(main_text, "plot_venn_diagram\\(") expect_match( main_text, - "select_contrasts = parse_optional_vector\\(args\\$select_contrasts\\)" + "venn_result <- plot_venn_diagram\\(\\s*moo,", + perl = TRUE + ) + expect_match( + main_text, + "select_contrasts = parse_character_vector\\(args\\$select_contrasts\\)" ) expect_match( main_text, @@ -47,6 +59,22 @@ test_that("Venn diagram capsule keeps expected CLI parameter contract", { expect_match(main_text, "venn_diagram_data\\.csv") }) +test_that("character vector parser handles optional contrast subsets", { + main_lines <- read_repo_file("code", "main.R") + parser_start <- grep("^parse_character_vector <- function", main_lines) + parser_end <- grep("^# parse comma-separated numeric vectors", main_lines) - 1 + parser_lines <- main_lines[parser_start:parser_end] + eval(parse(text = paste(parser_lines, collapse = "\n"))) + + expect_null(parse_character_vector(NULL)) + expect_null(parse_character_vector("")) + expect_null(parse_character_vector(" , ")) + expect_equal( + parse_character_vector("B-A, C-A,B-A"), + c("B-A", "C-A") + ) +}) + test_that("numeric vector parser handles Venn optional numeric fields", { main_lines <- read_repo_file("code", "main.R") parser_start <- grep("^parse_numeric_vector <- function", main_lines) @@ -72,6 +100,88 @@ test_that("Code Ocean boolean controls are TRUE/FALSE lists", { ) }) +test_that("Code Ocean panel exposes direct DEG controls", { + panel_lines <- read_repo_file(".codeocean", "app-panel.json") + panel_parameters <- extract_panel_param_names(panel_lines) + + expect_false("contrasts_colname" %in% panel_parameters) + expect_equal(extract_panel_default(panel_lines, "signif_colname"), "adjpval") + expect_equal(extract_panel_default(panel_lines, "signif_threshold"), "0.05") + expect_equal(extract_panel_default(panel_lines, "change_colname"), "logFC") + expect_equal(extract_panel_default(panel_lines, "change_threshold"), "1.0") + expect_true("select_contrasts" %in% panel_parameters) +}) + +test_that("capsule uses all DEG contrasts when selection is blank", { + workspace <- setup_cli_workspace() + on.exit(unlink(workspace$workspace, recursive = TRUE), add = TRUE) + + output <- withr::with_dir( + workspace$code_dir, + system2( + "Rscript", + c("main.R", common_cli_args, "--select_contrasts="), + stdout = TRUE, + stderr = TRUE + ) + ) + + expect_null(attr(output, "status"), info = paste(output, collapse = "\n")) + expect_outputs_created(workspace$results_dir) + result <- readr::read_csv( + file.path(workspace$results_dir, "moo", "venn_diagram_data.csv"), + show_col_types = FALSE + ) + expect_true(any(grepl("B-C", result$Intersection, fixed = TRUE))) +}) + +test_that("capsule accepts a comma-separated DEG contrast subset", { + workspace <- setup_cli_workspace() + on.exit(unlink(workspace$workspace, recursive = TRUE), add = TRUE) + + output <- withr::with_dir( + workspace$code_dir, + system2( + "Rscript", + c( + "main.R", + common_cli_args, + shQuote("--select_contrasts=B-A, C-A") + ), + stdout = TRUE, + stderr = TRUE + ) + ) + + expect_null(attr(output, "status"), info = paste(output, collapse = "\n")) + expect_outputs_created(workspace$results_dir) + result <- readr::read_csv( + file.path(workspace$results_dir, "moo", "venn_diagram_data.csv"), + show_col_types = FALSE + ) + expect_false(any(grepl("B-C", result$Intersection, fixed = TRUE))) +}) + +test_that("capsule reports unknown DEG contrasts", { + workspace <- setup_cli_workspace() + on.exit(unlink(workspace$workspace, recursive = TRUE), add = TRUE) + + output <- suppressWarnings( + withr::with_dir( + workspace$code_dir, + system2( + "Rscript", + c("main.R", common_cli_args, "--select_contrasts=missing"), + stdout = TRUE, + stderr = TRUE + ) + ) + ) + + expect_equal(attr(output, "status"), 1) + expect_true(any(grepl("Selected contrasts not found: missing", output))) +}) + test_that("run wrapper prepares result directories and forwards CLI arguments", { run_lines <- read_repo_file("code", "run") run_text <- paste(run_lines, collapse = "\n")