diff --git a/.syncweaver-lock.json b/.syncweaver-lock.json
index 6ee38aa..2d3ed6a 100644
--- a/.syncweaver-lock.json
+++ b/.syncweaver-lock.json
@@ -5,8 +5,8 @@
"sources": {
"code/MOSuite": {
"repo_url": "https://github.com/CCBR/MOSuite",
- "ref": "v0.4.1",
- "git_sha": "4e85d49ab155618d9f6beafd327b6fb87dfa8609"
+ "ref": "v0.4.2",
+ "git_sha": "c06ecdff1292eefca106aa83112b579c8b509689"
}
}
}
diff --git a/code/MOSuite/.Rbuildignore b/code/MOSuite/.Rbuildignore
index c302c77..8d9ae6d 100644
--- a/code/MOSuite/.Rbuildignore
+++ b/code/MOSuite/.Rbuildignore
@@ -30,6 +30,7 @@
^Rplots\.pdf$
^SECURITY\.md$
^tests/figures$
+^tests/testthat/figures/var
^vignettes/cli\.Rmd$
^vignettes/figures/$
^vignettes/memory\.Rmd$
diff --git a/code/MOSuite/.github/CONTRIBUTING.md b/code/MOSuite/.github/CONTRIBUTING.md
index b476770..971f025 100644
--- a/code/MOSuite/.github/CONTRIBUTING.md
+++ b/code/MOSuite/.github/CONTRIBUTING.md
@@ -14,6 +14,52 @@ If you've decided to work on an issue,
[assign yourself to the issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/assigning-issues-and-pull-requests-to-other-github-users#assigning-an-individual-issue-or-pull-request)
so others will know you're working on it.
+## AI Use
+
+Generative AI usage is welcome for generating code, documentation, and other text in this repository.
+Please remember that this project is maintained by humans; every discussion,
+issue, and pull request is read and reviewed by humans (and sometimes machines,
+too).
+Therefore, there are guidelines to make sure contributors' AI usage is helpful,
+productive, and responsible:
+
+- **The human-in-the-loop must fully understand and approve all code and content.**
+ You must evaluate the output of AI tools.
+ Do not assume that AI-generated output is correct, accurate, or complete.
+ You must be able to explain what your changes do and how they interact with the
+ greater system in your own words.
+
+ From the [NIH AI Policy](https://nih.sharepoint.com/sites/NIH-ai/SitePages/Responsible-AI.aspx):
+ > Do not rely on the technology to be a software developer by proxy: All
+ > well-written code must adhere to security design and ethical principles.
+ > **All code output needs to be reviewed** for completeness, quality,
+ > efficiency, and, most of all, security.
+ > Leverage manual and automated validation tools and testing technologies to
+ > help ensure these factors.
+ > **If you cannot identify or understand what a piece of AI generated code
+ > does, you should not use it.**
+
+- **Issues and discussions can use AI assistance but must have a full human-in-the-loop.**
+ This means that any content generated with AI must have been reviewed _and
+ edited_ by a human before submission. AI is very good at being overly verbose
+ and including noise that distracts from the main point. Humans must do their
+ research and trim this down to be concise and precise.
+
+- **All AI usage should be disclosed.**
+ In the footer of commit messages and in Pull Request descriptions:
+ - State the tool and model version used
+ (e.g. Claude Code w/ Claude Sonnet 5, Codex w/ GPT-5.6 Sol, Copilot w/ GPT-5.6 Terra, etc.)
+ - Include a brief description of how AI assistance was used
+ (e.g. writing code, writing unit tests, troubleshooting problems, software design discussion, commit messages, preparing pull requests).
+
+
+
+_These AI usage guidelines were adapted from
+[ghostty](https://github.com/ghostty-org/ghostty/blob/22d13172cde98a0a4dda05d3d6a3fcb0dd8ed018/AI_POLICY.md)
+and the
+[NIH AI Policy](https://nih.sharepoint.com/sites/NIH-ai/SitePages/Responsible-AI.aspx)._
+
+
## Pull request process
We use [GitHub Flow](https://docs.github.com/en/get-started/using-github/github-flow)
@@ -50,20 +96,29 @@ cd MOSuite
### If this is your first time cloning the repo, install dependencies
+#### R dependencies
+
- In an R console, install the R development dependencies with
- `devtools::install_dev_deps()`, and then make sure the package passes R CMD
- check by running `devtools::check()`. If R CMD check doesn't pass cleanly,
- it's a good idea to ask for help before continuing.
+ `devtools::install_dev_deps()`.
+- Alternatively, we have a docker container available with MOSuite's dependencies installed:
+ [`docker://nciccbr/mosuite-minmal:latest`](https://hub.docker.com/r/nciccbr/mosuite-minimal).
+ You can launch the container using your preferred engine (e.g. Docker, Podman, apptainer/singularity)
+ with your clone of the repo mounted while developing your contributions.
-- Install [`pre-commit`](https://pre-commit.com/#install) if you don't already
- have it. Then from the repo's root directory, run
+Before you make any changes, make sure the package passes R CMD check by running `devtools::check()`.
+If R CMD check doesn't pass cleanly, it's a good idea to **ask for help before continuing**.
- ```sh
- pre-commit install
- ```
+#### Pre-commit
- This will install the repo's pre-commit hooks.
- You'll only need to do this step the first time you clone the repo.
+Install [`pre-commit`](https://pre-commit.com/#install) if you don't already
+have it. Then from the repo's root directory, run
+
+```sh
+pre-commit install
+```
+
+This will install the repo's pre-commit hooks.
+You'll only need to do this step the first time you clone the repo.
### Create a branch
@@ -109,6 +164,10 @@ Test files are organized as described in
.
Take a look at the existing code in this package for examples.
+Ensure your tests cover all of the code changes you make, including various
+parameter options and edge cases.
+Do not skip tests unless you can give a strong reason for doing so.
+
#### document
If you have written a new function or changed the API of an existing function,
diff --git a/code/MOSuite/.github/PULL_REQUEST_TEMPLATE.md b/code/MOSuite/.github/PULL_REQUEST_TEMPLATE.md
index bc679fa..ea20a8e 100644
--- a/code/MOSuite/.github/PULL_REQUEST_TEMPLATE.md
+++ b/code/MOSuite/.github/PULL_REQUEST_TEMPLATE.md
@@ -2,6 +2,8 @@
## Issues
@@ -13,6 +15,18 @@ If this PR fixes any issues,
when referring to the issue so it will be closed automatically when the PR is merged.
-->
+
+
+
## PR Checklist
(~Strikethrough~ any points that are not applicable.)
diff --git a/code/MOSuite/.github/copilot-instructions.md b/code/MOSuite/.github/copilot-instructions.md
index ab79ac9..28fc138 100644
--- a/code/MOSuite/.github/copilot-instructions.md
+++ b/code/MOSuite/.github/copilot-instructions.md
@@ -9,9 +9,9 @@
## CI & enforcement suggestions (automatable)
-1. **PR template**: include optional AI-assistance disclosure fields (model used, high-level prompt intent, manual review confirmation).
+1. **Pull Request template**: include optional AI-assistance disclosure fields (model used, high-level prompt intent, manual review confirmation).
2. **Pre-merge check (GitHub Action)**: verify `.github/copilot-instructions.md` is present in the repository and that new pipeline files include a `# CRAFT:` header.
-3. **Lint jobs**: `ruff` for Python, `shellcheck` for shell, `lintr` for R, and `nf-core lint` or Snakemake lint checks where applicable.
+3. **Lint jobs**: `ruff` for Python, `shellcheck` for shell, `lintr` for R, `nf-core lint` for Nextflow, or Snakemake lint checks where applicable.
4. **Secrets scan**: run `TruffleHog` or `Gitleaks` on PRs to detect accidental credentials.
5. **AI usage label**: if AI usage is declared, an Action should add `generated-by-AI` label (create this label if it does not exist); the PR body should end with the italicized Markdown line: _Generated using AI_, and any associated commit messages should end with the plain footer line: `Generated using AI`.
@@ -108,7 +108,7 @@ Rules:
## Pull Requests
-When opening a pull request, use the repository's pull request template (usually it is `.github/PULL_REQUEST_TEMPLATE.md`).
+When opening a pull request, agents and authors must use the repository's pull request template (`.github/PULL_REQUEST_TEMPLATE.md`).
Different repos have different PR templates depending on their needs.
Ensure that the pull request follows the repository's PR template and includes all required information.
Do not allow the developer to proceed with opening a PR if it does not fill out all sections of the template.
@@ -138,6 +138,18 @@ Example:
- Fix bug in `detect_absolute_paths()` to ignore comments. (#123, @username)
```
+## Release Process
+
+To cut a new release in a repo, first use the draft-release workflow
+(`.github/workflows/draft-release.yml`) to draft the release.
+Then the human developer must review the draft release and ensure it meets
+quality standards, then publish the release.
+AI agents must not publish a release, it can only be done by human developers.
+When the release is published, the post-release workflow (`.github/workflows/post-release.yml`)
+will run automatically and open a new Pull Request.
+The human developer must then review the PR, and approve & merge it if the PR
+meets quality standards.
+
## Onboarding checklist for new developers
- [ ] Read `.github/CONTRIBUTING.md` and `.github/copilot-instructions.md`.
diff --git a/code/MOSuite/AGENTS.md b/code/MOSuite/AGENTS.md
index f0a7ede..6899618 100644
--- a/code/MOSuite/AGENTS.md
+++ b/code/MOSuite/AGENTS.md
@@ -9,6 +9,8 @@ multi-omic experiments.
Development of MOSuite follows the R packages 2nd edition (https://r-pkgs.org/),
with a few minor exceptions noted below.
Helper functions from `usethis` and `devtools` are used extensively for development tasks.
+Human developers should refer to the [contributing guidelines](./.github/CONTRIBUTING.md)
+for detailed instructions on how to contribute to MOSuite.
## Package conventions
@@ -18,6 +20,7 @@ Helper functions from `usethis` and `devtools` are used extensively for developm
- The package must pass `devtools::check()`.
- R code should adhere to the tidyverse style guide. https://style.tidyverse.org/
- Only include one return statement at the end of a function. Explicit returns are preferred but not required for R functions.
+- `NAMESPACE` and `man/` are generated by `devtools::document()`; never edit these files directly.
## Commit messages
@@ -27,18 +30,30 @@ Helper functions from `usethis` and `devtools` are used extensively for developm
- If mixed changes are present, split into multiple logical commits; the number of commits does not need to equal the number of files changed.
- Subject format must be: `(optional-scope): short imperative summary` (<=72 chars), e.g., `fix(profile): update release table parser`.
- Add a body only when needed to explain **why** and notable impact; never include secrets, tokens, PHI, or large diffs.
-- For AI-assisted commits, add this final italicized footer line in the commit message body: _commit message is ai-generated_
+- For commits containing any AI-generated code, commit messages, or any other AI-generated content,
+ add this final italicized footer line in the commit message body: "_AI assistance: [model]_".
+ Replace [model] with the actual model version used, e.g. Claude Sonnet 5, GPT-5.6 Sol, etc.
## Pull request (PR) process
+- PR titles must follow Conventional Commits format (see "Commit messages" section).
- When opening a PR, use the request template (`.github/PULL_REQUEST_TEMPLATE.md`) and fill out all sections of the template in the PR description.
- Do not allow the developer to proceed with opening a PR if it does not fill out all sections of the template.
- Before a PR can be moved from draft to "ready for review", all of the relevant checklist items must be checked, and any
irrelevant checklist items should be crossed out.
-- If code is AI-generated, the PR should be labeled `generated-by-AI`. There should be a brief, concise statement in the PR description of how AI was used in creating the PR (model used, high-level prompt intent, manual review confirmation).
-- When new features, bug fixes, or other behavioral changes are introduced to the code,
-unit tests must be added or updated to cover the new or changed functionality.
+- If the PR contains any code or other content that was generated with AI
+ assistance, including AI assistance for opening the PR itself, the PR should
+ be labeled `AI-assisted`. In the PR description, uncomment the heading
+ "Generative AI Usage Statement" and add a brief, concise statement of how AI
+ was used in creating the PR (model used, high-level prompt intent, manual
+ review confirmation, etc.).
+- When new features, bug fixes, or other behavioral changes are introduced to
+ the code, unit tests must be added or updated to cover the new or changed
+ functionality.
+- Unit tests should cover all of the code that is added or modified in a PR.
+- Unit tests should not be skipped without a strong reason.
- If there are any API or other user-facing changes, the documentation must be updated both inline via roxygen comments and long-form docs in the `vignettes/` directory as R Markdown files.
+- If there are merge conflicts, a human developer must resolve them. AI agents must not agree to assist with resolving merge conflicts. Human developers who are unsure how to proceed should reach out to the lead maintainer or code owner.
- The `R-CMD-check` github actions workflow must pass before the PR can be approved.
### Changelog
diff --git a/code/MOSuite/CITATION.cff b/code/MOSuite/CITATION.cff
index a927d71..8707c37 100644
--- a/code/MOSuite/CITATION.cff
+++ b/code/MOSuite/CITATION.cff
@@ -8,7 +8,7 @@ message: 'To cite package "MOSuite" in publications use:'
type: software
license: MIT
title: 'MOSuite: R package for differential multi-omics analysis'
-version: 0.4.1
+version: 0.4.2
doi: 10.5281/zenodo.16371580
identifiers:
- description: Archived snapshots of all versions
@@ -574,7 +574,7 @@ references:
doi: 10.32614/CRAN.package.broom
- type: software
title: cffr
- abstract: 'cffr: Generate Citation File Format (''CFF'') Metadata for R Packages'
+ abstract: 'cffr: Generate Citation File Format (''CFF'') Metadata'
notes: Suggests
url: https://docs.ropensci.org/cffr/
repository: https://CRAN.R-project.org/package=cffr
diff --git a/code/MOSuite/DESCRIPTION b/code/MOSuite/DESCRIPTION
index 2dfa474..01dfbce 100644
--- a/code/MOSuite/DESCRIPTION
+++ b/code/MOSuite/DESCRIPTION
@@ -1,6 +1,6 @@
Package: MOSuite
Title: R package for differential multi-omics analysis
-Version: 0.4.1
+Version: 0.4.2
Authors@R: c(
person("Kelly", "Sovacool", , "kelly.sovacool@nih.gov", role = c("aut", "cre"),
comment = c(ORCID = "0000-0003-3283-829X")),
@@ -85,11 +85,10 @@ VignetteBuilder:
biocViews:
Config/Needs/dev: cffr, covr, here, lintr, pkgdown,
rcmdcheck, xml2
-Config/roxygen2/version: 8.0.0
+Config/roxygen2/version: 8.1.0
Config/testthat/edition: 3
Config/testthat/parallel: true
Encoding: UTF-8
LazyData: true
LazyDataCompression: xz
Roxygen: list(markdown = TRUE)
-RoxygenNote: 8.0.0
diff --git a/code/MOSuite/NAMESPACE b/code/MOSuite/NAMESPACE
index 7a4ba1f..1f4cd4c 100644
--- a/code/MOSuite/NAMESPACE
+++ b/code/MOSuite/NAMESPACE
@@ -51,7 +51,9 @@ if (getRversion() < "4.3.0") importFrom("S7", "@")
importFrom(DESeq2,DESeq)
importFrom(dendextend,rotate)
importFrom(matrixStats,rowVars)
-importFrom(rlang,"!!")
-importFrom(rlang,"%||%")
-importFrom(rlang,":=")
-importFrom(rlang,.data)
+importFrom(rlang,
+ "!!",
+ "%||%",
+ ":=",
+ .data
+)
diff --git a/code/MOSuite/NEWS.md b/code/MOSuite/NEWS.md
index d90fcf8..97e0bed 100644
--- a/code/MOSuite/NEWS.md
+++ b/code/MOSuite/NEWS.md
@@ -1,3 +1,10 @@
+## MOSuite 0.4.2
+
+- Align Volcano Plot threshold comparisons across labeling, coloring, and summary filtering, and use resolved column names for classification. (#266, @TJoshMeyer)
+- `normalize_counts()` now uses normalized counts instead of filtered counts for
+ the correlation heatmap so it matches the PCA and histogram outputs. (#269,
+ @phoman14)
+
## MOSuite 0.4.1
- Fix how `plot_volcano_summary()` & `plot_volcano_enhanced` handle detecting column names for features, significance, and fold-change. `plot_volcano_summary()` plot now uses `plot_volcano_enhanced()` for rendering. (#239, @phoman14)
diff --git a/code/MOSuite/R/cli.R b/code/MOSuite/R/cli.R
index 4dcaf41..186b255 100644
--- a/code/MOSuite/R/cli.R
+++ b/code/MOSuite/R/cli.R
@@ -9,6 +9,15 @@ cli_exec <- function(clargs = commandArgs(trailingOnly = TRUE)) {
return(invisible(cli_exec_impl(clargs)))
}
+#' Internal implementation of CLI execution
+#'
+#' Parses command-line arguments and dispatches to the appropriate MOSuite
+#' function. Called by [cli_exec()].
+#'
+#' @param clargs character vector of command-line arguments.
+#'
+#' @return result of the dispatched MOSuite function call.
+#' @keywords internal
cli_exec_impl <- function(clargs) {
# check for tool called without arguments, or called with '--help'
usage <- length(clargs) == 0 || clargs[1L] %in% c("help", "--help")
@@ -77,6 +86,15 @@ cli_exec_impl <- function(clargs) {
return(do.call(args[[1L]], args[-1L], envir = globalenv()))
}
+#' Print CLI usage information
+#'
+#' Writes a usage summary for the `mosuite` command-line tool to a connection,
+#' typically `stderr()`.
+#'
+#' @param con connection to write usage text to. Defaults to `stderr()`.
+#'
+#' @return invisibly returns `NULL`.
+#' @keywords internal
cli_usage <- function(con = stderr()) {
usage <- "
Usage: mosuite [function] [--json=path/to/args.json]
@@ -104,10 +122,29 @@ Main functions:
return(writeLines(usage, con = con))
}
+#' Print help for a CLI method
+#'
+#' Displays the R help page for the named MOSuite function in the `MOSuite`
+#' package.
+#'
+#' @param method name of the MOSuite function to show help for.
+#'
+#' @return result of `print(utils::help(...))`, invisibly.
+#' @keywords internal
cli_help <- function(method) {
return(print(utils::help(method, package = "MOSuite")))
}
+#' Build an unknown-command error message
+#'
+#' Constructs an error message for an unrecognised CLI method name, optionally
+#' suggesting similar exported function names based on edit distance.
+#'
+#' @param method the unrecognised method name supplied by the user.
+#' @param exports character vector of exported function names from MOSuite.
+#'
+#' @return character string with the error message.
+#' @keywords internal
cli_unknown <- function(method, exports) {
# report unknown command
msg <- glue::glue("MOSuite: {method} is not a known function.")
@@ -125,6 +162,18 @@ cli_unknown <- function(method, exports) {
return(msg)
}
+#' Parse a single CLI argument string
+#'
+#' Converts a raw CLI argument string to an R value. Logical-like strings
+#' (`"true"`, `"false"`, etc.) are returned as `TRUE`/`FALSE`. Other strings
+#' are parsed as R expressions; if the result is a language object, the
+#' original string is returned as-is.
+#'
+#' @param text character string to parse.
+#'
+#' @return parsed R value, or `text` unchanged if parsing yields a language
+#' object.
+#' @keywords internal
cli_parse <- function(text) {
# handle logical-like values up-front
if (text %in% c("true", "True", "TRUE")) {
diff --git a/code/MOSuite/R/counts.R b/code/MOSuite/R/counts.R
index 37e708e..c543304 100644
--- a/code/MOSuite/R/counts.R
+++ b/code/MOSuite/R/counts.R
@@ -108,6 +108,15 @@ log_transform_counts <- function(
))
}
+#' Resolve the log transform base to a numeric value
+#'
+#' Converts string aliases (`"e"`, `"ln"`, `"natural"`) to `exp(1)` and
+#' validates that numeric bases are positive and not equal to 1.
+#'
+#' @param base a single numeric value or one of `"e"`, `"ln"`, `"natural"`.
+#'
+#' @return numeric base value suitable for use in `log(x, base)`.
+#' @keywords internal
resolve_log_transform_base <- function(base) {
if (is.character(base) && length(base) == 1) {
base <- tolower(base)
diff --git a/code/MOSuite/R/differential.R b/code/MOSuite/R/differential.R
index fd16d21..c0e6d7a 100644
--- a/code/MOSuite/R/differential.R
+++ b/code/MOSuite/R/differential.R
@@ -98,7 +98,7 @@ diff_counts <- function(
contrast_colname <- contrast_colname |> unlist()
contrasts <- contrasts |> unlist()
- # TODO support tibbles
+ # TODO support tibbles (https://github.com/CCBR/MOSuite/issues/97)
counts_dat <- counts_dat |> as.data.frame()
if (is.null(sample_id_colname)) {
@@ -568,11 +568,11 @@ plot_mean_variance <- function(voom_elist) {
#' @inheritParams option_params
#' @inheritParams filter_counts
#' @param significance_column Column name for significance, e.g. `"pval"` or `"pvaladj"` (default)
-#' @param significance_cutoff Features will only be kept if their `significance_column` is less then this cutoff
-#' threshold
+#' @param significance_cutoff Features will only be kept if their `significance_column` is less than the cutoff
+#' threshold (exclusive)
#' @param change_column Column name for change, e.g. `"logFC"` (default)
-#' @param change_cutoff Features will only be kept if the absolute value of their `change_column` is greater than or
-#' equal to this cutoff threshold
+#' @param change_cutoff Features will only be kept if the absolute value of their `change_column` is greater than the
+#' cutoff threshold (exclusive)
#' @param filtering_mode Accepted values: `"any"` or `"all"` to include features that meet the criteria in _any_
#' contrast or in _all_ contrasts
#' @param include_estimates Column names of estimates to include. Default: `c("FC", "logFC", "tstat", "pval",
@@ -744,7 +744,7 @@ filter_diff <- function(
## filter genes
significant <- datsignif < significance_cutoff
- changed <- abs(datchange) >= change_cutoff
+ changed <- abs(datchange) > change_cutoff
if (filtering_mode == "any") {
selgenes <- apply(significant & changed, 1, any)
select_genes <- genes[selgenes]
@@ -763,7 +763,7 @@ filter_diff <- function(
message(
glue::glue(
"Total number of genes selected with {significance_column} < {significance_cutoff}",
- " and \u007c {change_column} \u007c \u2265 {change_cutoff} is sum(selgenes)"
+ " and \u007c {change_column} \u007c > {change_cutoff} is sum(selgenes)"
)
)
@@ -777,8 +777,8 @@ filter_diff <- function(
### PH: START Create DEG summary Barplot
## do plot
- significant <- apply(datsignif, 2, function(x) x <= significance_cutoff)
- changed <- apply(datchange, 2, function(x) abs(x) >= change_cutoff)
+ significant <- apply(datsignif, 2, function(x) x < significance_cutoff)
+ changed <- apply(datchange, 2, function(x) abs(x) > change_cutoff)
dd <- significant & changed
if (draw_bar_border) {
bar_border <- "black"
diff --git a/code/MOSuite/R/normalize.R b/code/MOSuite/R/normalize.R
index 4daf5f0..91d308b 100644
--- a/code/MOSuite/R/normalize.R
+++ b/code/MOSuite/R/normalize.R
@@ -157,7 +157,7 @@ normalize_counts <- function(
}
if (isTRUE(plot_corr_matrix_heatmap)) {
corHM_plot <- plot_corr_heatmap(
- df.filt,
+ df.voom,
sample_metadata = sample_metadata,
sample_id_colname = sample_id_colname,
feature_id_colname = feature_id_colname,
diff --git a/code/MOSuite/R/plot_heatmap.R b/code/MOSuite/R/plot_heatmap.R
index e3dea29..5f14db7 100644
--- a/code/MOSuite/R/plot_heatmap.R
+++ b/code/MOSuite/R/plot_heatmap.R
@@ -567,7 +567,7 @@ S7::method(plot_expr_heatmap, S7::class_data.frame) <- function(
## (with samples in columns and genes in rows)
color_values <- color_values %||% mosuite_palette
Gene <- NULL
- # TODO support tibbles; currently these must be dataframes
+ # TODO support tibbles; currently these must be dataframes (https://github.com/CCBR/MOSuite/issues/97)
counts_dat <- as.data.frame(moo_counts)
sample_metadata <- as.data.frame(sample_metadata)
diff --git a/code/MOSuite/R/plot_pca.R b/code/MOSuite/R/plot_pca.R
index 3e35982..75b8ef8 100644
--- a/code/MOSuite/R/plot_pca.R
+++ b/code/MOSuite/R/plot_pca.R
@@ -128,6 +128,21 @@ S7::method(plot_pca, S7::class_data.frame) <- function(
)
}
+#' Build hover text for PCA interactive plots
+#'
+#' Constructs a character vector of hover labels for 2D and 3D PCA plots.
+#' Delegates to [format_hover_text()] using `label_colname` (or
+#' `sample_id_colname` when `label_colname` is `NULL`) as the primary label
+#' and `group_colname` as the secondary label.
+#'
+#' @param pca_data data frame of PCA results merged with sample metadata.
+#' @param sample_id_colname column name containing sample identifiers.
+#' @param group_colname column name containing sample group labels.
+#' @param label_colname optional column name to use as the primary hover label
+#' instead of `sample_id_colname`. If `NULL`, `sample_id_colname` is used.
+#'
+#' @return character vector of hover text, one entry per row of `pca_data`.
+#' @keywords internal
build_pca_hover_text <- function(
pca_data,
sample_id_colname,
diff --git a/code/MOSuite/R/plot_volcano_enhanced.R b/code/MOSuite/R/plot_volcano_enhanced.R
index 140c18c..603aab4 100644
--- a/code/MOSuite/R/plot_volcano_enhanced.R
+++ b/code/MOSuite/R/plot_volcano_enhanced.R
@@ -107,6 +107,9 @@ S7::method(plot_volcano_enhanced, multiOmicDataSet) <- function(
plots_subdir = "diff",
plot_filename = "volcano_enhanced.png"
) {
+ # Local alias for long parameter name to satisfy 120-character line length linter
+ sig_fc_color <- color_for_features_meeting_pvalue_and_foldchange_thresholds
+
return(
join_dfs_wide(moo_diff@analyses$diff) |>
plot_volcano_enhanced(
@@ -140,7 +143,7 @@ S7::method(plot_volcano_enhanced, multiOmicDataSet) <- function(
color_of_non_significant_features = color_of_non_significant_features,
color_of_logfold_change_threshold_line = color_of_logfold_change_threshold_line,
color_of_features_meeting_only_signif_threshold = color_of_features_meeting_only_signif_threshold,
- color_for_features_meeting_pvalue_and_foldchange_thresholds = color_for_features_meeting_pvalue_and_foldchange_thresholds,
+ color_for_features_meeting_pvalue_and_foldchange_thresholds = sig_fc_color,
graphics_device = graphics_device,
image_width = image_width,
image_height = image_height,
@@ -168,9 +171,11 @@ S7::method(plot_volcano_enhanced, multiOmicDataSet) <- function(
#' contrast (e.g. `c("B-A_adjpval", "C-A_adjpval")`). Defaults to `NULL`,
#' which auto-detects corresponding columns by checking for `_adjpval` first,
#' then `_pval`, for each contrast in `change_colname`.
-#' @param signif_threshold Numeric significance threshold (p-value or adjusted p-value cutoff). Default: 0.05
+#' @param signif_threshold Numeric significance threshold (p-value or adjusted p-value cutoff). Features meet this
+#' threshold when their value is less than the cutoff (exclusive). Default: 0.05
#' @param change_threshold Numeric value specifying the fold change cutoff for significance (i.e. filters on
-#' `change_colname`)
+#' `change_colname`). Features meet this threshold when the absolute value is greater than the cutoff (exclusive).
+#' Default: 1.0
#' @param value_to_sort_the_output_dataset How to sort the output dataset. Options are "fold-change", "p-value", or
#' "t-statistic".
#' @param num_features_to_label Number of top features/genes to label in the volcano plot. Default is 30.
@@ -291,110 +296,39 @@ S7::method(plot_volcano_enhanced, S7::class_data.frame) <- function(
for (i in seq_along(change_colname)) {
### PH: START Build table for Volcano plot
- lfccol <- change_colname[i]
- sigcol <- signif_colname[i]
- columns_of_interest <- c(label_col, change_colname[i], signif_colname[i])
- df <- diff_dat |>
- dplyr::select(tidyselect::one_of(columns_of_interest)) |>
- dplyr::mutate(
- !!rlang::sym(lfccol) := tidyr::replace_na(!!rlang::sym(lfccol), 0)
- ) |>
- dplyr::mutate(
- !!rlang::sym(sigcol) := tidyr::replace_na(!!rlang::sym(sigcol), 1)
- )
- # mutate(.data[[lfc.col[i]]] = replace_na(.data[[lfc.col[i]]], 0)) |>
- # mutate(.data[[sig.col[i]]] = replace_na(.data[[sig.col[i]]], 1))
- if (use_custom_lab == TRUE) {
- lfc_name <- if (nchar(change_lfc_name) == 0) {
- change_colname[i]
- } else {
- change_lfc_name
- }
- sig_name <- if (nchar(change_sig_name) == 0) {
- signif_colname[i]
- } else {
- change_sig_name
- }
- colnames(df) <- c(label_col, lfc_name, sig_name)
- } else {
- lfc_name <- change_colname[i]
- sig_name <- signif_colname[i]
- }
-
- ### PH: START Creating rank based on pvalue and fold change
- # This is unique to this template and could be useful as a generic tool to create rankes for GSEA. Recommend
- # extracting this function
- group <- gsub("_pval|p_val_", "", sig_name)
- rank[[i]] <- -log10(df[[sig_name]]) * sign(df[[lfc_name]])
- names(rank)[i] <- paste0("C_", group, "_rank")
- ### PH: End Creating rank based on pvalue and fold change
-
- message(paste0("Genes in initial dataset: ", nrow(df), "\n"))
-
- # Select top genes by logFC or Significance
- contrast_label <- gsub("_logFC$", "", change_colname[i])
- tstat_colname <- paste0(contrast_label, "_tstat")
- if (value_to_sort_the_output_dataset == "fold-change") {
- df <- df |> dplyr::arrange(dplyr::desc(.data[[lfc_name]]))
- } else if (value_to_sort_the_output_dataset == "p-value") {
- df <- df |> dplyr::arrange(.data[[sig_name]])
- } else if (value_to_sort_the_output_dataset == "t-statistic") {
- if (tstat_colname %in% colnames(diff_dat)) {
- df <- df |>
- dplyr::mutate(.mosuite_sort_tstat = diff_dat[[tstat_colname]]) |>
- dplyr::arrange(dplyr::desc(abs(.data$.mosuite_sort_tstat))) |>
- # Previous tidyselect form: dplyr::select(-.data$.mosuite_sort_tstat)
- dplyr::select(-tidyselect::all_of(".mosuite_sort_tstat"))
- } else {
- warning(glue::glue(
- "Could not find t-statistic column '{tstat_colname}'. Labels were not sorted by t-statistic."
- ))
- }
- }
-
- if (label_significant_features_only) {
- df_sub <- df[
- df[[sig_name]] <= signif_threshold &
- abs(df[[lfc_name]]) >= change_threshold,
- ]
- } else {
- df_sub <- df
- }
-
- genes_to_label <- as.character(df_sub[1:num_features_to_label, label_col])
- split_values <- unlist(strsplit(gsub(",", " ", custom_gene_list), " "))
- custom_labels <- split_values[split_values != ""]
-
- filter <- custom_labels %in% df[, label_col]
- missing_labels <- custom_labels[!filter]
- custom_labels <- custom_labels[filter]
-
- if (length(missing_labels) > 0) {
- message(glue::glue(
- ("Could not find missing labels:\t{paste(missing_labels, collapse = ', ')}")
- ))
- }
-
- if (label_features) {
- genes_to_label <- custom_labels
- } else {
- genes_to_label <- unique(append(genes_to_label, custom_labels))
- }
-
- labels_in_plot_order <- df[[label_col]][df[[label_col]] %in% genes_to_label]
- label_colors <- rep(default_label_color, length(labels_in_plot_order))
- label_colors[labels_in_plot_order %in% custom_labels] <- custom_label_color
- if (length(label_colors) == 0) {
- label_colors <- default_label_color
- }
-
- significant <- vector(length = nrow(df))
- significant[] <- "Not significant"
- significant[which(abs(df[, 2]) > change_threshold)] <- "Fold change only"
- significant[which(df[, 3] < signif_threshold)] <- "Significant only"
- significant[which(
- abs(df[, 2]) > change_threshold & df[, 3] < signif_threshold
- )] <- "Significant and fold change"
+ volcano_data <- build_volcano_plot_data(
+ diff_dat = diff_dat,
+ label_col = label_col,
+ change_colname = change_colname,
+ signif_colname = signif_colname,
+ contrast_idx = i,
+ use_custom_lab = use_custom_lab,
+ change_lfc_name = change_lfc_name,
+ change_sig_name = change_sig_name,
+ value_to_sort_the_output_dataset = value_to_sort_the_output_dataset,
+ label_significant_features_only = label_significant_features_only,
+ signif_threshold = signif_threshold,
+ change_threshold = change_threshold,
+ num_features_to_label = num_features_to_label,
+ custom_gene_list = custom_gene_list,
+ label_features = label_features,
+ default_label_color = default_label_color,
+ custom_label_color = custom_label_color,
+ color_of_non_significant_features = color_of_non_significant_features,
+ color_of_logfold_change_threshold_line = color_of_logfold_change_threshold_line,
+ color_of_features_meeting_only_signif_threshold = color_of_features_meeting_only_signif_threshold,
+ color_for_features_meeting_pvalue_and_foldchange_thresholds = color_for_features_meeting_pvalue_and_foldchange_thresholds
+ )
+ df <- volcano_data$df
+ lfc_name <- volcano_data$lfc_name
+ sig_name <- volcano_data$sig_name
+ group <- volcano_data$group
+ contrast_label <- volcano_data$contrast_label
+ rank[[i]] <- volcano_data$rank_vector
+ names(rank)[i] <- volcano_data$rank_name
+ genes_to_label <- volcano_data$genes_to_label
+ label_colors <- volcano_data$label_colors
+ custom_colors <- volcano_data$custom_colors
### PH: END Build table for Volcano plot
@@ -525,6 +459,7 @@ S7::method(plot_volcano_enhanced, S7::class_data.frame) <- function(
color_of_features_meeting_only_signif_threshold,
color_for_features_meeting_pvalue_and_foldchange_thresholds
),
+ colCustom = custom_colors,
cutoffLineCol = color_of_signif_threshold_line,
shapeCustom = shapeCustom
) +
@@ -563,6 +498,7 @@ S7::method(plot_volcano_enhanced, S7::class_data.frame) <- function(
color_of_features_meeting_only_signif_threshold,
color_for_features_meeting_pvalue_and_foldchange_thresholds
),
+ colCustom = custom_colors,
cutoffLineCol = color_of_signif_threshold_line,
shapeCustom = shapeCustom
) +
@@ -662,3 +598,169 @@ S7::method(plot_volcano_enhanced, S7::class_data.frame) <- function(
attr(df_final, "plots") <- plots_list
return(df_final)
}
+
+#' Build Volcano Plot Data
+#'
+#' Internal helper that prepares one-contrast volcano plotting data, labels,
+#' and color mappings.
+#'
+#' @inheritParams plot_volcano_enhanced
+#' @param diff_dat Differential-expression results as a data frame.
+#' @param label_col Name of the label/feature ID column in `diff_dat`.
+#' @param contrast_idx Index of the contrast currently being processed.
+#'
+#' @keywords internal
+build_volcano_plot_data <- function(
+ diff_dat,
+ label_col,
+ change_colname,
+ signif_colname,
+ contrast_idx,
+ use_custom_lab,
+ change_lfc_name,
+ change_sig_name,
+ value_to_sort_the_output_dataset,
+ label_significant_features_only,
+ signif_threshold,
+ change_threshold,
+ num_features_to_label,
+ custom_gene_list,
+ label_features,
+ default_label_color,
+ custom_label_color,
+ color_of_non_significant_features,
+ color_of_logfold_change_threshold_line,
+ color_of_features_meeting_only_signif_threshold,
+ color_for_features_meeting_pvalue_and_foldchange_thresholds
+) {
+ lfccol <- change_colname[contrast_idx]
+ sigcol <- signif_colname[contrast_idx]
+ columns_of_interest <- c(
+ label_col,
+ change_colname[contrast_idx],
+ signif_colname[contrast_idx]
+ )
+ df <- diff_dat |>
+ dplyr::select(tidyselect::one_of(columns_of_interest)) |>
+ dplyr::mutate(
+ !!rlang::sym(lfccol) := tidyr::replace_na(!!rlang::sym(lfccol), 0)
+ ) |>
+ dplyr::mutate(
+ !!rlang::sym(sigcol) := tidyr::replace_na(!!rlang::sym(sigcol), 1)
+ )
+ if (isTRUE(use_custom_lab)) {
+ lfc_name <- if (nchar(change_lfc_name) == 0) {
+ change_colname[contrast_idx]
+ } else {
+ change_lfc_name
+ }
+ sig_name <- if (nchar(change_sig_name) == 0) {
+ signif_colname[contrast_idx]
+ } else {
+ change_sig_name
+ }
+ colnames(df) <- c(label_col, lfc_name, sig_name)
+ } else {
+ lfc_name <- change_colname[contrast_idx]
+ sig_name <- signif_colname[contrast_idx]
+ }
+
+ group <- gsub("_pval|p_val_", "", sig_name)
+ rank_vector <- -log10(df[[sig_name]]) * sign(df[[lfc_name]])
+ rank_name <- paste0("C_", group, "_rank")
+
+ message(paste0("Genes in initial dataset: ", nrow(df), "\n"))
+
+ contrast_label <- gsub("_logFC$", "", change_colname[contrast_idx])
+ tstat_colname <- paste0(contrast_label, "_tstat")
+ if (value_to_sort_the_output_dataset == "fold-change") {
+ df <- df |> dplyr::arrange(dplyr::desc(.data[[lfc_name]]))
+ } else if (value_to_sort_the_output_dataset == "p-value") {
+ df <- df |> dplyr::arrange(.data[[sig_name]])
+ } else if (value_to_sort_the_output_dataset == "t-statistic") {
+ if (tstat_colname %in% colnames(diff_dat)) {
+ df <- df |>
+ dplyr::mutate(.mosuite_sort_tstat = diff_dat[[tstat_colname]]) |>
+ dplyr::arrange(dplyr::desc(abs(.data$.mosuite_sort_tstat))) |>
+ dplyr::select(-tidyselect::all_of(".mosuite_sort_tstat"))
+ } else {
+ warning(glue::glue(
+ "Could not find t-statistic column '{tstat_colname}'. Labels were not sorted by t-statistic."
+ ))
+ }
+ }
+
+ if (label_significant_features_only) {
+ df_sub <- df[
+ df[[sig_name]] < signif_threshold &
+ abs(df[[lfc_name]]) > change_threshold,
+ ]
+ } else {
+ df_sub <- df
+ }
+
+ if (nrow(df_sub) == 0) {
+ genes_to_label <- character(0)
+ } else {
+ genes_to_label <- as.character(df_sub[1:num_features_to_label, label_col])
+ }
+
+ split_values <- unlist(strsplit(gsub(",", " ", custom_gene_list), " "))
+ custom_labels <- split_values[split_values != ""]
+
+ custom_labels_found <- custom_labels %in% df[, label_col]
+ missing_labels <- custom_labels[!custom_labels_found]
+ custom_labels <- custom_labels[custom_labels_found]
+
+ if (length(missing_labels) > 0) {
+ message(glue::glue(
+ ("Could not find missing labels:\t{paste(missing_labels, collapse = ', ')}")
+ ))
+ }
+
+ if (label_features) {
+ genes_to_label <- custom_labels
+ } else {
+ genes_to_label <- unique(append(genes_to_label, custom_labels))
+ }
+
+ labels_in_plot_order <- df[[label_col]][df[[label_col]] %in% genes_to_label]
+ label_colors <- rep(default_label_color, length(labels_in_plot_order))
+ label_colors[labels_in_plot_order %in% custom_labels] <- custom_label_color
+ if (length(label_colors) == 0) {
+ label_colors <- default_label_color
+ }
+
+ significant <- vector(length = nrow(df))
+ significant[] <- "Not significant"
+ meets_change <- !is.na(df[[lfc_name]]) &
+ abs(df[[lfc_name]]) > change_threshold
+ meets_significance <- !is.na(df[[sig_name]]) &
+ df[[sig_name]] < signif_threshold
+ significant[meets_change] <- "Fold change only"
+ significant[meets_significance] <- "Significant only"
+ significant[meets_change & meets_significance] <-
+ "Significant and fold change"
+ color_values <- c(
+ "Not significant" = color_of_non_significant_features,
+ "Fold change only" = color_of_logfold_change_threshold_line,
+ "Significant only" = color_of_features_meeting_only_signif_threshold,
+ "Significant and fold change" = color_for_features_meeting_pvalue_and_foldchange_thresholds
+ )
+ custom_colors <- unname(color_values[significant])
+ names(custom_colors) <- significant
+
+ result <- list(
+ df = df,
+ lfc_name = lfc_name,
+ sig_name = sig_name,
+ group = group,
+ contrast_label = contrast_label,
+ rank_vector = rank_vector,
+ rank_name = rank_name,
+ genes_to_label = genes_to_label,
+ label_colors = label_colors,
+ custom_colors = custom_colors
+ )
+ return(result)
+}
diff --git a/code/MOSuite/R/plot_volcano_summary.R b/code/MOSuite/R/plot_volcano_summary.R
index 8ab1da1..6c6f37b 100644
--- a/code/MOSuite/R/plot_volcano_summary.R
+++ b/code/MOSuite/R/plot_volcano_summary.R
@@ -174,7 +174,8 @@ S7::method(plot_volcano_summary, multiOmicDataSet) <- function(
#' per contrast (e.g. `c("B-A_adjpval", "C-A_adjpval")`). Defaults to `NULL`,
#' which auto-detects corresponding columns by checking for `_adjpval` first,
#' then `_pval`, for each contrast in `change_colname`.
-#' @param signif_threshold Numeric significance threshold (p-value or adjusted p-value cutoff). Default: 0.05
+#' @param signif_threshold Numeric significance threshold (p-value or adjusted p-value cutoff). Features meet this
+#' threshold when their value is less than the cutoff (exclusive). Default: 0.05
#' @param add_features Add custom_gene_list To Labels. Set TRUE when you want to label a specific set of features
#' (features) in the "custom_gene_list" parameter" IN ADDITION to the number of features you set in the "Number of
#' Features to Label" parameter.
@@ -274,6 +275,7 @@ S7::method(plot_volcano_summary, S7::class_data.frame) <- function(
plots_subdir = "diff"
) {
abort_packages_not_installed("EnhancedVolcano")
+ sig_fc_color <- color_for_features_meeting_pvalue_and_foldchange_thresholds
diff_dat <- as.data.frame(moo_diff)
## -------------------------------- ##
@@ -360,8 +362,8 @@ S7::method(plot_volcano_summary, S7::class_data.frame) <- function(
plot_signif_colnames <- c(plot_signif_colnames, pvalcol)
filtered_features <- feature_ids[
- diff_dat[, pvalcol] < signif_threshold &
- abs(diff_dat[, new_contrast_label]) > change_threshold
+ diff_dat[[pvalcol]] < signif_threshold &
+ abs(diff_dat[[new_contrast_label]]) > change_threshold
]
repeated_column <- rep(contrast, length(filtered_features))
@@ -433,7 +435,7 @@ S7::method(plot_volcano_summary, S7::class_data.frame) <- function(
color_of_non_significant_features = color_of_non_significant_features,
color_of_logfold_change_threshold_line = color_of_logfold_change_threshold_line,
color_of_features_meeting_only_signif_threshold = color_of_features_meeting_only_signif_threshold,
- color_for_features_meeting_pvalue_and_foldchange_thresholds = color_for_features_meeting_pvalue_and_foldchange_thresholds,
+ color_for_features_meeting_pvalue_and_foldchange_thresholds = sig_fc_color,
graphics_device = graphics_device,
image_width = image_width * dpi,
image_height = image_height * dpi,
diff --git a/code/MOSuite/R/plots.R b/code/MOSuite/R/plots.R
index 81e3ee9..850a199 100644
--- a/code/MOSuite/R/plots.R
+++ b/code/MOSuite/R/plots.R
@@ -79,6 +79,24 @@ print_or_save_plot <- function(
return(invisible(filename))
}
+#' Format hover text for interactive plots
+#'
+#' Builds a character vector of hover labels from one or two metadata columns.
+#' Validates that required columns exist in `plot_data` and raises an error if
+#' any are missing.
+#'
+#' @param plot_data data frame containing plot metadata columns.
+#' @param primary_colname name of the primary column to display in hover text.
+#' @param secondary_colname optional name of a secondary column appended below
+#' the primary label. If `NULL` or not present in `plot_data`, only the
+#' primary label is returned.
+#' @param missing_col_context string describing the plot context, used in the
+#' error message when required columns are absent.
+#' @param require_secondary if `TRUE` and `secondary_colname` is not `NULL`,
+#' the secondary column is also validated as required.
+#'
+#' @return character vector of hover text labels, one per row of `plot_data`.
+#' @keywords internal
format_hover_text <- function(
plot_data,
primary_colname,
@@ -138,7 +156,6 @@ format_hover_text <- function(
#' @return Integer column count for top/bottom legends, or `NULL` when no
#' wrapping should be applied.
#' @keywords internal
-#' @noRd
get_legend_column_count <- function(
labels,
legend_position = "top",
@@ -184,7 +201,6 @@ get_legend_column_count <- function(
#'
#' @return Numeric legend text size.
#' @keywords internal
-#' @noRd
get_legend_text_size <- function(
labels,
legend_text_size = NULL,
@@ -222,7 +238,6 @@ get_legend_text_size <- function(
#'
#' @return A `ggplot2` plot object with colour legend layout applied when needed.
#' @keywords internal
-#' @noRd
add_colour_legend_layout <- function(
plot,
labels,
diff --git a/code/MOSuite/R/render_report.R b/code/MOSuite/R/render_report.R
index 2a4d686..e4c4178 100644
--- a/code/MOSuite/R/render_report.R
+++ b/code/MOSuite/R/render_report.R
@@ -19,6 +19,7 @@
#' @export
#'
#' @examples
+#' \dontrun{
#' render_report(execute_params = list(
#' counts_csv = system.file("extdata", "nidap", "Raw_Counts.csv.gz",
#' package = "MOSuite"),
@@ -38,6 +39,7 @@
#' package = "MOSuite")
#' )
#' )
+#' }
render_report <- function(
qmd_template = system.file("quarto", "report.qmd", package = "MOSuite"),
qmd_src = NULL,
diff --git a/code/MOSuite/codemeta.json b/code/MOSuite/codemeta.json
index 53c9282..9414b48 100644
--- a/code/MOSuite/codemeta.json
+++ b/code/MOSuite/codemeta.json
@@ -32,5 +32,5 @@
"license": "https://spdx.org/licenses/MIT",
"name": "MOSuite: R package for differential multi-omics analysis",
"url": "https://ccbr.github.io/MOSuite/",
- "version": "0.4.0.9000"
+ "version": "0.4.1.9000"
}
diff --git a/code/MOSuite/inst/extdata/run-check-podman.sh b/code/MOSuite/inst/extdata/run-check-podman.sh
new file mode 100755
index 0000000..cedeeda
--- /dev/null
+++ b/code/MOSuite/inst/extdata/run-check-podman.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+# Run `devtools::check()` locally inside the mosuite-minimal container image
+# via podman (e.g. on macOS with Podman Desktop / podman machine).
+# Note: you may need to disable the VPN in order to pull the image.
+#
+# Usage:
+# ./inst/extdata/run-check-podman.sh
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$REPO_ROOT"
+
+IMAGE="docker.io/nciccbr/mosuite-minimal:latest"
+
+if ! command -v podman >/dev/null 2>&1; then
+ echo "Error: podman is not installed or not on \$PATH. Install it from https://podman.io/" >&2
+ exit 1
+fi
+
+if ! podman info >/dev/null 2>&1; then
+ echo "Podman machine is not running. Starting it..." >&2
+ podman machine set --memory 8192 && podman machine start
+fi
+
+echo "Using image: $IMAGE"
+podman pull "$IMAGE"
+
+podman run --rm \
+ -v "$REPO_ROOT":/workspace:Z \
+ -w /workspace \
+ "$IMAGE" \
+ R -e 'devtools::check()'
diff --git a/code/MOSuite/man/add_colour_legend_layout.Rd b/code/MOSuite/man/add_colour_legend_layout.Rd
new file mode 100644
index 0000000..3bc3b2e
--- /dev/null
+++ b/code/MOSuite/man/add_colour_legend_layout.Rd
@@ -0,0 +1,39 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/plots.R
+\name{add_colour_legend_layout}
+\alias{add_colour_legend_layout}
+\title{Add wrapped colour legend layout to a ggplot}
+\usage{
+add_colour_legend_layout(
+ plot,
+ labels,
+ legend_position = "top",
+ ncol = NULL,
+ legend_text_size = 10,
+ max_label_characters_per_row = 45,
+ guide_override_aes = NULL
+)
+}
+\arguments{
+\item{plot}{A \code{ggplot2} plot object.}
+
+\item{labels}{Character vector of legend labels.}
+
+\item{legend_position}{Legend position passed to \code{ggplot2::theme()}.}
+
+\item{ncol}{Optional maximum number of legend columns.}
+
+\item{legend_text_size}{Legend text size used to scale the horizontal space
+estimate. Larger legend text uses fewer columns.}
+
+\item{max_label_characters_per_row}{Approximate total label characters to fit
+on one horizontal legend row.}
+}
+\value{
+A \code{ggplot2} plot object with colour legend layout applied when needed.
+}
+\description{
+Applies a colour guide with a wrapped column count for top and bottom legends.
+Left, right, and hidden legends are returned unchanged.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/build_pca_hover_text.Rd b/code/MOSuite/man/build_pca_hover_text.Rd
new file mode 100644
index 0000000..33edce3
--- /dev/null
+++ b/code/MOSuite/man/build_pca_hover_text.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/plot_pca.R
+\name{build_pca_hover_text}
+\alias{build_pca_hover_text}
+\title{Build hover text for PCA interactive plots}
+\usage{
+build_pca_hover_text(
+ pca_data,
+ sample_id_colname,
+ group_colname,
+ label_colname = NULL
+)
+}
+\arguments{
+\item{pca_data}{data frame of PCA results merged with sample metadata.}
+
+\item{sample_id_colname}{column name containing sample identifiers.}
+
+\item{group_colname}{column name containing sample group labels.}
+
+\item{label_colname}{optional column name to use as the primary hover label
+instead of \code{sample_id_colname}. If \code{NULL}, \code{sample_id_colname} is used.}
+}
+\value{
+character vector of hover text, one entry per row of \code{pca_data}.
+}
+\description{
+Constructs a character vector of hover labels for 2D and 3D PCA plots.
+Delegates to \code{\link[=format_hover_text]{format_hover_text()}} using \code{label_colname} (or
+\code{sample_id_colname} when \code{label_colname} is \code{NULL}) as the primary label
+and \code{group_colname} as the secondary label.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/build_volcano_plot_data.Rd b/code/MOSuite/man/build_volcano_plot_data.Rd
new file mode 100644
index 0000000..9787979
--- /dev/null
+++ b/code/MOSuite/man/build_volcano_plot_data.Rd
@@ -0,0 +1,91 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/plot_volcano_enhanced.R
+\name{build_volcano_plot_data}
+\alias{build_volcano_plot_data}
+\title{Build Volcano Plot Data}
+\usage{
+build_volcano_plot_data(
+ diff_dat,
+ label_col,
+ change_colname,
+ signif_colname,
+ contrast_idx,
+ use_custom_lab,
+ change_lfc_name,
+ change_sig_name,
+ value_to_sort_the_output_dataset,
+ label_significant_features_only,
+ signif_threshold,
+ change_threshold,
+ num_features_to_label,
+ custom_gene_list,
+ label_features,
+ default_label_color,
+ custom_label_color,
+ color_of_non_significant_features,
+ color_of_logfold_change_threshold_line,
+ color_of_features_meeting_only_signif_threshold,
+ color_for_features_meeting_pvalue_and_foldchange_thresholds
+)
+}
+\arguments{
+\item{diff_dat}{Differential-expression results as a data frame.}
+
+\item{label_col}{Name of the label/feature ID column in \code{diff_dat}.}
+
+\item{change_colname}{Character vector of logFC column names, one per
+contrast (e.g. \code{c("B-A_logFC", "C-A_logFC")}). Defaults to \code{NULL}, which
+auto-detects all columns ending in \verb{_logFC}.}
+
+\item{signif_colname}{Character vector of significance column names, one per
+contrast (e.g. \code{c("B-A_adjpval", "C-A_adjpval")}). Defaults to \code{NULL},
+which auto-detects corresponding columns by checking for \verb{_adjpval} first,
+then \verb{_pval}, for each contrast in \code{change_colname}.}
+
+\item{contrast_idx}{Index of the contrast currently being processed.}
+
+\item{use_custom_lab}{If TRUE, uses custom labels for the plot (set by \code{change_sig_name} and \code{change_lfc_name})}
+
+\item{change_lfc_name}{Name for the fold change column in the plot. Default is "log2FC".}
+
+\item{change_sig_name}{Name for the significance column in the plot. Default is "p-value".}
+
+\item{value_to_sort_the_output_dataset}{How to sort the output dataset. Options are "fold-change", "p-value", or
+"t-statistic".}
+
+\item{label_significant_features_only}{If \code{TRUE}, automatic labels are selected only from features that pass both
+the significance and fold-change thresholds.}
+
+\item{signif_threshold}{Numeric significance threshold (p-value or adjusted p-value cutoff). Features meet this
+threshold when their value is less than the cutoff (exclusive). Default: 0.05}
+
+\item{change_threshold}{Numeric value specifying the fold change cutoff for significance (i.e. filters on
+\code{change_colname}). Features meet this threshold when the absolute value is greater than the cutoff (exclusive).
+Default: 1.0}
+
+\item{num_features_to_label}{Number of top features/genes to label in the volcano plot. Default is 30.}
+
+\item{custom_gene_list}{comma-separated string of feature names or IDs to include in the volcano plot.}
+
+\item{label_features}{If \code{TRUE}, only the features specified in \code{custom_gene_list} will be used for labeling in the
+volcano plot, ignoring the top features.}
+
+\item{default_label_color}{Set the color for the text used to add feature labels to points.}
+
+\item{custom_label_color}{Set the color for labels from \code{custom_gene_list}.}
+
+\item{color_of_non_significant_features}{Color of the non-significant features.}
+
+\item{color_of_logfold_change_threshold_line}{Color of the features that meet only the log fold change threshold.}
+
+\item{color_of_features_meeting_only_signif_threshold}{Color of the features that meet only the significance
+threshold.}
+
+\item{color_for_features_meeting_pvalue_and_foldchange_thresholds}{Color of the features that meet both the p-value
+and fold change thresholds.}
+}
+\description{
+Internal helper that prepares one-contrast volcano plotting data, labels,
+and color mappings.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/cli_exec_impl.Rd b/code/MOSuite/man/cli_exec_impl.Rd
new file mode 100644
index 0000000..0bca633
--- /dev/null
+++ b/code/MOSuite/man/cli_exec_impl.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/cli.R
+\name{cli_exec_impl}
+\alias{cli_exec_impl}
+\title{Internal implementation of CLI execution}
+\usage{
+cli_exec_impl(clargs)
+}
+\arguments{
+\item{clargs}{character vector of command-line arguments.}
+}
+\value{
+result of the dispatched MOSuite function call.
+}
+\description{
+Parses command-line arguments and dispatches to the appropriate MOSuite
+function. Called by \code{\link[=cli_exec]{cli_exec()}}.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/cli_help.Rd b/code/MOSuite/man/cli_help.Rd
new file mode 100644
index 0000000..951d9cc
--- /dev/null
+++ b/code/MOSuite/man/cli_help.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/cli.R
+\name{cli_help}
+\alias{cli_help}
+\title{Print help for a CLI method}
+\usage{
+cli_help(method)
+}
+\arguments{
+\item{method}{name of the MOSuite function to show help for.}
+}
+\value{
+result of \code{print(utils::help(...))}, invisibly.
+}
+\description{
+Displays the R help page for the named MOSuite function in the \code{MOSuite}
+package.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/cli_parse.Rd b/code/MOSuite/man/cli_parse.Rd
new file mode 100644
index 0000000..a174e52
--- /dev/null
+++ b/code/MOSuite/man/cli_parse.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/cli.R
+\name{cli_parse}
+\alias{cli_parse}
+\title{Parse a single CLI argument string}
+\usage{
+cli_parse(text)
+}
+\arguments{
+\item{text}{character string to parse.}
+}
+\value{
+parsed R value, or \code{text} unchanged if parsing yields a language
+object.
+}
+\description{
+Converts a raw CLI argument string to an R value. Logical-like strings
+(\code{"true"}, \code{"false"}, etc.) are returned as \code{TRUE}/\code{FALSE}. Other strings
+are parsed as R expressions; if the result is a language object, the
+original string is returned as-is.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/cli_unknown.Rd b/code/MOSuite/man/cli_unknown.Rd
new file mode 100644
index 0000000..5a5ed40
--- /dev/null
+++ b/code/MOSuite/man/cli_unknown.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/cli.R
+\name{cli_unknown}
+\alias{cli_unknown}
+\title{Build an unknown-command error message}
+\usage{
+cli_unknown(method, exports)
+}
+\arguments{
+\item{method}{the unrecognised method name supplied by the user.}
+
+\item{exports}{character vector of exported function names from MOSuite.}
+}
+\value{
+character string with the error message.
+}
+\description{
+Constructs an error message for an unrecognised CLI method name, optionally
+suggesting similar exported function names based on edit distance.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/cli_usage.Rd b/code/MOSuite/man/cli_usage.Rd
new file mode 100644
index 0000000..5fab621
--- /dev/null
+++ b/code/MOSuite/man/cli_usage.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/cli.R
+\name{cli_usage}
+\alias{cli_usage}
+\title{Print CLI usage information}
+\usage{
+cli_usage(con = stderr())
+}
+\arguments{
+\item{con}{connection to write usage text to. Defaults to \code{stderr()}.}
+}
+\value{
+invisibly returns \code{NULL}.
+}
+\description{
+Writes a usage summary for the \code{mosuite} command-line tool to a connection,
+typically \code{stderr()}.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/filter_diff.Rd b/code/MOSuite/man/filter_diff.Rd
index 2a82849..950aa9c 100644
--- a/code/MOSuite/man/filter_diff.Rd
+++ b/code/MOSuite/man/filter_diff.Rd
@@ -43,13 +43,13 @@ used.)}
\item{significance_column}{Column name for significance, e.g. \code{"pval"} or \code{"pvaladj"} (default)}
-\item{significance_cutoff}{Features will only be kept if their \code{significance_column} is less then this cutoff
-threshold}
+\item{significance_cutoff}{Features will only be kept if their \code{significance_column} is less than the cutoff
+threshold (exclusive)}
\item{change_column}{Column name for change, e.g. \code{"logFC"} (default)}
-\item{change_cutoff}{Features will only be kept if the absolute value of their \code{change_column} is greater than or
-equal to this cutoff threshold}
+\item{change_cutoff}{Features will only be kept if the absolute value of their \code{change_column} is greater than the
+cutoff threshold (exclusive)}
\item{filtering_mode}{Accepted values: \code{"any"} or \code{"all"} to include features that meet the criteria in \emph{any}
contrast or in \emph{all} contrasts}
diff --git a/code/MOSuite/man/format_hover_text.Rd b/code/MOSuite/man/format_hover_text.Rd
new file mode 100644
index 0000000..cc843f4
--- /dev/null
+++ b/code/MOSuite/man/format_hover_text.Rd
@@ -0,0 +1,38 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/plots.R
+\name{format_hover_text}
+\alias{format_hover_text}
+\title{Format hover text for interactive plots}
+\usage{
+format_hover_text(
+ plot_data,
+ primary_colname,
+ secondary_colname = NULL,
+ missing_col_context = "plot",
+ require_secondary = TRUE
+)
+}
+\arguments{
+\item{plot_data}{data frame containing plot metadata columns.}
+
+\item{primary_colname}{name of the primary column to display in hover text.}
+
+\item{secondary_colname}{optional name of a secondary column appended below
+the primary label. If \code{NULL} or not present in \code{plot_data}, only the
+primary label is returned.}
+
+\item{missing_col_context}{string describing the plot context, used in the
+error message when required columns are absent.}
+
+\item{require_secondary}{if \code{TRUE} and \code{secondary_colname} is not \code{NULL},
+the secondary column is also validated as required.}
+}
+\value{
+character vector of hover text labels, one per row of \code{plot_data}.
+}
+\description{
+Builds a character vector of hover labels from one or two metadata columns.
+Validates that required columns exist in \code{plot_data} and raises an error if
+any are missing.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/get_legend_column_count.Rd b/code/MOSuite/man/get_legend_column_count.Rd
new file mode 100644
index 0000000..bca4a1f
--- /dev/null
+++ b/code/MOSuite/man/get_legend_column_count.Rd
@@ -0,0 +1,38 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/plots.R
+\name{get_legend_column_count}
+\alias{get_legend_column_count}
+\title{Compute a wrapped colour legend column count}
+\usage{
+get_legend_column_count(
+ labels,
+ legend_position = "top",
+ ncol = NULL,
+ legend_text_size = 10,
+ max_label_characters_per_row = 45
+)
+}
+\arguments{
+\item{labels}{Character vector of legend labels.}
+
+\item{legend_position}{Legend position passed to \code{ggplot2::theme()}.}
+
+\item{ncol}{Optional maximum number of legend columns.}
+
+\item{legend_text_size}{Legend text size used to scale the horizontal space
+estimate. Larger legend text uses fewer columns.}
+
+\item{max_label_characters_per_row}{Approximate total label characters to fit
+on one horizontal legend row.}
+}
+\value{
+Integer column count for top/bottom legends, or \code{NULL} when no
+wrapping should be applied.
+}
+\description{
+Computes a conservative number of legend columns for horizontal ggplot colour
+legends. Top and bottom legends are wrapped based on the number of labels and
+the longest label length. Other legend positions return \code{NULL} so their
+existing ggplot layout is preserved.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/get_legend_text_size.Rd b/code/MOSuite/man/get_legend_text_size.Rd
new file mode 100644
index 0000000..c6897de
--- /dev/null
+++ b/code/MOSuite/man/get_legend_text_size.Rd
@@ -0,0 +1,32 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/plots.R
+\name{get_legend_text_size}
+\alias{get_legend_text_size}
+\title{Compute colour legend text size}
+\usage{
+get_legend_text_size(
+ labels,
+ legend_text_size = NULL,
+ min_legend_text_size = 8,
+ max_legend_text_size = 18
+)
+}
+\arguments{
+\item{labels}{Character vector of legend labels.}
+
+\item{legend_text_size}{Optional explicit legend text size. When supplied,
+this value is returned unchanged.}
+
+\item{min_legend_text_size}{Smallest automatically selected legend text size.}
+
+\item{max_legend_text_size}{Largest automatically selected legend text size.}
+}
+\value{
+Numeric legend text size.
+}
+\description{
+Computes legend text size from legend labels. Short legends keep the larger
+default text used by simple group legends, while longer or denser legends are
+scaled down.
+}
+\keyword{internal}
diff --git a/code/MOSuite/man/plot_volcano_enhanced.Rd b/code/MOSuite/man/plot_volcano_enhanced.Rd
index f2d4491..072328f 100644
--- a/code/MOSuite/man/plot_volcano_enhanced.Rd
+++ b/code/MOSuite/man/plot_volcano_enhanced.Rd
@@ -166,10 +166,12 @@ contrast (e.g. \code{c("B-A_adjpval", "C-A_adjpval")}). Defaults to \code{NULL},
which auto-detects corresponding columns by checking for \verb{_adjpval} first,
then \verb{_pval}, for each contrast in \code{change_colname}.}
-\item{signif_threshold}{Numeric significance threshold (p-value or adjusted p-value cutoff). Default: 0.05}
+\item{signif_threshold}{Numeric significance threshold (p-value or adjusted p-value cutoff). Features meet this
+threshold when their value is less than the cutoff (exclusive). Default: 0.05}
\item{change_threshold}{Numeric value specifying the fold change cutoff for significance (i.e. filters on
-\code{change_colname})}
+\code{change_colname}). Features meet this threshold when the absolute value is greater than the cutoff (exclusive).
+Default: 1.0}
\item{value_to_sort_the_output_dataset}{How to sort the output dataset. Options are "fold-change", "p-value", or
"t-statistic".}
diff --git a/code/MOSuite/man/plot_volcano_summary.Rd b/code/MOSuite/man/plot_volcano_summary.Rd
index 25d5054..fb17416 100644
--- a/code/MOSuite/man/plot_volcano_summary.Rd
+++ b/code/MOSuite/man/plot_volcano_summary.Rd
@@ -169,10 +169,12 @@ per contrast (e.g. \code{c("B-A_adjpval", "C-A_adjpval")}). Defaults to \code{NU
which auto-detects corresponding columns by checking for \verb{_adjpval} first,
then \verb{_pval}, for each contrast in \code{change_colname}.}
-\item{signif_threshold}{Numeric significance threshold (p-value or adjusted p-value cutoff). Default: 0.05}
+\item{signif_threshold}{Numeric significance threshold (p-value or adjusted p-value cutoff). Features meet this
+threshold when their value is less than the cutoff (exclusive). Default: 0.05}
\item{change_threshold}{Numeric value specifying the fold change cutoff for significance (i.e. filters on
-\code{change_colname})}
+\code{change_colname}). Features meet this threshold when the absolute value is greater than the cutoff (exclusive).
+Default: 1.0}
\item{value_to_sort_the_output_dataset}{How to sort the output dataset. Options are "fold-change", "p-value", or
"t-statistic".}
diff --git a/code/MOSuite/man/render_report.Rd b/code/MOSuite/man/render_report.Rd
index e826af6..95ce0eb 100644
--- a/code/MOSuite/man/render_report.Rd
+++ b/code/MOSuite/man/render_report.Rd
@@ -32,6 +32,7 @@ current working directory. You can specify additional arguments to
You can edit the copy of \code{report.qmd} in the current working directory to customize the report.
}
\examples{
+\dontrun{
render_report(execute_params = list(
counts_csv = system.file("extdata", "nidap", "Raw_Counts.csv.gz",
package = "MOSuite"),
@@ -52,3 +53,4 @@ render_report(
)
)
}
+}
diff --git a/code/MOSuite/man/resolve_log_transform_base.Rd b/code/MOSuite/man/resolve_log_transform_base.Rd
new file mode 100644
index 0000000..c2454b3
--- /dev/null
+++ b/code/MOSuite/man/resolve_log_transform_base.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/counts.R
+\name{resolve_log_transform_base}
+\alias{resolve_log_transform_base}
+\title{Resolve the log transform base to a numeric value}
+\usage{
+resolve_log_transform_base(base)
+}
+\arguments{
+\item{base}{a single numeric value or one of \code{"e"}, \code{"ln"}, \code{"natural"}.}
+}
+\value{
+numeric base value suitable for use in \code{log(x, base)}.
+}
+\description{
+Converts string aliases (\code{"e"}, \code{"ln"}, \code{"natural"}) to \code{exp(1)} and
+validates that numeric bases are positive and not equal to 1.
+}
+\keyword{internal}
diff --git a/code/MOSuite/tests/testthat/_snaps/E2E.md b/code/MOSuite/tests/testthat/_snaps/E2E.md
index ac833d9..01ccaf4 100644
--- a/code/MOSuite/tests/testthat/_snaps/E2E.md
+++ b/code/MOSuite/tests/testthat/_snaps/E2E.md
@@ -49,7 +49,7 @@
Setting first column of `counts` as gene annotation.
Total number of genes included: 291
* filtering differential features
- Total number of genes selected with adjpval < 0.05 and | logFC | ≥ 1 is sum(selgenes)
+ Total number of genes selected with adjpval < 0.05 and | logFC | > 1 is sum(selgenes)
# E2E workflow succeeds for NIDAP data
@@ -100,5 +100,5 @@
Joining with `by = join_by(GeneName)`
Joining with `by = join_by(GeneName)`
* filtering differential features
- Total number of genes selected with adjpval < 0.05 and | logFC | ≥ 1 is sum(selgenes)
+ Total number of genes selected with adjpval < 0.05 and | logFC | > 1 is sum(selgenes)
diff --git a/code/MOSuite/tests/testthat/test-differential.R b/code/MOSuite/tests/testthat/test-differential.R
index c2aedf1..580a5be 100644
--- a/code/MOSuite/tests/testthat/test-differential.R
+++ b/code/MOSuite/tests/testthat/test-differential.R
@@ -494,3 +494,36 @@ test_that("filter_diff accepts valid plot_type values", {
moo |> filter_diff(plot_type = "pie")
)
})
+
+test_that("filter_diff excludes exact boundary values using strict comparisons", {
+ options(moo_print_plots = FALSE)
+ boundary_df <- data.frame(
+ Gene = c(
+ "both_boundary",
+ "p_boundary",
+ "fc_boundary",
+ "strict_hit",
+ "neither"
+ ),
+ `B-A_FC` = c(2, 1.414, 2, 2.3, 1.414),
+ `B-A_logFC` = c(1.0, 0.5, 1.0, 1.2, 0.5),
+ `B-A_tstat` = c(2.0, 1.0, 2.0, 2.5, 1.0),
+ `B-A_pval` = c(0.05, 0.05, 0.06, 0.04, 0.06),
+ `B-A_adjpval` = c(0.05, 0.05, 0.06, 0.04, 0.06),
+ check.names = FALSE
+ )
+ moo <- moo_nidap
+ moo@analyses$diff <- list("B-A" = boundary_df)
+ out <- filter_diff(
+ moo,
+ feature_id_colname = "Gene",
+ significance_column = "adjpval",
+ significance_cutoff = 0.05,
+ change_column = "logFC",
+ change_cutoff = 1.0,
+ filtering_mode = "any",
+ print_plots = FALSE,
+ save_plots = FALSE
+ )
+ expect_equal(out@analyses$diff_filt$Gene, "strict_hit")
+})
diff --git a/code/MOSuite/tests/testthat/test-plot_volcano_enhanced.R b/code/MOSuite/tests/testthat/test-plot_volcano_enhanced.R
index 216e500..efe0374 100644
--- a/code/MOSuite/tests/testthat/test-plot_volcano_enhanced.R
+++ b/code/MOSuite/tests/testthat/test-plot_volcano_enhanced.R
@@ -84,6 +84,101 @@ test_that("plot_volcano_enhanced forwards shared styling parameters", {
expect_equal(captured_args$cutoffLineCol, "cyan")
})
+test_that("build_volcano_plot_data colors exact threshold boundaries as non-significant", {
+ diff_dat <- data.frame(
+ Gene = c("both_boundary", "p_boundary", "fc_boundary", "neither"),
+ annotation = c("a", "b", "c", "d"),
+ `B-A_logFC` = c(1, 0.5, 1, 0.5),
+ `B-A_pval` = c(0.05, 0.05, 0.06, 0.06),
+ check.names = FALSE
+ )
+
+ volcano_data <- build_volcano_plot_data(
+ diff_dat = diff_dat,
+ label_col = "Gene",
+ change_colname = "B-A_logFC",
+ signif_colname = "B-A_pval",
+ contrast_idx = 1,
+ use_custom_lab = FALSE,
+ change_lfc_name = "log2FC",
+ change_sig_name = "p-value",
+ value_to_sort_the_output_dataset = "p-value",
+ label_significant_features_only = TRUE,
+ signif_threshold = 0.05,
+ change_threshold = 1,
+ num_features_to_label = 30,
+ custom_gene_list = "",
+ label_features = FALSE,
+ default_label_color = "black",
+ custom_label_color = "black",
+ color_of_non_significant_features = "grey30",
+ color_of_logfold_change_threshold_line = "forestgreen",
+ color_of_features_meeting_only_signif_threshold = "royalblue",
+ color_for_features_meeting_pvalue_and_foldchange_thresholds = "red2"
+ )
+
+ point_colors_by_class <- volcano_data$custom_colors
+ expect_equal(
+ unname(point_colors_by_class[
+ names(point_colors_by_class) == "Not significant"
+ ]),
+ rep("grey30", 4)
+ )
+ expect_true(
+ all(
+ point_colors_by_class[
+ names(point_colors_by_class) == "Significant only"
+ ] ==
+ "royalblue"
+ )
+ )
+ expect_true(
+ all(
+ point_colors_by_class[
+ names(point_colors_by_class) == "Fold change only"
+ ] ==
+ "forestgreen"
+ )
+ )
+})
+
+test_that("plot_volcano_enhanced labels only strict threshold hits", {
+ options(mosuite_test_volcano_label_args = list())
+ trace(
+ EnhancedVolcano::EnhancedVolcano,
+ tracer = quote(options(
+ mosuite_test_volcano_label_args = append(
+ getOption("mosuite_test_volcano_label_args"),
+ list(list(selectLab = selectLab))
+ )
+ )),
+ print = FALSE
+ )
+ on.exit(untrace(EnhancedVolcano::EnhancedVolcano), add = TRUE)
+ on.exit(options(mosuite_test_volcano_label_args = NULL), add = TRUE)
+
+ boundary_data <- data.frame(
+ Gene = c("both_boundary", "p_boundary", "fc_boundary", "neither"),
+ annotation = c("a", "b", "c", "d"),
+ `B-A_logFC` = c(1, 0.5, 1, 0.5),
+ `B-A_pval` = c(0.05, 0.05, 0.06, 0.06),
+ check.names = FALSE
+ )
+
+ plot_volcano_enhanced(
+ boundary_data,
+ feature_id_colname = "Gene",
+ change_colname = "B-A_logFC",
+ signif_colname = "B-A_pval",
+ label_significant_features_only = TRUE,
+ save_plots = FALSE,
+ print_plots = FALSE
+ )
+
+ captured_labels <- getOption("mosuite_test_volcano_label_args")[[1]]$selectLab
+ expect_length(captured_labels, 0)
+})
+
test_that("plot_volcano_enhanced uses EnhancedVolcano default colors", {
options(mosuite_test_volcano_args = list())
trace(
diff --git a/code/MOSuite/tests/testthat/test-plot_volcano_summary.R b/code/MOSuite/tests/testthat/test-plot_volcano_summary.R
index 0cfc92e..9f5e3e8 100644
--- a/code/MOSuite/tests/testthat/test-plot_volcano_summary.R
+++ b/code/MOSuite/tests/testthat/test-plot_volcano_summary.R
@@ -28,6 +28,33 @@ test_that("plot_volcano_summary respects non-Gene feature ID column", {
expect_false("Gene" %in% colnames(df_volc_sum))
})
+test_that("plot_volcano_summary excludes features on exact thresholds", {
+ boundary_data <- data.frame(
+ Gene = c("both_boundary", "p_boundary", "fc_boundary", "neither"),
+ `B-A_logFC` = c(1, 0.5, 1, 0.5),
+ `B-A_pval` = c(0.05, 0.05, 0.06, 0.06),
+ `B-A_tstat` = c(2, 2, 2, 2),
+ check.names = FALSE
+ )
+
+ result <- plot_volcano_summary(
+ boundary_data,
+ feature_id_colname = "Gene",
+ change_colname = "B-A_logFC",
+ signif_colname = "B-A_pval",
+ add_deg_columns = "none",
+ save_plots = FALSE,
+ print_plots = FALSE
+ )
+
+ expect_false(
+ any(
+ c("both_boundary", "p_boundary", "fc_boundary", "neither") %in%
+ result$Gene
+ )
+ )
+})
+
test_that("plot_volcano_summary only forwards custom labels when requested", {
options(mosuite_test_select_labels = list())
trace(