1 #' @importFrom methods is
2 #' @importFrom utils read.csv write.csv
3 server
<- function(input
, output
) {
4 reactive
<- shiny
::reactiveValues(
5 data_table
= data.frame(Name
= character(0),
6 `Reporting Frequency`
= character(0),
7 `Case Counts`
= numeric(0), check.names
= FALSE),
8 est_table
= data.frame(Dataset
= character(0)),
12 # Validate and add datasets when button is clicked.
13 # Also evaluate the new datasets on existing estimators.
14 shiny
::observeEvent(input$addData
, {
15 # Option 1: Manual entry.
16 if (input$dataInputMethod
== 1) {
19 # Ensure the dataset name is not blank.
20 if (grepl("^\\s*$", input$dataName
)) {
21 output$dataNameWarn
<- shiny
::renderText(
22 "Error: The dataset name cannot be blank.")
23 checks_passed
<- FALSE
25 # Ensure the dataset name is not a duplicate.
26 else if (input$dataName
%in% reactive$data_table
[,1]) {
27 output$dataNameWarn
<- shiny
::renderText(
28 "Error: There is already a dataset with the specified name.")
29 checks_passed
<- FALSE
32 output$dataNameWarn
<- shiny
::renderText("")
34 # Ensure the case counts consist only of positive integers, separated by
36 counts
<- as.numeric(unlist(strsplit(input$dataCounts
, split
= ",")))
37 if (any(is.na(counts
)) || any(counts
<= 0) || any(counts
%% 1 != 0)) {
38 output$dataCountsWarn
<- shiny
::renderText("Error: The list of case
39 counts should only contain positive integers, separated by commas.")
40 checks_passed
<- FALSE
42 # Ensure the case counts contain at least two entries.
43 else if (length(counts
) < 2) {
44 output$dataCountsWarn
<- shiny
::renderText(
45 "Error: The list of case counts should contain at least two entries.")
46 checks_passed
<- FALSE
49 output$dataCountsWarn
<- shiny
::renderText("")
52 d
<- data.frame(input$dataName
, input$dataUnits
, t(counts
))
56 checks_passed
<- FALSE
58 # Option 2: Upload .csv
59 if (input$dataInputMethod
== 2)
60 d
<- try(read.csv(input$dataUpload$datapath
, header
= FALSE))
61 # Option 3: Paste .csv
63 d
<- try(read.csv(text
= input$dataPaste
, header
= FALSE))
65 if (is(d
, "try-error"))
66 output$dataCSVWarn
<- shiny
::renderText("Error reading file.")
67 else if (ncol(d
) < 4 || anyNA(d
[,1]) || anyNA(sapply(d
[,3:4], as.numeric
))
68 || !all(trimws(d
[,2]) %in% c("Daily", "Weekly")))
69 output$dataCSVWarn
<- shiny
::renderText(
70 "Error: The provided .csv file does not match the required format.")
71 else if (length(intersect(reactive$data_table
[,1], d
[,1])) > 0)
72 output$dataCSVWarn
<- shiny
::renderText("Error: The provided .csv file
73 contains dataset names which already exist.")
74 else if (length(unique(d
[,1])) != length(d
[,1]))
75 output$dataCSVWarn
<- shiny
::renderText(
76 "Error: The provided .csv file contains duplicate dataset names.")
78 output$dataCSVWarn
<- shiny
::renderText("")
84 d
[,3:ncol(d
)] <- apply(d
[,3:ncol(d
)], 2, as.numeric
)
85 d
[,3] <- data.frame(I(lapply(split(d
[,3:ncol(d
)], 1:nrow(d
)),
86 function(x
) x
[!is.na(x
)])))
88 d
[,2] <- trimws(d
[,2])
89 colnames(d
) <- c("Name", "Reporting Frequency", "Case Counts")
90 reactive$data_table
<- rbind(reactive$data_table
, d
)
91 reactive$est_table
<- update_est_row(input
, output
, d
,
92 reactive$estimators
, reactive$est_table
)
96 output$dataTable
<- shiny
::renderDataTable(reactive$data_table
,
98 output$estTable
<- shiny
::renderDataTable(reactive$est_table
,
101 # Download table of estimates as a .csv file.
102 output$downloadEst
<- shiny
::downloadHandler(
103 filename
= function() { paste0("Rnaught-", Sys.Date(), ".csv") },
104 content
= function(file
) { write.csv(reactive$est_table
, file
) }
107 shiny
::observeEvent(input$addWP
, {
108 if (input$serialWPKnown
== 1) {
109 serial
<- validate_serial(input
, output
, "serialWPInput", "serialWPWarn")
110 if (!is.na(serial
)) {
111 reactive$estimators
[[length(reactive$estimators
) + 1]] <- list(
112 method
= "WP", mu
= serial
, mu_units
= input$serialWPUnits
,
113 search
= list(B
= 100, shape.max
= 10, scale.max
= 10))
114 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
,
115 reactive$estimators
[[length(reactive$estimators
)]],
120 checks_passed
<- TRUE
122 grid_length
<- as.numeric(input$gridLengthInput
)
123 max_shape
<- as.numeric(input$gridShapeInput
)
124 max_scale
<- as.numeric(input$gridScaleInput
)
126 if (is.na(grid_length
) || grid_length
<= 0 || grid_length
%% 1 != 0) {
127 output$gridLengthWarn
<- shiny
::renderText(
128 "Error: The grid size must be a positive integer.")
129 output$gridShapeWarn
<- shiny
::renderText("")
130 output$gridScaleWarn
<- shiny
::renderText("")
131 checks_passed
<- FALSE
134 output$gridLengthWarn
<- shiny
::renderText("")
136 if (is.na(max_shape
) || max_shape
< 1 / grid_length
) {
137 output$gridShapeWarn
<- shiny
::renderText("Error: The maximum shape
138 must be at least the reciprocal of the grid length.")
139 checks_passed
<- FALSE
142 output$gridShapeWarn
<- shiny
::renderText("")
144 if (is.na(max_scale
) || max_scale
< 1 / grid_length
) {
145 output$gridShapeWarn
<- shiny
::renderText("Error: The maximum scale
146 must be at least the reciprocal of the grid length.")
147 checks_passed
<- FALSE
150 output$gridScaleWarn
<- shiny
::renderText("")
154 reactive$estimators
[[length(reactive$estimators
) + 1]] <- list(
155 method
= "WP", mu
= NA, mu_units
= input$serialWPUnits
,
156 search
= list(B
= grid_length
, shape.max
= max_shape
,
157 scale.max
= max_scale
))
158 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
,
159 reactive$estimators
[[length(reactive$estimators
)]],
165 shiny
::observeEvent(input$addseqB
, {
166 serial
<- validate_serial(input
, output
, "serialseqBInput",
168 checks_passed
<- !is.na(serial
)
170 kappa
<- as.numeric(input$kappaInput
)
171 if (is.na(kappa
) || kappa
<= 0) {
172 output$kappaWarn
<- shiny
::renderText(
173 "Error: The maximum value must be a positive number.")
174 checks_passed
<- FALSE
177 output$kappaWarn
<- shiny
::renderText("")
180 reactive$estimators
[[length(reactive$estimators
) + 1]] <- list(
181 method
="seqB", mu
= serial
, kappa
= kappa
,
182 mu_units
= input$serialseqBUnits
)
183 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
,
184 reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
188 shiny
::observeEvent(input$addID
, {
189 serial
<- validate_serial(input
, output
, "serialIDInput", "serialIDWarn")
190 if (!is.na(serial
)) {
191 reactive$estimators
[[length(reactive$estimators
) + 1]] <- list(
192 method
= "ID", mu
= serial
, mu_units
= input$serialIDUnits
)
193 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
,
194 reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
198 shiny
::observeEvent(input$addIDEA
, {
199 serial
<- validate_serial(input
, output
, "serialIDEAInput",
201 if (!is.na(serial
)) {
202 reactive$estimators
[[length(reactive$estimators
) + 1]] <- list(
203 method
= "IDEA", mu
= serial
, mu_units
= input$serialIDEAUnits
)
204 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
,
205 reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
210 validate_serial
<- function(input
, output
, serialInputId
, serialWarnId
) {
211 serial
<- as.numeric(input
[[serialInputId
]])
212 if (is.na(serial
) || serial
<= 0) {
213 output
[[serialWarnId
]] <- shiny
::renderText(
214 "Error: The mean serial interval should be a positive number.")
218 output
[[serialWarnId
]] <- shiny
::renderText("") # Clear warning text.
223 # Create a new column in the estimator table when a new estimator is added.
224 update_est_col
<- function(input
, output
, datasets
, estimator
, est_table
) {
225 if (nrow(datasets
) == 0)
226 new_est_table
<- data.frame(matrix(nrow
= 0, ncol
= ncol(est_table
) + 1))
228 estimates
<- rep(NA, nrow(datasets
))
230 for (row
in 1:nrow(datasets
))
231 estimates
[row
] <- eval_estimator(input
, output
, estimator
, datasets
[row
,])
233 if (nrow(est_table
) == 0)
234 new_est_table
<- cbind(datasets
[,1], estimates
)
236 new_est_table
<- cbind(est_table
, estimates
)
239 colnames(new_est_table
) <- c(colnames(est_table
), shiny
::HTML(
240 paste0(estimator$method
, "<br/>(μ = ", estimator$mu
, " ",
241 tolower(estimator$mu_units
), ")")))
243 return(new_est_table
)
246 # Create a new row in the estimator table when new datasets are added.
247 update_est_row
<- function(input
, output
, datasets
, estimators
, est_table
) {
248 if (length(estimators
) == 0) {
249 if (nrow(est_table
) == 0)
250 new_est_table
<- data.frame(datasets
[,1])
252 new_est_table
<- data.frame(c(est_table
[,1], datasets
[,1]))
254 colnames(new_est_table
) <- colnames(est_table
)
257 new_est_table
<- data.frame(matrix(nrow
= nrow(datasets
),
258 ncol
= length(estimators
)))
260 for (row
in 1:nrow(datasets
))
261 for (col
in 1:length(estimators
))
262 new_est_table
[row
, col
] <- eval_estimator(input
, output
,
263 estimators
[[col
]], datasets
[row
,])
265 new_est_table
<- cbind(datasets
[,1], new_est_table
)
266 colnames(new_est_table
) <- colnames(est_table
)
267 new_est_table
<- rbind(est_table
, new_est_table
)
270 return(new_est_table
)
273 # Evaluate an estimator on a given dataset.
274 eval_estimator
<- function(input
, output
, estimator
, dataset
) {
275 # Adjust serial interval to match time unit of case counts.
276 serial
<- estimator$mu
277 if (estimator$mu_units
== "Days" && dataset
[2] == "Weekly")
279 else if (estimator$mu_units
== "Weeks" && dataset
[2] == "Daily")
283 if (estimator$method
== "WP") {
284 estimate
<- WP(unlist(dataset
[3]), mu
= serial
, search
= estimator$search
)
286 if (!is.na(estimator$mu
))
287 estimate
<- round(estimate$Rhat
, 2)
288 # Display the estimated mean of the serial distribution if mu was not
291 if (dataset
[2] == "Daily")
295 MSI
<- sum(estimate$SD$supp
* estimate$SD$pmf
)
296 estimate
<- shiny
::HTML(paste0(round(estimate$Rhat
, 2), "<br/>(μ = ",
297 round(MSI
, 2), " ", mu_units
, ")"))
301 else if (estimator$method
== "seqB")
302 estimate
<- round(seqB(unlist(dataset
[3]), mu
= serial
,
303 kappa
= estimator$kappa
)$Rhat
, 2)
305 else if (estimator$method
== "ID")
306 estimate
<- round(ID(unlist(dataset
[3]), mu
= serial
), 2)
307 # Incidence Decay with Exponential Adjustement
308 else if (estimator$method
== "IDEA")
309 estimate
<- round(IDEA(unlist(dataset
[3]), mu
= serial
), 2)