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:
teal.data::teal_data() object (when you
relate multiple tables use join_keys so teal knows how they
link).picks() specs:
datasets(), optionally variables(), optionally
values() for filters or ranges.The examples below show the usual building blocks—not every app needs every level.
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")]When the analysis can draw from more than one table, let the user pick the active source (here, demographics vs labs).
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.
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.
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 depend on the slot type.
datasets(), the default choices are
all available datasets. A picks() call needs a
datasets() as first positional argument, except when
check_no_dataset = FALSE.variables(), the default choices is
all variables in the selected dataset.values(), the default choices is all
values of the selected variable.selected, the default is the first available
choice, or all choices when multiple = TRUE. [1m<picks>[0m
[1m<datasets>[0m:
choices: ADSL
selected: ADSL
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE[0m
[1m<variables>[0m:
choices: tidyselect::everything()
selected: 1L
[3mmultiple=FALSE, ordered=FALSE, fixed=FALSE, allow-clear=FALSE[0m
picks(
datasets(choices = "ADSL", selected = "ADSL"),
variables(choices = "SEX", selected = "SEX", multiple = FALSE),
values()
) [1m<picks>[0m
[1m<datasets>[0m:
choices: ADSL
selected: ADSL
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE[0m
[1m<variables>[0m:
choices: SEX
selected: SEX
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE, allow-clear=FALSE[0m
[1m<values>[0m:
choices: <fn>
selected: <fn>
[3mmultiple=TRUE, ordered=FALSE, fixed=FALSE[0m
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).
# 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 [1m<picks>[0m
[1m<datasets>[0m:
choices: ADSL, ADLB
selected: ADSL
[3mmultiple=FALSE, ordered=FALSE, fixed=FALSE[0m
[1m<picks>[0m
[1m<datasets>[0m:
choices: ADSL
selected: ADSL
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE[0m
[1m<variables>[0m:
choices: AGE, SEX, ARM
selected: AGE
[3mmultiple=FALSE, ordered=FALSE, fixed=FALSE, allow-clear=FALSE[0m
[1m<picks>[0m
[1m<datasets>[0m:
choices: ADSL
selected: ADSL
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE[0m
[1m<variables>[0m:
choices: SEX
selected: SEX
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE, allow-clear=FALSE[0m
[1m<values>[0m:
choices: M, F
selected: F
[3mmultiple=TRUE, ordered=FALSE, fixed=FALSE[0m
tidyselect helperstidyselect 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 itemstidyselect::starts_with() /
tidyselect::ends_with() /
tidyselect::contains() — pattern matchingtidyselect::matches() — regex matchingtidyselect::where(predicate) — columns satisfying a
predicate functiontidyselect::all_of() /
tidyselect::any_of() — vector-based selection (error-safe
or silent)1L, 1L:3L — select
by positionNote:
tidyselectis not supported byvalues(). Use explicit vectors or a function there instead (see Functions).
# 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 [1m<picks>[0m
[1m<datasets>[0m:
choices: <fn>
selected: 1L
[3mmultiple=FALSE, ordered=FALSE, fixed=FALSE[0m
[1m<picks>[0m
[1m<datasets>[0m:
choices: ADSL
selected: ADSL
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE[0m
[1m<variables>[0m:
choices: <fn>
selected: 1L
[3mmultiple=FALSE, ordered=FALSE, fixed=FALSE, allow-clear=FALSE[0m
[1m<picks>[0m
[1m<datasets>[0m:
choices: ADSL
selected: ADSL
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE[0m
[1m<variables>[0m:
choices: tidyselect::starts_with("A")
selected: <int>
[3mmultiple=TRUE, ordered=FALSE, fixed=FALSE, allow-clear=FALSE[0m
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().
# 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
)
) [1m<picks>[0m
[1m<datasets>[0m:
choices: ADSL
selected: ADSL
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE[0m
[1m<variables>[0m:
choices: <fn>
selected: 1L
[3mmultiple=TRUE, ordered=FALSE, fixed=FALSE, allow-clear=FALSE[0m
# 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 [1m<picks>[0m
[1m<datasets>[0m:
choices: ADSL
selected: ADSL
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE[0m
[1m<variables>[0m:
choices: AGE
selected: AGE
[3mmultiple=FALSE, ordered=FALSE, fixed=TRUE, allow-clear=FALSE[0m
[1m<values>[0m:
choices: <function>
selected: <function>
[3mmultiple=TRUE, ordered=FALSE, fixed=FALSE[0m
Use multiple = TRUE when analysts should pass more than
one variable into the next step (for example stratifiers or outcomes
together).
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.
| 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().
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
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)
}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 UIpicks_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).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.
?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().