From 336017bd64d44f851b8c12d9f45359b5c2a6e2c3 Mon Sep 17 00:00:00 2001 From: Naeem Model Date: Sat, 24 Jun 2023 23:37:22 +0000 Subject: Create shiny server --- R/server.R | 276 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 R/server.R diff --git a/R/server.R b/R/server.R new file mode 100644 index 0000000..9929010 --- /dev/null +++ b/R/server.R @@ -0,0 +1,276 @@ +#' @importFrom methods is +#' @importFrom utils read.csv write.csv +server <- function(input, output) { + # Hide the sidebar if the 'About' tab is active. + shiny::observeEvent(input$tabset, { + if (input$tabset == "About") { + shinyjs::hideElement(selector="#sidebar") + shinyjs::removeCssClass("main", "col-sm-8") + shinyjs::addCssClass("main", "col-sm-12") + } else { + shinyjs::showElement(selector="#sidebar") + shinyjs::removeCssClass("main", "col-sm-12") + shinyjs::addCssClass("main", "col-sm-8") + } + }) + + reactive <- shiny::reactiveValues( + data_table=data.frame(Name=character(0), `Reporting Frequency`=character(0), `Case Counts`=numeric(0), check.names=FALSE), + est_table=data.frame(Dataset=character(0)), + estimators=list() + ) + + # Validate and add datasets when button is clicked. + # Also evaluate the new datasets on existing estimators. + shiny::observeEvent(input$addData, { + # Option 1: Manual entry. + if (input$dataInputMethod == 1) { + checks_passed <- TRUE + + # Ensure the dataset name is not blank. + if (grepl("^\\s*$", input$dataName)) { + output$dataNameWarn <- shiny::renderText("Error: The dataset name cannot be blank.") + checks_passed <- FALSE + } + # Ensure the dataset name is not a duplicate. + else if (input$dataName %in% reactive$data_table[,1]) { + output$dataNameWarn <- shiny::renderText("Error: There is already a dataset with the specified name.") + checks_passed <- FALSE + } + else + output$dataNameWarn <- shiny::renderText("") + + # Ensure the case counts consist only of non-negative integers, separated by commas. + counts <- as.numeric(unlist(strsplit(input$dataCounts, split=","))) + if (any(is.na(counts)) || any(counts <= 0) || any(counts %% 1 != 0)) { + output$dataCountsWarn <- shiny::renderText("Error: The list of case counts should only contain non-negative integers, separated by commas.") + checks_passed <- FALSE + } + # Ensure the case counts contain at least two entries. + else if (length(counts) < 2) { + output$dataCountsWarn <- shiny::renderText("Error: The list of case counts should contain at least two entries.") + checks_passed <- FALSE + } + else + output$dataCountsWarn <- shiny::renderText("") + + if (checks_passed) + d <- data.frame(input$dataName, input$dataUnits, t(counts)) + } + + else { + checks_passed <- FALSE + + # Option 2: Upload .csv + if (input$dataInputMethod == 2) + d <- try(read.csv(input$dataUpload$datapath, header=FALSE)) + # Option 3: Paste .csv + else + d <- try(read.csv(text=input$dataPaste, header=FALSE)) + + if (is(d, "try-error")) + output$dataCSVWarn <- shiny::renderText("Error reading file.") + else if (ncol(d) < 4 || anyNA(d[,1]) || anyNA(sapply(d[,3:4], as.numeric)) || !all(trimws(d[,2]) %in% c("Daily", "Weekly"))) + output$dataCSVWarn <- shiny::renderText("Error: The provided .csv file does not match the required format.") + else if (length(intersect(reactive$data_table[,1], d[,1])) > 0) + output$dataCSVWarn <- shiny::renderText("Error: The provided .csv file contains dataset names which already exist.") + else if (length(unique(d[,1])) != length(d[,1])) + output$dataCSVWarn <- shiny::renderText("Error: The provided .csv file contains duplicate dataset names.") + else { + output$dataCSVWarn <- shiny::renderText("") + checks_passed <- TRUE + } + } + + if (checks_passed) { + d[,3:ncol(d)] <- apply(d[,3:ncol(d)], 2, as.numeric) + d[,3] <- data.frame(I(lapply(split(d[,3:ncol(d)], 1:nrow(d)), function(x) x[!is.na(x)]))) + d <- d[,1:3] + d[,2] <- trimws(d[,2]) + colnames(d) <- c("Name", "Reporting Frequency", "Case Counts") + reactive$data_table <- rbind(reactive$data_table, d) + reactive$est_table <- update_est_row(input, output, d, reactive$estimators, reactive$est_table) + } + }) + + output$dataTable <- shiny::renderDataTable(reactive$data_table, escape=FALSE) + output$estTable <- shiny::renderDataTable(reactive$est_table, escape=FALSE) + + # Download table of estimates as a .csv file. + output$downloadEst <- shiny::downloadHandler( + filename=function() { paste0("Rnaught-", Sys.Date(), ".csv") }, + content=function(file) { write.csv(reactive$est_table, file) } + ) + + shiny::observeEvent(input$addWP, { + if (input$serialWPKnown == 1) { + serial <- validate_serial(input, output, "serialWPInput", "serialWPWarn") + if (!is.na(serial)) { + reactive$estimators[[length(reactive$estimators)+1]] <- list(method="WP", mu=serial, search=list(B=100, shape.max=10, scale.max=10), mu_units=input$serialWPUnits) + reactive$est_table <- update_est_col(input, output, reactive$data_table, reactive$estimators[[length(reactive$estimators)]], reactive$est_table) + } + } + else { + checks_passed <- TRUE + + grid_length <- as.numeric(input$gridLengthInput) + max_shape <- as.numeric(input$gridShapeInput) + max_scale <- as.numeric(input$gridScaleInput) + + if (is.na(grid_length) || grid_length <= 0 || grid_length %% 1 != 0) { + output$gridLengthWarn <- shiny::renderText("Error: The grid size must be a positive integer.") + output$gridShapeWarn <- shiny::renderText("") + output$gridScaleWarn <- shiny::renderText("") + checks_passed <- FALSE + } + else { + output$gridLengthWarn <- shiny::renderText("") + + if (is.na(max_shape) || max_shape < 1 / grid_length) { + output$gridShapeWarn <- shiny::renderText("Error: The maximum shape must be at least the reciprocal of the grid length.") + checks_passed <- FALSE + } + else + output$gridShapeWarn <- shiny::renderText("") + + if (is.na(max_scale) || max_scale < 1 / grid_length) { + output$gridShapeWarn <- shiny::renderText("Error: The maximum scale must be at least the reciprocal of the grid length.") + checks_passed <- FALSE + } + else + output$gridScaleWarn <- shiny::renderText("") + } + + if (checks_passed) { + reactive$estimators[[length(reactive$estimators)+1]] <- list(method="WP", mu=NA, search=list(B=grid_length, shape.max=max_shape, scale.max=max_scale), mu_units=input$serialWPUnits) + reactive$est_table <- update_est_col(input, output, reactive$data_table, reactive$estimators[[length(reactive$estimators)]], reactive$est_table) + } + } + }) + + shiny::observeEvent(input$addseqB, { + serial <- validate_serial(input, output, "serialseqBInput", "serialseqBWarn") + checks_passed <- !is.na(serial) + + kappa <- as.numeric(input$kappaInput) + if (is.na(kappa) || kappa <= 0) { + output$kappaWarn <- shiny::renderText("Error: The maximum value must be a positive number.") + checks_passed <- FALSE + } + else + output$kappaWarn <- shiny::renderText("") + + if (checks_passed) { + reactive$estimators[[length(reactive$estimators)+1]] <- list(method="seqB", mu=serial, kappa=kappa, mu_units=input$serialseqBUnits) + reactive$est_table <- update_est_col(input, output, reactive$data_table, reactive$estimators[[length(reactive$estimators)]], reactive$est_table) + } + }) + + shiny::observeEvent(input$addID, { + serial <- validate_serial(input, output, "serialIDInput", "serialIDWarn") + if (!is.na(serial)) { + reactive$estimators[[length(reactive$estimators)+1]] <- list(method="ID", mu=serial, mu_units=input$serialIDUnits) + reactive$est_table <- update_est_col(input, output, reactive$data_table, reactive$estimators[[length(reactive$estimators)]], reactive$est_table) + } + }) + + shiny::observeEvent(input$addIDEA, { + serial <- validate_serial(input, output, "serialIDEAInput", "serialIDEAWarn") + if (!is.na(serial)) { + reactive$estimators[[length(reactive$estimators)+1]] <- list(method="IDEA", mu=serial, mu_units=input$serialIDEAUnits) + reactive$est_table <- update_est_col(input, output, reactive$data_table, reactive$estimators[[length(reactive$estimators)]], reactive$est_table) + } + }) +} + +validate_serial <- function(input, output, serialInputId, serialWarnId) { + serial <- as.numeric(input[[serialInputId]]) + if (is.na(serial) || serial <= 0) { + output[[serialWarnId]] <- shiny::renderText("Error: The mean serial interval should be a non-negative number.") + serial <- NA + } + else + output[[serialWarnId]] <- shiny::renderText("") # Clear warning text. + + return(serial) +} + +update_est_col <- function(input, output, datasets, estimator, est_table) { + if (nrow(datasets) == 0) + new_est_table <- data.frame(matrix(nrow=0, ncol=ncol(est_table)+1)) + else { + estimates <- rep(NA, nrow(datasets)) + + for (row in 1:nrow(datasets)) + estimates[row] <- eval_estimator(input, output, estimator, datasets[row,]) + + if (nrow(est_table) == 0) + new_est_table <- cbind(datasets[,1], estimates) + else + new_est_table <- cbind(est_table, estimates) + } + + colnames(new_est_table) <- c(colnames(est_table), shiny::HTML(paste0(estimator$method, "
(μ = ", estimator$mu, " ", tolower(estimator$mu_units), ")"))) + return(new_est_table) +} + +update_est_row <- function(input, output, datasets, estimators, est_table) { + if (length(estimators) == 0) { + if (nrow(est_table) == 0) + new_est_table <- data.frame(datasets[,1]) + else + new_est_table <- data.frame(c(est_table[,1], datasets[,1])) + + colnames(new_est_table) <- colnames(est_table) + } + else { + new_est_table <- data.frame(matrix(nrow=nrow(datasets), ncol=length(estimators))) + + for (row in 1:nrow(datasets)) + for (col in 1:length(estimators)) + new_est_table[row, col] <- eval_estimator(input, output, estimators[[col]], datasets[row,]) + + new_est_table <- cbind(datasets[,1], new_est_table) + colnames(new_est_table) <- colnames(est_table) + new_est_table <- rbind(est_table, new_est_table) + } + + return(new_est_table) +} + +eval_estimator <- function(input, output, estimator, dataset) { + # Adjust serial interval to match time unit of case counts. + serial <- estimator$mu + if (estimator$mu_units == "Days" && dataset[2] == "Weekly") + serial <- serial / 7 + else if (estimator$mu_units == "Weeks" && dataset[2] == "Daily") + serial <- serial * 7 + + # White and Panago + if (estimator$method == "WP") { + estimate <- WP(unlist(dataset[3]), mu=serial, search=estimator$search) + + if (!is.na(estimator$mu)) + estimate <- round(estimate$Rhat, 2) + # Display the estimated mean of the serial distribution if mu was not specified. + else { + if (dataset[2] == "Daily") + mu_units <- "days" + else + mu_units <- "weeks" + MSI <- sum(estimate$SD$supp * estimate$SD$pmf) + estimate <- shiny::HTML(paste0(round(estimate$Rhat, 2), "
(μ = ", round(MSI, 2), " ", mu_units, ")")) + } + } + # Sequential Bayes + else if (estimator$method == "seqB") + estimate <- round(seqB(unlist(dataset[3]), mu=serial, kappa=estimator$kappa)$Rhat, 2) + # Incidence Decay + else if (estimator$method == "ID") + estimate <- round(ID(unlist(dataset[3]), mu=serial), 2) + # Incidence Decay with Exponential Adjustement + else if (estimator$method == "IDEA") + estimate <- round(IDEA(unlist(dataset[3]), mu=serial), 2) + + return(estimate) +} -- cgit v1.2.3