teal.picks is part of the teal framework and provides the dataset,
variable and value pickers used in teal modules. It lets teal app
developers declare which dataset, columns and values the app user can
choose, renders the matching inputs, and merges the selected data into
an analysis-ready teal_data object with reproducible code.
It provides 3 main features to the framework:
picksspecification (R/picks.R): S3 objects that definechoicesand defaultselectedfor a hierarchy of selectionspicks()is a list ofpickobjects named after their class:datasets()first, followed byvariables()and thenvalues()- a
pickislist(choices, selected)withmultiple,fixed,orderedand extrapickerInputoptions stored as attributes choices/selectedcould be:- eager:
characternames, or plain values forvalues() tidyselectexpressions, quoted withrlang::enquo(); integer positions such as the defaultselected = 1Lcount astidyselect- predicate functions, e.g.
is.numeric, applied to each element (functions with class"des-delayed"are called once with all the data); the only delayed form accepted byvalues()
- eager:
- Shiny modules
picks_ui()/picks_srv()render one input perpickand return areactiveVal(picks_resolved) with the resolvedpicks - Merging function
merge_srv()merges the selected data into one dataset (anlby default) and returns the mergedteal_dataand the selected variables
See vignettes/teal-picks-in-teal.Rmd for usage in teal modules and
vignettes/teal-picks-standalone-shiny.Rmd for standalone shiny
usage.
resolver()walks thepicksin order: thedetermine()S3 generic resolveschoicesand thenselectedof each element and passes the selected data to the next one (teal_data→ dataset → selected columns).- Names given as text are matched against the data and unknown ones are
dropped silently (a warning appears only if none match):
variables(c("AGE", "SEXX"))offers onlyAGE. values()are resolved against the unique values of the selected columns.- If an element ends up with nothing selected, every element after it is
emptied (
choicesandselectedbecomeNULL).
picks_ui()only creates placeholders;picks_srv()renders the inputs from the resolvedpicks.- The
values()input depends on the type of the selected column:ranged()on a numeric column gives anumericRangeInput(), on aDate/POSIXctcolumn adateRangeInput(); anything else (character, factor, logical, or numeric withoutranged()) gives apickerInput().
- Changing the dataset resets
variables()andvalues()to their defaults, and changing the variables resetsvalues()(.resolve()). - Categorical inputs (
shinyWidgets::pickerInput()) commit only when the drop-down closes (input$<element>-selected_openbecomesFALSE). - Inputs are rendered again only when
choiceschange (bindEvent(choices())); updating onlyselectedinpicks_resolveddoesn’t update the input.
.merge_summary_list()checks the selections andjoin_keys, merges parents first (e.g.,ADSLbeforeADAE), renames duplicated columns (AGE_ADAE) and sets thejoin_keysofanl..merge_expr()writes the merge code:dplyr::select()(keys always kept),dplyr::filter()only ifvalues()drop rows, thenjoin_funfor each extra dataset, chained with%>%.
Direct dependencies:
teal.data:teal_dataandjoin_keys.- Any issue with
join_keysshould be addressed inteal.data.
- Any issue with
teal.code: reproducible code (eval_code(),get_code()).teal:tm_merge()andteal_transform_filter()buildteal::module()/teal::teal_transform_module()objects.tidyselect: dynamicchoices/selected.teal.transformis inSuggests;as.picks()convertsdata_extract_spec/select_spec/filter_spectopicksto ease the transition away fromteal.transform.
Usage in other framework packages:
- Module packages (
teal.modules.general,teal.modules.clinical,teal.modules.gtsummary,teal.goshawk) callpicks(),picks_ui(),picks_srv()andmerge_srv()in their modules and theapp_driver_*()helpers in theirshinytest2tests, so changing these can break them. Open an issue before changing argument checks inpicks(),datasets(),variables()orvalues().teal.modules.generalis migrating fromteal.transform: thepicksversion of each module is in atm_*_picks.Rfile next to the olddata_extract_specversion.
Use the exported helpers instead of reading the structure or attributes
of picks directly:
check_picks()/assert_picks(): the object is apickswith the required elements (datasets = TRUE,variables = TRUE,values = TRUE).check_last_level()/assert_last_level(): the last element is of a given class, e.g.assert_last_level(x, "variables").is_pick_multiple(),is_pick_fixed(),is_pick_ordered(): readpickattributes instead ofattr(x, "multiple").picks_datanames(): datasets used by a set ofpicks, e.g. for a module’sdatanames.
- In
testServer(), set"variables-selected"and then"variables-selected_open" = FALSEin twosession$setInputs()calls before checkingpicks_resolved(). The input’s HTML is insession$output[["variables-selected_container"]]$html. - In tests of other features, wrap only the
picks()call insuppressWarnings(classes = "picks_delayed"): it warns whenever eager choices follow a dynamic element, including the defaultselected = 1L. - In
shinytest2, Shiny can’t see badge inputs until the badge is opened. Useapp_driver_get_teal_picks_slot()andapp_driver_set_teal_picks_slot().
- Check resolution without an app:
print(resolver(picks, data)).datacan be ateal_data, an environment or a named list. - Errors in
choices/selectedfunctions ortidyselectare hidden:- Symptom: an empty picker and the warning “None of the
choices/selected… Emptying choices…”. - To see the error, run it on the data:
tidyselect::eval_select(rlang::quo(c(Species, Sepal.Lenght)), iris). - Unlike text names, one unknown column in a
tidyselectexpression empties the whole selection; usetidyselect::any_of()for optional columns.
- Symptom: an empty picker and the warning “None of the
no applicable method for 'join_keys<-' ... 'qenv.error'means the merge code failed:- To see the real error, run
teal.code::eval_code(data, teal.picks:::.merge_expr(teal.picks:::.merge_summary_list(selectors, teal.data::join_keys(data), "anl"), "anl", join_fun, data)), withselectorsa named list of resolvedpicks. - Common cause:
%>%is not available. Runlibrary(dplyr)in theteal_datafirst;devtools::load_all()hides this because it attachestestthat, which exports%>%. - Common cause: a column is missing from a newly selected dataset (#56).
- To see the real error, run
- Wrong results or missing columns in a module: check that the merged
dataset has the required columns with
names(merged$data()[["anl"]])andmerged$variables()before debugging the module code. - Debug logs:
logger::log_threshold("DEBUG", namespace = "teal.picks"), orTEAL.LOG_LEVEL=DEBUGbefore loading.
This package is part of the teal framework. The following configuration applies to all packages within the teal framework.
Follow the standard R package structure with teal-specific conventions:
package_name/
├── .github # CI/CD workflows
├── R/ # R source code
├── tests/testthat/ # Unit tests using testthat
├── vignettes/ # Long-form documentation
├── inst/ # Package assets
├── AGENTS.md # Development guide for AI agents (this file)
├── DESCRIPTION # Package metadata
├── NAMESPACE # Exports and imports
├── NEWS.md # Change log
├── README.md # Package overview
├── _pkgdown.yml # Documentation website config
├── .lintr # Linting configuration
└── .Rbuildignore # Build exclusions
- Function names: Use
snake_caseconsistently - Class names: Use
PascalCase(e.g.,TealAppDriver) - Module functions: Prefix UI functions with
ui_and server functions withsrv_ - Internal functions: Use descriptive names without export
- One main function per file when the function is substantial
- Group related utilities in shared files (e.g.,
utils.R,validations.R) - Module files: Use pattern
tm_<name>.Rfor teal modules - Helper functions: Prefix with the main function they support
- Run
pre-commithooks: Always runpre-commit run --all-filesbefore committing, Fix any issues it reports - the error messages are informative and will guide you. It automatically checks code style, documentation and other quality issues. If pre-commit is not available, run the checks manually. Lint the R code manually as well if not called by pre-commit. - Follow
tidyversestyle: General R code style follows thetidyversestyle guide. - Documentation: All exported functions must have
roxygen2documentation with@returnsand@examplesfields. - Formatting rules are configured in the
.lintrfile.
- Minimize dependencies: Only add dependencies that provide significant value
- Version constraints: Specify minimum versions for critical dependencies
- Ecosystem coherence: Prefer packages already used within teal ecosystem
Avoid importing package functions via roxygen2 (#' @import pkg)tags in
favor of explicit namespacing for clarity when appropriate. When needed
prefer specific imports over full package imports.
Each module should produce one or more Table, Listing, or Graph (TLG):
- Reproducibility: All code being executed to generate TLGs should
be run using
teal_dataandwithin()/teal.code::eval_code()- At the end of the module this object should be returned to enable Reporter and “Show R code” functionalities
- User Parameters: Configurable inputs via
teal.picks::picks()for flexible data selection - Transformators: Optional pre-processing functions that derive variables and validate data before analysis
- Decorators: Optional post-processing functions that customize output presentation (titles, legends, annotations)
Teal modules follow a specific pattern with UI and server components:
# UI Function
ui_example_module <- function(id, var_x, var_y, decorators) {
ns <- shiny::NS(id)
select_decorators <- getFromNamespace("select_decorators", "teal") # import from teal internal functions
shiny::tagList(
# Input controls
teal.widgets::standard_layout(
# Output displays
output = teal.widgets::white_small_well(
teal::ui_transform_teal_data("decorator_table", select_decorators(decorators, "plot")),
teal::ui_transform_teal_data("decorator_table", select_decorators(decorators, "table")),
shiny::tags$h4("Results"),
shiny::plotOutput(ns("plot")),
shiny::tags$h4("Summary data"),
gt::gt_output(ns("table"))
),
# Encoding panel
encoding = shiny::tags$div(
shiny::tags$label("Encodings", class = "text-primary"),
shiny::tags$br(),
shiny::tags$div(
shiny::tags$strong("Select X-Axis Variable"),
teal.picks::picks_ui(ns("var_x"), var_x)
),
shiny::tags$div(
shiny::tags$strong("Select Y-Axis Variable"),
teal.picks::picks_ui(ns("var_y"), var_y)
)
)
)
)
}
# Server Function
srv_example_module <- function(id, data, var_x, var_y, decorators) {
checkmate::assert_string(id)
checkmate::assert_class(data, "reactive")
select_decorators <- getFromNamespace("select_decorators", "teal") # import from teal internal functions
shiny::moduleServer(id, function(input, output, session) {
selectors <- teal.picks::picks_srv("picks", picks = list(var_x = var_x, var_y = var_y), data = data)
merged <- teal.picks::merge_srv(
"merge_picks",
data = data,
selectors = selectors,
output_name = "anl",
join_fun = "dplyr::inner_join"
)
# Data preparation
validated_q <- shiny::reactive({
shiny::validate(
teal::need_input(
inputId = "var_x-variables-selected",
condition = length(selectors$var_x()$variables$selected) > 0,
message = "X-Axis Variable must be selected"
),
teal::need_input(
inputId = "var_y-variables-selected",
condition = length(selectors$var_y()$variables$selected) > 0,
message = "Y-Axis Variable must be selected"
)
)
shiny::validate(
teal::need_input(
inputId = c("var_x-variables-selected", "var_y-variables-selected"),
condition = !any(selectors$var_x()$variables$selected %in% selectors$var_y()$variables$selected),
message = "X-axis variable and Y-axis variable must be different"
)
)
q <- merged$data()
teal.reporter::teal_card(q) <- c(teal.reporter::teal_card(q), "## Module's output")
q
})
# Generate plot inside qenv
qenv_plot <- reactive({
within(validated_q(), {
plot <- ggplot2::ggplot(anl) +
ggplot2::geom_point(ggplot2::aes(x = env_var_x, y = env_var_y))
}, env_var_x = as.name(merged$variables()$var_x), env_var_y = as.name(merged$variables()$var_y))
})
decorated_plot <- teal::srv_transform_teal_data(
"decorator_table",
qenv_plot,
select_decorators(decorators, "plot"),
expr = quote(plot)
)
qenv_table <- reactive({
within(validated_q(), {
table <- gtsummary::tbl_summary(anl, by = env_var_x, missing = "no")
}, env_var_x = as.name(merged$variables()$var_x), env_var_y = as.name(merged$variables()$var_y))
})
decorated_table <- teal::srv_transform_teal_data(
"decorator_table",
qenv_table,
select_decorators(decorators, "table"),
expr = quote(table)
)
# Output rendering: use ggplot2 for visualizations
output$plot <- shiny::renderPlot(decorated_plot()[["plot"]])
output$table <- gt::render_gt(expr = gtsummary::as_gt(decorated_table()[["table"]]))
# Return reactive
reactive(c(decorated_plot(), decorated_table()))
})
}
tm_example_module <- function(
label = "Example Module",
var_x = teal.picks::picks(teal.picks::datasets(), teal.picks::variables(is.numeric, selected = 1L)),
var_y = teal.picks::picks(teal.picks::datasets(), teal.picks::variables(is.numeric, selected = 2L)),
decorators = list(),
transformators = list()
) {
checkmate::assert_string(label)
checkmate::assert_class(var_x, "picks")
checkmate::assert_class(var_y, "picks")
checkmate::assert_list(transformators, types = "teal_transform_module")
args <- list(var_x = var_x, var_y = var_y, decorators = decorators)
teal::module(
label = label,
server = srv_example_module,
ui = ui_example_module,
ui_args = args[names(args) %in% names(formals(ui_example_module))],
server_args = args[names(args) %in% names(formals(srv_example_module))],
transformators = transformators
)
}- Use
tidyversestyle: Write clear, readable code usingdplyr,ggplot2patterns - Use
magrittrpipes in reproducible execution: For code executed forteal_data/qenvdata objects witheval_code()andwithin() - Use crane and gtsummary: For statistical tables and summaries
- Error handling: Implement proper validation using
checkmateandshiny::validate(teal::need_input(...))
# Good: Clear data manipulation
plot_data <- data %>%
dplyr::filter(!is.na(variable)) %>%
dplyr::group_by(category) %>%
dplyr::summarise(
mean_value = mean(value),
n = dplyr::n(),
.groups = "drop"
)
# Good: Descriptive ggplot2 code
ggplot2::ggplot(plot_data, ggplot2::aes(x = category, y = mean_value)) +
ggplot2::geom_col(fill = "steelblue") +
ggplot2::labs(
title = "Mean Values by Category",
x = "Category",
y = "Mean Value"
) +
ggplot2::theme_minimal()- Test public functions only: Internal utilities should be tested through public interfaces
- Precise, focused tests: Each test should verify one specific behavior
- High coverage: Maintain at least 80% test coverage as measured by
covr - Integration over units: Test realistic usage patterns
- Test Dependencies.: Add
testthat::skip_if_not_installed(package_name)only for dependencies inSuggestsor related to tests cases
- Server functions: Test with
shiny::testServer() - UI functions: Test basic usage with regular testing (class checks,
error generation, snapshots, regexp search). Test UI scenarios and
interactions with
teal:::TealAppDriver(based onshinytest2::AppDriver) for integration testing - Reactive behavior: Test reactive chains and side effects
- One test file per R file:
test-module_example.Rformodule_example.R - Descriptive test names: Clearly describe what is being tested
- End to end test names:
test-shinytest2-module_example.Rformodule_example.R - Logical grouping: Group related tests using
describe()and individual tests withit()when beneficial - Test data: Create minimal test datasets, avoid external dependencies
README.md: Clear overview, installation, basic usage examples- Vignettes: Comprehensive guides for complex functionality
- Function documentation: All exported functions must have
roxygen2documentation NEWS.md: Detailed changelog of features, bugs and miscellanea changes affecting the users
Do not change versions on your own. There is a CI/CD workflow that
manages the versions automatically on the main branch.
Prefer to reuse templates from r.pkg.template. Main checks in place are:
check.yaml: R CMD check, unit tests, coveragedocs.yaml: Documentation building and deploymentaudit.yaml: Security and dependency auditingpkgdown.yaml: Website generation
- Test Coverage: ≥80% line coverage
- Linting: No lint violations using configured
.lintr - Documentation: 100% of exports documented
- Pull Request Reviews: All changes require human review and approval
- Automated Checks: CI must pass before merging
- Breaking Changes: Require special consideration and communication
- Documentation Updates: Must accompany functional changes
- Shiny Reactivity: Minimize unnecessary reactive computations
- Data Processing: Use efficient data manipulation patterns
- Memory Usage: Consider memory implications for large datasets
- Loading Time: Optimize package loading and module initialization
- Long-term Support: Maintain backward compatibility when possible
- Dependencies: Minimal and justified dependencies only
- Deprecation: Use
lifecyclepackage for function deprecation