richtext's picture
Initial release - 0.1 Alpha
7e54fbe
# ==============================================================================
# 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"
)
}