diff --git a/inst/shiny/app.R b/inst/shiny/app.R index 2ef02386a..1c26d4bc4 100644 --- a/inst/shiny/app.R +++ b/inst/shiny/app.R @@ -152,7 +152,8 @@ server <- function(input, output, session) { tab_nca_outputs <- tab_nca_server( "nca", tab_data_outputs$pknca_data, - tab_data_outputs$extra_group_vars + tab_data_outputs$extra_group_vars, + tab_data_outputs$settings_override ) # TLG diff --git a/inst/shiny/modules/tab_data.R b/inst/shiny/modules/tab_data.R index 008558c68..f054ad787 100644 --- a/inst/shiny/modules/tab_data.R +++ b/inst/shiny/modules/tab_data.R @@ -120,12 +120,12 @@ tab_data_server <- function(id) { updateTabsetPanel(session, "data_navset", selected = step_labels[idx - 1]) }) #' Load raw ADNCA data - adnca_raw <- data_upload_server("raw_data") + uploaded_data <- data_upload_server("raw_data") # Call the column mapping module column_mapping <- data_mapping_server( id = "column_mapping", - adnca_data = adnca_raw, + adnca_data = uploaded_data$adnca_raw, trigger = trigger_mapping_submit ) #' Reactive value for the processed dataset @@ -223,7 +223,8 @@ tab_data_server <- function(id) { list( pknca_data = pknca_data, - extra_group_vars = extra_group_vars + extra_group_vars = extra_group_vars, + settings_override = uploaded_data$settings_override ) }) } diff --git a/inst/shiny/modules/tab_data/data_upload.R b/inst/shiny/modules/tab_data/data_upload.R index ca13c9fb7..ed7597876 100644 --- a/inst/shiny/modules/tab_data/data_upload.R +++ b/inst/shiny/modules/tab_data/data_upload.R @@ -15,7 +15,7 @@ data_upload_ui <- function(id) { div( class = "upload-container", id = ns("upload_container"), - p("Upload your PK dataset."), + p("Upload your PK dataset and Settings file (optional)."), fileInput( ns("data_upload"), width = "50%", @@ -41,6 +41,7 @@ data_upload_server <- function(id) { #' Display file loading error if any issues arise file_loading_error <- reactiveVal(NULL) + settings_override <- reactiveVal(NULL) # Store loaded settings output$file_loading_message <- renderUI({ if (is.null(file_loading_error())) { @@ -71,27 +72,65 @@ data_upload_server <- function(id) { filenames <- input$data_upload$name } - # Iterate over files: Read and classify + # Iterate over files: Try reading as Data, then as Settings read_results <- purrr::map2(paths, filenames, function(path, name) { tryCatch({ # Attempt to read data <- read_pk(path) list(status = "success", data = data, name = name, type = "data") - }, error = function(e) { - # TODO: @Jana, check if settings file is loaded and then create settings override (719) - list(status = "error", message = e$message, name = name) + }, error = function(e_pk) { + # If read_pk fails + # check if settings file is loaded and then create settings override + tryCatch({ + # check if error aligns with what we expect for setttings file + if (conditionMessage(e_pk) != "Invalid data format. + Data frame was expected, but received list.") { + return(list(status = "error", msg = conditionMessage(e_pk), name = name)) + } + + obj <- readRDS(path) + # Check for settings + is_settings <- is.list(obj) && "settings" %in% names(obj) + + if (!is_settings) { + stop(conditionMessage(e_pk)) + } + + list(status = "success", type = "settings", content = obj, name = name) + }, error = function(e_rds) { + list(status = "error", msg = conditionMessage(e_pk), name = name) + }) }) }) # Process results - successful_loads <- purrr::keep(read_results, ~ .x$status == "success") + successful_loads <- purrr::keep(read_results, \(x) x$status == "success") errors <- purrr::keep(read_results, \(x) x$status == "error") %>% - purrr::map(\(x) paste0(x$name, ": ", x$message)) + purrr::map(\(x) paste0(x$name, ": ", x$msg)) + + # Extract and apply settings if any found + found_settings <- purrr::keep(successful_loads, \(x) x$type == "settings") + + if (length(found_settings) > 1) { + # Error: Too many settings files + errors <- append(errors, "Error: Multiple settings files detected. + Please upload only one settings file.") + # Do not apply any settings if ambiguous + settings_override(NULL) + + } else if (length(found_settings) == 1) { + # Success: Single settings file + latest <- found_settings[[1]] + settings_override(latest$content) + log_success("Settings successfully loaded from ", latest$name) + showNotification(paste("Settings successfully loaded."), type = "message") + } loaded_data <- DUMMY_DATA + found_data <- purrr::keep(successful_loads, \(x) x$type == "data") # Handle Errors - if (length(successful_loads) > 0) { + if (length(found_data) > 0) { tryCatch({ loaded_data <- successful_loads %>% purrr::map("data") %>% @@ -127,6 +166,9 @@ data_upload_server <- function(id) { style = list(fontSize = "0.75em") ) - raw_data + list( + adnca_raw = raw_data, + settings_override = settings_override + ) }) } diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index 299d58099..1e9646fd9 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -62,7 +62,7 @@ tab_nca_ui <- function(id) { ) } -tab_nca_server <- function(id, pknca_data, extra_group_vars) { +tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override) { moduleServer(id, function(input, output, session) { ns <- session$ns @@ -74,7 +74,7 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars) { adnca_data <- reactive(pknca_data()$conc$data) # #' NCA Setup module - nca_setup <- setup_server("nca_setup", adnca_data, pknca_data) + nca_setup <- setup_server("nca_setup", adnca_data, pknca_data, settings_override) processed_pknca_data <- nca_setup$processed_pknca_data settings <- nca_setup$settings diff --git a/inst/shiny/modules/tab_nca/setup.R b/inst/shiny/modules/tab_nca/setup.R index 7f4664519..6b8c78e12 100644 --- a/inst/shiny/modules/tab_nca/setup.R +++ b/inst/shiny/modules/tab_nca/setup.R @@ -21,20 +21,9 @@ setup_ui <- function(id) { nav_panel( "Settings", fluidRow( - column(6, - fileInput( - ns("settings_upload"), - width = "100%", - label = "Upload settings", - buttonLabel = list(icon("folder"), "Browse"), - accept = ".rds" - ) - ), - column(6, - downloadButton( - ns("settings_download"), - label = "Download settings" - ) + downloadButton( + ns("settings_download"), + label = "Download settings" ) ), fluidRow(units_table_ui(ns("units_table"))), @@ -53,25 +42,20 @@ setup_ui <- function(id) { ) } -setup_server <- function(id, data, adnca_data) { +setup_server <- function(id, data, adnca_data, settings_override) { moduleServer(id, function(input, output, session) { - imported_settings <- reactive({ - req(input$settings_upload) - readRDS(input$settings_upload$datapath) - }) - - settings_override <- reactive(imported_settings()$settings) - manual_slopes_override <- reactive(imported_settings()$slope_rules) - parameters_override <- reactive(imported_settings()$settings$parameter_selections) - general_excl_override <- reactive(imported_settings()$settings$general_exclusions) + imported_settings <- reactive(settings_override()$settings) + imported_slopes <- reactive(settings_override()$slope_rules) + imported_params <- reactive(imported_settings()$parameter_selections) + general_excl_override <- reactive(imported_settings()$general_exclusions) # Gather all settings from the appropriate module settings <- settings_server( "nca_settings", data, adnca_data, - settings_override + imported_settings ) general_exclusions <- general_exclusions_server("general_exclusions", processed_pknca_data) @@ -104,7 +88,7 @@ setup_server <- function(id, data, adnca_data) { parameters_output <- parameter_selection_server( "nca_setup_parameter", base_pknca_data, - parameters_override + imported_params ) final_settings <- reactive({ @@ -151,9 +135,9 @@ setup_server <- function(id, data, adnca_data) { session$userData$ratio_table <- reactive(ratio_table()) # Automatically update the units table when settings are uploaded. - observeEvent(settings_override(), { - req(settings_override()$units) - session$userData$units_table(settings_override()$units) + observeEvent(imported_settings(), { + req(imported_settings()$units) + session$userData$units_table(imported_settings()$units) }) # Parameter unit changes option: Opens a modal message with a units table to edit @@ -189,7 +173,7 @@ setup_server <- function(id, data, adnca_data) { slope_rules <- slope_selector_server( "slope_selector", slopes_pknca_data, - manual_slopes_override + imported_slopes ) # Handle downloading and uploading settings diff --git a/inst/shiny/modules/tab_nca/setup/parameter_selection.R b/inst/shiny/modules/tab_nca/setup/parameter_selection.R index 70f6baca1..296e55756 100644 --- a/inst/shiny/modules/tab_nca/setup/parameter_selection.R +++ b/inst/shiny/modules/tab_nca/setup/parameter_selection.R @@ -265,7 +265,6 @@ parameter_selection_server <- function(id, processed_pknca_data, parameter_overr # Validation checks if (length(study_type_names) == 0) return(list()) req(all(study_type_names %in% names(df))) - # Convert from wide to long, filter for selected rows, # and then split the result into a list by study_type. df %>% diff --git a/inst/shiny/modules/tab_nca/setup/settings.R b/inst/shiny/modules/tab_nca/setup/settings.R index 360f66e58..3f49ce772 100644 --- a/inst/shiny/modules/tab_nca/setup/settings.R +++ b/inst/shiny/modules/tab_nca/setup/settings.R @@ -136,167 +136,156 @@ settings_server <- function(id, data, adnca_data, settings_override) { conc_data <- reactive(adnca_data()$conc$data) # File Upload Handling - observeEvent(settings_override(), { + observeEvent(c(data(), settings_override()), { + req(data()) + # Initialize Analyte + Global Validation + choices <- unique(data()$PARAM) %>% na.omit() settings <- settings_override() - log_debug_list("User settings override:", settings) - + # Defaults + selected <- choices not_compatible <- c() - # General # - if (all(settings$analyte %in% unique(data()$PARAM))) { - updatePickerInput(inputId = "select_analyte", selected = settings$analyte) - } else { - not_compatible <- append(not_compatible, "Analyte") - } - - if (all(settings$profile %in% unique(data()$ATPTREF))) { - updatePickerInput(inputId = "select_profile", selected = settings$profile) - } else { - not_compatible <- append(not_compatible, "NCA Profile") - } - - if (all(settings$pcspec %in% unique(data()$PCSPEC))) { - updatePickerInput(inputId = "select_pcspec", selected = settings$pcspec) - } else { - not_compatible <- append(not_compatible, "Dose Specimen") - } + # if settings exist, update analyte picker input and check compatibility + if (!is.null(settings)) { - if (length(not_compatible) != 0) { - msg <- paste0( - paste0(not_compatible, collapse = ", "), - " not compatible with current data, leaving as default." - ) - log_warn(msg) - showNotification(msg, type = "warning", duration = 10) - } - - updateSelectInput(inputId = "method", selected = settings$method) - - if (!is.null(settings$bioavailability)) - updateSelectInput(inputId = "bioavailability", selected = settings$bioavailability) - - # Data imputation # - update_switch("should_impute_c0", value = settings$data_imputation$impute_c0) - - # Partial AUCs # - auc_data(settings$partial_aucs) - refresh_reactable(refresh_reactable() + 1) + if (all(settings$analyte %in% choices)) { + selected <- settings$analyte + } else { + not_compatible <- c(not_compatible, "Analyte") + } - # Flags # - .update_rule_input( - session, - "R2ADJ", - settings$flags$R2ADJ$is.checked, - settings$flags$R2ADJ$threshold - ) + # We check raw data here just to see if the settings exist + if (!all(settings$profile %in% data()$ATPTREF)) { + not_compatible <- c(not_compatible, "Profile") + } + if (!all(settings$pcspec %in% data()$PCSPEC)) { + not_compatible <- c(not_compatible, "Dose Specimen") + } - .update_rule_input( - session, - "R2", - settings$flags$R2$is.checked, - settings$flags$R2$threshold - ) + # Additional settings (PCSPEC and ATPTREF handled later) + updateSelectInput(session, inputId = "method", selected = settings$method) - .update_rule_input( - session, - "AUCPEO", - settings$flags$AUCPEO$is.checked, - settings$flags$AUCPEO$threshold - ) + if (!is.null(settings$bioavailability)) + updateSelectInput(session, inputId = "bioavailability", + selected = settings$bioavailability) - .update_rule_input( - session, - "AUCPEP", - settings$flags$AUCPEP$is.checked, - settings$flags$AUCPEP$threshold - ) + # Data imputation # + update_switch("should_impute_c0", value = settings$data_imputation$impute_c0) - .update_rule_input( - session, - "LAMZSPN", - settings$flags$LAMZSPN$is.checked, - settings$flags$LAMZSPN$threshold - ) - }) + # Partial AUCs # + auc_data(settings$partial_aucs) + refresh_reactable(refresh_reactable() + 1) - # Include keyboard limits for the settings GUI display + # Flags # + .update_rule_input( + session, + "R2ADJ", + settings$flags$R2ADJ$is.checked, + settings$flags$R2ADJ$threshold + ) - # Keyboard limits for the setting thresholds - limit_input_value(input, session, "R2ADJ_threshold", max = 1, min = 0, lab = "RSQADJ") - limit_input_value(input, session, "AUCPEO_threshold", max = 100, min = 0, lab = "AUCPEO") - limit_input_value(input, session, "AUCPEP_threshold", max = 100, min = 0, lab = "AUCPEP") - limit_input_value(input, session, "LAMZSPN_threshold", min = 0, lab = "LAMZSPN") + .update_rule_input( + session, + "R2", + settings$flags$R2$is.checked, + settings$flags$R2$threshold + ) + .update_rule_input( + session, + "AUCPEO", + settings$flags$AUCPEO$is.checked, + settings$flags$AUCPEO$threshold + ) - # Choose data to be analyzed - observeEvent(data(), priority = -1, { - req(data()) + .update_rule_input( + session, + "AUCPEP", + settings$flags$AUCPEP$is.checked, + settings$flags$AUCPEP$threshold + ) - choices <- unique(data()$PARAM) %>% - na.omit() + .update_rule_input( + session, + "LAMZSPN", + settings$flags$LAMZSPN$is.checked, + settings$flags$LAMZSPN$threshold + ) + } - updatePickerInput( - session, - inputId = "select_analyte", - choices = choices, - selected = choices - ) + if (length(not_compatible) > 0) { + msg <- paste0(paste0(not_compatible, collapse = ", "), + " settings not found in data. Reverting to defaults.") + log_warn(msg) + showNotification(msg, type = "warning", duration = 10) + } + updatePickerInput(session, "select_analyte", choices = choices, selected = selected) }) + # Update Downstream Inputs (Profile & Specimen) observeEvent(input$select_analyte, { req(data()) - # Isolate current selections to prevent reactive loops - current_profile <- isolate(input$select_profile) - current_pcspec <- isolate(input$select_pcspec) + settings <- settings_override() + # Filter data based on Analyte filtered_data <- data() %>% - filter(PARAM %in% input$select_analyte, - !is.na(PCSPEC), - !is.na(ATPTREF)) # Filter together so there's no combinations of NAs + filter(PARAM %in% input$select_analyte, !is.na(PCSPEC), !is.na(ATPTREF)) - profile_choices <- unique(filtered_data$ATPTREF) %>% - sort() - - pcspec_choices <- unique(filtered_data$PCSPEC) + profile_choices <- sort(unique(filtered_data$ATPTREF)) + pcspec_choices <- unique(filtered_data$PCSPEC) + # PROFILE + current_profile <- isolate(input$select_profile) + # SPECIMEN + current_pcspec <- isolate(input$select_pcspec) - # Fallback if the current selection is empty - if (length(current_profile) == 0) { - current_profile <- profile_choices[1] - } - if (length(current_pcspec) == 0) { - # Select plasma/serum if available - plasma_serum_values <- grep("^plasma$|^serum$", - pcspec_choices, - value = TRUE, - ignore.case = TRUE) - - # Assign to current_pcspec if found, otherwise select all - if (length(plasma_serum_values) > 0) { - current_pcspec <- plasma_serum_values + if (!is.null(settings)) { + if (all(settings$profile %in% profile_choices)) { + target_profile <- settings$profile + } + if (all(settings$pcspec %in% pcspec_choices)) { + target_pcspec <- settings$pcspec + } + } else { + if (length(intersect(current_profile, profile_choices)) > 0) { + target_profile <- current_profile # Keep current if valid + } else { + target_profile <- profile_choices[1] + } + if (length(intersect(current_pcspec, pcspec_choices)) > 0) { + target_pcspec <- current_pcspec # Keep current if valid } else { - current_pcspec <- pcspec_choices + # Smart Default (Plasma/Serum) + plasma_serum <- grep("^plasma$|^serum$", pcspec_choices, value = TRUE, ignore.case = TRUE) + target_pcspec <- if (length(plasma_serum) > 0) plasma_serum else pcspec_choices } } updatePickerInput( session, - inputId = "select_profile", + "select_profile", choices = profile_choices, - selected = current_profile + selected = target_profile ) - updatePickerInput( session, - inputId = "select_pcspec", + "select_pcspec", choices = pcspec_choices, - selected = current_pcspec + selected = target_pcspec ) }) + # Include keyboard limits for the settings GUI display + + # Keyboard limits for the setting thresholds + limit_input_value(input, session, "R2ADJ_threshold", max = 1, min = 0, lab = "RSQADJ") + limit_input_value(input, session, "AUCPEO_threshold", max = 100, min = 0, lab = "AUCPEO") + limit_input_value(input, session, "AUCPEP_threshold", max = 100, min = 0, lab = "AUCPEP") + limit_input_value(input, session, "LAMZSPN_threshold", min = 0, lab = "LAMZSPN") + # Reactive value to store the AUC data table auc_data <- reactiveVal( tibble(start_auc = rep(NA_real_, 2), end_auc = rep(NA_real_, 2)) diff --git a/inst/shiny/modules/tab_nca/setup/slope_selector.R b/inst/shiny/modules/tab_nca/setup/slope_selector.R index 540b7ab6e..6557ee932 100644 --- a/inst/shiny/modules/tab_nca/setup/slope_selector.R +++ b/inst/shiny/modules/tab_nca/setup/slope_selector.R @@ -340,25 +340,21 @@ slope_selector_server <- function( # nolint }) #' If any settings are uploaded by the user, overwrite current rules - observeEvent(manual_slopes_override(), { - req(manual_slopes_override()) + observeEvent(list(manual_slopes_override(), plot_data()), { + req(manual_slopes_override(), plot_data()) if (nrow(manual_slopes_override()) == 0) return(NULL) log_debug_list("Manual slopes override:", manual_slopes_override()) - override_valid <- apply(manual_slopes_override(), 1, function(r) { - dplyr::filter( - plot_data()$conc$data, - PCSPEC == r["PCSPEC"], - USUBJID == r["USUBJID"], - PARAM == r["PARAM"], - ATPTREF == r["ATPTREF"], - DOSNOA == r["DOSNOA"] - ) %>% - NROW() != 0 - }) %>% - all() + allowed_keys <- c("PCSPEC", "USUBJID", "PARAM", "ATPTREF", "DOSNOA") + use_keys <- intersect(allowed_keys, names(manual_slopes_override())) + + override_valid <- manual_slopes_override() %>% + mutate(across(all_of(use_keys), as.character)) %>% # mutate incase of type issues + semi_join(plot_data()$conc$data %>% + mutate(across(all_of(use_keys), as.character)), by = use_keys) %>% + nrow() == nrow(manual_slopes_override()) if (!override_valid) { msg <- "Manual slopes not compatible with current data, leaving as default."