Multi-Panel Applications and Interactive Graphics

Mark Andrews

Larger applications

So far our applications have had one or two outputs. Real applications often have:

  • Multiple views of the same data (distribution, summary, raw data)
  • Side-by-side comparisons
  • Interactive selection that links plots together

tabsetPanel

mainPanel(
  tabsetPanel(
    tabPanel("Distribution", plotOutput("hist")),
    tabPanel("Summary",      verbatimTextOutput("stats")),
    tabPanel("Data",         tableOutput("table"))
  )
)
  • Each tabPanel is a named view
  • Only the active tab is visible; all outputs are still reactive
  • The sidebar remains accessible from all tabs

Grid layout recap

fluidRow(
  column(4, wellPanel( ... inputs ... )),
  column(5, plotOutput("main_plot")),
  column(3, plotOutput("small_plot"))
)
  • 12-unit grid: columns sum to 12
  • Any combination of inputs and outputs can occupy a column
  • Rows can be nested inside mainPanel or used at top level

Brush selection

plotOutput("scatter", brush = "plot_brush")
selected <- reactive({
  brushedPoints(mtcars, input$plot_brush, xvar = "wt", yvar = "mpg")
})
output$table <- renderTable(selected())
  • brush = "plot_brush" adds a draggable rectangle to the plot
  • brushedPoints extracts the rows whose points fall inside the brush
  • input$plot_brush is NULL when nothing is selected

Click events

plotOutput("scatter", click = "plot_click")
output$info <- renderPrint({
  np <- nearPoints(mtcars, input$plot_click,
                   xvar = "hp", yvar = "mpg",
                   threshold = 10, maxpoints = 1)
  print(np)
})
  • nearPoints returns rows closest to the click coordinates
  • threshold is in pixels; maxpoints limits how many rows are returned

Coordinated views: the pattern

input$plot_brush
      │
      ▼
selected()  ──►  output$plot_left   (shows all data; selected points highlighted)
            └──►  output$plot_right  (shows same data with same highlighting)
  • One reactive expression computes the selection
  • Multiple outputs consume it
  • Brush in one plot affects both

Coordinated views: the code

selected <- reactive({
  brushedPoints(mtcars, input$shared_brush, xvar = "wt", yvar = "mpg")
})

output$p1 <- renderPlot({
  ggplot(mtcars, aes(wt, mpg)) +
    geom_point(colour = "grey70") +
    geom_point(data = selected(), colour = "firebrick")
})

output$p2 <- renderPlot({
  ggplot(mtcars, aes(hp, mpg)) +
    geom_point(colour = "grey70") +
    geom_point(data = selected(), colour = "firebrick")
})

Structuring code for larger apps

As an application grows, some conventions help:

  • Define all reactive expressions before render* blocks
  • Name reactives after what they contain: data_filtered, model_fitted
  • Name outputs after their role: plot_scatter, table_coef
  • Group observeEvent blocks near the inputs they watch
  • Extract plotting helpers as plain R functions (not reactive) above the server

When render blocks should be thin

A heavy renderPlot:

output$plot <- renderPlot({
  df <- read_csv(input$file$datapath)
  df <- df |> filter(group == input$group) |> mutate(z = (x - mean(x)) / sd(x))
  model <- lm(y ~ z, data = df)
  ggplot(...) + geom_smooth(method = "lm")
})

A thin renderPlot:

data_clean <- reactive({ read_csv(...) |> filter(...) |> mutate(...) })
model      <- reactive({ lm(y ~ z, data = data_clean()) })
output$plot <- renderPlot({
  ggplot(data_clean(), aes(z, y)) + geom_smooth(method = "lm")
})

The thin version is easier to read, easier to test, and avoids redundant computation.

Summary

  • tabsetPanel is the standard structure for multi-view applications
  • fluidRow / column give precise side-by-side control
  • Brush and click events extract data from user interaction with plots
  • Coordinated views are implemented by having multiple outputs consume one reactive
  • Keep render blocks thin; do computation in reactive expressions