Adding Support for Reporting to Custom Modules
NEST CoreDev
Source:vignettes/adding-support-for-reporting.Rmd
adding-support-for-reporting.Rmd
Introduction
The teal
package offers an integrated reporting feature
utilizing the teal.reporter
package. For a comprehensive
explanation of the reporting functionality itself, please refer to the
documentation therein.
This article is intended for module developers and aims to provide
guidance on enhancing a custom teal
module with an
automatic reporting feature. This enhancement enables users to
incorporate snapshots of the module outputs into a report which can then
be reviewed in another module automatically provided by
teal
. Thus the app user can interact with the report.
The responsibilities of a module developer include:
- Adding support for reporting to their module.
- Specifying the outputs that constitute a snapshot of their module.
The entire life cycle of objects involved in creating the report and
configuring the module to preview the report is handled by
teal
.
Custom module
Let us consider an example module, based on the example module from
teal
:
library(teal)
my_module <- function(label = "example teal module") {
module(
label = label,
server = function(id, data) {
assert_reactive(data)
checkmate::assert_class(isolate(data()), "teal_data")
moduleServer(id, function(input, output, session) {
updateSelectInput(session, "dataname", choices = isolate(names(data())))
output$dataset <- renderPrint({
req(input$dataname)
data()[[input$dataname]]
})
})
},
ui = function(id) {
ns <- NS(id)
sidebarLayout(
sidebarPanel(selectInput(ns("dataname"), "Choose a dataset", choices = NULL)),
mainPanel(verbatimTextOutput(ns("dataset")))
)
}
)
}
Using teal
, you can launch this example module with the
following:
app <- init(
data = teal_data(IRIS = iris, MTCARS = mtcars),
modules = my_module()
)
if (interactive()) shinyApp(app$ui, app$server)
Add support for reporting
Modify the declaration of the server function
The first step is to add an additional argument to the server
function declaration - reporter
. This informs
teal
that the module requires reporter
, and it
will be included when the module is called. See below:
my_module_with_reporting <- function(label = "example teal module") {
module(
label = label,
server = function(id, data, reporter) {
moduleServer(id, function(input, output, session) {
updateSelectInput(session, "dataname", choices = isolate(names(data())))
output$dataset <- renderPrint({
req(input$dataname)
data()[[input$dataname]]
})
})
},
ui = function(id) {
ns <- NS(id)
sidebarLayout(
sidebarPanel(selectInput(ns("dataname"), "Choose a dataset", choices = NULL)),
mainPanel(verbatimTextOutput(ns("dataset")))
)
}
)
}
With these modifications, the module is now ready to be launched with
teal
:
## Initializing reporter_previewer_module
if (interactive()) shinyApp(app$ui, app$server)
teal
adds another tab to the application, titled
Report previewer
. However, there is no visible change in
how the module operates and appears and the user cannot add content to
the report from this module. That requires inserting UI and server
elements of the teal.reporter
module into the module
body.
Insert teal.reporter
module
The UI and the server logic necessary for adding cards from
my_module_with_reporting
to the report are provided by
teal.reporter::simple_reporter_ui
and
teal.reporter::simple_reporter_srv
.
my_module_with_reporting <- function(label = "example teal module") {
module(
label = label,
server = function(id, data, reporter) {
moduleServer(id, function(input, output, session) {
teal.reporter::simple_reporter_srv(
id = "reporter",
reporter = reporter,
card_fun = function(card) card
)
updateSelectInput(session, "dataname", choices = isolate(names(data())))
output$dataset <- renderPrint({
req(input$dataname)
data()[[input$dataname]]
})
})
},
ui = function(id) {
ns <- NS(id)
sidebarLayout(
sidebarPanel(
teal.reporter::simple_reporter_ui(ns("reporter")),
selectInput(ns("dataname"), "Choose a dataset", choices = NULL)
),
mainPanel(verbatimTextOutput(ns("dataset")))
)
}
)
}
This updated module is now ready to be launched:
## Initializing reporter_previewer_module
if (interactive()) shinyApp(app$ui, app$server)
A new piece of UI
has been added, and the buttons are
clickable. The user can now add a card to the report and view it in the
Report previewer
module but the preview is still empty
since we have not instructed our module what to put on the card.
Add content to the card
To add content to a card, we will utilize the public API exposed by
the TealReportCard
class. The
teal.reporter::simple_reporter_srv
module accepts the
card_fun
argument that determines the appearance of the
output from our custom module. ReportCard
and its
derivatives allow the sequential addition of content according to the
order of method calls. To explore the content, we can use the
$get_content
method. For further details, refer to the
documentation of TealReportCard
and
teal.reporter::ReportCard
.
We will add simple text to the card by modifying the
card_fun
argument passed to
teal.reporter::simple_reporter_srv
. The function must
return the card
object, otherwise errors may occur in
teal
.
custom_function <- function(card = teal.reporter::ReportCard$new()) {
card$append_text("This is content from a custom teal module!")
card
}
my_module_with_reporting <- function(label = "example teal module") {
module(
label = label,
server = function(id, data, reporter) {
moduleServer(id, function(input, output, session) {
teal.reporter::simple_reporter_srv(
id = "reporter",
reporter = reporter,
card_fun = custom_function
)
updateSelectInput(session, "dataname", choices = isolate(names(data())))
output$dataset <- renderPrint({
req(input$dataname)
data()[[input$dataname]]
})
})
},
ui = function(id) {
ns <- NS(id)
sidebarLayout(
sidebarPanel(
teal.reporter::simple_reporter_ui(ns("reporter")),
selectInput(ns("dataname"), "Choose a dataset", choices = NULL)
),
mainPanel(verbatimTextOutput(ns("dataset")))
)
}
)
}
## Initializing reporter_previewer_module
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 the addition of tables, charts,
and more. For more information, explore the API of
teal.reporter::ReportCard
to learn about the supported
content types.
TealReportCard
teal
exports the TealReportCard
class,
which extends the teal.reporter::ReportCard
class and
provides several convenient methods to facilitate working with
teal
features like the filter panel or source code. For
more details, refer to the documentation of
TealReportCard
.
To support TealReportCard
, the function that is passed
to teal.reporter::simple_reporter_srv
must define a default
value for the card, as shown below:
custom_function <- function(card = TealReportCard$new()) {
# ... some code ... #
card
}
Without this definition, the API of TealReportCard
will
not be available within the function.
Example
In conclusion, we have demonstrated how to build a standard
teal
app with code reproducibility and reporter
functionalities. Note that the server function requires the
filter_panel_api
argument so that the filter panel state
can be added to the report.
In the final example, we have incorporated teal.code
snippets. teal.code
is an R
library that
offers utilities for storing code and associating it with an execution
environment. This allows ReporterCard
to store the code
necessary to generate the table along with the table itself. To learn
more about teal.code
see the vignette
qenv
in teal.code
.
##
example_reporter_module <- function(label = "Example") {
module(
label = label,
server = function(id, data, reporter, filter_panel_api) {
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelApi")
moduleServer(id, function(input, output, session) {
updateSelectInput(session, "dataname", choices = isolate(names(data())))
dat <- reactive(data()[[input$dataname]])
observe({
req(dat())
updateSliderInput(session, "nrow", max = nrow(dat()), value = floor(nrow(dat()) / 5))
})
table_q <- reactive({
req(input$dataname)
req(input$nrow)
within(
data(),
result <- head(dataset, nrows),
dataset = as.name(input$dataname),
nrows = input$nrow
)
})
output$table <- renderTable(table_q()[["result"]])
### REPORTER
card_fun <- function(card = teal.reporter::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(teal.code::get_code(table_q()), "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) {
ns <- NS(id)
sidebarLayout(
sidebarPanel(selectInput(ns("dataname"), "Choose a dataset", choices = NULL)),
mainPanel(
teal.reporter::simple_reporter_ui(ns("reporter")),
verbatimTextOutput(ns("dataset"))
)
)
sidebarLayout(
sidebarPanel(
tags$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 = NULL),
sliderInput(ns("nrow"), "Number of rows", min = 1, max = 1, value = 1, step = 1)
),
mainPanel(tableOutput(ns("table")))
)
}
)
}
app <- init(
data = teal_data(AIR = airquality, IRIS = iris),
modules = list(
example_reporter_module(label = "with Reporter"),
my_module(label = "without Reporter")
),
filter = teal_slices(teal_slice(dataname = "AIR", varname = "Temp", selected = c(72, 85))),
header = "Example teal app with reporter"
)
## Initializing reporter_previewer_module
if (interactive()) shinyApp(app$ui, app$server)