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 grid_length
= 100, max_shape
= 10, max_scale
= 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$gridScaleWarn
<- 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 grid_length
= grid_length
, max_shape
= max_shape
, max_scale
= max_scale
)
157 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
,
158 reactive$estimators
[[length(reactive$estimators
)]],
164 shiny
::observeEvent(input$addseqB
, {
165 serial
<- validate_serial(input
, output
, "serialseqBInput",
167 checks_passed
<- !is.na(serial
)
169 kappa
<- as.numeric(input$kappaInput
)
170 if (is.na(kappa
) || kappa
<= 0) {
171 output$kappaWarn
<- shiny
::renderText(
172 "Error: The maximum value must be a positive number.")
173 checks_passed
<- FALSE
176 output$kappaWarn
<- shiny
::renderText("")
179 reactive$estimators
[[length(reactive$estimators
) + 1]] <- list(
180 method
="seqB", mu
= serial
, kappa
= kappa
,
181 mu_units
= input$serialseqBUnits
)
182 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
,
183 reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
187 shiny
::observeEvent(input$addID
, {
188 serial
<- validate_serial(input
, output
, "serialIDInput", "serialIDWarn")
189 if (!is.na(serial
)) {
190 reactive$estimators
[[length(reactive$estimators
) + 1]] <- list(
191 method
= "ID", mu
= serial
, mu_units
= input$serialIDUnits
)
192 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
,
193 reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
197 shiny
::observeEvent(input$addIDEA
, {
198 serial
<- validate_serial(input
, output
, "serialIDEAInput",
200 if (!is.na(serial
)) {
201 reactive$estimators
[[length(reactive$estimators
) + 1]] <- list(
202 method
= "IDEA", mu
= serial
, mu_units
= input$serialIDEAUnits
)
203 reactive$est_table
<- update_est_col(input
, output
, reactive$data_table
,
204 reactive$estimators
[[length(reactive$estimators
)]], reactive$est_table
)
209 validate_serial
<- function(input
, output
, serialInputId
, serialWarnId
) {
210 serial
<- as.numeric(input
[[serialInputId
]])
211 if (is.na(serial
) || serial
<= 0) {
212 output
[[serialWarnId
]] <- shiny
::renderText(
213 "Error: The mean serial interval should be a positive number.")
217 output
[[serialWarnId
]] <- shiny
::renderText("") # Clear warning text.
222 # Create a new column in the estimator table when a new estimator is added.
223 update_est_col
<- function(input
, output
, datasets
, estimator
, est_table
) {
224 if (nrow(datasets
) == 0)
225 new_est_table
<- data.frame(matrix(nrow
= 0, ncol
= ncol(est_table
) + 1))
227 estimates
<- rep(NA, nrow(datasets
))
229 for (row
in 1:nrow(datasets
))
230 estimates
[row
] <- eval_estimator(input
, output
, estimator
, datasets
[row
,])
232 if (nrow(est_table
) == 0)
233 new_est_table
<- cbind(datasets
[,1], estimates
)
235 new_est_table
<- cbind(est_table
, estimates
)
238 colnames(new_est_table
) <- c(colnames(est_table
), shiny
::HTML(
239 paste0(estimator$method
, "<br/>(μ = ", estimator$mu
, " ",
240 tolower(estimator$mu_units
), ")")))
242 return(new_est_table
)
245 # Create a new row in the estimator table when new datasets are added.
246 update_est_row
<- function(input
, output
, datasets
, estimators
, est_table
) {
247 if (length(estimators
) == 0) {
248 if (nrow(est_table
) == 0)
249 new_est_table
<- data.frame(datasets
[,1])
251 new_est_table
<- data.frame(c(est_table
[,1], datasets
[,1]))
253 colnames(new_est_table
) <- colnames(est_table
)
256 new_est_table
<- data.frame(matrix(nrow
= nrow(datasets
),
257 ncol
= length(estimators
)))
259 for (row
in 1:nrow(datasets
))
260 for (col
in 1:length(estimators
))
261 new_est_table
[row
, col
] <- eval_estimator(input
, output
,
262 estimators
[[col
]], datasets
[row
,])
264 new_est_table
<- cbind(datasets
[,1], new_est_table
)
265 colnames(new_est_table
) <- colnames(est_table
)
266 new_est_table
<- rbind(est_table
, new_est_table
)
269 return(new_est_table
)
272 # Evaluate an estimator on a given dataset.
273 eval_estimator
<- function(input
, output
, estimator
, dataset
) {
274 # Adjust serial interval to match time unit of case counts.
275 serial
<- estimator$mu
276 if (estimator$mu_units
== "Days" && dataset
[2] == "Weekly")
278 else if (estimator$mu_units
== "Weeks" && dataset
[2] == "Daily")
282 if (estimator$method
== "WP") {
283 estimate
<- wp(unlist(dataset
[3]), mu
= serial
, serial
= TRUE,
284 grid_length
= estimator$grid_length
,
285 max_shape
= estimator$max_shape
,
286 max_scale
= estimator$max_scale
)
288 if (!is.na(estimator$mu
))
289 estimate
<- round(estimate$r0
, 2)
290 # Display the estimated mean of the serial distribution if mu was not
293 if (dataset
[2] == "Daily")
297 MSI
<- sum(estimate$supp
* estimate$pmf
)
298 estimate
<- shiny
::HTML(paste0(round(estimate$r0
, 2), "<br/>(μ = ",
299 round(MSI
, 2), " ", mu_units
, ")"))
303 else if (estimator$method
== "seqB")
304 estimate
<- round(seq_bayes(unlist(dataset
[3]), mu
= serial
,
305 kappa
= estimator$kappa
), 2)
307 else if (estimator$method
== "ID")
308 estimate
<- round(id(unlist(dataset
[3]), mu
= serial
), 2)
309 # Incidence Decay with Exponential Adjustement
310 else if (estimator$method
== "IDEA")
311 estimate
<- round(idea(unlist(dataset
[3]), mu
= serial
), 2)