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

Improve documentation on creating custom teal module #1356

Closed
donyunardi opened this issue Sep 27, 2024 · 8 comments · Fixed by #1400
Closed

Improve documentation on creating custom teal module #1356

donyunardi opened this issue Sep 27, 2024 · 8 comments · Fixed by #1400
Assignees
Labels

Comments

@donyunardi
Copy link
Contributor

Summary

We currently have an article on how to create custom teal module here. However, I’m still receiving feedback from internal users about the difficulty in understanding the content of this vignette.

While we provide a full code example on how to write a teal module, there isn’t much context explaining what the code does, leaving users to figure it out themselves. Another common issue I’ve heard is related to troubleshooting custom teal modules. While it’s easy to do this with browser(), explicitly mentioning it in the documentation would be helpful.

Let’s improve this documentation by providing a detailed step-by-step guide on building a teal module. We’ll focus on adding explanations alongside the code and include guidance on troubleshooting teal modules.

Personally, I really like the flow of explanation this topic in our youtube video. It would be great if we follow the same explanation style.

@donyunardi
Copy link
Contributor Author

donyunardi commented Sep 27, 2024

@kumamiao
Can I please hear your thought on this?

@kumamiao
Copy link

kumamiao commented Sep 27, 2024

Just spoke with Vincent on this yesterday as well, I agree that adding more explicit documentation on creating customized modules will be helpful for our users. I agree with adding explanations along side the code to make the code examples less intimidating and include guidance on troubleshooting, and like the idea of utilizing the previous video.

Besides mentioned above, a specific flow based on a pretty common use case would be easy-to-follow for app developers: I have a ggplot function that generates a static plot with certain features, how do I convert it into a teal module and what are the steps to follow? We could have an example plot function and then elaborate on how to convert this function to a module step by step:

  1. how to convert it to a teal module function, i.e., adding the ui and server components
  2. how to add the interactive component
  3. how to add show R code
  4. how to add it to reporter, etc.

@donyunardi
Copy link
Contributor Author

Another topic that we have to include in the docs: functional subset for delayed data
insightsengineering/teal.goshawk#301 (comment)

@kumamiao kumamiao moved this to Todo in teal Roadmap Oct 18, 2024
@gogonzo gogonzo self-assigned this Oct 30, 2024
@donyunardi
Copy link
Contributor Author

donyunardi commented Oct 30, 2024

I asked teal assistant to help us build this.
What do you guys think? @gogonzo @kumamiao


Creating a Custom teal Module for Dynamic Histograms

In this guide, we will convert a simple histogram formula, hist(dataset[, selected], las = 1), into a robust teal module. This module will allow users to dynamically select datasets and variables to create histograms within a teal application. We’ll cover best practices, including setting up dynamic inputs, structuring server logic, and using the teal_data object to ensure reactivity and reproducibility.

1. Understanding the Inputs and Requirements

When developing a custom teal module for visualizations, identify the primary inputs that users will interact with:

  • Dataset Input (dataset): Allows users to select which dataset to explore.
  • Variable Input (selected): Allows users to choose a specific numeric variable from the chosen dataset, ensuring only appropriate columns are available for plotting.

These inputs are dynamically populated based on the available datasets and variables, creating an intuitive and error-resistant user experience.

2. Setting Up the Module UI

The UI function defines the controls and display area for the histogram. For this module, we’ll use:

  • selectInput for Dataset: Enables users to select a dataset from the list of available datasets.
  • selectInput for Variable: Allows users to choose a numeric variable from the chosen dataset, dynamically filtering out any non-numeric columns.
  • checkboxInput for Code Display: Lets users toggle the display of the generated code for the plot.
  • plotOutput for Histogram: Displays the histogram once both dataset and variable inputs are selected.
  • verbatimTextOutput for Code: Displays the generated plot code based on user input.

Here’s the code for the histogram_module_ui function:

# UI function for the custom histogram module
histogram_module_ui <- function(id) {
  ns <- shiny::NS(id)
  shiny::tagList(
    shiny::selectInput(ns("dataset"), "Select Dataset", choices = NULL),
    shiny::selectInput(ns("variable"), "Select Variable", choices = NULL),
    shiny::checkboxInput(ns("show_code"), "Show Plot Code", value = FALSE),
    shiny::plotOutput(ns("histogram_plot")),
    shiny::verbatimTextOutput(ns("plot_code"))  # To display the reactive plot code
  )
}

3. Leveraging the teal_data Object in Server Logic

The teal_data object provides a powerful and flexible way to manage data in teal applications. In this section, we’ll look at best practices for using teal_data in server logic and how to interact with it effectively.

A. Understanding teal_data as a Reactive Object in Server Logic

When used in the server logic of a teal module, the teal_data object becomes a reactive data container. This means that to access its contents, you need to call it like a function, using parentheses: data(). This syntax triggers reactivity, ensuring that the data within teal_data stays up-to-date with any filters or changes applied elsewhere in the application.

Note: The teal_data object behaves as a reactive data container only when used within the server logic. If accessed outside of the server, it will not be reactive.

B. Using datanames() to Access Dataset Names

The teal_data object can contain multiple datasets. To retrieve the names of these datasets, use the datanames() function:

datanames(data())

This will return a character vector of the dataset names contained in teal_data. You can then use these names to dynamically populate input controls, like a dataset selection dropdown.

C. Accessing Specific Datasets with Double Brackets ([[ ]])

To access an individual dataset from teal_data, use double brackets ([[ ]]) along with the dataset name. This allows you to extract the specific dataset as a data frame:

data()[[input$dataset]]

Here, input$dataset represents the name of the dataset selected by the user. This syntax is highly flexible because it dynamically references whichever dataset the user has chosen. You can further subset or manipulate this extracted data frame as needed.

D. Setting Up Server Logic Using teal_data and Dynamic Variable Injection

In this updated server function:

  1. Create new_data as a modified version of data() using within(), dynamically injecting input$dataset and input$variable.
  2. Render the Plot: renderPlot() displays the plot by referencing the plot stored in the updated teal_data object, new_data.

Here’s the code:

# Server function for the custom histogram module with injected variables in within()
histogram_module_server <- function(id, data) {
  moduleServer(id, function(input, output, session) {
    
    # Update dataset choices based on available datasets in teal_data
    shiny::observe({
      shiny::updateSelectInput(
        session,
        "dataset",
        choices = teal.data::datanames(data())
      )
    })
    
    # Update variable choices based on selected dataset, only including numeric variables
    observeEvent(input$dataset, {
      req(input$dataset)  # Ensure dataset is selected
      numeric_vars <- names(data()[[input$dataset]])[sapply(data()[[input$dataset]], is.numeric)]
      shiny::updateSelectInput(session, "variable", choices = numeric_vars)
    })
    
    # Create a reactive `teal_data` object with the histogram plot
    result <- reactive({
      req(input$dataset, input$variable)  # Ensure both dataset and variable are selected

      # Create a new teal_data object with the histogram plot
      new_data <- within(
        data(),
        {
          my_plot <- hist(
            input_dataset[[input_vars]],  # Injected dataset and variable names
            las = 1,                       # Orientation for axis labels
            main = paste("Histogram of", input_vars), # Dynamic title
            xlab = input_vars,             # Dynamic x-axis label
            col = "lightblue",             # Optional color for histogram
            border = "black"               # Optional border color
          )
        },
        input_dataset = as.name(input$dataset),  # Replace `input_dataset` with dataset name
        input_vars = input$variable              # Use input variable name directly
      )
      new_data
    })

    # Render the histogram from the updated teal_data object
    output$histogram_plot <- shiny::renderPlot({
      result()[["my_plot"]]  # Access and render the plot stored in `new_data`
    })
    
    # Reactive expression to get the generated code for the plot
    plot_code_q <- reactive({
      req(input$show_code)  # Only update if "Show Plot Code" is checked
      teal.code::get_code(result())  # Retrieve the code for the updated `teal_data` object
    })
    
    # Display the code in the UI
    output$plot_code <- shiny::renderText({
      req(input$show_code)  # Only display if checkbox is checked
      plot_code_q()
    })
  })
}

Explanation of Changes

  1. Dynamic Variable Injection with within():

    • input_dataset = as.name(input$dataset) passes the dataset name dynamically as input_dataset.
    • input_vars = input$variable passes the selected variable name directly as input_vars.
    • Inside within(), my_plot uses these injected variables to dynamically generate the histogram plot.
  2. Rendering the Plot:

    • output$histogram_plot uses renderPlot() to display the plot stored in new_data by referencing result()[["my_plot"]].
  3. Dynamic Code Display:

    • The plot_code_q reactive expression retrieves the dynamically generated code using teal.code::get_code(result()), allowing users to see the exact code used to generate the plot.

4. Creating the Custom Module Object

The teal::module() function allows you to encapsulate your UI and server logic into a teal module, making it reusable and ready to integrate into any teal application. By setting datanames = "all", you give the module access to all datasets specified in the teal_data object.

# Custom histogram module creation
create_histogram_module <- function(label = "Histogram Module") {
  teal::module(
    label = label,
    ui = histogram_module_ui,
    server = histogram_module_server,
    datanames = "all"
  )
}

5. Integrating the Module into a teal App

With the module set up, it can now be integrated into a teal app. We’ll use teal::init() to specify the datasets and modules used in the app, then run the app to test functionality.

library(teal)

# Define datasets in `teal_data`
data_obj <- teal.data::teal_data(
  iris = iris,
  mtcars = mtcars
)

# Initialize the teal app
app <- teal::init(
  data = data_obj,
  modules = teal::modules(create_histogram_module())
)

# Run the app
if (interactive()) {
  shiny::shinyApp(ui = app$ui, server = app$server)
}

Summary of Enhancements

  • Reactive Plot Code Display: The show_code checkbox lets users see the exact code being used to generate the plot in real-time.
  • Encapsulation with within(): The histogram plot is generated within the teal_data object, tracking the code dynamically and ensuring reproducibility.
  • Enhanced Modularity: This approach not only makes the module more user-friendly but also adheres to teal’s best practices for reactivity and reproducibility, making the module robust and transparent.

This setup provides a fully dynamic, user-controlled teal module that allows for interactive data exploration and code visibility, enhancing both usability and transparency.

@donyunardi
Copy link
Contributor Author

Note: The teal_data object behaves as a reactive data container only in the server logic. If used outside the server (e.g., in the UI definition), it would not be reactive.

Just re-read this again and I don't think we should mention about using teal_data in UI definition because it's no longer allowed. I can continue conversing with the bot but just want to make sure that we're on the right path.

@donyunardi
Copy link
Contributor Author

donyunardi commented Oct 30, 2024

I just spoke with the bot again to provide a more accurate response, including the code reproducibility aspect of teal as an example. I updated the last comment's output from the bot to reflect this.

@donyunardi donyunardi assigned donyunardi and unassigned gogonzo Oct 31, 2024
@donyunardi
Copy link
Contributor Author

As discussed, I'll move forward with the PR.

@m7pr
Copy link
Contributor

m7pr commented Oct 31, 2024

I think you should prepare a file under which we could comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

4 participants