10  Interactive Elements

10.1 Introduction to Interactive Clinical Data Tools

Interactive tools enable clinicians, researchers, and stakeholders to directly engage with clinical data, supporting more effective decision-making and exploratory analysis. This chapter explores approaches for creating interactive applications in R for clinical research contexts.

10.1.1 The Value of Interactivity in Clinical Research

Interactive tools provide several key benefits in clinical research:

  1. Exploratory analysis: Facilitating investigation of patterns and relationships without requiring custom code for each analysis
  2. Stakeholder engagement: Making complex clinical data accessible to non-statistical stakeholders
  3. Real-time decision support: Supporting rapid, data-driven decisions during clinical trials
  4. Patient communication: Enhancing discussion of trial results with patients and advocacy groups
  5. Knowledge dissemination: Sharing findings more effectively with the broader scientific community

In this chapter, we’ll explore how to implement interactive tools using R, focusing on approaches that maintain reproducibility, regulatory compliance, and scientific rigor.

10.2 Shiny Applications for Clinical Data

10.2.1 Introduction to Shiny for Clinical Data

Shiny is an R package that enables the creation of interactive web applications directly from R code. Its integration with R’s statistical capabilities makes it particularly well-suited for clinical data applications:

Code
library(tidyverse)
library(shiny)
library(knitr)

# Key benefits of Shiny for clinical applications
shiny_benefits <- tribble(
  ~Benefit, ~Description, ~Clinical_Example,
  "Statistical integration", "Direct access to R's statistical capabilities", "Interactive survival curve exploration",
  "Reproducibility", "Code-based approach maintains reproducibility", "Analysis parameters saved with results",
  "Customization", "Highly customizable interface and behavior", "Trial-specific visualizations and analyses",
  "Minimal web development", "Create web apps without HTML/CSS/JavaScript expertise", "Rapid deployment of clinical tools",
  "Secure deployment", "Options for secure, validated deployment", "Protected health information compliance"
)

# Display Shiny benefits
kable(shiny_benefits, 
     caption = "Benefits of Shiny for Clinical Research Applications")

10.2.2 Basic Structure of a Clinical Shiny Application

Shiny applications consist of a user interface (UI) and a server function. Here’s a template for a clinical data exploration application:

Code
library(shiny)
library(tidyverse)
library(DT)

# UI definition
ui <- fluidPage(
  # Application title
  titlePanel("Clinical Trial Data Explorer"),
  
  # Sidebar with controls
  sidebarLayout(
    sidebarPanel(
      # Study selection
      selectInput("study_id", 
                 "Select Study:",
                 choices = c("Study A", "Study B", "Study C")),
      
      # Endpoint selection
      selectInput("endpoint", 
                 "Select Endpoint:",
                 choices = c("Primary", "Secondary 1", "Secondary 2")),
      
      # Analysis parameters
      checkboxGroupInput("subgroups", 
                        "Select Subgroups:",
                        choices = c("Age", "Sex", "Region")),
      
      # Action button
      actionButton("analyze_btn", "Run Analysis")
    ),
    
    # Main panel with outputs
    mainPanel(
      tabsetPanel(
        tabPanel("Results Summary", 
                 h3("Analysis Results"),
                 verbatimTextOutput("summary_text"),
                 plotOutput("main_plot")),
        tabPanel("Patient Data", 
                 DT::dataTableOutput("patient_table")),
        tabPanel("Statistical Details", 
                 verbatimTextOutput("stats_output"))
      )
    )
  ),
  
  # Footer with validation information
  tags$footer(
    hr(),
    p(paste("Application Version: 1.0.0 | Last Updated:", Sys.Date())),
    p("Validated for exploratory use only. Not for regulatory submission.")
  )
)

# Server logic
server <- function(input, output, session) {
  # Reactive value to store analysis results
  analysis_results <- reactiveVal(NULL)
  
  # Load data based on study selection
  study_data <- reactive({
    # In practice, would load from database or files
    req(input$study_id)
    
    # Simulated data loading
    message(paste("Loading data for", input$study_id))
    
    # Return simulated dataset
    data.frame(
      patient_id = paste0("PT", 1:100),
      treatment = sample(c("Treatment", "Placebo"), 100, replace = TRUE),
      age = round(rnorm(100, 65, 10)),
      sex = sample(c("Male", "Female"), 100, replace = TRUE),
      region = sample(c("North America", "Europe", "Asia"), 100, replace = TRUE),
      response = sample(c("Responder", "Non-responder"), 100, replace = TRUE),
      time_to_event = rexp(100, 0.1),
      event = sample(c(0, 1), 100, replace = TRUE, prob = c(0.3, 0.7))
    )
  })
  
  # Run analysis when button is clicked
  observeEvent(input$analyze_btn, {
    # Get current data
    data <- study_data()
    
    # Perform analysis based on selected endpoint
    if (input$endpoint == "Primary") {
      # Example: Survival analysis for primary endpoint
      if (requireNamespace("survival", quietly = TRUE)) {
        fit <- survival::survfit(
          survival::Surv(time_to_event, event) ~ treatment, 
          data = data
        )
        
        # Store results
        analysis_results(list(
          type = "survival",
          fit = fit,
          summary = summary(fit),
          coxph = survival::coxph(
            survival::Surv(time_to_event, event) ~ treatment, 
            data = data
          )
        ))
      }
    } else {
      # Example: Response rate analysis for secondary endpoints
      resp_table <- table(data$treatment, data$response)
      test_result <- fisher.test(resp_table)
      
      # Store results
      analysis_results(list(
        type = "response",
        table = resp_table,
        test = test_result
      ))
    }
  })
  
  # Render results summary
  output$summary_text <- renderPrint({
    results <- analysis_results()
    req(results)
    
    if (results$type == "survival") {
      summary(results$coxph)
    } else {
      results$test
    }
  })
  
  # Render main plot
  output$main_plot <- renderPlot({
    results <- analysis_results()
    req(results)
    
    if (results$type == "survival") {
      # Plot survival curves
      if (requireNamespace("survminer", quietly = TRUE)) {
        survminer::ggsurvplot(
          results$fit,
          data = study_data(),
          pval = TRUE,
          conf.int = TRUE,
          risk.table = TRUE,
          title = paste("Survival Analysis -", input$study_id)
        )
      } else {
        plot(results$fit, 
             main = paste("Survival Analysis -", input$study_id),
             col = c("blue", "red"),
             xlab = "Time",
             ylab = "Survival Probability")
        legend("topright", 
               legend = c("Treatment", "Placebo"),
               col = c("blue", "red"),
               lty = 1)
      }
    } else {
      # Plot response rates
      data <- study_data()
      response_rates <- data %>%
        group_by(treatment) %>%
        summarize(
          n = n(),
          responders = sum(response == "Responder"),
          rate = responders / n * 100
        )
      
      ggplot(response_rates, aes(x = treatment, y = rate, fill = treatment)) +
        geom_bar(stat = "identity") +
        geom_text(aes(label = sprintf("%.1f%%", rate)), vjust = -0.5) +
        labs(
          title = paste("Response Rates -", input$study_id),
          x = "Treatment Group",
          y = "Response Rate (%)"
        ) +
        theme_minimal()
    }
  })
  
  # Render patient data table
  output$patient_table <- DT::renderDataTable({
    DT::datatable(
      study_data(),
      options = list(
        pageLength = 10,
        searchHighlight = TRUE,
        scrollX = TRUE
      ),
      filter = "top"
    )
  })
  
  # Render statistical details
  output$stats_output <- renderPrint({
    results <- analysis_results()
    req(results)
    
    if (results$type == "survival") {
      results$fit
    } else {
      results$table
    }
  })
}

# Run the application
# shinyApp(ui = ui, server = server)

10.2.3 Creating Modular Clinical Shiny Applications

For more complex clinical applications, a modular approach improves maintainability:

Code
# Modular UI for patient selection
patientSelectUI <- function(id) {
  ns <- NS(id)
  
  tagList(
    h3("Patient Selection"),
    selectInput(ns("study_id"), "Study:", choices = c("Study A", "Study B")),
    selectInput(ns("site_id"), "Site:", choices = NULL),
    checkboxGroupInput(ns("inclusion"), "Inclusion Criteria:",
                      choices = c("Age 18-75", "ECOG 0-1", "Measurable disease")),
    actionButton(ns("apply"), "Apply Filters")
  )
}

# Modular server for patient selection
patientSelectServer <- function(id, parent_session) {
  moduleServer(id, function(input, output, session) {
    # Reactive site list based on study
    observeEvent(input$study_id, {
      # In practice, would query database
      sites <- switch(input$study_id,
                     "Study A" = c("Site 101", "Site 102", "Site 103"),
                     "Study B" = c("Site 201", "Site 202"),
                     c())
      
      updateSelectInput(session, "site_id", choices = sites)
    })
    
    # Return reactive expression with selected patients
    return(reactive({
      req(input$apply)
      
      # Simulate patient selection
      input$study_id  # Reactive dependency
      input$site_id   # Reactive dependency
      input$inclusion # Reactive dependency
      
      # In practice, would filter patient database
      message("Filtering patients based on criteria")
      
      # Return sample patient IDs
      paste0("PT", sample(1000:9999, 20))
    }))
  })
}

# Modular UI for efficacy analysis
efficacyAnalysisUI <- function(id) {
  ns <- NS(id)
  
  tagList(
    h3("Efficacy Analysis"),
    selectInput(ns("endpoint"), "Endpoint:", 
               choices = c("PFS", "OS", "ORR", "DoR")),
    checkboxGroupInput(ns("covariates"), "Adjustment Covariates:",
                      choices = c("Age", "Sex", "Region", "Baseline score")),
    actionButton(ns("run"), "Run Analysis"),
    hr(),
    plotOutput(ns("efficacy_plot")),
    verbatimTextOutput(ns("efficacy_results"))
  )
}

# Modular server for efficacy analysis
efficacyAnalysisServer <- function(id, patient_ids) {
  moduleServer(id, function(input, output, session) {
    # Run analysis when button clicked
    results <- eventReactive(input$run, {
      # Get current patients
      patients <- patient_ids()
      req(length(patients) > 0)
      
      # Log analysis parameters
      message(paste(
        "Running", input$endpoint, "analysis for", length(patients), 
        "patients with covariates:", paste(input$covariates, collapse = ", ")
      ))
      
      # Simulate analysis results
      list(
        endpoint = input$endpoint,
        hazard_ratio = runif(1, 0.5, 0.9),
        p_value = runif(1, 0.001, 0.1),
        median_trt = runif(1, 10, 20),
        median_ctl = runif(1, 5, 15)
      )
    })
    
    # Render efficacy plot
    output$efficacy_plot <- renderPlot({
      res <- results()
      req(res)
      
      # Create simulated survival curves for visualization
      time <- 0:24
      surv_trt <- exp(-(time/res$median_trt)^1.2)
      surv_ctl <- exp(-(time/res$median_ctl)^1.2)
      
      # Plot
      plot(time, surv_trt, type = "l", col = "blue", lwd = 2,
           main = paste(res$endpoint, "Analysis"),
           xlab = "Time (months)", ylab = "Survival Probability",
           ylim = c(0, 1))
      lines(time, surv_ctl, col = "red", lwd = 2)
      legend("topright", 
             legend = c("Treatment", "Control"), 
             col = c("blue", "red"), 
             lwd = 2)
      
      # Add HR and p-value
      text(x = max(time) * 0.7, y = 0.2,
           labels = sprintf("HR = %.2f (p = %.3f)", res$hazard_ratio, res$p_value),
           pos = 4)
    })
    
    # Render efficacy results
    output$efficacy_results <- renderPrint({
      res <- results()
      req(res)
      
      cat("Analysis Results:\n")
      cat("-----------------\n")
      cat("Endpoint:", res$endpoint, "\n")
      cat("Hazard Ratio:", round(res$hazard_ratio, 2), "\n")
      cat("P-value:", format(res$p_value, digits = 3), "\n")
      cat("Median (Treatment):", round(res$median_trt, 1), "months\n")
      cat("Median (Control):", round(res$median_ctl, 1), "months\n")
    })
  })
}

# Main application UI
ui <- fluidPage(
  titlePanel("Modular Clinical Trial Application"),
  
  sidebarLayout(
    sidebarPanel(
      patientSelectUI("patients")
    ),
    
    mainPanel(
      tabsetPanel(
        tabPanel("Efficacy", efficacyAnalysisUI("efficacy")),
        tabPanel("Safety", h3("Safety Analysis Module Would Go Here")),
        tabPanel("Patient Data", h3("Patient Data Module Would Go Here"))
      )
    )
  )
)

# Main application server
server <- function(input, output, session) {
  # Initialize patient selection module
  selected_patients <- patientSelectServer("patients", session)
  
  # Initialize efficacy analysis module
  efficacyAnalysisServer("efficacy", selected_patients)
  
  # Safety and patient data modules would be initialized here
}

# Run the application
# shinyApp(ui = ui, server = server)

10.2.4 Deployment Options for Clinical Shiny Apps

Several options exist for deploying Shiny applications in clinical environments:

Code
library(tidyverse)
library(knitr)

# Deployment options for Shiny apps
deployment_options <- tribble(
  ~Option, ~Description, ~Advantages, ~Considerations,
  "shinyapps.io", "Cloud hosting by RStudio", "Easy deployment, scalable", "Data privacy, validation for clinical use",
  "Shiny Server Open Source", "Self-hosted server software", "Free, full control", "IT support required, manual scaling",
  "RStudio Connect", "Commercial publishing platform", "Integrated security, scheduling", "Licensing costs, server maintenance",
  "Kubernetes/Docker", "Containerized deployment", "Scalable, reproducible environment", "Complex setup, DevOps expertise needed",
  "Electron wrapper", "Desktop application packaging", "Offline use, local data", "Separate deployment for updates"
)

# Display deployment options
kable(deployment_options, 
     caption = "Deployment Options for Clinical Shiny Applications")

10.2.5 Best Practices for Clinical Shiny Applications

When developing Shiny applications for clinical research, consider these best practices:

  1. Modularize complex applications: Use Shiny modules to break down functionality
  2. Implement proper error handling: Catch and gracefully handle errors with tryCatch() and req()
  3. Include data validation: Validate inputs and intermediate results
  4. Document calculations: Make statistical calculations transparent to users
  5. Version your applications: Include version information in UI and logs
  6. Secure sensitive data: Never embed sensitive data in app code
  7. Optimize performance: Use reactive programming efficiently
  8. Test thoroughly: Implement automated and manual testing
  9. Manage dependencies: Use renv to ensure consistent package versions
  10. Plan for updates: Establish a process for validation of updates
Code
# Example of robust error handling in Shiny
output$analysis_result <- renderPrint({
  # Validate required inputs
  validate(
    need(input$endpoint != "", "Please select an endpoint"),
    need(length(input$covariates) > 0, "Please select at least one covariate")
  )
  
  # Attempt analysis with error handling
  tryCatch({
    # Perform analysis
    result <- analyze_endpoint(
      data = analysis_data(),
      endpoint = input$endpoint,
      covariates = input$covariates
    )
    
    # Log successful execution
    message(paste("Analysis completed successfully for endpoint:", input$endpoint))
    
    # Return result
    result
  }, error = function(e) {
    # Log error
    message(paste("Error in analysis:", e$message))
    
    # Return user-friendly error message
    cat("An error occurred during the analysis:\n\n")
    cat(e$message, "\n\n")
    cat("Please check your inputs and try again.")
  })
})

10.3 Interactive Dashboards

10.3.1 Introduction to Clinical Dashboards

Dashboards provide a consolidated view of key metrics and analyses, making them ideal for clinical trial monitoring and data exploration. Several R packages support dashboard creation:

Code
library(tidyverse)
library(knitr)

# R dashboard frameworks
dashboard_frameworks <- tribble(
  ~Framework, ~Package, ~Strengths, ~Use_Cases,
  "flexdashboard", "flexdashboard", "Simple, R Markdown-based, static or Shiny-powered", "Trial monitoring, investigator dashboards",
  "shinydashboard", "shinydashboard", "Structured layout, professional appearance", "Interactive exploratory tools, data review",
  "bs4Dash", "bs4Dash", "Modern Bootstrap 4 components, responsive", "Patient-facing applications, mobile-friendly dashboards",
  "semantic.dashboard", "semantic.dashboard", "Clean interface based on Semantic UI", "Safety monitoring, enrollment tracking"
)

# Display dashboard frameworks
kable(dashboard_frameworks, 
     caption = "Dashboard Frameworks for Clinical Applications in R")

10.3.2 Creating a Clinical Trial Monitoring Dashboard

A flexdashboard provides a straightforward way to create a clinical trial monitoring dashboard:

Code
# Content for a flexdashboard R Markdown file
# This would be saved as a .Rmd file
dashboard_rmd <- '---
title: "Clinical Trial Monitoring Dashboard"
output: 
  flexdashboard::flex_dashboard:
    orientation: rows
    vertical_layout: fill
    theme: cosmo
    social: ["twitter", "linkedin"]
    source_code: embed
runtime: shiny
---

11 Enrollment

11.1 Row

11.1.1 Total Enrollment

Code
valueBox(
  value = nrow(trial_data),
  caption = "Total Patients",
  icon = "fa-users",
  color = ifelse(nrow(trial_data) >= 200, "success", "warning")
)

11.1.2 Sites Activated

Code
n_sites <- length(unique(trial_data$site_id))
valueBox(
  value = n_sites,
  caption = "Active Sites",
  icon = "fa-hospital",
  color = "primary"
)

11.1.3 Enrollment Rate

Code
days_active <- as.numeric(difftime(Sys.Date(), min(trial_data$enrollment_date), units = "days"))
rate <- round(nrow(trial_data) / days_active * 7, 1)
valueBox(
  value = paste(rate, "/week"),
  caption = "Enrollment Rate",
  icon = "fa-chart-line",
  color = ifelse(rate >= 10, "success", "warning")
)

11.1.4 Screen Failure Rate

Code
screen_failure_rate <- mean(trial_data$screen_failure) * 100
gauge(
  value = screen_failure_rate,
  min = 0,
  max = 50,
  symbol = "%",
  label = "Screen Failure Rate",
  gaugeSectors(
    success = c(0, 15),
    warning = c(15, 25),
    danger = c(25, 50)
  )
)

11.2 Row

11.2.1 Enrollment Over Time

Code
# Calculate cumulative enrollment
enrollment_trend <- trial_data %>%
  arrange(enrollment_date) %>%
  mutate(cumulative = row_number()) %>%
  select(enrollment_date, cumulative)

# Create interactive plot with plotly
plot_ly(enrollment_trend, x = ~enrollment_date, y = ~cumulative, type = "scatter", mode = "lines+markers") %>%
  layout(
    title = "Cumulative Enrollment",
    xaxis = list(title = "Date"),
    yaxis = list(title = "Patients Enrolled")
  )

11.2.2 Enrollment by Site

Code
# Calculate enrollment by site
site_enrollment <- trial_data %>%
  count(site_id, name = "patients") %>%
  arrange(desc(patients))

# Create interactive bar chart
plot_ly(site_enrollment, x = ~site_id, y = ~patients, type = "bar") %>%
  layout(
    title = "Enrollment by Site",
    xaxis = list(title = "Site ID"),
    yaxis = list(title = "Patients Enrolled")
  )

11.3 Row

11.3.1 Patient Details

Code
# Create interactive data table
DT::renderDataTable({
  trial_data %>%
    select(patient_id, site_id, enrollment_date, age, sex, race) %>%
    arrange(desc(enrollment_date)) %>%
    DT::datatable(
      options = list(
        pageLength = 10,
        scrollX = TRUE,
        searchHighlight = TRUE
      ),
      rownames = FALSE
    )
})

12 Safety Monitoring

12.1 Row

12.1.1 Total AEs

Code
ae_count <- nrow(filter(trial_data, has_ae))
valueBox(
  value = ae_count,
  caption = "Adverse Events",
  icon = "fa-exclamation-triangle",
  color = ifelse(ae_count > 50, "danger", "warning")
)

12.1.2 Serious AEs

Code
sae_count <- nrow(filter(trial_data, has_sae))
valueBox(
  value = sae_count,
  caption = "Serious Adverse Events",
  icon = "fa-ambulance",
  color = ifelse(sae_count > 10, "danger", "warning")
)

12.1.3 Discontinuations

Code
disc_count <- nrow(filter(trial_data, discontinued))
disc_rate <- disc_count / nrow(trial_data) * 100
valueBox(
  value = paste0(disc_count, " (", round(disc_rate, 1), "%)"),
  caption = "Discontinuations",
  icon = "fa-user-slash",
  color = ifelse(disc_rate > 20, "danger", "warning")
)

12.2 Row

12.2.1 AEs by System Organ Class

Code
# In a real dashboard, this would use actual AE data
ae_by_soc <- data.frame(
  soc = c("Gastrointestinal", "Nervous system", "Skin", "Respiratory", "Cardiovascular"),
  count = c(45, 32, 27, 18, 12)
)

plot_ly(ae_by_soc, x = ~soc, y = ~count, type = "bar") %>%
  layout(
    title = "AEs by System Organ Class",
    xaxis = list(title = "System Organ Class"),
    yaxis = list(title = "Number of Events")
  )

12.2.2 AE Rates by Treatment Arm

Code
# Treatment arm comparison (simulated)
ae_by_arm <- data.frame(
  arm = rep(c("Treatment", "Placebo"), each = 5),
  soc = rep(c("Gastrointestinal", "Nervous system", "Skin", "Respiratory", "Cardiovascular"), 2),
  rate = c(15.2, 10.8, 9.1, 6.0, 4.0, 12.1, 9.7, 8.5, 5.8, 4.2)
)

plot_ly(ae_by_arm, x = ~soc, y = ~rate, color = ~arm, type = "bar") %>%
  layout(
    title = "AE Rates by Treatment Arm",
    xaxis = list(title = "System Organ Class"),
    yaxis = list(title = "Rate (%)"),
    barmode = "group"
  )

13 Efficacy

13.1 Row

13.2 Row

13.2.1 Efficacy Analysis

Code
# Reactive analysis based on selections
output$efficacy_plot <- renderPlotly({
  # Trigger on button click
  input$update
  
  # Select endpoint data based on input
  if(input$endpoint == "Primary Endpoint") {
    # Time-to-event data simulation
    time <- 0:24
    surv_trt <- exp(-(time/18)^1.2)
    surv_ctl <- exp(-(time/12)^1.2)
    
    efficacy_data <- data.frame(
      time = rep(time, 2),
      surv = c(surv_trt, surv_ctl),
      arm = rep(c("Treatment", "Control"), each = length(time))
    )
    
    plot_ly(efficacy_data, x = ~time, y = ~surv, color = ~arm, type = "scatter", mode = "lines") %>%
      layout(
        title = paste("Time to Progression -", input$subgroup),
        xaxis = list(title = "Time (months)"),
        yaxis = list(title = "Probability"),
        hovermode = "compare"
      )
  } else if(input$endpoint == "Secondary Endpoint 1") {
    # Response rate data (simulated)
    if(input$subgroup == "Overall") {
      response_data <- data.frame(
        arm = c("Treatment", "Control"),
        rate = c(45, 32),
        lower = c(38, 26),
        upper = c(52, 38)
      )
    } else {
      # Different results by subgroup (simulated)
      response_data <- data.frame(
        arm = c("Treatment", "Control"),
        rate = c(42, 30),
        lower = c(35, 24),
        upper = c(49, 36)
      )
    }
    
    plot_ly(response_data, x = ~arm, y = ~rate, type = "bar",
            error_y = list(array = response_data$upper - response_data$rate,
                          arrayminus = response_data$rate - response_data$lower)) %>%
      layout(
        title = paste("Response Rate (%) -", input$subgroup),
        xaxis = list(title = "Treatment Arm"),
        yaxis = list(title = "Response Rate (%)")
      )
  } else {
    # Change from baseline data (simulated)
    visit_weeks <- c(0, 4, 8, 12, 16, 20, 24)
    treatment_change <- c(0, -5, -10, -15, -18, -20, -22)
    control_change <- c(0, -3, -6, -8, -9, -10, -11)
    
    change_data <- data.frame(
      week = rep(visit_weeks, 2),
      change = c(treatment_change, control_change),
      arm = rep(c("Treatment", "Control"), each = length(visit_weeks))
    )
    
    plot_ly(change_data, x = ~week, y = ~change, color = ~arm, 
            type = "scatter", mode = "lines+markers") %>%
      layout(
        title = paste("Change from Baseline -", input$subgroup),
        xaxis = list(title = "Week"),
        yaxis = list(title = "Change from Baseline")
      )
  }
})

plotlyOutput("efficacy_plot")

13.3 Row

13.3.1 Statistical Summary

Code
# Display statistical results
renderUI({
  # Trigger on button click
  input$update
  
  # Generate results based on inputs
  if(input$endpoint == "Primary Endpoint") {
    # Time-to-event analysis summary
    hr <- round(runif(1, 0.6, 0.8), 2)
    p_value <- round(runif(1, 0.001, 0.05), 4)
    
    HTML(paste0(
      "<h4>Time to Progression Analysis</h4>",
      "<p><strong>Hazard Ratio:</strong> ", hr, " (95% CI: ", round(hr * 0.8, 2), 
      " - ", round(hr * 1.2, 2), ")</p>",
      "<p><strong>P-value:</strong> ", p_value, "</p>",
      "<p><strong>Median (Treatment):</strong> 18.3 months</p>",
      "<p><strong>Median (Control):</strong> 12.1 months</p>"
    ))
  } else if(input$endpoint == "Secondary Endpoint 1") {
    # Response rate analysis
    odds_ratio <- round(runif(1, 1.5, 2.2), 2)
    p_value <- round(runif(1, 0.001, 0.05), 4)
    
    HTML(paste0(
      "<h4>Response Rate Analysis</h4>",
      "<p><strong>Treatment:</strong> 45% (95% CI: 38% - 52%)</p>",
      "<p><strong>Control:</strong> 32% (95% CI: 26% - 38%)</p>",
      "<p><strong>Odds Ratio:</strong> ", odds_ratio, " (95% CI: ", round(odds_ratio * 0.8, 2), 
      " - ", round(odds_ratio * 1.2, 2), ")</p>",
      "<p><strong>P-value:</strong> ", p_value, "</p>"
    ))
  } else {
    # Change from baseline analysis
    diff <- round(runif(1, -8, -12), 1)
    p_value <- round(runif(1, 0.001, 0.05), 4)
    
    HTML(paste0(
      "<h4>Change from Baseline Analysis</h4>",
      "<p><strong>LS Mean Difference at Week 24:</strong> ", diff, " (95% CI: ", round(diff - 2, 1), 
      " - ", round(diff + 2, 1), ")</p>",
      "<p><strong>P-value:</strong> ", p_value, "</p>",
      "<p><strong>Treatment:</strong> -22.0 points</p>",
      "<p><strong>Control:</strong> -11.0 points</p>"
    ))
  }
})


### Creating Dashboards with shinydashboard

For more structured layouts, shinydashboard provides a robust framework:

::: {.cell}

```{.r .cell-code}
library(shiny)
library(shinydashboard)

# UI definition
ui <- dashboardPage(
  # Dashboard header
  dashboardHeader(
    title = "Clinical Data Explorer",
    dropdownMenu(type = "notifications",
                notificationItem(
                  text = "5 new AEs reported",
                  icon = icon("exclamation-triangle"),
                  status = "warning"
                ),
                notificationItem(
                  text = "Enrollment target reached",
                  icon = icon("users"),
                  status = "success"
                ))
  ),
  
  # Dashboard sidebar
  dashboardSidebar(
    sidebarMenu(
      menuItem("Dashboard", tabName = "dashboard", icon = icon("dashboard")),
      menuItem("Patient Data", tabName = "patients", icon = icon("user")),
      menuItem("Safety", tabName = "safety", icon = icon("heartbeat")),
      menuItem("Efficacy", tabName = "efficacy", icon = icon("chart-line")),
      menuItem("Reports", tabName = "reports", icon = icon("file-pdf"))
    ),
    
    # Study selector
    selectInput("study_id", "Study:", choices = c("Study A", "Study B")),
    
    # Date range filter
    dateRangeInput("date_range", "Date Range:",
                  start = "2022-01-01",
                  end = Sys.Date())
  ),
  
  # Dashboard body
  dashboardBody(
    tabItems(
      # Dashboard tab
      tabItem(tabName = "dashboard",
              fluidRow(
                # Info boxes
                infoBox("Patients", "250", icon = icon("users"), fill = TRUE),
                infoBox("Sites", "12", icon = icon("hospital"), fill = TRUE),
                infoBox("AEs", "45", icon = icon("exclamation-triangle"), 
                       color = "yellow", fill = TRUE)
              ),
              fluidRow(
                # Value boxes
                valueBox(95, "Enrollment %", icon = icon("chart-line"), 
                        color = "green"),
                valueBox("15.2", "AE Rate", icon = icon("heartbeat"), 
                        color = "yellow"),
                valueBox("0.72", "Hazard Ratio", icon = icon("chart-bar"), 
                        color = "blue")
              ),
              fluidRow(
                # Summary charts
                box(title = "Enrollment Trend", status = "primary", solidHeader = TRUE,
                   plotOutput("enrollment_plot")),
                box(title = "Safety Summary", status = "warning", solidHeader = TRUE,
                   plotOutput("safety_plot"))
              )
      ),
      
      # Patient tab
      tabItem(tabName = "patients",
              fluidRow(
                box(title = "Patient Listing", status = "primary", solidHeader = TRUE,
                   width = 12,
                   DT::dataTableOutput("patient_table"))
              ),
              fluidRow(
                box(title = "Demographics", status = "info", solidHeader = TRUE,
                   plotOutput("demographics_plot")),
                box(title = "Disposition", status = "info", solidHeader = TRUE,
                   plotOutput("disposition_plot"))
              )
      ),
      
      # Safety tab
      tabItem(tabName = "safety",
              fluidRow(
                box(title = "Filters", width = 12,
                   column(4, selectInput("ae_soc", "System Organ Class:",
                                        choices = c("All", "Gastrointestinal", "Cardiovascular"))),
                   column(4, selectInput("ae_severity", "Severity:",
                                        choices = c("All", "Mild", "Moderate", "Severe"))),
                   column(4, checkboxInput("ae_serious", "Serious AEs Only", FALSE))
                )
              ),
              fluidRow(
                box(title = "Adverse Events", status = "warning", solidHeader = TRUE,
                   width = 12,
                   DT::dataTableOutput("ae_table"))
              ),
              fluidRow(
                box(title = "AE Counts by SOC", status = "warning", solidHeader = TRUE,
                   plotOutput("ae_soc_plot")),
                box(title = "AE Timeline", status = "warning", solidHeader = TRUE,
                   plotOutput("ae_timeline_plot"))
              )
      ),
      
      # Efficacy tab
      tabItem(tabName = "efficacy",
              fluidRow(
                box(title = "Analysis Parameters", width = 12,
                   column(4, selectInput("efficacy_endpoint", "Endpoint:",
                                        choices = c("Primary", "Secondary 1", "Secondary 2"))),
                   column(4, selectInput("efficacy_population", "Population:",
                                        choices = c("ITT", "PP", "Safety"))),
                   column(4, actionButton("run_analysis", "Run Analysis"))
                )
              ),
              fluidRow(
                box(title = "Efficacy Results", status = "primary", solidHeader = TRUE,
                   width = 12,
                   plotOutput("efficacy_plot"))
              ),
              fluidRow(
                box(title = "Statistical Summary", status = "primary", solidHeader = TRUE,
                   verbatimTextOutput("efficacy_summary")),
                box(title = "Subgroup Analysis", status = "info", solidHeader = TRUE,
                   plotOutput("subgroup_plot"))
              )
      ),
      
      # Reports tab
      tabItem(tabName = "reports",
              fluidRow(
                box(title = "Available Reports", width = 12,
                   DT::dataTableOutput("report_table"))
              ),
              fluidRow(
                box(title = "Report Preview", width = 12,
                   uiOutput("report_preview"))
              )
      )
    )
  )
)

# Server logic (simplified)
server <- function(input, output, session) {
  # Example output for enrollment plot
  output$enrollment_plot <- renderPlot({
    # Simulated data
    dates <- seq(as.Date("2022-01-01"), Sys.Date(), by = "week")
    cumulative <- cumsum(rpois(length(dates), lambda = 5))
    
    df <- data.frame(date = dates, patients = cumulative)
    
    ggplot(df, aes(x = date, y = patients)) +
      geom_line(color = "blue", size = 1) +
      geom_point(color = "blue") +
      labs(title = "Cumulative Enrollment", x = "Date", y = "Patients") +
      theme_minimal()
  })
  
  # Additional outputs would be defined here
}

# Run the application (commented out)
# shinyApp(ui, server)

:::

13.3.2 Dashboard Design Best Practices

When designing dashboards for clinical research, consider these principles:

  1. Focus on key metrics: Highlight the most important information using infoBoxes and valueBoxes
  2. Layer information: Start with summary metrics, then provide deeper details on demand
  3. Consistent visual language: Use color consistently (e.g., red for safety concerns, green for targets met)
  4. Appropriate visualizations: Choose chart types that best represent the data
  5. Performance considerations: Optimize load times for large datasets
  6. Mobile compatibility: Test dashboard responsiveness on different devices
  7. Clear labeling: Ensure all metrics and visualizations are clearly labeled
  8. Interactive filtering: Allow users to filter data to focus on specific areas
  9. Context and benchmarks: Include target lines or comparison values where appropriate
  10. Documentation: Include methodology notes and data definitions
Code
# Example of a well-structured clinical dashboard layout
dashboard_structure <- list(
  "Overview" = list(
    "Key Performance Indicators" = c("Enrollment %", "Retention %", "Data Quality %"),
    "Timeline" = c("Study progress", "Key milestones"),
    "Alerts" = c("Enrollment delays", "Safety signals", "Data issues")
  ),
  "Enrollment" = list(
    "Current Status" = c("Total enrolled", "By site", "By demographic"),
    "Projections" = c("Enrollment curves", "Site activation timeline"),
    "Screen Failures" = c("Reasons", "Rates by site")
  ),
  "Safety" = list(
    "Adverse Events" = c("Summary", "By SOC", "By severity"),
    "Serious AEs" = c("Listing", "Analysis by causality"),
    "Laboratory Values" = c("Out of range values", "Trends over time")
  ),
  "Efficacy" = list(
    "Primary Endpoint" = c("Interim analysis", "Trends over time"),
    "Secondary Endpoints" = c("Results summary", "Statistical analysis"),
    "Subgroups" = c("Forest plots", "Interaction tests")
  ),
  "Data Quality" = list(
    "Query Status" = c("Open queries", "Resolution time"),
    "Missing Data" = c("By CRF page", "By site"),
    "Protocol Deviations" = c("Summary", "By category")
  )
)

13.4 Interactive Reports with R Markdown and Quarto

13.4.1 Introduction to Interactive Reports

Interactive reports combine narrative text with interactive elements, allowing readers to explore data and analyses. R Markdown and Quarto provide powerful frameworks for creating these reports:

Code
library(tidyverse)
library(knitr)

# Interactive report frameworks
report_frameworks <- tribble(
  ~Framework, ~Format, ~Interactivity_Level, ~Use_Cases,
  "R Markdown", "HTML, PDF, Word", "Low to High", "Regulatory submissions, clinical study reports",
  "Quarto", "HTML, PDF, Word, Revealjs", "Low to High", "Enhanced study reports, presentations",
  "Bookdown", "HTML, PDF, EPUB", "Medium", "Clinical protocols, investigator manuals",
  "Distill", "HTML", "Medium", "Publication-ready clinical research articles"
)

# Display report frameworks
kable(report_frameworks, 
     caption = "Interactive Report Frameworks for Clinical Research")

13.4.2 Interactive Elements for Clinical Reports

Several packages provide interactive elements that can be embedded in R Markdown and Quarto documents:

Code
# Interactive visualization packages
interactive_viz_packages <- tribble(
  ~Package, ~Specialization, ~Clinical_Uses,
  "plotly", "General interactive plots", "Interactive survival curves, forest plots, safety plots",
  "DT", "Interactive tables", "Patient listings, adverse event tables, concomitant medications",
  "leaflet", "Interactive maps", "Site locations, patient geography, regional recruitment",
  "dygraphs", "Time series", "Laboratory trends, vital signs, enrollment over time",
  "visNetwork", "Network visualization", "Adverse event relationships, comorbidity networks"
)

# Display interactive visualization packages
kable(interactive_viz_packages, 
     caption = "Interactive Visualization Packages for Clinical Reports")

13.4.3 Creating an Interactive Clinical Study Report

Here’s a simplified example of key interactive elements for a clinical study report:

13.4.3.1 Interactive Survival Curves with plotly

Code
# Simulate time-to-event data
set.seed(123)
time_data <- data.frame(
  patient_id = 1:200,
  treatment = rep(c("Treatment", "Control"), each = 100),
  time = c(rweibull(100, shape = 1.2, scale = 15), 
          rweibull(100, shape = 1.2, scale = 10)),
  event = c(rbinom(100, 1, 0.7), rbinom(100, 1, 0.8))
)

# Truncate at 24 weeks and handle censoring
time_data$time <- pmin(time_data$time, 24)
time_data$event[time_data$time >= 24] <- 0

# Fit survival curves
library(survival)
surv_fit <- survfit(Surv(time, event) ~ treatment, data = time_data)

# Create interactive survival plot
library(plotly)
plot_data <- data.frame(
  time = c(0, surv_fit$time),
  surv = c(1, surv_fit$surv),
  group = c("Treatment", rep(c("Treatment", "Control"), 
                           c(length(surv_fit$time[surv_fit$strata == "treatment=Treatment"]),
                             length(surv_fit$time[surv_fit$strata == "treatment=Control"]))))
)

plot_ly(plot_data, x = ~time, y = ~surv, color = ~group, 
       colors = c("blue", "red"),
       type = "scatter", mode = "lines", line = list(width = 2)) %>%
  layout(
    title = "Kaplan-Meier Survival Curve",
    xaxis = list(title = "Time (weeks)"),
    yaxis = list(title = "Survival Probability"),
    annotations = list(
      list(
        x = 18, y = 0.2,
        text = "HR = 0.72 (95% CI: 0.58, 0.89)<br>p = 0.003",
        showarrow = FALSE
      )
    )
  )

13.4.3.2 Interactive Data Tables with DT

Code
# Create an adverse event table
library(DT)
ae_data <- data.frame(
  preferred_term = c("Headache", "Nausea", "Diarrhea", "Fatigue", "Dizziness", 
                    "Upper respiratory tract infection", "Insomnia", "Back pain"),
  treatment_n = c(24, 20, 18, 15, 12, 12, 10, 9),
  treatment_pct = c(16.2, 13.5, 12.2, 10.1, 8.1, 8.1, 6.8, 6.1),
  control_n = c(22, 15, 12, 14, 10, 13, 8, 11),
  control_pct = c(14.8, 10.1, 8.1, 9.4, 6.7, 8.7, 5.4, 7.4)
)

datatable(
  ae_data,
  colnames = c("Preferred Term", "N", "%", "N", "%"),
  caption = "Adverse Events by Treatment Group",
  options = list(
    columnDefs = list(list(className = "dt-center", targets = 1:4)),
    dom = "Bfrtip",
    buttons = c("copy", "csv", "excel")
  )
) %>% 
  formatStyle(
    columns = c("treatment_pct", "control_pct"),
    background = styleColorBar(c(0, 20), "lightblue"),
    backgroundSize = "98% 88%",
    backgroundRepeat = "no-repeat",
    backgroundPosition = "center"
  ) %>%
  formatPercentage(c("treatment_pct", "control_pct"), 1)

13.4.3.3 Interactive Forest Plots for Subgroup Analysis

Code
# Create subgroup analysis data
subgroup_data <- data.frame(
  subgroup = c("Overall", "Age < 65", "Age ≥ 65", "Male", "Female"),
  n = c(200, 85, 115, 120, 80),
  hr = c(0.72, 0.68, 0.75, 0.70, 0.75),
  lower = c(0.58, 0.49, 0.56, 0.52, 0.51),
  upper = c(0.89, 0.94, 1.01, 0.94, 1.10),
  p_value = c(0.003, 0.021, 0.056, 0.018, 0.142)
)

# Create horizontal forest plot
p <- plot_ly(subgroup_data) %>%
  # Add points for hazard ratios
  add_trace(
    x = ~hr,
    y = ~subgroup,
    type = "scatter",
    mode = "markers",
    marker = list(size = 10, symbol = "square"),
    name = "Hazard Ratio",
    error_x = list(
      type = "data",
      array = ~upper - hr,
      arrayminus = ~hr - lower,
      thickness = 1.5,
      width = 5
    ),
    hoverinfo = "text",
    text = ~paste("Subgroup:", subgroup, 
                 "<br>HR:", hr, 
                 "<br>95% CI:", lower, "-", upper,
                 "<br>p-value:", p_value)
  ) %>%
  # Add vertical reference line at HR = 1
  layout(
    title = "Subgroup Analysis",
    xaxis = list(
      title = "Hazard Ratio (95% CI)",
      range = c(0.4, 1.2),
      zeroline = FALSE
    ),
    yaxis = list(
      title = "",
      autorange = "reversed"
    ),
    shapes = list(
      list(
        type = "line", 
        x0 = 1, x1 = 1, 
        y0 = -1, y1 = 6,
        line = list(color = "red", dash = "dash")
      )
    ),
    annotations = list(
      list(
        x = 1.15,
        y = 0.5,
        text = "Favors Control",
        showarrow = FALSE
      ),
      list(
        x = 0.45,
        y = 0.5,
        text = "Favors Treatment",
        showarrow = FALSE
      )
    )
  )

p

13.4.4 Interactive Presentations with Quarto

Quarto extends R Markdown’s capabilities with enhanced interactive presentations. Key features for clinical presentations include:

  1. Vertical and horizontal navigation for organizing content hierarchically
  2. Interactive code execution for live data exploration
  3. Chalkboard and annotation tools for meeting discussions
  4. Multiple output formats from a single source document
  5. Advanced layout options for complex clinical data displays
Code
# Basic structure for a Quarto clinical presentation
quarto_structure <- '
---
title: "Clinical Trial Results"
format:
  revealjs:
    theme: simple
    navigation-mode: vertical
    chalkboard: true
---

## Study Design
- Key design elements
- Interactive study diagram

## Patient Population
- Demographics table
- Interactive patient flow diagram

## Efficacy Results
- Primary endpoint visualization
- Secondary endpoints with interactive filtering

## Safety Results
- Interactive adverse event explorer
- Laboratory trends visualization

## Conclusions & Next Steps
'

13.4.5 Best Practices for Interactive Clinical Reports

When creating interactive reports for clinical research, consider these best practices:

  1. Provide static alternatives: Include static versions of key visualizations for non-HTML outputs
  2. Ensure reproducibility: Set random seeds and document data sources
  3. Optimize performance: Consider file size and loading times for large datasets
  4. Maintain regulatory compliance: Document version information and validation status
  5. Progressive disclosure: Start with high-level summaries, then enable exploration of details
  6. Consistent interaction patterns: Use similar interaction models across visualizations
  7. Accessibility: Ensure interactive elements are accessible to all users
  8. Version control: Track changes to interactive reports in version control systems
  9. Testing across browsers: Verify functionality in different environments
  10. Documentation: Include clear instructions for interactive features

13.5 Regulatory Considerations for Interactive Tools

The creation and deployment of interactive tools in clinical research contexts introduces specific regulatory considerations:

Code
library(tidyverse)
library(knitr)

# Regulatory considerations for interactive tools
regulatory_considerations <- tribble(
  ~Consideration, ~Description, ~Implementation,
  "Validation", "Ensuring interactive applications function as intended", "Comprehensive testing, test cases that cover all interactive elements",
  "Audit trails", "Tracking user interactions and decisions", "Logging of user actions and parameter selections",
  "Version control", "Managing application versions", "Application versioning, clear communication of version in UI",
  "Data integrity", "Ensuring calculations and visualizations are accurate", "Backend validation, comparison with non-interactive results",
  "User access control", "Limiting access to authorized users", "Authentication systems, role-based access controls"
)

# Display regulatory considerations
kable(regulatory_considerations, 
     caption = "Regulatory Considerations for Interactive Clinical Tools")

When developing interactive tools for clinical research, consider the following best practices:

  1. Document validation approach: Maintain clear documentation of how the interactive application was validated
  2. Include version information: Display version information prominently in the application
  3. Implement audit logging: Track user actions, especially for applications that support decision-making
  4. Limit customization in regulatory contexts: For applications supporting regulatory decisions, limit user customization to prevent misinterpretation
  5. Provide calculation transparency: Make the underlying calculations transparent to users

13.5.1 Example: Implementing Audit Trails in Shiny

Code
# Function to set up audit logging for a Shiny application
setup_audit_logging <- function(app_name, log_dir = "logs") {
  # Create log directory if it doesn't exist
  if (!dir.exists(log_dir)) {
    dir.create(log_dir, recursive = TRUE)
  }
  
  # Create log file name with date
  log_file <- file.path(log_dir, paste0(app_name, "_", format(Sys.Date(), "%Y%m%d"), ".log"))
  
  # Function to log events
  log_event <- function(user_id, event_type, event_details) {
    timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")
    log_entry <- data.frame(
      timestamp = timestamp,
      user_id = user_id,
      app_name = app_name,
      event_type = event_type,
      event_details = jsonlite::toJSON(event_details),
      ip_address = Sys.info()[["nodename"]], # In real app, would use client IP
      app_version = "1.0.0"  # Would come from app config
    )
    
    # Append to log file
    write.table(log_entry, log_file, append = TRUE, row.names = FALSE, 
               col.names = !file.exists(log_file), sep = ",", quote = TRUE)
    
    # Return invisible for function chaining
    invisible(log_entry)
  }
  
  # Return logging function
  return(log_event)
}

# Usage in a Shiny app (conceptual)
# In server.R:
# logger <- setup_audit_logging("clinical_explorer")
# 
# # In server function:
# observeEvent(input$analyze_button, {
#   logger(
#     user_id = session$user,
#     event_type = "analysis_run",
#     event_details = list(
#       analysis_type = input$analysis_type,
#       parameters = list(
#         endpoint = input$endpoint,
#         subgroup = input$subgroup,
#         model = input$model
#       )
#     )
#   )
#   
#   # Perform analysis...
# })

13.6 Future Directions in Interactive Clinical Tools

The field of interactive clinical research tools continues to evolve rapidly. Several emerging trends are worth monitoring:

  1. Real-time clinical trial monitoring: Interactive dashboards that update as trial data is collected, supporting adaptive trial designs
  2. Patient-facing applications: Tools that engage patients directly in the research process
  3. Integration with electronic health records: Applications that combine clinical trial data with real-world data
  4. Machine learning-enhanced exploration: Interactive tools that leverage ML to suggest potentially interesting patterns
  5. Cross-platform deployment: Applications that work seamlessly across web, mobile, and desktop environments

As these trends develop, maintaining a balance between innovation and regulatory compliance will remain essential. The approaches outlined in this chapter provide a foundation that can be extended to incorporate these emerging capabilities.

13.7 Conclusion

Interactive tools represent a powerful approach for enhancing the impact and accessibility of clinical research data. By implementing these tools using the R frameworks discussed in this chapter—Shiny, dashboards, and interactive reports—clinical researchers can create engaging, informative applications while maintaining the reproducibility and rigor required in clinical contexts.

The combination of interactive elements with the statistical rigor, reproducible workflows, and visualization techniques discussed throughout this book creates a comprehensive approach to modern clinical data analysis with R.

13.8 References