# ============================================================================== # Polyphenol Estimation Pipeline - Shiny Application Server # ============================================================================== # # PIPELINE DEVELOPED BY: # Stephanie M.G. Wilson # University of California, Davis # Contact: smgwilson@ucdavis.edu # Repository: https://github.com/SWi1/polyphenol_pipeline/ # License: MIT # # SHINY APP DEVELOPED BY: # Richard Stoker # United States Department of Agriculture - Agricultural Research Service # Contact: Richard.Stoker@usda.gov # License: CC0 (Public Domain) # # VERSION: 0.1 Alpha # DATE: November 2025 # ============================================================================== server <- function(input, output, session) { # -------------------------------------------------------------------------- # Reactive Values # -------------------------------------------------------------------------- rv <- reactiveValues( raw_data = NULL, data_source = NULL, data_validated = FALSE, analysis_complete = FALSE, results = list(), unmapped_foods = NULL, processing = FALSE ) # -------------------------------------------------------------------------- # Splash Screen Modal # -------------------------------------------------------------------------- observeEvent(TRUE, { showModal(modalDialog( title = NULL, size = "l", easyClose = FALSE, footer = NULL, tags$div( class = "splash-content text-center", # Logo container with video and static image overlay tags$div( class = "splash-logo-container", # Video plays first tags$video( id = "splash-video", class = "splash-video", autoplay = NA, muted = NA, playsinline = NA, tags$source(src = "splash_animation.mp4", type = "video/mp4") ), # Static logo (hidden initially, shown after video ends) tags$img( id = "splash-logo", class = "splash-logo hidden", src = "PEPlogo_720x720.jpeg", alt = "Polyphenol Estimation Pipeline" ) ), # Attribution section tags$div( class = "splash-info", tags$p( class = "splash-attribution", tags$span(class = "attribution-label", "Pipeline Developed by"), tags$br(), tags$span(class = "developer-name", "Stephanie M.G. Wilson"), tags$br(), tags$span(class = "developer-affiliation", "University of California, Davis"), tags$br(), tags$a( href = "https://github.com/SWi1/polyphenol_pipeline", target = "_blank", class = "repo-link", "View Pipeline Repository" ) ) ), # Get Started button tags$div( class = "splash-buttons", actionButton("splash_start", "Get Started", class = "btn-primary btn-lg splash-btn") ), # JavaScript for video control - stop 1 second before end to avoid fade-out tags$script(HTML(" (function() { // Wait for modal to be fully rendered var checkVideo = setInterval(function() { var video = document.getElementById('splash-video'); var logo = document.getElementById('splash-logo'); if (video) { clearInterval(checkVideo); // Stop video 1 second before end to avoid the fade-out frames video.addEventListener('timeupdate', function() { if (video.duration && video.currentTime >= video.duration - 1.0) { video.pause(); } }); // Fallback: if video fails to load, show static logo video.addEventListener('error', function() { video.style.display = 'none'; if (logo) { logo.classList.remove('hidden'); logo.classList.add('fade-in'); } }); // If video can't play (e.g., autoplay blocked), show static logo var playPromise = video.play(); if (playPromise !== undefined) { playPromise.catch(function(error) { video.style.display = 'none'; if (logo) { logo.classList.remove('hidden'); logo.classList.add('fade-in'); } }); } } }, 100); })(); ")) ) )) }, once = TRUE) observeEvent(input$splash_start, { removeModal() }) # Navigate to Input tab from Get Started page observeEvent(input$go_to_input, { updateNavbarPage(session, "main_nav", selected = "input") }) # -------------------------------------------------------------------------- # Demo Data Loading # -------------------------------------------------------------------------- load_demo_data <- function() { if (file.exists(DEMO_DATA_FILE)) { rv$raw_data <- vroom(DEMO_DATA_FILE, show_col_types = FALSE) rv$data_source <- "ASA24" rv$data_validated <- TRUE updateRadioGroupButtons(session, "data_source", selected = "ASA24") showNotification("Demo data loaded (ASA24 format)", type = "message") } else { showNotification("Demo data file not found", type = "error") } } observeEvent(input$load_demo, { load_demo_data() }) # -------------------------------------------------------------------------- # File Upload Handler # -------------------------------------------------------------------------- observeEvent(input$diet_file, { req(input$diet_file) tryCatch({ file_ext <- tools::file_ext(input$diet_file$name) if (file_ext %in% c("csv", "CSV")) { rv$raw_data <- vroom(input$diet_file$datapath, show_col_types = FALSE) } else if (file_ext %in% c("xlsx", "xls")) { rv$raw_data <- read_xlsx(input$diet_file$datapath) } else { showNotification("Unsupported file format. Use CSV or Excel.", type = "error") return() } rv$data_source <- input$data_source if (input$data_source == "ASA24") { validation <- validate_asa24_data(rv$raw_data) } else { validation <- validate_nhanes_data(rv$raw_data) } rv$data_validated <- validation$valid if (!validation$valid) { showNotification(validation$message, type = "error", duration = 10) } else { showNotification("Data loaded and validated", type = "message") } }, error = function(e) { showNotification(paste("Error reading file:", e$message), type = "error") rv$raw_data <- NULL rv$data_validated <- FALSE }) }) # -------------------------------------------------------------------------- # Reset Handler with Confirmation # -------------------------------------------------------------------------- observeEvent(input$reset_btn, { showModal(modalDialog( title = "Confirm Reset", "Are you sure you want to clear all data and results? This cannot be undone.", footer = tagList( modalButton("Cancel"), actionButton("confirm_reset", "Yes, Reset", class = "btn-danger") ) )) }) observeEvent(input$confirm_reset, { rv$raw_data <- NULL rv$data_source <- NULL rv$data_validated <- FALSE rv$analysis_complete <- FALSE rv$results <- list() rv$unmapped_foods <- NULL removeModal() showNotification("All data cleared", type = "message") }) # -------------------------------------------------------------------------- # Data Status Output # -------------------------------------------------------------------------- output$data_status <- renderUI({ if (is.null(rv$raw_data)) { tags$div( class = "alert alert-info", icon("info-circle"), " Upload a dietary data file or load demo data to begin." ) } else if (!rv$data_validated) { tags$div( class = "alert alert-warning", icon("triangle-exclamation"), " Data validation failed. Check that your file has the required columns." ) } else { n_subjects <- length(unique(rv$raw_data[[if(rv$data_source == "ASA24") "UserName" else "SEQN"]])) n_rows <- nrow(rv$raw_data) tags$div( class = "alert alert-success", icon("check-circle"), sprintf(" Data loaded: %d subjects, %d food records", n_subjects, n_rows) ) } }) # -------------------------------------------------------------------------- # Data Preview Table # -------------------------------------------------------------------------- output$data_preview <- renderDT({ req(rv$raw_data) preview_cols <- if (rv$data_source == "ASA24") { intersect(c("UserName", "RecallNo", "FoodCode", "Food_Description", "FoodAmt", "KCAL"), names(rv$raw_data)) } else { intersect(c("SEQN", "RecallNo", "DRXIFDCD", "DRXIKCAL"), names(rv$raw_data)) } datatable( head(rv$raw_data[, preview_cols, drop = FALSE], 100), options = list( pageLength = 10, scrollX = TRUE, dom = 'tip' ), class = "display compact", rownames = FALSE ) }) # -------------------------------------------------------------------------- # Main Pipeline Execution # -------------------------------------------------------------------------- observeEvent(input$run_pipeline, { req(rv$raw_data, rv$data_validated) if (!databases_ready()) { showNotification("Reference databases not loaded. Cannot run pipeline.", type = "error") return() } rv$processing <- TRUE withProgress(message = "Running pipeline...", value = 0, { tryCatch({ # Step 1: Prepare and clean data incProgress(0.1, detail = "Preparing dietary data") if (rv$data_source == "ASA24") { input_data <- rv$raw_data %>% rename(subject = UserName) } else { input_data <- rv$raw_data %>% rename(subject = SEQN) } input_data_clean <- input_data %>% group_by(subject) %>% filter(n_distinct(RecallNo) > 1) %>% ungroup() if ("RecallStatus" %in% names(input_data_clean)) { input_data_clean <- input_data_clean %>% filter(RecallStatus != 5) } # Calculate total nutrients incProgress(0.15, detail = "Calculating nutrient totals") nutrient_cols <- if ("KCAL" %in% names(input_data_clean)) { kcal_idx <- which(names(input_data_clean) == "KCAL") b12_idx <- which(names(input_data_clean) == "B12_ADD") if (length(b12_idx) > 0) { names(input_data_clean)[kcal_idx:b12_idx] } else { "KCAL" } } else { grep("^DRXI", names(input_data_clean), value = TRUE) } input_total_nutrients <- input_data_clean %>% group_by(subject, RecallNo) %>% summarize(across(any_of(nutrient_cols), ~ sum(.x, na.rm = TRUE), .names = "Total_{.col}"), .groups = "drop") if (rv$data_source == "ASA24") { input_data_minimal <- input_data_clean %>% rename(wweia_food_code = FoodCode, food_description = Food_Description) %>% select(any_of(c("subject", "RecallNo", "wweia_food_code", "food_description", "FoodAmt"))) } else { input_data_minimal <- input_data_clean %>% rename(wweia_food_code = DRXIFDCD) %>% select(any_of(c("subject", "RecallNo", "wweia_food_code", "DRXIGRMS"))) %>% rename(FoodAmt = DRXIGRMS) } # Step 2: Apply brewing adjustment and disaggregate incProgress(0.25, detail = "Disaggregating foods") FDD_adjusted <- apply_brewing_adjustment(FDD_V3) merged_data <- left_join(input_data_minimal, FDD_adjusted, by = "wweia_food_code", relationship = "many-to-many") %>% mutate(FoodAmt_Ing_g = FoodAmt * ( coalesce(brewing_adjustment_percentage, ingredient_percent) / 100)) # Step 3: Map to FooDB incProgress(0.35, detail = "Mapping to FooDB database") input_mapped <- merged_data %>% left_join(fdd_foodb_mapping, by = "fdd_ingredient") rv$unmapped_foods <- input_mapped %>% filter(!is.na(fdd_ingredient) & is.na(orig_food_common_name)) %>% distinct(fdd_ingredient) %>% pull(fdd_ingredient) # Step 4: Calculate polyphenol content incProgress(0.45, detail = "Calculating polyphenol content") input_mapped_content <- input_mapped %>% left_join(FooDB_mg_100g, by = "food_id", relationship = "many-to-many") %>% mutate( pp_consumed = if_else( compound_public_id %in% c("FDB000095", "FDB017114") & food_id == 38, (orig_content_avg_RFadj * 0.01) * FoodAmt_Ing_g * (ingredient_percent / 100), (orig_content_avg_RFadj * 0.01) * FoodAmt_Ing_g ) ) input_kcal <- input_total_nutrients %>% select(subject, RecallNo, any_of(c("Total_KCAL", "Total_DRXIKCAL"))) if (!"Total_KCAL" %in% names(input_kcal) && "Total_DRXIKCAL" %in% names(input_kcal)) { input_kcal <- input_kcal %>% rename(Total_KCAL = Total_DRXIKCAL) } input_polyphenol_kcal <- left_join(input_mapped_content, input_kcal, by = c("subject", "RecallNo")) # Step 5: Calculate total intake incProgress(0.55, detail = "Summarizing total intake") content_by_recall <- input_polyphenol_kcal %>% group_by(subject, RecallNo) %>% summarise( pp_recallsum_mg = sum(pp_consumed, na.rm = TRUE), Total_KCAL = first(Total_KCAL), .groups = "drop" ) %>% mutate(pp_recallsum_mg1000kcal = pp_recallsum_mg / (Total_KCAL / 1000)) content_by_subject <- content_by_recall %>% group_by(subject) %>% summarise( pp_average_mg = mean(pp_recallsum_mg, na.rm = TRUE), kcal_average = mean(Total_KCAL, na.rm = TRUE), pp_average_mg_1000kcal = pp_average_mg / (kcal_average / 1000), .groups = "drop" ) rv$results$total_by_subject <- content_by_subject rv$results$total_by_recall <- content_by_recall # Step 6: Calculate class-level intake incProgress(0.65, detail = "Calculating class-level intake") input_with_class <- input_polyphenol_kcal %>% left_join(class_tax, by = "compound_public_id") class_by_recall <- input_with_class %>% group_by(subject, RecallNo, class) %>% summarise( class_intake_mg = sum(pp_consumed, na.rm = TRUE), Total_KCAL = first(Total_KCAL), .groups = "drop" ) %>% filter(!is.na(class)) %>% mutate(class_intake_mg1000kcal = class_intake_mg / (Total_KCAL / 1000)) class_by_subject <- class_by_recall %>% group_by(subject, class) %>% summarise( Avg_class_intake_mg = mean(class_intake_mg, na.rm = TRUE), avg_Total_KCAL = mean(Total_KCAL, na.rm = TRUE), .groups = "drop" ) %>% mutate(class_intake_mg1000kcal = Avg_class_intake_mg / (avg_Total_KCAL / 1000)) rv$results$class_by_subject <- class_by_subject rv$results$class_by_recall <- class_by_recall # Step 7: Calculate food contributors incProgress(0.75, detail = "Identifying food contributors") food_contributors <- input_polyphenol_kcal %>% group_by(fdd_ingredient) %>% summarise( food_pp_average_mg1000kcal = mean(pp_consumed, na.rm = TRUE) / mean(Total_KCAL, na.rm = TRUE) * 1000, total_times_consumed = n(), n_subjects = n_distinct(subject), .groups = "drop" ) %>% filter(!is.na(fdd_ingredient)) %>% arrange(desc(food_pp_average_mg1000kcal)) rv$results$food_contributors <- food_contributors # Step 8: DII calculation if (input$calculate_dii) { incProgress(0.85, detail = "Calculating DII scores") # DII Step 1: Calculate eugenol intake if (!is.null(FooDB_eugenol)) { eugenol_intake <- input_mapped %>% left_join( FooDB_eugenol %>% select(-c(source_type, food_name, orig_food_common_name, orig_food_scientific_name, orig_source_id, orig_source_name, citation, preparation_type)), by = "food_id" ) %>% filter(!is.na(orig_content_avg)) %>% mutate(eugenol_mg = (orig_content_avg * 0.01) * FoodAmt_Ing_g) %>% group_by(subject, RecallNo) %>% summarise(EUGENOL = sum(eugenol_mg, na.rm = TRUE), .groups = "drop") } else { eugenol_intake <- input_mapped %>% distinct(subject, RecallNo) %>% mutate(EUGENOL = 0) } # DII Step 2: Calculate polyphenol subclass intakes if (!is.null(FooDB_DII_subclasses)) { subclass_intake <- input_polyphenol_kcal %>% filter(compound_public_id %in% FooDB_DII_subclasses$compound_public_id) %>% left_join(FooDB_DII_subclasses %>% select(compound_public_id, component), by = "compound_public_id") %>% group_by(subject, RecallNo, component) %>% summarise(component_sum = sum(pp_consumed, na.rm = TRUE), .groups = "drop") %>% pivot_wider(names_from = component, values_from = component_sum, values_fill = 0) %>% rename_with(~ case_when( . == "Isoflavones" ~ "ISOFLAVONES", . == "Flavan-3-ols" ~ "FLA3OL", . == "Flavones" ~ "FLAVONES", . == "Flavonols" ~ "FLAVONOLS", . == "Flavanones" ~ "FLAVONONES", . == "Anthocyanidins" ~ "ANTHOC", TRUE ~ . ), .cols = -c(subject, RecallNo)) } else { subclass_intake <- input_mapped %>% distinct(subject, RecallNo) %>% mutate(ISOFLAVONES = 0, FLA3OL = 0, FLAVONES = 0, FLAVONOLS = 0, FLAVONONES = 0, ANTHOC = 0) } # DII Step 3: Calculate DII food component intakes # Following the exact pattern matching from DII_STEP3_Food.Rmd # Get unique FDD ingredients fdd_unique <- FDD_V3 %>% distinct(fdd_ingredient) # Simple single-pattern matches garlic_ingredients <- fdd_unique %>% filter(grepl("garlic", fdd_ingredient, ignore.case = TRUE)) %>% mutate(component = "GARLIC") ginger_ingredients <- fdd_unique %>% filter(grepl("ginger", fdd_ingredient, ignore.case = TRUE)) %>% mutate(component = "GINGER") onion_ingredients <- fdd_unique %>% filter(grepl("onion", fdd_ingredient, ignore.case = TRUE)) %>% mutate(component = "ONION") turmeric_ingredients <- fdd_unique %>% filter(grepl("turmeric", fdd_ingredient, ignore.case = TRUE)) %>% mutate(component = "TURMERIC") # Tea: must contain "tea" AND ("black" OR "oolong" OR "green") # Excludes herbal teas tea_ingredients <- fdd_unique %>% filter(grepl("tea", fdd_ingredient, ignore.case = TRUE)) %>% filter(grepl("black|oolong|green", fdd_ingredient, ignore.case = TRUE)) %>% mutate(component = "TEA") # Pepper: must contain "pepper" AND "spices" # Excludes fresh peppers (bell peppers, etc.) pepper_ingredients <- fdd_unique %>% filter(grepl("pepper", fdd_ingredient, ignore.case = TRUE)) %>% filter(grepl("spices", fdd_ingredient, ignore.case = TRUE)) %>% mutate(component = "PEPPER") # Thyme or oregano thyme_ingredients <- fdd_unique %>% filter(grepl("thyme|oregano", fdd_ingredient, ignore.case = TRUE)) %>% mutate(component = "THYME") # Combine all DII food ingredients dii_foods_df <- bind_rows( garlic_ingredients, ginger_ingredients, onion_ingredients, turmeric_ingredients, tea_ingredients, pepper_ingredients, thyme_ingredients ) if (nrow(dii_foods_df) > 0) { food_component_intake <- input_mapped %>% filter(fdd_ingredient %in% dii_foods_df$fdd_ingredient) %>% left_join(dii_foods_df, by = "fdd_ingredient") %>% group_by(subject, RecallNo, component) %>% summarise(component_sum = sum(FoodAmt_Ing_g, na.rm = TRUE), .groups = "drop") %>% pivot_wider(names_from = component, values_from = component_sum, values_fill = 0) } else { food_component_intake <- input_mapped %>% distinct(subject, RecallNo) } # DII Step 4: Merge all components with total nutrients dii_merge <- input_total_nutrients %>% rename_with(~ gsub("^Total_", "", .x)) %>% left_join(eugenol_intake, by = c("subject", "RecallNo")) %>% left_join(subclass_intake, by = c("subject", "RecallNo")) %>% left_join(food_component_intake, by = c("subject", "RecallNo")) # Standardize nutrient names (ASA24 vs NHANES) nutrient_mapping <- tribble( ~new_name, ~asa24_name, ~nhanes_name, "ALCOHOL", "ALC", "DRXIALCO", "VITB12", "VB12", "DRXIVB12", "VITB6", "VB6", "DRXIVB6", "BCAROTENE", "BCAR", "DRXIBCAR", "CAFF", "CAFF", "DRXICAFF", "CARB", "CARB", "DRXICARB", "CHOLES", "CHOLE", "DRXICHOL", "KCAL", "KCAL", "DRXIKCAL", "TOTALFAT", "TFAT", "DRXITFAT", "FIBER", "FIBE", "DRXIFIBE", "FOLICACID", "FA", "DRXIFA", "IRON", "IRON", "DRXIIRON", "MG", "MAGN", "DRXIMAGN", "MUFA", "MFAT", "DRXIMFAT", "NIACIN", "NIAC", "DRXINIAC", "P183", "P183", "DRXIP183", "P184", "P184", "DRXIP184", "P205", "P205", "DRXIP205", "P225", "P225", "DRXIP225", "P226", "P226", "DRXIP226", "P182", "P182", "DRXIP182", "P204", "P204", "DRXIP204", "PROTEIN", "PROT", "DRXIPROT", "PUFA", "PFAT", "DRXIPFAT", "RIBOFLAVIN", "VB2", "DRXIVB2", "SATFAT", "SFAT", "DRXISFAT", "SE", "SELE", "DRXISELE", "THIAMIN", "VB1", "DRXIVB1", "VITA", "VARA", "DRXIVARA", "VITC", "VC", "DRXIVC", "VITD", "VITD", "DRXIVD", "VITE", "ATOC", "DRXIATOC", "ZN", "ZINC", "DRXIZINC" ) is_nhanes <- any(startsWith(names(dii_merge), "DRX")) old_names <- if (is_nhanes) nutrient_mapping$nhanes_name else nutrient_mapping$asa24_name new_names <- nutrient_mapping$new_name existing_idx <- which(old_names %in% names(dii_merge)) if (length(existing_idx) > 0) { dii_merge <- dii_merge %>% rename(!!!setNames(old_names[existing_idx], new_names[existing_idx])) } # Calculate derived components dii_cohort <- dii_merge %>% mutate( CAFFEINE = if ("CAFF" %in% names(.)) CAFF / 1000 else 0, N3FAT = rowSums(across(any_of(c("P183", "P184", "P205", "P225", "P226"))), na.rm = TRUE), N6FAT = rowSums(across(any_of(c("P182", "P204"))), na.rm = TRUE), TURMERIC = if ("TURMERIC" %in% names(.)) TURMERIC * 1000 else 0, THYME = if ("THYME" %in% names(.)) THYME * 1000 else 0 ) # DII scoring parameters from Shivappa et al. dii_params <- tribble( ~Variable, ~Overall_inflammatory_score, ~Global_mean, ~SD, "ALCOHOL", -0.278, 13.98, 3.72, "VITB12", 0.106, 5.15, 2.7, "VITB6", -0.365, 1.47, 0.74, "BCAROTENE", -0.584, 3718, 1720, "CAFFEINE", -0.11, 8.05, 6.67, "CARB", 0.097, 272.2, 40, "CHOLES", 0.11, 279.4, 51.2, "KCAL", 0.18, 2056, 338, "EUGENOL", -0.14, 0.01, 0.08, "TOTALFAT", 0.298, 71.4, 19.4, "FIBER", -0.663, 18.8, 4.9, "FOLICACID", -0.19, 273, 70.7, "GARLIC", -0.412, 4.35, 2.9, "GINGER", -0.453, 59, 63.2, "IRON", 0.032, 13.35, 3.71, "MG", -0.484, 310.1, 139.4, "MUFA", -0.009, 27, 6.1, "NIACIN", -0.246, 25.9, 11.77, "N3FAT", -0.436, 1.06, 1.06, "N6FAT", -0.159, 10.8, 7.5, "ONION", -0.301, 35.9, 18.4, "PROTEIN", 0.021, 79.4, 13.9, "PUFA", -0.337, 13.88, 3.76, "RIBOFLAVIN", -0.068, 1.7, 0.79, "SATFAT", 0.373, 28.6, 8, "SE", -0.191, 67, 25.1, "THIAMIN", -0.098, 1.7, 0.66, "VITA", -0.401, 983.9, 518.6, "VITC", -0.424, 118.2, 43.46, "VITD", -0.446, 6.26, 2.21, "VITE", -0.419, 8.73, 1.49, "ZN", -0.313, 9.84, 2.19, "TEA", -0.536, 1.69, 1.53, "FLA3OL", -0.415, 95.8, 85.9, "FLAVONES", -0.616, 1.55, 0.07, "FLAVONOLS", -0.467, 17.7, 6.79, "FLAVONONES", -0.25, 11.7, 3.82, "ANTHOC", -0.131, 18.05, 21.14, "ISOFLAVONES", -0.593, 1.2, 0.2, "PEPPER", -0.131, 10, 7.07, "THYME", -0.102, 0.33, 0.99, "TURMERIC", -0.785, 533.6, 754.3 ) # Pivot to long format for DII calculation dii_long <- dii_cohort %>% select(subject, RecallNo, any_of(dii_params$Variable)) %>% pivot_longer(-c(subject, RecallNo), names_to = "Variable", values_to = "Value") %>% left_join(dii_params, by = "Variable") %>% filter(!is.na(Overall_inflammatory_score)) %>% mutate( Z_SCORE = (Value - Global_mean) / SD, PERCENTILE = pnorm(Z_SCORE) * 2 - 1, IND_DII_SCORE = PERCENTILE * Overall_inflammatory_score ) # Calculate total DII scores dii_scores <- dii_long %>% group_by(subject, RecallNo) %>% summarise( DII_ALL = sum(IND_DII_SCORE, na.rm = TRUE), DII_NOETOH = sum(IND_DII_SCORE[Variable != "ALCOHOL"], na.rm = TRUE), n_components = n(), .groups = "drop" ) # Average by subject dii_by_subject <- dii_scores %>% group_by(subject) %>% summarise( DII_ALL_avg = mean(DII_ALL, na.rm = TRUE), DII_NOETOH_avg = mean(DII_NOETOH, na.rm = TRUE), n_recalls = n(), .groups = "drop" ) rv$results$dii_by_recall <- dii_scores rv$results$dii_by_subject <- dii_by_subject rv$results$dii_components <- dii_long %>% select(subject, RecallNo, Variable, Value, IND_DII_SCORE) %>% pivot_wider(names_from = Variable, values_from = c(Value, IND_DII_SCORE)) } # Calculate missing food stats for QA/QC incProgress(0.95, detail = "Generating QA/QC report") missing_counts <- input_mapped %>% group_by(subject, RecallNo) %>% summarise( missing = sum(is.na(orig_food_common_name)), total = n(), pct_missing = missing / total * 100, .groups = "drop" ) rv$results$missing_counts <- missing_counts rv$analysis_complete <- TRUE rv$processing <- FALSE incProgress(1, detail = "Complete") # Navigate to results tab using bslib function nav_select("main_nav", selected = "results") showNotification("Pipeline complete", type = "message") }, error = function(e) { rv$processing <- FALSE showNotification(paste("Pipeline error:", e$message), type = "error", duration = 15) }) }) }) # -------------------------------------------------------------------------- # Summary Cards # -------------------------------------------------------------------------- output$summary_cards <- renderUI({ if (!rv$analysis_complete) { return(tags$div( class = "alert alert-info text-center", icon("chart-bar"), " Run the pipeline to view results." )) } n_subjects <- nrow(rv$results$total_by_subject) mean_intake <- mean(rv$results$total_by_subject$pp_average_mg, na.rm = TRUE) n_classes <- length(unique(rv$results$class_by_subject$class)) n_unmapped <- length(rv$unmapped_foods) layout_columns( col_widths = c(3, 3, 3, 3), value_box( title = "Subjects Analyzed", value = n_subjects, theme = "primary", showcase = icon("users") ), value_box( title = "Mean Polyphenol Intake", value = paste(round(mean_intake, 1), "mg/day"), theme = "success", showcase = icon("leaf") ), value_box( title = "Polyphenol Classes", value = n_classes, theme = "info", showcase = icon("layer-group") ), value_box( title = "Unmapped Foods", value = n_unmapped, theme = if (n_unmapped > 0) "warning" else "success", showcase = icon("triangle-exclamation") ) ) }) # -------------------------------------------------------------------------- # Visualizations # -------------------------------------------------------------------------- output$plot_total_intake <- renderPlotly({ req(rv$analysis_complete, rv$results$total_by_subject) df <- rv$results$total_by_subject %>% arrange(desc(pp_average_mg)) %>% head(30) plot_ly(df, x = ~reorder(subject, pp_average_mg), y = ~pp_average_mg, type = "bar", marker = list(color = PRIMARY_COLOR)) %>% layout( xaxis = list(title = "Subject", tickangle = -45), yaxis = list(title = "Mean Polyphenol Intake (mg/day)"), margin = list(b = 100) ) }) output$plot_intake_distribution <- renderPlotly({ req(rv$analysis_complete, rv$results$total_by_subject) plot_ly(rv$results$total_by_subject, x = ~pp_average_mg, type = "histogram", marker = list(color = PRIMARY_COLOR, line = list(color = "white", width = 1))) %>% layout( xaxis = list(title = "Mean Polyphenol Intake (mg/day)"), yaxis = list(title = "Count") ) }) output$table_total_intake <- renderDT({ req(rv$analysis_complete, rv$results$total_by_subject) datatable( rv$results$total_by_subject %>% mutate(across(where(is.numeric), ~ round(.x, 2))), options = list( pageLength = 25, scrollX = TRUE, scrollY = "400px", dom = 'Bfrtip' ), class = "display compact stripe", rownames = FALSE ) }) output$plot_class_intake <- renderPlotly({ req(rv$analysis_complete, rv$results$class_by_subject) class_summary <- rv$results$class_by_subject %>% group_by(class) %>% summarise(mean_intake = mean(Avg_class_intake_mg, na.rm = TRUE), .groups = "drop") %>% arrange(desc(mean_intake)) colors <- get_viz_colors(nrow(class_summary)) plot_ly(class_summary, x = ~reorder(class, mean_intake), y = ~mean_intake, type = "bar", marker = list(color = colors)) %>% layout( xaxis = list(title = "Polyphenol Class", tickangle = -45), yaxis = list(title = "Mean Intake (mg/day)"), margin = list(b = 150) ) }) output$table_class_intake <- renderDT({ req(rv$analysis_complete, rv$results$class_by_subject) datatable( rv$results$class_by_subject %>% mutate(across(where(is.numeric), ~ round(.x, 2))), options = list( pageLength = 25, scrollX = TRUE, scrollY = "400px", dom = 'Bfrtip' ), class = "display compact stripe", rownames = FALSE ) }) output$plot_food_treemap <- renderPlotly({ req(rv$analysis_complete, rv$results$food_contributors) top_foods <- rv$results$food_contributors %>% filter(!is.na(fdd_ingredient), food_pp_average_mg1000kcal > 0) %>% head(50) if (nrow(top_foods) == 0) { return(plotly_empty() %>% layout(title = "No data available")) } plot_ly( top_foods, labels = ~fdd_ingredient, parents = "", values = ~food_pp_average_mg1000kcal, type = "treemap", textinfo = "label+value", marker = list( colors = get_viz_colors(nrow(top_foods)), line = list(width = 1, color = "white") ) ) %>% layout(margin = list(l = 0, r = 0, t = 30, b = 0)) }) output$table_food_contributors <- renderDT({ req(rv$analysis_complete, rv$results$food_contributors) datatable( rv$results$food_contributors %>% filter(!is.na(fdd_ingredient)) %>% mutate(across(where(is.numeric), ~ round(.x, 2))) %>% head(100), options = list( pageLength = 25, scrollX = TRUE, scrollY = "400px", dom = 'Bfrtip' ), class = "display compact stripe", rownames = FALSE ) }) output$dii_content <- renderUI({ if (!input$calculate_dii) { return(tags$div( class = "alert alert-secondary text-center my-4", icon("info-circle"), " DII calculation was not enabled. Enable it in the Input tab and re-run the pipeline." )) } if (!rv$analysis_complete) { return(tags$div( class = "alert alert-info text-center my-4", icon("chart-bar"), " Run the pipeline to view DII scores." )) } if (is.null(rv$results$dii_by_subject)) { return(tags$div( class = "alert alert-warning text-center my-4", icon("triangle-exclamation"), " DII scores could not be calculated. Check that your data contains the required nutrient columns." )) } tagList( layout_columns( col_widths = c(6, 6), card( card_header("DII Score Distribution"), card_body( plotlyOutput("plot_dii_distribution", height = "350px") ) ), card( card_header("DII Scores by Subject"), card_body( plotlyOutput("plot_dii_by_subject", height = "350px") ) ) ), card( card_header( class = "d-flex justify-content-between align-items-center", tags$span("Subject-Level DII Scores"), downloadButton("download_dii", "Export CSV", class = "btn-sm btn-outline-primary") ), card_body( DTOutput("table_dii_scores") ) ), tags$div( class = "alert alert-info mt-3", tags$strong("About the DII: "), "The Dietary Inflammatory Index (DII) is a literature-derived score that assesses the inflammatory ", "potential of the diet. Negative scores indicate anti-inflammatory diets, while positive scores ", "indicate pro-inflammatory diets. This calculation uses 42 components including nutrients, ", "polyphenol subclasses, and specific anti-inflammatory foods." ) ) }) # DII Plots output$plot_dii_distribution <- renderPlotly({ req(rv$analysis_complete, rv$results$dii_by_subject) plot_ly(rv$results$dii_by_subject, x = ~DII_ALL_avg, type = "histogram", marker = list(color = PRIMARY_COLOR, line = list(color = "white", width = 1))) %>% layout( xaxis = list(title = "Average DII Score"), yaxis = list(title = "Number of Subjects"), shapes = list( list(type = "line", x0 = 0, x1 = 0, y0 = 0, y1 = 1, yref = "paper", line = list(color = "red", dash = "dash", width = 2)) ) ) }) output$plot_dii_by_subject <- renderPlotly({ req(rv$analysis_complete, rv$results$dii_by_subject) df <- rv$results$dii_by_subject %>% arrange(DII_ALL_avg) %>% head(30) colors <- ifelse(df$DII_ALL_avg < 0, "#28a745", "#dc3545") plot_ly(df, x = ~reorder(subject, DII_ALL_avg), y = ~DII_ALL_avg, type = "bar", marker = list(color = colors)) %>% layout( xaxis = list(title = "Subject", tickangle = -45), yaxis = list(title = "Average DII Score"), margin = list(b = 100) ) }) output$table_dii_scores <- renderDT({ req(rv$analysis_complete, rv$results$dii_by_subject) datatable( rv$results$dii_by_subject %>% mutate(across(where(is.numeric), ~ round(.x, 3))) %>% rename( Subject = subject, `DII (All Components)` = DII_ALL_avg, `DII (No Alcohol)` = DII_NOETOH_avg, `Number of Recalls` = n_recalls ), options = list( pageLength = 25, scrollX = TRUE, scrollY = "400px", dom = 'Bfrtip' ), class = "display compact stripe", rownames = FALSE ) }) output$download_dii <- downloadHandler( filename = function() paste0("dii_scores_", Sys.Date(), ".csv"), content = function(file) { write_csv(rv$results$dii_by_subject, file) } ) # -------------------------------------------------------------------------- # QA/QC Tab # -------------------------------------------------------------------------- output$unmapped_summary <- renderUI({ if (is.null(rv$unmapped_foods) || length(rv$unmapped_foods) == 0) { return(tags$div( class = "alert alert-success", icon("check-circle"), " All foods were successfully mapped to FooDB." )) } tags$div( class = "alert alert-warning", icon("triangle-exclamation"), sprintf(" %d unique food items could not be mapped.", length(rv$unmapped_foods)) ) }) output$table_unmapped_foods <- renderDT({ req(rv$unmapped_foods) if (length(rv$unmapped_foods) == 0) { return(NULL) } datatable( data.frame(Unmapped_Food = rv$unmapped_foods), options = list( pageLength = 25, scrollX = TRUE, scrollY = "300px" ), class = "display compact stripe", rownames = FALSE ) }) output$plot_missing_distribution <- renderPlotly({ req(rv$analysis_complete, rv$results$missing_counts) plot_ly(rv$results$missing_counts, x = ~pct_missing, type = "histogram", marker = list(color = "#ffc107", line = list(color = "white", width = 1))) %>% layout( xaxis = list(title = "Unmapped Foods (%)"), yaxis = list(title = "Number of Recalls") ) }) # -------------------------------------------------------------------------- # Download Handlers # -------------------------------------------------------------------------- output$download_total <- downloadHandler( filename = function() paste0("polyphenol_total_intake_", Sys.Date(), ".csv"), content = function(file) { write_csv(rv$results$total_by_subject, file) } ) output$download_class <- downloadHandler( filename = function() paste0("polyphenol_class_intake_", Sys.Date(), ".csv"), content = function(file) { write_csv(rv$results$class_by_subject, file) } ) output$download_foods <- downloadHandler( filename = function() paste0("polyphenol_food_contributors_", Sys.Date(), ".csv"), content = function(file) { write_csv(rv$results$food_contributors, file) } ) output$download_all <- downloadHandler( filename = function() paste0("polyphenol_results_", Sys.Date(), ".zip"), content = function(file) { # Create temp directory for files temp_dir <- tempdir() on.exit(unlink(file.path(temp_dir, "*.csv"))) files_to_zip <- c() if (!is.null(rv$results$total_by_subject)) { f <- file.path(temp_dir, "total_intake_by_subject.csv") write_csv(rv$results$total_by_subject, f) files_to_zip <- c(files_to_zip, f) } if (!is.null(rv$results$total_by_recall)) { f <- file.path(temp_dir, "total_intake_by_recall.csv") write_csv(rv$results$total_by_recall, f) files_to_zip <- c(files_to_zip, f) } if (!is.null(rv$results$class_by_subject)) { f <- file.path(temp_dir, "class_intake_by_subject.csv") write_csv(rv$results$class_by_subject, f) files_to_zip <- c(files_to_zip, f) } if (!is.null(rv$results$class_by_recall)) { f <- file.path(temp_dir, "class_intake_by_recall.csv") write_csv(rv$results$class_by_recall, f) files_to_zip <- c(files_to_zip, f) } if (!is.null(rv$results$food_contributors)) { f <- file.path(temp_dir, "food_contributors.csv") write_csv(rv$results$food_contributors, f) files_to_zip <- c(files_to_zip, f) } if (!is.null(rv$unmapped_foods) && length(rv$unmapped_foods) > 0) { f <- file.path(temp_dir, "unmapped_foods.csv") write_csv(data.frame(unmapped_food = rv$unmapped_foods), f) files_to_zip <- c(files_to_zip, f) } if (!is.null(rv$results$dii_by_subject)) { f <- file.path(temp_dir, "dii_scores_by_subject.csv") write_csv(rv$results$dii_by_subject, f) files_to_zip <- c(files_to_zip, f) } if (!is.null(rv$results$dii_by_recall)) { f <- file.path(temp_dir, "dii_scores_by_recall.csv") write_csv(rv$results$dii_by_recall, f) files_to_zip <- c(files_to_zip, f) } zip::zip(file, files_to_zip, mode = "cherry-pick") }, contentType = "application/zip" ) }