diff --git a/DESCRIPTION b/DESCRIPTION index beeb8c04e0..065b806ca3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -91,6 +91,7 @@ Collate: 'modules_debugging.R' 'reporter_previewer_module.R' 'show_rcode_modal.R' + 'tdata.R' 'teal.R' 'utils.R' 'validations.R' diff --git a/NAMESPACE b/NAMESPACE index cd11e02fa6..79643f73d2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,10 @@ # Generated by roxygen2: do not edit by hand +S3method(get_code,tdata) +S3method(get_join_keys,default) +S3method(get_join_keys,tdata) +S3method(get_metadata,default) +S3method(get_metadata,tdata) S3method(is_arg_used,"function") S3method(is_arg_used,default) S3method(is_arg_used,teal_module) @@ -20,6 +25,8 @@ export(.log) export(bookmarkableShinyApp) export(default_filter) export(example_module) +export(get_join_keys) +export(get_metadata) export(get_rcode) export(get_rcode_srv) export(get_rcode_ui) @@ -27,10 +34,12 @@ export(init) export(log_app_usage) export(module) export(modules) +export(new_tdata) export(reporter_previewer_module) export(root_modules) export(show_rcode_modal) export(srv_teal_with_splash) +export(tdata2env) export(ui_teal_with_splash) export(validate_has_data) export(validate_has_elements) diff --git a/NEWS.md b/NEWS.md index 0e72a05e42..96103f9658 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,7 +3,7 @@ ### Enhancements * Added option to choose which variables can be filtered in the filter panel by using the `filterable` attributes for the per-dataset lists in the `filter` argument of `init`. -* `teal_module` having `data` argument in its arguments will receive a list of reactive filter data with `"code"` and `"join_keys"` attributes. +* `teal_module` having `data` argument in its arguments of type `tdata`. Modules receive this object (a list of reactive filtered datasets with `"code"` and `"join_keys"` attributes). * Updated the internals of `module_teal` to reflect changes in `teal.slice`. * `teal_module` having `filter_panel_api` argument in its arguments will receive a `FilterPanelAPI` object. diff --git a/R/example_module.R b/R/example_module.R index 42537fe24d..7ed53c2fdd 100644 --- a/R/example_module.R +++ b/R/example_module.R @@ -20,6 +20,7 @@ example_module <- function(label = "example teal module") { module( label, server = function(id, data) { + checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { output$text <- renderPrint(data[[input$dataname]]()) }) diff --git a/R/module_nested_tabs.R b/R/module_nested_tabs.R index 8b5f6c5bb0..6cec98fdaa 100644 --- a/R/module_nested_tabs.R +++ b/R/module_nested_tabs.R @@ -228,9 +228,9 @@ srv_nested_tabs.teal_module <- function(id, datasets, modules, reporter) { reactive(modules) } -#' Convert `FilteredData` to reactive list of data +#' Convert `FilteredData` to reactive list of datasets of the `tdata` type. #' -#' Converts `FilteredData` object to list of data containing datasets needed for specific module. +#' Converts `FilteredData` object to `tdata` object containing datasets needed for a specific module. #' Please note that if module needs dataset which has a parent, then parent will be also returned. #' #' @param module (`teal_module`) module where needed filters are taken from @@ -255,10 +255,13 @@ srv_nested_tabs.teal_module <- function(id, datasets, modules, reporter) { } ) - # code from previous stages - attr(data, "code") <- get_datasets_code(datanames, datasets) + metadata <- lapply(datanames, datasets$get_metadata) + names(metadata) <- datanames - # join_keys - attr(data, "join_keys") <- datasets$get_join_keys() - data + new_tdata( + data, + reactive(get_datasets_code(datanames, datasets)), + datasets$get_join_keys(), + metadata + ) } diff --git a/R/modules_debugging.R b/R/modules_debugging.R index 83a8f3232e..5b8a59a108 100644 --- a/R/modules_debugging.R +++ b/R/modules_debugging.R @@ -10,8 +10,6 @@ #' and avoids session restarts! #' #' @param label `character` label of module -#' @param active_datanames `character vector` datanames shown in filter panel; -#' can be `"all"` to mean all available datasets #' @keywords internal #' #' @examples @@ -34,19 +32,19 @@ #' ), #' header = "Simple teal app" #' ) -#' \dontrun{ -#' shinyApp(app$ui, app$server) +#' if (interactive()) { +#' shinyApp(app$ui, app$server) #' } -filter_calls_module <- function(label = "Filter Calls Module", active_datanames = "all") { # nolint +filter_calls_module <- function(label = "Filter Calls Module") { # nolint checkmate::assert_string(label) - checkmate::check_character(active_datanames, min.len = 1, any.missing = FALSE) module( label = label, - server = function(input, output, session, datasets) { + server = function(input, output, session, data) { + checkmate::assert_class(data, "tdata") + output$filter_calls <- renderText({ - active_datanames <- datasets$handle_active_datanames(active_datanames) - teal.slice::get_filter_expr(datasets, datanames = active_datanames) + get_code(data) }) }, ui = function(id, ...) { @@ -56,6 +54,6 @@ filter_calls_module <- function(label = "Filter Calls Module", active_datanames verbatimTextOutput(ns("filter_calls")) ) }, - filters = active_datanames + filters = "all" ) } diff --git a/R/tdata.R b/R/tdata.R new file mode 100644 index 0000000000..d1bca33191 --- /dev/null +++ b/R/tdata.R @@ -0,0 +1,131 @@ +#' Create a new `tdata` object which contains +#' +#' - `data` a `reactive` list of data.frames (or `MultiAssayExperiment`) with attributes +#' i) `code` (`reactive`) containing code used to generate the data +#' and ii) join_keys (`JoinKeys`) containing the relationships between +#' the data iii) metadata (`named list`) containing any metadata associated with +#' the data frames +#' +#' @param data A `named list` of `data.frames` (or `MultiAssayExperiment`) +#' which optionally can be `reactive`. +#' Inside this object all of these items will be made `reactive`. +#' @param code A `character` (or `reactive` which evaluates to a `character`) containing +#' the code used to generate the data. This should be `reactive` if the code is changing +#' during a reactive context (e.g. if filtering changes the code). Inside this +#' object `code` will be made reactive +#' @param join_keys A `teal.data::JoinKeys` object containing relationships between the +#' datasets. +#' @param metadata A `named list` each element contains a list of metadata about the named data.frame +#' Each element of these list should be atomic and length one. +#' @return A `tdata` object +#' @examples +#' +#' data <- new_tdata( +#' data = list(iris = iris, mtcars = reactive(mtcars), dd = data.frame(x = 1:10)), +#' code = "iris <- iris +#' mtcars <- mtcars +#' dd <- data.frame(x = 1:10)", +#' metadata = list(dd = list(author = "NEST"), iris = list(version = 1)) +#' ) +#' +#' # Extract a data.frame +#' isolate(data[["iris"]]()) +#' +#' @export +new_tdata <- function(data, code = "", join_keys = NULL, metadata = NULL) { + checkmate::assert_list( + data, + any.missing = FALSE, names = "unique", + types = c("data.frame", "reactive", "MultiAssayExperiment") + ) + checkmate::assert_class(join_keys, "JoinKeys", null.ok = TRUE) + checkmate::assert_multi_class(code, c("character", "reactive")) + + checkmate::assert_list(metadata, names = "unique", null.ok = TRUE) + checkmate::assert_subset(names(metadata), names(data)) + for (m in metadata) teal.data::validate_metadata(m) + + if (is.reactive(code)) { + isolate(checkmate::assert_class(code(), "character")) + } + + #create reactive data.frames + for (x in names(data)) { + if (!is.reactive(data[[x]])) { + data[[x]] <- do.call(reactive, list(as.name(x)), envir = list2env(data[x])) + } else { + isolate(checkmate::assert_multi_class(data[[x]](), c("data.frame", "MultiAssayExperiment"))) + } + } + + # set attributes + attr(data, "code") <- if (is.reactive(code)) code else reactive(code) + attr(data, "join_keys") <- join_keys + attr(data, "metadata") <- metadata + + # set class + class(data) <- c("tdata", class(data)) + data +} + +#' Function to convert a `tdata` object to an `environment` +#' Any `reactives` inside `tdata` are first evaluated +#' @param `data` a `tdata` object +#' @return an `environment` +#' @examples +#' +#' data <- new_tdata( +#' data = list(iris = iris, mtcars = reactive(mtcars)), +#' code = "iris <- iris +#' mtcars = mtcars" +#' ) +#' +#' my_env <- isolate(tdata2env(data)) +#' +#' @export +tdata2env <- function(data) { # nolint + checkmate::assert_class(data, "tdata") + list2env(lapply(data, function(x) if (is.reactive(x)) x() else x)) +} + +#' @export +get_code.tdata <- function(data) { + # note teal.data which teal depends on defines the get_code method + attr(data, "code")() +} + +#' @export +get_join_keys <- function(data) { + UseMethod("get_join_keys", data) +} + + +#' @export +get_join_keys.tdata <- function(data) { + attr(data, "join_keys") +} + +#' @export +get_join_keys.default <- function(data) { + stop("get_join_keys function not implemented for this object") +} + +#' @export +get_metadata <- function(data, dataname) { + checkmate::assert_string(dataname) + UseMethod("get_metadata", data) +} + +#' @export +get_metadata.tdata <- function(data, dataname) { + metadata <- attr(data, "metadata") + if (is.null(metadata)) { + return(NULL) + } + metadata[[dataname]] +} + +#' @export +get_metadata.default <- function(data, dataname) { + stop("get_metadata function not implemented for this object") +} diff --git a/man/dot-datasets_to_data.Rd b/man/dot-datasets_to_data.Rd index 4821e3ffcd..71c31ea5ba 100644 --- a/man/dot-datasets_to_data.Rd +++ b/man/dot-datasets_to_data.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/module_nested_tabs.R \name{.datasets_to_data} \alias{.datasets_to_data} -\title{Convert \code{FilteredData} to reactive list of data} +\title{Convert \code{FilteredData} to reactive list of datasets of the \code{tdata} type.} \usage{ .datasets_to_data(module, datasets) } @@ -18,7 +18,7 @@ list of reactive datasets with following attributes: } } \description{ -Converts \code{FilteredData} object to list of data containing datasets needed for specific module. +Converts \code{FilteredData} object to \code{tdata} object containing datasets needed for a specific module. Please note that if module needs dataset which has a parent, then parent will be also returned. } \keyword{(`JoinKeys`)} diff --git a/man/filter_calls_module.Rd b/man/filter_calls_module.Rd index 3ceb67c161..dc49091cb8 100644 --- a/man/filter_calls_module.Rd +++ b/man/filter_calls_module.Rd @@ -4,13 +4,10 @@ \alias{filter_calls_module} \title{Dummy module to show the filter calls generated by the right encoding panel} \usage{ -filter_calls_module(label = "Filter Calls Module", active_datanames = "all") +filter_calls_module(label = "Filter Calls Module") } \arguments{ \item{label}{\code{character} label of module} - -\item{active_datanames}{\verb{character vector} datanames shown in filter panel; -can be \code{"all"} to mean all available datasets} } \description{ Please do not remove, this is useful for debugging teal without @@ -37,8 +34,8 @@ app <- init( ), header = "Simple teal app" ) -\dontrun{ -shinyApp(app$ui, app$server) +if (interactive()) { + shinyApp(app$ui, app$server) } } \keyword{internal} diff --git a/man/new_tdata.Rd b/man/new_tdata.Rd new file mode 100644 index 0000000000..b61a0d0da7 --- /dev/null +++ b/man/new_tdata.Rd @@ -0,0 +1,50 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tdata.R +\name{new_tdata} +\alias{new_tdata} +\title{Create a new \code{tdata} object which contains} +\usage{ +new_tdata(data, code = "", join_keys = NULL, metadata = NULL) +} +\arguments{ +\item{data}{A \verb{named list} of \code{data.frames} (or \code{MultiAssayExperiment}) +which optionally can be \code{reactive}. +Inside this object all of these items will be made \code{reactive}.} + +\item{code}{A \code{character} (or \code{reactive} which evaluates to a \code{character}) containing +the code used to generate the data. This should be \code{reactive} if the code is changing +during a reactive context (e.g. if filtering changes the code). Inside this +object \code{code} will be made reactive} + +\item{join_keys}{A \code{teal.data::JoinKeys} object containing relationships between the +datasets.} + +\item{metadata}{A \verb{named list} each element contains a list of metadata about the named data.frame +Each element of these list should be atomic and length one.} +} +\value{ +A \code{tdata} object +} +\description{ +\itemize{ +\item \code{data} a \code{reactive} list of data.frames (or \code{MultiAssayExperiment}) with attributes +i) \code{code} (\code{reactive}) containing code used to generate the data +and ii) join_keys (\code{JoinKeys}) containing the relationships between +the data iii) metadata (\verb{named list}) containing any metadata associated with +the data frames +} +} +\examples{ + +data <- new_tdata( + data = list(iris = iris, mtcars = reactive(mtcars), dd = data.frame(x = 1:10)), + code = "iris <- iris + mtcars <- mtcars + dd <- data.frame(x = 1:10)", + metadata = list(dd = list(author = "NEST"), iris = list(version = 1)) +) + +# Extract a data.frame +isolate(data[["iris"]]()) + +} diff --git a/man/tdata2env.Rd b/man/tdata2env.Rd new file mode 100644 index 0000000000..629e166294 --- /dev/null +++ b/man/tdata2env.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tdata.R +\name{tdata2env} +\alias{tdata2env} +\title{Function to convert a \code{tdata} object to an \code{environment} +Any \code{reactives} inside \code{tdata} are first evaluated} +\usage{ +tdata2env(data) +} +\arguments{ +\item{`data`}{a \code{tdata} object} +} +\value{ +an \code{environment} +} +\description{ +Function to convert a \code{tdata} object to an \code{environment} +Any \code{reactives} inside \code{tdata} are first evaluated +} +\examples{ + +data <- new_tdata( + data = list(iris = iris, mtcars = reactive(mtcars)), + code = "iris <- iris + mtcars = mtcars" +) + +my_env <- isolate(tdata2env(data)) + +} diff --git a/tests/testthat/test-module_nested_tabs.R b/tests/testthat/test-module_nested_tabs.R index 35ddcfd554..4b2aba55a9 100644 --- a/tests/testthat/test-module_nested_tabs.R +++ b/tests/testthat/test-module_nested_tabs.R @@ -311,7 +311,7 @@ testthat::test_that(".datasets_to_data returns required attributes", { # code testthat::expect_equal( - attr(data, "code")[1], + isolate(attr(data, "code")()[1]), "d1 <- data.frame(id = 1:5, pk = c(2, 3, 2, 1, 4), val = 1:5)\nd2 <- data.frame(id = 1:5, value = 1:5)\n\n" ) }) diff --git a/vignettes/adding-support-for-reporting.Rmd b/vignettes/adding-support-for-reporting.Rmd index 8a06cd28ef..dd52c3c1c6 100644 --- a/vignettes/adding-support-for-reporting.Rmd +++ b/vignettes/adding-support-for-reporting.Rmd @@ -34,6 +34,7 @@ teal_example_module <- function(label = "example teal module") { module( label, server = function(id, data) { + checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { output$text <- renderPrint(data[[input$dataname]]()) }) @@ -65,14 +66,14 @@ if (interactive()) shinyApp(app$ui, app$server) ## Add support for Reporting ### Change the declaration of the server function -The first step is to add another argument to the server function declaration - `reporter`. This needs to be the third -argument to the server function inside the `teal.module` call. See below: +The first step is to add another argument to the server function declaration - `reporter`. See below: ```{r} example_module_with_reporting <- function(label = "example teal module") { checkmate::assert_string(label) module( label, server = function(id, data, reporter) { + checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { output$text <- renderPrint(data[[input$dataname]]()) }) @@ -115,6 +116,7 @@ example_module_with_reporting <- function(label = "example teal module") { module( label, server = function(id, data, reporter) { + checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { teal.reporter::simple_reporter_srv( id = "reporter", @@ -177,6 +179,7 @@ example_module_with_reporting <- function(label = "example teal module") { module( label, server = function(id, data, reporter) { + checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { teal.reporter::simple_reporter_srv(id = "simpleReporter", reporter = reporter, card_fun = custom_function) output$text <- renderPrint(data[[input$dataname]]()) @@ -244,6 +247,7 @@ example_reporter_module <- function(label = "Example") { label, server = function(id, data, reporter, filter_panel_api) { with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelApi") + checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { dat <- reactive(data[[input$dataname]]()) output$nrow_ui <- renderUI({ @@ -253,7 +257,7 @@ example_reporter_module <- function(label = "Example") { table_q <- reactive({ req(input$nrow) - teal.code::new_quosure(env = data) %>% + teal.code::new_qenv(tdata2env(data), code = get_code(data)) %>% teal.code::eval_code( substitute( result <- head(data, nrows), diff --git a/vignettes/creating-custom-modules.Rmd b/vignettes/creating-custom-modules.Rmd index 112b333455..9f4e2db81c 100644 --- a/vignettes/creating-custom-modules.Rmd +++ b/vignettes/creating-custom-modules.Rmd @@ -23,6 +23,7 @@ example_module <- function(label = "example teal module") { module( label, server = function(id, data) { + checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { output$text <- renderPrint(data[[input$dataname]]()) }) @@ -70,7 +71,8 @@ function(id, ``` When used inside a teal application called with `teal::init` the `data` argument is a named list of reactive data.frames -containing the data after having been filtered through the filter panel. +containing the data after having been filtered through the filter panel. It is of the `tdata` type and can be created using +the `new_tdata` function. ## A More Complicated Example @@ -114,6 +116,7 @@ ui_histogram_example <- function(id, histogram_var) { # histogram_var is a teal.transform::data_extract_spec object # specifying which columns of which datasets users can choose srv_histogram_example <- function(id, data, histogram_var) { + checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { # get the selected dataset and column from the UI @@ -121,18 +124,18 @@ srv_histogram_example <- function(id, data, histogram_var) { id = "histogram_var", datasets = data, data_extract_spec = histogram_var, - join_keys = attr(data, "join_keys") + join_keys = get_join_keys(data) ) dataname <- reactive(extracted()$dataname) selected <- reactive(extracted()$select) - # the reactive which adds the code to plot the histogram into the quosure + # the reactive which adds the code to plot the histogram into the qenv plot_code_q <- reactive({ validate(need(length(selected) == 1, "Please select a variable")) - # take the filtered data from the data object and add it into the quosure environment - teal.code::new_quosure(env = data) %>% + # take the filtered data from the data object and add it into the qenv environment + teal.code::new_qenv(tdata2env(data), code = get_code(data)) %>% teal.code::eval_code( substitute( expr = p <- hist(dataname[, selected]),