#' @importFrom methods is #' @importFrom utils read.csv write.csv server <- function(input, output) { 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 positive 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 positive 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, mu_units = input$serialWPUnits, search = list(B = 100, shape.max = 10, scale.max = 10)) 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, mu_units = input$serialWPUnits, search = list(B = grid_length, shape.max = max_shape, scale.max = max_scale)) 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 positive number.") serial <- NA } else output[[serialWarnId]] <- shiny::renderText("") # Clear warning text. return(serial) } # Create a new column in the estimator table when a new estimator is added. 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) } # Create a new row in the estimator table when new datasets are added. 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) } # Evaluate an estimator on a given dataset. 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) }