# app.R # ---- Robust package setup ---- options(repos = c(CRAN = "https://packagemanager.posit.co/cran/latest")) required_packages <- c( "shiny", "bslib", "shinyWidgets", "dplyr", "ggplot2", "plotly", "DT", "htmlwidgets", "pharmaverseadamjnj" ) installed_packages <- rownames(installed.packages()) missing_packages <- setdiff(required_packages, installed_packages) if (length(missing_packages) > 0) { install.packages( missing_packages, type = "binary", dependencies = TRUE ) } # ---- Load packages only after all checks/installations are complete ---- library(shiny) library(bslib) library(shinyWidgets) library(dplyr) library(ggplot2) library(plotly) library(DT) library(htmlwidgets) library(pharmaverseadamjnj) # ---- Data preparation ---- adsl <- pharmaverseadamjnj::adsl adsl2 <- adsl %>% select(USUBJID, TRT01P, TRTDURD, WEIGHTBL, HEIGHTBL) %>% filter( !is.na(TRT01P), !is.na(TRTDURD), !is.na(WEIGHTBL), !is.na(HEIGHTBL) ) adsl3 <- adsl2 %>% mutate_if(is.character, as.factor) tf_colors <- c( "Placebo" = "#0072B2", "Xanomeline High Dose" = "#D55E00", "Xanomeline Low Dose" = "#CC79A7" ) duration_range <- range(adsl3$TRTDURD, na.rm = TRUE) weight_range <- range(adsl3$WEIGHTBL, na.rm = TRUE) height_range <- range(adsl3$HEIGHTBL, na.rm = TRUE) # ---- Reusable plot functions ---- make_duration_plot <- function(data) { ggplot( data = data, aes(x = TRT01P, y = TRTDURD, fill = TRT01P) ) + geom_boxplot() + scale_x_discrete(name = "Treatment Group") + scale_y_continuous( limits = c(0, 250), breaks = seq(0, 250, 50), expand = c(0.05, 0.05), name = "Treatment Duration (Days)" ) + scale_fill_manual( values = tf_colors, name = "Treatment Group" ) + theme_bw() + theme( legend.position = "top", plot.title = element_text(face = "bold"), axis.title = element_text(face = "bold") ) } make_height_weight_plot <- function(data) { ggplot( data = data, aes( x = HEIGHTBL, y = WEIGHTBL, shape = TRT01P, colour = TRT01P ) ) + geom_point(size = 2.3, alpha = 0.85) + scale_x_continuous( limits = c(0, 150), breaks = seq(0, 150, 10), name = "Baseline Height (cm)" ) + scale_y_continuous( limits = c(0, 150), breaks = seq(0, 150, 10), name = "Baseline Weight (kg)" ) + scale_shape_manual( values = c(16, 17, 15), name = "Treatment Group" ) + scale_colour_manual( values = tf_colors, name = "Treatment Group" ) + theme_bw() + theme( legend.position = "top", plot.title = element_text(face = "bold"), axis.title = element_text(face = "bold") ) } make_subject_table <- function(data) { data %>% rename( "Unique Subject Identifier" = USUBJID, "Treatment Group" = TRT01P, "Total Treatment Duration (Days)" = TRTDURD, "Baseline Weight (kg)" = WEIGHTBL, "Baseline Height (cm)" = HEIGHTBL ) } # ---- UI ---- ui <- page_sidebar( title = "Clinical Subject-Level Explorer", theme = bs_theme( bootswatch = "cosmo", base_font = font_google("Source Sans 3"), heading_font = font_google("Source Sans 3") ), sidebar = sidebar( title = "Dashboard Controls", width = 340, div( class = "text-muted", "Filter the subject-level dataset and download the current filtered outputs." ), hr(), h5("Subject Selection"), pickerInput( inputId = "treatment_filter", label = "Treatment Group", choices = levels(adsl3$TRT01P), selected = levels(adsl3$TRT01P), multiple = TRUE, options = pickerOptions( actionsBox = TRUE, liveSearch = TRUE, selectedTextFormat = "values", noneSelectedText = "No treatment groups selected", selectAllText = "Select All", deselectAllText = "Deselect All", iconBase = "glyphicon", tickIcon = "glyphicon-ok" ) ), pickerInput( inputId = "subject_filter", label = "Subject", choices = levels(adsl3$USUBJID), selected = levels(adsl3$USUBJID), multiple = TRUE, options = pickerOptions( actionsBox = TRUE, liveSearch = TRUE, selectedTextFormat = "values", noneSelectedText = "No subjects selected", selectAllText = "Select All", deselectAllText = "Deselect All", iconBase = "glyphicon", tickIcon = "glyphicon-ok" ) ), hr(), h5("Clinical Measures"), sliderInput( inputId = "duration_filter", label = "Total Treatment Duration (Days)", min = floor(duration_range[1]), max = ceiling(duration_range[2]), value = c(floor(duration_range[1]), ceiling(duration_range[2])), step = 1 ), sliderInput( inputId = "weight_filter", label = "Baseline Weight (kg)", min = floor(weight_range[1]), max = ceiling(weight_range[2]), value = c(floor(weight_range[1]), ceiling(weight_range[2])), step = 1 ), sliderInput( inputId = "height_filter", label = "Baseline Height (cm)", min = floor(height_range[1]), max = ceiling(height_range[2]), value = c(floor(height_range[1]), ceiling(height_range[2])), step = 1 ), hr(), h5("Downloads"), downloadButton( outputId = "download_duration_plot", label = "Treatment Duration PNG", class = "btn-primary btn-sm w-100" ), br(), br(), downloadButton( outputId = "download_height_weight_plot", label = "Height vs Weight PNG", class = "btn-primary btn-sm w-100" ), br(), br(), downloadButton( outputId = "download_subject_table", label = "Filtered Table CSV", class = "btn-secondary btn-sm w-100" ) ), div( class = "mb-4", h2("Clinical Subject-Level Explorer"), p( class = "text-muted", "Interactive review of treatment duration and baseline subject characteristics by treatment group." ) ), layout_columns( col_widths = c(6, 6), card( full_screen = TRUE, card_header("Treatment Duration by Treatment Group"), plotlyOutput("duration_plot", height = "420px") ), card( full_screen = TRUE, card_header("Baseline Weight vs Baseline Height"), plotlyOutput("height_weight_plot", height = "420px") ) ), br(), card( full_screen = TRUE, card_header("Filtered Subject-Level Data"), DTOutput("subject_table") ) ) # ---- Server ---- server <- function(input, output, session) { filtered_data <- reactive({ req(input$treatment_filter) req(input$subject_filter) adsl3 %>% filter( TRT01P %in% input$treatment_filter, USUBJID %in% input$subject_filter, TRTDURD >= input$duration_filter[1], TRTDURD <= input$duration_filter[2], WEIGHTBL >= input$weight_filter[1], WEIGHTBL <= input$weight_filter[2], HEIGHTBL >= input$height_filter[1], HEIGHTBL <= input$height_filter[2] ) }) output$duration_plot <- renderPlotly({ f1 <- make_duration_plot(filtered_data()) ggplotly(f1) %>% layout( legend = list( orientation = "h", x = 0.5, xanchor = "center", y = 1.02, yanchor = "bottom" ), margin = list(t = 45) ) }) output$height_weight_plot <- renderPlotly({ f2 <- make_height_weight_plot(filtered_data()) ggplotly(f2) %>% layout( legend = list( orientation = "h", x = 0.5, xanchor = "center", y = 1.02, yanchor = "bottom" ), margin = list(t = 45) ) }) output$subject_table <- renderDT({ datatable( make_subject_table(filtered_data()), extensions = c("ColReorder"), rownames = FALSE, filter = "top", class = "stripe hover compact", fillContainer = FALSE, autoHideNavigation = FALSE, options = list( colReorder = TRUE, bPaginate = TRUE, paging = TRUE, lengthChange = TRUE, searching = TRUE, info = TRUE, pageLength = 5, lengthMenu = c(5, 10, 12, 20, 50, 100), dom = '<"top"lf>rt<"bottom"ip>', language = list( info = "Showing _START_ to _END_ of _TOTAL_ entries" ) ) ) }) output$download_duration_plot <- downloadHandler( filename = function() { paste0("treatment_duration_plot_", Sys.Date(), ".png") }, content = function(file) { ggsave( filename = file, plot = make_duration_plot(filtered_data()), device = "png", width = 8, height = 6, dpi = 300 ) } ) output$download_height_weight_plot <- downloadHandler( filename = function() { paste0("height_weight_plot_", Sys.Date(), ".png") }, content = function(file) { ggsave( filename = file, plot = make_height_weight_plot(filtered_data()), device = "png", width = 8, height = 6, dpi = 300 ) } ) output$download_subject_table <- downloadHandler( filename = function() { paste0("filtered_subject_level_data_", Sys.Date(), ".csv") }, content = function(file) { write.csv( make_subject_table(filtered_data()), file, row.names = FALSE ) } ) } # ---- Run app ---- shinyApp(ui = ui, server = server)