Tutorials › Biostatistics › Building a Shiny App for Clinical Data Review

R Programming & Clinical Data

Building a Shiny App for Clinical Data Review

A practical guide to building interactive R Shiny applications for clinical-trial data review, including reactive filtering, subject-level dashboards, laboratory and adverse-event review, data-quality flags, interactive tables, longitudinal plots, downloads, validation, security, and deployment.

Intermediate 24 min read

What You'll Learn

  • What Shiny adds to clinical data review
  • How Shiny's reactive programming model works
  • How to build subject-level review screens
  • How to review labs, adverse events, and concomitant data
  • How to create interactive clinical plots and tables
  • How to validate and deploy a clinical Shiny application

Introduction

Clinical-trial data review often involves moving between many different tables, listings, datasets, spreadsheets, and statistical programming outputs.

A statistician or clinical data reviewer may need to answer questions such as:

  • Which subjects have abnormal laboratory results?
  • Which subjects have serious adverse events?
  • Which subjects discontinued treatment?
  • Which visits are missing?
  • Which subjects have inconsistent dates?
  • Which patients have important changes in laboratory parameters?
  • Which subjects have protocol or data-quality flags?

Traditional listings are useful, but they can become cumbersome when the reviewer repeatedly needs to filter, sort, cross-reference, and inspect individual subjects.

R Shiny provides a way to build an interactive review environment directly in R.

Key idea: A clinical Shiny application is not simply a web page containing R output. It is an interactive analytical system in which user inputs, data transformations, tables, plots, summaries, and downloads can be connected through Shiny's reactive programming model.

What Is Shiny?

Shiny is an R framework for building interactive web applications.

The fundamental architecture consists of two major components:

  • UI: defines what the user sees and interacts with.
  • Server: defines how the application responds to inputs.

A minimal Shiny application looks like this:

library(shiny)

ui <- fluidPage(
  h1("Clinical Data Review")
)

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

shinyApp(
  ui = ui,
  server = server
)

The application can then be launched from R.

shiny::runApp()

Why Use Shiny for Clinical Data Review?

The main advantage is that a reviewer can interact directly with the data.

Instead of generating a new listing every time a question arises, the reviewer can select:

  • Treatment group
  • Site
  • Subject
  • Visit
  • Laboratory parameter
  • Adverse-event category
  • Data-quality flag

and have the application update the displayed information.

Traditional Review Shiny Review
Generate static listing Filter interactively
Open multiple outputs Navigate one integrated application
Re-run program for each filter Reactive outputs update automatically
Review subject information across files Combine related domains in one subject dashboard
Static plot Interactive plot with subject selection
Manual export Download filtered review data

Clinical Data Review Is Different From Final Analysis

A Shiny review application should not automatically be considered the analysis environment used to generate primary statistical results.

Clinical data review is frequently exploratory and operational.

The purpose may be to identify:

  • Potential data inconsistencies
  • Outliers
  • Unexpected values
  • Missing assessments
  • Potential coding issues
  • Subject-level patterns
  • Questions requiring clinical review

A production statistical analysis, by contrast, should generally follow the prespecified analysis plan and validated programming processes.

Important distinction: A Shiny application can be extremely useful for data review without becoming the authoritative source for a regulated clinical-trial analysis. If an application contributes to a regulated workflow, its intended use, validation, access controls, auditability, and change control should be defined explicitly.

A Typical Clinical Data Review Architecture

Clinical Data SDTM / ADaM / analysis extracts
→
Review Layer Filtering, flags, derivations
→
Shiny UI Tables, plots, subject review

The important principle is separation of responsibilities.

The underlying clinical data should be treated as the source data. The Shiny application provides a controlled presentation and review layer.

What Should a Clinical Review App Contain?

A useful application can contain several modules.

Module Purpose
Overview Study-level counts and review metrics
Subject Review Integrated patient-level information
Laboratory Review Identify abnormal and changing laboratory values
Adverse Events Review event severity, seriousness, relationship, and outcomes
Exposure Review treatment exposure and dosing information
Vital Signs Review longitudinal vital-sign measurements
Data Quality Identify potential inconsistencies and missing information
Downloads Export the currently filtered review dataset

An Interactive Clinical Review Mockup

The following embedded demonstration is a browser-side illustration of the type of workflow a Shiny application can provide. It uses simulated data and does not connect to a real clinical database.

Clinical Data Review
SIMULATED DATA
Subjects
12
Flagged
4
Lab Alerts
7
SAEs
2
Subjects
USUBJID 1001
Drug A · Site 101
Review flags
ALT above reference range at Week 8.
Laboratory review
Visit ALT ULN Status
Adverse events
Fatigue · Grade 2 · Not serious

What is happening conceptually? The reviewer selects filters, the application identifies the relevant subjects, and the selected subject drives the subject-level review panel. In a real Shiny application, these relationships would normally be implemented using reactive expressions and outputs on the server.

The Shiny Reactive Model

Reactive programming is the central concept to understand before building a larger Shiny application.

Suppose the user selects a treatment:

selectInput(
  "treatment",
  "Treatment",
  choices = c("All", "Drug A", "Drug B")
)

The server can use that input to create a reactive dataset:

filtered_data <- reactive({

  req(input$treatment)

  if (input$treatment == "All") {
    data
  } else {
    subset(
      data,
      TRT01P == input$treatment
    )
  }

})

Any output that depends on filtered_data() can react when the input changes.

$$ \text{User Input} \rightarrow \text{Reactive Expression} \rightarrow \text{Output} $$

This dependency graph is the conceptual foundation of a Shiny application.

Inputs, Reactives, and Outputs

Component Example
Input input$subject
Reactive expression subject_data()
Output output$lab_table
UI placeholder DTOutput("lab_table")

Building the UI

A clinical review interface often begins with a sidebar containing filters and a main panel containing the review outputs.

library(shiny)
library(DT)

ui <- fluidPage(

  titlePanel(
    "Clinical Data Review"
  ),

  sidebarLayout(

    sidebarPanel(

      selectInput(
        "treatment",
        "Treatment",
        choices = c(
          "All",
          "Drug A",
          "Drug B"
        )
      ),

      selectInput(
        "subject",
        "Subject",
        choices = NULL
      )

    ),

    mainPanel(

      tabsetPanel(

        tabPanel(
          "Overview",
          DTOutput("overview")
        ),

        tabPanel(
          "Laboratory",
          DTOutput("labs")
        ),

        tabPanel(
          "Adverse Events",
          DTOutput("ae")
        )

      )

    )

  )

)

Building the Server

The server function contains the application's behavior.

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

  filtered_data <- reactive({

    req(input$treatment)

    if (input$treatment == "All") {
      data
    } else {
      subset(
        data,
        TRT01P == input$treatment
      )
    }

  })

  output$overview <- renderDT({

    datatable(
      filtered_data()
    )

  })

}

The key pattern is:

reactive({
  ...
})

renderDT({
  ...
})

The reactive expression creates data, while the render function turns the data into an output.

Loading Clinical Data

For a prototype, a CSV file may be sufficient.

data <- read.csv(
  "clinical_data.csv",
  stringsAsFactors = FALSE
)

For larger applications, more structured data-loading strategies may be appropriate.

Possible sources include:

  • CSV files
  • RDS files
  • Parquet files
  • Relational databases
  • SQL queries
  • Controlled application data services
Do not hard-code credentials into an application. Database passwords, API tokens, encryption keys, and other secrets should be managed through appropriate deployment or secret-management mechanisms rather than embedded directly in an R script.

Clinical Data Often Uses Multiple Domains

A realistic review application may combine several datasets.

For example:

dm <- readRDS("dm.rds")
lb <- readRDS("lb.rds")
ae <- readRDS("ae.rds")
ex <- readRDS("ex.rds")
vs <- readRDS("vs.rds")

The datasets can then be linked through the appropriate subject identifier.

For example:

subject_dm <- reactive({

  req(input$subject)

  dm[
    dm$USUBJID == input$subject,
  ]

})

Laboratory data can similarly be filtered:

subject_lb <- reactive({

  req(input$subject)

  lb[
    lb$USUBJID == input$subject,
  ]

})

Do Not Treat All Data Domains as Equivalent

A clinical reviewer may see information from several domains on one screen, but each domain can have different structures and semantics.

Domain Typical Review Focus
DM Subject demographics, treatment, disposition context
LB Laboratory values, abnormalities, trends
AE Event timing, severity, seriousness, relationship, outcome
EX Treatment exposure and dosing
VS Longitudinal vital-sign changes
EG ECG measurements and review status
CM Concomitant medications

The application should preserve the meaning of the underlying data rather than flattening everything into a single ambiguous table.

Subject Selection

A particularly useful feature is dynamically updating the subject list after the user changes other filters.

observe({

  subjects <- filtered_data()$USUBJID

  updateSelectInput(
    session,
    "subject",
    choices = sort(unique(subjects))
  )

})

The dependency is:

$$ \text{Treatment Filter} \rightarrow \text{Eligible Subjects} \rightarrow \text{Subject Selector} \rightarrow \text{Subject Review} $$

Using req() to Protect Reactive Logic

Shiny outputs can execute before all inputs are available.

The req() function prevents downstream code from running until a required input exists.

subject_data <- reactive({

  req(input$subject)

  data[
    data$USUBJID == input$subject,
  ]

})

This is particularly useful in clinical applications because incomplete initialization should not result in confusing errors.

Displaying Clinical Listings With DT

The DT package provides interactive HTML tables that work particularly well for clinical review.

library(DT)

output$labs <- renderDT({

  datatable(
    subject_lb(),
    filter = "top",
    rownames = FALSE,
    options = list(
      pageLength = 15,
      scrollX = TRUE
    )
  )

})

The reviewer can then search and sort the displayed observations without regenerating the underlying data.

Formatting Clinical Laboratory Tables

A laboratory review table might contain:

Variable Example
USUBJID ABC-001
LBTEST ALT
VISIT Week 8
LBSTRESN 86
LBSTRESU U/L
LBORNRLO 0
LBORNRHI 40
LBSTAT

A review flag can then be derived.

lb_review <- reactive({

  x <- subject_lb()

  x$REVIEW_FLAG <-
    !is.na(x$LBSTRESN) &
    !is.na(x$LBORNRHI) &
    x$LBSTRESN > x$LBORNRHI

  x

})

Why Review Flags Should Be Separate Variables

It is often better to derive a clearly named review variable than to overwrite the source value.

For example:

x$REVIEW_FLAG <-
  x$LBSTRESN > x$LBORNRHI

is preferable to changing LBSTRESN itself.

Source-data principle: A review application should preserve the source observations. Derived review flags should be clearly distinguishable from source variables and should not silently modify the underlying clinical data.

Displaying Flags in DT

Conditional formatting can draw attention to observations requiring review.

datatable(
  lb_review(),
  rownames = FALSE
) %>%
  formatStyle(
    "REVIEW_FLAG",
    backgroundColor = styleEqual(
      c(TRUE, FALSE),
      c("#E7D6B0", "#FFFFFF")
    )
  )

For a regulated or controlled environment, the visual flagging logic should itself be tested and documented.

Reviewing Adverse Events

Adverse-event review is another natural application for Shiny.

A subject-level AE panel might show:

  • AE term
  • Start date
  • End date
  • Severity
  • Seriousness
  • Relationship to study treatment
  • Action taken
  • Outcome
output$ae <- renderDT({

  datatable(
    ae[
      ae$USUBJID == input$subject,
    ],
    rownames = FALSE,
    filter = "top"
  )

})

Highlighting Serious Adverse Events

A derived flag can identify serious adverse events.

ae_review <- reactive({

  x <- subject_ae()

  x$SAE_REVIEW_FLAG <-
    x$AESER == "Y"

  x

})

The application can then provide a summary:

output$sae_count <- renderText({

  sum(
    ae_review()$SAE_REVIEW_FLAG,
    na.rm = TRUE
  )

})

Clinical Review Flags Should Not Be Overinterpreted

A flag is generally an indication that something deserves attention.

It does not necessarily mean that the underlying record is incorrect.

For example:

  • An ALT above the reference range may be clinically expected.
  • A missing value may be legitimately unavailable.
  • An AE ending after treatment discontinuation may be appropriate.
  • A date discrepancy may arise from different collection conventions.
Think "review candidate," not "error." The application should distinguish between identifying an observation for review and determining that the observation is actually erroneous.

Creating a Laboratory Trend Plot

A longitudinal laboratory plot can be especially useful for reviewing potentially important changes over time.

library(ggplot2)

output$lab_plot <- renderPlot({

  x <- subject_lb()

  ggplot(
    x,
    aes(
      x = ADY,
      y = LBSTRESN
    )
  ) +

    geom_line() +

    geom_point() +

    labs(
      x = "Study Day",
      y = input$lab_parameter
    ) +

    theme_classic()

})

The reviewer can select a laboratory parameter and subject, allowing the plot to update automatically.

Reference Ranges

If a laboratory reference range is available, it can be displayed visually.

ggplot(
  x,
  aes(
    x = ADY,
    y = LBSTRESN
  )
) +

  geom_line() +

  geom_point() +

  geom_hline(
    yintercept = upper_limit,
    linetype = "dashed"
  ) +

  geom_hline(
    yintercept = lower_limit,
    linetype = "dashed"
  )

The exact handling of reference ranges requires care because reference ranges may vary by laboratory, test, unit, sex, age, and other factors.

Do Not Assume One Reference Range

This is a common clinical-data programming mistake.

Consider two laboratory measurements:

Subject Laboratory Test Upper Limit
001 Central Lab A ALT 40
002 Central Lab B ALT 45

Applying 40 U/L to both subjects could generate an incorrect review flag.

Clinical-data principle: Reference limits should be derived or displayed according to the study's actual laboratory metadata and analysis conventions rather than assumed to be universal.

Reactive Expressions for Complex Review Logic

As applications become larger, reactive expressions should perform logically coherent transformations rather than becoming giant blocks of code.

For example:

filtered_subjects <- reactive({
  ...
})

selected_subject <- reactive({
  ...
})

selected_labs <- reactive({
  ...
})

selected_ae <- reactive({
  ...
})

subject_summary <- reactive({
  ...
})

This creates a dependency structure that is easier to understand and test.

Reactive Dependency Graph

Inputs Treatment, site, subject, visit
→
Reactive Data Filtered domains and flags
→
Outputs Tables, plots, counts, downloads

The goal is to make the dependencies explicit.

Using dplyr Inside Shiny

The tidyverse can make clinical review transformations concise.

library(dplyr)

filtered_data <- reactive({

  data %>%

    filter(
      input$treatment == "All" |
        TRT01P == input$treatment
    ) %>%

    filter(
      input$site == "All" |
        SITEID == input$site
    )

})

This is particularly useful when several filters must be applied together.

Multiple Filters

A more complete example might look like:

filtered_data <- reactive({

  x <- data

  if (input$treatment != "All") {

    x <- x %>%
      filter(
        TRT01P == input$treatment
      )

  }

  if (input$site != "All") {

    x <- x %>%
      filter(
        SITEID == input$site
      )

  }

  x

})

The advantage is that each filter is applied only when the reviewer requests it.

Filtering by Review Flag

A particularly useful filter is:

selectInput(
  "review_status",
  "Review Status",
  choices = c(
    "All",
    "Flagged Only",
    "No Current Flags"
  )
)

Then:

review_data <- reactive({

  x <- filtered_data()

  if (
    input$review_status ==
    "Flagged Only"
  ) {

    x <- x %>%
      filter(REVIEW_FLAG)

  }

  x

})

Using validate() for Clinical Review Outputs

Sometimes an output should not attempt to render when no suitable data are available.

output$lab_plot <- renderPlot({

  x <- subject_lb()

  validate(
    need(
      nrow(x) > 0,
      "No laboratory data are available for this subject."
    )
  )

  ...

})

This is better than displaying a cryptic R error to the reviewer.

Subject-Level Dashboards

A subject-level dashboard can combine several clinically relevant elements.

For example:

1
Select a subject.
2
Display demographics and treatment assignment.
3
Display treatment exposure.
4
Display laboratory results.
5
Display adverse events.
6
Display vital signs and other relevant measurements.
7
Display potential data-quality flags.

Displaying Subject Summary Cards

A summary card can be rendered using ordinary Shiny outputs.

output$subject_treatment <- renderText({

  req(subject_dm())

  subject_dm()$TRT01P[1]

})

The corresponding UI can be:

verbatimTextOutput(
  "subject_treatment"
)

For more polished interfaces, dashboard-oriented packages can provide cards, boxes, tabs, and other components.

Tabs Are Useful for Domain Separation

Instead of placing every table on one screen, tabs can organize information.

tabsetPanel(

  tabPanel(
    "Subject",
    ...
  ),

  tabPanel(
    "Laboratory",
    ...
  ),

  tabPanel(
    "Adverse Events",
    ...
  ),

  tabPanel(
    "Exposure",
    ...
  ),

  tabPanel(
    "Vital Signs",
    ...
  ),

  tabPanel(
    "Data Quality",
    ...
  )

)

This makes the application easier to navigate and reduces information density.

Clinical Data Quality Checks

Shiny can also serve as a front end for data-quality checks.

Examples include:

  • Missing required dates
  • Impossible date sequences
  • Unexpected treatment assignments
  • Laboratory values outside expected ranges
  • Duplicate observations
  • Missing visits
  • Inconsistent subject identifiers
  • Unexpected treatment exposure

Example Date Consistency Check

Suppose a treatment start date should not occur after an adverse-event start date for an event recorded as occurring before treatment.

A review flag might be created as:

x <- x %>%

  mutate(

    DATE_REVIEW_FLAG =
      AE_START < TRT_START

  )

The exact logic depends on the clinical meaning of the variables.

Do not encode assumptions as data-quality rules without defining them. Clinical-trial dates can have nuanced meanings, and an apparent temporal inconsistency may be valid. Review rules should be specified, documented, and clinically evaluated.

Detecting Duplicate Records

A simple exploratory duplicate check can use a combination of key variables.

duplicates <- data %>%

  group_by(
    USUBJID,
    VISIT,
    LBTEST
  ) %>%

  filter(
    n() > 1
  ) %>%

  ungroup()

The important question is not merely whether duplicates exist, but whether they are legitimate according to the domain structure.

Missing Visit Review

For longitudinal clinical data, the application can compare observed visits with expected visits.

For example:

expected_visits <-
  c(
    "Baseline",
    "Week 4",
    "Week 8",
    "Week 12",
    "Week 16"
  )

The application can then identify expected visits that are absent.

Important: A missing record is not necessarily a data error. Visits may legitimately be missed because of treatment discontinuation, illness, withdrawal, scheduling issues, or other protocol-defined circumstances.

Interactive Plots With plotly

Static ggplot2 graphics can be converted to interactive Plotly graphics.

library(plotly)

output$lab_plot <- renderPlotly({

  p <- ggplot(
    subject_lb(),
    aes(
      x = ADY,
      y = LBSTRESN,
      text = paste(
        "Visit:",
        AVISIT,
        "<br>Value:",
        LBSTRESN
      )
    )
  ) +

    geom_line() +

    geom_point()

  ggplotly(
    p,
    tooltip = "text"
  )

})

This can allow reviewers to hover over measurements and inspect exact values.

Why Interactivity Helps Clinical Review

Consider a plot containing 100 laboratory measurements.

A static image forces the reviewer to estimate which point corresponds to which visit.

An interactive plot can expose:

  • Exact value
  • Study day
  • Visit
  • Laboratory test
  • Reference range
  • Subject identifier

This can significantly improve exploratory review.

Adding a Patient-Level Tumor Burden Plot

For oncology studies, a Shiny application can combine clinical data review with longitudinal tumor-burden visualization.

output$tumor_plot <- renderPlot({

  x <- tumor_data() %>%

    filter(
      USUBJID == input$subject
    )

  ggplot(
    x,
    aes(
      x = ADY,
      y = PCHG
    )
  ) +

    geom_line() +

    geom_point() +

    geom_hline(
      yintercept = 0
    ) +

    geom_hline(
      yintercept = -30,
      linetype = "dashed"
    ) +

    labs(
      x = "Study Day",
      y = "Percent Change from Baseline"
    ) +

    theme_classic()

})

This type of visualization can help a reviewer connect quantitative longitudinal measurements with other subject-level information.

Downloading Filtered Data

Reviewers may need to export the observations currently being displayed.

Shiny provides downloadHandler() for this purpose.

output$download_data <-
  downloadHandler(

    filename = function() {

      paste0(
        "clinical_review_",
        Sys.Date(),
        ".csv"
      )

    },

    content = function(file) {

      write.csv(
        review_data(),
        file,
        row.names = FALSE
      )

    }

  )

The UI can contain:

downloadButton(
  "download_data",
  "Download Review Data"
)

Downloads Need Governance

The ability to download clinical data is powerful and potentially sensitive.

A production application should define:

  • Who can download data
  • Which data may be downloaded
  • Whether direct identifiers are present
  • Whether exports are logged
  • How downloaded files are stored
  • How long exports are retained
  • Whether exports contain derived review flags
Never assume that a download button is harmless. A Shiny application can expose sensitive clinical information if access controls and output restrictions are not designed appropriately.

Handling Subject Identifiers

Clinical applications commonly use subject identifiers such as:

USUBJID
SUBJID
SUBJECT_ID

The application should use a consistent identifier throughout the reactive workflow.

For example:

subject_data <- reactive({

  req(input$subject)

  list(

    dm = dm %>%
      filter(
        USUBJID == input$subject
      ),

    lb = lb %>%
      filter(
        USUBJID == input$subject
      ),

    ae = ae %>%
      filter(
        USUBJID == input$subject
      )

  )

})

Modularizing a Large Shiny Application

A small application can live in one app.R file.

A large clinical application should generally be modularized.

A possible structure is:

clinical-review-app/

  app.R

  R/
    mod_overview.R
    mod_subject.R
    mod_labs.R
    mod_ae.R
    mod_exposure.R
    mod_data_quality.R

  data/
    dm.rds
    lb.rds
    ae.rds

  www/
    styles.css

  tests/
    testthat/
      test-review-flags.R
      test-date-rules.R

This separates application modules from data, styling, and tests.

Shiny Modules

A module usually has a UI function and a server function.

mod_labs_ui <- function(id) {

  ns <- NS(id)

  tagList(

    selectInput(
      ns("parameter"),
      "Laboratory Parameter",
      choices = NULL
    ),

    DTOutput(
      ns("table")
    )

  )

}

The corresponding server function might be:

mod_labs_server <-
  function(
    id,
    data
  ) {

    moduleServer(
      id,
      function(
        input,
        output,
        session
      ) {

        ...

      }
    )

  }

Why Modules Matter

Modules provide several advantages:

  • Reduced code duplication
  • Clear separation of responsibilities
  • Easier testing
  • Easier maintenance
  • Smaller server functions
  • Better collaboration among programmers

For a clinical review application containing multiple domains, modularity quickly becomes important.

Separating Clinical Logic From UI Logic

A common mistake is placing complex clinical derivations directly inside render functions.

For example, avoid building a large clinical algorithm inside:

output$table <- renderDT({

  # hundreds of lines
  # of clinical logic

})

Instead, calculate the review dataset separately.

review_labs <- reactive({

  ...

})

output$table <- renderDT({

  datatable(
    review_labs()
  )

})

This makes the logic easier to inspect and test.

Functions for Reusable Clinical Logic

Even better, some logic can be implemented as ordinary R functions.

flag_high_lab <- function(
  value,
  upper_limit
) {

  !is.na(value) &&
    !is.na(upper_limit) &&
    value > upper_limit

}

The Shiny server can then call the function.

review_labs <- reactive({

  x <- subject_lb()

  x$HIGH_FLAG <- mapply(
    flag_high_lab,
    x$LBSTRESN,
    x$LBORNRHI
  )

  x

})

This is easier to unit-test than logic embedded exclusively in the UI.

Testing Clinical Review Functions

A function such as:

flag_high_lab(
  value = 86,
  upper_limit = 40
)

should return:

TRUE

while:

flag_high_lab(
  value = 35,
  upper_limit = 40
)

should return:

FALSE

A unit-testing framework such as testthat can automate these checks.

test_that(
  "high laboratory values are flagged",
  {

    expect_true(
      flag_high_lab(86, 40)
    )

    expect_false(
      flag_high_lab(35, 40)
    )

  }
)

Testing Shiny Applications

Testing should not stop at the underlying R functions.

Shiny provides tools for testing reactive behavior and application outputs.

Useful testing levels include:

Testing Level Question
Unit test Does a clinical derivation return the correct value?
Reactive test Does an input produce the expected filtered data?
UI test Does the expected control or output exist?
Integration test Do several application components work together?
User acceptance test Can the intended reviewer complete the required workflow?

Clinical Validation Versus Software Testing

These concepts overlap but are not identical.

Software testing asks whether the application behaves according to its specification.

Clinical validation asks whether the implemented review logic appropriately represents the intended clinical meaning.

Example: A program can correctly calculate "ALT > ULN" according to its programmed logic while still being clinically inappropriate if the wrong reference-range variable was selected.

Handling Missing Data

Missingness is common in clinical data.

The application should distinguish between:

  • Missing because not collected
  • Missing because not applicable
  • Missing because subject discontinued
  • Missing because of an invalid result
  • Missing because data are not yet available

A generic NA does not necessarily explain the reason for missingness.

Use Clinical Status Variables Where Available

For example:

LBSTAT
LBREASND
AEOUT
DSDECOD
EPOCH
VISIT

The application can display these variables alongside missing or incomplete observations when appropriate.

Performance Considerations

Clinical datasets can become large.

An application that works well with 10,000 observations may become slow with millions of observations.

Potential strategies include:

  • Precompute stable derivations
  • Load only necessary variables
  • Filter data before expensive operations
  • Use database queries for large datasets
  • Avoid unnecessary reactive dependencies
  • Cache expensive calculations where appropriate
  • Use server-side table processing for large listings

Reactive Expressions Should Be Narrow

Suppose a single reactive expression calculates every domain:

all_data <- reactive({

  # process DM
  # process LB
  # process AE
  # process EX
  # process VS
  # process ECG
  # process tumor data

})

A small change to one input could cause unnecessary work.

It is usually better to create focused reactive expressions with clear dependencies.

Caching

Some computations are expensive but produce the same result for repeated inputs.

Caching can reduce repeated computation.

The appropriate strategy depends on the application architecture and the specific Shiny version and packages being used.

Do not optimize prematurely. First establish correct clinical behavior. Then profile the application and optimize the actual performance bottlenecks.

Security Is Part of the Application Design

A clinical review application may contain confidential trial information.

Security should therefore be considered before deployment rather than added afterward.

Important areas include:

  • Authentication
  • Authorization
  • Encryption in transit
  • Secure storage
  • Session management
  • Download permissions
  • Logging
  • Infrastructure security
  • Secret management

Authentication and Authorization Are Different

Authentication answers:

$$ \text{Who are you?} $$

Authorization answers:

$$ \text{What are you allowed to access?} $$

A user successfully logging in does not necessarily mean that the user should have access to every study, treatment arm, subject, or download function.

Do Not Put Security Logic Only in the UI

Hiding a button does not necessarily constitute access control.

For example, this:

if (user_is_admin) {

  downloadButton(
    "download_data"
  )

}

may change the interface, but authorization must also be enforced at the server or application infrastructure level.

Auditability

For a controlled clinical workflow, it may be important to know:

  • Who accessed the application
  • When they accessed it
  • Which study they reviewed
  • Which subject was selected
  • Which filters were applied
  • Which outputs were downloaded
  • Which application version was used

The appropriate level of logging depends on the intended use and governance requirements.

Application Versioning

Every production application should have an identifiable version.

For example:

APP_VERSION <- "1.4.0"

The version can be displayed in the UI:

tags$small(
  paste(
    "Application version:",
    APP_VERSION
  )
)

Version control systems such as Git are particularly useful for maintaining the history of application changes.

Data Versioning Matters Too

The same application can produce different review results when the underlying clinical data change.

Therefore, a robust review workflow should consider both:

  • Application version
  • Data snapshot/version

For example:

Item Example
Application v1.4.0
Clinical data snapshot 2026-08-31
Review population Safety population
Study ABC-001

Separating Development, Test, and Production

A useful deployment model is:

1
Development: programmers implement and debug changes.
2
Test: reviewers evaluate the application against defined test cases.
3
Production: approved users access the controlled application.

Production should not simply be the programmer's current working copy.

Using Configuration Files

Different environments may require different data locations or settings.

These should generally be separated from application logic.

For example:

config <- config::get()

data_path <- config$data_path

This can reduce the need to edit application code when moving between environments.

Logging and Diagnostics

A production application should provide enough diagnostic information to identify technical failures.

However, logs should be designed carefully so that they do not unnecessarily expose sensitive clinical information.

Logging principle: Do not casually write complete clinical records, patient-level laboratory results, or other sensitive information to application logs merely to simplify debugging.

Building a Complete Minimal Clinical Review App

The following example combines several concepts into a small but realistic Shiny application.

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

dm <- readRDS("dm.rds")
lb <- readRDS("lb.rds")

ui <- fluidPage(

  titlePanel(
    "Clinical Data Review"
  ),

  sidebarLayout(

    sidebarPanel(

      selectInput(
        "treatment",
        "Treatment",
        choices = c(
          "All",
          sort(unique(dm$TRT01P))
        )
      ),

      selectInput(
        "subject",
        "Subject",
        choices = NULL
      ),

      downloadButton(
        "download",
        "Download"
      )

    ),

    mainPanel(

      tabsetPanel(

        tabPanel(
          "Subject",
          DTOutput("subject_table")
        ),

        tabPanel(
          "Laboratory",
          DTOutput("lab_table"),
          plotOutput("lab_plot")
        )

      )

    )

  )

)

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

  filtered_dm <- reactive({

    if (
      input$treatment == "All"
    ) {

      dm

    } else {

      dm %>%
        filter(
          TRT01P == input$treatment
        )

    }

  })

  observe({

    updateSelectInput(
      session,
      "subject",
      choices =
        sort(
          unique(
            filtered_dm()$USUBJID
          )
        )
    )

  })

  subject_dm <- reactive({

    req(input$subject)

    filtered_dm() %>%
      filter(
        USUBJID == input$subject
      )

  })

  subject_lb <- reactive({

    req(input$subject)

    lb %>%
      filter(
        USUBJID == input$subject
      )

  })

  output$subject_table <-
    renderDT({

      datatable(
        subject_dm(),
        rownames = FALSE
      )

    })

  output$lab_table <-
    renderDT({

      datatable(
        subject_lb(),
        rownames = FALSE,
        filter = "top"
      )

    })

  output$lab_plot <-
    renderPlot({

      x <- subject_lb()

      validate(
        need(
          nrow(x) > 0,
          "No laboratory data available."
        )
      )

      ggplot(
        x,
        aes(
          x = ADY,
          y = LBSTRESN
        )
      ) +

        geom_line() +

        geom_point() +

        labs(
          x = "Study Day",
          y = "Laboratory Value"
        ) +

        theme_classic()

    })

  output$download <-
    downloadHandler(

      filename = function() {

        paste0(
          input$subject,
          "_clinical_review.csv"
        )

      },

      content = function(file) {

        write.csv(
          subject_lb(),
          file,
          row.names = FALSE
        )

      }

    )

}

shinyApp(
  ui,
  server
)

Improving the Minimal Application

The example above demonstrates the architecture, but a production-quality application would typically add:

  • Input validation
  • Better error messages
  • Authentication
  • Authorization
  • Application version information
  • Data version information
  • Clinical review flags
  • Domain-specific modules
  • Automated testing
  • Controlled downloads
  • Performance monitoring

Using shinyvalidate

Input validation can be useful when the reviewer must make a valid selection before proceeding.

For example, a subject selector should not silently accept an invalid value.

Validation packages and ordinary Shiny validation functions can be used depending on the complexity of the application.

Using updateSelectInput()

Dynamic controls are especially useful in clinical review.

For example, the available subjects can depend on the selected treatment.

observeEvent(
  input$treatment,
  {

    choices <- filtered_dm() %>%
      pull(USUBJID) %>%
      unique() %>%
      sort()

    updateSelectInput(
      session,
      "subject",
      choices = choices
    )

  }
)

This prevents the reviewer from selecting a subject that is outside the current treatment filter.

observe() Versus observeEvent()

These functions serve related but different purposes.

Function Typical Use
observe() Respond to changes in reactive dependencies
observeEvent() Respond specifically to an event such as a button click or input change

For example:

observeEvent(
  input$refresh,
  {
    ...
  }
)

This is useful when the reviewer explicitly requests a refresh or recalculation.

Action Buttons

An action button can allow a reviewer to deliberately initiate a workflow.

actionButton(
  "refresh",
  "Refresh Review"
)

The server can respond with:

observeEvent(
  input$refresh,
  {

    # perform requested action

  }
)

However, many Shiny outputs should update automatically through reactivity. Action buttons should therefore be used when explicit user initiation has meaning.

Clinical Review Workflow With an Action Button

For example:

1
Reviewer selects study and review population.
2
Reviewer selects a subject.
3
Reviewer clicks "Run Review."
4
Application evaluates predefined review rules.
5
Potential review candidates are displayed.
6
Reviewer examines the underlying source records.

Don't Hide the Underlying Data

A review application should make it easy to move from a flag to the observations that produced it.

For example, instead of simply displaying:

FLAG: Laboratory abnormality

the application should allow the reviewer to inspect:

  • Subject
  • Test
  • Visit
  • Study day
  • Result
  • Units
  • Reference range
  • Collection date
  • Source status

This makes the flag auditable and clinically interpretable.

Displaying a Review Explanation

A useful application can display the reason a record was flagged.

x$REVIEW_REASON <-

  ifelse(
    x$LBSTRESN > x$LBORNRHI,
    "Result exceeds upper reference limit",
    ""
  )

The reviewer then sees both the flag and the reason.

Flagging Multiple Conditions

A subject may have several types of potential issues.

x <- x %>%

  mutate(

    HIGH_LAB =
      LBSTRESN > LBORNRHI,

    LOW_LAB =
      LBSTRESN < LBORNRLO,

    MISSING_UNIT =
      is.na(LBSTRESU),

    MISSING_DATE =
      is.na(LBDTC)

  )

A summary flag can then be derived:

x$ANY_REVIEW_FLAG <-

  x$HIGH_LAB |
  x$LOW_LAB |
  x$MISSING_UNIT |
  x$MISSING_DATE

Use a Flag Hierarchy

Not all flags have equal importance.

A useful review interface may distinguish:

Level Example
Informational Value outside a typical display range
Review Unexpected laboratory abnormality
High priority Potential serious safety-data inconsistency

The exact classification should be based on the intended clinical workflow.

Data Review Versus Data Cleaning

A Shiny review application can identify potential issues without necessarily changing the underlying data.

For example:

$$ \text{Source Data} \rightarrow \text{Review Rule} \rightarrow \text{Potential Issue} \rightarrow \text{Human Review} $$

This is different from:

$$ \text{Source Data} \rightarrow \text{Automatic Modification} $$
Prefer review over silent correction. Clinical data should not be automatically altered merely because a programmed rule identifies an unusual observation.

Deployment Options

A Shiny application can be deployed in several environments.

Examples include:

  • Local R sessions
  • Internal servers
  • Shiny Server environments
  • Managed Shiny hosting
  • Containerized infrastructure
  • Enterprise cloud environments

The appropriate option depends on organizational security, data location, authentication, scalability, and governance requirements.

Local Development

For development, the simplest workflow is often:

shiny::runApp(
  "clinical-review-app"
)

This is appropriate for development and testing, not necessarily for production deployment.

Containerization

A larger application may be packaged using a container.

A simplified conceptual Dockerfile might look like:

FROM rocker/r-ver:latest

WORKDIR /app

COPY . /app

RUN R -e "install.packages(
  c('shiny','dplyr','DT','ggplot2'),
  repos='https://cloud.r-project.org'
)"

EXPOSE 3838

CMD ["R", "-e",
     "shiny::runApp(
       '/app',
       host='0.0.0.0',
       port=3838
     )"]

The exact base image, package versions, system dependencies, and deployment configuration should be controlled for the intended environment.

Package Version Management

Reproducibility is especially important for clinical applications.

Consider an application that uses:

shiny
dplyr
DT
ggplot2
plotly

Changes in package versions can sometimes affect application behavior.

A dependency-management approach such as renv can help capture the R package environment.

renv::init()

renv::snapshot()

renv::restore()

This allows a defined package environment to be recreated more reliably.

Reproducibility Checklist

1
Record the application version.
2
Record the clinical-data snapshot.
3
Record the R version.
4
Record package versions.
5
Store application code in version control.
6
Document review rules.
7
Maintain automated tests.

Common Shiny Performance Problems

Reading Data Inside Every Reactive Expression

This is inefficient:

labs <- reactive({

  readRDS(
    "lb.rds"
  )

})

If the underlying data do not change during the session, they can generally be loaded outside the reactive expression.

lb <- readRDS(
  "lb.rds"
)

labs <- reactive({

  lb %>%
    filter(
      USUBJID == input$subject
    )

})

Repeatedly Performing Expensive Joins

If several outputs repeatedly perform the same expensive join, consider preparing the required review dataset once.

review_base <- reactive({

  dm %>%

    left_join(
      treatment,
      by = "USUBJID"
    )

})

Then downstream outputs can use the result.

Common Clinical Shiny Mistakes

  1. Building one enormous app.R file. Large applications become difficult to test and maintain.
  2. Putting clinical logic directly inside render functions. This makes derivations harder to validate.
  3. Modifying source data in place. Review flags should generally be derived separately.
  4. Assuming a flag equals an error. A flagged record is a review candidate.
  5. Ignoring reference-range metadata. Clinical laboratory limits may differ by laboratory and context.
  6. Hard-coding credentials. Secrets should be managed securely.
  7. Using UI visibility as authorization. Hiding a button is not sufficient access control.
  8. Failing to version the application. Review outputs should be traceable to the application version.
  9. Failing to version the data. The same application can produce different results from different data snapshots.
  10. Skipping independent validation. Interactive applications still require testing.

Clinical Data Review Application Specification

Before programming, define the intended application behavior.

Specification Example
Purpose Interactive exploratory clinical data review
Population All randomized subjects
Data snapshot Defined clinical database extract
Primary identifier USUBJID
Filters Treatment, site, subject, visit
Domains DM, LB, AE, EX, VS
Flags Predefined review rules
Plots Laboratory and longitudinal clinical measurements
Downloads Filtered review dataset
Authentication Required for controlled deployment
Version Displayed within application

Example Application Requirements

A more formal requirement might say:

Example requirement: The application shall allow an authorized reviewer to select a treatment group and subject identifier and display the subject's demographic, laboratory, adverse-event, exposure, and vital-sign records. The application shall identify predefined review conditions without modifying the source observations and shall provide a controlled download of the currently displayed review dataset.

This is much more useful than a vague requirement such as "build a dashboard."

User Acceptance Testing

A clinical review application should have explicit test scenarios.

Test Expected Result
Select Drug A Only Drug A subjects appear
Select Site 101 Only Site 101 subjects appear
Select subject 1001 All subject-level domains show 1001
Select laboratory parameter Corresponding laboratory records appear
Subject has no AE records Application displays an appropriate empty-state message
Flagged laboratory result exists Expected review flag is displayed
Download selected data Downloaded file matches displayed filtered data

Empty States Matter

An application should explicitly communicate when there is no data.

For example:

validate(
  need(
    nrow(x) > 0,
    "No adverse events are available for this subject."
  )
)

This is much better than showing an empty table with no explanation.

Error Handling

Unexpected technical problems should be separated from expected clinical conditions.

Situation Preferred Behavior
No subject selected Prompt for a subject
No laboratory data Display informative empty state
Invalid data structure Generate controlled technical error
Database unavailable Display service-unavailable message and log technical details appropriately
Unexpected application exception Fail safely without exposing sensitive internals

Clinical Review and Traceability

One of the strongest features of a well-designed application is the ability to trace a displayed result back to its underlying data.

For example:

$$ \text{Screen} \rightarrow \text{Derived Flag} \rightarrow \text{Analysis Variable} \rightarrow \text{Source Observation} $$

This principle is particularly important when the application is used in a clinical-development environment.

Example Traceability Structure

Displayed Item Source / Derivation
Treatment DM treatment variable
Subject status Disposition-derived value
ALT flag LB result compared with applicable reference limit
SAE count AE records with serious-event indicator
Laboratory trend Longitudinal LB observations

Shiny for Exploratory Clinical Statistics

Although clinical data review is the focus of this tutorial, Shiny can also support exploratory statistical analysis.

Examples include:

  • Descriptive summaries
  • Box plots
  • Longitudinal plots
  • Patient-level profiles
  • Response summaries
  • Subgroup exploration
  • Laboratory shift tables
  • Exposure-response exploration
Exploratory does not mean uncontrolled. Exploratory outputs can still influence clinical decisions. Their purpose, data source, calculation logic, and limitations should therefore be clear to the users.

Example Laboratory Shift Summary

A Shiny application can generate a simple baseline-versus-post-baseline summary.

shift_data <- reactive({

  x <- subject_lb()

  x %>%

    mutate(

      SHIFT = case_when(

        BASELINE == "Normal" &
          POSTBASE == "High"
          ~ "Normal → High",

        BASELINE == "High" &
          POSTBASE == "Normal"
          ~ "High → Normal",

        TRUE
          ~ "Other"

      )

    )

})

The exact definition of baseline and post-baseline status should follow the study's analysis conventions.

Using Shiny With ADaM Data

A clinical review application can also consume analysis-ready ADaM datasets.

For example:

  • ADSL for subject-level information
  • ADLB for laboratory analysis
  • ADAE for adverse-event analysis
  • ADVS for vital signs
  • ADTTE for time-to-event review

This can be particularly useful when the application is intended to review analysis populations or derived endpoints.

Choose the data layer deliberately. SDTM may be appropriate when reviewing standardized collected observations. ADaM may be appropriate when reviewing analysis-ready derivations. The choice depends on the purpose of the application.

Using Shiny With SDTM Data

For source-oriented review, SDTM domains may provide useful standardized structures.

For example:

dm <- readRDS("sdtm_dm.rds")
lb <- readRDS("sdtm_lb.rds")
ae <- readRDS("sdtm_ae.rds")

The application can expose domain-specific variables while preserving their SDTM meaning.

Using Shiny With ADaM Data

For analysis review:

adsl <- readRDS("adsl.rds")
adlb <- readRDS("adlb.rds")
adae <- readRDS("adae.rds")

The application can then use analysis variables such as treatment, population flags, baseline indicators, change from baseline, and analysis values.

Don't Mix SDTM and ADaM Variables Without Explaining the Difference

A reviewer should be able to determine whether a displayed value represents:

  • A collected clinical observation
  • A standardized SDTM value
  • An analysis derivation
  • A review-specific flag

Clear labeling is therefore essential.

Clinical Review App Versus CSR Output

Feature Clinical Review App CSR Output
Interactivity High Usually limited
Filtering User controlled Prespecified
Exploration Excellent Limited
Traceability Can be interactive Usually static/documented
Primary analysis Usually not Yes, when applicable
Clinical review Excellent Limited

When Shiny Is a Good Choice

Shiny is particularly useful when:

  • Reviewers need to explore many subjects.
  • Multiple clinical domains need to be connected.
  • Interactive filtering provides substantial value.
  • Longitudinal trends need exploration.
  • Users need patient-level drill-down.
  • Repeated static report generation is inefficient.

When Shiny May Not Be the Best Choice

A Shiny application may be unnecessary when:

  • A single static table completely answers the question.
  • The output is a formal submission deliverable.
  • The workflow requires a tightly controlled validated reporting system rather than exploratory review.
  • The dataset is too large for the chosen architecture without an appropriate data backend.
  • The intended users do not need interactive functionality.

Production Readiness Checklist

1
Define the intended use.
2
Define the source data.
3
Define clinical review rules.
4
Separate source data from derived review variables.
5
Build modular UI and server components.
6
Implement input validation and meaningful empty states.
7
Implement unit and application-level testing.
8
Control authentication and authorization.
9
Version the application and data snapshot.
10
Validate production deployment and review workflows.

A Practical Development Workflow

A sensible development sequence is:

  1. Start with one domain.
  2. Build the subject selector.
  3. Build one interactive table.
  4. Add one longitudinal plot.
  5. Add clinical review flags.
  6. Separate clinical logic into functions.
  7. Add automated tests.
  8. Introduce modules.
  9. Add authentication and authorization.
  10. Optimize performance.
  11. Deploy to a controlled test environment.
  12. Perform user acceptance testing.
  13. Release the approved version.

Trying to build the entire clinical review platform at once often creates unnecessary complexity.

Example Project Structure

clinical-review/

├── app.R
│
├── R/
│   ├── mod_overview.R
│   ├── mod_subject.R
│   ├── mod_labs.R
│   ├── mod_ae.R
│   ├── mod_exposure.R
│   └── review_rules.R
│
├── data/
│   ├── adsl.rds
│   ├── adlb.rds
│   ├── adae.rds
│   └── adex.rds
│
├── tests/
│   └── testthat/
│       ├── test-review-rules.R
│       └── test-derived-flags.R
│
├── www/
│   └── custom.css
│
├── renv.lock
│
└── README.md

Documentation Should Be Part of the Application

A production clinical review application should have documentation describing:

  • Purpose
  • Intended users
  • Data sources
  • Data refresh process
  • Review rules
  • Known limitations
  • Application version
  • Deployment architecture
  • Security model
  • Testing strategy

What a README Should Explain

# Clinical Review Application

## Purpose

Interactive review of clinical-trial data.

## Data Sources

ADSL, ADLB, ADAE, and related analysis datasets.

## Application

Shiny application implemented in R.

## Review Flags

Defined in R/review_rules.R.

## Testing

Automated tests are stored under tests/testthat/.

## Version

1.4.0

## Data Snapshot

2026-08-31

Clinical Review App Design Principles

Several principles are particularly important.

Principle Meaning
Traceability Users can identify where displayed values originate.
Transparency Review flags have understandable explanations.
Reproducibility Application and data versions are identifiable.
Security Users only access information appropriate to their role.
Validation Clinical and technical behavior is tested.
Separation Source data, derivations, presentation, and review actions remain distinguishable.
Usability Reviewers can reach the relevant subject-level information efficiently.

The Most Important Shiny Concept

The most important concept is not a particular widget or plotting package.

It is the reactive dependency graph.

For a clinical review application, the graph might look like:

$$ \text{Treatment} \rightarrow \text{Subjects} \rightarrow \text{Selected Subject} \rightarrow \begin{cases} \text{Demographics}\\ \text{Laboratory}\\ \text{Adverse Events}\\ \text{Exposure}\\ \text{Vital Signs} \end{cases} $$

Once this dependency structure is understood, the rest of Shiny becomes much more manageable.

The Most Important Clinical Concept

The most important clinical concept is that the application is a review layer, not an excuse to obscure the underlying data.

Every important displayed result should have a clear explanation of:

  • What the value represents
  • Where it came from
  • How it was derived
  • Why it was flagged
  • What the reviewer should do with the information

Recommended Mental Model

Think of the application as an interactive clinical data microscope. The Shiny application helps the reviewer zoom from the study level to the treatment group, then to the subject, then to individual observations. It should make patterns easier to discover while preserving a transparent path back to the underlying data.

Final Checklist

1
Define the clinical review question.
2
Identify the authoritative source data.
3
Design the reactive dependency graph.
4
Build filters and subject selection.
5
Create domain-specific review modules.
6
Add tables and interactive visualizations.
7
Implement transparent review flags.
8
Separate clinical logic from presentation logic.
9
Test the derivations and application behavior.
10
Control access, versioning, deployment, and downloads.

Summary

R Shiny provides a powerful framework for turning clinical-trial data into an interactive review environment.

The basic architecture is straightforward:

$$ \text{Input} \rightarrow \text{Reactive Data} \rightarrow \text{Output} $$

The challenge in clinical applications is not simply creating an attractive interface. The application must preserve the meaning, provenance, and traceability of clinical data while making review faster and more intuitive.

A well-designed application can allow a reviewer to move from study-level summaries to treatment groups, subjects, individual laboratory results, adverse events, exposure records, and longitudinal plots without repeatedly generating static listings.

The most effective architecture separates:

  • Source data
  • Clinical derivation logic
  • Review rules
  • Reactive application logic
  • User interface
  • Security
  • Testing
  • Deployment

When these layers are kept distinct, Shiny becomes much more than an R dashboard. It becomes a flexible framework for interactive clinical-data review.

Bottom line: A strong clinical Shiny application should make clinical data easier to explore without making the underlying data harder to understand. Use reactivity to connect reviewer inputs to focused data transformations, use interactive tables and plots to expose patient-level patterns, use explicit review flags rather than silent data modification, and build traceability, testing, security, versioning, and governance into the application from the beginning.

References

Chang, W., Cheng, J., Allaire, J., Xie, Y., McPherson, J. shiny: Web Application Framework for R. R package documentation.

Wickham, H., Averick, M., Bryan, J., et al. (2019). Welcome to the tidyverse. Journal of Open Source Software, 4(43), 1686.

Wickham, H., François, R., Henry, L., Müller, K., Vaughan, D. dplyr: A Grammar of Data Manipulation. R package documentation.

Xie, Y., Cheng, J., Allaire, J.J. DT: A Wrapper of the JavaScript Library DataTables. R package documentation.

Wickham, H. ggplot2: Elegant Graphics for Data Analysis. Springer.

Posit. Shiny Documentation. Documentation for building, testing, and deploying Shiny applications.

R Core Team. R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing.