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;",