Adding Support for Reporting to Custom Modules

NEST CoreDev

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:

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)
example_module <- function(label = "example teal module") {
  module(
    label = label,
    server = function(id, data) {
      checkmate::assert_class(data, "reactive")
      checkmate::assert_class(isolate(data()), "teal_data")

      moduleServer(id, function(input, output, session) {
        updateSelectInput(session, "dataname", choices = isolate(datanames(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 = example_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:

example_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(datanames(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:

app <- init(
  data = teal_data(IRIS = iris, MTCARS = mtcars),
  modules = example_module_with_reporting()
)

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 example_module_with_reporting to the report are provided by teal.reporter::simple_reporter_ui and teal.reporter::simple_reporter_srv.

example_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(datanames(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:

app <- init(
  data = teal_data(IRIS = iris, MTCARS = mtcars),
  modules = example_module_with_reporting()
)

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
}

example_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(datanames(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")))
      )
    }
  )
}
app <- init(
  data = teal_data(IRIS = iris, 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 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.

library(teal)
library(teal.reporter)
## 
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(datanames(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(
          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"),
    example_module(label = "without Reporter")
  ),
  filter = teal_slices(teal_slice(dataname = "AIR", varname = "Temp", selected = c(72, 85))),
  header = "Example teal app with reporter"
)

if (interactive()) shinyApp(app$ui, app$server)