Skip to contents

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:

app <- init(
  data = teal_data(
    dataset("IRIS", iris),
    dataset("MTCARS", mtcars)
  ),
  modules = teal_example_module()
)

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. 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)