Spaces:
Build error
Build error
#app.R | |
library(shiny) | |
library(BioAge) | |
library(dplyr) | |
#library(RSQLite) | |
library(googlesheets4) | |
library(googledrive) | |
library(shinyvalidate) | |
library(shinyjs) | |
library(plotly) | |
library(lubridate) | |
library(pander) | |
library(tidyr) | |
library(shiny.router) | |
#options(gargle_oauth_cache = ".secrets") | |
createReactiveDataset <- function() { | |
return(reactiveVal(data.frame(date = as.Date(character()), quantity = integer(), item = character()))) | |
} | |
# Define dataset and its reactive variable | |
dataset <- createReactiveDataset() | |
# Dummy dataset for development | |
localDataset <- reactiveVal(data.frame( | |
date = as.Date(c('2024-12-01', '2024-12-02')), | |
quantity = c(10, 20), | |
item = c('Apples', 'Oranges') | |
)) | |
# For local development, bypass authentication | |
isLocal <- TRUE | |
home_route <- route("/", function() { | |
fluidPage( | |
h1("Home Page"), | |
DT::dataTableOutput("dataTable") | |
) | |
}) | |
checkRange <- function(value, min, max){ | |
if(!is.na(value) & (value < min || value > max)){ | |
paste0("Please specify a number that is between ", min, " and ", max, ", thank you!") | |
} else { | |
"" | |
} | |
} | |
search_string = "" | |
query_params <- parseQueryString(search_string) | |
#if !is.null() | |
emiglio = query_params$useremail | |
nomignolo = paste0(query_params$firstname, " ", query_params$lastname) | |
phone_prefix_regex <- "^\\+?\\d{1,3}$" | |
phone_number_regex <- "^[0-9]{10,10}$" | |
validatePhone <- function(value, n_char_min){ | |
if(value != ""){ | |
if(grepl("^[0-9]{1,100}$", value)){ | |
if (nchar(value) == n_char_min) { | |
"" | |
} else{ | |
paste0("Please specify a number that contains ", n_char_min, " digits") | |
} | |
}else{ | |
"Please enter only digits" | |
} | |
} else{ | |
"" | |
} | |
} | |
ui <- fluidPage( | |
tags$head( | |
tags$link(rel = "stylesheet", href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"), | |
tags$style(HTML(" | |
.control-label { | |
color: black !important; | |
} | |
.strongy{ | |
background-color: #c8e6c9; | |
padding: 10px | |
} | |
.form-control{ | |
border-color: grey !important | |
} | |
#loading { | |
display: none; | |
text-align: center; | |
} | |
.loader { | |
border: 6px solid #f3f3f3; /* Light grey */ | |
border-top: 6px solid #3498db; /* Blue */ | |
border-radius: 50%; | |
width: 50px; | |
height: 50px; | |
animation: spin 2s linear infinite; | |
margin: 20px auto; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
")) | |
), | |
useShinyjs(), | |
titlePanel("Biological Age Calculator"), | |
sidebarLayout( | |
sidebarPanel( | |
fluidRow( | |
column(6, textInput("Name", "Enter name", query_params$firstname)), | |
column(6, textInput("Surname", "Enter your last name", query_params$lastname)) | |
), | |
fluidRow( | |
column(6, textInput("phone_prefix", "Enter Country Code", "+91")), | |
column(6, textInput("phone_number", "Enter your phone number", "")) | |
), | |
dateInput("dob", "Date of Birth*", value="1990-01-01"), | |
dateInput("bloodTestDate", "Date of TEST", value = Sys.Date()), | |
numericInput("albumin", "Albumin (g/dL)", value = NA, min = 0), | |
verbatimTextOutput("values"), | |
numericInput("lymph", "Lymphocytes (%)", value = NA, min = 0), | |
numericInput("mcv", "Mean Cell Volume (MCV)", value = NA, min = 0), | |
numericInput("glucose", "Glucose (mg/L)", value = NA, min = 0), | |
numericInput("rdw", "Red Cell Dist Width (RDW)", value = NA, min = 0), | |
numericInput("creat", "Creatinine (mg/dL)", value = NA, min = 0), | |
numericInput("crp", "CRP (mg/L)", value = NA, min = 0), | |
numericInput("alp", "Alkaline Phosphatase (U/L)", value = NA, min = 0), | |
numericInput("wbc", "White Blood Cells (cells/mL)", value = NA, min = 0), | |
actionButton("submit", "Submit", title = "Fill in the phone number field to enable") | |
), | |
mainPanel( | |
tabsetPanel( | |
tabPanel("Calculator", | |
div(id = "loading", class = "loader", style = "display: none"), # Loading spinner | |
uiOutput("results"), | |
uiOutput("message")), | |
tabPanel("My historical data - Plot", | |
uiOutput("notlogged"), | |
actionLink("openLinkButton", "Create an account here / log in here", href = "", style = "background-color:#c8e6c9; padding: 10px; font-size: 2em"), | |
plotOutput("biologicalAgePlot")) | |
, | |
tabPanel("My historical data - Table", | |
tableOutput("table"), | |
actionLink("openLinkButton", "Create an account here / log in here", href = "", style = "background-color:#c8e6c9; padding: 10px; font-size: 2em"), | |
uiOutput("notlogged1")) | |
) | |
) | |
) | |
) | |
server <- function(input, output, session) { | |
sheet_id <- "https://docs.google.com/spreadsheets/d/1xQpghper_5FCWkYFByIpdVKFmBofgM7uVrG_bsaGW58/edit?gid=0#gid=0" | |
values <- reactiveVal(data.frame()) | |
router_server( | |
list(home_route) | |
) | |
# Dummy dataset | |
#dataset <- data.frame(date = as.Date(c('2024-12-01', '2024-12-02')), | |
#quantity = c(10, 20), | |
#item = c('Apples', 'Oranges')) | |
# Render the dataset in a table | |
#output$dataTable <- DT::renderDataTable({ | |
#dataset | |
#}) | |
observeEvent(input$submit, { | |
shinyjs::show(id = "loading") # Show loading spinner | |
# Collecting inputs | |
name <- paste0(input$Name, " ", input$Surname) | |
ageAtTestDate <- round(as.numeric(difftime(Sys.Date(), input$dob, units = "days")) / 365.25, 1) | |
biological_age <- runif(1, min = ageAtTestDate - 5, max = ageAtTestDate + 5) # Simulated calculation | |
bioage_color <- if (biological_age < ageAtTestDate) "green" else if (biological_age > ageAtTestDate) "red" else "yellow" | |
# Show the BioAge Modal | |
show_bioage_modal(name, ageAtTestDate, biological_age, bioage_color) | |
output$results <- renderUI({ | |
paste("Thank you for submitting your details. Your biological age will be calculated based on the provided data.") | |
}) | |
}) | |
# Define show_bioage_modal function outside observeEvent | |
show_bioage_modal <- function(name, ageAtTestDate, biological_age, bioage_color) { | |
diff <- round(abs(biological_age - ageAtTestDate), 2) | |
message <- if (biological_age < ageAtTestDate) { | |
tags$p( | |
paste0( | |
name,", your healthy choices show! your BioAge is ", diff, " years younger than your calendar Age." | |
), | |
style = "padding-bottom: 24px; font-size: 16px; font-weight: bold; font-family: sans-serif; font-variant: contextual;" | |
) | |
} else if (biological_age > ageAtTestDate) { | |
tags$p( | |
paste0( | |
name,", your BioAge is ", diff, " years higher than your Calendar Age. Don’t worry—small, consistent steps in the right direction can help you close the gap!" | |
), | |
style = "padding-bottom: 24px; font-size: 16px; font-weight: bold; font-family: sans-serif; font-variant: contextual;" | |
) | |
} else { | |
tags$p( | |
paste0( | |
name,", your BioAge matches your Calendar Age, indicating balanced health markers." | |
), | |
style = "padding-bottom: 24px; font-size: 16px; font-weight: bold; font-family: sans-serif; font-variant: contextual;" | |
) | |
} | |
showModal( | |
modalDialog( | |
title = NULL, | |
div( | |
style = "background-color: #0E406B; color: white; padding: 24px; text-align: center; | |
border-top-left-radius: 5px; border-top-right-radius: 5px; margin: -15px -15px 0 -15px; font-family: 'Archia', sans-serif;", | |
h3( | |
paste0(name,"'s BioAge Results"), | |
style = "margin: 0; padding: 0; font-size: 26px; text-align: center; font-family: 'Archia', sans-serif; font-variant: common-ligatures; color: white; line-height: 1;" | |
) | |
), | |
div( | |
style = "background-color: white; text-align: center; font-family: 'Archia', sans-serif; padding-top: 20px;", | |
fluidRow( | |
column(5, div( | |
tags$p("Calendar Age", style = "font-weight: bold; font-size: 20px; color: black;"), | |
tags$p(paste(ageAtTestDate, "years"), style = "font-size: 24px; font-weight: bold; color: black;") | |
)), | |
column(2, div( | |
tags$p("→", style = "font-size: 59px; font-weight: bold; color: darkblue; margin-top: 8px;") | |
)), | |
column(5, div( | |
tags$p("BioAge", style = "font-weight: bold; font-size: 20px; color: black;"), | |
tags$p( | |
paste(round(biological_age, 2), "years"), | |
style = sprintf("font-size: 24px; font-weight: bold; color: %s;", bioage_color) | |
) | |
)) | |
) | |
), | |
div( | |
style = "text-align: center; margin-top: 20px; color: #333333; margin-bottom: 0px;", | |
message | |
), | |
div( | |
style = "text-align: center;", | |
tags$p("Complete report sent to your WhatsApp", | |
tags$i(class = "fa-brands fa-whatsapp fa-beat", | |
style = "color: #25D366; margin-left: 5px; font-size: 16px;"), | |
style = "font-size: 14px; color: black; margin-bottom: 0px;"), | |
tags$p("xxxxxxxx4321", style = "font-size: 14px; color: black; margin-bottom: -16px;") | |
), | |
easyClose = FALSE, | |
footer = tags$button( | |
"Close", | |
onclick = "Shiny.setInputValue('close_modal', true);", | |
style = "background-color: #86D1F1; color: black; border: none; padding: 10px 20px; border-radius: 5px; font-size: 16px; cursor: pointer;" | |
) | |
) | |
) | |
} | |
output$message <- renderUI({ | |
HTML("<div class='biological-age-text'> | |
<h2>What is Biological Age?</h2> | |
<p>Biological age refers to how old a person seems based on the functioning and condition of their | |
body systems, rather than the time since birth (chronological age). It is influenced by genetics, | |
lifestyle, and environmental factors, and can provide a more accurate representation of an | |
individual's health and longevity prospects.</p> | |
<h3 class = 'strongy'>Our tool can estimate your biological age using even one blood test parameter. However, for the most accurate results, we recommend including as many parameters as possible.</h3> | |
<h2>Here is what each of the parameters mean for your health:</h2> | |
<ol> | |
<li><strong>Albumin:</strong> A protein in the blood that helps maintain fluid balance and transport hormones, | |
vitamins, and drugs; low levels can indicate liver or kidney disease.</li> | |
<li><strong>Creatinine:</strong> A waste product from muscle metabolism; its blood level is a marker of kidney | |
function, with high levels indicating potential kidney impairment.</li> | |
<li><strong>Glucose:</strong> The main sugar found in the blood and the body's primary energy source; levels | |
are used to diagnose and monitor diabetes.</li> | |
<li><strong>C-reactive Protein (CRP):</strong> A substance produced by the liver in response to inflammation; | |
high CRP levels can indicate infection or chronic inflammatory diseases.</li> | |
<li><strong>Lymphocyte (Lymphs):</strong> A type of white blood cell involved in the immune response; | |
changes in lymphocyte levels can indicate infections, autoimmune diseases, or blood disorders.</li> | |
<li><strong>Mean Cell Volume (MCV):</strong> The average size of red blood cells; it helps diagnose and classify | |
anemias, with high MCV indicating macrocytic anemia and low MCV indicating microcytic | |
anemia.</li> | |
<li><strong>Red Cell Distribution Width (RDW):</strong> A measure of the variation in red blood cell size; | |
increased RDW can indicate anemia and has been associated with cardiovascular diseases.</li> | |
<li><strong>Alkaline Phosphatase:</strong> An enzyme found in the liver, bones, and other tissues; high levels | |
can indicate liver disease, bone disorders, or bile duct obstruction.</li> | |
<li><strong>White Blood Cells (WBCs):</strong> Cells in the immune system that help defend against infections; | |
abnormal levels can indicate infection, inflammation, or immune system disorders.</li> | |
</ol> | |
<h2>Based on scientific data:</h2> | |
<p>We have based this calculation of your biological age on scientific data from the National Health | |
and Nutrition Examination Survey (NHANES). The package uses published biomarker | |
algorithms to calculate three biological aging measures: Klemera-Doubal Method (KDM) | |
biological age, phenotypic age, and homeostatic dysregulation.</p> | |
<h2>Citation:</h2> | |
<p>Kwon, D., Belsky, D.W. A toolkit for quantification of biological age from blood chemistry and organ function | |
test data: BioAge. GeroScience 43, 2795–2808 (2021).</p> | |
<h2>Disclaimer:</h2> | |
<p>These results are solely for informational use and shouldn't replace professional medical advice, | |
diagnosis, or treatment. Always seek the guidance of a healthcare provider for any medical | |
conditions or before making changes to your health regimen, including starting new diets, | |
exercises, or supplements, or altering medication. Never discontinue medication or follow any | |
health advice without consulting your healthcare provider.</p></div>") | |
}) | |
observeEvent(input$submit, { | |
# Handle form submission | |
output$results <- renderUI({ | |
paste("Thank you for submitting your details. Your biological age will be calculated based on the provided data.") | |
}) | |
}) | |
output$notlogged <- renderUI({ | |
if (is.null(emiglio)){ | |
HTML("<div class='biological-age-text'> | |
<h2>Please log in to View your history</h2>") | |
} else { | |
"" | |
} | |
}) | |
output$notlogged1 <- renderUI({ | |
if (is.null(emiglio)){ | |
HTML("<div class='biological-age-text'> | |
<h2>Please log in to View your history</h2>") | |
} else { | |
"" | |
} | |
}) | |
output$biologicalAgePlot <- renderPlot( | |
if (!is.null(emiglio)){ | |
if(nrow(values %>% filter(email == emiglio))>0){ | |
values %>% | |
filter(email == emiglio) %>% | |
select(c(age, age_bio, date)) %>% | |
group_by(date) %>% | |
mutate(age = mean(age, na.rm = T), age_bio = mean(age_bio, na.rm = T)) %>% | |
rename( | |
`Actual age` = age, | |
`Biological age` = age_bio | |
) %>% | |
mutate(Date = as_date(date)) %>% | |
pivot_longer(cols = c(1:2), names_to = "name", values_to = "value") %>% | |
ggplot(aes( | |
x = Date, | |
y = value, | |
color = name | |
) | |
)+ | |
geom_line(size = 1.3)+ | |
theme_bw()+ | |
scale_color_discrete(name = "")+ | |
scale_y_continuous(breaks = round(seq(min(c(values$age, values$age_bio)), max(c(values$age, values$age_bio)), by = 2),0)) | |
} | |
} | |
) | |
output$table <- renderTable( | |
if (!is.null(emiglio)){ | |
if(nrow(values %>% filter(email == emiglio))>0){ | |
values %>% | |
filter(email == emiglio) %>% | |
mutate_at(c(1, 11), round, 1) %>% | |
select(c(15, 2:10, 1, 11)) %>% | |
#arrange(-date) %>% | |
mutate(date = as.character(as.Date(date)))%>% | |
rename( | |
Creatinine = creat, | |
`Lymphocyte (Lymphs)` = lymph, | |
`CRP (C-reactive Protein)` = crp, | |
`White Blood Cells` = wbc, | |
`Mean Cell Volume` = mcv, | |
`Red cells distribution width` = rdw, | |
Glucose = glucose, | |
`Alkaline Phosphatase` = alp, | |
`Actual Age` = age, | |
`Biological Age` = age_bio | |
) | |
} | |
} | |
) | |
observe({ | |
iv <- InputValidator$new() | |
iv$add_rule("albumin", checkRange, 2, 8) | |
iv$add_rule("creat", checkRange, .2, 2) | |
iv$add_rule("rdw", checkRange, 10, 20) | |
iv$add_rule("crp", checkRange, 0, 50) | |
iv$add_rule("lymph", checkRange, 0, 100) | |
iv$add_rule("glucose", checkRange, 40, 400) | |
iv$add_rule("wbc", checkRange, 2, 25) | |
iv$add_rule("alp", checkRange, 0, 300) | |
iv$add_rule("mcv", checkRange, 70, 120) | |
iv$add_rule("phone_number", validatePhone, 10) | |
iv$enable() | |
output$values <- renderPrint({ | |
req(iv$is_valid()) | |
}) | |
}) | |
observeEvent(input$submit, { | |
shinyjs::show(id = "loading") # Show loading spinner | |
nome = paste0(input$Name, " ", input$Surname) | |
ageAtTestDate <- round(as.numeric(difftime(Sys.Date(), input$dob, units = "days")) / 365.25, 1) | |
if (!is.null(input$dob)) { | |
allInputs <- data.frame( | |
age = as.numeric(difftime(Sys.Date(), input$dob, units = "days")) / 365.25, | |
albumin = input$albumin, | |
lymph = input$lymph, | |
mcv = input$mcv, | |
glucose = input$glucose, | |
rdw = input$rdw, | |
creat = input$creat, | |
crp = input$crp, | |
alp = input$alp, | |
wbc = input$wbc | |
) | |
} | |
if (!is.null(input$phone_number) && !is.null(phone_number_regex) && | |
grepl(phone_prefix_regex, input$phone_prefix) && | |
grepl(phone_number_regex, input$phone_number)) { | |
validated_phone <- paste(input$phone_prefix, input$phone_number) | |
} else { | |
validated_phone = NA | |
} | |
mrkrs = colnames(allInputs[, colSums(is.na(allInputs)) < nrow(allInputs)]) | |
train = phenoage_calc(NHANES3, biomarkers = mrkrs) | |
biological_age <- phenoage_calc(allInputs, biomarkers = mrkrs, fit = train$fit)$data$phenoage | |
allInputs_tosave = allInputs %>% | |
mutate( | |
age_bio = biological_age, | |
email = ifelse(is.null(emiglio), "", emiglio), | |
name = nome, | |
phone = validated_phone, | |
date= input$bloodTestDate | |
) | |
biologicalAge <- if (!is.null(input$dob)) { | |
paste("Your biological age is approximately:", round(biological_age, 1), "years") | |
} else { | |
"Please enter at least your date of birth" | |
} | |
observeEvent(input$close_modal, { | |
removeModal() | |
}) | |
output$results <- renderUI( | |
HTML( | |
paste0( | |
"<div style='background-color: #c8e6c9; padding: 10px;'>", | |
"<h2>Hello ", nome, "</h2>", | |
"<p>Your biological age is ", round(biological_age, 1), " years </p>", | |
"<p>Your actual age is ", ageAtTestDate, " years</p></div>" | |
) | |
) | |
) | |
# Append data to Google Sheet | |
if (nrow(allInputs_tosave) == 1) { | |
if (nrow(values()) == 0) { | |
sheet_write(data = allInputs_tosave, ss = sheet_id, sheet = "longevity") | |
} else { | |
sheet_append(data = allInputs_tosave, ss = sheet_id, sheet = "longevity") | |
} | |
} | |
# Update values to reflect the new data | |
values(read_sheet(ss = sheet_id, sheet = "longevity")) | |
shinyjs::hide(id = "loading") # Hide loading spinner | |
# Generate biological age plot | |
output$biologicalAgePlot <- renderPlot({ | |
if (!is.null(emiglio)) { | |
if (nrow(values() %>% filter(email == emiglio)) > 0) { | |
values() %>% | |
filter(email == emiglio) %>% | |
select(c(age, age_bio, date)) %>% | |
group_by(date) %>% | |
mutate(age = mean(age, na.rm = TRUE), age_bio = mean(age_bio, na.rm = TRUE)) %>% | |
rename(`Actual age` = age, `Biological age` = age_bio) %>% | |
mutate(Date = as_date(date)) %>% | |
pivot_longer(cols = c(1:2), names_to = "name", values_to = "value") %>% | |
ggplot(aes(x = Date, y = value, color = name)) + | |
geom_line(size = 1.3) + | |
theme_bw() + | |
scale_color_discrete(name = "") + | |
scale_y_continuous(breaks = round(seq(min(c(values()$age, values()$age_bio)), | |
max(c(values()$age, values()$age_bio)), by = 2), 0)) | |
} | |
} | |
}) | |
# Generate biological age table | |
output$table <- renderTable({ | |
if (!is.null(emiglio)) { | |
if (nrow(values() %>% filter(email == emiglio)) > 0) { | |
values() %>% | |
filter(email == emiglio) %>% | |
mutate_at(c(1, 11), round, 1) %>% | |
select(c(15, 2:10, 1, 11)) %>% | |
mutate(date = as.character(as.Date(date))) %>% | |
rename( | |
Creatinine = creat, | |
`Lymphocyte (Lymphs)` = lymph, | |
`CRP (C-reactive Protein)` = crp, | |
`White Blood Cells` = wbc, | |
`Mean Cell Volume` = mcv, | |
`Alkaline Phosphatase` = alp, | |
Glucose = glucose, | |
`Red cells distribution width` = rdw, | |
`Actual Age` = age, | |
`Biological Age` = age_bio | |
) | |
} | |
} | |
}) | |
}) | |
} | |
shinyApp(ui = ui, server = server) |