Shiny Integration

tabviz works seamlessly with Shiny through the htmlwidgets framework. This guide covers basic integration, reactive patterns, and proxy functions for dynamic updates.

Basic Usage

Output and Render

Use forestOutput() in the UI and renderForest() in the server:

library(shiny)
library(tabviz)

ui <- fluidPage(
  titlePanel("Forest Plot Explorer"),
  forestOutput("forest", height = "500px")
)

server <- function(input, output, session) {
  output$forest <- renderForest({
    tabviz(meta_data,
      label = "study",
      columns = list(
        viz_forest(point = "hr", lower = "lower", upper = "upper",
                   scale = "log", null_value = 1)
      )
    )
  })
}

shinyApp(ui, server)

Sizing

forestOutput() accepts standard CSS sizing:

Argument Default Examples
width "100%" "800px", "50vw"
height "400px" "600px", "80vh"
# Fixed size
forestOutput("forest", width = "900px", height = "600px")

# Responsive
forestOutput("forest", width = "100%", height = "70vh")

Reactive Data Patterns

Basic Reactive Data

Connect the plot to reactive data sources:

server <- function(input, output, session) {
  # Reactive data source
  filtered_data <- reactive({
    meta_data |>
      filter(region == input$region)
  })

  output$forest <- renderForest({
    tabviz(filtered_data(),
      label = "study",
      columns = list(
        viz_forest(point = "hr", lower = "lower", upper = "upper",
                   scale = "log", null_value = 1)
      )
    )
  })
}

Conditional Rendering

Show different plots based on user input:

server <- function(input, output, session) {
  output$forest <- renderForest({
    # Select data based on input
    data <- switch(input$analysis,
      "primary" = primary_results,
      "sensitivity" = sensitivity_results,
      "subgroup" = subgroup_results
    )

    # Select theme based on input
    theme <- switch(input$theme,
      "default" = web_theme_default(),
      "jama" = web_theme_jama(),
      "lancet" = web_theme_lancet()
    )

    tabviz(data,
      label = "study",
      columns = list(
        viz_forest(point = "hr", lower = "lower", upper = "upper",
                   scale = "log", null_value = 1)
      ),
      theme = theme
    )
  })
}

Debouncing Rapid Updates

For inputs that change frequently (like sliders), debounce to avoid excessive re-renders:

server <- function(input, output, session) {
  # Debounce the filter value
  filter_value <- reactive(input$min_n) |>
    debounce(300)  # Wait 300ms after last change

  filtered_data <- reactive({
    meta_data |>
      filter(n >= filter_value())
  })

  output$forest <- renderForest({
    tabviz(filtered_data(), ...)
  })
}

Proxy Functions

Proxy functions update the plot without full re-render, providing smoother interactions for sorting, filtering, and data updates.

Creating a Proxy

Create a proxy inside a reactive context:

observeEvent(input$some_button, {
  proxy <- forestProxy("forest")
  # Use proxy for updates...
})

Important: Always create the proxy inside an observe, observeEvent, or other reactive context.

Available Proxy Functions

Function Purpose
forest_update_data(proxy, spec) Replace data with new WebSpec
forest_sort(proxy, column, direction) Sort by column
forest_filter(proxy, field, operator, value) Apply filter
forest_clear_filter(proxy) Remove all filters
forest_toggle_subgroup(proxy, subgroup_id, collapsed) Expand/collapse group

Sorting

# Sort by hazard ratio ascending
observeEvent(input$sort_asc, {
  forestProxy("forest") |>
    forest_sort("hr", "asc")
})

# Sort descending
observeEvent(input$sort_desc, {
  forestProxy("forest") |>
    forest_sort("hr", "desc")
})

# Clear sort (restore original order)
observeEvent(input$sort_clear, {
  forestProxy("forest") |>
    forest_sort("hr", "none")
})

Filtering

Filter operators:

Operator Meaning Example
"eq" Equals value = "Phase 3"
"neq" Not equals value = "Excluded"
"gt" Greater than value = 100
"lt" Less than value = 0.05
"contains" String contains value = "SGLT2"
# Filter to large studies
observeEvent(input$filter_n, {
  forestProxy("forest") |>
    forest_filter("n", "gt", input$min_sample_size)
})

# Filter by region
observeEvent(input$filter_region, {
  forestProxy("forest") |>
    forest_filter("region", "eq", input$selected_region)
})

# Clear all filters
observeEvent(input$clear_filters, {
  forestProxy("forest") |>
    forest_clear_filter()
})

Updating Data

Replace the entire dataset with forest_update_data():

observeEvent(input$switch_dataset, {
  # Create new spec
  new_spec <- tabviz(
    get_dataset(input$dataset_name),
    label = "study",
    columns = list(
      viz_forest(point = "hr", lower = "lower", upper = "upper",
                 scale = "log", null_value = 1)
    ),
    .spec_only = TRUE
  )

  forestProxy("forest") |>
    forest_update_data(new_spec)
})

Toggling Subgroups

Expand or collapse hierarchical groups:

# Collapse a specific group
observeEvent(input$collapse_north, {
  forestProxy("forest") |>
    forest_toggle_subgroup("North America", collapsed = TRUE)
})

# Expand a group
observeEvent(input$expand_all, {
  forestProxy("forest") |>
    forest_toggle_subgroup("North America", collapsed = FALSE) |>
    forest_toggle_subgroup("Europe", collapsed = FALSE)
})

# Toggle (flip current state)
observeEvent(input$toggle_group, {
  forestProxy("forest") |>
    forest_toggle_subgroup(input$group_id, collapsed = NULL)
})

Chaining Proxy Calls

Proxy functions return the proxy object, enabling method chaining:

observeEvent(input$reset_view, {
  forestProxy("forest") |>
    forest_clear_filter() |>
    forest_sort("study", "none") |>
    forest_toggle_subgroup("all", collapsed = FALSE)
})

Complete Example App

Here’s a full example combining reactive data, proxy functions, and user controls:

library(shiny)
library(tabviz)
library(dplyr)

# Sample data
meta_data <- data.frame(
  study = c("ADVANCE", "SPRINT", "ACCORD", "ONTARGET", "HOPE-3",
            "EMPA-REG", "CANVAS", "DECLARE", "DAPA-HF", "EMPEROR"),
  region = c("Global", "North America", "North America", "Global", "Global",
             "Global", "Global", "Global", "Global", "Global"),
  drug_class = c("ACEi", "Intensive BP", "Intensive BP", "ARB", "Statin",
                 "SGLT2i", "SGLT2i", "SGLT2i", "SGLT2i", "SGLT2i"),
  hr = c(0.91, 0.75, 0.88, 0.94, 0.90, 0.62, 0.86, 0.93, 0.74, 0.75),
  lower = c(0.83, 0.64, 0.76, 0.86, 0.82, 0.49, 0.75, 0.84, 0.65, 0.65),
  upper = c(1.01, 0.87, 1.01, 1.02, 0.98, 0.77, 0.97, 1.03, 0.85, 0.86),
  n = c(11140, 9361, 10251, 25620, 12705, 7020, 10142, 17160, 4744, 3730)
)

ui <- fluidPage(
  titlePanel("Interactive Forest Plot"),

  sidebarLayout(
    sidebarPanel(
      width = 3,
      h4("Filters"),
      selectInput("drug_class", "Drug Class",
        choices = c("All", unique(meta_data$drug_class)),
        selected = "All"
      ),
      sliderInput("min_n", "Minimum Sample Size",
        min = 0, max = 20000, value = 0, step = 1000
      ),
      hr(),
      h4("Display"),
      selectInput("theme", "Theme",
        choices = c("Default" = "default", "JAMA" = "jama", "Lancet" = "lancet")
      ),
      actionButton("sort_hr", "Sort by HR"),
      actionButton("reset", "Reset View")
    ),

    mainPanel(
      width = 9,
      forestOutput("forest", height = "600px")
    )
  )
)

server <- function(input, output, session) {

  # Reactive filtered data
  plot_data <- reactive({
    d <- meta_data

    if (input$drug_class != "All") {
      d <- d |> filter(drug_class == input$drug_class)
    }

    d |> filter(n >= input$min_n)
  })

  # Reactive theme
  plot_theme <- reactive({
    switch(input$theme,
      "jama" = web_theme_jama(),
      "lancet" = web_theme_lancet(),
      web_theme_default()
    )
  })

  # Render the plot
  output$forest <- renderForest({
    tabviz(plot_data(),
      label = "study",
      group = "drug_class",
      columns = list(
        col_numeric("n", "N"),
        col_interval("HR (95% CI)", point = "hr", lower = "lower", upper = "upper"),
        viz_forest(point = "hr", lower = "lower", upper = "upper",
                   scale = "log", null_value = 1, axis_label = "Hazard Ratio")
      ),
      theme = plot_theme()
    )
  })

  # Sort button - uses proxy for smooth update
  sort_state <- reactiveVal("none")

  observeEvent(input$sort_hr, {
    current <- sort_state()
    new_dir <- switch(current,
      "none" = "asc",
      "asc" = "desc",
      "desc" = "none"
    )
    sort_state(new_dir)

    forestProxy("forest") |>
      forest_sort("hr", new_dir)
  })

  # Reset button
  observeEvent(input$reset, {
    updateSelectInput(session, "drug_class", selected = "All")
    updateSliderInput(session, "min_n", value = 0)
    sort_state("none")

    forestProxy("forest") |>
      forest_clear_filter() |>
      forest_sort("hr", "none")
  })
}

shinyApp(ui, server)

When to Use Proxy vs Re-render

Use Case Approach Reason
Filter/sort existing data Proxy Smoother, no flicker
Change dataset completely Re-render New data structure
Change theme Re-render Theme affects all rendering
Toggle subgroups Proxy UI state change only
Add/remove columns Re-render Structural change

Best Practices

  1. Create proxies in reactive contexts - Never create a proxy at the top level of your server function

  2. Use debouncing for sliders - Prevent excessive re-renders during drag

  3. Prefer proxy for interactions - Sorting, filtering, and toggling are smoother via proxy

  4. Use reactive for data changes - When the underlying data changes, let Shiny re-render

  5. Combine proxy calls - Chain multiple operations for atomic updates:

    forestProxy("forest") |>
      forest_clear_filter() |>
      forest_sort("hr", "asc")