Adding support for Reporting to custom modules
NEST CoreDev
2022-05-23
adding-support-for-reporting.Rmd
Introduction
teal
supports an in-built reporting feature using the
vignette("teal.reporter")
package. Head to its
documentation if you want to know more about the reporting itself.
This article is targeted to module developers and explains how to
enhance a custom teal
module with an automatic reporting
feature. The enhancement allows users to add snapshots of the module
outputs to a report and review it in another module that is
automatically provided by teal
and designed to let users
interact with the report.
The responsibilities of a module developer include:
- adding the support for reporting to their module
- specifying outputs that go into a snapshot of their module.
The lifecycle of objects involved in creation of the report and
setting up the module to preview the report is handled by
teal
.
Custom module
Let us consider the example module from teal
:
library(teal)
teal_example_module <- function(label = "example teal module") {
checkmate::assert_string(label)
module(
label,
server = function(id, data) {
checkmate::assert_class(data, "tdata")
moduleServer(id, function(input, output, session) {
output$text <- renderPrint(data[[input$dataname]]())
})
},
ui = function(id, data) {
ns <- NS(id)
teal.widgets::standard_layout(
output = verbatimTextOutput(ns("text")),
encoding = selectInput(ns("dataname"), "Choose a dataset", choices = names(data))
)
},
filters = "all"
)
}
teal
can launch this example module with the following
lines:
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
. See below:
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]]())
})
},
ui = function(id, data) {
ns <- NS(id)
teal.widgets::standard_layout(
output = verbatimTextOutput(ns("text")),
encoding = selectInput(ns("dataname"), "Choose a dataset", choices = names(data))
)
},
filters = "all"
)
}
Such a module is ready to be launched again by teal
:
app <- init(
data = teal_data(
dataset("IRIS", iris),
dataset("MTCARS", mtcars)
),
modules = example_module_with_reporting()
)
if (interactive()) shinyApp(app$ui, app$server)
teal
added another tab to the application titled
Report previewer
but besides that there appears to be no
change in how the module works and what it looks like. Users cannot
interact with it to add to the report from this module yet. Thankfully,
teal.reporter
provides ui
and
server
objects that support that.
Introduce the new UI
and the supporting
shiny
modules
We will use teal.reporter::simple_reporter_ui
and
teal.reporter::simple_reporter_srv
to set up the
UI
and the shiny
module that allow users to
add content from example_module_with_reporting
to the
report.
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) {
teal.reporter::simple_reporter_srv(
id = "reporter",
reporter = reporter,
card_fun = function(card) card
)
output$text <- renderPrint(data[[input$dataname]]())
})
},
ui = function(id, data) {
ns <- NS(id)
teal.widgets::standard_layout(
output = tagList(
teal.reporter::simple_reporter_ui(ns("reporter")),
verbatimTextOutput(ns("text"))
),
encoding = selectInput(ns("dataname"), "Choose a dataset", choices = names(data))
)
},
filters = "all"
)
}
This module is ready to be launched:
app <- init(
data = teal_data(
dataset("IRIS", iris),
dataset("MTCARS", mtcars)
),
modules = example_module_with_reporting()
)
if (interactive()) shinyApp(app$ui, app$server)
The new UI
is visible and the buttons are clickable. An
application user can review the card in the
Report previewer
module and it will appear empty because,
as a module developer, we have not yet added any content to the card of
our module.
Add content to the card
We will use the public API exposed by the
teal.reporter::ReportCard
and
teal.reporter::TealReportCard
classes to add content to a
card. The teal.reporter::simple_reporter_srv
module accepts
the card_fun
argument that dictates the way the output from
our custom module will look. ReportCard
and its derivatives
add the content sequentially according to the order of the calls to its
methods. The content itself can be explored by calling the
$get_content
method. If you want to learn more, check out
teal.reporter::ReportCard
’s documentation.
We will add simple text to the card by modifying the
card_fun
argument passed to
teal.reporter::simple_reporter_srv
. Make sure to return the
card
object from the passed function, otherwise
teal
might encounter errors.
custom_function <- function(card = teal.reporter::ReportCard$new()) {
card$append_text("This is content from a custom teal module!")
card
}
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) {
teal.reporter::simple_reporter_srv(id = "simpleReporter", reporter = reporter, card_fun = custom_function)
output$text <- renderPrint(data[[input$dataname]]())
})
},
ui = function(id, data) {
ns <- NS(id)
teal.widgets::standard_layout(
output = tagList(
teal.reporter::simple_reporter_ui(ns("simpleReporter")),
verbatimTextOutput(ns("text"))
),
encoding = selectInput(ns("dataname"), "Choose a dataset", choices = names(data))
)
},
filters = "all"
)
}
app <- init(
data = teal_data(
dataset("IRIS", iris),
dataset("MTCARS", mtcars)
),
modules = example_module_with_reporting()
)
if (interactive()) shinyApp(app$ui, app$server)
Now, an application user can see the text added by
custom_function
in the Report previewer
module.
Add non-text content to the card
teal.reporter
supports adding tables, charts and more.
Explore the API of teal.reporter::ReportCard
to learn what
types of content are supported.
TealReportCard
teal.reporter
exports a teal
-specific
ReportCard
class that has a number of convenience methods
built into it to make working with teal
objects like the
filter panel or source code easier. Check out its documentation to learn
more at teal.reporter::TealReportCard
.
To support TealReportCard
, the function that is passed
to teal.reporter::simple_reporter_srv
needs to define a
default value for the card, like below:
custom_fun <- function(card = teal.reporter::TealReportCard$new()) {
card
}
Otherwise, the API of TealReportCard
will not be
available inside the function.
Example
Summing up, we could build a regular teal app with code
reproducibility and reporter functionality. Note that the
server
function requires the filter_panel_api
argument so that the filter panel state can be added to the report.
library(teal)
library(teal.reporter)
example_reporter_module <- function(label = "Example") {
module(
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({
sliderInput(session$ns("nrow"), "Number of rows:", 1, nrow(data[[input$dataname]]()), 10)
})
table_q <- reactive({
req(input$nrow)
teal.code::new_qenv(tdata2env(data), code = get_code(data)) %>%
teal.code::eval_code(
substitute(
result <- head(data, nrows),
list(
data = as.name(input$dataname),
nrows = input$nrow
)
)
)
})
output$table <- renderTable(table_q()[["result"]])
### REPORTER
card_fun <- function(card = ReportCard$new(), comment) {
card$set_name("Table Module")
card$append_text(paste("Selected dataset", input$dataname), "header2")
card$append_text("Selected Filters", "header3")
if (with_filter) {
card$append_text(filter_panel_api$get_filter_state(), "verbatim")
}
card$append_text("Encoding", "header3")
card$append_text(
yaml::as.yaml(
stats::setNames(lapply(c("dataname", "nrow"), function(x) input[[x]]), c("dataname", "nrow"))
),
"verbatim"
)
card$append_text("Module Table", "header3")
card$append_table(table_q()[["result"]])
card$append_text("Show R Code", "header3")
card$append_text(paste(teal.code::get_code(table_q()), collapse = "\n"), "verbatim")
if (!comment == "") {
card$append_text("Comment", "header3")
card$append_text(comment)
}
card
}
teal.reporter::add_card_button_srv("addReportCard", reporter = reporter, card_fun = card_fun)
teal.reporter::download_report_button_srv("downloadButton", reporter = reporter)
teal.reporter::reset_report_button_srv("resetButton", reporter)
###
})
},
ui = function(id, data) {
ns <- NS(id)
teal.widgets::standard_layout(
output = tableOutput(ns("table")),
encoding = tagList(
div(
teal.reporter::add_card_button_ui(ns("addReportCard")),
teal.reporter::download_report_button_ui(ns("downloadButton")),
teal.reporter::reset_report_button_ui(ns("resetButton"))
),
selectInput(ns("dataname"), "Choose a dataset", choices = names(data)),
uiOutput(ns("nrow_ui"))
)
)
},
filters = "all"
)
}
app <- init(
data = teal_data(
dataset("AIR", airquality, code = "data(airquality); AIR <- airquality"),
dataset("IRIS", iris, code = "data(iris); IRIS <- iris"),
check = FALSE
),
modules = list(
example_reporter_module(label = "with Reporter"),
example_module(label = "without Reporter")
),
filter = list(AIR = list(Month = c(5, 5))),
header = "Example teal app with reporter"
)
if (interactive()) shinyApp(app$ui, app$server)