1 #' @importFrom methods is
2 #' @importFrom utils read.csv write.csv
3 server
<- function(input
, output
) {
4 # Hide the sidebar if the 'About' tab is active.
5 shiny
::observeEvent(input$tabset
, {
6 if (input$tabset
== "About") {
7 shinyjs
::hideElement(selector
="#sidebar")
8 shinyjs
::removeCssClass("main", "col-sm-8")
9 shinyjs
::addCssClass("main", "col-sm-12")
11 shinyjs
::showElement(selector
="#sidebar")
12 shinyjs
::removeCssClass("main", "col-sm-12")
13 shinyjs
::addCssClass("main", "col-sm-8")
17 reactive
<- shiny
::reactiveValues(
18 data_table
=data.frame(Name
=character(0), `Reporting Frequency`
=character(0), `Case Counts`
=numeric(0), check.names
=FALSE),
19 est_table
=data.frame(Dataset
=character(0)),
23 # Validate and add datasets when button is clicked.
24 # Also evaluate the new datasets on existing estimators.
25 shiny
::observeEvent(input$addData
, {
26 # Option 1: Manual entry.
27 if (input$dataInputMethod
== 1) {
30 # Ensure the dataset name is not blank.
31 if (grepl("^\\s*$", input$dataName
)) {
32 output$dataNameWarn
<- shiny
::renderText("Error: The dataset name cannot be blank.")
33 checks_passed
<- FALSE
35 # Ensure the dataset name is not a duplicate.
36 else if (input$dataName
%in% reactive$data_table
[,1]) {
37 output$dataNameWarn
<- shiny
::renderText("Error: There is already a dataset with the specified name.")
38 checks_passed
<- FALSE
41 output$dataNameWarn
<- shiny
::renderText("")
43 # Ensure the case counts consist only of non-negative integers, separated by commas.
44 counts
<- as.numeric(unlist(strsplit(input$dataCounts
, split
=",")))
45 if (any(is.na(counts
)) || any(counts
<= 0) || any(counts
%% 1 != 0)) {
46 output$dataCountsWarn
<- shiny
::renderText("Error: The list of case counts should only contain non-negative integers, separated by commas.")
47 checks_passed
<- FALSE
49 # Ensure the case counts contain at least two entries.
50 else if (length(counts
) < 2) {
51 output$dataCountsWarn
<- shiny
::renderText("Error: The list of case counts should contain at least two entries.")
52 checks_passed
<- FALSE
55 output$dataCountsWarn
<- shiny
::renderText("")
58 d
<- data.frame(input$dataName
, input$dataUnits
, t(counts
))
62 checks_passed
<- FALSE
64 # Option 2: Upload .csv
65 if (input$dataInputMethod
== 2)
66 d
<- try(read.csv(input$dataUpload$datapath
, header
=FALSE))
67 # Option 3: Paste .csv
69 d
<- try(read.csv(text
=input$dataPaste
, header
=FALSE))
71 if (is(d
, "try-error"))
72 output$dataCSVWarn
<- shiny
::renderText("Error reading file.")
73 else if (ncol(d
) < 4 || anyNA(d
[,1]) || anyNA(sapply(d
[,3:4], as.numeric
)) || !all(trimws(d
[,2]) %in% c("Daily", "Weekly")))
74 output$dataCSVWarn
<- shiny
::renderText("Error: The provided .csv file does not match the required format.")
75 else if (length(intersect(reactive$data_table
[,1], d
[,1])) > 0)
76 output$dataCSVWarn
<- shiny
::renderText("Error: The provided .csv file contains dataset names which already exist.")
77 else if (length(unique(d
[,1])) != length(d
[,1]))
78 output$dataCSVWarn
<- shiny
::renderText("Error: The provided .csv file contains duplicate dataset names.")
80 output$dataCSVWarn
<- shiny
::renderText("")
86 d
[,3:ncol(d
)] <- apply(d
[,3:ncol(d
)], 2, as.numeric
)
87 d
[,3] <- data.frame(I(lapply(split(d
[,3:ncol(d
)], 1:nrow(d
)), function(x
) x
[!is.na(x
)])))
89 d
[,2] <- trimws(d
[,2])
90 colnames(d
) <- c("Name", "Reporting Frequency", "Case Counts")
91 reactive$data_table
<- rbind(reactive$data_table
, d
)
92 reactive$est_table
<- update_est_row(input
, output
, d
, reactive$estimators
, reactive$est_table
)
96 output$dataTable
<- shiny
::renderDataTable(reactive$data_table
, escape
=FALSE)
97 output$estTable
<- shiny
::renderDataTable(reactive$est_table
, escape
=FALSE)
99 # Download table of estimates as a .csv file.
100 output$downloadEst
<- shiny
::downloadHandler(
101 filename
=function() { paste0("Rnaught-", Sys.Date(), ".csv") },
102 content
=function(file
) { write.csv(reactive$est_table
, file
) }
105 shiny
::observeEvent(input$addWP
, {
106 if (input$serialWPKnown
== 1) {
107 serial
<- validate_serial(input
, output
, "serialWPInput", "serialWPWarn")
108 if (!is.na(serial
)) {
109 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
)
110 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
, reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
114 checks_passed
<- TRUE
116 grid_length
<- as.numeric(input$gridLengthInput
)
117 max_shape
<- as.numeric(input$gridShapeInput
)
118 max_scale
<- as.numeric(input$gridScaleInput
)
120 if (is.na(grid_length
) || grid_length
<= 0 || grid_length
%% 1 != 0) {
121 output$gridLengthWarn
<- shiny
::renderText("Error: The grid size must be a positive integer.")
122 output$gridShapeWarn
<- shiny
::renderText("")
123 output$gridScaleWarn
<- shiny
::renderText("")
124 checks_passed
<- FALSE
127 output$gridLengthWarn
<- shiny
::renderText("")
129 if (is.na(max_shape
) || max_shape
< 1 / grid_length
) {
130 output$gridShapeWarn
<- shiny
::renderText("Error: The maximum shape must be at least the reciprocal of the grid length.")
131 checks_passed
<- FALSE
134 output$gridShapeWarn
<- shiny
::renderText("")
136 if (is.na(max_scale
) || max_scale
< 1 / grid_length
) {
137 output$gridShapeWarn
<- shiny
::renderText("Error: The maximum scale must be at least the reciprocal of the grid length.")
138 checks_passed
<- FALSE
141 output$gridScaleWarn
<- shiny
::renderText("")
145 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
)
146 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
, reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
151 shiny
::observeEvent(input$addseqB
, {
152 serial
<- validate_serial(input
, output
, "serialseqBInput", "serialseqBWarn")
153 checks_passed
<- !is.na(serial
)
155 kappa
<- as.numeric(input$kappaInput
)
156 if (is.na(kappa
) || kappa
<= 0) {
157 output$kappaWarn
<- shiny
::renderText("Error: The maximum value must be a positive number.")
158 checks_passed
<- FALSE
161 output$kappaWarn
<- shiny
::renderText("")
164 reactive$estimators
[[length(reactive$estimators
)+1]] <- list(method
="seqB", mu
=serial
, kappa
=kappa
, mu_units
=input$serialseqBUnits
)
165 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
, reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
169 shiny
::observeEvent(input$addID
, {
170 serial
<- validate_serial(input
, output
, "serialIDInput", "serialIDWarn")
171 if (!is.na(serial
)) {
172 reactive$estimators
[[length(reactive$estimators
)+1]] <- list(method
="ID", mu
=serial
, mu_units
=input$serialIDUnits
)
173 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
, reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
177 shiny
::observeEvent(input$addIDEA
, {
178 serial
<- validate_serial(input
, output
, "serialIDEAInput", "serialIDEAWarn")
179 if (!is.na(serial
)) {
180 reactive$estimators
[[length(reactive$estimators
)+1]] <- list(method
="IDEA", mu
=serial
, mu_units
=input$serialIDEAUnits
)
181 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
, reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
186 validate_serial
<- function(input
, output
, serialInputId
, serialWarnId
) {
187 serial
<- as.numeric(input
[[serialInputId
]])
188 if (is.na(serial
) || serial
<= 0) {
189 output
[[serialWarnId
]] <- shiny
::renderText("Error: The mean serial interval should be a non-negative number.")
193 output
[[serialWarnId
]] <- shiny
::renderText("") # Clear warning text.
198 update_est_col
<- function(input
, output
, datasets
, estimator
, est_table
) {
199 if (nrow(datasets
) == 0)
200 new_est_table
<- data.frame(matrix(nrow
=0, ncol
=ncol(est_table
)+1))
202 estimates
<- rep(NA, nrow(datasets
))
204 for (row
in 1:nrow(datasets
))
205 estimates
[row
] <- eval_estimator(input
, output
, estimator
, datasets
[row
,])
207 if (nrow(est_table
) == 0)
208 new_est_table
<- cbind(datasets
[,1], estimates
)
210 new_est_table
<- cbind(est_table
, estimates
)
213 colnames(new_est_table
) <- c(colnames(est_table
), shiny
::HTML(paste0(estimator$method
, "<br/>(μ = ", estimator$mu
, " ", tolower(estimator$mu_units
), ")")))
214 return(new_est_table
)
217 update_est_row
<- function(input
, output
, datasets
, estimators
, est_table
) {
218 if (length(estimators
) == 0) {
219 if (nrow(est_table
) == 0)
220 new_est_table
<- data.frame(datasets
[,1])
222 new_est_table
<- data.frame(c(est_table
[,1], datasets
[,1]))
224 colnames(new_est_table
) <- colnames(est_table
)
227 new_est_table
<- data.frame(matrix(nrow
=nrow(datasets
), ncol
=length(estimators
)))
229 for (row
in 1:nrow(datasets
))
230 for (col
in 1:length(estimators
))
231 new_est_table
[row
, col
] <- eval_estimator(input
, output
, estimators
[[col
]], datasets
[row
,])
233 new_est_table
<- cbind(datasets
[,1], new_est_table
)
234 colnames(new_est_table
) <- colnames(est_table
)
235 new_est_table
<- rbind(est_table
, new_est_table
)
238 return(new_est_table
)
241 eval_estimator
<- function(input
, output
, estimator
, dataset
) {
242 # Adjust serial interval to match time unit of case counts.
243 serial
<- estimator$mu
244 if (estimator$mu_units
== "Days" && dataset
[2] == "Weekly")
246 else if (estimator$mu_units
== "Weeks" && dataset
[2] == "Daily")
250 if (estimator$method
== "WP") {
251 estimate
<- WP(unlist(dataset
[3]), mu
=serial
, search
=estimator$search
)
253 if (!is.na(estimator$mu
))
254 estimate
<- round(estimate$Rhat
, 2)
255 # Display the estimated mean of the serial distribution if mu was not specified.
257 if (dataset
[2] == "Daily")
261 MSI
<- sum(estimate$SD$supp
* estimate$SD$pmf
)
262 estimate
<- shiny
::HTML(paste0(round(estimate$Rhat
, 2), "<br/>(μ = ", round(MSI
, 2), " ", mu_units
, ")"))
266 else if (estimator$method
== "seqB")
267 estimate
<- round(seqB(unlist(dataset
[3]), mu
=serial
, kappa
=estimator$kappa
)$Rhat
, 2)
269 else if (estimator$method
== "ID")
270 estimate
<- round(ID(unlist(dataset
[3]), mu
=serial
), 2)
271 # Incidence Decay with Exponential Adjustement
272 else if (estimator$method
== "IDEA")
273 estimate
<- round(IDEA(unlist(dataset
[3]), mu
=serial
), 2)