Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vignette custom modules #980

Merged
merged 4 commits into from
Nov 30, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 61 additions & 66 deletions vignettes/creating-custom-modules.Rmd
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Creating Custom Modules"
author: "Nikolas Burkoff"
author: "NEST CoreDev"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Creating Custom Modules}
Expand All @@ -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:

Expand All @@ -21,16 +22,17 @@ example_module <- function(label = "example teal module") {
module(
label,
server = function(id, data) {
checkmate::assert_class(data, "tdata")
checkmate::assert_class(data, "reactive")
checkmate::assert_class(isolate(data()), "teal_data")
moduleServer(id, function(input, output, session) {
output$text <- renderPrint(data[[input$dataname]]())
output$text <- renderPrint(data()[[input$dataname]])
})
},
ui = function(id, data) {
chlebowa marked this conversation as resolved.
Show resolved Hide resolved
ns <- NS(id)
teal.widgets::standard_layout(
output = verbatimTextOutput(ns("text")),
encoding = selectInput(ns("dataname"), "Choose a dataset", choices = names(data))
encoding = selectInput(ns("dataname"), "Choose a dataset", choices = datanames(data))
)
},
datanames = "all"
Expand All @@ -44,9 +46,13 @@ 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
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
Expand All @@ -66,7 +72,8 @@ 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 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.

## A More Complicated Example

Expand All @@ -84,18 +91,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
ui_histogram_example <- function(id, histogram_var) {
# allows for selecting dataset and one of its numeric variables
ui_histogram_example <- function(id) {
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
Expand All @@ -104,45 +107,49 @@ 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
srv_histogram_example <- function(id, data, histogram_var) {
checkmate::assert_class(data, "tdata")
# presents datasets and numeric variables for selection
# displays a histogram of the selected variable
srv_histogram_example <- function(id, data) {
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 dataset and variable choices
# each selection stored in separate reactive expression
output$datasets <- renderUI({
selectInput(ns("datasets"), "select dataset", datanames(data()))
})
dataset <- reactive(input$datasets)
output$numerics <- renderUI({
req(dataset())
nums <- vapply(data()[[dataset()]], is.numeric, logical(1L))
selectInput(ns("numerics"), "select numeric variable", names(nums[nums]))
})
selected <- reactive(input$numerics)

# the reactive which adds the code to plot the histogram into the qenv
# add plot code
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 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()
)
)
)
validate(need(length(dataset()) == 1L, "Please select a dataset"))
validate(need(length(selected()) == 1L, "Please select a variable"))
req(selected() %in% names(data()[[dataset()]]))

# evaluate plotting expression within data
# inject input values into plotting expression
within(
data(),
p <- hist(dataset[, selected], las = 1),
dataset = as.name(dataset()), selected = selected()
)
})

# shiny component to view
# view plot
output$plot <- renderPlot({
plot_code_q()[["p"]]
})

# Show the R code when user clicks 'Show R Code' button
# code upon clicking 'Show R Code' button
teal.widgets::verbatim_popup_srv(
id = "rcode",
verbatim_content = reactive(teal.code::get_code(plot_code_q())),
Expand All @@ -151,17 +158,12 @@ srv_histogram_example <- function(id, data, histogram_var) {
})
}

# the function which creates the teal module for users
tm_histogram_example <- function(label, histogram_var) {
checkmate::assert_character(label)
checkmate::assert_class(histogram_var, "data_extract_spec")

# function that creates module instance to use in `teal` app
tm_histogram_example <- function(label) {
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"
)
}
Expand All @@ -176,19 +178,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),
modules = tm_histogram_example(label = "Histogram Module"),
header = "Simple app with custom histogram module"
)

Expand All @@ -199,7 +190,11 @@ 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.