--- title: "Using teal.picks in a teal application" author: "NEST CoreDev" date: "`r Sys.Date()`" vignette: > %\VignetteIndexEntry{Using teal.picks in a teal application} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} output: rmarkdown::html_vignette: toc: true --- ```{r setup, include = FALSE} knitr::opts_chunk$set(comment = NA) ``` ## What this package is for `teal.picks` helps you expose analysis choices in a `teal` Shiny app in a way that mimicks how people think about data: first *which dataset*, then *which columns*, and optionally *which values* to filter to. You set all those steps at once with `picks()`; the app renders the right controls and keeps selections consistent when upstream choices change. Typical workflow: 1. Prepare a `teal.data::teal_data()` object (when you relate multiple tables use `join_keys` so teal knows how they link). 2. Define one or more `picks()` specs: `datasets()`, optionally `variables()`, optionally `values()` for filters or ranges. 3. In your teal module, wire UI and server so those specs drive your tables, plots, or merged analysis data (see [Writing your own module](#writing-your-own-module)). The examples below show the usual building blocks—not every app needs every level. ```{r data, message = FALSE} library(teal) library(teal.data) library(teal.picks) data <- teal_data() data <- within(data, { ADSL <- teal.data::rADSL ADLB <- teal.data::rADLB }) join_keys(data) <- teal.data::default_cdisc_join_keys[c("ADSL", "ADLB")] ``` ### Choose a dataset When the analysis can draw from more than one table, let the user pick the active source (here, demographics vs labs). ```{r picks-datasets} picks_datasets <- list( source = picks( datasets( choices = c("ADSL", "ADLB"), selected = "ADLB" ) ) ) ``` ### Choose dataset and columns Add `variables()` so, after a dataset is chosen, the user picks one or more columns. You can list names explicitly or use tidyselect-style expressions; see `?picks` for details. ```{r picks-datasets-variables} picks_datasets_variables <- list( adsl_cols = picks( datasets(choices = "ADSL", selected = "ADSL"), variables( choices = c("USUBJID", "AGE", "SEX"), selected = "AGE", multiple = FALSE ) ) ) ``` ### Add a value filter (levels or ranges) `values()` sits after `variables()`. It adapts to the column type—for example category levels for a `PARAM`-style variable, or a numeric or date range for continuous data. ```{r picks-datasets-variables-values} picks_datasets_variables_values <- list( labs = picks( datasets(choices = "ADLB", selected = "ADLB"), variables(choices = "PARAM", selected = "PARAM", multiple = FALSE), values( choices = c("ALT", "AST", "CRP", "GLU"), selected = c("ALT", "AST"), multiple = TRUE ) ) ) ``` ### Define choices and selections Each `picks()` element has `choices` and `selected`. You can define them in four ways: | Approach | Works with | |---|---| | Default behavior - no arguments needed for generic usage | `datasets()`, `variables()`, `values()` | Static — character vector, integer index, or numeric range | `datasets()`, `variables()`, `values()` | | `tidyselect` helpers — `everything()`, `starts_with()`, `where()`, … | `datasets()`, `variables()` only | | Function — an R function applied at runtime to the data | `datasets()`, `variables()`, `values()` | The sections below walk through each approach with runnable examples. #### Defaults Defaults depend on the slot type. * For `datasets()`, the default `choices` are all available datasets. A `picks()` call needs a `datasets()` as first positional argument, except when `check_no_dataset = FALSE`. * For `variables()`, the default `choices` is all variables in the selected dataset. * For `values()`, the default `choices` is all values of the selected variable. * For `selected`, the default is the first available choice, or all choices when `multiple = TRUE`. ```{r defaults} picks( datasets(choices = "ADSL", selected = "ADSL"), variables() ) picks( datasets(choices = "ADSL", selected = "ADSL"), variables(choices = "SEX", selected = "SEX", multiple = FALSE), values() ) ``` #### Static choices Pass a character vector to `choices` to enumerate options exactly. Use `selected` to set the default. Integer indices work too (for example `selected = 1L` means the first element of `choices`). ```{r static} # Datasets — user may switch between ADSL and ADLB; ADSL is the default p_datasets <- picks( datasets( choices = c("ADSL", "ADLB"), selected = "ADSL" ) ) # Variables — only a named subset is offered; first column pre-selected p_variables <- picks( datasets(choices = "ADSL", selected = "ADSL"), variables( choices = c("AGE", "SEX", "ARM"), selected = "AGE", multiple = FALSE ) ) # Values — categorical filter; two levels pre-selected p_values <- picks( datasets(choices = "ADSL", selected = "ADSL"), variables(choices = "SEX", selected = "SEX", multiple = FALSE), values( choices = c("M", "F"), selected = "F" ) ) p_datasets p_variables p_values ``` #### `tidyselect` helpers `tidyselect` predicates let `choices` and `selected` adapt to the actual data at runtime instead of being hard-coded. This is supported for `datasets()` and `variables()`. Commonly used helpers: - `tidyselect::everything()` — all items - `tidyselect::starts_with()` / `tidyselect::ends_with()` / `tidyselect::contains()` — pattern matching - `tidyselect::matches()` — regex matching - `tidyselect::where(predicate)` — columns satisfying a predicate function - `tidyselect::all_of()` / `tidyselect::any_of()` — vector-based selection (error-safe or silent) - Integer indices such as `1L`, `1L:3L` — select by position > Note: `tidyselect` is not supported by `values()`. > Use explicit vectors or a function there instead (see [Functions](#functions)). ```{r tidyselect} # Datasets — offer any data.frame in the teal_data object p_any_dataset <- picks( datasets( choices = tidyselect::where(is.data.frame), selected = 1L # first dataset by default ) ) # Variables — all numeric columns; first one pre-selected p_numeric_vars <- picks( datasets(choices = "ADSL", selected = "ADSL"), variables( choices = tidyselect::where(is.numeric), selected = 1L, multiple = FALSE ) ) # Variables — columns whose names start with "A"; first two pre-selected p_a_prefix <- picks( datasets(choices = "ADSL", selected = "ADSL"), variables( choices = tidyselect::starts_with("A"), selected = 1L:2L, multiple = TRUE ) ) p_any_dataset p_numeric_vars p_a_prefix ``` #### Functions {#functions} You can pass a plain R function as `choices` or `selected`. The function receives the relevant context object (the current dataset for `datasets()` and `variables()`, the current column vector for `values()`) and must return the subset to use. This is the only runtime-dynamic approach supported by `values()`. ```{r functions} # Variables — use the package helper is_categorical() as a column predicate. # Without "des-delayed", the resolver calls it via vapply(data, fn, logical(1)), # so it must accept one column and return a single logical value — which is_categorical() does. picks( datasets(choices = "ADSL", selected = "ADSL"), variables( choices = is_categorical(), selected = 1L, multiple = TRUE ) ) # Values — select only even ages from the AGE column. # Functions passed to values() must carry the "des-delayed" class so the resolver # calls them with the column vector rather than treating them as a column predicate. even_vals <- function(x) sort(unique(x[x %% 2 == 0])) class(even_vals) <- append(class(even_vals), "des-delayed") p_even_ages <- picks( datasets(choices = "ADSL", selected = "ADSL"), variables(choices = "AGE", selected = "AGE", multiple = FALSE), values( choices = even_vals, selected = even_vals ) ) p_even_ages ``` #### Use of multiple to select more than one columns at once Use `multiple = TRUE` when analysts should pass more than one variable into the next step (for example stratifiers or outcomes together). ```{r picks-multiple-variables} picks_multiple_variables <- list( demo = picks( datasets(choices = "ADSL", selected = "ADSL"), variables( choices = c("USUBJID", "AGE", "SEX"), selected = c("AGE", "SEX"), multiple = TRUE, ordered = TRUE ) ) ) ``` See `?picks` for other arguments that affect how choices/selections are presented to users regardless of using static, tidyselect or functions to define them. #### Choices and Selection Summary | | Static | `tidyselect` | Function | |---|:---:|:---:|:---:| | `datasets()` | yes | yes | yes | | `variables()` | yes | yes | yes | | `values()` | yes | **no** | yes | Use **static** choices when the set of options is fixed and known at app-development time. Use **`tidyselect`** when you want the choices to adapt to the shape of the data without writing a custom function. Use **functions** when the logic is more involved, or when you need runtime behavior for `values()`. ## Trying these patterns inside teal You can paste the `picks` objects above into any module that consumes them. To quickly explore picks object(s) in teal apps, the package includes `tm_merge()`: a small example teal module that connects `picks` to a merged preview table. That is not required for normal use—it is simply a convenient host while you learn the `picks()` API. Run the next block to explore a teal app with different picks patterns ```{r teal-app, eval = FALSE} library(shiny) app <- init( data = data, modules = modules( modules( label = "teal.picks patterns", tm_merge( label = "1. Dataset choice", picks = picks_datasets ), tm_merge( label = "2. Dataset & variables", picks = picks_datasets_variables ), tm_merge( label = "3. Dataset, variables & values", picks = picks_datasets_variables_values ), tm_merge( label = "4. Multiple variables", picks = picks_multiple_variables ) ) ) ) if (interactive()) { shinyApp(app$ui, app$server) } ``` ## Writing your own module {#writing-your-own-module} In real apps you usually integrate `picks` with your own `teal::module()`. Two functions do the wiring: - `picks_ui()` — place the selector controls in your picks object in your module UI - `picks_srv()` — in the module server, pass `data` as a reactive `teal_data` object; it returns the resolved picks (what the user chose, after any dynamic choices are applied). ```{r module-skeleton, eval = FALSE} tm_picks_preview <- function(label = "Custom picks module", picks) { teal::module( label = label, ui = function(id, picks) { ns <- shiny::NS(id) shiny::tagList( teal.picks::picks_ui(ns("sel"), picks = picks), shiny::tags$h5("Preview (first rows)"), shiny::tableOutput(ns("preview")), shiny::tags$h5("Resolved picks"), shiny::verbatimTextOutput(ns("resolved")) ) }, server = function(id, data, picks) { shiny::moduleServer(id, function(input, output, session) { resolved <- teal.picks::picks_srv("sel", picks = picks, data = data) preview_tbl <- shiny::reactive({ shiny::req(data(), resolved()) ds <- resolved()$datasets$selected vars <- resolved()$variables$selected shiny::req(length(ds) == 1L, length(vars) >= 1L) data()[[ds]][, vars, drop = FALSE] }) output$preview <- shiny::renderTable({ utils::head(preview_tbl(), 8L) }) output$resolved <- shiny::renderPrint({ shiny::req(resolved()) str(resolved(), max.level = 2L, give.attr = FALSE) }) }) }, ui_args = list(picks = picks), server_args = list(picks = picks), datanames = "ADSL" ) } app <- init( data = data, modules = modules( tm_picks_preview( label = "Custom picks module", picks = picks_datasets_variables$adsl_cols ) ) ) if (interactive()) { shinyApp(app$ui, app$server) } ``` See `?picks_ui` and `?picks_srv` for container options and multiple named selectors. ## See also - `?picks` — full `datasets()` / `variables()` / `values()` reference. - `vignette("teal-picks-standalone-shiny", package = "teal.picks")` — `picks_ui()` / `picks_srv()` in plain Shiny (no `init()`). - `teal` documentation for `init()`, `modules()`, and `teal::module()`.