ETC5523: Communicating with Data

R Shiny — Advanced: tabsets, shared reactives, debugging, conditional UI

Lecturer: Michael Lydeamore

Department of Econometrics and Business Statistics



Aim

  • Tabbed layouts (tabsets) to organise views
  • Shared reactive datasets / reactiveValues used across components
  • More debugging techniques and diagnostics
  • Auto-generated narrative text driven by reactive expressions
  • Conditionally showing UI elements (show/hide based on inputs)

Why

These patterns let you write cleaner, more efficient apps and craft persuasive narratives using the same underlying data.

Dataset

smartmeter <- read.csv(here::here("data", "smartmeter.csv"))

head(smartmeter)
         NMI METER.SERIAL.NUMBER     CON.GEN       DATE ESTIMATED.
1 6001050733               90655 Consumption 2024-07-15         No
2 6001050733               90655 Consumption 2024-07-16         No
3 6001050733               90655 Consumption 2024-07-17         No
4 6001050733               90655 Consumption 2024-07-18         No
5 6001050733               90655 Consumption 2024-07-19         No
6 6001050733               90655 Consumption 2024-07-20         No
  X00.00...00.30 X00.30...01.00 X01.00...01.30 X01.30...02.00 X02.00...02.30
1         0.0312         0.0437         0.0250         0.0437         0.0250
2         0.0375         0.0375         0.0375         0.0375         0.0312
3         0.0250         0.0437         0.0250         0.0437         0.0312
4         0.0437         0.0250         0.0437         0.0250         0.0437
5         0.0437         0.0250         0.0437         0.0250         0.0437
6         0.0437         0.0250         0.0437         0.0312         0.0375
  X02.30...03.00 X03.00...03.30 X03.30...04.00 X04.00...04.30 X04.30...05.00
1         0.0437         0.0250         0.0437         0.0250         0.0437
2         0.0375         0.0312         0.0437         0.0250         0.0437
3         0.0437         0.0375         0.0250         0.0500         0.0250
4         0.0312         0.0375         0.0375         0.0375         0.0375
5         0.0250         0.0437         0.0250         0.0437         0.0312
6         0.0375         0.0312         0.0437         0.0250         0.0437
  X05.00...05.30 X05.30...06.00 X06.00...06.30 X06.30...07.00 X07.00...07.30
1         0.0250         0.0437         0.0250         0.0437         0.0250
2         0.0250         0.0437         0.0250         0.0437         0.0250
3         0.0375         0.0312         0.0437         0.0250         0.0437
4         0.0312         0.0375         0.0312         0.0437         0.0250
5         0.0375         0.0312         0.0312         0.0375         0.0375
6         0.0250         0.0437         0.0250         0.0437         0.0250
  X07.30...08.00 X08.00...08.30 X08.30...09.00 X09.00...09.30 X09.30...10.00
1         0.0437         0.0250         0.0375         0.0312         0.0375
2         0.0437         0.0312         0.0375         0.0375         0.0312
3         0.0250         0.0437         0.0312         0.0375         0.0375
4         0.0375         0.0312         0.0437         0.0250         0.0375
5         0.0375         0.0312         0.0375         0.0312         0.0375
6         0.0437         0.0312         0.7562         2.0187         1.9875
  X10.00...10.30 X10.30...11.00 X11.00...11.30 X11.30...12.00 X12.00...12.30
1         0.0312         0.0375         0.0250         0.0437         0.0312
2         0.0437         0.0312         0.0375         0.0250         0.0500
3         0.0312         0.0375         0.0312         0.0437         0.0250
4         0.0375         0.0375         0.0312         0.0312         0.0437
5         0.0250         0.0437         0.0312         0.0375         0.0250
6         1.9062         1.6437         1.5000         1.3750         1.2437
  X12.30...13.00 X13.00...13.30 X13.30...14.00 X14.00...14.30 X14.30...15.00
1         0.0375         0.0312         0.0375         0.0312         0.0375
2         0.0250         0.0375         0.0312         0.0437         0.0375
3         0.0437         0.0250         0.0437         0.0312         0.0375
4         0.0312         0.0375         0.0250         0.0500         0.0250
5         0.0437         0.0250         0.0437         0.0250         0.0437
6         1.1250         1.0312         1.0000         0.6125         0.2687
  X15.00...15.30 X15.30...16.00 X16.00...16.30 X16.30...17.00 X17.00...17.30
1         0.0312         0.0375         0.0375         0.0312         0.0437
2         0.0312         0.0437         0.0312         0.0375         0.0250
3         0.0437         0.0312         0.0437         0.0312         0.0500
4         0.0375         0.0312         0.0437         0.0375         0.0312
5         0.0250         0.0437         0.0250         0.0437         0.0312
6         0.1125         0.0437         0.0625         0.0375         0.0625
  X17.30...18.00 X18.00...18.30 X18.30...19.00 X19.00...19.30 X19.30...20.00
1         0.0250         0.0437         0.0250         0.0437         0.0250
2         0.0500         0.0312         0.0375         0.0437         0.0312
3         0.0687         0.1000         0.0687         0.0500         0.0375
4         0.0437         0.0250         0.0437         0.0250         0.0500
5         0.0375         0.0375         0.0375         0.0375         0.0250
6         0.0500         0.0500         1.6875         2.1125         1.8875
  X20.00...20.30 X20.30...21.00 X21.00...21.30 X21.30...22.00 X22.00...22.30
1         0.0437         0.0250         0.0437         0.0312         0.0375
2         0.0437         0.0250         0.0437         0.0312         0.0375
3         0.0375         0.0500         0.0250         0.0437         0.0250
4         0.0250         0.0437         0.0312         0.0375         0.0375
5         0.0500         0.0250         0.0375         0.0312         0.0437
6         1.6125         1.2750         1.3625         1.1875         1.1937
  X22.30...23.00 X23.00...23.30 X23.30...00.00
1         0.0312         0.0437         0.0250
2         0.0437         0.0312         0.0437
3         0.0500         0.0312         0.0375
4         0.0312         0.0437         0.0250
5         0.0250         0.0375         0.0312
6         1.9375         1.3562         1.7125

Dataset after some cleaning

solar_data <- smartmeter |>
  pivot_longer(
    cols = starts_with("X"),
    names_to = "datetime",
    values_to = "energy_kwh"
  ) |>
  mutate(
    datetime = gsub("X(\\d{2}\\.\\d{2})\\.\\.\\.(\\d{2}\\.\\d{2})", "\\1", datetime),
    energy_kwh = as.numeric(energy_kwh),
    datetime = paste0(DATE, " ", datetime),
    datetime = lubridate::ymd_hm(datetime)
  ) |>
  janitor::clean_names()

solar_data
# A tibble: 21,840 × 7
          nmi meter_serial_number con_gen    date  estimated datetime           
        <dbl>               <int> <chr>      <chr> <chr>     <dttm>             
 1 6001050733               90655 Consumpti… 2024… No        2024-07-15 00:00:00
 2 6001050733               90655 Consumpti… 2024… No        2024-07-15 00:30:00
 3 6001050733               90655 Consumpti… 2024… No        2024-07-15 01:00:00
 4 6001050733               90655 Consumpti… 2024… No        2024-07-15 01:30:00
 5 6001050733               90655 Consumpti… 2024… No        2024-07-15 02:00:00
 6 6001050733               90655 Consumpti… 2024… No        2024-07-15 02:30:00
 7 6001050733               90655 Consumpti… 2024… No        2024-07-15 03:00:00
 8 6001050733               90655 Consumpti… 2024… No        2024-07-15 03:30:00
 9 6001050733               90655 Consumpti… 2024… No        2024-07-15 04:00:00
10 6001050733               90655 Consumpti… 2024… No        2024-07-15 04:30:00
# ℹ 21,830 more rows
# ℹ 1 more variable: energy_kwh <dbl>

The basic app

library(shiny)

ui <- fluidPage(
  headerPanel("Solar dashboard"),
  sidebarPanel(
    sliderInput("date_range", "Subset dates", min = as.Date('2024-07-15'), max = as.Date('2025-10-05'),
                value = c(as.Date('2024-07-15'), as.Date('2025-10-05'))),
  ),
  mainPanel(
    plotOutput("solar_plot")
  )
)

server <- function(input, output) {
  solar_data <- read.csv(here::here("data", "smartmeter.csv")) |>
    pivot_longer(
    cols = starts_with("X"),
    names_to = "datetime",
    values_to = "energy_kwh"
  ) |>
  mutate(
    datetime = gsub("X(\\d{2}\\.\\d{2})\\.\\.\\.(\\d{2}\\.\\d{2})", "\\1", datetime),
    energy_kwh = as.numeric(energy_kwh),
    datetime = paste0(DATE, " ", datetime),
    datetime = lubridate::ymd_hm(datetime)
  ) |>
  janitor::clean_names()

  output$solar_plot <- renderPlot({
    solar_data <- solar_data |>
      filter(datetime >= input$date_range[1] & datetime <= input$date_range[2])
    ggplot(solar_data, aes(x = datetime, y = energy_kwh)) +
      geom_line() +
      labs(y = 'kWh', x = 'Date') +
      theme_minimal()
  })
}

Tabsets and layout patterns

How to implement tabsets (step-by-step)

  1. Decide what content belongs together — group related plots, tables or controls.
  2. Use tabsetPanel(id = 'tabs', tabPanel('A', ...), tabPanel('B', ...)) in your UI.
  3. If you need to react to which tab is active, give the tabset an id and inspect input$tabs on the server.

How to implement tabsets (example)

Small example:

ui <- fluidPage(
  tabsetPanel(id = 'main_tabs',
    tabPanel('Overview', plotOutput('p1')),
    tabPanel('Details', plotOutput('p2'))
  ),
  mainPanel(
    textOutput('which_tab')
  )
)

server <- function(input, output) {
  output$which_tab <- renderText({ paste("Current tab:", input$main_tabs)})
}

Updating our app to include a tabset

library(shiny)
library(DT)

ui <- fluidPage(
  headerPanel("Solar dashboard"),
  sidebarPanel(
    sliderInput("date_range", "Subset dates", min = as.Date('2024-07-15'), max = as.Date('2025-10-05'),
                value = c(as.Date('2024-07-15'), as.Date('2025-10-05'))),
  ),
  mainPanel(
    tabsetPanel(
      tabPanel("Plot", plotOutput("solar_plot")),
      tabPanel("Data", DTOutput("data_table"))
    )
  )
)

server <- function(input, output) {
  solar_data <- read.csv(here::here("data", "smartmeter.csv")) |>
    pivot_longer(
    cols = starts_with("X"),
    names_to = "datetime",
    values_to = "energy_kwh"
  ) |>
  mutate(
    datetime = gsub("X(\\d{2}\\.\\d{2})\\.\\.\\.(\\d{2}\\.\\d{2})", "\\1", datetime),
    energy_kwh = as.numeric(energy_kwh),
    datetime = paste0(DATE, " ", datetime),
    datetime = lubridate::ymd_hm(datetime)
  ) |>
  janitor::clean_names()

  output$solar_plot <- renderPlot({
    solar_data <- solar_data |>
      filter(datetime >= input$date_range[1] & datetime <= input$date_range[2])
    ggplot(solar_data, aes(x = datetime, y = energy_kwh)) +
      geom_line() +
      labs(y = 'kWh', x = 'Date') +
      theme_minimal()
  })

  output$data_table <- renderDT({
    solar_data <- solar_data |>
      filter(datetime >= input$date_range[1] & datetime <= input$date_range[2])
    solar_data
  })
}

When to use tabsets

  • Use for logically separate views or different analysis perspectives of the same data (like our three framings).
  • Avoid deep nesting of tabs (it adds cognitive load). Prefer progressive disclosure: keep primary controls visible and move advanced filters to secondary tabs.

Reactive variables & shared datasets

Reactive variables & shared datasets

You may have noticed in our app we are re-cleaning and processing the dataset inside each render*() function. This is inefficient and error-prone.

Instead, we can create shared reactive expressions that parse and clean the data once, and then multiple outputs can re-use the cleaned data.

Reactive variables & shared datasets

To implement:

# Shared reactive dataset
solar_data <- reactive({
  df <- read.csv(here::here("data", "smartmeter.csv")) |>
    pivot_longer(
      cols = starts_with("X"),
      names_to = "datetime",
      values_to = "energy_kwh"
    ) |>
    mutate(
      datetime = gsub("X(\\d{2}\\.\\d{2})\\.\\.\\.(\\d{2}\\.\\d{2})", "\\1", datetime),
      energy_kwh = as.numeric(energy_kwh),
      datetime = paste0(DATE, " ", datetime),
      datetime = lubridate::ymd_hm(datetime)
    ) |>
    janitor::clean_names()
  df
})

Then inside renderPlot() or renderDT() we call solar_data() to get the cleaned data frame.

output$solar_plot <- renderPlot({
  df <- solar_data() |>
    filter(datetime >= input$date_range[1] & datetime <= input$date_range[2])
  ggplot(df, aes(x = datetime, y = energy_kwh)) +
    geom_line() +
    labs(y = 'kWh', x = 'Date') +
    theme_minimal()
})

Best practice checklist

  • Validate early: use req() and validate() at the top of reactive expressions
  • Keep single responsibility: each reactive does one transformation step and you chain them (parse -> clean -> aggregate)
  • Use descriptive names and document the contract for each reactive (inputs, outputs, error modes)

Other inputs and outputs

File inputs

So far we have hard-coded the data source. To make the app reusable, we can add a file input control to let users upload their own solar CSV.

fileInput("solar_file", "Upload solar data (CSV)", accept = c('.csv'))

Then inside the solar_data reactive we check if a file is uploaded and read it.

solar_data <- reactive({
  req(input$solar_file) # require a file
  read.csv(input$solar_file$datapath)
  # ... rest of parsing and validation ...
})

Showing and hiding UI elements

You might only want to show certain controls or outputs based on user selections.

For example, if you are changing geoms then the user might want to include a parameter that only applies to one geom.

Enter the conditionalPanel

conditionalPanel(
  condition = "input.geom_type == 'hist'",
  sliderInput("bin_width", "Bin Width", min = 1, max = 30, value = 7)
)

Here, the condition is in JavaScript. In R land, you read this as

inpuut$geom_type == 'hist'

Example of conditional UI

# client
radioButtons("plot_geom", "Plot type",
                 choices = c("Line" = "line", "Histogram" = "hist"),
                 selected = "line"),
conditionalPanel(
  condition = "input.plot_geom == 'hist'",
  sliderInput("bins", "Number of bins", min = 5, max = 60, value = 30),
  selectInput("aggregation_size", "Aggregation type", choices = c("None", "Daily", "Weekly", "Monthly")),
)

# server

solar_data_aggregated <- reactive({
  req(solar_data_filtered())
  if (input$aggregation_size == "Daily") {
    solar_data_filtered() |>
      group_by(date) |>
      summarise(energy_kwh = sum(energy_kwh, na.rm = TRUE))
  } else if (input$aggregation_size == "Weekly") { ... }
})

File downloading

You can let users download processed datasets or reports using downloadButton() in the UI and downloadHandler() on the server.

downloadButton("download_plot", "Download Plot")

# server

output$download_plot <- downloadHandler(
  filename = function() { paste("solar_plot", Sys.Date(), ".png", sep = "") },
  content = function(file) {
    ggsave(file, plot = solar_plot(), device = "png", bg = "white", width = 8, height = 6)
  }
)

On-the-fly data manipulation

On-the-fly data manipulation

You can let users manipulate data on-the-fly using inputs that control filtering, aggregation, or transformations.

In our solar app, we could let users input their price per kWh and feed-in tariff to calculate potential savings.

observeEvent

Sometimes you want to trigger something when something else happens, like when a button is clicked, rather than on every input change. If you have a long running computation, you might want to only run it when the user clicks “Update”.

This is where we use observeEvent():

# client

actionButton("update", "Update calculations")

# server

observeEvent(input$update, {
  # Re-run expensive calculations here
  solar_data_priced <- solar_data_filtered()
  solar_data_priced$cost <- input$electricity_price * solar_data_priced$energy_kwh
  solar_data_priced$total_cost <- cumsum(solar_data_priced$cost)
})

Dynamic UI generation

You can also use actionButton with reactiveValues to let users input multiple conditions without pre-specifying the numbers.

# client

div(id = "price_inputs",
  numericInput("electricity_price_1", "Price per kwh", value = 0.28, min = 0)
),
div(style = "text-align: right;",
  actionButton("add_price", "Add another price"),
)

# server


n_prices <- reactiveVal(1)
observeEvent(input$add_price, {
  n <- n_prices() + 1
  n_prices(n)

  insertUI(
    selector = "#price_inputs",
    where = "beforeEnd",
    ui = numericInput(
      paste0("electricity_price_", n), 
      paste("Price per kwh"), value = 0.28, min = 0)
  )
})


solar_data_priced <- reactive({
  req(prices())
  solar_data_priced <- solar_data_filtered()
  for (i in 1:n_prices()) {
    solar_data_priced[, paste0("electricity_price_", i)] <- prices()[i] * solar_data_priced$energy_kwh
    solar_data_priced[, paste0("total_cost_", i)] <- cumsum(solar_data_priced[, paste0("electricity_price_", i)])
  }

  solar_data_priced
})

Code management

By this point, your app.R file may be getting quite long. To keep things manageable:

  • Split UI and server into separate files (ui.R and server.R)
  • Modularise repeated components into functions or modules and then source them

When using source, remember the path is relative to the app’s working directory, not the file location.

You can’t define variables in sourced files and expect them to be available in the main app. Instead, define functions that return values, and then use the functions inside reactive or similar.

Thinking in this modularised, functional way is similar to the workflow in targets, and helps to avoid the 1000-line script problem. Get in the habit early!

Recap: Packaging shiny apps in an R package

Recall the R package structure:

my_package/
  R/
    functions.R
    plots.R
  man/
    my_function.Rd
  vignettes/
    my_vignette.Rmd

Recap: Packaging shiny apps in an R package

To include a shiny app, create an inst/app/ directory inside your package:

my_package/
  R/
    functions.R
    plots.R
    man/
    my_function.Rd
  vignettes/
    my_vignette.Rmd
  inst/
    app/
      app.R
      www/
        styles.css
        script.js
      data/
        dataset.csv

Access things inside the package using system.file():

dataset <- read.csv(system.file("app/data/dataset.csv", package = "my_package"))

Or export them using roxygen and then use as normal

Demo

Recap

  • Tabsets for organizing views
  • Shared reactive datasets for efficiency
  • Conditional UI elements
  • On-the-fly data manipulation with observeEvent
  • Packaging shiny apps in an R package