From 5c85c9a1cc49082635bff66e1ec6a03e14076ef6 Mon Sep 17 00:00:00 2001 From: Aleksander Chlebowski Date: Mon, 27 Nov 2023 13:30:41 +0100 Subject: [PATCH] rewrite example and minor corrections --- vignettes/creating-custom-modules.Rmd | 96 ++++++++++++--------------- 1 file changed, 41 insertions(+), 55 deletions(-) diff --git a/vignettes/creating-custom-modules.Rmd b/vignettes/creating-custom-modules.Rmd index dbaa62b1e4..7cd208fbc9 100644 --- a/vignettes/creating-custom-modules.Rmd +++ b/vignettes/creating-custom-modules.Rmd @@ -10,7 +10,8 @@ vignette: > ## Introduction -The `teal` framework provides a large number of analysis modules to be incorporated into `teal` applications. However, it is also possible to create your own modules using the `module` function. +The `teal` framework provides a large number of analysis modules to be incorporated into `teal` applications. +However, it is also possible to create your own modules using the `module` function. Here is an implementation of a simple module: @@ -44,10 +45,11 @@ which can be added into `teal` apps using `example_module(label = "Label for tab ### UI function -This function contains the UI required for the module. It should be a function with at least the arguments `id`. It can also contain the argument `data` for access to the application data. See the server section below for more details. +This function contains the UI required for the module. It should be a function with at least the arguments `id`. +It can also contain the argument `data` for access to the application data. See the server section below for more details. -The UI function can contain standard UI components alongside additional widgets provided by the `teal.widgets` package. In the example above we are using the `standard_layout` function of `teal.widgets` which generates a layout -including an encoding panel on the left and main output covering the rest of the module's UI. +The UI function can contain standard UI components alongside additional widgets provided by the `teal.widgets` package. +In the example above we are using the `standard_layout` function of `teal.widgets` which generates a layout including an encoding panel on the left and main output covering the rest of the module's UI. ### Server function @@ -66,15 +68,13 @@ function(id, ``` -When used inside a `teal` application called with `init`, the `data` argument is a named list of reactive `data.frame`s 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. +When used inside a `teal` application called with `init`, the `data` argument receives a `teal_data` object containing the data (`data.frame`s) after having been filtered through the filter panel. ## A More Complicated Example The `teal` framework also provides: - A way to create modules which then generate the R code needed to reproduce their outputs; these modules use the [`teal.code`](https://insightsengineering.github.io/teal.code/) package. -- A way extract from and merge related datasets using the [`teal.transform`](https://insightsengineering.github.io/teal.transform/) package. -- A way to allow app creators to customize your modules also using `teal.transform`. The annotated example below demonstrates these features within a simple histogram module, allowing app developers to choose the data and columns that their app users can select for display in a histogram. @@ -84,18 +84,14 @@ See the package and function documentation for further details. library(teal) # ui function for the module -# histogram_var is a teal.transform::data_extract_spec object -# specifying which columns of which datasets users can choose +# allows for selecting dataset and a numeric variable from it ui_histogram_example <- function(id, histogram_var) { ns <- NS(id) teal.widgets::standard_layout( output = plotOutput(ns("plot")), encoding = div( - teal.transform::data_extract_ui( - id = ns("histogram_var"), - label = "Variable", - data_extract_spec = histogram_var - ) + uiOutput(ns("datasets")), + uiOutput(ns("numerics")) ), # we have a show R code button to show the code needed # to generate the histogram @@ -104,37 +100,38 @@ ui_histogram_example <- function(id, histogram_var) { } # server function for the module -# histogram_var is a teal.transform::data_extract_spec object -# specifying which columns of which datasets users can choose +# presents choices for selection of dataset and then numeric variable +# plots histogram of selected variable +# records code srv_histogram_example <- function(id, data, histogram_var) { - checkmate::assert_class(data, "tdata") + checkmate::assert_class(data, "reactive") + checkmate::assert_class(isolate(data()), "teal_data") moduleServer(id, function(input, output, session) { - # get the selected dataset and column from the UI - extracted <- teal.transform::data_extract_srv( - id = "histogram_var", - datasets = data, - data_extract_spec = histogram_var, - join_keys = teal.data::join_keys(data) - ) + ns <- session$ns - dataname <- reactive(extracted()$dataname) - selected <- reactive(extracted()$select) + # present data sets columns for selection + output$datasets <- renderUI({ + selectInput(ns("datasets"), "select dataset", choices = datanames(data())) + }) + dataset <- reactive(input$datasets) + + # present numeric columns for selection + output$numerics <- renderUI({ + req(dataset()) + dataset <- data()[[dataset()]] + nums <- vapply(dataset, is.numeric, logical(1L)) + selectInput(ns("numerics"), "select numeric variable", choices = names(nums[nums])) + }) + selected <- reactive(input$numerics) # 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")) + validate(need(length(dataset()) == 1, "Please select a dataset")) + validate(need(length(selected()) == 1, "Please select a variable")) + req(selected() %in% names(data()[[dataset()]])) # take the filtered data from the data object and add it into the qenv environment - teal.code::new_qenv(tdata2env(data), code = get_code_tdata(data)) %>% - teal.code::eval_code( - substitute( - expr = p <- hist(dataname[, selected]), - env = list( - dataname = as.name(dataname()), - selected = selected() - ) - ) - ) + within(data(), p <- hist(dataset[, selected], las = 1), dataset = as.name(dataset()), selected = selected()) }) # shiny component to view @@ -152,16 +149,13 @@ srv_histogram_example <- function(id, data, histogram_var) { } # the function which creates the teal module for users -tm_histogram_example <- function(label, histogram_var) { +tm_histogram_example <- function(label) { checkmate::assert_character(label) - checkmate::assert_class(histogram_var, "data_extract_spec") module( label = label, server = srv_histogram_example, ui = ui_histogram_example, - ui_args = list(histogram_var = histogram_var), - server_args = list(histogram_var = histogram_var), datanames = "all" ) } @@ -176,19 +170,8 @@ An example `teal` application using this module is shown below: library(teal) app <- init( - data = teal_data( - IRIS = iris, - code = "IRIS <- iris" - ), - modules = tm_histogram_example( - label = "Simple Module", - histogram_var = data_extract_spec( - dataname = "IRIS", - select = select_spec( - choices = c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width") - ) - ) - ), + data = teal_data(IRIS = iris, NPK = npk, code = expression(IRIS <- iris, NPK <- npk)), + modules = tm_histogram_example(label = "Simple Module"), header = "Simple app with custom histogram module" ) @@ -199,7 +182,10 @@ if (interactive()) { ## `shiny` input cycle -When `teal` modules are run inside the `init` the initial shiny input cycle is empty for each of them. In practice, this means that some inputs might be initialized with `NULL` value, unnecessary triggering some observers. A developer has to be aware of this situation as often it will require `shiny::req` or `ignoreInit` argument in observers or `reactive` expressions. This side effect is caused by the `shiny::insertUI` function. We are aware of this inconvenience and have already started to look for a solution. +When `teal` modules are run inside the `init` the initial shiny input cycle is empty for each of them. +In practice, this means that some inputs might be initialized with `NULL` value, unnecessary triggering some observers. +A developer has to be aware of this situation as often it will require `shiny::req` or `ignoreInit` argument in observers or `reactive` expressions. +This side effect is caused by the `shiny::insertUI` function. We are aware of this inconvenience and have already started to look for a solution. ## Adding reporting to a module Refer to `vignette("adding_support_for_reporting")` to read about adding support for reporting in your `teal` module.