ETC5523: Communicating with Data

From a reactive app to an audience

Lecturer: Michael Lydeamore

Department of Econometrics and Business Statistics


By the end of today

Important

You should be able to:

  1. use one reactive expression to keep several outputs consistent;
  2. explain what changes when an app moves from your computer to someone else’s; and
  3. choose between an R package and a hosted web app for a particular audience.

One calculation, several outputs

A small app with one question

For a selected WPL season and run threshold, which leading batters remain, and how did they score?

The app will show:

  • how many batters meet the conditions;
  • the leading run-scorer;
  • a plot of batting patterns; and
  • a small table.

All four claims should describe the same selection.

Demo 1: start with repeated calculations

library(shiny)
library(bslib)
library(dplyr)
library(ggplot2)

batters <- read.csv("data/wpl_batters.csv")

ui <- page_sidebar(
  title = "WPL batting explorer",
  sidebar = sidebar(
    selectInput(
      "season", "Choose a season",
      choices = sort(unique(batters$season))
    ),
    sliderInput(
      "minimum_runs", "Minimum runs",
      min = 0,
      max = max(batters$runs),
      value = 150,
      step = 25
    )
  ),
  layout_columns(
    value_box(
      title = "Batters shown",
      value = textOutput("number_shown")
    ),
    value_box(
      title = "Leading run-scorer",
      value = textOutput("leading_batter")
    )
  ),
  plotOutput("batters_plot"),
  tableOutput("batters_table")
)

server <- function(input, output, session) {
  output$number_shown <- renderText({
    batters |>
      filter(
        season == input$season,
        runs >= input$minimum_runs
      ) |>
      nrow()
  })

  output$leading_batter <- renderText({
    filtered <- batters |>
      filter(
        season == input$season,
        runs >= input$minimum_runs
      )

    req(nrow(filtered) > 0)
    filtered$striker[which.max(filtered$runs)]
  })

  output$batters_plot <- renderPlot({
    batters |>
      filter(
        season == input$season,
        runs >= input$minimum_runs
      ) |>
      ggplot(aes(
        x = dot_percent,
        y = boundary_percent,
        size = balls_faced
      )) +
      geom_point(colour = "#006DAE", alpha = 0.7) +
      labs(
        x = "Dot-ball percentage",
        y = "Boundary percentage",
        size = "Balls faced"
      ) +
      theme_minimal(base_size = 14)
  })

  output$batters_table <- renderTable({
    batters |>
      filter(
        season == input$season,
        runs > input$minimum_runs
      ) |>
      arrange(desc(runs)) |>
      select(striker, batting_team, runs, balls_faced) |>
      head(5)
  })
}

shinyApp(ui, server)

Duplication creates several sources of truth

# Number and plot
runs >= input$minimum_runs

# Table
runs > input$minimum_runs

At an exact threshold, the app disagrees with itself.

Let the running app draw its dependencies

Start a fresh R session, then enable the reactive log before running the app:

library(reactlog)
reactlog_enable()

shiny::runApp("inst/app")

Change each input once, then open the visualiser:

  • macOS: Cmd + F3
  • Windows/Linux: Ctrl + F3

The full log may also contain browser and layout nodes. Filter or double-click a named output to isolate the part of the graph we are discussing.

Before refactoring: inputs feed every output

input$season -------+---> number
                    +---> leader
input$minimum_runs -+---> plot
                    +---> table

Name the shared calculation

selected_batters <- reactive({
  batters |>
    filter(
      season == input$season,
      runs >= input$minimum_runs
    )
}, label = "selected_batters")
  • reactive() records the calculation and its reactive dependencies.
  • label = "selected_batters" supplies a readable node name for reactlog.
  • Calling selected_batters() retrieves its current value.

Call a reactive expression with parentheses

Definition

selected_batters <- reactive({
  # calculation
})

Use

selected_batters()

Without (), you have the reactive object—not the data it currently returns.

Several outputs can consume the same value

output$number_shown <- renderText({
  nrow(selected_batters())
})

output$leading_batter <- renderText({
  selected <- selected_batters()
  selected$striker[which.max(selected$runs)]
})

output$batters_table <- renderTable({
  selected_batters() |>
    arrange(desc(runs)) |>
    select(striker, batting_team, runs)
})

Demo 2: replace server() with this version

server <- function(input, output, session) {
  selected_batters <- reactive({
    batters |>
      filter(
        season == input$season,
        runs >= input$minimum_runs
      )
  }, label = "selected_batters")

  output$number_shown <- renderText({
    nrow(selected_batters())
  })

  output$leading_batter <- renderText({
    selected <- selected_batters()
    req(nrow(selected) > 0)
    selected$striker[which.max(selected$runs)]
  })

  output$batters_plot <- renderPlot({
    selected_batters() |>
      ggplot(aes(
        x = dot_percent,
        y = boundary_percent,
        size = balls_faced
      )) +
      geom_point(colour = "#006DAE", alpha = 0.7) +
      labs(
        x = "Dot-ball percentage",
        y = "Boundary percentage",
        size = "Balls faced"
      ) +
      theme_minimal(base_size = 14)
  })

  output$batters_table <- renderTable({
    selected_batters() |>
      arrange(desc(runs)) |>
      select(striker, batting_team, runs, balls_faced) |>
      head(5)
  })
}

The dependency graph is the explanation

input$season -------+
                    +---> selected_batters() --+---> number
input$minimum_runs -+                           +---> leader
                                                +---> plot
                                                +---> table

Shiny records a dependency whenever reactive code reads an input or another reactive value.

Reopen reactlog after refactoring

Stop the app, clear the old history and run the revised version:

shiny::reactlogReset()
shiny::runApp("inst/app")

Change one input and reopen reactlog.

The graph should now contain one labelled selected_batters node between the two inputs and all four outputs.

Read the animation from left to right

When input$minimum_runs changes, step through the reactive history:

  1. the input changes;
  2. selected_batters becomes invalid;
  3. its dependent outputs become invalid;
  4. the shared reactive recalculates when needed; and
  5. the outputs consume its new value.

Predict before running

The user changes input$minimum_runs.

  1. selected_batters() is invalidated.
  2. Each output that called it is invalidated.
  3. When an output is needed, the shared calculation runs again.
  4. All outputs receive the same selected rows.

Why share a reactive?

Use a shared reactive when several consumers need the same value.

  • Consistency: outputs cannot quietly apply different rules.
  • Legibility: the calculation receives a meaningful name.
  • Maintenance: one rule changes in one place.
  • Efficiency: Shiny can reuse the current result until a dependency changes.

Do not make everything reactive

# Loaded once when the R process starts
batters <- read.csv("data/wpl_batters.csv")

server <- function(input, output, session) {
  # Different for each user session
  selected_batters <- reactive({
    batters |>
      filter(season == input$season)
  })
}
  • Code outside server() runs once for the R process and is shared across sessions.
  • Code inside server() is created separately for each user session.

A function and a reactive solve different problems

Function

select_batters <- function(data,
                           season_value,
                           minimum_runs) {
  data |>
    filter(
      season == season_value,
      runs >= minimum_runs
    )
}

Explicit inputs; easy to test outside Shiny.

Reactive expression

selected_batters <- reactive({
  select_batters(
    batters,
    input$season,
    input$minimum_runs
  )
})

Connects the function to the current session.

Empty data are still a possible value

output$leading_batter <- renderText({
  selected <- selected_batters()

  if (nrow(selected) == 0) {
    return("No batter meets this threshold")
  }

  selected$striker[which.max(selected$runs)]
})

Checkpoint

Which object should own the filter?

  1. Each output, so it is self-contained
  2. The UI, because the inputs live there
  3. One reactive expression used by every relevant output
  4. A global variable that is overwritten whenever an input changes

3. It represents the current selection without duplicating the rule or mixing sessions.

From your computer to someone else

“Share the app” is incomplete

Who is the audience?

What software do they have?

Who will run R?

Who controls access?

Who maintains the dependencies and data connections?

Two useful delivery routes

R package Hosted web app
Primary audience R users Browser users
R process runs on User’s computer Hosting server
User receives Installable software A URL
User can reuse functions Yes Only through the interface
Author maintains Package releases A running service

A package puts the app beside reusable code

wplcommunicate/
├── DESCRIPTION
├── R/
│   ├── select_batters.R
│   └── run_app.R
└── inst/
    └── app/
        ├── app.R
        └── data/
            └── wpl_batters.csv

inst/ ships non-R files with the package

During installation, files under inst/ are copied into the installed package.

run_app <- function() {
  app_dir <- system.file(
    "app",
    package = "wplcommunicate"
  )

  shiny::runApp(app_dir)
}

system.file() finds the app after installation, wherever the package lives.

A hosted app moves the R process

Local package
browser ↔︎ R on the user’s computer


Hosted app
user’s browser ↔︎ network ↔︎ R on a server

What deployment sends

rsconnect::deployApp(appDir = "inst/app")

For a typical hosted deployment, the service needs:

  • the app files;
  • the R version and package dependencies;
  • data or connections required at runtime; and
  • configuration that is safe for the server environment.

Only files included in the deployment bundle are available to the server.

The clean-computer test

Imagine the hosting server has never seen your project.

Can it answer each question?

  • Where is every required file?
  • Which packages and versions are needed?
  • Does filename case match exactly?
  • Where do credentials come from?
  • Where should durable data be stored?
  • What happens when the app restarts?

Use relative paths within the app bundle

Fragile

read.csv(
  "/Users/michael/Desktop/wpl.csv"
)

Portable

read.csv("data/wpl_batters.csv")

Declare packages; do not install them in the app

library(shiny)
library(dplyr)
library(ggplot2)

or use qualified calls:

dplyr::filter(...)

Do not put this inside app.R:

install.packages("dplyr")

During deployment, rsconnect detects dependencies from calls such as library(dplyr) and dplyr::filter(); the hosting service installs them in the application environment.

Current hosting choices answer different needs

Route Useful when
shinyapps.io You want managed Shiny hosting with a straightforward rsconnect workflow
Posit Connect Cloud You want GitHub-connected cloud publishing for public data products
Posit Connect An organisation needs authentication, access control and managed publishing
Shiny Server An organisation will operate its own open-source Linux server

Choose the route from the constraint

  1. The audience has no R installation. Host the app.
  2. Analysts need to call the calculation in their own scripts. Package it.
  3. The app uses confidential organisational data. Choose hosting with the required identity and access controls.
  4. A colleague needs to inspect and extend the code. Share the source repository; a URL alone is insufficient.

Week 11 lesson

Summary

  • A shared reactive gives several outputs one current source of truth.
  • Ordinary functions own reusable calculations; reactives connect them to a user session.
  • A package asks the user to run R; a hosted app asks a server to run R.
  • Distribution succeeds when files, dependencies, secrets, storage and access work away from the author’s computer.

Resources

AutoSave
Presentation — Saved to OneDrive
▤
✂ Cut▧ Copy♧ Format Painter
Clipboard ↘
▣+
▤ Layout⌄↶ Reset▧ Section⌄
Slides
Aptos (Body)18A⌃A⌄A×
BIUabx₂x²Av↔A🖍
Font ↘
•☰1☰⇤⇥↕
≡≣≡▥↔
Paragraph ↘
□
○
◇
→
▱ Shape Fill⌄□ Shape Outline⌄✦ Shape Effects⌄
Drawing
⌕
▣
Editing
0123456789101112
Click to add notes