Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
^Makefile$
^make$
^\.githooks$
^CITATION\.cff$
^SKILLS\.md$
13 changes: 6 additions & 7 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ Description: A Shiny application to help users explore and enjoy their 'omics da
License: MIT + file LICENSE
Encoding: UTF-8
Imports:
devtools,
shiny,
shinydashboard,
tidyverse,
Expand Down Expand Up @@ -68,7 +67,9 @@ Imports:
openxlsx,
readr,
tibble,
xml2
xml2,
dendextend,
rlang
Suggests:
DOSE,
testthat (>= 3.0.0),
Expand All @@ -78,12 +79,10 @@ Suggests:
arrow,
knitr,
rmarkdown,
dendextend,
rlang,
withr
withr,
devtools
VignetteBuilder: knitr
Config/testthat/edition: 3
Remotes:
github::scverse/anndataR
Config/roxygen2/version: 8.0.0
RoxygenNote: 7.3.3
Config/roxygen2/version: 8.1.1
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export(build_explore_h5ad)
export(exploreDE)
export(new_explore_dataset)
export(validate_explore_dataset)
importFrom(R6,R6Class)
importFrom(dplyr,"%>%")
importFrom(rlang,.data)
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@
and a minimal Python writer) and `exploreDE-session-2.md` (session files).
- README, the bring-your-own-data vignette, and the AI skill now cover
writing files outside R, opening files from a link, and saving sessions.
- The help pages of the `ExploreDataset*` classes describe every public method and its arguments.

## Removed

- The App Data tab (deprecated).
- `devtools` as a dependency: installing exploreDE no longer installs it. `dendextend` and `rlang`, which the package code calls, move from Suggests to Imports.

# exploreDE 3.8.0

Expand Down
83 changes: 83 additions & 0 deletions R/ExploreDataset.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,74 @@ ExploreDataset <- R6::R6Class(
classname = "ExploreDataset",

public = list(
#' @description Always errors: the base class is abstract. Use [new_explore_dataset()] or a concrete subclass.
#' @param ... Ignored.
initialize = function(...) {
stop("ExploreDataset is abstract. ", "Use new_explore_dataset() or one of the concrete subclasses.")
},

# ---- identity (config-only) -------------------------------------------
#' @description Omics class from the config: `"genomics"` or `"proteomics"`.
omics_class = function() private$.config$omics_class,
#' @description Dataset type from the config, e.g. `"RNASeq"`, `"CountQC"`, `"CrisprCountQC"` or `"proteomics"`.
data_type = function() private$.config$data_type,
#' @description `TRUE` when the dataset has at least one contrast.
has_de = function() length(self$contrasts()) > 0L,
#' @description `TRUE` when the config lists at least one enrichment kind.
has_enrichment = function() {
length(private$.config$enrichment_kinds %||% character()) > 0L
},
#' @description Organism from the config, or `NA` when unknown.
organism = function() private$.config$organism %||% NA_character_,
#' @description Reference genome build from the config, or `NA` when unknown.
ref_build = function() private$.config$ref_build %||% NA_character_,
#' @description What a feature ID denotes, from the config, e.g. `"gene"`, `"isoform"`, `"protein"`,
#' `"peptide"`, `"phospho"` or `"guide"`.
feature_id_kind = function() private$.config$feature_id_kind,
#' @description Low-signal offset added to expression values before taking log2; from the config, default 10.
background_expression = function() {
private$.config$background_expression %||% 10
},
#' @description Short tag naming the storage backend (`"se"`, `"h5ad"` or `"parquet"`). Implemented by subclasses.
backend_kind = function() stop("not implemented"),
#' @description The underlying storage object. Implemented by subclasses.
backend = function() stop("not implemented"),

# ---- samples (samples() virtual; rest derived) ------------------------
#' @description Sample metadata as a data.frame, one row per sample, rownames are sample IDs.
#' Implemented by subclasses.
samples = function() stop("not implemented"),
#' @description Sample IDs: the rownames of `samples()`.
sample_ids = function() rownames(self$samples()),
#' @description Number of samples: the number of rows of `samples()`.
n_samples = function() nrow(self$samples()),
#' @description Names of the experimental factors from the config; empty when there are none.
factor_names = function() private$.config$factors %||% character(),
#' @description Levels of one experimental factor, from the config.
#' @param factor Factor name, one of `factor_names()`.
factor_levels = function(factor) {
private$.config$factor_levels[[factor]]
},
#' @description Name of the factor used as the primary sample grouping, from the config.
primary_grouping = function() private$.config$primary_grouping,
# NA when the dataset has no secondary grouping (e.g. proteomics, or
# SUSHI params with grouping2Name blank).
#' @description Name of the secondary grouping factor, or `NA` when there is none.
secondary_grouping = function() private$.config$secondary_grouping %||% NA_character_,
#' @description One column of `samples()`: the values of a factor, one per sample.
#' @param factor Column name in `samples()`, usually one of `factor_names()`.
sample_factor = function(factor) self$samples()[[factor]],

# ---- features (features() virtual; rest derived) ----------------------
#' @description Feature annotation as a data.frame, one row per feature, rownames are feature IDs, with a
#' `display_name` column. Implemented by subclasses.
features = function() stop("not implemented"),
#' @description Feature IDs: the rownames of `features()`.
feature_ids = function() rownames(self$features()),
#' @description Number of features: the number of rows of `features()`.
n_features = function() nrow(self$features()),
#' @description Display names of features, named by feature ID.
#' @param ids Feature IDs to look up; `NULL` (default) returns all features.
feature_display = function(ids = NULL) {
f <- self$features()
if (is.null(ids)) {
Expand All @@ -62,35 +92,58 @@ ExploreDataset <- R6::R6Class(
},

# ---- assays (assay_names + assay virtual; rest derived) ---------------
#' @description Canonical names of the assays this dataset provides. Implemented by subclasses.
assay_names = function() stop("not implemented"),
#' @description `TRUE` when `name` is one of `assay_names()`.
#' @param name Canonical assay name.
has_assay = function(name) name %in% self$assay_names(),
#' @description One assay as a features x samples matrix, optionally subset. Implemented by subclasses.
#' @param name Canonical assay name, one of `assay_names()`.
#' @param features Feature IDs (rows) to keep; `NULL` keeps all.
#' @param samples Sample IDs (columns) to keep; `NULL` keeps all.
assay = function(name, features = NULL, samples = NULL) {
stop("not implemented")
},
# Human-readable label for an assay (used by UI dropdowns).
# Subclass populates private$.config$assay_labels at build_config() time.
# Falls back to the canonical name if no label is registered.
#' @description Human-readable label for an assay, from the config; `name` itself when no label is registered.
#' @param name Canonical assay name.
assay_label = function(name) {
labels <- private$.config$assay_labels %||% list()
if (name %in% names(labels)) {
return(labels[[name]])
}
name
},
#' @description Canonical name of the raw assay, from the config.
raw_assay_name = function() private$.config$raw_assay_name,
#' @description Canonical name of the primary assay, from the config.
primary_assay_name = function() private$.config$primary_assay_name,
#' @description The raw assay: shortcut for `assay(raw_assay_name(), ...)`.
#' @param ... Passed to `assay()`: `features`, `samples`.
raw_assay = function(...) self$assay(self$raw_assay_name(), ...),
#' @description The primary assay: shortcut for `assay(primary_assay_name(), ...)`.
#' @param ... Passed to `assay()`: `features`, `samples`.
primary_assay = function(...) self$assay(self$primary_assay_name(), ...),

# ---- DE (de() virtual; rest config-only) ------------------------------
#' @description Names of the DE contrasts, from the config; empty when the dataset has no DE results.
contrasts = function() {
names(private$.config$contrast_meta) %||% character()
},
#' @description Name of the primary contrast, from the config; `NA` when there are no contrasts.
primary_contrast = function() private$.config$primary_contrast,
#' @description DE result table for one contrast, as a data.frame. Implemented by subclasses.
#' @param contrast Contrast name, one of `contrasts()`.
de = function(contrast) stop("not implemented"),
#' @description DE result tables of all contrasts, as a list named by contrast.
de_all = function() {
setNames(lapply(self$contrasts(), self$de), self$contrasts())
},
#' @description Metadata list of one contrast from the config: test and reference group, factor, method,
#' thresholds. Errors on an unknown contrast.
#' @param contrast Contrast name, one of `contrasts()`.
contrast_meta = function(contrast) {
stopifnot(contrast %in% self$contrasts())
private$.config$contrast_meta[[contrast]]
Expand All @@ -100,6 +153,8 @@ ExploreDataset <- R6::R6Class(
# _RNASeq$.build_config; supplied directly by prolfqua for proteomics),
# so this helper just validates and returns it. Modules wanting prose
# ("diff vs undiff") for plot titles can `gsub("_vs_", " vs ", x)`.
#' @description Display label of a contrast: currently the contrast name itself. Errors on an unknown contrast.
#' @param contrast Contrast name, one of `contrasts()`.
contrast_label = function(contrast) {
stopifnot(contrast %in% self$contrasts())
contrast
Expand All @@ -108,11 +163,15 @@ ExploreDataset <- R6::R6Class(
# Extra DE columns to surface for display only (beyond log2Ratio/pValue/fdr)
# — e.g. SAINTexpress's SaintScore. Default none; backends declaring a
# mapping (H5AD) override via config. Names are also the DE-table labels.
#' @description Names of extra DE columns shown for display only (e.g. `SaintScore`), from the config;
#' empty by default.
de_display_extras = function() private$.config$de_display_extras %||% character(),

# Does any contrast carry a usable RAW p-value? FALSE for FDR-only producers
# (SAINTexpress), which the p-value-type selectors use to degrade to
# FDR-only rather than plotting all-NA -log10(pValue).
#' @description `TRUE` when the DE table of at least one contrast has a non-missing raw `pValue`;
#' `FALSE` for FDR-only producers such as SAINTexpress.
has_pvalue = function() {
if (!self$has_de()) {
return(FALSE)
Expand All @@ -127,10 +186,18 @@ ExploreDataset <- R6::R6Class(
},

# ---- enrichment (all virtual except enrichment_kinds) -----------------
#' @description Enrichment kinds available, from the config: any of `"ora"` and `"gsea"`.
enrichment_kinds = function() {
private$.config$enrichment_kinds %||% character()
},
#' @description Enrichment results of one contrast and kind, as a flat named list of clusterProfiler results.
#' Implemented by subclasses.
#' @param contrast Contrast name, one of `contrasts()`.
#' @param kind `"ora"` or `"gsea"`.
enrichment = function(contrast, kind) stop("not implemented"),
#' @description Inputs for enrichment of one contrast: `list(seq_anno, ranked_l2fc, selections)`.
#' Implemented by subclasses.
#' @param contrast Contrast name, one of `contrasts()`.
enrichment_input = function(contrast) stop("not implemented"),

# Tidy enrichment as a plain data.frame — the form that does NOT require
Expand All @@ -142,6 +209,10 @@ ExploreDataset <- R6::R6Class(
# file ever written, including those with no uns$enrichment block. Backends
# holding a stored tidy mirror should override and prefer it.
# See plans/exploredelight/enrichment_detach_plan.md.
#' @description Enrichment results of one contrast and kind as one tidy data.frame, derived from
#' `enrichment()`; an empty data.frame when there are none.
#' @param contrast Contrast name, one of `contrasts()`.
#' @param kind `"ora"` or `"gsea"`.
enrichment_table = function(contrast, kind) {
objs <- self$enrichment(contrast, kind)
if (!length(objs)) {
Expand All @@ -166,12 +237,18 @@ ExploreDataset <- R6::R6Class(
# were derived live and nothing was filtered. Callers that let a user raise
# a GO threshold need this: without it an absent row is ambiguous between
# "cut by the writer", "not significant" and "never run".
#' @description The `p.adjust` cut-off applied to the stored enrichment tables, from the config;
#' `NA` when none was applied.
enrichment_filter = function() {
private$.config$enrichment_filter %||% NA_real_
},

# ---- domain extras (config-only) --------------------------------------
#' @description Named character vector mapping sgRNA IDs to gene names (CRISPR datasets); `NULL` otherwise.
sgrna_to_gene = function() private$.config$sgrna_to_gene,
#' @description Choices and lookup for the proteomics feature picker, `list(choices, lookup)`; `NULL` when the
#' dataset has none.
#' @param kind `"gene"` (default) or `"protein"`.
proteomics_choices = function(kind = c("gene", "protein")) {
kind <- match.arg(kind)
private$.config$proteomics[[kind]]
Expand All @@ -183,6 +260,8 @@ ExploreDataset <- R6::R6Class(
# the GO BP / GO MF / GO CC columns in features(), so works for any
# backend. Returns character(0) when those columns are absent (proteomics,
# CountQC without GO annotations).
#' @description GO terms named in the `GO BP`, `GO MF` and `GO CC` columns of `features()`, as
#' `"GO:ID term name"` strings; empty when those columns are absent.
all_pathways = function() {
f <- self$features()
go_cols <- intersect(c("GO BP", "GO MF", "GO CC"), colnames(f))
Expand All @@ -209,9 +288,13 @@ ExploreDataset <- R6::R6Class(
},

# ---- provenance --------------------------------------------------------
#' @description Provenance list from the config: backend kind, schema version and build time.
source = function() private$.config$source,

# ---- lifecycle ---------------------------------------------------------
#' @description Print a summary of the dataset (type, dimensions, factors, contrasts, assays, enrichment)
#' and return the object invisibly.
#' @param ... Ignored.
print = function(...) {
cat("<", class(self)[1], " (", self$backend_kind(), ")>\n", sep = "")
cat(" data_type: ", self$data_type(), "\n", sep = "")
Expand Down
32 changes: 32 additions & 0 deletions R/ExploreDatasetH5AD.R
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ ExploreDatasetH5AD <- R6::R6Class(
inherit = ExploreDataset,

public = list(
#' @description Read a canonical `.h5ad` or take a live AnnData, build the config from it unless one is given,
#' and prepare the feature and DE tables.
#' @param input An `anndataR` AnnData object, or a path to an existing `.h5ad` file.
#' @param config Optional pre-built config list; built from the AnnData when `NULL`.
initialize = function(input, config = NULL) {
adata <- if (inherits(input, "AbstractAnnData")) {
input
Expand All @@ -131,11 +135,16 @@ ExploreDatasetH5AD <- R6::R6Class(
invisible(self)
},

#' @description The AnnData parts read at construction, as `list(obs, var, layers, varm, uns)` of plain R
#' objects.
backend = function() {
list(obs = private$.obs, var = private$.var, layers = private$.layers, varm = private$.varm, uns = private$.uns)
},
#' @description Returns `"h5ad"`.
backend_kind = function() "h5ad",

#' @description Sample metadata from `obs`, with the `factor_` prefix stripped from column names and a `Name`
#' column added when missing.
samples = function() {
cd <- private$.obs
colnames(cd) <- .h5ad_strip_factor(colnames(cd))
Expand All @@ -153,13 +162,20 @@ ExploreDatasetH5AD <- R6::R6Class(
cd
},

#' @description Feature annotation from `var`, with `feature_id` and `display_name` columns, ordered by the
#' p-value of the primary contrast.
features = function() private$.features_df,

#' @description DE table of one contrast from `varm`, with the legacy aliases `log2Ratio`, `pValue` and `fdr`
#' added; an empty data.frame when the slot is missing. Errors on an unknown contrast.
#' @param contrast Contrast name, one of `contrasts()`.
de = function(contrast) {
stopifnot(contrast %in% self$contrasts())
private$.de_dfs[[contrast]] %||% data.frame()
},

#' @description Canonical assay names from the config's assay map whose layer exists; computed assays are
#' always included.
assay_names = function() {
keys <- names(private$.config$assay_map %||% list())
keys[vapply(
Expand All @@ -172,6 +188,11 @@ ExploreDatasetH5AD <- R6::R6Class(
)]
},

#' @description One assay as a features x samples matrix (the layer transposed); `normalised`,
#' `normalised_log2`, `vst`, `fpkm` and `tpm` are computed when the config maps them to no layer.
#' @param name Canonical assay name, one of `assay_names()`.
#' @param features Feature IDs (rows) to keep; `NULL` keeps all.
#' @param samples Sample IDs (columns) to keep; `NULL` keeps all.
assay = function(name, features = NULL, samples = NULL) {
layer_key <- private$.config$assay_map[[name]]
m <- if (!is.null(layer_key)) {
Expand Down Expand Up @@ -199,6 +220,10 @@ ExploreDatasetH5AD <- R6::R6Class(
m
},

#' @description Enrichment results of one contrast and kind: decoded from the serialised objects in older files,
#' otherwise rebuilt from `uns$enrichment` and `uns$enrichment_support`.
#' @param contrast Contrast name, one of `contrasts()`.
#' @param kind `"ora"` or `"gsea"`.
enrichment = function(contrast, kind) {
stopifnot(kind %in% c("ora", "gsea"))
stopifnot(contrast %in% self$contrasts())
Expand Down Expand Up @@ -226,6 +251,10 @@ ExploreDatasetH5AD <- R6::R6Class(
# Prefer the stored tidy mirror; fall back to the base class, which derives
# it from the native objects. So a new file costs no decode at all, and an
# old one still works.
#' @description Tidy enrichment table of one contrast and kind: the table stored in `uns$enrichment` when
#' present, otherwise derived from `enrichment()` as in the base class.
#' @param contrast Contrast name, one of `contrasts()`.
#' @param kind `"ora"` or `"gsea"`.
enrichment_table = function(contrast, kind) {
stopifnot(kind %in% c("ora", "gsea"))
slot <- sub("^de_", "", private$.contrast_slot[[contrast]] %||% contrast)
Expand All @@ -238,6 +267,9 @@ ExploreDatasetH5AD <- R6::R6Class(

# Canonical spec drops enrichInput; reconstruct on demand from varm + var
# so the (accessor-migrated) pathway modules have what they need.
#' @description Enrichment inputs rebuilt from the DE table: `features()`, log2 fold changes sorted decreasing,
#' and up/down feature selections at the contrast's FDR and log2FC thresholds.
#' @param contrast Contrast name, one of `contrasts()`.
enrichment_input = function(contrast) {
de <- tryCatch(self$de(contrast), error = function(e) NULL)
if (is.null(de) || !nrow(de)) {
Expand Down
Loading
Loading