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) |