diff --git a/R/plots_crossmodel.R b/R/plots_crossmodel.R
index 6c7675e..49c6751 100644
--- a/R/plots_crossmodel.R
+++ b/R/plots_crossmodel.R
@@ -1,12 +1,21 @@
# Cross-model holdout visualisations.
-# makeCrossModelFeatureImportancePlot: heatmap of top features for holdout models.
-# top_data: pre-loaded top-features tibble (country or year stratified rows).
-# amRml column mapping:
-# drug_or_class -> drug/class abbreviation
-# strat_label -> "country" or "year"
-# strat_value -> trained-on country/year
+#' Holdout-model feature-importance heatmap
+#'
+#' Heatmap of top features across the stratified (country or year) holdout
+#' models, column-wise min-max normalised. Uses the amRml columns drug_or_class,
+#' strat_label ("country"/"year"), and strat_value (trained-on stratum).
+#'
+#' @param top_data Top-features tibble (country/year stratified rows).
+#' @param bug Species code(s) to include.
+#' @param drug Drug/class identifier(s) to include.
+#' @param cross_model "country" or "time".
+#' @param top_n_features Number of features per stratum, or "all".
+#' @param annotated_dir Currently unused; kept for call-site consistency.
+#' @return A plotly heatmap, or NULL when there is nothing to plot.
+#' @keywords internal
+#' @noRd
makeCrossModelFeatureImportancePlot <- function(
top_data, bug, drug, cross_model, top_n_features,
annotated_dir = NULL
@@ -41,7 +50,7 @@ makeCrossModelFeatureImportancePlot <- function(
}
vi_wider <- features_df |>
- dplyr::select(.data$Variable, .data$Importance, !!rlang::sym(strat_col)) |>
+ dplyr::select("Variable", "Importance", !!rlang::sym(strat_col)) |>
tidyr::pivot_wider(
names_from = strat_col,
values_from = "Importance",
@@ -65,9 +74,6 @@ makeCrossModelFeatureImportancePlot <- function(
(x - rng[1]) / diff(rng)
})
- max_val <- max(vi_mat, na.rm = TRUE)
- min_val <- min(vi_mat, na.rm = TRUE)
-
plotly::plot_ly(
x = colnames(vi_mat),
y = rownames(vi_mat),
@@ -89,9 +95,18 @@ makeCrossModelFeatureImportancePlot <- function(
}
-# makeCrossModelRidgePlot: balanced accuracy by drug class for holdout models,
-# coloured by Same (self-eval) vs Different (cross-eval).
-# cross_model: "country" or "time"
+#' Balanced-accuracy box plot for holdout models
+#'
+#' Balanced accuracy by drug class, coloured by Same (self-evaluation) vs
+#' Different (cross-evaluation) test strata.
+#'
+#' @param perf_data Performance tibble (country/year stratified rows).
+#' @param bug Species code(s) to include.
+#' @param cross_model "country" or "time".
+#' @return A horizontal plotly box plot (empty placeholder when there is no
+#' matching data).
+#' @keywords internal
+#' @noRd
makeCrossModelRidgePlot <- function(perf_data, bug, cross_model) {
if (is.null(perf_data) || !is.data.frame(perf_data) || !nrow(perf_data)) {
return(plotly::plot_ly() |>
@@ -167,13 +182,20 @@ makeCrossModelRidgePlot <- function(perf_data, bug, cross_model) {
}
-# makeCrossModelPerformancePlot: heatmap of balanced accuracy for holdout models.
-# perf_data: pre-loaded performance tibble (country or year stratified rows).
-# amRml column mapping:
-# drug_or_class -> drug/class abbreviation
-# strat_label -> "country" or "year"
-# strat_value -> trained-on country/year
-# strat_value_test -> tested-on country/year (NA for self-evaluation)
+#' Cross-stratum balanced-accuracy heatmap
+#'
+#' Train-on vs test-on heatmap of mean balanced accuracy for the holdout models.
+#' Self-evaluation rows (strat_value_test is NA) are treated as tested on the
+#' trained stratum. Uses the amRml columns drug_or_class, strat_label, and
+#' strat_value / strat_value_test.
+#'
+#' @param perf_data Performance tibble (country/year stratified rows).
+#' @param bug Species code(s) to include.
+#' @param drug Drug/class identifier(s) to include.
+#' @param cross_model "country" or "time".
+#' @return A plotly heatmap, or NULL when there is nothing to plot.
+#' @keywords internal
+#' @noRd
makeCrossModelPerformancePlot <- function(perf_data, bug, drug, cross_model) {
if (is.null(perf_data) || !is.data.frame(perf_data) || !nrow(perf_data)) {
return(NULL)
@@ -215,9 +237,6 @@ makeCrossModelPerformancePlot <- function(perf_data, bug, drug, cross_model) {
return(NULL)
}
- min_val <- min(models_performance, na.rm = TRUE)
- max_val <- max(models_performance, na.rm = TRUE)
-
plotly::plot_ly(
x = colnames(models_performance),
y = rownames(models_performance),
diff --git a/R/plots_featureimportance.R b/R/plots_featureimportance.R
index 9e276d4..9d3bb24 100644
--- a/R/plots_featureimportance.R
+++ b/R/plots_featureimportance.R
@@ -1,15 +1,51 @@
# Feature importance visualisations.
-# makeFeatureImportancePlot: heatmap of top features across bugs or drugs.
-# data: pre-loaded top-features tibble from loadTopFeat() / topFeatures()
-# amRml column mapping (new -> expected here):
-# drug_or_class -> drug/class abbreviation identifier
-# feature_subtype -> data encoding (binary/counts)
-# feature_type -> molecular scale for baseline (genes/domains/proteins/struct)
-# strat_label -> NA for baseline models
-# Annotation join is attempted from results_root/Annotated/ or extdata/Annotated/;
-# if no annotated files found, Variable name is used directly as feature label.
+#' Rank wide importance rows and convert to a labelled matrix
+#'
+#' Orders rows by coverage (number of non-NA groups) then peak importance, drops
+#' the scratch ranking columns, and returns a feature x group numeric matrix.
+#'
+#' @param vi_wider Wide importance tibble with a `COG_name` column.
+#' @param group_cols Names of the group (value) columns.
+#' @return A numeric matrix with features as row names and groups as columns.
+#' @keywords internal
+#' @noRd
+.vi_matrix <- function(vi_wider, group_cols) {
+ vi_wider |>
+ dplyr::rowwise() |>
+ dplyr::mutate(
+ .n = sum(!is.na(c_across(all_of(group_cols)))),
+ .mx = max(c_across(all_of(group_cols)), na.rm = TRUE)
+ ) |>
+ dplyr::ungroup() |>
+ dplyr::arrange(dplyr::desc(.data$.n), dplyr::desc(.data$.mx)) |>
+ dplyr::select(-c(".n", ".mx")) |>
+ tibble::column_to_rownames("COG_name") |>
+ as.matrix()
+}
+
+
+#' Feature-importance heatmap across species or drugs
+#'
+#' Heatmap of top features across bugs (across_bug) or drugs (across_drug),
+#' min-max normalised within each group. Feature labels come from an annotation
+#' join when available, otherwise the raw Variable id. Uses the amRml columns
+#' drug_or_class, feature_subtype (encoding), feature_type (scale), and
+#' strat_label (NA for baseline).
+#'
+#' @param data Top-features tibble from loadTopFeat() / topFeatures().
+#' @param bug Species code(s) to include.
+#' @param amr_drug Drug/class identifier(s) to include.
+#' @param model_scale Molecular scale (feature_type) to include.
+#' @param data_type_ Data encoding(s) (feature_subtype) to include.
+#' @param top_n_features Number of features per group, or "all".
+#' @param feature_importance_tabset "across_bug" or "across_drug".
+#' @param annotated_dir Optional directory of annotated parquet files.
+#' @param amrdata_root,results_root Optional roots for the name-map lookup.
+#' @return A plotly heatmap, or NULL when there is nothing to plot.
+#' @keywords internal
+#' @noRd
makeFeatureImportancePlot <- function(
data, bug, amr_drug, model_scale, data_type_,
top_n_features, feature_importance_tabset,
@@ -183,7 +219,7 @@ makeFeatureImportancePlot <- function(
# Build wide matrix
if (feature_importance_tabset == "across_bug") {
vi_wider <- top_features_df |>
- dplyr::select(.data$COG_name, .data$Importance, .data$species) |>
+ dplyr::select("COG_name", "Importance", "species") |>
dplyr::distinct() |>
tidyr::pivot_wider(names_from = "species", values_from = "Importance")
@@ -192,19 +228,7 @@ makeFeatureImportancePlot <- function(
return(NULL)
}
- vi_wider <- vi_wider |>
- dplyr::rowwise() |>
- dplyr::mutate(
- .n = sum(!is.na(c_across(all_of(group_cols)))),
- .mx = max(c_across(all_of(group_cols)), na.rm = TRUE)
- ) |>
- dplyr::ungroup() |>
- dplyr::arrange(dplyr::desc(.data$.n), dplyr::desc(.data$.mx)) |>
- dplyr::select(-.data$.n, -.data$.mx)
-
- vi_mat <- vi_wider |>
- tibble::column_to_rownames("COG_name") |>
- as.matrix()
+ vi_mat <- .vi_matrix(vi_wider, group_cols)
eskape_order <- c("Efa", "Sau", "Kpn", "Aba", "Pae", "Esp")
col_ord <- intersect(eskape_order, colnames(vi_mat))
@@ -213,41 +237,32 @@ makeFeatureImportancePlot <- function(
if (feature_importance_tabset == "across_drug") {
top_features_df <- top_features_df |>
- dplyr::mutate(drug_or_class = stringr::str_trim(as.character(.data$drug_or_class)))
+ dplyr::mutate(
+ drug_or_class = stringr::str_trim(as.character(.data$drug_or_class))
+ )
vi_wider <- top_features_df |>
- dplyr::select(.data$COG_name, .data$Importance, .data$drug_or_class) |>
+ dplyr::select("COG_name", "Importance", "drug_or_class") |>
dplyr::group_by(.data$COG_name, .data$drug_or_class) |>
- dplyr::summarise(Importance = max(.data$Importance, na.rm = TRUE), .groups = "drop") |>
- tidyr::pivot_wider(names_from = "drug_or_class", values_from = "Importance")
+ dplyr::summarise(
+ Importance = max(.data$Importance, na.rm = TRUE), .groups = "drop"
+ ) |>
+ tidyr::pivot_wider(
+ names_from = "drug_or_class", values_from = "Importance"
+ )
group_cols <- setdiff(colnames(vi_wider), "COG_name")
if (!length(group_cols)) {
return(NULL)
}
- vi_wider <- vi_wider |>
- dplyr::rowwise() |>
- dplyr::mutate(
- .n = sum(!is.na(c_across(all_of(group_cols)))),
- .mx = max(c_across(all_of(group_cols)), na.rm = TRUE)
- ) |>
- dplyr::ungroup() |>
- dplyr::arrange(dplyr::desc(.data$.n), dplyr::desc(.data$.mx)) |>
- dplyr::select(-.data$.n, -.data$.mx)
-
- vi_mat <- vi_wider |>
- tibble::column_to_rownames("COG_name") |>
- as.matrix()
+ vi_mat <- .vi_matrix(vi_wider, group_cols)
}
if (!exists("vi_mat") || !length(vi_mat)) {
return(NULL)
}
- max_val <- max(vi_mat, na.rm = TRUE)
- min_val <- min(vi_mat, na.rm = TRUE)
-
plotly::plot_ly(
x = colnames(vi_mat),
y = rownames(vi_mat),
@@ -269,9 +284,18 @@ makeFeatureImportancePlot <- function(
}
-# makeCogBarChart: horizontal bar chart of the most common COGs across the
-# features in a top-features tibble (already enriched with annotations).
-# top_n: number of COGs to display.
+#' Horizontal bar chart of the most common COGs
+#'
+#' Counts COG occurrences across the features in an annotation-enriched
+#' top-features tibble and shows the `top_n` most frequent.
+#'
+#' @param enriched_tbl Annotation-enriched top-features tibble (needs a `COG`
+#' column; see enrich_with_annotations()).
+#' @param top_n Number of COGs to display.
+#' @return A horizontal plotly bar chart (empty placeholder when there are no
+#' annotations).
+#' @keywords internal
+#' @noRd
makeCogBarChart <- function(enriched_tbl, top_n = 15) {
if (is.null(enriched_tbl) || !nrow(enriched_tbl) ||
!"COG" %in% names(enriched_tbl)) {
@@ -343,6 +367,40 @@ makeCogBarChart <- function(enriched_tbl, top_n = 15) {
}
+#' Render comma-separated ids as HTML links
+#'
+#' @param ids A single string of comma-separated ids (or NA).
+#' @param make_url Function mapping one id to its URL.
+#' @return The ids rendered as comma-separated `` links, or `ids` unchanged
+#' when empty/NA.
+#' @keywords internal
+#' @noRd
+.link_ids <- function(ids, make_url) {
+ if (is.na(ids) || !nzchar(ids)) {
+ return(ids)
+ }
+ parts <- trimws(strsplit(ids, ",", fixed = TRUE)[[1]])
+ linked <- vapply(parts, function(id) {
+ paste0(
+ "",
+ id, ""
+ )
+ }, character(1))
+ paste(linked, collapse = ", ")
+}
+
+
+#' Build an interactive feature-importance table
+#'
+#' Formats numeric columns, reorders to a preferred column order, and renders
+#' clickable links (accession -> NCBI, cluster -> BV-BRC, COG -> NCBI COG) as a
+#' DT datatable.
+#'
+#' @param feature_import_table Feature-importance tibble to display.
+#' @return A `DT::datatable` HTML widget.
+#' @keywords internal
+#' @noRd
makeFeatureImportTable <- function(feature_import_table) {
# Early exit for empty/zero-column data
if (is.null(feature_import_table) || ncol(feature_import_table) == 0) {
@@ -382,46 +440,24 @@ makeFeatureImportTable <- function(feature_import_table) {
TRUE ~ .data$accession
))
}
- # Link cluster (fig IDs) to BVBRC, one per unique id (comma-sep cells).
+ # Link cluster (fig IDs) to BV-BRC, one per unique id (comma-sep cells).
if ("cluster" %in% names(tbl)) {
- link_fig <- function(ids) {
- if (is.na(ids) || !nzchar(ids)) {
- return(ids)
- }
- parts <- trimws(strsplit(ids, ",", fixed = TRUE)[[1]])
- linked <- vapply(parts, function(id) {
- url <- paste0(
+ tbl$cluster <- vapply(tbl$cluster, function(ids) {
+ .link_ids(ids, function(id) {
+ paste0(
"https://www.bv-brc.org/view/Feature/",
utils::URLencode(id, reserved = TRUE)
)
- paste0(
- "",
- id, ""
- )
- }, character(1))
- paste(linked, collapse = ", ")
- }
- tbl$cluster <- vapply(tbl$cluster, link_fig, character(1))
+ })
+ }, character(1))
}
- # Link COG ids (comma-separated) to NCBI COG page.
+ # Link COG ids (comma-separated) to the NCBI COG page.
if ("COG" %in% names(tbl)) {
- link_cog <- function(ids) {
- if (is.na(ids) || !nzchar(ids)) {
- return(ids)
- }
- parts <- trimws(strsplit(ids, ",", fixed = TRUE)[[1]])
- linked <- vapply(parts, function(id) {
- paste0(
- "",
- id, ""
- )
- }, character(1))
- paste(linked, collapse = ", ")
- }
- tbl$COG <- vapply(tbl$COG, link_cog, character(1))
+ tbl$COG <- vapply(tbl$COG, function(ids) {
+ .link_ids(ids, function(id) {
+ paste0("https://www.ncbi.nlm.nih.gov/research/cog/cog/", id)
+ })
+ }, character(1))
}
DT::datatable(
diff --git a/R/plots_metadata.R b/R/plots_metadata.R
index 26c9dfd..559666c 100644
--- a/R/plots_metadata.R
+++ b/R/plots_metadata.R
@@ -1,7 +1,18 @@
# Metadata tab visualisations.
-quickStatBox <- function(title, value, icon_name, bg_color, text_color = "white") {
+#' Styled summary-statistic card
+#'
+#' @param title Caption shown beneath the value.
+#' @param value Value (string or tag) shown prominently.
+#' @param icon_name Font Awesome icon name.
+#' @param bg_color Background color.
+#' @param text_color Text color (default "white").
+#' @return A shiny `div` styled as a stat card.
+#' @keywords internal
+#' @noRd
+quickStatBox <- function(title, value, icon_name, bg_color,
+ text_color = "white") {
div(
style = glue::glue("
background-color: {bg_color};
@@ -34,7 +45,16 @@ quickStatBox <- function(title, value, icon_name, bg_color, text_color = "white"
}
-makeQuickStats <- function(data) { # , drug_class_df, spp_name, amr_drugs) {
+#' Summary statistic cards for the metadata tab
+#'
+#' Builds the data-summary header: totals (records, genomes, drugs, classes,
+#' resistant/susceptible isolates) and the top-5 drugs, classes, and countries.
+#'
+#' @param data Metadata tibble (one row per genome-drug record).
+#' @return A shiny `tagList` of stat cards.
+#' @keywords internal
+#' @noRd
+makeQuickStats <- function(data) {
data_with_drug_class <- data
# Sample stat calculations
total_genomes <- nrow(data_with_drug_class)
@@ -118,6 +138,12 @@ makeQuickStats <- function(data) { # , drug_class_df, spp_name, amr_drugs) {
}
+#' Stacked bar of isolate counts by drug and phenotype
+#'
+#' @param data Metadata tibble.
+#' @return A plotly stacked bar chart.
+#' @keywords internal
+#' @noRd
makeDatAvailabilityPlot <- function(data) {
data <- data |>
dplyr::group_by(genome_drug.antibiotic, genome_drug.resistant_phenotype) |>
@@ -158,6 +184,12 @@ makeDatAvailabilityPlot <- function(data) {
}
+#' World choropleth of genome counts by country
+#'
+#' @param data Tibble with `genome.isolation_country` and `count` columns.
+#' @return A plotly choropleth map.
+#' @keywords internal
+#' @noRd
makeGeoChloroPlot <- function(data) {
data$iso3 <- countrycode::countrycode(data$genome.isolation_country, origin = "country.name", destination = "iso3c")
plot_ly(
@@ -190,6 +222,13 @@ makeGeoChloroPlot <- function(data) {
}
+#' Resistance trend over collection year
+#'
+#' @param data Tibble with collection year, phenotype, and isolate counts.
+#' @param amr_drug Drug to title the plot with, or "all".
+#' @return A plotly line/point time series.
+#' @keywords internal
+#' @noRd
makeTimeSeriesAMRPlot <- function(data, amr_drug) {
whole_data_title <- stringr::str_glue(
"AMR resistance trend"
@@ -238,6 +277,12 @@ makeTimeSeriesAMRPlot <- function(data, amr_drug) {
}
+#' Stacked bar of isolate counts by drug and host
+#'
+#' @param data Metadata tibble.
+#' @return A plotly stacked bar chart.
+#' @keywords internal
+#' @noRd
makeHostIsolatePlot <- function(data) {
data <- data |>
dplyr::mutate(
@@ -284,6 +329,14 @@ makeHostIsolatePlot <- function(data) {
}
+#' Stacked bar of isolate counts by drug and isolation source
+#'
+#' Keeps the top 10 isolation sources and groups the rest into "Other".
+#'
+#' @param data Metadata tibble.
+#' @return A plotly stacked bar chart.
+#' @keywords internal
+#' @noRd
makeIsolationSourcesPlot <- function(data) {
isolation_source <- data |>
dplyr::mutate(genome.isolation_source = stringr::str_to_lower(genome.isolation_source)) |>
@@ -344,12 +397,20 @@ makeIsolationSourcesPlot <- function(data) {
}
-# makeMetadataSankey: multi-tier sankey of resistance flow:
-# phenotype -> drug class -> antibiotic -> country -> host -> isolation source.
-# Filters to a chosen drug class (or top N classes) to keep the diagram
-# legible, since unfiltered metadata has too many flows.
-# data: metadata tibble (one row per genome-drug record)
-# drug_classes: vector of drug class names to keep (NULL = top 3 by count)
+#' Multi-tier resistance-flow Sankey
+#'
+#' Sankey of resistance flow:
+#' phenotype -> drug class -> antibiotic -> country -> host -> isolation source.
+#' Filters to the chosen drug classes (or the top `max_classes` by count) to
+#' keep the diagram legible, since unfiltered metadata has too many flows.
+#'
+#' @param data Metadata tibble (one row per genome-drug record).
+#' @param drug_classes Drug classes to keep (NULL = top `max_classes` by count).
+#' @param max_classes Number of top drug classes to keep when none are given.
+#' @return A `networkD3` sankeyNetwork widget, or NULL when prerequisites are
+#' missing.
+#' @keywords internal
+#' @noRd
makeMetadataSankey <- function(data, drug_classes = NULL,
max_classes = 3) {
if (!requireNamespace("networkD3", quietly = TRUE)) {
diff --git a/R/plots_modelperf.R b/R/plots_modelperf.R
index c63f0fb..d88c2a3 100644
--- a/R/plots_modelperf.R
+++ b/R/plots_modelperf.R
@@ -1,11 +1,24 @@
# Model performance tab visualisations.
-# makeModelPerformancePlot: plot baseline ML model performance metrics.
-# data: pre-loaded performance tibble from loadMLResults() / queryData()
-# Columns used from amRml parquet schema:
-# species, feature_type (scale), feature_subtype (data type),
-# drug_or_class (drug/class abbrev), drug_label ("drug"/"drug_class"), nmcc/bal_acc/f1
+#' Baseline ML model performance plot
+#'
+#' Grouped box plots of a performance metric by species and molecular scale,
+#' restricted to baseline (non-stratified) models. Uses the amRml parquet
+#' columns species, feature_type (scale), feature_subtype (data type),
+#' drug_or_class, drug_label, and the metric columns (nmcc/bal_acc/f1).
+#'
+#' @param data Performance tibble from loadMLResults() / queryData().
+#' @param bug Species code(s) to include.
+#' @param model_scale Molecular scale(s) (feature_type) to include.
+#' @param data_type Data encoding(s) (feature_subtype) to include.
+#' @param metrics Name of the metric column to plot on the y-axis.
+#' @param amr_drug_class Selected drug class(es), or "all" for no filter.
+#' @param amr_drug Selected drug(s); points for these are overlaid.
+#' @return A plotly box-plot figure (an empty placeholder when there is no
+#' matching data).
+#' @keywords internal
+#' @noRd
makeModelPerformancePlot <- function(
data, bug, model_scale, data_type, metrics,
amr_drug_class, amr_drug
@@ -123,9 +136,35 @@ makeModelPerformancePlot <- function(
}
-# .prep_nmcc_data: shared filter used by the Performance Overview plots.
-# Drops stratified / cross-test rows so we only show baseline models, and
-# adds a species_display column (uses species_label when present).
+#' Shared scale-label display map and matching colors
+#'
+#' @return A list with `labels` (feature_type -> display label), `order` (the
+#' feature_type keys in display order), and `colors` (SCALE_COLORS keyed by
+#' display label).
+#' @keywords internal
+#' @noRd
+.scale_label_map <- function() {
+ labels <- c(
+ domains = "Domain", genes = "Gene",
+ proteins = "Protein", struct = "Struct"
+ )
+ list(
+ labels = labels,
+ order = names(labels),
+ colors = setNames(unname(SCALE_COLORS[names(labels)]), labels)
+ )
+}
+
+
+#' Prepare baseline nMCC data for the Performance overview plots
+#'
+#' Drops stratified / cross-test rows so only baseline models remain, and adds a
+#' `species_display` column (from `species_label` when present).
+#'
+#' @param data Performance tibble from loadMLResults().
+#' @return A filtered tibble with `species_display`, or NULL when empty.
+#' @keywords internal
+#' @noRd
.prep_nmcc_data <- function(data) {
if (is.null(data) || !is.data.frame(data) || !nrow(data)) {
return(NULL)
@@ -147,25 +186,29 @@ makeModelPerformancePlot <- function(
}
-# makeNmccStripPlot: facetted nMCC distribution per species and molecular
-# scale on the Performance overview tab. Highlights the user's selected
-# drug or drug class via point alpha + size; baseline (non-stratified,
-# non-cross-test) rows only via .prep_nmcc_data().
-makeNmccStripPlot <- function(data, selected_drug_class = NULL, selected_drug = NULL) {
+#' Facetted nMCC strip plot (Performance overview)
+#'
+#' nMCC distribution per species and molecular scale, highlighting the selected
+#' drug or drug class via point alpha + size. Baseline rows only (via
+#' .prep_nmcc_data()).
+#'
+#' @param data Performance tibble from loadMLResults().
+#' @param selected_drug_class Drug class to highlight, "all"/NULL for none.
+#' @param selected_drug Drug to highlight (takes priority over the class).
+#' @return A plotly figure (empty placeholder when there is no matching data).
+#' @keywords internal
+#' @noRd
+makeNmccStripPlot <- function(data, selected_drug_class = NULL,
+ selected_drug = NULL) {
df <- .prep_nmcc_data(data)
if (is.null(df)) {
return(plotly::plot_ly() |> plotly::layout(title = "No data available"))
}
- scale_labels <- c(
- domains = "Domain", genes = "Gene",
- proteins = "Protein", struct = "Struct"
- )
- scale_order <- names(scale_labels)
- scale_colors <- setNames(
- unname(SCALE_COLORS[scale_order]),
- scale_labels
- )
+ sl <- .scale_label_map()
+ scale_labels <- sl$labels
+ scale_order <- sl$order
+ scale_colors <- sl$colors
df <- df |>
dplyr::filter(.data$feature_type %in% scale_order) |>
@@ -263,10 +306,18 @@ makeNmccStripPlot <- function(data, selected_drug_class = NULL, selected_drug =
}
-# makeNmccHeatmap: three-panel heatmap on the Performance overview tab.
-# Sections share the drug_class y-axis and show median nMCC by (species),
-# (molecular scale), and (data encoding). Highlights the selected drug
-# class row across all three sections.
+#' Three-panel nMCC heatmap (Performance overview)
+#'
+#' Three sections sharing the drug-class y-axis, showing median nMCC by species,
+#' molecular scale, and data encoding. Highlights the selected drug-class row
+#' across all three sections.
+#'
+#' @param data Performance tibble from loadMLResults().
+#' @param selected_drug_class Drug class row to highlight, "all"/NULL for none.
+#' @return A 3-panel plotly subplot (empty placeholder when there is no
+#' drug-class data).
+#' @keywords internal
+#' @noRd
makeNmccHeatmap <- function(data, selected_drug_class = NULL) {
df <- .prep_nmcc_data(data)
if (is.null(df)) {
@@ -331,15 +382,10 @@ makeNmccHeatmap <- function(data, selected_drug_class = NULL) {
)
# Section 2: molecular scale x drug_class (alpha-modulated scale color)
- scale_labels <- c(
- domains = "Domain", genes = "Gene",
- proteins = "Protein", struct = "Struct"
- )
- scale_order <- names(scale_labels)
- scale_colors <- setNames(
- unname(SCALE_COLORS[scale_order]),
- scale_labels
- )
+ sl <- .scale_label_map()
+ scale_labels <- sl$labels
+ scale_order <- sl$order
+ scale_colors <- sl$colors
sc_summ <- df |>
dplyr::filter(.data$feature_type %in% scale_order) |>
diff --git a/R/plots_network.R b/R/plots_network.R
index 9147c04..0c5be1a 100644
--- a/R/plots_network.R
+++ b/R/plots_network.R
@@ -1,14 +1,20 @@
# Force-directed network visualisations.
-# makeDrugFeatureNetwork: interactive force-directed graph linking drugs (or
-# drug classes) to their top features (Variables). Optionally extends to
-# cluster and COG tiers when an annotations parquet is available.
-# top_data: pre-loaded top-features tibble from loadTopFeat().
-# bug: 3-letter species code.
-# top_n: number of top features per drug to include as edges.
-# include_clusters / include_cogs: add annotation tiers when TRUE.
-# results_root: path for annotation lookup (falls back to extdata).
+#' Interactive force-directed drug -> feature network
+#'
+#' Links drugs (or drug classes) to their top features (Variables), optionally
+#' extending to cluster and COG tiers when an annotations parquet is available.
+#'
+#' @param top_data Pre-loaded top-features tibble from loadTopFeat().
+#' @param bug 3-letter species code.
+#' @param top_n Number of top features per drug to include as edges.
+#' @param include_clusters,include_cogs Add annotation tiers when TRUE.
+#' @param results_root Path for annotation lookup (falls back to extdata).
+#' @return A `networkD3` forceNetwork widget, or NULL when there is nothing to
+#' plot.
+#' @keywords internal
+#' @noRd
makeDrugFeatureNetwork <- function(top_data, bug, top_n = 10,
include_clusters = FALSE,
include_cogs = FALSE,
@@ -165,8 +171,18 @@ makeDrugFeatureNetwork <- function(top_data, bug, top_n = 10,
}
-# makeFeatureEgoNetwork: small force-directed graph for a single selected
-# feature, showing feature -> cluster -> COG links.
+#' Ego network for a single feature
+#'
+#' Small force-directed graph for one selected feature, showing its
+#' feature -> cluster -> COG links.
+#'
+#' @param enriched_tbl Annotation-enriched top-features tibble (see
+#' enrich_with_annotations()).
+#' @param variable The Variable (feature id) to centre the graph on.
+#' @return A `networkD3` forceNetwork widget, or NULL when the feature has no
+#' cluster/COG links to show.
+#' @keywords internal
+#' @noRd
makeFeatureEgoNetwork <- function(enriched_tbl, variable) {
if (!requireNamespace("networkD3", quietly = TRUE)) {
return(NULL)
diff --git a/R/utils_annotation.R b/R/utils_annotation.R
index 30c3991..27b9257 100644
--- a/R/utils_annotation.R
+++ b/R/utils_annotation.R
@@ -1,27 +1,44 @@
# Feature/cluster/COG + drug-class annotation lookups and enrichment.
+#' Load the drug -> drug-class lookup table
+#'
+#' Reads the packaged `drug_class_map.tsv` and returns the distinct
+#' antibiotic-name / drug-class pairs.
+#'
+#' @return A tibble with columns `drug.antibiotic_name` and `drug_class`.
+#' @keywords internal
+#' @noRd
loadDrugClassMap <- function() {
- cwd <- getwd()
- # drug_class_map_fp <- file.path(cwd, "data", "drug_class_map.tsv")
- drug_class_map_fp <- system.file("extdata", "drug_class_map.tsv", package = "amRviz")
- message(stringr::str_glue("loadDrugClassMap(): Looking for TSV at: {drug_class_map_fp}"))
- drug_class_map_df <- readr::read_tsv(
- here(drug_class_map_fp),
- show_col_types = FALSE
- ) |>
+ drug_class_map_fp <- system.file(
+ "extdata", "drug_class_map.tsv",
+ package = "amRviz"
+ )
+ message(stringr::str_glue(
+ "loadDrugClassMap(): Looking for TSV at: {drug_class_map_fp}"
+ ))
+ readr::read_tsv(here(drug_class_map_fp), show_col_types = FALSE) |>
dplyr::select(drug.antibiotic_name, drug_class) |>
dplyr::distinct()
- return(drug_class_map_df)
}
-# Load a feature-id -> human-readable name mapping from the amRdata-style
-# directory layout. Returns a tibble with columns `Variable` and `label`,
-# or NULL if no matching parquet is found. Looks in:
-# {amrdata_root}/{species_dir}/{gene|domain|protein}_names.parquet
-# {results_root}/{species_dir}/{gene|domain|protein}_names.parquet
-# {extdata}/{species_dir}/{gene|domain|protein}_names.parquet
+#' Load a feature-id -> human-readable name mapping
+#'
+#' Searches the amRdata-style layout
+#' (`{root}/{species_dir}/{gene|domain|protein}_names.parquet`) across the
+#' amrdata_root, results_root, and packaged extdata, then normalises the result
+#' to `{Variable, label}`.
+#'
+#' @param species_code Species code (currently unused in the lookup; kept for
+#' call-site consistency).
+#' @param model_scale Molecular scale ("genes", "proteins", or "domains").
+#' @param amrdata_root Optional amRdata data root, searched first.
+#' @param results_root Optional user results root, searched next.
+#' @return A tibble with columns `Variable` and `label`, or NULL when no
+#' matching parquet is found.
+#' @keywords internal
+#' @noRd
load_feature_name_map <- function(species_code, model_scale,
amrdata_root = NULL,
results_root = NULL) {
@@ -33,8 +50,6 @@ load_feature_name_map <- function(species_code, model_scale,
)
fname <- paste0(scale, "_names.parquet")
- # Species-code and species-label maps use the same lookup paths as
- # cluster_feature_COG.parquet.
roots <- c(
amrdata_root, results_root,
system.file("extdata", package = "amRviz")
@@ -43,13 +58,7 @@ load_feature_name_map <- function(species_code, model_scale,
fp <- NULL
for (r in roots) {
- for (d in list.dirs(r, full.names = TRUE, recursive = FALSE)) {
- cand <- file.path(d, fname)
- if (file.exists(cand)) {
- fp <- cand
- break
- }
- }
+ fp <- .find_file_in_subdirs(r, fname)
if (!is.null(fp)) break
}
if (is.null(fp)) {
@@ -63,16 +72,11 @@ load_feature_name_map <- function(species_code, model_scale,
# Normalise to {Variable, label}
if (scale == "gene" && all(c("Gene", "Annotation") %in% names(df))) {
- return(tibble::tibble(
- Variable = df$Gene, label = df$Annotation
- ))
+ return(tibble::tibble(Variable = df$Gene, label = df$Annotation))
}
- if (scale == "protein" && all(
- c("proteinID", "proteinName") %in% names(df)
- )) {
- return(tibble::tibble(
- Variable = df$proteinID, label = df$proteinName
- ))
+ if (scale == "protein" &&
+ all(c("proteinID", "proteinName") %in% names(df))) {
+ return(tibble::tibble(Variable = df$proteinID, label = df$proteinName))
}
if (scale == "domain" && all(c("DB.ID", "SignDesc") %in% names(df))) {
# domain ids need deduping since one Pfam can occur many times
@@ -89,35 +93,45 @@ load_feature_name_map <- function(species_code, model_scale,
}
-# Load cluster/COG annotations for a species if the parquet exists.
-# Searches in results_root//cluster_feature_COG.parquet first,
-# then falls back to extdata//cluster_feature_COG.parquet.
+#' Load cluster/COG annotations for a species
+#'
+#' Searches the species subdirectories under `results_root`, then the packaged
+#' extdata, for `cluster_feature_COG.parquet`.
+#'
+#' @param species_code Species code (currently unused; kept for call-site
+#' consistency).
+#' @param results_root Optional user results root, searched first.
+#' @return The annotations tibble, or NULL when no parquet is found.
+#' @keywords internal
+#' @noRd
load_feature_annotations <- function(species_code, results_root = NULL) {
fname <- "cluster_feature_COG.parquet"
- if (!is.null(results_root) && nzchar(results_root)) {
- for (d in list.dirs(results_root, full.names = TRUE, recursive = FALSE)) {
- fp <- file.path(d, fname)
- if (file.exists(fp)) {
- return(arrow::read_parquet(fp))
- }
- }
+ fp <- .find_file_in_subdirs(results_root, fname)
+ if (is.null(fp)) {
+ fp <- .find_file_in_subdirs(
+ system.file("extdata", package = "amRviz"), fname
+ )
}
- extdata <- system.file("extdata", package = "amRviz")
- if (nzchar(extdata)) {
- for (d in list.dirs(extdata, full.names = TRUE, recursive = FALSE)) {
- fp <- file.path(d, fname)
- if (file.exists(fp)) {
- return(arrow::read_parquet(fp))
- }
- }
+ if (is.null(fp)) {
+ return(NULL)
}
- NULL
+ arrow::read_parquet(fp)
}
-# Enrich a top-features tibble with cluster/COG annotations joined on
-# Variable -> feature. Collapses multiple COGs per feature into one comma-
-# separated cell. Returns the input unchanged if no annotations found.
+#' Enrich a top-features tibble with cluster/COG annotations
+#'
+#' Joins annotations on the feature key extracted from `Variable` (the part
+#' before the first "_"), collapsing multiple COGs per feature into a single
+#' comma-separated cell. Returns `tbl` unchanged when no annotations are found.
+#'
+#' @param tbl Top-features tibble (must contain a `Variable` column).
+#' @param species_code Species code passed to load_feature_annotations().
+#' @param results_root Optional user results root.
+#' @return `tbl` with cluster/COG columns joined on, or unchanged when no
+#' annotations are available.
+#' @keywords internal
+#' @noRd
enrich_with_annotations <- function(tbl, species_code, results_root = NULL) {
if (is.null(tbl) || !nrow(tbl) || !"Variable" %in% names(tbl)) {
return(tbl)
diff --git a/R/utils_colors.R b/R/utils_colors.R
index 7b423ff..ade16c5 100644
--- a/R/utils_colors.R
+++ b/R/utils_colors.R
@@ -29,10 +29,16 @@ META_COLORS <- c(
)
-# Return a vector of `n` muted categorical colors. For n <= length(META_COLORS)
-# returns the first n curated colors verbatim; for n above that, interpolates
-# via colorRampPalette() so callers get a full palette no matter how many
-# unique values their data has.
+#' Categorical palette of `n` muted colors
+#'
+#' For `n <= length(META_COLORS)` returns the first `n` curated colors verbatim;
+#' above that, interpolates via `grDevices::colorRampPalette()` so callers get a
+#' full palette regardless of how many unique values their data has.
+#'
+#' @param n Number of colors to return.
+#' @return A character vector of `n` hex colors (empty when `n <= 0`).
+#' @keywords internal
+#' @noRd
meta_palette <- function(n = length(META_COLORS)) {
if (n <= 0) {
return(character(0))
diff --git a/R/utils_data.R b/R/utils_data.R
index 6f2bbe8..7ebbc17 100644
--- a/R/utils_data.R
+++ b/R/utils_data.R
@@ -1,15 +1,30 @@
# Data loaders: ML results, top features, metadata, file discovery.
-# helpers to discover species folders and load to combine results generated from amRml
+#' Normalise a results-root path
+#'
+#' @param results_root Single path string, or NULL.
+#' @return The normalised absolute path, or NULL when `results_root` is missing,
+#' empty, or not a single string.
+#' @keywords internal
+#' @noRd
.normalize_results_root <- function(results_root) {
- if (is.null(results_root) || length(results_root) != 1 || is.na(results_root) || !nzchar(results_root)) {
+ if (is.null(results_root) || length(results_root) != 1 ||
+ is.na(results_root) || !nzchar(results_root)) {
return(NULL)
}
normalizePath(results_root, winslash = "/", mustWork = FALSE)
}
+#' Read a parquet file, returning an empty tibble on failure
+#'
+#' @param fp Path to a parquet file.
+#' @param verbose Whether to message on missing or unreadable files.
+#' @return A tibble of the file contents, or an empty tibble when `fp` is
+#' missing or cannot be read.
+#' @keywords internal
+#' @noRd
.read_parquet_safe <- function(fp, verbose = TRUE) {
if (is.null(fp) || !file.exists(fp)) {
if (isTRUE(verbose)) message("File not found: ", fp)
@@ -18,17 +33,29 @@
tryCatch(
arrow::read_parquet(fp),
error = function(e) {
- if (isTRUE(verbose)) message("Failed to read parquet: ", fp, " (", conditionMessage(e), ")")
+ if (isTRUE(verbose)) {
+ message("Failed to read parquet: ", fp, " (", conditionMessage(e), ")")
+ }
tibble::tibble()
}
)
}
-# Discover species subdirectories under results_root that contain amRml output.
-# Files live inside per-species subdirectories: {root}/{SpeciesDir}/{code}_ML_perf.parquet
-# Returns a named character vector: names = directory basename (display label),
-# values = full path to the species subdirectory.
+#' Discover species subdirectories that contain amRml output
+#'
+#' Files live in per-species subdirectories as
+#' `{root}/{SpeciesDir}/{code}_ML_perf.parquet`. Only directories holding at
+#' least one baseline `*_ML_perf.parquet` (not a country/year/MDR/cross variant)
+#' are kept.
+#'
+#' @param results_root Root directory to scan.
+#' @param verbose Unused; kept for signature consistency with the loaders.
+#' @return A named character vector (names = directory basenames used as display
+#' labels, values = full species-subdirectory paths), or empty when nothing
+#' matches.
+#' @keywords internal
+#' @noRd
listAmRmlSpeciesFolders <- function(results_root, verbose = TRUE) {
rr <- .normalize_results_root(results_root)
if (is.null(rr) || !dir.exists(rr)) {
@@ -40,7 +67,7 @@ listAmRmlSpeciesFolders <- function(results_root, verbose = TRUE) {
return(character(0))
}
- # Keep subdirs that contain at least one baseline *_ML_perf.parquet file
+ # Keep subdirs with at least one baseline *_ML_perf.parquet file
has_perf <- vapply(subdirs, function(d) {
fps <- list.files(d, pattern = "_ML_perf\\.parquet$", full.names = FALSE)
any(!grepl("_(country|year|MDR|cross)_ML_perf\\.parquet$", fps))
@@ -54,11 +81,21 @@ listAmRmlSpeciesFolders <- function(results_root, verbose = TRUE) {
}
-# Load all performance parquets (baseline + country + year + cross) from a species directory.
-# species_dir: full path to the species subdirectory (e.g. "/results/Shigella_flexneri")
-# Attaches species_label = basename(species_dir) so the full name is available for display.
+#' Load all performance parquets from one species directory
+#'
+#' Reads baseline + country + year + cross `*_ML_perf.parquet` files (excluding
+#' MDR) and tags rows with `species_label = basename(species_dir)`.
+#'
+#' @param species_dir Full path to a species subdirectory.
+#' @param verbose Passed through to .read_parquet_safe().
+#' @return A tibble of combined performance rows, or an empty tibble.
+#' @keywords internal
+#' @noRd
.load_one_species_perf <- function(species_dir, verbose = TRUE) {
- fps <- list.files(species_dir, pattern = "_ML_perf\\.parquet$", full.names = TRUE)
+ fps <- list.files(
+ species_dir,
+ pattern = "_ML_perf\\.parquet$", full.names = TRUE
+ )
fps <- fps[!grepl("_MDR_ML_perf\\.parquet$", fps)]
if (!length(fps)) {
return(tibble::tibble())
@@ -69,9 +106,21 @@ listAmRmlSpeciesFolders <- function(results_root, verbose = TRUE) {
}
-# Load all top-feature parquets (baseline + country + year) from a species directory.
+#' Load all top-feature parquets from one species directory
+#'
+#' Reads baseline + country + year `*_ML_top_features.parquet` files (excluding
+#' MDR) and tags rows with `species_label = basename(species_dir)`.
+#'
+#' @param species_dir Full path to a species subdirectory.
+#' @param verbose Passed through to .read_parquet_safe().
+#' @return A tibble of combined top-feature rows, or an empty tibble.
+#' @keywords internal
+#' @noRd
.load_one_species_top <- function(species_dir, verbose = TRUE) {
- fps <- list.files(species_dir, pattern = "_ML_top_features\\.parquet$", full.names = TRUE)
+ fps <- list.files(
+ species_dir,
+ pattern = "_ML_top_features\\.parquet$", full.names = TRUE
+ )
fps <- fps[!grepl("_MDR_ML_top_features\\.parquet$", fps)]
if (!length(fps)) {
return(tibble::tibble())
@@ -82,84 +131,118 @@ listAmRmlSpeciesFolders <- function(results_root, verbose = TRUE) {
}
-# Public loaders used by app.R (multi-species aware).
-# species_dirs: character vector of full paths to species subdirectories selected by the user.
-loadMLResults <- function(results_root = NULL, species_dirs = NULL, verbose = TRUE) {
+#' Resolve and load multi-species amRml results
+#'
+#' Shared mode-selection used by loadMLResults() and loadTopFeat(): user mode
+#' loads from the selected species subdirectories; if a results_root is set but
+#' nothing is selected, returns empty; otherwise falls back to the packaged demo
+#' parquets in extdata.
+#'
+#' @param loader Per-species loader, e.g. .load_one_species_perf().
+#' @param results_root User results root, or NULL for demo mode.
+#' @param species_dirs Selected species-subdirectory paths, or NULL.
+#' @param verbose Passed through to `loader`.
+#' @param demo_msg Message emitted when falling back to the demo parquets.
+#' @return A tibble of combined results across the resolved directories.
+#' @keywords internal
+#' @noRd
+.load_species_results <- function(loader, results_root, species_dirs,
+ verbose, demo_msg) {
rr <- .normalize_results_root(results_root)
- # User mode: results_root + species selected -> load from selected subdirectories
+ # User mode: results_root + selected species -> load from those subdirs
if (!is.null(rr) && !is.null(species_dirs) && length(species_dirs) > 0) {
- dfs <- lapply(species_dirs, .load_one_species_perf, verbose = verbose)
- return(dplyr::bind_rows(dfs))
+ return(dplyr::bind_rows(lapply(species_dirs, loader, verbose = verbose)))
}
- # User mode: results_root provided but nothing selected yet
+ # User mode: results_root set but nothing selected yet
if (!is.null(rr) && is.null(species_dirs)) {
return(tibble::tibble())
}
- # Demo fallback: scan extdata subdirectories recursively for *_ML_perf.parquet
+ # Demo fallback: scan packaged extdata subdirectories
extdata <- system.file("extdata", package = "amRviz")
if (!nzchar(extdata)) {
return(tibble::tibble())
}
subdirs <- list.dirs(extdata, full.names = TRUE, recursive = FALSE)
- if (isTRUE(verbose)) message("loadMLResults(): using packaged demo parquets")
- dplyr::bind_rows(lapply(subdirs, .load_one_species_perf, verbose = verbose))
+ if (isTRUE(verbose)) message(demo_msg)
+ dplyr::bind_rows(lapply(subdirs, loader, verbose = verbose))
}
-loadTopFeat <- function(results_root = NULL, species_dirs = NULL, verbose = TRUE) {
- rr <- .normalize_results_root(results_root)
+#' Load multi-species model performance results
+#'
+#' @param results_root User results root, or NULL for the packaged demo data.
+#' @param species_dirs Full paths to the species subdirectories selected by the
+#' user, or NULL.
+#' @param verbose Whether to emit progress/diagnostic messages.
+#' @return A tibble of combined performance rows.
+#' @keywords internal
+#' @noRd
+loadMLResults <- function(results_root = NULL, species_dirs = NULL,
+ verbose = TRUE) {
+ .load_species_results(
+ .load_one_species_perf, results_root, species_dirs, verbose,
+ "loadMLResults(): using packaged demo parquets"
+ )
+}
- if (!is.null(rr) && !is.null(species_dirs) && length(species_dirs) > 0) {
- dfs <- lapply(species_dirs, .load_one_species_top, verbose = verbose)
- return(dplyr::bind_rows(dfs))
- }
- if (!is.null(rr) && is.null(species_dirs)) {
- return(tibble::tibble())
- }
+#' Load multi-species top-feature results
+#'
+#' @param results_root User results root, or NULL for the packaged demo data.
+#' @param species_dirs Full paths to the species subdirectories selected by the
+#' user, or NULL.
+#' @param verbose Whether to emit progress/diagnostic messages.
+#' @return A tibble of combined top-feature rows.
+#' @keywords internal
+#' @noRd
+loadTopFeat <- function(results_root = NULL, species_dirs = NULL,
+ verbose = TRUE) {
+ .load_species_results(
+ .load_one_species_top, results_root, species_dirs, verbose,
+ "loadTopFeat(): using packaged demo parquets"
+ )
+}
- # Demo fallback: scan extdata subdirectories for *_ML_top_features.parquet
- extdata <- system.file("extdata", package = "amRviz")
- if (!nzchar(extdata)) {
- return(tibble::tibble())
+
+#' Find a file by name within the immediate subdirectories of a root
+#'
+#' @param root Directory whose immediate subdirectories are searched, or NULL.
+#' @param fname File name to look for in each subdirectory.
+#' @return The full path to the first match, or NULL when none is found.
+#' @keywords internal
+#' @noRd
+.find_file_in_subdirs <- function(root, fname) {
+ if (is.null(root) || !nzchar(root)) {
+ return(NULL)
}
- subdirs <- list.dirs(extdata, full.names = TRUE, recursive = FALSE)
- if (isTRUE(verbose)) message("loadTopFeat(): using packaged demo parquets")
- dplyr::bind_rows(lapply(subdirs, .load_one_species_top, verbose = verbose))
+ for (d in list.dirs(root, full.names = TRUE, recursive = FALSE)) {
+ fp <- file.path(d, fname)
+ if (file.exists(fp)) {
+ return(fp)
+ }
+ }
+ NULL
}
-# Return path to a species metadata parquet.
-# Searches inside the species subdirectory under results_root (user mode) or
-# extdata (demo mode). The metadata file follows the pattern {code}_metadata.parquet
-# and lives alongside the other parquets for that species.
+#' Locate a species metadata parquet
+#'
+#' Searches the species subdirectories under `results_root` (user mode) and then
+#' under the packaged extdata (demo mode) for `{species_code}_metadata.parquet`.
+#'
+#' @param species_code Species code prefix of the metadata file.
+#' @param results_root User results root, or NULL to search demo data only.
+#' @return The full path to the metadata parquet, or NULL when not found.
+#' @keywords internal
+#' @noRd
get_metadata_path <- function(species_code, results_root = NULL) {
fname <- paste0(species_code, "_metadata.parquet")
-
- # User mode: search species subdirectories under results_root
- if (!is.null(results_root) && nzchar(results_root)) {
- subdirs <- list.dirs(results_root, full.names = TRUE, recursive = FALSE)
- for (d in subdirs) {
- fp <- file.path(d, fname)
- if (file.exists(fp)) {
- return(fp)
- }
- }
+ fp <- .find_file_in_subdirs(results_root, fname)
+ if (!is.null(fp)) {
+ return(fp)
}
-
- # Demo mode: search species subdirectories under extdata
- extdata <- system.file("extdata", package = "amRviz")
- if (nzchar(extdata)) {
- subdirs <- list.dirs(extdata, full.names = TRUE, recursive = FALSE)
- for (d in subdirs) {
- fp <- file.path(d, fname)
- if (file.exists(fp)) {
- return(fp)
- }
- }
- }
- NULL
+ .find_file_in_subdirs(system.file("extdata", package = "amRviz"), fname)
}
diff --git a/R/utils_misc.R b/R/utils_misc.R
index 7c831cf..84ec5bc 100644
--- a/R/utils_misc.R
+++ b/R/utils_misc.R
@@ -5,8 +5,15 @@
SPECIES_PATTERN <- "(Efa|Sau|Kpn|Aba|Pae|Esp\\.?)"
-# normalize_species helper: make "Esp." and "Esp" equivalent by removing
-# a single trailing dot for comparisons (preserves NA).
+#' Normalise species codes for comparison
+#'
+#' Makes "Esp." and "Esp" equivalent by removing a single trailing dot, so
+#' species filters match regardless of the trailing-dot convention. NA-safe.
+#'
+#' @param x Character vector (or coercible) of species codes.
+#' @return A character vector with any single trailing dot removed.
+#' @keywords internal
+#' @noRd
normalize_species <- function(x) {
x_chr <- as.character(x)
x_chr[is.na(x_chr)] <- NA_character_
@@ -14,9 +21,17 @@ normalize_species <- function(x) {
}
-# getHoldoutsDrugChoices: derive drug/class choices for the holdouts tab
-# perf_data: combined performance tibble from loadMLResults()
-# bug: optional 3-letter species code to filter to
+#' Derive drug/class choices for the holdouts tab
+#'
+#' Restricts to stratified (country/year, non-baseline) models, optionally
+#' filters to one species, and returns the sorted unique drug/class labels.
+#'
+#' @param perf_data Combined performance tibble from loadMLResults().
+#' @param bug Optional 3-letter species code to filter to.
+#' @return A sorted character vector of unique drug/class choices (empty when
+#' there are no matching rows).
+#' @keywords internal
+#' @noRd
getHoldoutsDrugChoices <- function(perf_data, bug = NULL) {
if (is.null(perf_data) || !is.data.frame(perf_data) || !nrow(perf_data)) {
return(character(0))
diff --git a/R/utils_ui.R b/R/utils_ui.R
index 417121e..793a456 100644
--- a/R/utils_ui.R
+++ b/R/utils_ui.R
@@ -1,6 +1,15 @@
# Small reusable UI input/control builders.
+#' Padded action button with a styled label and icon
+#'
+#' @param id Input ID for the button.
+#' @param label Button label text.
+#' @param icon_name Name of the Font Awesome icon to show.
+#' @param class_name CSS class applied to the button.
+#' @return A shiny `div` wrapping the action button.
+#' @keywords internal
+#' @noRd
amr_button <- function(id, label, icon_name, class_name) {
div(
style = "padding: 5px; text-align: center;",
@@ -15,15 +24,29 @@ amr_button <- function(id, label, icon_name, class_name) {
}
+#' UI-output container with consistent box padding
+#'
+#' @param outputId Output ID to render into.
+#' @return A shiny `uiOutput` with a padded container.
+#' @keywords internal
+#' @noRd
styledBox <- function(outputId) {
uiOutput(outputId, container = function(...) {
- div(style = "display:inline-block")
div(style = "padding-top: 0px; padding-bottom: 10px; height: 80%", ...)
})
}
-# Select input helper
+#' Padded selectize input with a styled label
+#'
+#' @param id Input ID for the select control.
+#' @param label Label text.
+#' @param choices Choices passed to `selectInput()`.
+#' @param multiple Whether multiple selections are allowed.
+#' @param selected Initially selected value(s), or NULL.
+#' @return A shiny `div` wrapping the select input.
+#' @keywords internal
+#' @noRd
amr_select <- function(id, label, choices, multiple = TRUE, selected = NULL) {
div(
style = "padding: 10px;",