1 |
#' @title `FilterStates` subclass for MultiAssayExperiments |
|
2 |
#' @description Handles filter states in a `MultiAssayExperiment` |
|
3 |
#' @keywords internal |
|
4 |
#' |
|
5 |
#' |
|
6 |
MAEFilterStates <- R6::R6Class( # nolint |
|
7 |
classname = "MAEFilterStates", |
|
8 |
inherit = FilterStates, |
|
9 | ||
10 |
# public methods ---- |
|
11 |
public = list( |
|
12 |
#' @description Initializes `MAEFilterStates` object |
|
13 |
#' |
|
14 |
#' Initialize `MAEFilterStates` object |
|
15 |
#' |
|
16 |
#' @param dataname (`character(1)`)\cr |
|
17 |
#' name of the data used in the expression |
|
18 |
#' specified to the function argument attached to this `FilterStates`. |
|
19 |
#' |
|
20 |
#' @param datalabel (`character(0)` or `character(1)`)\cr |
|
21 |
#' text label value. |
|
22 |
#' |
|
23 |
#' @param varlabels (`character`)\cr |
|
24 |
#' labels of the variables used in this object |
|
25 |
#' |
|
26 |
#' @param keys (`character`)\cr |
|
27 |
#' key columns names |
|
28 |
initialize = function(dataname, datalabel, varlabels, keys) { |
|
29 | 53x |
if (!requireNamespace("MultiAssayExperiment", quietly = TRUE)) { |
30 | ! |
stop("Cannot load MultiAssayExperiment - please install the package or restart your session.") |
31 |
} |
|
32 | 53x |
super$initialize(dataname, datalabel) |
33 | 53x |
private$keys <- keys |
34 | 53x |
private$varlabels <- varlabels |
35 | 53x |
private$state_list <- list( |
36 | 53x |
y = reactiveVal() |
37 |
) |
|
38 | 53x |
return(invisible(self)) |
39 |
}, |
|
40 | ||
41 |
#' @description |
|
42 |
#' Returns the formatted string representing this `MAEFilterStates` object. |
|
43 |
#' |
|
44 |
#' @param indent (`numeric(1)`) the number of spaces before each line of the representation |
|
45 |
#' @return `character(1)` the formatted string |
|
46 |
format = function(indent = 0) { |
|
47 | 7x |
checkmate::assert_number(indent, finite = TRUE, lower = 0) |
48 | ||
49 | 6x |
if (length(self$state_list_get(1L)) > 0) { |
50 | 5x |
formatted_states <- sprintf("%sSubject filters:", format("", width = indent)) |
51 | 5x |
for (state in self$state_list_get(1L)) { |
52 | 14x |
formatted_states <- c(formatted_states, state$format(indent = indent + 2)) |
53 |
} |
|
54 | 5x |
paste(formatted_states, collapse = "\n") |
55 |
} |
|
56 |
}, |
|
57 | ||
58 |
#' @description |
|
59 |
#' Returns function name used to create filter call. |
|
60 |
#' For `MAEFilterStates` `MultiAssayExperiment::subsetByColData` is used. |
|
61 |
#' @return `character(1)` |
|
62 |
get_fun = function() { |
|
63 | 11x |
"MultiAssayExperiment::subsetByColData" |
64 |
}, |
|
65 | ||
66 |
#' @description |
|
67 |
#' Server module |
|
68 |
#' @param id (`character(1)`)\cr |
|
69 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
70 |
#' @return `moduleServer` function which returns `NULL` |
|
71 |
server = function(id) { |
|
72 | 1x |
moduleServer( |
73 | 1x |
id = id, |
74 | 1x |
function(input, output, session) { |
75 | 1x |
previous_state <- reactiveVal(isolate(self$state_list_get("y"))) |
76 | 1x |
added_state_name <- reactiveVal(character(0)) |
77 | 1x |
removed_state_name <- reactiveVal(character(0)) |
78 | ||
79 | 1x |
observeEvent(self$state_list_get("y"), { |
80 | 2x |
added_state_name(setdiff(names(self$state_list_get("y")), names(previous_state()))) |
81 | 2x |
removed_state_name(setdiff(names(previous_state()), names(self$state_list_get("y")))) |
82 | ||
83 | 2x |
previous_state(self$state_list_get("y")) |
84 |
}) |
|
85 | ||
86 | 1x |
observeEvent(added_state_name(), ignoreNULL = TRUE, { |
87 | 1x |
fstates <- self$state_list_get("y") |
88 | 1x |
html_ids <- private$map_vars_to_html_ids(names(fstates)) |
89 | 1x |
for (fname in added_state_name()) { |
90 | ! |
private$insert_filter_state_ui( |
91 | ! |
id = html_ids[fname], |
92 | ! |
filter_state = fstates[[fname]], |
93 | ! |
state_list_index = "y", |
94 | ! |
state_id = fname |
95 |
) |
|
96 |
} |
|
97 | 1x |
added_state_name(character(0)) |
98 |
}) |
|
99 | ||
100 | 1x |
observeEvent(removed_state_name(), { |
101 | 3x |
req(removed_state_name()) |
102 | 1x |
for (fname in removed_state_name()) { |
103 | 3x |
private$remove_filter_state_ui("y", fname, .input = input) |
104 |
} |
|
105 | 1x |
removed_state_name(character(0)) |
106 |
}) |
|
107 | 1x |
NULL |
108 |
} |
|
109 |
) |
|
110 |
}, |
|
111 | ||
112 |
#' @description |
|
113 |
#' Returns active `FilterState` objects. |
|
114 |
#' |
|
115 |
#' Gets all active filters from this dataset in form of the nested list. |
|
116 |
#' The output list can be used as input to `self$set_filter_state`. |
|
117 |
#' |
|
118 |
#' @return `list` with elements number equal number of `FilterStates`. |
|
119 |
get_filter_state = function() { |
|
120 | 11x |
lapply(self$state_list_get(state_list_index = "y"), function(x) x$get_state()) |
121 |
}, |
|
122 | ||
123 |
#' @description |
|
124 |
#' Set filter state |
|
125 |
#' |
|
126 |
#' @param data (`MultiAssayExperiment`)\cr |
|
127 |
#' data which are supposed to be filtered. |
|
128 |
#' @param state (`named list`)\cr |
|
129 |
#' should contain values which are initial selection in the `FilterState`. |
|
130 |
#' Names of the `list` element should correspond to the name of the |
|
131 |
#' column in `colData(data)`. |
|
132 |
#' @param ... ignored. |
|
133 |
#' @return `NULL` |
|
134 |
set_filter_state = function(data, state, ...) { |
|
135 | 17x |
checkmate::assert_class(data, "MultiAssayExperiment") |
136 | 17x |
checkmate::assert( |
137 | 17x |
checkmate::check_subset(names(state), names(SummarizedExperiment::colData(data))), |
138 | 17x |
checkmate::check_class(state, "default_filter"), |
139 | 17x |
combine = "or" |
140 |
) |
|
141 | 15x |
logger::log_trace("MAEFilterState$set_filter_state initializing, dataname: { private$dataname }") |
142 | 15x |
filter_states <- self$state_list_get("y") |
143 | 15x |
for (varname in names(state)) { |
144 | 42x |
value <- resolve_state(state[[varname]]) |
145 | 42x |
if (varname %in% names(filter_states)) { |
146 | 1x |
fstate <- filter_states[[varname]] |
147 | 1x |
fstate$set_state(value) |
148 |
} else { |
|
149 | 41x |
fstate <- init_filter_state( |
150 | 41x |
SummarizedExperiment::colData(data)[[varname]], |
151 | 41x |
varname = varname, |
152 | 41x |
varlabel = private$get_varlabels(varname), |
153 | 41x |
dataname = private$dataname, |
154 | 41x |
extract_type = "list" |
155 |
) |
|
156 | 41x |
fstate$set_state(value) |
157 | 41x |
fstate$set_na_rm(TRUE) |
158 | 41x |
self$state_list_push( |
159 | 41x |
x = fstate, |
160 | 41x |
state_list_index = "y", |
161 | 41x |
state_id = varname |
162 |
) |
|
163 |
} |
|
164 |
} |
|
165 | 15x |
logger::log_trace("MAEFilterState$set_filter_state initialized, dataname: { private$dataname }") |
166 | 15x |
NULL |
167 |
}, |
|
168 | ||
169 |
#' @description |
|
170 |
#' Removes a variable from the `state_list` and its corresponding UI element. |
|
171 |
#' |
|
172 |
#' @param state_id (`character(1)`)\cr name of `state_list` element. |
|
173 |
#' |
|
174 |
#' @return `NULL` |
|
175 |
#' |
|
176 |
remove_filter_state = function(state_id) { |
|
177 | 3x |
logger::log_trace( |
178 | 3x |
sprintf( |
179 | 3x |
"%s$remove_filter_state for %s called, dataname: %s", |
180 | 3x |
class(self)[1], |
181 | 3x |
state_id, |
182 | 3x |
private$dataname |
183 |
) |
|
184 |
) |
|
185 | ||
186 | 3x |
if (!state_id %in% names(self$state_list_get("y"))) { |
187 | 1x |
warning(paste( |
188 | 1x |
"Variable:", state_id, |
189 | 1x |
"is not present in the actual active filters of dataset: { private$dataname }", |
190 | 1x |
"therefore no changes are applied." |
191 |
)) |
|
192 | 1x |
logger::log_warn( |
193 | 1x |
paste( |
194 | 1x |
"Variable:", state_id, "is not present in the actual active filters of dataset:", |
195 | 1x |
"{ private$dataname } therefore no changes are applied." |
196 |
) |
|
197 |
) |
|
198 |
} else { |
|
199 | 2x |
self$state_list_remove(state_list_index = "y", state_id = state_id) |
200 | 2x |
logger::log_trace( |
201 | 2x |
sprintf( |
202 | 2x |
"%s$remove_filter_state for variable %s done, dataname: %s", |
203 | 2x |
class(self)[1], |
204 | 2x |
state_id, |
205 | 2x |
private$dataname |
206 |
) |
|
207 |
) |
|
208 |
} |
|
209 |
}, |
|
210 | ||
211 |
# shiny modules ---- |
|
212 | ||
213 |
#' @description |
|
214 |
#' Shiny UI module to add filter variable |
|
215 |
#' @param id (`character(1)`)\cr |
|
216 |
#' id of shiny module |
|
217 |
#' @param data (`MultiAssayExperiment`)\cr |
|
218 |
#' object containing `colData` which columns are used to be used |
|
219 |
#' to choose filter variables |
|
220 |
#' |
|
221 |
#' @return `shiny.tag` |
|
222 |
#' |
|
223 |
ui_add_filter_state = function(id, data) { |
|
224 | 2x |
checkmate::assert_string(id) |
225 | 2x |
stopifnot(is(data, "MultiAssayExperiment")) |
226 | ||
227 | 2x |
ns <- NS(id) |
228 | ||
229 | 2x |
if (ncol(SummarizedExperiment::colData(data)) == 0) { |
230 | 1x |
div("no sample variables available") |
231 | 1x |
} else if (nrow(SummarizedExperiment::colData(data)) == 0) { |
232 | 1x |
div("no samples available") |
233 |
} else { |
|
234 | ! |
teal.widgets::optionalSelectInput( |
235 | ! |
ns("var_to_add"), |
236 | ! |
choices = NULL, |
237 | ! |
options = shinyWidgets::pickerOptions( |
238 | ! |
liveSearch = TRUE, |
239 | ! |
noneSelectedText = "Select subject variable" |
240 |
) |
|
241 |
) |
|
242 |
} |
|
243 |
}, |
|
244 | ||
245 |
#' @description |
|
246 |
#' Shiny server module to add filter variable. |
|
247 |
#' |
|
248 |
#' Module controls available choices to select as a filter variable. |
|
249 |
#' Selected filter variable is being removed from available choices. |
|
250 |
#' Removed filter variable gets back to available choices. |
|
251 |
#' |
|
252 |
#' @param id (`character(1)`)\cr |
|
253 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
254 |
#' @param data (`MultiAssayExperiment`)\cr |
|
255 |
#' object containing `colData` which columns are used to choose filter variables in |
|
256 |
#' [teal.widgets::optionalSelectInput()]. |
|
257 |
#' @param ... ignored |
|
258 |
#' |
|
259 |
#' @return `moduleServer` function which returns `NULL` |
|
260 |
#' |
|
261 |
srv_add_filter_state = function(id, data, ...) { |
|
262 | ! |
stopifnot(is(data, "MultiAssayExperiment")) |
263 | ! |
check_ellipsis(..., stop = FALSE) |
264 | ! |
moduleServer( |
265 | ! |
id = id, |
266 | ! |
function(input, output, session) { |
267 | ! |
logger::log_trace( |
268 | ! |
"MAEFilterState$srv_add_filter_state initializing, dataname: { private$dataname }" |
269 |
) |
|
270 | ! |
shiny::setBookmarkExclude("var_to_add") |
271 | ! |
active_filter_vars <- reactive({ |
272 | ! |
vapply( |
273 | ! |
X = self$state_list_get(state_list_index = "y"), |
274 | ! |
FUN.VALUE = character(1), |
275 | ! |
FUN = function(x) x$get_varname() |
276 |
) |
|
277 |
}) |
|
278 | ||
279 |
# available choices to display |
|
280 | ! |
avail_column_choices <- reactive({ |
281 | ! |
choices <- setdiff( |
282 | ! |
get_supported_filter_varnames(data = SummarizedExperiment::colData(data)), |
283 | ! |
active_filter_vars() |
284 |
) |
|
285 | ! |
data_choices_labeled( |
286 | ! |
data = SummarizedExperiment::colData(data), |
287 | ! |
choices = choices, |
288 | ! |
varlabels = private$get_varlabels(choices), |
289 | ! |
keys = private$keys |
290 |
) |
|
291 |
}) |
|
292 | ! |
observeEvent( |
293 | ! |
avail_column_choices(), |
294 | ! |
ignoreNULL = TRUE, |
295 | ! |
handlerExpr = { |
296 | ! |
logger::log_trace(paste( |
297 | ! |
"MAEFilterStates$srv_add_filter_state@1 updating available column choices,", |
298 | ! |
"dataname: { private$dataname }" |
299 |
)) |
|
300 | ! |
if (is.null(avail_column_choices())) { |
301 | ! |
shinyjs::hide("var_to_add") |
302 |
} else { |
|
303 | ! |
shinyjs::show("var_to_add") |
304 |
} |
|
305 | ! |
teal.widgets::updateOptionalSelectInput( |
306 | ! |
session, |
307 | ! |
"var_to_add", |
308 | ! |
choices = avail_column_choices() |
309 |
) |
|
310 | ! |
logger::log_trace(paste( |
311 | ! |
"MAEFilterStates$srv_add_filter_state@1 updated available column choices,", |
312 | ! |
"dataname: { private$dataname }" |
313 |
)) |
|
314 |
} |
|
315 |
) |
|
316 | ||
317 | ! |
observeEvent( |
318 | ! |
eventExpr = input$var_to_add, |
319 | ! |
handlerExpr = { |
320 | ! |
logger::log_trace( |
321 | ! |
sprintf( |
322 | ! |
"MAEFilterStates$srv_add_filter_state@2 adding FilterState of variable %s, dataname: %s", |
323 | ! |
deparse1(input$var_to_add), |
324 | ! |
private$dataname |
325 |
) |
|
326 |
) |
|
327 | ! |
fstate <- init_filter_state( |
328 | ! |
SummarizedExperiment::colData(data)[[input$var_to_add]], |
329 | ! |
varname = input$var_to_add, |
330 | ! |
varlabel = private$get_varlabels(input$var_to_add), |
331 | ! |
dataname = private$dataname, |
332 | ! |
extract_type = "list" |
333 |
) |
|
334 | ! |
fstate$set_na_rm(TRUE) |
335 | ||
336 | ! |
self$state_list_push( |
337 | ! |
x = fstate, |
338 | ! |
state_list_index = "y", |
339 | ! |
state_id = input$var_to_add |
340 |
) |
|
341 | ! |
logger::log_trace( |
342 | ! |
sprintf( |
343 | ! |
"MAEFilterStates$srv_add_filter_state@2 added FilterState of variable %s, dataname: %s", |
344 | ! |
deparse1(input$var_to_add), |
345 | ! |
private$dataname |
346 |
) |
|
347 |
) |
|
348 |
} |
|
349 |
) |
|
350 | ||
351 | ! |
logger::log_trace( |
352 | ! |
"MAEFilterState$srv_add_filter_state initialized, dataname: { private$dataname }" |
353 |
) |
|
354 | ! |
NULL |
355 |
} |
|
356 |
) |
|
357 |
} |
|
358 |
), |
|
359 | ||
360 |
# private members ---- |
|
361 |
private = list( |
|
362 |
varlabels = character(0), |
|
363 |
keys = character(0), |
|
364 |
#' description |
|
365 |
#' Get label of specific variable. In case when variable label is missing |
|
366 |
#' name of the variable is returned. |
|
367 |
#' parameter variable (`character`)\cr |
|
368 |
#' name of the variable for which label should be returned |
|
369 |
#' return `character` |
|
370 |
get_varlabels = function(variables = character(0)) { |
|
371 | 41x |
checkmate::assert_character(variables) |
372 | 41x |
if (identical(variables, character(0))) { |
373 | ! |
private$varlabels |
374 |
} else { |
|
375 | 41x |
varlabels <- private$varlabels[variables] |
376 | 41x |
missing_labels <- is.na(varlabels) | varlabels == "" |
377 | 41x |
varlabels[missing_labels] <- variables[missing_labels] |
378 | 41x |
varlabels |
379 |
} |
|
380 |
} |
|
381 |
) |
|
382 |
) |
1 |
#' @title `FilterStates` subclass for SummarizedExperiments |
|
2 |
#' @description Handles filter states in a `SummaryExperiment` |
|
3 |
#' @keywords internal |
|
4 |
#' |
|
5 |
#' |
|
6 |
SEFilterStates <- R6::R6Class( # nolint |
|
7 |
classname = "SEFilterStates", |
|
8 |
inherit = FilterStates, |
|
9 | ||
10 |
# public methods ---- |
|
11 |
public = list( |
|
12 |
#' @description Initialize `SEFilterStates` object |
|
13 |
#' |
|
14 |
#' Initialize `SEFilterStates` object |
|
15 |
#' |
|
16 |
#' @param dataname (`character(1)`)\cr |
|
17 |
#' name of the data used in the expression |
|
18 |
#' specified to the function argument attached to this `FilterStates`. |
|
19 |
#' |
|
20 |
#' @param datalabel (`character(0)` or `character(1)`)\cr |
|
21 |
#' text label value. |
|
22 |
initialize = function(dataname, datalabel) { |
|
23 | 146x |
if (!requireNamespace("SummarizedExperiment", quietly = TRUE)) { |
24 | ! |
stop("Cannot load SummarizedExperiment - please install the package or restart your session.") |
25 |
} |
|
26 | 146x |
super$initialize(dataname, datalabel) |
27 | 146x |
private$state_list <- list( |
28 | 146x |
subset = reactiveVal(), |
29 | 146x |
select = reactiveVal() |
30 |
) |
|
31 |
}, |
|
32 | ||
33 |
#' @description |
|
34 |
#' Returns the formatted string representing this `MAEFilterStates` object. |
|
35 |
#' |
|
36 |
#' @param indent (`numeric(1)`) the number of spaces before each line of the representation |
|
37 |
#' @return `character(1)` the formatted string |
|
38 |
format = function(indent = 0) { |
|
39 | 19x |
checkmate::assert_number(indent, finite = TRUE, lower = 0) |
40 | ||
41 | 18x |
whitespace_indent <- format("", width = indent) |
42 | 18x |
formatted_states <- c() |
43 | 18x |
if (!is.null(self$state_list_get(state_list_index = "subset"))) { |
44 | 5x |
formatted_states <- c(formatted_states, paste0(whitespace_indent, " Subsetting:")) |
45 | 5x |
for (state in self$state_list_get(state_list_index = "subset")) { |
46 | 5x |
formatted_states <- c(formatted_states, state$format(indent = indent + 4)) |
47 |
} |
|
48 |
} |
|
49 | ||
50 | 18x |
if (!is.null(self$state_list_get(state_list_index = "select"))) { |
51 | 1x |
formatted_states <- c(formatted_states, paste0(whitespace_indent, " Selecting:")) |
52 | 1x |
for (state in self$state_list_get(state_list_index = "select")) { |
53 | 1x |
formatted_states <- c(formatted_states, state$format(indent = indent + 4)) |
54 |
} |
|
55 |
} |
|
56 | ||
57 | 18x |
if (length(formatted_states) > 0) { |
58 | 5x |
formatted_states <- c(paste0(whitespace_indent, "Assay ", self$get_datalabel(), " filters:"), formatted_states) |
59 | 5x |
paste(formatted_states, collapse = "\n") |
60 |
} |
|
61 |
}, |
|
62 | ||
63 |
#' @description |
|
64 |
#' Server module |
|
65 |
#' @param id (`character(1)`)\cr |
|
66 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
67 |
#' @return `moduleServer` function which returns `NULL` |
|
68 |
server = function(id) { |
|
69 | 4x |
moduleServer( |
70 | 4x |
id = id, |
71 | 4x |
function(input, output, session) { |
72 | 4x |
previous_state_subset <- reactiveVal(isolate(self$state_list_get("subset"))) |
73 | 4x |
added_state_name_subset <- reactiveVal(character(0)) |
74 | 4x |
removed_state_name_subset <- reactiveVal(character(0)) |
75 | ||
76 | 4x |
observeEvent(self$state_list_get("subset"), { |
77 | 5x |
added_state_name_subset( |
78 | 5x |
setdiff(names(self$state_list_get("subset")), names(previous_state_subset())) |
79 |
) |
|
80 | 5x |
removed_state_name_subset( |
81 | 5x |
setdiff(names(previous_state_subset()), names(self$state_list_get("subset"))) |
82 |
) |
|
83 | 5x |
previous_state_subset(self$state_list_get("subset")) |
84 |
}) |
|
85 | ||
86 | 4x |
observeEvent(added_state_name_subset(), ignoreNULL = TRUE, { |
87 | 4x |
fstates <- self$state_list_get("subset") |
88 | 4x |
html_ids <- private$map_vars_to_html_ids(keys = names(fstates), prefix = "rowData") |
89 | 4x |
for (fname in added_state_name_subset()) { |
90 | ! |
private$insert_filter_state_ui( |
91 | ! |
id = html_ids[fname], |
92 | ! |
filter_state = fstates[[fname]], |
93 | ! |
state_list_index = "subset", |
94 | ! |
state_id = fname |
95 |
) |
|
96 |
} |
|
97 | 4x |
added_state_name_subset(character(0)) |
98 |
}) |
|
99 | ||
100 | 4x |
observeEvent(removed_state_name_subset(), { |
101 | 6x |
req(removed_state_name_subset()) |
102 | 1x |
for (fname in removed_state_name_subset()) { |
103 | 1x |
private$remove_filter_state_ui("subset", fname, .input = input) |
104 |
} |
|
105 | 1x |
removed_state_name_subset(character(0)) |
106 |
}) |
|
107 | ||
108 |
# select |
|
109 | 4x |
previous_state_select <- reactiveVal(isolate(self$state_list_get("select"))) |
110 | 4x |
added_state_name_select <- reactiveVal(character(0)) |
111 | 4x |
removed_state_name_select <- reactiveVal(character(0)) |
112 | ||
113 | 4x |
observeEvent(self$state_list_get("select"), { |
114 |
# find what has been added or removed |
|
115 | 4x |
added_state_name_select( |
116 | 4x |
setdiff(names(self$state_list_get("select")), names(previous_state_select())) |
117 |
) |
|
118 | 4x |
removed_state_name_select( |
119 | 4x |
setdiff(names(previous_state_select()), names(self$state_list_get("select"))) |
120 |
) |
|
121 | 4x |
previous_state_select(self$state_list_get("select")) |
122 |
}) |
|
123 | ||
124 | 4x |
observeEvent(added_state_name_select(), ignoreNULL = TRUE, { |
125 | 4x |
fstates <- self$state_list_get("select") |
126 | 4x |
html_ids <- private$map_vars_to_html_ids(keys = names(fstates), prefix = "colData") |
127 | 4x |
for (fname in added_state_name_select()) { |
128 | ! |
private$insert_filter_state_ui( |
129 | ! |
id = html_ids[fname], |
130 | ! |
filter_state = fstates[[fname]], |
131 | ! |
state_list_index = "select", |
132 | ! |
state_id = fname |
133 |
) |
|
134 |
} |
|
135 | 4x |
added_state_name_select(character(0)) |
136 |
}) |
|
137 | ||
138 | 4x |
observeEvent(removed_state_name_select(), { |
139 | 4x |
req(removed_state_name_select()) |
140 | ! |
for (fname in removed_state_name_select()) { |
141 | ! |
private$remove_filter_state_ui("select", fname, .input = input) |
142 |
} |
|
143 | ! |
removed_state_name_select(character(0)) |
144 |
}) |
|
145 | 4x |
NULL |
146 |
} |
|
147 |
) |
|
148 |
}, |
|
149 | ||
150 |
#' @description |
|
151 |
#' Gets the reactive values from the active `FilterState` objects. |
|
152 |
#' |
|
153 |
#' Gets all active filters from this dataset in form of the nested list. |
|
154 |
#' The output list is a compatible input to `self$set_filter_state`. |
|
155 |
#' |
|
156 |
#' @return `list` containing one or two lists depending on the number of |
|
157 |
#' `state_list` object (I.e. if `rowData` and `colData` exist). Each |
|
158 |
#' `list` contains elements number equal to number of active filter variables. |
|
159 |
get_filter_state = function() { |
|
160 | 40x |
states <- sapply( |
161 | 40x |
X = names(private$state_list), |
162 | 40x |
simplify = FALSE, |
163 | 40x |
function(x) { |
164 | 80x |
lapply(self$state_list_get(state_list_index = x), function(xx) xx$get_state()) |
165 |
} |
|
166 |
) |
|
167 | 40x |
Filter(function(x) length(x) > 0, states) |
168 |
}, |
|
169 | ||
170 |
#' @description |
|
171 |
#' Set filter state |
|
172 |
#' |
|
173 |
#' @param data (`SummarizedExperiment`)\cr |
|
174 |
#' data which are supposed to be filtered. |
|
175 |
#' @param state (`named list`)\cr |
|
176 |
#' this list should contain `subset` and `select` element where |
|
177 |
#' each should be a named list containing values as a selection in the `FilterState`. |
|
178 |
#' Names of each the `list` element in `subset` and `select` should correspond to |
|
179 |
#' the name of the column in `rowData(data)` and `colData(data)`. |
|
180 |
#' @param ... ignored. |
|
181 |
#' @return `NULL` |
|
182 |
set_filter_state = function(data, state, ...) { |
|
183 | 30x |
checkmate::assert_class(data, "SummarizedExperiment") |
184 | 27x |
checkmate::assert_class(state, "list") |
185 | ||
186 | 26x |
checkmate::assert( |
187 | 26x |
checkmate::check_subset(names(state), c("subset", "select")), |
188 | 26x |
checkmate::check_class(state, "default_filter"), |
189 | 26x |
combine = "or" |
190 |
) |
|
191 | 25x |
checkmate::assert( |
192 | 25x |
checkmate::test_null(state$subset), |
193 | 25x |
checkmate::assert( |
194 | 25x |
checkmate::check_class(state$subset, "list"), |
195 | 25x |
checkmate::check_subset(names(state$subset), names(SummarizedExperiment::rowData(data))), |
196 | 25x |
combine = "and" |
197 |
), |
|
198 | 25x |
combine = "or" |
199 |
) |
|
200 | 25x |
checkmate::assert( |
201 | 25x |
checkmate::test_null(state$select), |
202 | 25x |
checkmate::assert( |
203 | 25x |
checkmate::check_class(state$select, "list"), |
204 | 25x |
checkmate::check_subset(names(state$select), names(SummarizedExperiment::colData(data))), |
205 | 25x |
combine = "and" |
206 |
), |
|
207 | 25x |
combine = "or" |
208 |
) |
|
209 | ||
210 | 25x |
filter_states <- self$state_list_get("subset") |
211 | 25x |
for (varname in names(state$subset)) { |
212 | 20x |
value <- resolve_state(state$subset[[varname]]) |
213 | 20x |
if (varname %in% names(filter_states)) { |
214 | 2x |
fstate <- filter_states[[varname]] |
215 | 2x |
fstate$set_state(value) |
216 |
} else { |
|
217 | 18x |
fstate <- init_filter_state( |
218 | 18x |
SummarizedExperiment::rowData(data)[[varname]], |
219 | 18x |
varname = varname, |
220 | 18x |
dataname = private$dataname |
221 |
) |
|
222 | 18x |
fstate$set_state(value) |
223 | 18x |
self$state_list_push( |
224 | 18x |
x = fstate, |
225 | 18x |
state_list_index = "subset", |
226 | 18x |
state_id = varname |
227 |
) |
|
228 |
} |
|
229 |
} |
|
230 | ||
231 | 25x |
filter_states <- self$state_list_get("select") |
232 | 25x |
for (varname in names(state$select)) { |
233 | 9x |
value <- resolve_state(state$select[[varname]]) |
234 | 9x |
if (varname %in% names(filter_states)) { |
235 | 1x |
fstate <- filter_states[[varname]] |
236 | 1x |
fstate$set_state(value) |
237 |
} else { |
|
238 | 8x |
fstate <- init_filter_state( |
239 | 8x |
SummarizedExperiment::colData(data)[[varname]], |
240 | 8x |
varname = varname, |
241 | 8x |
dataname = private$dataname |
242 |
) |
|
243 | 8x |
fstate$set_state(value) |
244 | 8x |
self$state_list_push( |
245 | 8x |
x = fstate, |
246 | 8x |
state_list_index = "select", |
247 | 8x |
state_id = varname |
248 |
) |
|
249 |
} |
|
250 |
} |
|
251 | 25x |
logger::log_trace(paste( |
252 | 25x |
"SEFilterState$set_filter_state initialized,", |
253 | 25x |
"dataname: { private$dataname }" |
254 |
)) |
|
255 | 25x |
NULL |
256 |
}, |
|
257 | ||
258 |
#' @description Remove a variable from the `state_list` and its corresponding UI element. |
|
259 |
#' |
|
260 |
#' @param state_id (`character(1)`)\cr name of `state_list` element. |
|
261 |
#' |
|
262 |
#' @return `NULL` |
|
263 |
remove_filter_state = function(state_id) { |
|
264 | 4x |
logger::log_trace( |
265 | 4x |
sprintf( |
266 | 4x |
"%s$remove_filter_state called, dataname: %s", |
267 | 4x |
class(self)[1], |
268 | 4x |
private$dataname |
269 |
) |
|
270 |
) |
|
271 | ||
272 | 4x |
checkmate::assert( |
273 | 4x |
!checkmate::test_null(names(state_id)), |
274 | 4x |
checkmate::check_subset(names(state_id), c("subset", "select")), |
275 | 4x |
combine = "and" |
276 |
) |
|
277 | 3x |
for (varname in state_id$subset) { |
278 | 3x |
if (!all(unlist(state_id$subset) %in% names(self$state_list_get("subset")))) { |
279 | 1x |
warning(paste( |
280 | 1x |
"Variable:", state_id, "is not present in the actual active subset filters of dataset:", |
281 | 1x |
"{ private$dataname } therefore no changes are applied." |
282 |
)) |
|
283 | 1x |
logger::log_warn( |
284 | 1x |
paste( |
285 | 1x |
"Variable:", state_id, "is not present in the actual active subset filters of dataset:", |
286 | 1x |
"{ private$dataname } therefore no changes are applied." |
287 |
) |
|
288 |
) |
|
289 |
} else { |
|
290 | 2x |
self$state_list_remove(state_list_index = "subset", state_id = varname) |
291 | 2x |
logger::log_trace( |
292 | 2x |
sprintf( |
293 | 2x |
"%s$remove_filter_state for subset variable %s done, dataname: %s", |
294 | 2x |
class(self)[1], |
295 | 2x |
varname, |
296 | 2x |
private$dataname |
297 |
) |
|
298 |
) |
|
299 |
} |
|
300 |
} |
|
301 | ||
302 | 3x |
for (varname in state_id$select) { |
303 | 1x |
if (!all(unlist(state_id$select) %in% names(self$state_list_get("select")))) { |
304 | ! |
warning(paste( |
305 | ! |
"Variable:", state_id, "is not present in the actual active select filters of dataset:", |
306 | ! |
"{ private$dataname } therefore no changes are applied." |
307 |
)) |
|
308 | ! |
logger::log_warn( |
309 | ! |
paste( |
310 | ! |
"Variable:", state_id, "is not present in the actual active select filters of dataset:", |
311 | ! |
"{ private$dataname } therefore no changes are applied." |
312 |
) |
|
313 |
) |
|
314 |
} else { |
|
315 | 1x |
self$state_list_remove(state_list_index = "select", state_id = varname) |
316 | 1x |
sprintf( |
317 | 1x |
"%s$remove_filter_state for select variable %s done, dataname: %s", |
318 | 1x |
class(self)[1], |
319 | 1x |
varname, |
320 | 1x |
private$dataname |
321 |
) |
|
322 |
} |
|
323 |
} |
|
324 |
}, |
|
325 | ||
326 |
# shiny modules ---- |
|
327 | ||
328 |
#' @description |
|
329 |
#' Shiny UI module to add filter variable |
|
330 |
#' @param id (`character(1)`)\cr |
|
331 |
#' id of shiny module |
|
332 |
#' @param data (`SummarizedExperiment`)\cr |
|
333 |
#' object containing `colData` and `rowData` which columns |
|
334 |
#' are used to choose filter variables. Column selection from `colData` |
|
335 |
#' and `rowData` are separate shiny entities. |
|
336 |
#' @return shiny.tag |
|
337 |
ui_add_filter_state = function(id, data) { |
|
338 | 2x |
checkmate::assert_string(id) |
339 | 2x |
stopifnot(is(data, "SummarizedExperiment")) |
340 | ||
341 | 2x |
ns <- NS(id) |
342 | ||
343 | 2x |
row_input <- if (ncol(SummarizedExperiment::rowData(data)) == 0) { |
344 | 1x |
div("no sample variables available") |
345 | 2x |
} else if (nrow(SummarizedExperiment::rowData(data)) == 0) { |
346 | 1x |
div("no samples available") |
347 |
} else { |
|
348 | ! |
teal.widgets::optionalSelectInput( |
349 | ! |
ns("row_to_add"), |
350 | ! |
choices = NULL, |
351 | ! |
options = shinyWidgets::pickerOptions( |
352 | ! |
liveSearch = TRUE, |
353 | ! |
noneSelectedText = "Select gene variable" |
354 |
) |
|
355 |
) |
|
356 |
} |
|
357 | ||
358 | 2x |
col_input <- if (ncol(SummarizedExperiment::colData(data)) == 0) { |
359 | 1x |
div("no sample variables available") |
360 | 2x |
} else if (nrow(SummarizedExperiment::colData(data)) == 0) { |
361 | 1x |
div("no samples available") |
362 |
} else { |
|
363 | ! |
teal.widgets::optionalSelectInput( |
364 | ! |
ns("col_to_add"), |
365 | ! |
choices = NULL, |
366 | ! |
options = shinyWidgets::pickerOptions( |
367 | ! |
liveSearch = TRUE, |
368 | ! |
noneSelectedText = "Select sample variable" |
369 |
) |
|
370 |
) |
|
371 |
} |
|
372 | ||
373 | 2x |
div( |
374 | 2x |
row_input, |
375 | 2x |
col_input |
376 |
) |
|
377 |
}, |
|
378 | ||
379 |
#' @description |
|
380 |
#' Shiny server module to add filter variable |
|
381 |
#' |
|
382 |
#' Module controls available choices to select as a filter variable. |
|
383 |
#' Selected filter variable is being removed from available choices. |
|
384 |
#' Removed filter variable gets back to available choices. |
|
385 |
#' This module unlike other `FilterStates` classes manages two |
|
386 |
#' sets of filter variables - one for `colData` and another for |
|
387 |
#' `rowData`. |
|
388 |
#' |
|
389 |
#' @param id (`character(1)`)\cr |
|
390 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
391 |
#' @param data (`SummarizedExperiment`)\cr |
|
392 |
#' object containing `colData` and `rowData` which columns |
|
393 |
#' are used to choose filter variables. Column selection from `colData` |
|
394 |
#' and `rowData` are separate shiny entities. |
|
395 |
#' @param ... ignored |
|
396 |
#' @return `moduleServer` function which returns `NULL` |
|
397 |
srv_add_filter_state = function(id, data, ...) { |
|
398 | ! |
stopifnot(is(data, "SummarizedExperiment")) |
399 | ! |
check_ellipsis(..., stop = FALSE) |
400 | ! |
moduleServer( |
401 | ! |
id = id, |
402 | ! |
function(input, output, session) { |
403 | ! |
logger::log_trace( |
404 | ! |
"SEFilterState$srv_add_filter_state initializing, dataname: { private$dataname }" |
405 |
) |
|
406 | ! |
shiny::setBookmarkExclude(c("row_to_add", "col_to_add")) |
407 | ! |
active_filter_col_vars <- reactive({ |
408 | ! |
vapply( |
409 | ! |
X = self$state_list_get(state_list_index = "select"), |
410 | ! |
FUN.VALUE = character(1), |
411 | ! |
FUN = function(x) x$get_varname() |
412 |
) |
|
413 |
}) |
|
414 | ! |
active_filter_row_vars <- reactive({ |
415 | ! |
vapply( |
416 | ! |
X = self$state_list_get(state_list_index = "subset"), |
417 | ! |
FUN.VALUE = character(1), |
418 | ! |
FUN = function(x) x$get_varname() |
419 |
) |
|
420 |
}) |
|
421 | ||
422 | ! |
row_data <- SummarizedExperiment::rowData(data) |
423 | ! |
col_data <- SummarizedExperiment::colData(data) |
424 | ||
425 |
# available choices to display |
|
426 | ! |
avail_row_data_choices <- reactive({ |
427 | ! |
choices <- setdiff( |
428 | ! |
get_supported_filter_varnames(data = row_data), |
429 | ! |
active_filter_row_vars() |
430 |
) |
|
431 | ||
432 | ! |
data_choices_labeled( |
433 | ! |
data = row_data, |
434 | ! |
choices = choices, |
435 | ! |
varlabels = character(0), |
436 | ! |
keys = NULL |
437 |
) |
|
438 |
}) |
|
439 | ! |
avail_col_data_choices <- reactive({ |
440 | ! |
choices <- setdiff( |
441 | ! |
get_supported_filter_varnames(data = col_data), |
442 | ! |
active_filter_col_vars() |
443 |
) |
|
444 | ||
445 | ! |
data_choices_labeled( |
446 | ! |
data = col_data, |
447 | ! |
choices = choices, |
448 | ! |
varlabels = character(0), |
449 | ! |
keys = NULL |
450 |
) |
|
451 |
}) |
|
452 | ||
453 | ||
454 | ! |
observeEvent( |
455 | ! |
avail_row_data_choices(), |
456 | ! |
ignoreNULL = TRUE, |
457 | ! |
handlerExpr = { |
458 | ! |
logger::log_trace(paste( |
459 | ! |
"SEFilterStates$srv_add_filter_state@1 updating available row data choices,", |
460 | ! |
"dataname: { private$dataname }" |
461 |
)) |
|
462 | ! |
if (is.null(avail_row_data_choices())) { |
463 | ! |
shinyjs::hide("row_to_add") |
464 |
} else { |
|
465 | ! |
shinyjs::show("row_to_add") |
466 |
} |
|
467 | ! |
teal.widgets::updateOptionalSelectInput( |
468 | ! |
session, |
469 | ! |
"row_to_add", |
470 | ! |
choices = avail_row_data_choices() |
471 |
) |
|
472 | ! |
logger::log_trace(paste( |
473 | ! |
"SEFilterStates$srv_add_filter_state@1 updated available row data choices,", |
474 | ! |
"dataname: { private$dataname }" |
475 |
)) |
|
476 |
} |
|
477 |
) |
|
478 | ||
479 | ! |
observeEvent( |
480 | ! |
avail_col_data_choices(), |
481 | ! |
ignoreNULL = TRUE, |
482 | ! |
handlerExpr = { |
483 | ! |
logger::log_trace(paste( |
484 | ! |
"SEFilterStates$srv_add_filter_state@2 updating available col data choices,", |
485 | ! |
"dataname: { private$dataname }" |
486 |
)) |
|
487 | ! |
if (is.null(avail_col_data_choices())) { |
488 | ! |
shinyjs::hide("col_to_add") |
489 |
} else { |
|
490 | ! |
shinyjs::show("col_to_add") |
491 |
} |
|
492 | ! |
teal.widgets::updateOptionalSelectInput( |
493 | ! |
session, |
494 | ! |
"col_to_add", |
495 | ! |
choices = avail_col_data_choices() |
496 |
) |
|
497 | ! |
logger::log_trace(paste( |
498 | ! |
"SEFilterStates$srv_add_filter_state@2 updated available col data choices,", |
499 | ! |
"dataname: { private$dataname }" |
500 |
)) |
|
501 |
} |
|
502 |
) |
|
503 | ||
504 | ! |
observeEvent( |
505 | ! |
eventExpr = input$col_to_add, |
506 | ! |
handlerExpr = { |
507 | ! |
logger::log_trace( |
508 | ! |
sprintf( |
509 | ! |
"SEFilterStates$srv_add_filter_state@3 adding FilterState of column %s to col data, dataname: %s", |
510 | ! |
deparse1(input$col_to_add), |
511 | ! |
private$dataname |
512 |
) |
|
513 |
) |
|
514 | ! |
self$state_list_push( |
515 | ! |
x = init_filter_state( |
516 | ! |
SummarizedExperiment::colData(data)[[input$col_to_add]], |
517 | ! |
varname = input$col_to_add, |
518 | ! |
dataname = private$dataname |
519 |
), |
|
520 | ! |
state_list_index = "select", |
521 | ! |
state_id = input$col_to_add |
522 |
) |
|
523 | ! |
logger::log_trace( |
524 | ! |
sprintf( |
525 | ! |
"SEFilterStates$srv_add_filter_state@3 added FilterState of column %s to col data, dataname: %s", |
526 | ! |
deparse1(input$col_to_add), |
527 | ! |
private$dataname |
528 |
) |
|
529 |
) |
|
530 |
} |
|
531 |
) |
|
532 | ||
533 | ! |
observeEvent( |
534 | ! |
eventExpr = input$row_to_add, |
535 | ! |
handlerExpr = { |
536 | ! |
logger::log_trace( |
537 | ! |
sprintf( |
538 | ! |
"SEFilterStates$srv_add_filter_state@4 adding FilterState of variable %s to row data, dataname: %s", |
539 | ! |
deparse1(input$row_to_add), |
540 | ! |
private$dataname |
541 |
) |
|
542 |
) |
|
543 | ! |
self$state_list_push( |
544 | ! |
x = init_filter_state( |
545 | ! |
SummarizedExperiment::rowData(data)[[input$row_to_add]], |
546 | ! |
varname = input$row_to_add, |
547 | ! |
dataname = private$dataname |
548 |
), |
|
549 | ! |
state_list_index = "subset", |
550 | ! |
state_id = input$row_to_add |
551 |
) |
|
552 | ! |
logger::log_trace( |
553 | ! |
sprintf( |
554 | ! |
"SEFilterStates$srv_add_filter_state@4 added FilterState of variable %s to row data, dataname: %s", |
555 | ! |
deparse1(input$row_to_add), |
556 | ! |
private$dataname |
557 |
) |
|
558 |
) |
|
559 |
} |
|
560 |
) |
|
561 | ||
562 | ! |
logger::log_trace( |
563 | ! |
"SEFilterState$srv_add_filter_state initialized, dataname: { private$dataname }" |
564 |
) |
|
565 | ! |
NULL |
566 |
} |
|
567 |
) |
|
568 |
} |
|
569 |
) |
|
570 |
) |
1 |
#' @name FilteredData |
|
2 |
#' @docType class |
|
3 |
#' |
|
4 |
#' @title Class to encapsulate filtered datasets |
|
5 |
#' |
|
6 |
#' @details |
|
7 |
#' The main purpose of this class is to provide a collection of reactive datasets, |
|
8 |
#' each dataset having a filter state that determines how it is filtered. |
|
9 |
#' |
|
10 |
#' For each dataset, `get_filter_expr` returns the call to filter the dataset according |
|
11 |
#' to the filter state. The data itself can be obtained through `get_data`. |
|
12 |
#' |
|
13 |
#' The datasets are filtered lazily, i.e. only when requested / needed in a Shiny app. |
|
14 |
#' |
|
15 |
#' By design, any dataname set through `set_dataset` cannot be removed because |
|
16 |
#' other code may already depend on it. As a workaround, the underlying |
|
17 |
#' data can be set to `NULL`. |
|
18 |
#' |
|
19 |
#' The class currently supports variables of the following types within datasets: |
|
20 |
#' - `choices`: variable of type `factor`, e.g. `ADSL$COUNTRY`, `iris$Species` |
|
21 |
#' zero or more options can be selected, when the variable is a factor |
|
22 |
#' - `logical`: variable of type `logical`, e.g. `ADSL$TRT_FLAG` |
|
23 |
#' exactly one option must be selected, `TRUE` or `FALSE` |
|
24 |
#' - `ranges`: variable of type `numeric`, e.g. `ADSL$AGE`, `iris$Sepal.Length` |
|
25 |
#' numerical range, a range within this range can be selected |
|
26 |
#' - `dates`: variable of type `Date`, `POSIXlt` |
|
27 |
#' Other variables cannot be used for filtering the data in this class. |
|
28 |
#' |
|
29 |
#' Common arguments are: |
|
30 |
#' 1. `filtered`: whether to return a filtered result or not |
|
31 |
#' 2. `dataname`: the name of one of the datasets in this `FilteredData` |
|
32 |
#' 3. `varname`: one of the columns in a dataset |
|
33 |
#' |
|
34 |
#' @keywords internal |
|
35 |
#' |
|
36 |
#' @examples |
|
37 |
#' library(shiny) |
|
38 |
#' datasets <- teal.slice:::FilteredData$new( |
|
39 |
#' list( |
|
40 |
#' iris = list(dataset = iris), |
|
41 |
#' mtcars = list(dataset = mtcars, metadata = list(type = "training")) |
|
42 |
#' ) |
|
43 |
#' ) |
|
44 |
#' |
|
45 |
#' # get datanames |
|
46 |
#' datasets$datanames() |
|
47 |
#' |
|
48 |
#' df <- datasets$get_data("iris", filtered = FALSE) |
|
49 |
#' print(df) |
|
50 |
#' |
|
51 |
#' datasets$get_metadata("mtcars") |
|
52 |
#' |
|
53 |
#' isolate( |
|
54 |
#' datasets$set_filter_state( |
|
55 |
#' list(iris = list(Species = list(selected = "virginica"))) |
|
56 |
#' ) |
|
57 |
#' ) |
|
58 |
#' isolate(datasets$get_call("iris")) |
|
59 |
#' |
|
60 |
#' isolate( |
|
61 |
#' datasets$set_filter_state( |
|
62 |
#' list(mtcars = list(mpg = list(selected = c(15, 20)))) |
|
63 |
#' ) |
|
64 |
#' ) |
|
65 |
#' |
|
66 |
#' isolate(datasets$get_filter_state()) |
|
67 |
#' isolate(datasets$get_filter_overview("iris")) |
|
68 |
#' isolate(datasets$get_filter_overview("mtcars")) |
|
69 |
#' isolate(datasets$get_call("iris")) |
|
70 |
#' isolate(datasets$get_call("mtcars")) |
|
71 |
#' |
|
72 |
FilteredData <- R6::R6Class( # nolint |
|
73 |
"FilteredData", |
|
74 |
## __Public Methods ==== |
|
75 |
public = list( |
|
76 |
#' @description |
|
77 |
#' Initialize a `FilteredData` object |
|
78 |
#' @param data_objects (`list`) should contain. |
|
79 |
#' - `dataset` data object object supported by [`FilteredDataset`]. |
|
80 |
#' - `metatada` (optional) additional metadata attached to the `dataset`. |
|
81 |
#' - `keys` (optional) primary keys. |
|
82 |
#' - `datalabel` (optional) label describing the `dataset`. |
|
83 |
#' - `parent` (optional) which `NULL` is a parent of this one. |
|
84 |
#' @param join_keys (`JoinKeys` or NULL) see [`teal.data::join_keys()`]. |
|
85 |
#' @param code (`CodeClass` or `NULL`) see [`teal.data::CodeClass`]. |
|
86 |
#' @param check (`logical(1)`) whether data has been check against reproducibility. |
|
87 |
#' |
|
88 |
initialize = function(data_objects, join_keys = NULL, code = NULL, check = FALSE) { |
|
89 | 66x |
checkmate::assert_list(data_objects, any.missing = FALSE, min.len = 0, names = "unique") |
90 |
# Note the internals of data_objects are checked in set_dataset |
|
91 | 66x |
checkmate::assert_class(join_keys, "JoinKeys", null.ok = TRUE) |
92 | 66x |
checkmate::assert_class(code, "CodeClass", null.ok = TRUE) |
93 | 66x |
checkmate::assert_flag(check) |
94 | ||
95 | 66x |
self$set_check(check) |
96 | 66x |
if (!is.null(code)) { |
97 | 15x |
self$set_code(code) |
98 |
} |
|
99 | ||
100 | 66x |
for (dataname in names(data_objects)) { |
101 | 105x |
self$set_dataset(data_objects[[dataname]], dataname) |
102 |
} |
|
103 | ||
104 | 66x |
if (!is.null(join_keys)) { |
105 | 13x |
self$set_join_keys(join_keys) |
106 |
} |
|
107 | ||
108 | 66x |
invisible(self) |
109 |
}, |
|
110 | ||
111 |
#' @description |
|
112 |
#' Gets datanames |
|
113 |
#' |
|
114 |
#' The datanames are returned in the order in which they must be |
|
115 |
#' evaluated (in case of dependencies). |
|
116 |
#' @return (`character` vector) of datanames |
|
117 |
datanames = function() { |
|
118 | 57x |
names(private$filtered_datasets) |
119 |
}, |
|
120 | ||
121 |
#' Gets data label for the dataset |
|
122 |
#' |
|
123 |
#' Useful to display in `Show R Code`. |
|
124 |
#' |
|
125 |
#' @param dataname (`character(1)`) name of the dataset |
|
126 |
#' @return (`character`) keys of dataset |
|
127 |
get_datalabel = function(dataname) { |
|
128 | 2x |
self$get_filtered_dataset(dataname)$get_dataset_label() |
129 |
}, |
|
130 | ||
131 |
#' @description |
|
132 |
#' Gets dataset names of a given dataname for the filtering. |
|
133 |
#' |
|
134 |
#' @param dataname (`character` vector) names of the dataset |
|
135 |
#' |
|
136 |
#' @return (`character` vector) of dataset names |
|
137 |
#' |
|
138 |
get_filterable_datanames = function(dataname) { |
|
139 | ! |
dataname |
140 |
}, |
|
141 | ||
142 |
#' @description |
|
143 |
#' Gets variable names of a given dataname for the filtering. |
|
144 |
#' |
|
145 |
#' @param dataname (`character(1)`) name of the dataset |
|
146 |
#' |
|
147 |
#' @return (`character` vector) of variable names |
|
148 |
#' |
|
149 |
get_filterable_varnames = function(dataname) { |
|
150 | 36x |
self$get_filtered_dataset(dataname)$get_filterable_varnames() |
151 |
}, |
|
152 | ||
153 |
#' @description |
|
154 |
#' Set the variable names of a given dataset for the filtering. |
|
155 |
#' |
|
156 |
#' @param dataname (`character(1)`) name of the dataset |
|
157 |
#' @param varnames (`character` or `NULL`) |
|
158 |
#' variables which users can choose to filter the data; |
|
159 |
#' see `self$get_filterable_varnames` for more details |
|
160 |
#' |
|
161 |
#' @return this `FilteredData` object invisibly |
|
162 |
#' |
|
163 |
set_filterable_varnames = function(dataname, varnames) { |
|
164 | 3x |
private$check_data_varname_exists(dataname) |
165 | 3x |
self$get_filtered_dataset(dataname)$set_filterable_varnames(varnames) |
166 | 3x |
invisible(self) |
167 |
}, |
|
168 | ||
169 |
# datasets methods ---- |
|
170 |
#' @description |
|
171 |
#' Gets a `call` to filter the dataset according to the filter state. |
|
172 |
#' |
|
173 |
#' It returns a `call` to filter the dataset only, assuming the |
|
174 |
#' other (filtered) datasets it depends on are available. |
|
175 |
#' |
|
176 |
#' Together with `self$datanames()` which returns the datasets in the correct |
|
177 |
#' evaluation order, this generates the whole filter code, see the function |
|
178 |
#' `FilteredData$get_filter_code`. |
|
179 |
#' |
|
180 |
#' For the return type, note that `rlang::is_expression` returns `TRUE` on the |
|
181 |
#' return type, both for base R expressions and calls (single expression, |
|
182 |
#' capturing a function call). |
|
183 |
#' |
|
184 |
#' The filtered dataset has the name given by `self$filtered_dataname(dataname)` |
|
185 |
#' |
|
186 |
#' This can be used for the `Show R Code` generation. |
|
187 |
#' |
|
188 |
#' @param dataname (`character(1)`) name of the dataset |
|
189 |
#' |
|
190 |
#' @return (`call` or `list` of calls) to filter dataset calls |
|
191 |
#' |
|
192 |
get_call = function(dataname) { |
|
193 | 24x |
private$check_data_varname_exists(dataname) |
194 | 24x |
self$get_filtered_dataset(dataname)$get_call() |
195 |
}, |
|
196 | ||
197 |
#' @description |
|
198 |
#' Gets the R preprocessing code string that generates the unfiltered datasets. |
|
199 |
#' |
|
200 |
#' @param dataname (`character(1)`) name(s) of dataset(s) |
|
201 |
#' |
|
202 |
#' @return (`character(1)`) deparsed code |
|
203 |
#' |
|
204 |
get_code = function(dataname = self$datanames()) { |
|
205 | 2x |
if (!is.null(private$code)) { |
206 | 1x |
paste0(private$code$get_code(dataname), collapse = "\n") |
207 |
} else { |
|
208 | 1x |
paste0("# No pre-processing code provided") |
209 |
} |
|
210 |
}, |
|
211 | ||
212 |
#' @description |
|
213 |
#' Gets `FilteredDataset` object which contains all information |
|
214 |
#' pertaining to the specified dataset. |
|
215 |
#' |
|
216 |
#' @param dataname (`character(1)`)\cr |
|
217 |
#' name of the dataset |
|
218 |
#' |
|
219 |
#' @return `FilteredDataset` object or list of `FilteredDataset`s |
|
220 |
#' |
|
221 |
get_filtered_dataset = function(dataname = character(0)) { |
|
222 | 326x |
if (length(dataname) == 0) { |
223 | 108x |
private$filtered_datasets |
224 |
} else { |
|
225 | 218x |
private$filtered_datasets[[dataname]] |
226 |
} |
|
227 |
}, |
|
228 | ||
229 |
#' @description |
|
230 |
#' Gets filtered or unfiltered dataset. |
|
231 |
#' |
|
232 |
#' For `filtered = FALSE`, the original data set with |
|
233 |
#' `set_data` is returned including all attributes. |
|
234 |
#' |
|
235 |
#' @param dataname (`character(1)`) name of the dataset |
|
236 |
#' @param filtered (`logical`) whether to return a filtered or unfiltered dataset |
|
237 |
#' |
|
238 |
get_data = function(dataname, filtered = TRUE) { |
|
239 | 29x |
private$check_data_varname_exists(dataname) |
240 | 29x |
checkmate::assert_flag(filtered) |
241 | 28x |
if (filtered) { |
242 |
# This try is specific for MAEFilteredDataset due to a bug in |
|
243 |
# S4Vectors causing errors when using the subset function on MAE objects. |
|
244 |
# The fix was introduced in S4Vectors 0.30.1, but is unavailable for R versions < 4.1 |
|
245 |
# Link to the issue: https://github.com/insightsengineering/teal/issues/210 |
|
246 | 26x |
tryCatch( |
247 | 26x |
private$reactive_data[[dataname]](), |
248 | 26x |
error = function(error) { |
249 | ! |
shiny::validate(paste( |
250 | ! |
"Filtering expression returned error(s). Please change filters.\nThe error message was:", |
251 | ! |
error$message, |
252 | ! |
sep = "\n" |
253 |
)) |
|
254 |
} |
|
255 |
) |
|
256 |
} else { |
|
257 | 2x |
self$get_filtered_dataset(dataname)$get_dataset() |
258 |
} |
|
259 |
}, |
|
260 | ||
261 |
#' @description |
|
262 |
#' Returns whether the datasets in the object has undergone a reproducibility check. |
|
263 |
#' |
|
264 |
#' @return `logical` |
|
265 |
#' |
|
266 |
get_check = function() { |
|
267 | 2x |
private$.check |
268 |
}, |
|
269 | ||
270 |
#' @description |
|
271 |
#' Gets metadata for a given dataset. |
|
272 |
#' |
|
273 |
#' @param dataname (`character(1)`) name of the dataset |
|
274 |
#' |
|
275 |
#' @return value of metadata for given data (or `NULL` if it does not exist) |
|
276 |
#' |
|
277 |
get_metadata = function(dataname) { |
|
278 | 3x |
private$check_data_varname_exists(dataname) |
279 | 2x |
self$get_filtered_dataset(dataname)$get_metadata() |
280 |
}, |
|
281 | ||
282 |
#' @description |
|
283 |
#' Get join keys between two datasets. |
|
284 |
#' |
|
285 |
#' @return (`JoinKeys`) |
|
286 |
#' |
|
287 |
get_join_keys = function() { |
|
288 | 3x |
return(private$keys) |
289 |
}, |
|
290 | ||
291 |
#' @description |
|
292 |
#' Get filter overview table in form of X (filtered) / Y (non-filtered). |
|
293 |
#' |
|
294 |
#' This is intended to be presented in the application. |
|
295 |
#' The content for each of the data names is defined in `get_filter_overview_info` method. |
|
296 |
#' |
|
297 |
#' @param datanames (`character` vector) names of the dataset |
|
298 |
#' |
|
299 |
#' @return (`matrix`) matrix of observations and subjects of all datasets |
|
300 |
#' |
|
301 |
get_filter_overview = function(datanames) { |
|
302 | ! |
if (identical(datanames, "all")) { |
303 | ! |
datanames <- self$datanames() |
304 |
} |
|
305 | ! |
check_in_subset(datanames, self$datanames(), "Some datasets are not available: ") |
306 | ||
307 | ! |
rows <- lapply( |
308 | ! |
datanames, |
309 | ! |
function(dataname) { |
310 | ! |
self$get_filtered_dataset(dataname)$get_filter_overview_info( |
311 | ! |
filtered_dataset = self$get_data(dataname = dataname, filtered = TRUE) |
312 |
) |
|
313 |
} |
|
314 |
) |
|
315 | ||
316 | ! |
do.call(rbind, rows) |
317 |
}, |
|
318 | ||
319 |
#' @description |
|
320 |
#' Get keys for the dataset. |
|
321 |
#' |
|
322 |
#' @param dataname (`character(1)`) name of the dataset |
|
323 |
#' |
|
324 |
#' @return (`character`) keys of dataset |
|
325 |
#' |
|
326 |
get_keys = function(dataname) { |
|
327 | 2x |
self$get_filtered_dataset(dataname)$get_keys() |
328 |
}, |
|
329 | ||
330 |
#' @description |
|
331 |
#' Gets labels of variables in the data. |
|
332 |
#' |
|
333 |
#' Variables are the column names of the data. |
|
334 |
#' Either, all labels must have been provided for all variables |
|
335 |
#' in `set_data` or `NULL`. |
|
336 |
#' |
|
337 |
#' @param dataname (`character(1)`) name of the dataset |
|
338 |
#' @param variables (`character`) variables to get labels for; |
|
339 |
#' if `NULL`, for all variables in data |
|
340 |
#' |
|
341 |
#' @return (`character` or `NULL`) variable labels, `NULL` if `column_labels` |
|
342 |
#' attribute does not exist for the data |
|
343 |
#' |
|
344 |
get_varlabels = function(dataname, variables = NULL) { |
|
345 | 4x |
self$get_filtered_dataset(dataname)$get_varlabels(variables = variables) |
346 |
}, |
|
347 | ||
348 |
#' @description |
|
349 |
#' Gets variable names. |
|
350 |
#' |
|
351 |
#' @param dataname (`character`) the name of the dataset |
|
352 |
#' |
|
353 |
#' @return (`character` vector) of variable names |
|
354 |
#' |
|
355 |
get_varnames = function(dataname) { |
|
356 | 1x |
self$get_filtered_dataset(dataname)$get_varnames() |
357 |
}, |
|
358 | ||
359 |
#' @description |
|
360 |
#' When active_datanames is "all", sets them to all `datanames`, |
|
361 |
#' otherwise, it makes sure that it is a subset of the available `datanames`. |
|
362 |
#' |
|
363 |
#' @param datanames `character vector` datanames to pick |
|
364 |
#' |
|
365 |
#' @return the intersection of `self$datanames()` and `datanames` |
|
366 |
#' |
|
367 |
handle_active_datanames = function(datanames) { |
|
368 | ! |
logger::log_trace("FilteredData$handle_active_datanames handling { paste(datanames, collapse = \" \") }") |
369 | ! |
if (identical(datanames, "all")) { |
370 | ! |
datanames <- self$datanames() |
371 |
} else { |
|
372 | ! |
for (dataname in datanames) { |
373 | ! |
tryCatch( |
374 | ! |
check_in_subset(datanames, self$datanames(), "Some datasets are not available: "), |
375 | ! |
error = function(e) { |
376 | ! |
message(e$message) |
377 |
} |
|
378 |
) |
|
379 |
} |
|
380 |
} |
|
381 | ! |
datanames <- self$get_filterable_datanames(datanames) |
382 | ! |
intersect(self$datanames(), datanames) |
383 |
}, |
|
384 | ||
385 |
#' @description |
|
386 |
#' Adds a dataset to this `FilteredData`. |
|
387 |
#' |
|
388 |
#' @details |
|
389 |
#' `set_dataset` creates a `FilteredDataset` object which keeps |
|
390 |
#' `dataset` for the filtering purpose. |
|
391 |
#' |
|
392 |
#' @param dataset_args (`list`)\cr |
|
393 |
#' containing the arguments except (`dataname`) |
|
394 |
#' needed by `init_filtered_dataset` |
|
395 |
#' @param dataname (`string`)\cr |
|
396 |
#' the name of the `dataset` to be added to this object |
|
397 |
#' |
|
398 |
#' @return (`self`) invisibly this `FilteredData` |
|
399 |
#' |
|
400 |
set_dataset = function(dataset_args, dataname) { |
|
401 | 97x |
logger::log_trace("FilteredData$set_dataset setting dataset, name: { dataname }") |
402 | 97x |
validate_dataset_args(dataset_args, dataname) |
403 | ||
404 | 97x |
dataset <- dataset_args$dataset |
405 | 97x |
dataset_args$dataset <- NULL |
406 | ||
407 |
# to include it nicely in the Show R Code; |
|
408 |
# the UI also uses datanames in ids, so no whitespaces allowed |
|
409 | 97x |
check_simple_name(dataname) |
410 | 97x |
private$filtered_datasets[[dataname]] <- do.call( |
411 | 97x |
what = init_filtered_dataset, |
412 | 97x |
args = c(list(dataset), dataset_args, list(dataname = dataname)) |
413 |
) |
|
414 | ||
415 | 97x |
private$reactive_data[[dataname]] <- reactive({ |
416 | 14x |
env <- new.env(parent = parent.env(globalenv())) |
417 | 14x |
env[[dataname]] <- self$get_filtered_dataset(dataname)$get_dataset() |
418 | 14x |
filter_call <- self$get_call(dataname) |
419 | 14x |
eval_expr_with_msg(filter_call, env) |
420 | 14x |
get(x = dataname, envir = env) |
421 |
}) |
|
422 | ||
423 | 97x |
invisible(self) |
424 |
}, |
|
425 | ||
426 |
#' @description |
|
427 |
#' Set the `join_keys`. |
|
428 |
#' |
|
429 |
#' @param join_keys (`JoinKeys`) join_key (converted to a nested list) |
|
430 |
#' |
|
431 |
#' @return (`self`) invisibly this `FilteredData` |
|
432 |
#' |
|
433 |
set_join_keys = function(join_keys) { |
|
434 | 13x |
checkmate::assert_class(join_keys, "JoinKeys") |
435 | 13x |
private$keys <- join_keys |
436 | 13x |
invisible(self) |
437 |
}, |
|
438 | ||
439 |
#' @description |
|
440 |
#' Sets whether the datasets in the object have undergone a reproducibility check. |
|
441 |
#' |
|
442 |
#' @param check (`logical`) whether datasets have undergone reproducibility check |
|
443 |
#' |
|
444 |
#' @return (`self`) |
|
445 |
#' |
|
446 |
set_check = function(check) { |
|
447 | 66x |
checkmate::assert_flag(check) |
448 | 66x |
private$.check <- check |
449 | 66x |
invisible(self) |
450 |
}, |
|
451 | ||
452 |
#' @description |
|
453 |
#' Sets the R preprocessing code for single dataset. |
|
454 |
#' |
|
455 |
#' @param code (`CodeClass`)\cr |
|
456 |
#' preprocessing code that can be parsed to generate the unfiltered datasets |
|
457 |
#' |
|
458 |
#' @return (`self`) |
|
459 |
#' |
|
460 |
set_code = function(code) { |
|
461 | 15x |
checkmate::assert_class(code, "CodeClass") |
462 | 15x |
logger::log_trace("FilteredData$set_code setting code") |
463 | 15x |
private$code <- code |
464 | 15x |
invisible(self) |
465 |
}, |
|
466 | ||
467 |
# Functions useful for restoring from another dataset ---- |
|
468 |
#' @description |
|
469 |
#' Gets the reactive values from the active `FilterState` objects. |
|
470 |
#' |
|
471 |
#' Gets all active filters in the form of a nested list. |
|
472 |
#' The output list is a compatible input to `self$set_filter_state`. |
|
473 |
#' The attribute `formatted` renders the output of `self$get_formatted_filter_state`, |
|
474 |
#' which is a character formatting of the filter state. |
|
475 |
#' |
|
476 |
#' @return `named list` with elements corresponding to `FilteredDataset` objects |
|
477 |
#' with active filters. In addition, the `formatted` attribute holds |
|
478 |
#' the character format of the active filter states. |
|
479 |
#' |
|
480 |
get_filter_state = function() { |
|
481 | 22x |
states <- lapply(self$get_filtered_dataset(), function(x) x$get_filter_state()) |
482 | 22x |
filtered_states <- Filter(function(x) length(x) > 0, states) |
483 | 22x |
structure(filtered_states, formatted = self$get_formatted_filter_state()) |
484 |
}, |
|
485 | ||
486 |
#' @description |
|
487 |
#' Returns the filter state formatted for printing to an `IO` device. |
|
488 |
#' |
|
489 |
#' @return `character` the pre-formatted filter state |
|
490 |
#' |
|
491 |
#' @examples |
|
492 |
#' utils::data(miniACC, package = "MultiAssayExperiment") |
|
493 |
#' datasets <- teal.slice:::FilteredData$new( |
|
494 |
#' list(iris = list(dataset = iris), |
|
495 |
#' mae = list(dataset = miniACC) |
|
496 |
#' ), |
|
497 |
#' join_keys = NULL |
|
498 |
#' ) |
|
499 |
#' fs <- list( |
|
500 |
#' iris = list( |
|
501 |
#' Sepal.Length = list(selected = c(5.1, 6.4), keep_na = TRUE, keep_inf = FALSE), |
|
502 |
#' Species = list(selected = c("setosa", "versicolor"), keep_na = FALSE) |
|
503 |
#' ), |
|
504 |
#' mae = list( |
|
505 |
#' subjects = list( |
|
506 |
#' years_to_birth = list(selected = c(30, 50), keep_na = TRUE, keep_inf = FALSE), |
|
507 |
#' vital_status = list(selected = "1", keep_na = FALSE), |
|
508 |
#' gender = list(selected = "female", keep_na = TRUE) |
|
509 |
#' ), |
|
510 |
#' RPPAArray = list( |
|
511 |
#' subset = list(ARRAY_TYPE = list(selected = "", keep_na = TRUE)) |
|
512 |
#' ) |
|
513 |
#' ) |
|
514 |
#' ) |
|
515 |
#' isolate(datasets$set_filter_state(state = fs)) |
|
516 |
#' cat(shiny::isolate(datasets$get_formatted_filter_state())) |
|
517 |
#' |
|
518 |
get_formatted_filter_state = function() { |
|
519 | 25x |
out <- |
520 | 25x |
unlist(sapply( |
521 | 25x |
self$get_filtered_dataset(), |
522 | 25x |
function(filtered_dataset) { |
523 | 45x |
filtered_dataset$get_formatted_filter_state() |
524 |
} |
|
525 |
)) |
|
526 | 25x |
paste(out, collapse = "\n") |
527 |
}, |
|
528 | ||
529 |
#' @description |
|
530 |
#' Sets active filter states. |
|
531 |
#' |
|
532 |
#' @param state (`named list`)\cr |
|
533 |
#' nested list of filter selections applied to datasets |
|
534 |
#' |
|
535 |
#' @return `NULL` |
|
536 |
#' |
|
537 |
#' @examples |
|
538 |
#' utils::data(miniACC, package = "MultiAssayExperiment") |
|
539 |
#' |
|
540 |
#' datasets <- teal.slice:::FilteredData$new( |
|
541 |
#' list(iris = list(dataset = iris), |
|
542 |
#' mae = list(dataset = miniACC) |
|
543 |
#' ), |
|
544 |
#' join_keys = NULL |
|
545 |
#' ) |
|
546 |
#' fs <- list( |
|
547 |
#' iris = list( |
|
548 |
#' Sepal.Length = list(selected = c(5.1, 6.4), keep_na = TRUE, keep_inf = FALSE), |
|
549 |
#' Species = list(selected = c("setosa", "versicolor"), keep_na = FALSE) |
|
550 |
#' ), |
|
551 |
#' mae = list( |
|
552 |
#' subjects = list( |
|
553 |
#' years_to_birth = list(selected = c(30, 50), keep_na = TRUE, keep_inf = FALSE), |
|
554 |
#' vital_status = list(selected = "1", keep_na = FALSE), |
|
555 |
#' gender = list(selected = "female", keep_na = TRUE) |
|
556 |
#' ), |
|
557 |
#' RPPAArray = list( |
|
558 |
#' subset = list(ARRAY_TYPE = list(selected = "", keep_na = TRUE)) |
|
559 |
#' ) |
|
560 |
#' ) |
|
561 |
#' ) |
|
562 |
#' shiny::isolate(datasets$set_filter_state(state = fs)) |
|
563 |
#' shiny::isolate(datasets$get_filter_state()) |
|
564 |
#' |
|
565 |
set_filter_state = function(state) { |
|
566 | 23x |
checkmate::assert_subset(names(state), self$datanames()) |
567 | 22x |
logger::log_trace( |
568 | 22x |
"FilteredData$set_filter_state initializing, dataname: { paste(names(state), collapse = ' ') }" |
569 |
) |
|
570 | 22x |
for (dataname in names(state)) { |
571 | 35x |
fdataset <- self$get_filtered_dataset(dataname = dataname) |
572 | 35x |
dataset_state <- state[[dataname]] |
573 | ||
574 | 35x |
fdataset$set_filter_state( |
575 | 35x |
state = dataset_state, |
576 | 35x |
vars_include = self$get_filterable_varnames(dataname) |
577 |
) |
|
578 |
} |
|
579 | 21x |
logger::log_trace( |
580 | 21x |
"FilteredData$set_filter_state initialized, dataname: { paste(names(state), collapse = ' ') }" |
581 |
) |
|
582 | ||
583 | 21x |
invisible(NULL) |
584 |
}, |
|
585 | ||
586 |
#' @description |
|
587 |
#' Removes one or more `FilterState` of a `FilteredDataset` in a `FilteredData` object. |
|
588 |
#' |
|
589 |
#' @param state (`named list`)\cr |
|
590 |
#' nested list of filter selections applied to datasets |
|
591 |
#' |
|
592 |
#' @return `NULL` invisibly |
|
593 |
#' |
|
594 |
remove_filter_state = function(state) { |
|
595 | 2x |
checkmate::assert_subset(names(state), self$datanames()) |
596 | ||
597 | 2x |
logger::log_trace( |
598 | 2x |
"FilteredData$remove_filter_state called, dataname: { paste(names(state), collapse = ' ') }" |
599 |
) |
|
600 | ||
601 | 2x |
for (dataname in names(state)) { |
602 | 3x |
fdataset <- self$get_filtered_dataset(dataname = dataname) |
603 | 3x |
fdataset$remove_filter_state(state_id = state[[dataname]]) |
604 |
} |
|
605 | ||
606 | 2x |
logger::log_trace( |
607 | 2x |
"FilteredData$remove_filter_state done, dataname: { paste(names(state), collapse = ' ') }" |
608 |
) |
|
609 | ||
610 | 2x |
invisible(NULL) |
611 |
}, |
|
612 | ||
613 |
#' @description |
|
614 |
#' Remove all `FilterStates` of a `FilteredDataset` or all `FilterStates` |
|
615 |
#' of a `FilteredData` object. |
|
616 |
#' |
|
617 |
#' @param datanames (`character`)\cr |
|
618 |
#' datanames to remove their `FilterStates` or empty which removes |
|
619 |
#' all `FilterStates` in the `FilteredData` object |
|
620 |
#' |
|
621 |
#' @return `NULL` invisibly |
|
622 |
#' |
|
623 |
remove_all_filter_states = function(datanames = self$datanames()) { |
|
624 | 9x |
logger::log_trace( |
625 | 9x |
"FilteredData$remove_all_filter_states called, datanames: { paste(datanames, collapse = ', ') }" |
626 |
) |
|
627 | ||
628 | 9x |
for (dataname in datanames) { |
629 | 13x |
fdataset <- self$get_filtered_dataset(dataname = dataname) |
630 | 13x |
fdataset$state_lists_empty() |
631 |
} |
|
632 | ||
633 | 9x |
logger::log_trace( |
634 | 9x |
paste( |
635 | 9x |
"FilteredData$remove_all_filter_states removed all FilterStates,", |
636 | 9x |
"datanames: { paste(datanames, collapse = ', ') }" |
637 |
) |
|
638 |
) |
|
639 | ||
640 | 9x |
invisible(NULL) |
641 |
}, |
|
642 | ||
643 |
#' @description |
|
644 |
#' Sets this object from a bookmarked state. |
|
645 |
#' |
|
646 |
#' Only sets the filter state, does not set the data |
|
647 |
#' and the preprocessing code. The data should already have been set. |
|
648 |
#' Also checks the preprocessing code is identical if provided in the `state`. |
|
649 |
#' |
|
650 |
#' Since this function is used from the end-user part, its error messages |
|
651 |
#' are more verbose. We don't call the Shiny modals from here because this |
|
652 |
#' class may be used outside of a Shiny app. |
|
653 |
#' |
|
654 |
#' @param state (`named list`)\cr |
|
655 |
#' containing fields `data_hash`, `filter_states` and `preproc_code` |
|
656 |
#' @param check_data_hash (`logical`) whether to check that `md5sums` agree |
|
657 |
#' for the data; may not make sense with randomly generated data per session |
|
658 |
#' |
|
659 |
restore_state_from_bookmark = function(state, check_data_hash = TRUE) { |
|
660 | 1x |
stop("Pure virtual method") |
661 |
}, |
|
662 | ||
663 |
#' @description |
|
664 |
#' Disable the filter panel by adding `disable` class to `filter_add_vars` |
|
665 |
#' and `filter_panel_active_vars` tags in the User Interface. |
|
666 |
#' In addition, it will store the existing filter states in a private field called `cached_states` |
|
667 |
#' before removing all filter states from the object. |
|
668 |
#' |
|
669 |
filter_panel_disable = function() { |
|
670 | 4x |
private$filter_panel_active <- FALSE |
671 | 4x |
shinyjs::disable("filter_add_vars") |
672 | 4x |
shinyjs::disable("filter_active_vars") |
673 | 4x |
private$cached_states <- self$get_filter_state() |
674 | 4x |
self$remove_all_filter_states() |
675 | 4x |
invisible(NULL) |
676 |
}, |
|
677 | ||
678 |
#' @description enable the filter panel |
|
679 |
#' Enable the filter panel by adding `enable` class to `filter_add_vars` |
|
680 |
#' and `filter_active_vars` tags in the User Interface. |
|
681 |
#' In addition, it will restore the filter states from a private field called `cached_states`. |
|
682 |
#' |
|
683 |
filter_panel_enable = function() { |
|
684 | 3x |
private$filter_panel_active <- TRUE |
685 | 3x |
shinyjs::enable("filter_add_vars") |
686 | 3x |
shinyjs::enable("filter_active_vars") |
687 | 3x |
if (length(private$cached_states) && (length(self$get_filter_state()) == 0)) { |
688 | 1x |
self$set_filter_state(private$cached_states) |
689 |
} |
|
690 | 3x |
invisible(NULL) |
691 |
}, |
|
692 | ||
693 |
#' @description |
|
694 |
#' Gets the state of filter panel, if activated. |
|
695 |
#' |
|
696 |
get_filter_panel_active = function() { |
|
697 | 18x |
private$filter_panel_active |
698 |
}, |
|
699 | ||
700 |
#' @description |
|
701 |
#' Gets the id of the filter panel UI. |
|
702 |
get_filter_panel_ui_id = function() { |
|
703 | 2x |
private$filter_panel_ui_id |
704 |
}, |
|
705 | ||
706 |
# shiny modules ----- |
|
707 | ||
708 |
#' Module for the right filter panel in the teal app |
|
709 |
#' with a filter overview panel and a filter variable panel. |
|
710 |
#' |
|
711 |
#' This panel contains info about the number of observations left in |
|
712 |
#' the (active) datasets and allows to filter the datasets. |
|
713 |
#' |
|
714 |
#' @param id (`character(1)`)\cr |
|
715 |
#' module id |
|
716 |
ui_filter_panel = function(id) { |
|
717 | ! |
ns <- NS(id) |
718 | ! |
div( |
719 | ! |
id = ns(NULL), # used for hiding / showing |
720 | ! |
include_css_files(pattern = "filter-panel"), |
721 | ! |
div( |
722 | ! |
id = ns("switch-button"), |
723 | ! |
class = "flex justify-content-right", |
724 | ! |
div( |
725 | ! |
title = "Enable/Disable filtering", |
726 | ! |
shinyWidgets::prettySwitch( |
727 | ! |
ns("filter_panel_active"), |
728 | ! |
label = "", |
729 | ! |
status = "success", |
730 | ! |
fill = TRUE, |
731 | ! |
value = TRUE, |
732 | ! |
inline = FALSE, |
733 | ! |
width = 30 |
734 |
) |
|
735 |
) |
|
736 |
), |
|
737 | ! |
div( |
738 | ! |
id = ns("filters_overview"), # not used, can be used to customize CSS behavior |
739 | ! |
class = "well", |
740 | ! |
tags$div( |
741 | ! |
class = "row", |
742 | ! |
tags$div( |
743 | ! |
class = "col-sm-9", |
744 | ! |
tags$label("Active Filter Summary", class = "text-primary mb-4") |
745 |
), |
|
746 | ! |
tags$div( |
747 | ! |
class = "col-sm-3", |
748 | ! |
actionLink( |
749 | ! |
ns("minimise_filter_overview"), |
750 | ! |
label = NULL, |
751 | ! |
icon = icon("angle-down", lib = "font-awesome"), |
752 | ! |
title = "Minimise panel", |
753 | ! |
class = "remove pull-right" |
754 |
) |
|
755 |
) |
|
756 |
), |
|
757 | ! |
tags$br(), |
758 | ! |
div( |
759 | ! |
id = ns("filters_overview_contents"), |
760 | ! |
self$ui_filter_overview(ns("teal_filters_info")) |
761 |
) |
|
762 |
), |
|
763 | ! |
div( |
764 | ! |
id = ns("filter_active_vars"), # not used, can be used to customize CSS behavior |
765 | ! |
class = "well", |
766 | ! |
tags$div( |
767 | ! |
class = "row", |
768 | ! |
tags$div( |
769 | ! |
class = "col-sm-6", |
770 | ! |
tags$label("Active Filter Variables", class = "text-primary mb-4") |
771 |
), |
|
772 | ! |
tags$div( |
773 | ! |
class = "col-sm-6", |
774 | ! |
actionLink( |
775 | ! |
ns("remove_all_filters"), |
776 | ! |
label = "", |
777 | ! |
icon("circle-xmark", lib = "font-awesome"), |
778 | ! |
title = "Remove active filters", |
779 | ! |
class = "remove_all pull-right" |
780 |
), |
|
781 | ! |
actionLink( |
782 | ! |
ns("minimise_filter_active"), |
783 | ! |
label = NULL, |
784 | ! |
icon = icon("angle-down", lib = "font-awesome"), |
785 | ! |
title = "Minimise panel", |
786 | ! |
class = "remove pull-right" |
787 |
) |
|
788 |
) |
|
789 |
), |
|
790 | ! |
div( |
791 | ! |
id = ns("filter_active_vars_contents"), |
792 | ! |
tagList( |
793 | ! |
lapply( |
794 | ! |
self$datanames(), |
795 | ! |
function(dataname) { |
796 | ! |
fdataset <- self$get_filtered_dataset(dataname) |
797 | ! |
fdataset$ui(id = ns(private$get_ui_id(dataname))) |
798 |
} |
|
799 |
) |
|
800 |
) |
|
801 |
), |
|
802 | ! |
shinyjs::hidden( |
803 | ! |
div( |
804 | ! |
id = ns("filters_active_count"), |
805 | ! |
textOutput(ns("teal_filters_count")) |
806 |
) |
|
807 |
) |
|
808 |
), |
|
809 | ! |
div( |
810 | ! |
id = ns("filter_add_vars"), # not used, can be used to customize CSS behavior |
811 | ! |
class = "well", |
812 | ! |
tags$div( |
813 | ! |
class = "row", |
814 | ! |
tags$div( |
815 | ! |
class = "col-sm-9", |
816 | ! |
tags$label("Add Filter Variables", class = "text-primary mb-4") |
817 |
), |
|
818 | ! |
tags$div( |
819 | ! |
class = "col-sm-3", |
820 | ! |
actionLink( |
821 | ! |
ns("minimise_filter_add_vars"), |
822 | ! |
label = NULL, |
823 | ! |
icon = icon("angle-down", lib = "font-awesome"), |
824 | ! |
title = "Minimise panel", |
825 | ! |
class = "remove pull-right" |
826 |
) |
|
827 |
) |
|
828 |
), |
|
829 | ! |
div( |
830 | ! |
id = ns("filter_add_vars_contents"), |
831 | ! |
tagList( |
832 | ! |
lapply( |
833 | ! |
self$datanames(), |
834 | ! |
function(dataname) { |
835 | ! |
fdataset <- self$get_filtered_dataset(dataname) |
836 | ! |
id <- ns(private$get_ui_add_filter_id(dataname)) |
837 |
# add span with same id to show / hide |
|
838 | ! |
return( |
839 | ! |
span( |
840 | ! |
id = id, |
841 | ! |
fdataset$ui_add_filter_state(id) |
842 |
) |
|
843 |
) |
|
844 |
} |
|
845 |
) |
|
846 |
) |
|
847 |
) |
|
848 |
) |
|
849 |
) |
|
850 |
}, |
|
851 | ||
852 |
#' Server function for filter panel |
|
853 |
#' |
|
854 |
#' @param id (`character(1)`)\cr |
|
855 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
856 |
#' @param active_datanames `function / reactive` returning datanames that |
|
857 |
#' should be shown on the filter panel, |
|
858 |
#' must be a subset of the `datanames` argument provided to `ui_filter_panel`; |
|
859 |
#' if the function returns `NULL` (as opposed to `character(0)`), the filter |
|
860 |
#' panel will be hidden |
|
861 |
#' @return `moduleServer` function which returns `NULL` |
|
862 |
srv_filter_panel = function(id, active_datanames = function() "all") { |
|
863 | 6x |
stopifnot( |
864 | 6x |
is.function(active_datanames) || is.reactive(active_datanames) |
865 |
) |
|
866 | 6x |
moduleServer( |
867 | 6x |
id = id, |
868 | 6x |
function(input, output, session) { |
869 | 6x |
logger::log_trace("FilteredData$srv_filter_panel initializing") |
870 | 6x |
shiny::setBookmarkExclude("remove_all_filters") |
871 | 6x |
self$srv_filter_overview( |
872 | 6x |
id = "teal_filters_info", |
873 | 6x |
active_datanames = active_datanames |
874 |
) |
|
875 | ||
876 | 6x |
shiny::observeEvent(input$minimise_filter_overview, { |
877 | ! |
shinyjs::toggle("filters_overview_contents") |
878 | ! |
toggle_icon(session$ns("minimise_filter_overview"), c("fa-angle-right", "fa-angle-down")) |
879 | ! |
toggle_title(session$ns("minimise_filter_overview"), c("Restore panel", "Minimise Panel")) |
880 |
}) |
|
881 | ||
882 | 6x |
shiny::observeEvent(input$minimise_filter_active, { |
883 | ! |
shinyjs::toggle("filter_active_vars_contents") |
884 | ! |
shinyjs::toggle("filters_active_count") |
885 | ! |
toggle_icon(session$ns("minimise_filter_active"), c("fa-angle-right", "fa-angle-down")) |
886 | ! |
toggle_title(session$ns("minimise_filter_active"), c("Restore panel", "Minimise Panel")) |
887 |
}) |
|
888 | ||
889 | 6x |
shiny::observeEvent(private$get_filter_count(), { |
890 | ! |
shinyjs::toggle("remove_all_filters", condition = private$get_filter_count() != 0) |
891 | ! |
shinyjs::show("filter_active_vars_contents") |
892 | ! |
shinyjs::hide("filters_active_count") |
893 | ! |
toggle_icon(session$ns("minimise_filter_active"), c("fa-angle-right", "fa-angle-down"), TRUE) |
894 | ! |
toggle_title(session$ns("minimise_filter_active"), c("Restore panel", "Minimise Panel"), TRUE) |
895 |
}) |
|
896 | ||
897 | 6x |
shiny::observeEvent(input$minimise_filter_add_vars, { |
898 | ! |
shinyjs::toggle("filter_add_vars_contents") |
899 | ! |
toggle_icon(session$ns("minimise_filter_add_vars"), c("fa-angle-right", "fa-angle-down")) |
900 | ! |
toggle_title(session$ns("minimise_filter_add_vars"), c("Restore panel", "Minimise Panel")) |
901 |
}) |
|
902 | ||
903 |
# use isolate because we assume that the number of datasets does not change |
|
904 |
# over the course of the teal app |
|
905 |
# alternatively, one can proceed as in modules_filter_items to dynamically insert, remove UIs |
|
906 | 6x |
isol_datanames <- isolate(self$datanames()) # they are already ordered |
907 |
# should not use for-loop as variables are otherwise only bound by reference |
|
908 |
# and last dataname would be used |
|
909 | 6x |
lapply( |
910 | 6x |
isol_datanames, |
911 | 6x |
function(dataname) { |
912 | 8x |
fdataset <- self$get_filtered_dataset(dataname) |
913 | 8x |
fdataset$server(id = private$get_ui_id(dataname)) |
914 |
} |
|
915 |
) |
|
916 | ||
917 | 6x |
lapply( |
918 | 6x |
isol_datanames, |
919 | 6x |
function(dataname) { |
920 | 8x |
fdataset <- self$get_filtered_dataset(dataname) |
921 | 8x |
fdataset$srv_add_filter_state( |
922 | 8x |
id = private$get_ui_add_filter_id(dataname), |
923 | 8x |
vars_include = self$get_filterable_varnames(dataname) |
924 |
) |
|
925 |
} |
|
926 |
) |
|
927 | ||
928 | 6x |
output$teal_filters_count <- shiny::renderText({ |
929 | ! |
n_filters_active <- private$get_filter_count() |
930 | ! |
shiny::req(n_filters_active > 0L) |
931 | ! |
sprintf( |
932 | ! |
"%s filter%s applied across datasets", |
933 | ! |
n_filters_active, |
934 | ! |
ifelse(n_filters_active == 1, "", "s") |
935 |
) |
|
936 |
}) |
|
937 | ||
938 | 6x |
private$filter_panel_ui_id <- session$ns(NULL) |
939 | 6x |
observeEvent( |
940 | 6x |
eventExpr = input$filter_panel_active, |
941 | 6x |
handlerExpr = { |
942 | ! |
if (isTRUE(input$filter_panel_active)) { |
943 | ! |
self$filter_panel_enable() |
944 | ! |
logger::log_trace("Enable the Filtered Panel with the filter_panel_enable method") |
945 |
} else { |
|
946 | ! |
self$filter_panel_disable() |
947 | ! |
logger::log_trace("Disable the Filtered Panel with the filter_panel_enable method") |
948 |
} |
|
949 | 6x |
}, ignoreNULL = TRUE |
950 |
) |
|
951 | ||
952 | 6x |
observeEvent( |
953 | 6x |
eventExpr = active_datanames(), |
954 | 6x |
handlerExpr = { |
955 | ! |
private$hide_inactive_datasets(active_datanames) |
956 |
}, |
|
957 | 6x |
priority = 1 |
958 |
) |
|
959 | ||
960 | 6x |
observeEvent(input$remove_all_filters, { |
961 | ! |
logger::log_trace("FilteredData$srv_filter_panel@1 removing all filters") |
962 | ! |
lapply(self$datanames(), function(dataname) { |
963 | ! |
fdataset <- self$get_filtered_dataset(dataname = dataname) |
964 | ! |
fdataset$state_lists_empty() |
965 |
}) |
|
966 | ! |
logger::log_trace("FilteredData$srv_filter_panel@1 removed all filters") |
967 |
}) |
|
968 | ||
969 | 6x |
logger::log_trace("FilteredData$srv_filter_panel initialized") |
970 | 6x |
NULL |
971 |
} |
|
972 |
) |
|
973 |
}, |
|
974 | ||
975 |
#' Creates the UI for the module showing counts for each dataset |
|
976 |
#' contrasting the filtered to the full unfiltered dataset |
|
977 |
#' |
|
978 |
#' Per dataset, it displays |
|
979 |
#' the number of rows/observations in each dataset, |
|
980 |
#' the number of unique subjects. |
|
981 |
#' |
|
982 |
#' @param id module id |
|
983 |
ui_filter_overview = function(id) { |
|
984 | ! |
ns <- NS(id) |
985 | ||
986 | ! |
div( |
987 | ! |
class = "teal_active_summary_filter_panel", |
988 | ! |
tableOutput(ns("table")) |
989 |
) |
|
990 |
}, |
|
991 | ||
992 |
#' Server function to display the number of records in the filtered and unfiltered |
|
993 |
#' data |
|
994 |
#' |
|
995 |
#' @param id (`character(1)`)\cr |
|
996 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
997 |
#' @param active_datanames (`function`, `reactive`)\cr |
|
998 |
#' returning datanames that should be shown on the filter panel, |
|
999 |
#' must be a subset of the `datanames` argument provided to `ui_filter_panel`; |
|
1000 |
#' if the function returns `NULL` (as opposed to `character(0)`), the filter |
|
1001 |
#' panel will be hidden. |
|
1002 |
#' @return `moduleServer` function which returns `NULL` |
|
1003 |
srv_filter_overview = function(id, active_datanames = function() "all") { |
|
1004 | 6x |
stopifnot( |
1005 | 6x |
is.function(active_datanames) || is.reactive(active_datanames) |
1006 |
) |
|
1007 | 6x |
moduleServer( |
1008 | 6x |
id = id, |
1009 | 6x |
function(input, output, session) { |
1010 | 6x |
logger::log_trace("FilteredData$srv_filter_overview initializing") |
1011 | 6x |
output$table <- renderUI({ |
1012 | ! |
logger::log_trace("FilteredData$srv_filter_overview@1 updating counts") |
1013 | ! |
datanames <- if (identical(active_datanames(), "all")) { |
1014 | ! |
self$datanames() |
1015 |
} else { |
|
1016 | ! |
active_datanames() |
1017 |
} |
|
1018 | ||
1019 | ! |
if (length(datanames) == 0) { |
1020 | ! |
return(NULL) |
1021 |
} |
|
1022 | ||
1023 | ! |
datasets_df <- self$get_filter_overview(datanames = datanames) |
1024 | ||
1025 | ! |
body_html <- lapply( |
1026 | ! |
seq_len(nrow(datasets_df)), |
1027 | ! |
function(x) { |
1028 | ! |
tags$tr( |
1029 | ! |
tags$td(rownames(datasets_df)[x]), |
1030 | ! |
tags$td(datasets_df[x, 1]), |
1031 | ! |
tags$td(datasets_df[x, 2]) |
1032 |
) |
|
1033 |
} |
|
1034 |
) |
|
1035 | ||
1036 | ! |
header_html <- tags$tr( |
1037 | ! |
tags$td(""), |
1038 | ! |
tags$td(colnames(datasets_df)[1]), |
1039 | ! |
tags$td(colnames(datasets_df)[2]) |
1040 |
) |
|
1041 | ||
1042 | ! |
table_html <- tags$table( |
1043 | ! |
class = "table custom-table", |
1044 | ! |
tags$thead(header_html), |
1045 | ! |
tags$tbody(body_html) |
1046 |
) |
|
1047 | ! |
logger::log_trace("FilteredData$srv_filter_overview@1 updated counts") |
1048 | ! |
table_html |
1049 |
}) |
|
1050 | ||
1051 | 6x |
shiny::outputOptions(output, "table", suspendWhenHidden = FALSE) |
1052 | 6x |
logger::log_trace("FilteredData$srv_filter_overview initialized") |
1053 | 6x |
NULL |
1054 |
} |
|
1055 |
) |
|
1056 |
} |
|
1057 |
), |
|
1058 | ||
1059 |
## __Private Methods ==== |
|
1060 |
private = list( |
|
1061 |
# selectively hide / show to only show `active_datanames` out of all datanames |
|
1062 |
hide_inactive_datasets = function(active_datanames) { |
|
1063 | ! |
lapply( |
1064 | ! |
self$datanames(), |
1065 | ! |
function(dataname) { |
1066 | ! |
id_add_filter <- private$get_ui_add_filter_id(dataname) |
1067 | ! |
id_filter_dataname <- private$get_ui_id(dataname) |
1068 | ||
1069 | ! |
if (dataname %in% active_datanames()) { |
1070 |
# shinyjs takes care of the namespace around the id |
|
1071 | ! |
shinyjs::show(id_add_filter) |
1072 | ! |
shinyjs::show(id_filter_dataname) |
1073 |
} else { |
|
1074 | ! |
shinyjs::hide(id_add_filter) |
1075 | ! |
shinyjs::hide(id_filter_dataname) |
1076 |
} |
|
1077 |
} |
|
1078 |
) |
|
1079 |
}, |
|
1080 | ||
1081 |
# private attributes ---- |
|
1082 |
filtered_datasets = list(), |
|
1083 | ||
1084 |
# activate/deactivate filter panel |
|
1085 |
filter_panel_active = TRUE, |
|
1086 | ||
1087 |
# filter panel ui id |
|
1088 |
filter_panel_ui_id = character(0), |
|
1089 | ||
1090 |
# whether the datasets had a reproducibility check |
|
1091 |
.check = FALSE, |
|
1092 | ||
1093 |
# preprocessing code used to generate the unfiltered datasets as a string |
|
1094 |
code = NULL, |
|
1095 | ||
1096 |
# keys used for joining/filtering data a JoinKeys object (see teal.data) |
|
1097 |
keys = NULL, |
|
1098 | ||
1099 |
# reactive i.e. filtered data |
|
1100 |
reactive_data = list(), |
|
1101 |
cached_states = NULL, |
|
1102 | ||
1103 |
# we implement these functions as checks rather than returning logicals so they can |
|
1104 |
# give informative error messages immediately |
|
1105 | ||
1106 |
# @details |
|
1107 |
# Composes id for the FilteredDataset shiny element (active filter vars) |
|
1108 |
# @param dataname (`character(1)`) name of the dataset which ui is composed for. |
|
1109 |
# @return `character(1)` - `<dataname>_filter` |
|
1110 |
get_ui_id = function(dataname) { |
|
1111 | 8x |
sprintf("%s_filter", dataname) |
1112 |
}, |
|
1113 | ||
1114 |
# @details |
|
1115 |
# Composes id for the FilteredDataset shiny element (add filter state) |
|
1116 |
# @param dataname (`character(1)`) name of the dataset which ui is composed for. |
|
1117 |
# @return `character(1)` - `<dataname>_filter` |
|
1118 |
get_ui_add_filter_id = function(dataname) { |
|
1119 | 8x |
sprintf("add_%s_filter", dataname) |
1120 |
}, |
|
1121 | ||
1122 |
# @details |
|
1123 |
# Validates the state of this FilteredData. |
|
1124 |
# The call to this function should be isolated to avoid a reactive dependency. |
|
1125 |
# Getting the names of a reactivevalues also needs a reactive context. |
|
1126 |
validate = function() { |
|
1127 |
# Note: Here, we directly refer to the private attributes because the goal of this |
|
1128 |
# function is to check the underlying attributes and the get / set functions might be corrupted |
|
1129 | ||
1130 | ! |
has_same_names <- function(x, y) setequal(names(x), names(y)) |
1131 |
# check `filter_states` are all valid |
|
1132 | ! |
lapply( |
1133 | ! |
names(private$filter_states), |
1134 | ! |
function(dataname) { |
1135 | ! |
stopifnot(is.list(private$filter_states)) # non-NULL, possibly empty list |
1136 | ! |
lapply( |
1137 | ! |
names(private$filter_states[[dataname]]), |
1138 | ! |
function(varname) { |
1139 | ! |
var_state <- private$filter_states[[dataname]][[varname]] |
1140 | ! |
stopifnot(!is.null(var_state)) # should not be NULL, see doc of this attribute |
1141 | ! |
check_valid_filter_state( |
1142 | ! |
filter_state = var_state, |
1143 | ! |
dataname = dataname, |
1144 | ! |
varname = varname |
1145 |
) |
|
1146 |
} |
|
1147 |
) |
|
1148 |
} |
|
1149 |
) |
|
1150 | ||
1151 | ! |
return(invisible(NULL)) |
1152 |
}, |
|
1153 | ||
1154 |
# @description |
|
1155 |
# Checks if the dataname exists and |
|
1156 |
# (if provided) that varname is a valid column in the dataset |
|
1157 |
# |
|
1158 |
# Stops when this is not the case. |
|
1159 |
# |
|
1160 |
# @param dataname (`character`) name of the dataset |
|
1161 |
# @param varname (`character`) column within the dataset; |
|
1162 |
# if `NULL`, this check is not performed |
|
1163 |
check_data_varname_exists = function(dataname, varname = NULL) { |
|
1164 | 59x |
checkmate::assert_string(dataname) |
1165 | 59x |
checkmate::assert_string(varname, null.ok = TRUE) |
1166 | ||
1167 | 59x |
isolate({ |
1168 |
# we isolate everything because we don't want to trigger again when datanames |
|
1169 |
# change (which also triggers when any of the data changes) |
|
1170 | 59x |
if (!dataname %in% names(self$get_filtered_dataset())) { |
1171 |
# data must be set already |
|
1172 | 1x |
stop(paste("data", dataname, "is not available")) |
1173 |
} |
|
1174 | 58x |
if (!is.null(varname) && !(varname %in% self$get_varnames(dataname = dataname))) { |
1175 | ! |
stop(paste("variable", varname, "is not in data", dataname)) |
1176 |
} |
|
1177 |
}) |
|
1178 | ||
1179 | 58x |
return(invisible(NULL)) |
1180 |
}, |
|
1181 | ||
1182 |
# @description |
|
1183 |
# Gets the number of active `FilterState` objects in all `FilterStates` |
|
1184 |
# in all `FilteredDataset`s in this `FilteredData` object. |
|
1185 |
# @return `integer(1)` |
|
1186 |
get_filter_count = function() { |
|
1187 | 4x |
sum(vapply(self$datanames(), function(dataname) { |
1188 | 8x |
self$get_filtered_dataset(dataname)$get_filter_count() |
1189 | 4x |
}, numeric(1L))) |
1190 |
} |
|
1191 |
) |
|
1192 |
) |
|
1193 | ||
1194 |
# Wrapper functions for `FilteredData` class ---- |
|
1195 | ||
1196 | ||
1197 |
#' Gets filter expression for multiple datanames taking into account its order. |
|
1198 |
#' |
|
1199 |
#' @description `r lifecycle::badge("stable")` |
|
1200 |
#' To be used in show R code button. |
|
1201 |
#' |
|
1202 |
#' @param datasets (`FilteredData`) |
|
1203 |
#' @param datanames (`character`) vector of dataset names |
|
1204 |
#' |
|
1205 |
#' @export |
|
1206 |
#' |
|
1207 |
#' @return (`expression`) |
|
1208 |
get_filter_expr <- function(datasets, datanames = datasets$datanames()) { |
|
1209 | 1x |
checkmate::assert_character(datanames, min.len = 1, any.missing = FALSE) |
1210 | 1x |
stopifnot( |
1211 | 1x |
is(datasets, "FilteredData"), |
1212 | 1x |
all(datanames %in% datasets$datanames()) |
1213 |
) |
|
1214 | ||
1215 | 1x |
paste( |
1216 | 1x |
unlist(lapply( |
1217 | 1x |
datanames, |
1218 | 1x |
function(dataname) { |
1219 | 2x |
datasets$get_call(dataname) |
1220 |
} |
|
1221 |
)), |
|
1222 | 1x |
collapse = "\n" |
1223 |
) |
|
1224 |
} |
1 |
#' Initializes `FilterState` |
|
2 |
#' |
|
3 |
#' Initializes `FilterState` depending on a variable class.\cr |
|
4 |
#' @param x (`vector`)\cr |
|
5 |
#' values of the variable used in filter |
|
6 |
#' |
|
7 |
#' @param varname (`character(1)`)\cr |
|
8 |
#' name of the variable. |
|
9 |
#' |
|
10 |
#' @param varlabel (`character(0)`, `character(1)` or `NULL`)\cr |
|
11 |
#' label of the variable (optional). |
|
12 |
#' |
|
13 |
#' @param dataname (`character(1)`)\cr |
|
14 |
#' optional name of dataset where `x` is taken from. Must be specified |
|
15 |
#' if `extract_type` argument is not empty. |
|
16 |
#' |
|
17 |
#' @param extract_type (`character(0)`, `character(1)`)\cr |
|
18 |
#' whether condition calls should be prefixed by dataname. Possible values: |
|
19 |
#' \itemize{ |
|
20 |
#' \item{`character(0)` (default)}{ `varname` in the condition call will not be prefixed} |
|
21 |
#' \item{`"list"`}{ `varname` in the condition call will be returned as `<dataname>$<varname>`} |
|
22 |
#' \item{`"matrix"`}{ `varname` in the condition call will be returned as `<dataname>[, <varname>]`} |
|
23 |
#' } |
|
24 |
#' @keywords internal |
|
25 |
#' |
|
26 |
#' @examples |
|
27 |
#' filter_state <- teal.slice:::RangeFilterState$new( |
|
28 |
#' c(1:10, NA, Inf), |
|
29 |
#' varname = "x", |
|
30 |
#' varlabel = "Pretty name", |
|
31 |
#' dataname = "dataname", |
|
32 |
#' extract_type = "matrix" |
|
33 |
#' ) |
|
34 |
#' |
|
35 |
#' filter_state$get_varname() |
|
36 |
#' filter_state$get_varlabel() |
|
37 |
#' isolate(filter_state$get_call()) |
|
38 |
#' \dontrun{ |
|
39 |
#' shinyApp( |
|
40 |
#' ui = fluidPage( |
|
41 |
#' isolate(filter_state$ui(id = "app")), |
|
42 |
#' verbatimTextOutput("call") |
|
43 |
#' ), |
|
44 |
#' server = function(input, output, session) { |
|
45 |
#' filter_state$server("app") |
|
46 |
#' |
|
47 |
#' output$call <- renderText( |
|
48 |
#' deparse1(filter_state$get_call(), collapse = "\n") |
|
49 |
#' ) |
|
50 |
#' } |
|
51 |
#' ) |
|
52 |
#' } |
|
53 |
#' @return `FilterState` object |
|
54 |
init_filter_state <- function(x, |
|
55 |
varname, |
|
56 |
varlabel = attr(x, "label"), |
|
57 |
dataname = NULL, |
|
58 |
extract_type = character(0)) { |
|
59 | 168x |
checkmate::assert_string(varname) |
60 | 167x |
checkmate::assert_character(varlabel, max.len = 1L, any.missing = FALSE, null.ok = TRUE) |
61 | 167x |
checkmate::assert_string(dataname, null.ok = TRUE) |
62 | 167x |
checkmate::assert_character(extract_type, max.len = 1L, any.missing = FALSE) |
63 | 167x |
if (length(extract_type) == 1) { |
64 | 47x |
checkmate::assert_choice(extract_type, choices = c("list", "matrix")) |
65 |
} |
|
66 | 167x |
if (length(extract_type) == 1 && is.null(dataname)) { |
67 | 1x |
stop("if extract_type is specified, dataname must also be specified") |
68 |
} |
|
69 | ||
70 | 45x |
if (is.null(varlabel)) varlabel <- character(0L) |
71 | ||
72 | 166x |
if (all(is.na(x))) { |
73 | 1x |
return( |
74 | 1x |
EmptyFilterState$new( |
75 | 1x |
x = x, |
76 | 1x |
varname = varname, |
77 | 1x |
varlabel = varlabel, |
78 | 1x |
dataname = dataname, |
79 | 1x |
extract_type = extract_type |
80 |
) |
|
81 |
) |
|
82 |
} |
|
83 | 165x |
UseMethod("init_filter_state") |
84 |
} |
|
85 | ||
86 |
#' @keywords internal |
|
87 |
#' @export |
|
88 |
init_filter_state.default <- function(x, |
|
89 |
varname, |
|
90 |
varlabel = attr(x, "label"), |
|
91 |
dataname = NULL, |
|
92 |
extract_type = character(0)) { |
|
93 | 1x |
if (is.null(varlabel)) varlabel <- character(0) |
94 | 1x |
FilterState$new( |
95 | 1x |
x = x, |
96 | 1x |
varname = varname, |
97 | 1x |
varlabel = varlabel, |
98 | 1x |
dataname = dataname, |
99 | 1x |
extract_type = extract_type |
100 |
) |
|
101 |
} |
|
102 | ||
103 |
#' @keywords internal |
|
104 |
#' @export |
|
105 |
init_filter_state.logical <- function(x, |
|
106 |
varname, |
|
107 |
varlabel = attr(x, "label"), |
|
108 |
dataname = NULL, |
|
109 |
extract_type = character(0)) { |
|
110 | 1x |
if (is.null(varlabel)) varlabel <- character(0) |
111 | 1x |
LogicalFilterState$new( |
112 | 1x |
x = x, |
113 | 1x |
varname = varname, |
114 | 1x |
varlabel = varlabel, |
115 | 1x |
dataname = dataname, |
116 | 1x |
extract_type = extract_type |
117 |
) |
|
118 |
} |
|
119 | ||
120 |
#' @keywords internal |
|
121 |
#' @export |
|
122 |
init_filter_state.numeric <- function(x, |
|
123 |
varname, |
|
124 |
varlabel = attr(x, "label"), |
|
125 |
dataname = NULL, |
|
126 |
extract_type = character(0)) { |
|
127 | 9x |
if (is.null(varlabel)) varlabel <- character(0) |
128 | 89x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
129 | 31x |
ChoicesFilterState$new( |
130 | 31x |
x = x, |
131 | 31x |
varname = varname, |
132 | 31x |
varlabel = varlabel, |
133 | 31x |
dataname = dataname, |
134 | 31x |
extract_type = extract_type |
135 |
) |
|
136 |
} else { |
|
137 | 58x |
RangeFilterState$new( |
138 | 58x |
x = x, |
139 | 58x |
varname = varname, |
140 | 58x |
varlabel = varlabel, |
141 | 58x |
dataname = dataname, |
142 | 58x |
extract_type = extract_type |
143 |
) |
|
144 |
} |
|
145 |
} |
|
146 | ||
147 |
#' @keywords internal |
|
148 |
#' @export |
|
149 |
init_filter_state.factor <- function(x, |
|
150 |
varname, |
|
151 |
varlabel = attr(x, "label"), |
|
152 |
dataname = NULL, |
|
153 |
extract_type = character(0)) { |
|
154 | ! |
if (is.null(varlabel)) varlabel <- character(0) |
155 | 26x |
ChoicesFilterState$new( |
156 | 26x |
x = x, |
157 | 26x |
varname = varname, |
158 | 26x |
varlabel = varlabel, |
159 | 26x |
dataname = dataname, |
160 | 26x |
extract_type = extract_type |
161 |
) |
|
162 |
} |
|
163 | ||
164 |
#' @keywords internal |
|
165 |
#' @export |
|
166 |
init_filter_state.character <- function(x, |
|
167 |
varname, |
|
168 |
varlabel = attr(x, "label"), |
|
169 |
dataname = NULL, |
|
170 |
extract_type = character(0)) { |
|
171 | 27x |
if (is.null(varlabel)) varlabel <- character(0) |
172 | 42x |
ChoicesFilterState$new( |
173 | 42x |
x = x, |
174 | 42x |
varname = varname, |
175 | 42x |
varlabel = varlabel, |
176 | 42x |
dataname = dataname, |
177 | 42x |
extract_type = extract_type |
178 |
) |
|
179 |
} |
|
180 | ||
181 |
#' @keywords internal |
|
182 |
#' @export |
|
183 |
init_filter_state.Date <- function(x, |
|
184 |
varname, |
|
185 |
varlabel = attr(x, "label"), |
|
186 |
dataname = NULL, |
|
187 |
extract_type = character(0)) { |
|
188 | 2x |
if (is.null(varlabel)) varlabel <- character(0) |
189 | 2x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
190 | 1x |
ChoicesFilterState$new( |
191 | 1x |
x = x, |
192 | 1x |
varname = varname, |
193 | 1x |
varlabel = varlabel, |
194 | 1x |
dataname = dataname, |
195 | 1x |
extract_type = extract_type |
196 |
) |
|
197 |
} else { |
|
198 | 1x |
DateFilterState$new( |
199 | 1x |
x = x, |
200 | 1x |
varname = varname, |
201 | 1x |
varlabel = varlabel, |
202 | 1x |
dataname = dataname, |
203 | 1x |
extract_type = extract_type |
204 |
) |
|
205 |
} |
|
206 |
} |
|
207 | ||
208 |
#' @keywords internal |
|
209 |
#' @export |
|
210 |
init_filter_state.POSIXct <- function(x, |
|
211 |
varname, |
|
212 |
varlabel = attr(x, "label"), |
|
213 |
dataname = NULL, |
|
214 |
extract_type = character(0)) { |
|
215 | 2x |
if (is.null(varlabel)) varlabel <- character(0) |
216 | 2x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
217 | 1x |
ChoicesFilterState$new( |
218 | 1x |
x = x, |
219 | 1x |
varname = varname, |
220 | 1x |
varlabel = varlabel, |
221 | 1x |
dataname = dataname, |
222 | 1x |
extract_type = extract_type |
223 |
) |
|
224 |
} else { |
|
225 | 1x |
DatetimeFilterState$new( |
226 | 1x |
x = x, |
227 | 1x |
varname = varname, |
228 | 1x |
varlabel = varlabel, |
229 | 1x |
dataname = dataname, |
230 | 1x |
extract_type = extract_type |
231 |
) |
|
232 |
} |
|
233 |
} |
|
234 | ||
235 |
#' @keywords internal |
|
236 |
#' @export |
|
237 |
init_filter_state.POSIXlt <- function(x, |
|
238 |
varname, |
|
239 |
varlabel = attr(x, "label"), |
|
240 |
dataname = NULL, |
|
241 |
extract_type = character(0)) { |
|
242 | 2x |
if (is.null(varlabel)) varlabel <- character(0) |
243 | 2x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
244 | 1x |
ChoicesFilterState$new( |
245 | 1x |
x = x, |
246 | 1x |
varname = varname, |
247 | 1x |
varlabel = varlabel, |
248 | 1x |
dataname = dataname, |
249 | 1x |
extract_type = extract_type |
250 |
) |
|
251 |
} else { |
|
252 | 1x |
DatetimeFilterState$new( |
253 | 1x |
x = x, |
254 | 1x |
varname = varname, |
255 | 1x |
varlabel = varlabel, |
256 | 1x |
dataname = dataname, |
257 | 1x |
extract_type = extract_type |
258 |
) |
|
259 |
} |
|
260 |
} |
|
261 | ||
262 |
#' Check that a given range is valid |
|
263 |
#' |
|
264 |
#' @param subinterval (`numeric` or `date`)\cr vector of length 2 to be compared against the full range. |
|
265 |
#' @param range (`numeric` or `date`)\cr vector of length 2 containing the full range to validate against. |
|
266 |
#' @param pre_msg `character` message to print before error for additional context. |
|
267 |
#' |
|
268 |
#' @return `NULL` if `subinterval` is a valid range or error with message otherwise. |
|
269 |
#' @keywords internal |
|
270 |
#' |
|
271 |
#' @examples |
|
272 |
#' \dontrun{ |
|
273 |
#' check_in_range(c(3, 1), c(1, 3)) |
|
274 |
#' check_in_range(c(0, 3), c(1, 3)) |
|
275 |
#' check_in_range( |
|
276 |
#' c(as.Date("2020-01-01"), as.Date("2020-01-20")), |
|
277 |
#' c(as.Date("2020-01-01"), as.Date("2020-01-02")) |
|
278 |
#' ) |
|
279 |
#' } |
|
280 |
check_in_range <- function(subinterval, range, pre_msg = "") { |
|
281 | 81x |
epsilon <- .Machine$double.eps^0.5 # needed for floating point arithmetic; same value as in base::all.equal() |
282 | 81x |
if ((length(subinterval) != 2)) { |
283 | ! |
stop( |
284 | ! |
sprintf( |
285 | ! |
"%s range length should be 2 while it is %s", |
286 | ! |
pre_msg, |
287 | ! |
length(subinterval) |
288 |
) |
|
289 |
) |
|
290 |
} |
|
291 | 81x |
if (subinterval[[2]] + epsilon < subinterval[[1]]) { |
292 | 2x |
stop(sprintf( |
293 | 2x |
"%s unexpected: the upper bound of the range lower than the lower bound \n %s < %s", |
294 | 2x |
pre_msg, |
295 | 2x |
subinterval[[2]], |
296 | 2x |
subinterval[[1]] |
297 |
)) |
|
298 |
} |
|
299 | ||
300 | 79x |
if ((subinterval[[1]] + epsilon < range[[1]]) || (subinterval[[2]] - epsilon > range[[2]])) { |
301 | ! |
stop( |
302 | ! |
sprintf( |
303 | ! |
"%s range (%s) not valid for full range (%s)", |
304 | ! |
pre_msg, toString(subinterval), toString(range) |
305 |
) |
|
306 |
) |
|
307 |
} |
|
308 |
} |
|
309 | ||
310 |
#' Check that one set is a subset of another |
|
311 |
#' |
|
312 |
#' Raises an error message if not and says which elements are not in the allowed `choices`. |
|
313 |
#' |
|
314 |
#' @param subset `collection-like` should be a subset of the second argument `choices` |
|
315 |
#' @param choices `collection-like` superset |
|
316 |
#' @param pre_msg `character` message to print before error should there be any errors |
|
317 |
#' @keywords internal |
|
318 |
#' |
|
319 |
#' @examples |
|
320 |
#' \dontrun{ |
|
321 |
#' check_in_subset <- check_in_subset |
|
322 |
#' check_in_subset(c("a", "b"), c("a", "b", "c")) |
|
323 |
#' \dontrun{ |
|
324 |
#' check_in_subset(c("a", "b"), c("b", "c"), pre_msg = "Error: ") |
|
325 |
#' # truncated because too long |
|
326 |
#' check_in_subset("a", LETTERS, pre_msg = "Error: ") |
|
327 |
#' } |
|
328 |
#' } |
|
329 |
check_in_subset <- function(subset, choices, pre_msg = "") { |
|
330 | 333x |
checkmate::assert_string(pre_msg) |
331 | ||
332 | 333x |
subset <- unique(subset) |
333 | 333x |
choices <- unique(choices) |
334 | ||
335 | 333x |
if (any(!(subset %in% choices))) { |
336 | 3x |
stop(paste0( |
337 | 3x |
pre_msg, |
338 | 3x |
"(", toString(subset[!(subset %in% choices)], width = 30), ")", |
339 | 3x |
" not in valid choices ", |
340 | 3x |
"(", toString(choices, width = 30), ")" |
341 | 3x |
), call. = FALSE) |
342 |
} |
|
343 | 330x |
return(invisible(NULL)) |
344 |
} |
|
345 | ||
346 |
#' Find containing limits for interval. |
|
347 |
#' |
|
348 |
#' Given an interval and a numeric vector, |
|
349 |
#' find the smallest interval within the numeric vector that contains the interval. |
|
350 |
#' |
|
351 |
#' This is a helper function for `RangeFilterState` that modifies slider selection |
|
352 |
#' so that the _subsetting call_ includes the value specified by the filter API call. |
|
353 |
#' |
|
354 |
#' Regardless of the underlying numeric data, the slider always presents 100 steps. |
|
355 |
#' The ticks on the slider do not represent actual observations but rather borders between virtual bins. |
|
356 |
#' Since the value selected on the slider is passed to `private$selected` and that in turn |
|
357 |
#' updates the slider selection, programmatic selection of arbitrary values may inadvertently shift |
|
358 |
#' the selection to the closest tick, thereby dropping the actual value set (if it exists in the data). |
|
359 |
#' |
|
360 |
#' This function purposely shifts the selection to the closest ticks whose values form an interval |
|
361 |
#' that will contain the interval defined by the filter API call. |
|
362 |
#' |
|
363 |
#' @param x `numeric(2)` interval to contain |
|
364 |
#' @param range `numeric(>=2)` vector of values to contain `x` in |
|
365 |
#' |
|
366 |
#' @return Numeric vector of length 2 that lies within `range`. |
|
367 |
#' |
|
368 |
#' @keywords internal |
|
369 |
#' |
|
370 |
#' @examples |
|
371 |
#' \dontrun{ |
|
372 |
#' ticks <- 1:10 |
|
373 |
#' values1 <- c(3, 5) |
|
374 |
#' contain_interval(values1, ticks) |
|
375 |
#' values2 <- c(3.1, 5.7) |
|
376 |
#' contain_interval(values2, ticks) |
|
377 |
#' values3 <- c(0, 20) |
|
378 |
#' contain_interval(values3, ticks) |
|
379 |
#'} |
|
380 |
contain_interval <- function(x, range) { |
|
381 | 169x |
checkmate::assert_numeric(x, len = 2L, any.missing = FALSE, sorted = TRUE) |
382 | 165x |
checkmate::assert_numeric(range, min.len = 2L, any.missing = FALSE, sorted = TRUE) |
383 | ||
384 | 161x |
x[1] <- Find(function(i) i <= x[1], range, nomatch = min(range), right = TRUE) |
385 | 161x |
x[2] <- Find(function(i) i >= x[2], range, nomatch = max(range)) |
386 | 161x |
x |
387 |
} |
1 |
#' @title `FilterStates` R6 class |
|
2 |
#' |
|
3 |
#' @description |
|
4 |
#' Abstract class that manages adding and removing `FilterState` objects |
|
5 |
#' and builds a \emph{subset expression}. |
|
6 |
#' |
|
7 |
#' A `FilterStates` object tracks all subsetting expressions |
|
8 |
#' (logical predicates that limit observations) associated with a given dataset |
|
9 |
#' and composes them into a single reproducible R expression |
|
10 |
#' that will assign a subset of the original data to a new variable. |
|
11 |
#' This expression is hereafter referred to as \emph{subset expression}. |
|
12 |
#' |
|
13 |
#' The \emph{subset expression} is constructed differently for different |
|
14 |
#' classes of the underlying data object and `FilterStates` subclasses. |
|
15 |
#' Currently implemented for `data.frame`, `matrix`, |
|
16 |
#' `SummarizedExperiment`, and `MultiAssayExperiment`. |
|
17 |
#' |
|
18 |
#' @keywords internal |
|
19 |
#' |
|
20 |
#' @examples |
|
21 |
#' library(shiny) |
|
22 |
#' filter_states <- teal.slice:::DFFilterStates$new( |
|
23 |
#' dataname = "data", |
|
24 |
#' varlabels = c(x = "x variable", SEX = "Sex"), |
|
25 |
#' datalabel = character(0), |
|
26 |
#' keys = character(0) |
|
27 |
#' ) |
|
28 |
#' filter_state <- teal.slice:::RangeFilterState$new( |
|
29 |
#' c(NA, Inf, seq(1:10)), |
|
30 |
#' varname = "x", |
|
31 |
#' varlabel = "x variable", |
|
32 |
#' dataname = "data", |
|
33 |
#' extract_type = "list" |
|
34 |
#' ) |
|
35 |
#' isolate(filter_state$set_selected(c(3L, 8L))) |
|
36 |
#' |
|
37 |
#' isolate( |
|
38 |
#' filter_states$state_list_push( |
|
39 |
#' x = filter_state, |
|
40 |
#' state_list_index = 1L, |
|
41 |
#' state_id = "x" |
|
42 |
#' ) |
|
43 |
#' ) |
|
44 |
#' isolate(filter_states$get_call()) |
|
45 |
#' |
|
46 |
FilterStates <- R6::R6Class( # nolint |
|
47 |
classname = "FilterStates", |
|
48 | ||
49 |
# public members ---- |
|
50 |
public = list( |
|
51 |
#' @description |
|
52 |
#' Initializes `FilterStates` object. |
|
53 |
#' |
|
54 |
#' Initializes `FilterStates` object by setting |
|
55 |
#' `dataname`, and `datalabel`. |
|
56 |
#' |
|
57 |
#' @param dataname (`character(1)`)\cr |
|
58 |
#' name of the data used in the expression |
|
59 |
#' specified to the function argument attached to this `FilterStates` |
|
60 |
#' @param datalabel (`character(0)` or `character(1)`)\cr |
|
61 |
#' text label value |
|
62 |
#' |
|
63 |
#' @return |
|
64 |
#' self invisibly |
|
65 |
#' |
|
66 |
initialize = function(dataname, datalabel) { |
|
67 | 392x |
checkmate::assert_string(dataname) |
68 | 390x |
checkmate::assert_character(datalabel, max.len = 1, any.missing = FALSE) |
69 | ||
70 | 390x |
private$dataname <- dataname |
71 | 390x |
private$datalabel <- datalabel |
72 | ||
73 | 390x |
logger::log_trace("Instantiated { class(self)[1] }, dataname: { private$dataname }") |
74 | 390x |
invisible(self) |
75 |
}, |
|
76 | ||
77 |
#' @description |
|
78 |
#' Returns the label of the dataset. |
|
79 |
#' |
|
80 |
#' @return `character(1)` the data label |
|
81 |
#' |
|
82 |
get_datalabel = function() { |
|
83 | 5x |
private$datalabel |
84 |
}, |
|
85 | ||
86 |
#' @description |
|
87 |
#' Returns a formatted string representing this `FilterStates` object. |
|
88 |
#' |
|
89 |
#' @param indent (`numeric(1)`) the number of spaces prepended to each line of the output |
|
90 |
#' |
|
91 |
#' @return `character(1)` the formatted string |
|
92 |
#' |
|
93 |
format = function(indent) { |
|
94 | ! |
sprintf( |
95 | ! |
paste( |
96 | ! |
"%sThis is an instance of an abstract class.", |
97 | ! |
"Use child class constructors to instantiate objects." |
98 |
), |
|
99 | ! |
paste(rep(" ", indent), collapse = "") |
100 |
) |
|
101 |
}, |
|
102 | ||
103 |
#' @description |
|
104 |
#' Filter call |
|
105 |
#' |
|
106 |
#' Builds \emph{subset expression} from condition calls stored in `FilterState` |
|
107 |
#' objects selection. The `lhs` of the expression is `private$dataname`. |
|
108 |
#' The `rhs` is a call to `self$get_fun()` with `private$dataname` |
|
109 |
#' as argument and a list of condition calls from `FilterState` objects |
|
110 |
#' stored in `private$state_list`. |
|
111 |
#' If no filters are applied, |
|
112 |
#' `NULL` is returned to avoid no-op calls such as `x <- x`. |
|
113 |
#' |
|
114 |
#' @return `call` or `NULL` |
|
115 |
#' |
|
116 |
get_call = function() { |
|
117 |
# state_list (list) names must be the same as argument of the function |
|
118 |
# for ... list should be unnamed |
|
119 | 120x |
states_list <- private$state_list |
120 | 120x |
filter_items <- sapply( |
121 | 120x |
X = states_list, |
122 | 120x |
USE.NAMES = TRUE, |
123 | 120x |
simplify = FALSE, |
124 | 120x |
function(state_list) { |
125 | 165x |
items <- state_list() |
126 | 165x |
filtered_items <- Filter(f = function(x) x$is_any_filtered(), x = items) |
127 | 165x |
calls <- lapply( |
128 | 165x |
filtered_items, |
129 | 165x |
function(state) { |
130 | 68x |
state$get_call() |
131 |
} |
|
132 |
) |
|
133 | 165x |
calls_combine_by(calls, operator = "&") |
134 |
} |
|
135 |
) |
|
136 | 120x |
filter_items <- Filter( |
137 | 120x |
x = filter_items, |
138 | 120x |
f = Negate(is.null) |
139 |
) |
|
140 | 120x |
if (length(filter_items) > 0L) { |
141 | 44x |
filter_function <- str2lang(self$get_fun()) |
142 | 44x |
data_name <- str2lang(private$dataname) |
143 | 44x |
substitute( |
144 | 44x |
env = list( |
145 | 44x |
lhs = data_name, |
146 | 44x |
rhs = as.call(c(filter_function, c(list(data_name), filter_items))) |
147 |
), |
|
148 | 44x |
expr = lhs <- rhs |
149 |
) |
|
150 |
} else { |
|
151 |
# return NULL to avoid no-op call |
|
152 | 76x |
NULL |
153 |
} |
|
154 |
}, |
|
155 | ||
156 |
#' @description |
|
157 |
#' Prints this `FilterStates` object. |
|
158 |
#' |
|
159 |
#' @param ... additional arguments to this method |
|
160 |
print = function(...) { |
|
161 | ! |
cat(shiny::isolate(self$format()), "\n") |
162 |
}, |
|
163 | ||
164 |
#' @description |
|
165 |
#' Gets the name of the function used to filter the data in this `FilterStates`. |
|
166 |
#' |
|
167 |
#' Get name of function used to create the \emph{subset expression}. |
|
168 |
#' Defaults to "subset" but can be overridden by child class method. |
|
169 |
#' |
|
170 |
#' @return `character(1)` the name of the function |
|
171 |
#' |
|
172 |
get_fun = function() { |
|
173 | 12x |
"subset" |
174 |
}, |
|
175 | ||
176 |
# state_list methods ---- |
|
177 | ||
178 |
#' @description |
|
179 |
#' Returns a list of `FilterState` objects stored in this `FilterStates`. |
|
180 |
#' |
|
181 |
#' @param state_list_index (`character(1)`, `integer(1)`)\cr |
|
182 |
#' index on the list in `private$state_list` where filter states are kept |
|
183 |
#' @param state_id (`character(1)`)\cr |
|
184 |
#' name of element in a filter state (which is a `reactiveVal` containing a list) |
|
185 |
#' |
|
186 |
#' @return `list` of `FilterState` objects |
|
187 |
#' |
|
188 |
state_list_get = function(state_list_index, state_id = NULL) { |
|
189 | 503x |
private$validate_state_list_exists(state_list_index) |
190 | 501x |
checkmate::assert_string(state_id, null.ok = TRUE) |
191 | ||
192 | 501x |
if (is.null(state_id)) { |
193 | 498x |
private$state_list[[state_list_index]]() |
194 |
} else { |
|
195 | 3x |
private$state_list[[state_list_index]]()[[state_id]] |
196 |
} |
|
197 |
}, |
|
198 | ||
199 |
#' @description |
|
200 |
#' Adds a new `FilterState` object to this `FilterStates`.\cr |
|
201 |
#' Raises error if the length of `x` does not match the length of `state_id`. |
|
202 |
#' |
|
203 |
#' @param x (`FilterState`)\cr |
|
204 |
#' object to be added to filter state list |
|
205 |
#' @param state_list_index (`character(1)`, `integer(1)`)\cr |
|
206 |
#' index on the list in `private$state_list` where filter states are kept |
|
207 |
#' @param state_id (`character(1)`)\cr |
|
208 |
#' name of element in a filter state (which is a `reactiveVal` containing a list) |
|
209 |
#' |
|
210 |
#' @return NULL |
|
211 |
#' |
|
212 |
state_list_push = function(x, state_list_index, state_id) { |
|
213 | 206x |
logger::log_trace( |
214 | 206x |
"{ class(self)[1] } pushing into state_list, dataname: { private$dataname }" |
215 |
) |
|
216 | 206x |
private$validate_state_list_exists(state_list_index) |
217 | 205x |
checkmate::assert_string(state_id) |
218 | ||
219 | 205x |
states <- if (is.list(x)) { |
220 | ! |
x |
221 |
} else { |
|
222 | 205x |
list(x) |
223 |
} |
|
224 | ||
225 | 205x |
state <- stats::setNames(states, state_id) |
226 | 205x |
new_state_list <- c(private$state_list[[state_list_index]](), state) |
227 | 205x |
private$state_list[[state_list_index]](new_state_list) |
228 | ||
229 | 205x |
logger::log_trace( |
230 | 205x |
"{ class(self)[1] } pushed into queue, dataname: { private$dataname }" |
231 |
) |
|
232 | 205x |
invisible(NULL) |
233 |
}, |
|
234 | ||
235 |
#' @description |
|
236 |
#' Removes a single filter state with all associated shiny elements:\cr |
|
237 |
#' * specified `FilterState` from `private$state_list` |
|
238 |
#' * UI card created for this filter |
|
239 |
#' * observers tracking the selection and remove button |
|
240 |
#' |
|
241 |
#' @param state_list_index (`character(1)`, `integer(1)`)\cr |
|
242 |
#' index on the list in `private$state_list` where filter states are kept |
|
243 |
#' @param state_id (`character(1)`)\cr |
|
244 |
#' name of element in a filter state (which is a `reactiveVal` containing a list) |
|
245 |
#' |
|
246 |
#' @return NULL |
|
247 |
#' |
|
248 |
state_list_remove = function(state_list_index, state_id) { |
|
249 | 28x |
logger::log_trace(paste( |
250 | 28x |
"{ class(self)[1] } removing a filter from state_list { state_list_index },", |
251 | 28x |
"dataname: { private$dataname }" |
252 |
)) |
|
253 | 28x |
private$validate_state_list_exists(state_list_index) |
254 | 27x |
checkmate::assert_string(state_id) |
255 | 27x |
checkmate::assert( |
256 | 27x |
checkmate::check_string(state_list_index), |
257 | 27x |
checkmate::check_int(state_list_index) |
258 |
) |
|
259 | ||
260 | 27x |
new_state_list <- private$state_list[[state_list_index]]() |
261 | 27x |
new_state_list[[state_id]] <- NULL |
262 | 27x |
private$state_list[[state_list_index]](new_state_list) |
263 | ||
264 | 27x |
logger::log_trace(paste( |
265 | 27x |
"{ class(self)[1] } removed from state_list { state_list_index },", |
266 | 27x |
"dataname: { private$dataname }" |
267 |
)) |
|
268 | 27x |
invisible(NULL) |
269 |
}, |
|
270 | ||
271 |
#' @description |
|
272 |
#' Remove all `FilterState` objects from this `FilterStates` object. |
|
273 |
#' |
|
274 |
#' @return NULL |
|
275 |
#' |
|
276 |
state_list_empty = function() { |
|
277 | 24x |
logger::log_trace( |
278 | 24x |
"{ class(self)[1] } emptying state_list, dataname: { private$dataname }" |
279 |
) |
|
280 | ||
281 | 24x |
for (i in seq_along(private$state_list)) { |
282 | 28x |
private$state_list[[i]](list()) |
283 |
} |
|
284 | ||
285 | 24x |
logger::log_trace( |
286 | 24x |
"{ class(self)[1] } emptied state_list, dataname: { private$dataname }" |
287 |
) |
|
288 | 24x |
invisible(NULL) |
289 |
}, |
|
290 | ||
291 |
#' @description |
|
292 |
#' Gets the number of active `FilterState` objects in this `FilterStates` object. |
|
293 |
#' |
|
294 |
#' @return `integer(1)` |
|
295 |
#' |
|
296 |
get_filter_count = function() { |
|
297 | 66x |
sum(vapply(private$state_list, function(state_list) { |
298 | 103x |
length(state_list()) |
299 | 66x |
}, FUN.VALUE = integer(1))) |
300 |
}, |
|
301 | ||
302 |
#' @description Remove a single `FilterState` from `state_list`. |
|
303 |
#' |
|
304 |
#' @param state_id (`character`)\cr |
|
305 |
#' name of variable for which to remove `FilterState` |
|
306 |
#' |
|
307 |
#' @return `NULL` |
|
308 |
#' |
|
309 |
remove_filter_state = function(state_id) { |
|
310 | ! |
stop("This variable can not be removed from the filter.") |
311 |
}, |
|
312 | ||
313 |
# shiny modules ---- |
|
314 | ||
315 |
#' @description |
|
316 |
#' Shiny module UI |
|
317 |
#' |
|
318 |
#' Shiny UI element that stores `FilterState` UI elements. |
|
319 |
#' Populated with elements created with `renderUI` in the module server. |
|
320 |
#' |
|
321 |
#' @param id (`character(1)`)\cr |
|
322 |
#' shiny element (module instance) id |
|
323 |
#' |
|
324 |
#' @return `shiny.tag` |
|
325 |
#' |
|
326 |
ui = function(id) { |
|
327 | ! |
ns <- NS(id) |
328 | ! |
private$cards_container_id <- ns("cards") |
329 | ! |
tagList( |
330 | ! |
include_css_files(pattern = "filter-panel"), |
331 | ! |
tags$div( |
332 | ! |
id = private$cards_container_id, |
333 | ! |
class = "list-group hideable-list-group", |
334 | ! |
`data-label` = ifelse(private$datalabel == "", "", (paste0("> ", private$datalabel))) |
335 |
) |
|
336 |
) |
|
337 |
}, |
|
338 | ||
339 |
#' @description |
|
340 |
#' Gets reactive values from active `FilterState` objects. |
|
341 |
#' |
|
342 |
#' Get active filter state from `FilterState` objects stored in `state_list`(s). |
|
343 |
#' The output is a list compatible with input to `self$set_filter_state`. |
|
344 |
#' |
|
345 |
#' @return `list` containing `list` per `FilterState` in the `state_list` |
|
346 |
#' |
|
347 |
get_filter_state = function() { |
|
348 | ! |
stop("Pure virtual method.") |
349 |
}, |
|
350 | ||
351 |
#' @description |
|
352 |
#' Sets active `FilterState` objects. |
|
353 |
#' |
|
354 |
#' @param data (`data.frame`)\cr |
|
355 |
#' data object for which to define a subset |
|
356 |
#' @param state (`named list`)\cr |
|
357 |
#' should contain values of initial selections in the `FilterState`; |
|
358 |
#' `list` names must correspond to column names in `data` |
|
359 |
#' @param filtered_dataset |
|
360 |
#' data object for which to define a subset(?) |
|
361 |
#' |
|
362 |
set_filter_state = function(data, state, filtered_dataset) { |
|
363 | ! |
stop("Pure virtual method.") |
364 |
}, |
|
365 | ||
366 |
#' @description |
|
367 |
#' Shiny module UI that adds a filter variable. |
|
368 |
#' |
|
369 |
#' @param id (`character(1)`)\cr |
|
370 |
#' shiny element (module instance) id |
|
371 |
#' @param data (`data.frame`, `MultiAssayExperiment`, `SummarizedExperiment`, `matrix`)\cr |
|
372 |
#' data object for which to define a subset |
|
373 |
#' |
|
374 |
#' @return `shiny.tag` |
|
375 |
#' |
|
376 |
ui_add_filter_state = function(id, data) { |
|
377 | ! |
div("This object cannot be filtered") |
378 |
}, |
|
379 | ||
380 |
#' @description |
|
381 |
#' Shiny module server that adds a filter variable. |
|
382 |
#' |
|
383 |
#' @param id (`character(1)`)\cr |
|
384 |
#' shiny module instance id |
|
385 |
#' @param data (`data.frame`, `MultiAssayExperiment`, `SummarizedExperiment`, `matrix`)\cr |
|
386 |
#' data object for which to define a subset |
|
387 |
#' @param ... ignored |
|
388 |
#' |
|
389 |
#' @return `moduleServer` function which returns `NULL` |
|
390 |
#' |
|
391 |
srv_add_filter_state = function(id, data, ...) { |
|
392 | ! |
check_ellipsis(..., stop = FALSE) |
393 | ! |
moduleServer( |
394 | ! |
id = id, |
395 | ! |
function(input, output, session) { |
396 | ! |
NULL |
397 |
} |
|
398 |
) |
|
399 |
} |
|
400 |
), |
|
401 |
private = list( |
|
402 |
# private fields ---- |
|
403 |
cards_container_id = character(0), |
|
404 |
card_ids = character(0), |
|
405 |
datalabel = character(0), |
|
406 |
dataname = NULL, # because it holds object of class name |
|
407 |
ns = NULL, # shiny ns() |
|
408 |
observers = list(), # observers |
|
409 |
state_list = NULL, # list of `reactiveVal`s initialized by init methods of child classes |
|
410 | ||
411 |
# private methods ---- |
|
412 | ||
413 |
# Module to insert/remove `FilterState` UI |
|
414 |
# |
|
415 |
# This module adds the shiny UI of the `FilterState` object newly added |
|
416 |
# to state_list to the Active Filter Variables, |
|
417 |
# calls `FilterState` modules and creates an observer to remove state |
|
418 |
# parameter filter_state (`FilterState`). |
|
419 |
# |
|
420 |
# @param id (`character(1)`)\cr |
|
421 |
# shiny module instance id |
|
422 |
# @param filter_state (`named list`)\cr |
|
423 |
# should contain values of initial selections in the `FilterState`; |
|
424 |
# `list` names must correspond to column names in `data` |
|
425 |
# @param state_list_index (`character(1)`, `integer(1)`)\cr |
|
426 |
# index on the list in `private$state_list` where filter states are kept |
|
427 |
# @param state_id (`character(1)`)\cr |
|
428 |
# name of element in a filter state (which is a `reactiveVal` containing a list) |
|
429 |
# |
|
430 |
# @return `moduleServer` function which returns `NULL` |
|
431 |
# |
|
432 |
insert_filter_state_ui = function(id, filter_state, state_list_index, state_id) { |
|
433 | ! |
checkmate::assert_class(filter_state, "FilterState") |
434 | ! |
checkmate::assert( |
435 | ! |
checkmate::check_int(state_list_index), |
436 | ! |
checkmate::check_character(state_list_index, len = 1), |
437 | ! |
combine = "or" |
438 |
) |
|
439 | ! |
checkmate::assert_character(state_id, len = 1) |
440 | ! |
moduleServer( |
441 | ! |
id = id, |
442 | ! |
function(input, output, session) { |
443 | ! |
logger::log_trace( |
444 | ! |
sprintf( |
445 | ! |
"%s$insert_filter_state_ui, adding FilterState UI of variable %s, dataname: %s", |
446 | ! |
class(self)[1], |
447 | ! |
state_id, |
448 | ! |
private$dataname |
449 |
) |
|
450 |
) |
|
451 | ||
452 |
# card_id of inserted card must be saved in private$card_ids as |
|
453 |
# it might be removed by the several events: |
|
454 |
# - remove button in FilterStates module |
|
455 |
# - remove button in FilteredDataset module |
|
456 |
# - remove button in FilteredData module |
|
457 |
# - API call remove_filter_state |
|
458 | ! |
card_id <- session$ns("card") |
459 | ! |
state_list_id <- sprintf("%s-%s", state_list_index, state_id) |
460 | ! |
private$card_ids[state_list_id] <- card_id |
461 | ||
462 | ! |
insertUI( |
463 | ! |
selector = sprintf("#%s", private$cards_container_id), |
464 | ! |
where = "beforeEnd", |
465 |
# add span with id to be removable |
|
466 | ! |
ui = div( |
467 | ! |
id = card_id, |
468 | ! |
class = "list-group-item", |
469 | ! |
filter_state$ui(session$ns("content")) |
470 |
) |
|
471 |
) |
|
472 |
# signal sent from filter_state when it is marked for removal |
|
473 | ! |
remove_fs <- filter_state$server(id = "content") |
474 | ||
475 | ! |
private$observers[[state_list_id]] <- observeEvent( |
476 | ! |
ignoreInit = TRUE, |
477 | ! |
ignoreNULL = TRUE, |
478 | ! |
eventExpr = remove_fs(), |
479 | ! |
handlerExpr = { |
480 | ! |
logger::log_trace(paste( |
481 | ! |
"{ class(self)[1] }$insert_filter_state_ui@1", |
482 | ! |
"removing FilterState from state_list '{ state_list_index }',", |
483 | ! |
"dataname: { private$dataname }" |
484 |
)) |
|
485 | ! |
self$state_list_remove(state_list_index, state_id) |
486 | ! |
logger::log_trace(paste( |
487 | ! |
"{ class(self)[1] }$insert_filter_state_ui@1", |
488 | ! |
"removed FilterState from state_list '{ state_list_index }',", |
489 | ! |
"dataname: { private$dataname }" |
490 |
)) |
|
491 |
} |
|
492 |
) |
|
493 | ||
494 | ! |
logger::log_trace( |
495 | ! |
sprintf( |
496 | ! |
"%s$insert_filter_state_ui, added FilterState UI of variable %s, dataname: %s", |
497 | ! |
class(self)[1], |
498 | ! |
state_id, |
499 | ! |
private$dataname |
500 |
) |
|
501 |
) |
|
502 | ! |
NULL |
503 |
} |
|
504 |
) |
|
505 |
}, |
|
506 | ||
507 |
# Remove shiny element. Method can be called from reactive session where |
|
508 |
# `observeEvent` for remove-filter-state is set and also from `FilteredDataset` |
|
509 |
# level, where shiny-session-namespace is different. That is why it's important |
|
510 |
# to remove shiny elements from anywhere. In `add_filter_state` `session$ns(NULL)` |
|
511 |
# is equivalent to `private$ns(state_list_index)`. |
|
512 |
# In addition, an unused reactive is being removed from input: |
|
513 |
# method searches input for the unique matches with the filter name |
|
514 |
# and then removes objects constructed with current card id + filter name. |
|
515 |
# |
|
516 |
remove_filter_state_ui = function(state_list_index, state_id, .input) { |
|
517 | 4x |
state_list_id <- sprintf("%s-%s", state_list_index, state_id) |
518 | 4x |
removeUI(selector = sprintf("#%s", private$card_ids[state_list_id])) |
519 | 4x |
private$card_ids <- private$card_ids[names(private$card_ids) != state_list_id] |
520 | 4x |
if (length(private$observers[[state_list_id]]) > 0) { |
521 | ! |
private$observers[[state_list_id]]$destroy() |
522 | ! |
private$observers[[state_list_id]] <- NULL |
523 |
} |
|
524 |
# Remove unused reactive from shiny input (leftover of removeUI). |
|
525 |
# This default behavior may change in the future, making this part obsolete. |
|
526 | 4x |
prefix <- paste0(gsub("cards$", "", private$cards_container_id)) |
527 | 4x |
invisible( |
528 | 4x |
lapply( |
529 | 4x |
unique(grep(state_id, names(.input), value = TRUE)), |
530 | 4x |
function(i) { |
531 | ! |
.subset2(.input, "impl")$.values$remove(paste0(prefix, i)) |
532 |
} |
|
533 |
) |
|
534 |
) |
|
535 |
}, |
|
536 |
# Checks if the state_list of the given index was initialized in this `FilterStates` |
|
537 |
# @param state_list_index (character or integer) |
|
538 |
validate_state_list_exists = function(state_list_index) { |
|
539 | 737x |
checkmate::assert( |
540 | 737x |
checkmate::check_string(state_list_index), |
541 | 737x |
checkmate::check_int(state_list_index) |
542 |
) |
|
543 |
if ( |
|
544 |
!( |
|
545 | 737x |
is.numeric(state_list_index) && |
546 | 737x |
all(state_list_index <= length(private$state_list) && state_list_index > 0) || |
547 | 737x |
is.character(state_list_index) && all(state_list_index %in% names(private$state_list)) |
548 |
) |
|
549 |
) { |
|
550 | 4x |
stop( |
551 | 4x |
paste( |
552 | 4x |
"Filter state list", |
553 | 4x |
state_list_index, |
554 | 4x |
"has not been initialized in FilterStates object belonging to the dataset", |
555 | 4x |
private$datalabel |
556 |
) |
|
557 |
) |
|
558 |
} |
|
559 |
}, |
|
560 | ||
561 |
# Maps the array of strings to sanitized unique HTML ids. |
|
562 |
# @param keys `character` the array of strings |
|
563 |
# @param prefix `character(1)` text to prefix id. Needed in case of multiple |
|
564 |
# state_list objects where keys (variables) might be duplicated across state_lists |
|
565 |
# @return `list` the mapping |
|
566 |
map_vars_to_html_ids = function(keys, prefix = "") { |
|
567 | 10x |
checkmate::assert_character(keys, null.ok = TRUE) |
568 | 10x |
checkmate::assert_character(prefix, len = 1) |
569 | 10x |
sanitized_values <- make.unique(gsub("[^[:alnum:]]", perl = TRUE, replacement = "", x = keys)) |
570 | 10x |
sanitized_values <- paste(prefix, "var", sanitized_values, sep = "_") |
571 | 10x |
stats::setNames(object = sanitized_values, nm = keys) |
572 |
} |
|
573 |
) |
|
574 |
) |
1 |
#' @name CDISCFilteredData |
|
2 |
#' @docType class |
|
3 |
#' |
|
4 |
#' @title Class to encapsulate relational filtered datasets with its parents. |
|
5 |
#' @description `r lifecycle::badge("stable")` |
|
6 |
#' @details |
|
7 |
#' The `CDISCFilteredData` class implements logic to filter a relational |
|
8 |
#' dataset by inheriting from `FilteredData`. |
|
9 |
#' A dataset can have up to one parent dataset. Rows are identified by the foreign |
|
10 |
#' key and only those rows that appear in the parent dataset are kept in the filtered |
|
11 |
#' dataset. |
|
12 |
#' |
|
13 |
#' The teal UI works with objects of class `FilteredData` which may mix CDISC and other |
|
14 |
#' data (e.g. `iris`). |
|
15 |
#' |
|
16 |
#' @seealso `FilteredData` class |
|
17 |
#' |
|
18 |
#' @examples |
|
19 |
#' library(scda) |
|
20 |
#' library(teal.data) |
|
21 |
#' |
|
22 |
#' ADSL <- synthetic_cdisc_data("latest")$adsl |
|
23 |
#' ADTTE <- synthetic_cdisc_data("latest")$adtte |
|
24 |
#' datasets <- teal.slice:::CDISCFilteredData$new( |
|
25 |
#' list( |
|
26 |
#' ADSL = list(dataset = ADSL, keys = c("STUDYID", "USUBJID")), |
|
27 |
#' ADTTE = list(dataset = ADTTE, keys = c("STUDYID", "USUBJID", "PARAMCD"), parent = "ADSL") |
|
28 |
#' ), |
|
29 |
#' check = FALSE, |
|
30 |
#' join_keys = join_keys(join_key("ADSL", "ADTTE", c("STUDYID", "USUBJID"))) |
|
31 |
#' ) |
|
32 |
#' |
|
33 |
#' # to avoid using isolate(), you can provide a default isolate context by calling |
|
34 |
#' # options(shiny.suppressMissingContextError = TRUE) #nolint |
|
35 |
#' # don't forget to deactivate this option at the end |
|
36 |
#' # options(shiny.suppressMissingContextError = FALSE) #nolint |
|
37 |
#' |
|
38 |
#' isolate({ |
|
39 |
#' datasets$datanames() |
|
40 |
#' |
|
41 |
#' # number observations and subjects of filtered/non-filtered dataset |
|
42 |
#' datasets$get_filter_overview("ADSL") |
|
43 |
#' |
|
44 |
#' print(datasets$get_call("ADSL")) |
|
45 |
#' print(datasets$get_call("ADTTE")) |
|
46 |
#' |
|
47 |
#' df <- datasets$get_data("ADSL", filtered = FALSE) |
|
48 |
#' print(df) |
|
49 |
#' }) |
|
50 |
#' |
|
51 |
#' |
|
52 |
#' isolate(datasets$set_filter_state(list(ADTTE = list(PARAMCD = "OS")))) |
|
53 |
#' isolate(datasets$get_filter_state()) |
|
54 |
CDISCFilteredData <- R6::R6Class( # nolint |
|
55 |
"CDISCFilteredData", |
|
56 |
inherit = FilteredData, |
|
57 |
## CDISCFilteredData ==== |
|
58 |
## __Public Methods ==== |
|
59 |
public = list( |
|
60 |
#' @description |
|
61 |
#' Get datanames |
|
62 |
#' |
|
63 |
#' The datanames are returned in the order in which they must be |
|
64 |
#' evaluated (in case of dependencies). |
|
65 |
#' @return (`character` vector) of datanames |
|
66 |
datanames = function() { |
|
67 | 15x |
datanames <- super$datanames() |
68 | 15x |
child_parent <- sapply(datanames, function(i) self$get_parentname(i), USE.NAMES = TRUE, simplify = FALSE) |
69 | 15x |
ordered_datanames <- topological_sort(child_parent) |
70 | 15x |
return(as.character(intersect(as.character(ordered_datanames), datanames))) |
71 |
}, |
|
72 | ||
73 |
#' @description |
|
74 |
#' |
|
75 |
#' Produces language required to filter a single dataset and merge it with its parent. |
|
76 |
#' The datasets in question are assumed to be available. |
|
77 |
#' |
|
78 |
#' @param dataname (`character(1)`) name of the dataset |
|
79 |
#' @return (`call` or `list` of calls ) to filter dataset |
|
80 |
#' |
|
81 |
get_call = function(dataname) { |
|
82 | 13x |
parent_dataname <- self$get_parentname(dataname) |
83 | ||
84 | 13x |
if (length(parent_dataname) == 0) { |
85 | 12x |
super$get_call(dataname) |
86 |
} else { |
|
87 | 1x |
dataset <- self$get_filtered_dataset(dataname) |
88 | 1x |
premerge_call <- Filter( |
89 | 1x |
f = Negate(is.null), |
90 | 1x |
x = lapply( |
91 | 1x |
dataset$get_filter_states(), |
92 | 1x |
function(x) x$get_call() |
93 |
) |
|
94 |
) |
|
95 | ||
96 | 1x |
join_keys <- self$get_join_keys() |
97 | 1x |
keys <- |
98 | 1x |
if (!is.null(join_keys)) { |
99 | 1x |
join_keys$get(parent_dataname, dataname) |
100 |
} else { |
|
101 | ! |
character(0) |
102 |
} |
|
103 | 1x |
parent_keys <- names(keys) |
104 | 1x |
dataset_keys <- unname(keys) |
105 | ||
106 | 1x |
y_arg <- |
107 | 1x |
if (length(parent_keys) == 0L) { |
108 | ! |
parent_dataname |
109 |
} else { |
|
110 | 1x |
sprintf("%s[, c(%s), drop = FALSE]", parent_dataname, toString(dQuote(parent_keys, q = FALSE))) |
111 |
} |
|
112 | 1x |
more_args <- |
113 | 1x |
if (length(parent_keys) == 0 || length(dataset_keys) == 0) { |
114 | ! |
list() |
115 | 1x |
} else if (identical(parent_keys, dataset_keys)) { |
116 | 1x |
list(by = parent_keys) |
117 |
} else { |
|
118 | ! |
list(by = stats::setNames(parent_keys, dataset_keys)) |
119 |
} |
|
120 | ||
121 | 1x |
merge_call <- call( |
122 |
"<-", |
|
123 | 1x |
as.name(dataname), |
124 | 1x |
as.call( |
125 | 1x |
c( |
126 | 1x |
str2lang("dplyr::inner_join"), |
127 | 1x |
x = as.name(dataname), |
128 | 1x |
y = str2lang(y_arg), |
129 | 1x |
more_args |
130 |
) |
|
131 |
) |
|
132 |
) |
|
133 | ||
134 | 1x |
c(premerge_call, merge_call) |
135 |
} |
|
136 |
}, |
|
137 | ||
138 |
#' @description |
|
139 |
#' Get names of datasets available for filtering |
|
140 |
#' |
|
141 |
#' @param dataname (`character` vector) names of the dataset |
|
142 |
#' @return (`character` vector) of dataset names |
|
143 |
get_filterable_datanames = function(dataname) { |
|
144 | ! |
parents <- character(0) |
145 | ! |
for (i in dataname) { |
146 | ! |
while (length(i) > 0) { |
147 | ! |
parent_i <- self$get_parentname(i) |
148 | ! |
parents <- c(parents, parent_i) |
149 | ! |
i <- parent_i |
150 |
} |
|
151 |
} |
|
152 | ||
153 | ! |
return(unique(c(parents, dataname))) |
154 |
}, |
|
155 | ||
156 |
#' @description |
|
157 |
#' Gets variable names of a given dataname for the filtering. This excludes parent dataset variable names. |
|
158 |
#' |
|
159 |
#' @param dataname (`character(1)`) name of the dataset |
|
160 |
#' @return (`character` vector) of variable names |
|
161 |
get_filterable_varnames = function(dataname) { |
|
162 | 8x |
varnames <- self$get_filtered_dataset(dataname)$get_filterable_varnames() |
163 | 8x |
parent_dataname <- self$get_parentname(dataname) |
164 | 8x |
parent_varnames <- if (length(parent_dataname) > 0) { |
165 |
# cannot call get_filterable_varnames on the parent filtered_dataset in case |
|
166 |
# some of its variables are set to be non-filterable |
|
167 | 5x |
get_supported_filter_varnames(self$get_filtered_dataset(parent_dataname)) |
168 |
} |
|
169 | 8x |
setdiff(varnames, parent_varnames) |
170 |
}, |
|
171 | ||
172 |
#' @description |
|
173 |
#' Get filter overview table in form of X (filtered) / Y (non-filtered) |
|
174 |
#' |
|
175 |
#' This is intended to be presented in the application. |
|
176 |
#' |
|
177 |
#' @param datanames (`character` vector) names of the dataset (or "all") |
|
178 |
#' |
|
179 |
#' @return (`matrix`) matrix of observations and subjects of all datasets |
|
180 |
get_filter_overview = function(datanames) { |
|
181 | 10x |
if (identical(datanames, "all")) { |
182 | 1x |
datanames <- self$datanames() |
183 |
} |
|
184 | 9x |
check_in_subset(datanames, self$datanames(), "Some datasets are not available: ") |
185 | ||
186 | 6x |
rows <- lapply( |
187 | 6x |
datanames, |
188 | 6x |
function(dataname) { |
189 | 12x |
obs <- self$get_filtered_dataset(dataname)$get_filter_overview_info( |
190 | 12x |
filtered_dataset = self$get_data(dataname = dataname, filtered = TRUE) |
191 | 12x |
)[, 1] |
192 | ||
193 | 12x |
subs <- private$get_filter_overview_nsubjs(dataname) |
194 | ||
195 | 12x |
df <- cbind( |
196 | 12x |
obs, subs |
197 |
) |
|
198 | ||
199 | 12x |
rownames(df) <- if (!is.null(names(obs))) { |
200 | 4x |
names(obs) |
201 |
} else { |
|
202 | 8x |
dataname |
203 |
} |
|
204 | 12x |
colnames(df) <- c("Obs", "Subjects") |
205 | 12x |
df |
206 |
} |
|
207 |
) |
|
208 | ||
209 | 6x |
do.call(rbind, rows) |
210 |
}, |
|
211 | ||
212 |
#' @description |
|
213 |
#' Get parent dataset name |
|
214 |
#' |
|
215 |
#' @param dataname (`character(1)`) name of the dataset |
|
216 |
#' @return (`character`) name of parent dataset |
|
217 |
get_parentname = function(dataname) { |
|
218 | 77x |
private$parents[[dataname]] |
219 |
}, |
|
220 | ||
221 |
#' @description |
|
222 |
#' Add dataset |
|
223 |
#' |
|
224 |
#' Add dataset and preserve all attributes attached to this object. |
|
225 |
#' Technically `set_dataset` created `FilteredDataset` which keeps |
|
226 |
#' `dataset` for filtering purpose. |
|
227 |
#' |
|
228 |
#' @param dataset_args (`list`)\cr |
|
229 |
#' containing the arguments except (`dataname`) |
|
230 |
#' needed by `init_filtered_dataset` (can also |
|
231 |
#' include `parent` which will be ignored) |
|
232 |
#' @param dataname (`character(1)`)\cr |
|
233 |
#' the name of the `dataset` to be added to this object |
|
234 |
#' @return (`self`) object of this class |
|
235 |
set_dataset = function(dataset_args, dataname) { |
|
236 | 40x |
logger::log_trace("CDISCFilteredData$set_dataset setting dataset, name: { dataname }") |
237 | 40x |
validate_dataset_args(dataset_args, dataname, allowed_parent = TRUE) |
238 | ||
239 | 40x |
parent_dataname <- dataset_args[["parent"]] |
240 | 40x |
dataset_args[["parent"]] <- NULL |
241 | 40x |
private$parents[[dataname]] <- parent_dataname |
242 | ||
243 | 40x |
if (length(parent_dataname) == 0) { |
244 | 30x |
super$set_dataset(dataset_args, dataname) |
245 |
} else { |
|
246 | 10x |
dataset <- dataset_args[["dataset"]] |
247 | 10x |
dataset_args[["dataset"]] <- NULL |
248 | ||
249 |
# to include it nicely in the Show R Code; the UI also uses datanames in ids, so no whitespaces allowed |
|
250 | 10x |
check_simple_name(dataname) |
251 | 10x |
private$filtered_datasets[[dataname]] <- do.call( |
252 | 10x |
what = init_filtered_dataset, |
253 | 10x |
args = c(list(dataset), dataset_args, list(dataname = dataname)) |
254 |
) |
|
255 | ||
256 | 10x |
private$reactive_data[[dataname]] <- reactive({ |
257 | ! |
env <- new.env(parent = parent.env(globalenv())) |
258 | ! |
env[[dataname]] <- self$get_filtered_dataset(dataname)$get_dataset() |
259 | ! |
env[[private$parents[[dataname]]]] <- |
260 | ! |
private$reactive_data[[private$parents[[dataname]]]]() |
261 | ||
262 | ! |
filter_call <- self$get_call(dataname) |
263 | ! |
eval_expr_with_msg(filter_call, env) |
264 | ! |
get(x = dataname, envir = env) |
265 |
}) |
|
266 |
} |
|
267 | ||
268 | 40x |
invisible(self) |
269 |
} |
|
270 |
), |
|
271 | ||
272 |
## __Private Methods--------------------- |
|
273 |
private = list( |
|
274 | ||
275 |
# named list of dataset parents parents[[child_dataset]] = its parent |
|
276 |
parents = NULL, |
|
277 | ||
278 |
# datanames in the order in which they must be evaluated (in case of dependencies) |
|
279 |
# this is a reactive and kept as a field for caching |
|
280 |
ordered_datanames = NULL, |
|
281 |
validate = function() { |
|
282 | ! |
stopifnot( |
283 | ! |
setequal(private$ordered_datanames, names(private$dataset_filters)), |
284 |
) |
|
285 | ! |
super$validate() |
286 |
}, |
|
287 |
get_filter_overview_nsubjs = function(dataname) { |
|
288 |
# Gets filter overview subjects number and returns a list |
|
289 |
# of the number of subjects of filtered/non-filtered datasets |
|
290 | 12x |
subject_keys <- if (length(self$get_parentname(dataname)) > 0) { |
291 | ! |
self$get_keys(self$get_parentname(dataname)) |
292 |
} else { |
|
293 | 12x |
self$get_filtered_dataset(dataname)$get_keys() |
294 |
} |
|
295 | ||
296 | 12x |
self$get_filtered_dataset(dataname)$get_filter_overview_nsubjs( |
297 | 12x |
self$get_data(dataname = dataname, filtered = TRUE), |
298 | 12x |
subject_keys |
299 |
) |
|
300 |
} |
|
301 |
) |
|
302 |
) |
|
303 | ||
304 |
#' Topological graph sort |
|
305 |
#' |
|
306 |
#' Graph is a list which for each node contains a vector of child nodes |
|
307 |
#' in the returned list, parents appear before their children. |
|
308 |
#' |
|
309 |
#' Implementation of Kahn algorithm with a modification to maintain the order of input elements. |
|
310 |
#' |
|
311 |
#' @param graph (named `list`) list with node vector elements |
|
312 |
#' @keywords internal |
|
313 |
#' |
|
314 |
#' @examples |
|
315 |
#' \dontrun{ |
|
316 |
#' topological_sort(list(A = c(), B = c("A"), C = c("B"), D = c("A"))) |
|
317 |
#' topological_sort(list(D = c("A"), A = c(), B = c("A"), C = c("B"))) |
|
318 |
#' topological_sort(list(D = c("A"), B = c("A"), C = c("B"), A = c())) |
|
319 |
#' } |
|
320 |
topological_sort <- function(graph) { |
|
321 |
# compute in-degrees |
|
322 | 15x |
in_degrees <- list() |
323 | 15x |
for (node in names(graph)) { |
324 | 42x |
in_degrees[[node]] <- 0 |
325 | 42x |
for (to_edge in graph[[node]]) { |
326 | 3x |
in_degrees[[to_edge]] <- 0 |
327 |
} |
|
328 |
} |
|
329 | ||
330 | 15x |
for (node in graph) { |
331 | 42x |
for (to_edge in node) { |
332 | 3x |
in_degrees[[to_edge]] <- in_degrees[[to_edge]] + 1 |
333 |
} |
|
334 |
} |
|
335 | ||
336 |
# sort |
|
337 | 15x |
visited <- 0 |
338 | 15x |
sorted <- list() |
339 | 15x |
zero_in <- list() |
340 | 15x |
for (node in names(in_degrees)) { |
341 | 39x |
if (in_degrees[[node]] == 0) zero_in <- append(zero_in, node) |
342 |
} |
|
343 | 15x |
zero_in <- rev(zero_in) |
344 | ||
345 | 15x |
while (length(zero_in) != 0) { |
346 | 42x |
visited <- visited + 1 |
347 | 42x |
sorted <- c(zero_in[[1]], sorted) |
348 | 42x |
for (edge_to in graph[[zero_in[[1]]]]) { |
349 | 3x |
in_degrees[[edge_to]] <- in_degrees[[edge_to]] - 1 |
350 | 3x |
if (in_degrees[[edge_to]] == 0) { |
351 | 3x |
zero_in <- append(zero_in, edge_to, 1) |
352 |
} |
|
353 |
} |
|
354 | 42x |
zero_in[[1]] <- NULL |
355 |
} |
|
356 | ||
357 | 15x |
if (visited != length(in_degrees)) { |
358 | ! |
stop( |
359 | ! |
"Graph is not a directed acyclic graph. Cycles involving nodes: ", |
360 | ! |
paste0(setdiff(names(in_degrees), sorted), collapse = " ") |
361 |
) |
|
362 |
} else { |
|
363 | 15x |
return(sorted) |
364 |
} |
|
365 |
} |
1 |
#' @name FilterState |
|
2 |
#' @docType class |
|
3 |
#' |
|
4 |
#' |
|
5 |
#' @title FilterState Abstract Class |
|
6 |
#' |
|
7 |
#' @description Abstract class to encapsulate filter states |
|
8 |
#' |
|
9 |
#' @details |
|
10 |
#' This class is responsible for managing single filter item within |
|
11 |
#' `FilteredData` class. Filter states depend on the variable type: |
|
12 |
#' (`logical`, `integer`, `numeric`, `factor`, `character`, `Date`, `POSIXct`, `POSIXlt`) |
|
13 |
#' and returns `FilterState` object with class corresponding to input variable. |
|
14 |
#' Class controls single filter entry in `module_single_filter_item` and returns |
|
15 |
#' code relevant to selected values. |
|
16 |
#' - `factor`, `character`: `class = ChoicesFilterState` |
|
17 |
#' - `numeric`: `class = RangeFilterState` |
|
18 |
#' - `logical`: `class = LogicalFilterState` |
|
19 |
#' - `Date`: `class = DateFilterState` |
|
20 |
#' - `POSIXct`, `POSIXlt`: `class = DatetimeFilterState` |
|
21 |
#' - all `NA` entries: `class: FilterState`, cannot be filtered |
|
22 |
#' - default: `FilterState`, cannot be filtered |
|
23 |
#' \cr |
|
24 |
#' Each variable's filter state is an `R6` object which contains `choices`, |
|
25 |
#' `selected`, `varname`, `dataname`, `labels`, `na_count`, `keep_na` and other |
|
26 |
#' variable type specific fields (`keep_inf`, `inf_count`, `timezone`). |
|
27 |
#' Object contains also shiny module (`ui` and `server`) which manages |
|
28 |
#' state of the filter through reactive values `selected`, `keep_na`, `keep_inf` |
|
29 |
#' which trigger `get_call()` and every R function call up in reactive |
|
30 |
#' chain. |
|
31 |
#' \cr |
|
32 |
#' \cr |
|
33 |
#' @section Modifying state: |
|
34 |
#' Modifying a `FilterState` object is possible in three scenarios: |
|
35 |
#' * In the interactive session by directly specifying values of `selected`, |
|
36 |
#' `keep_na` or `keep_inf` using `set_state` method (to update all at once), |
|
37 |
#' or using `set_selected`, `set_keep_na` or `set_keep_inf` |
|
38 |
#' * In a running application by changing appropriate inputs |
|
39 |
#' * In a running application by using [filter_state_api] which directly uses `set_state` method |
|
40 |
#' of the `FilterState` object. |
|
41 |
#' |
|
42 |
#' @keywords internal |
|
43 |
FilterState <- R6::R6Class( # nolint |
|
44 |
"FilterState", |
|
45 | ||
46 |
# public methods ---- |
|
47 |
public = list( |
|
48 |
#' @description |
|
49 |
#' Initialize a `FilterState` object |
|
50 |
#' @param x (`vector`)\cr |
|
51 |
#' values of the variable used in filter |
|
52 |
#' @param varname (`character`)\cr |
|
53 |
#' name of the variable |
|
54 |
#' @param varlabel (`character(1)`)\cr |
|
55 |
#' label of the variable (optional). |
|
56 |
#' @param dataname (`character(1)`)\cr |
|
57 |
#' name of dataset where `x` is taken from. Must be specified if `extract_type` argument |
|
58 |
#' is not empty. |
|
59 |
#' @param extract_type (`character(0)`, `character(1)`)\cr |
|
60 |
#' whether condition calls should be prefixed by dataname. Possible values: |
|
61 |
#' \itemize{ |
|
62 |
#' \item{`character(0)` (default)}{ `varname` in the condition call will not be prefixed} |
|
63 |
#' \item{`"list"`}{ `varname` in the condition call will be returned as `<dataname>$<varname>`} |
|
64 |
#' \item{`"matrix"`}{ `varname` in the condition call will be returned as `<dataname>[, <varname>]`} |
|
65 |
#' } |
|
66 |
#' |
|
67 |
#' @return self invisibly |
|
68 |
#' |
|
69 |
initialize = function(x, |
|
70 |
varname, |
|
71 |
varlabel = character(0), |
|
72 |
dataname = NULL, |
|
73 |
extract_type = character(0)) { |
|
74 | 356x |
checkmate::assert_string(varname) |
75 | 353x |
checkmate::assert_character(varlabel, max.len = 1, any.missing = FALSE) |
76 | 353x |
checkmate::assert_string(dataname, null.ok = TRUE) |
77 | 353x |
checkmate::assert_character(extract_type, max.len = 1, any.missing = FALSE) |
78 | 353x |
if (length(extract_type) == 1) { |
79 | 53x |
checkmate::assert_choice(extract_type, choices = c("list", "matrix")) |
80 |
} |
|
81 | 353x |
if (length(extract_type) == 1 && is.null(dataname)) { |
82 | 1x |
stop("if extract_type is specified, dataname must also be specified") |
83 |
} |
|
84 | ||
85 | 352x |
private$dataname <- dataname |
86 | 352x |
private$varname <- varname |
87 | 352x |
private$varlabel <- if (identical(varlabel, as.character(varname))) { |
88 |
# to not display duplicated label |
|
89 | 41x |
character(0) |
90 |
} else { |
|
91 | 311x |
varlabel |
92 |
} |
|
93 | 352x |
private$extract_type <- extract_type |
94 | 352x |
private$selected <- reactiveVal(NULL) |
95 | 352x |
private$na_count <- sum(is.na(x)) |
96 | 352x |
private$keep_na <- reactiveVal(FALSE) |
97 | 352x |
logger::log_trace( |
98 | 352x |
sprintf( |
99 | 352x |
"Instantiated %s with variable %s, dataname: %s", |
100 | 352x |
class(self)[1], |
101 | 352x |
private$varname, |
102 | 352x |
private$dataname |
103 |
) |
|
104 |
) |
|
105 | 352x |
invisible(self) |
106 |
}, |
|
107 | ||
108 |
#' @description |
|
109 |
#' Destroy observers stored in `private$observers`. |
|
110 |
#' |
|
111 |
#' @return NULL invisibly |
|
112 |
#' |
|
113 |
destroy_observers = function() { |
|
114 | ! |
lapply(private$observers, function(x) x$destroy()) |
115 | ! |
return(invisible(NULL)) |
116 |
}, |
|
117 | ||
118 |
#' @description |
|
119 |
#' Returns a formatted string representing this `FilterState`. |
|
120 |
#' |
|
121 |
#' @param indent (`numeric(1)`) |
|
122 |
#' number of spaces before after each new line character of the formatted string; |
|
123 |
#' defaults to 0 |
|
124 |
#' @param wrap_width (`numeric(1)`) |
|
125 |
#' number of characters to wrap lines at in the printed output; |
|
126 |
#' allowed range is 30 to 120; defaults to 76 |
|
127 |
#' |
|
128 |
#' @return `character(1)` the formatted string |
|
129 |
#' |
|
130 |
format = function(indent = 0L, wrap_width = 76L) { |
|
131 | 50x |
checkmate::assert_number(indent, finite = TRUE, lower = 0L) |
132 | 49x |
checkmate::assert_number(wrap_width, finite = TRUE, lower = 30L, upper = 120L) |
133 | ||
134 |
# List all selected values separated by commas. |
|
135 | 48x |
values <- paste(format(self$get_selected(), nsmall = 3L, justify = "none"), collapse = ", ") |
136 | 48x |
paste(c( |
137 | 48x |
strwrap( |
138 | 48x |
sprintf("Filtering on: %s", private$varname), |
139 | 48x |
width = wrap_width, |
140 | 48x |
indent = indent |
141 |
), |
|
142 |
# Add wrapping and progressive indent to values enumeration as it is likely to be long. |
|
143 | 48x |
strwrap( |
144 | 48x |
sprintf("Selected values: %s", values), |
145 | 48x |
width = wrap_width, |
146 | 48x |
indent = indent + 2L, |
147 | 48x |
exdent = indent + 4L |
148 |
), |
|
149 | 48x |
strwrap( |
150 | 48x |
sprintf("Include missing values: %s", self$get_keep_na()), |
151 | 48x |
width = wrap_width, |
152 | 48x |
indent = indent + 2L |
153 |
) |
|
154 | 48x |
), collapse = "\n") |
155 |
}, |
|
156 | ||
157 |
#' @description |
|
158 |
#' Returns reproducible condition call for current selection relevant |
|
159 |
#' for selected variable type. |
|
160 |
#' Method is using internal reactive values which makes it reactive |
|
161 |
#' and must be executed in reactive or isolated context. |
|
162 |
#' |
|
163 |
get_call = function() { |
|
164 | 1x |
NULL |
165 |
}, |
|
166 | ||
167 |
#' @description |
|
168 |
#' Returns dataname or "NULL" if dataname is NULL. |
|
169 |
#' |
|
170 |
#' @return `character(1)` |
|
171 |
#' |
|
172 |
get_dataname = function() { |
|
173 | 409x |
if (!is.null(private$dataname)) { |
174 | 221x |
private$dataname |
175 |
} else { |
|
176 | 188x |
character(1) |
177 |
} |
|
178 |
}, |
|
179 | ||
180 |
#' @description |
|
181 |
#' Returns current `keep_na` selection. |
|
182 |
#' |
|
183 |
#' @return `logical(1)` |
|
184 |
#' |
|
185 |
get_keep_na = function() { |
|
186 | 335x |
private$keep_na() |
187 |
}, |
|
188 | ||
189 |
#' @description |
|
190 |
#' Returns variable label. |
|
191 |
#' |
|
192 |
#' @return `character(1)` |
|
193 |
#' |
|
194 |
get_varlabel = function() { |
|
195 | 2x |
private$varlabel |
196 |
}, |
|
197 | ||
198 |
#' @description |
|
199 |
#' Get variable name. |
|
200 |
#' |
|
201 |
#' @return `character(1)` |
|
202 |
#' |
|
203 |
get_varname = function() { |
|
204 | 423x |
private$varname |
205 |
}, |
|
206 | ||
207 |
#' @description |
|
208 |
#' Get selected values from `FilterState`. |
|
209 |
#' |
|
210 |
#' @return class of the returned object depends of class of the `FilterState` |
|
211 |
#' |
|
212 |
get_selected = function() { |
|
213 | 428x |
private$selected() |
214 |
}, |
|
215 | ||
216 |
#' @description |
|
217 |
#' Returns the filtering state. |
|
218 |
#' |
|
219 |
#' @return `list` containing values taken from the reactive fields: |
|
220 |
#' * `selected` (`atomic`) length depends on a `FilterState` variant. |
|
221 |
#' * `keep_na` (`logical(1)`) whether `NA` should be kept. |
|
222 |
#' |
|
223 |
get_state = function() { |
|
224 | 38x |
list( |
225 | 38x |
selected = self$get_selected(), |
226 | 38x |
keep_na = self$get_keep_na() |
227 |
) |
|
228 |
}, |
|
229 | ||
230 |
#' @description |
|
231 |
#' Prints this `FilterState` object. |
|
232 |
#' |
|
233 |
#' @param ... additional arguments to this method |
|
234 |
#' |
|
235 |
print = function(...) { |
|
236 | ! |
cat(shiny::isolate(self$format()), "\n") |
237 |
}, |
|
238 | ||
239 |
#' @description |
|
240 |
#' Set whether to keep NAs. |
|
241 |
#' |
|
242 |
#' @param value `logical(1)`\cr |
|
243 |
#' value(s) which come from the filter selection. Value is set in `server` |
|
244 |
#' modules after selecting check-box-input in the shiny interface. Values are set to |
|
245 |
#' `private$keep_na` which is reactive. |
|
246 |
#' |
|
247 |
#' @return NULL invisibly |
|
248 |
#' |
|
249 |
set_keep_na = function(value) { |
|
250 | 117x |
checkmate::assert_flag(value) |
251 | 117x |
private$keep_na(value) |
252 | 117x |
logger::log_trace( |
253 | 117x |
sprintf( |
254 | 117x |
"%s$set_keep_na set for variable %s to %s.", |
255 | 117x |
class(self)[1], |
256 | 117x |
private$varname, |
257 | 117x |
value |
258 |
) |
|
259 |
) |
|
260 | 117x |
invisible(NULL) |
261 |
}, |
|
262 | ||
263 |
#' @description |
|
264 |
#' Some methods need an additional `!is.na(varame)` condition to drop |
|
265 |
#' missing values. When `private$na_rm = TRUE`, `self$get_call` returns |
|
266 |
#' condition extended by `!is.na`. |
|
267 |
#' |
|
268 |
#' @param value `logical(1)`\cr |
|
269 |
#' when `TRUE`, `FilterState$get_call` appends an expression |
|
270 |
#' removing `NA` values to the filter expression returned by `get_call` |
|
271 |
#' |
|
272 |
#' @return NULL invisibly |
|
273 |
#' |
|
274 |
set_na_rm = function(value) { |
|
275 | 48x |
checkmate::assert_flag(value) |
276 | 48x |
private$na_rm <- value |
277 | 48x |
invisible(NULL) |
278 |
}, |
|
279 | ||
280 |
#' @description |
|
281 |
#' Set selection. |
|
282 |
#' |
|
283 |
#' @param value (`vector`)\cr |
|
284 |
#' value(s) that come from filter selection; values are set in the |
|
285 |
#' module server after a selection is made in the app interface; |
|
286 |
#' values are stored in `private$selected` which is reactive; |
|
287 |
#' value types have to be the same as `private$choices` |
|
288 |
#' |
|
289 |
#' @return NULL invisibly |
|
290 |
#' |
|
291 |
set_selected = function(value) { |
|
292 | 580x |
logger::log_trace( |
293 | 580x |
sprintf( |
294 | 580x |
"%s$set_selected setting selection of variable %s, dataname: %s.", |
295 | 580x |
class(self)[1], |
296 | 580x |
private$varname, |
297 | 580x |
private$dataname |
298 |
) |
|
299 |
) |
|
300 | 580x |
value <- private$cast_and_validate(value) |
301 | 577x |
value <- private$remove_out_of_bound_values(value) |
302 | 577x |
private$validate_selection(value) |
303 | 573x |
private$selected(value) |
304 | 573x |
logger::log_trace(sprintf( |
305 | 573x |
"%s$set_selected selection of variable %s set, dataname: %s", |
306 | 573x |
class(self)[1], |
307 | 573x |
private$varname, |
308 | 573x |
private$dataname |
309 |
)) |
|
310 | 573x |
invisible(NULL) |
311 |
}, |
|
312 | ||
313 |
#' @description |
|
314 |
#' Set state. |
|
315 |
#' |
|
316 |
#' @param state (`list`)\cr |
|
317 |
#' contains fields relevant for a specific class: |
|
318 |
#' \itemize{ |
|
319 |
#' \item{`selected`}{ defines initial selection} |
|
320 |
#' \item{`keep_na` (`logical`)}{ defines whether to keep or remove `NA` values} |
|
321 |
#' } |
|
322 |
#' |
|
323 |
#' @return NULL invisibly |
|
324 |
#' |
|
325 |
set_state = function(state) { |
|
326 | 191x |
logger::log_trace(sprintf( |
327 | 191x |
"%s$set_state, dataname: %s setting state of variable %s to: selected=%s, keep_na=%s", |
328 | 191x |
class(self)[1], |
329 | 191x |
private$dataname, |
330 | 191x |
private$varname, |
331 | 191x |
paste(state$selected, collapse = " "), |
332 | 191x |
state$keep_na |
333 |
)) |
|
334 | 191x |
stopifnot(is.list(state) && all(names(state) %in% c("selected", "keep_na"))) |
335 | 187x |
if (!is.null(state$keep_na)) { |
336 | 68x |
self$set_keep_na(state$keep_na) |
337 |
} |
|
338 | 187x |
if (!is.null(state$selected)) { |
339 | 182x |
self$set_selected(state$selected) |
340 |
} |
|
341 | 187x |
logger::log_trace( |
342 | 187x |
sprintf( |
343 | 187x |
"%s$set_state, dataname: %s done setting state for variable %s", |
344 | 187x |
class(self)[1], |
345 | 187x |
private$dataname, |
346 | 187x |
private$varname |
347 |
) |
|
348 |
) |
|
349 | 187x |
invisible(NULL) |
350 |
}, |
|
351 | ||
352 |
#' @description |
|
353 |
#' Shiny module server. |
|
354 |
#' |
|
355 |
#' @param id (`character(1)`)\cr |
|
356 |
#' shiny module instance id |
|
357 |
#' |
|
358 |
#' @return `moduleServer` function which returns reactive value |
|
359 |
#' signaling that remove button has been clicked |
|
360 |
#' |
|
361 |
server = function(id) { |
|
362 | ! |
moduleServer( |
363 | ! |
id = id, |
364 | ! |
function(input, output, session) { |
365 | ! |
private$server_inputs("inputs") |
366 | ! |
reactive(input$remove) # back to parent to remove self |
367 |
} |
|
368 |
) |
|
369 |
}, |
|
370 | ||
371 |
#' @description |
|
372 |
#' Shiny module UI. |
|
373 |
#' |
|
374 |
#' @param id (`character(1)`)\cr |
|
375 |
#' shiny element (module instance) id; |
|
376 |
#' the UI for this class contains simple message stating that it is not supported |
|
377 |
#' |
|
378 |
ui = function(id) { |
|
379 | ! |
ns <- NS(id) |
380 | ! |
fluidPage( |
381 | ! |
theme = get_teal_bs_theme(), |
382 | ! |
fluidRow( |
383 | ! |
column( |
384 | ! |
width = 10, |
385 | ! |
class = "no-left-right-padding", |
386 | ! |
tags$div( |
387 | ! |
tags$span(private$varname, |
388 | ! |
class = "filter_panel_varname" |
389 |
), |
|
390 | ! |
if (checkmate::test_character(self$get_varlabel(), min.len = 1) && |
391 | ! |
tolower(private$varname) != tolower(self$get_varlabel())) { |
392 | ! |
tags$span(self$get_varlabel(), class = "filter_panel_varlabel") |
393 |
} |
|
394 |
) |
|
395 |
), |
|
396 | ! |
column( |
397 | ! |
width = 2, |
398 | ! |
class = "no-left-right-padding", |
399 | ! |
actionLink( |
400 | ! |
ns("remove"), |
401 | ! |
label = "", |
402 | ! |
icon = icon("circle-xmark", lib = "font-awesome"), |
403 | ! |
class = "remove pull-right" |
404 |
) |
|
405 |
) |
|
406 |
), |
|
407 | ! |
private$ui_inputs(ns("inputs")) |
408 |
) |
|
409 |
} |
|
410 |
), |
|
411 | ||
412 |
# private members ---- |
|
413 |
private = list( |
|
414 |
choices = NULL, # because each class has different choices type |
|
415 |
dataname = character(0), |
|
416 |
keep_na = NULL, # reactiveVal logical() |
|
417 |
na_count = integer(0), |
|
418 |
na_rm = FALSE, # it's logical(1) |
|
419 |
observers = NULL, # here observers are stored |
|
420 |
selected = NULL, # because it holds reactiveVal and each class has different choices type |
|
421 |
varname = character(0), |
|
422 |
varlabel = character(0), |
|
423 |
extract_type = logical(0), |
|
424 | ||
425 |
# private methods ---- |
|
426 | ||
427 |
# @description |
|
428 |
# Return variable name prefixed by dataname to be evaluated as extracted object, |
|
429 |
# for example `data$var` |
|
430 |
# @return a character string representation of a subset call |
|
431 |
# that extracts the variable from the dataset |
|
432 |
get_varname_prefixed = function() { |
|
433 | 191x |
ans <- |
434 | 191x |
if (isTRUE(private$extract_type == "list")) { |
435 | 25x |
sprintf("%s$%s", private$dataname, private$varname) |
436 | 191x |
} else if (isTRUE(private$extract_type == "matrix")) { |
437 | 5x |
sprintf("%s[, \"%s\"]", private$dataname, private$varname) |
438 |
} else { |
|
439 | 161x |
private$varname |
440 |
} |
|
441 | 191x |
str2lang(ans) |
442 |
}, |
|
443 | ||
444 |
# @description |
|
445 |
# Adds `is.na(varname)` before existing condition calls if `keep_na` is selected. |
|
446 |
# Otherwise, if missings are found in the variable `!is.na` will be added |
|
447 |
# only if `private$na_rm = TRUE` |
|
448 |
# @return a `call` |
|
449 |
add_keep_na_call = function(filter_call) { |
|
450 | 123x |
if (isTRUE(self$get_keep_na())) { |
451 | 13x |
call("|", call("is.na", private$get_varname_prefixed()), filter_call) |
452 | 110x |
} else if (isTRUE(private$na_rm) && private$na_count > 0L) { |
453 | 5x |
call( |
454 |
"&", |
|
455 | 5x |
call("!", call("is.na", private$get_varname_prefixed())), |
456 | 5x |
filter_call |
457 |
) |
|
458 |
} else { |
|
459 | 105x |
filter_call |
460 |
} |
|
461 |
}, |
|
462 | ||
463 |
# Sets `keep_na` field according to observed `input$keep_na` |
|
464 |
# If `keep_na = TRUE` `is.na(varname)` is added to the returned call. |
|
465 |
# Otherwise returned call excludes `NA` when executed. |
|
466 |
observe_keep_na = function(input) { |
|
467 | ||
468 |
}, |
|
469 | ||
470 |
# @description |
|
471 |
# Set choices is supposed to be executed once in the constructor |
|
472 |
# to define set/range which selection is made from. |
|
473 |
# parameter choices (`vector`)\cr |
|
474 |
# class of the vector depends on the `FilterState` class. |
|
475 |
# @return `NULL` |
|
476 |
set_choices = function(choices) { |
|
477 | 295x |
private$choices <- choices |
478 | 295x |
invisible(NULL) |
479 |
}, |
|
480 | ||
481 |
# Checks if the selection is valid in terms of class and length. |
|
482 |
# It should not return anything but throw an error if selection |
|
483 |
# has a wrong class or is outside of possible choices |
|
484 |
validate_selection = function(value) { |
|
485 | 13x |
invisible(NULL) |
486 |
}, |
|
487 | ||
488 |
# Filters out erroneous values from an array. |
|
489 |
# |
|
490 |
# @param values the array of values |
|
491 |
# |
|
492 |
# @return the array of values without elements, which are outside of |
|
493 |
# the accepted set for this FilterState |
|
494 |
remove_out_of_bound_values = function(values) { |
|
495 | 57x |
values |
496 |
}, |
|
497 | ||
498 |
# Casts an array of values to the type fitting this `FilterState` |
|
499 |
# and validates the elements of the casted array |
|
500 |
# satisfy the requirements of this `FilterState`. |
|
501 |
# |
|
502 |
# @param values the array of values |
|
503 |
# |
|
504 |
# @return the casted array |
|
505 |
# |
|
506 |
# @note throws an error if the casting did not execute successfully. |
|
507 |
cast_and_validate = function(values) { |
|
508 | 13x |
values |
509 |
}, |
|
510 | ||
511 |
# shiny modules ----- |
|
512 |
# module with inputs |
|
513 |
ui_inputs = function(id) { |
|
514 | ! |
stop("abstract class") |
515 |
}, |
|
516 |
# module with inputs |
|
517 |
server_inputs = function(id) { |
|
518 | ! |
stop("abstract class") |
519 |
}, |
|
520 | ||
521 |
# @description |
|
522 |
# module displaying input to keep or remove NA in the FilterState call |
|
523 |
# @param id `shiny` id parameter |
|
524 |
# renders checkbox input only when variable from which FilterState has |
|
525 |
# been created has some NA values. |
|
526 |
keep_na_ui = function(id) { |
|
527 | ! |
ns <- NS(id) |
528 | ! |
if (private$na_count > 0) { |
529 | ! |
checkboxInput( |
530 | ! |
ns("value"), |
531 | ! |
sprintf("Keep NA (%s)", private$na_count), |
532 | ! |
value = self$get_keep_na() |
533 |
) |
|
534 |
} else { |
|
535 | ! |
NULL |
536 |
} |
|
537 |
}, |
|
538 | ||
539 |
# @description |
|
540 |
# module to handle NA values in the FilterState |
|
541 |
# @param shiny `id` parameter passed to moduleServer |
|
542 |
# module sets `private$keep_na` according to the selection. |
|
543 |
# Module also updates a UI element if the `private$keep_na` has been |
|
544 |
# changed through the api |
|
545 |
keep_na_srv = function(id) { |
|
546 | ! |
moduleServer(id, function(input, output, session) { |
547 |
# this observer is needed in the situation when private$keep_inf has been |
|
548 |
# changed directly by the api - then it's needed to rerender UI element |
|
549 |
# to show relevant values |
|
550 | ! |
private$observers$keep_na_api <- observeEvent( |
551 | ! |
ignoreNULL = FALSE, # nothing selected is possible for NA |
552 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
553 | ! |
eventExpr = self$get_keep_na(), |
554 | ! |
handlerExpr = { |
555 | ! |
if (!setequal(self$get_keep_na(), input$value)) { |
556 | ! |
updateCheckboxInput( |
557 | ! |
inputId = "value", |
558 | ! |
value = self$get_keep_na() |
559 |
) |
|
560 |
} |
|
561 |
} |
|
562 |
) |
|
563 | ! |
private$observers$keep_na <- observeEvent( |
564 | ! |
ignoreNULL = FALSE, # ignoreNULL: we don't want to ignore NULL when nothing is selected in the `selectInput` |
565 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
566 | ! |
eventExpr = input$value, |
567 | ! |
handlerExpr = { |
568 | ! |
keep_na <- if (is.null(input$value)) { |
569 | ! |
FALSE |
570 |
} else { |
|
571 | ! |
input$value |
572 |
} |
|
573 | ! |
self$set_keep_na(keep_na) |
574 | ! |
logger::log_trace( |
575 | ! |
sprintf( |
576 | ! |
"%s$server keep_na of variable %s set to: %s, dataname: %s", |
577 | ! |
class(self)[1], |
578 | ! |
private$varname, |
579 | ! |
deparse1(input$value), |
580 | ! |
private$dataname |
581 |
) |
|
582 |
) |
|
583 |
} |
|
584 |
) |
|
585 | ! |
invisible(NULL) |
586 |
}) |
|
587 |
} |
|
588 |
) |
|
589 |
) |
1 |
.filterable_class <- c("logical", "integer", "numeric", "factor", "character", "Date", "POSIXct", "POSIXlt") |
|
2 | ||
3 |
#' Initialize `FilterStates` object |
|
4 |
#' |
|
5 |
#' Initialize `FilterStates` object |
|
6 |
#' @param data (`data.frame`, `MultiAssayExperiment`, `SummarizedExperiment`, `matrix`)\cr |
|
7 |
#' the R object which `subset` function is applied on. |
|
8 |
#' |
|
9 |
#' @param dataname (`character(1)`)\cr |
|
10 |
#' name of the data used in the expression |
|
11 |
#' specified to the function argument attached to this `FilterStates`. |
|
12 |
#' |
|
13 |
#' @param datalabel (`character(0)` or `character(1)`)\cr |
|
14 |
#' text label value. |
|
15 |
#' |
|
16 |
#' @param ... (optional) |
|
17 |
#' additional arguments for specific classes: keys |
|
18 |
#' @keywords internal |
|
19 |
#' @export |
|
20 |
#' @examples |
|
21 |
#' df <- data.frame( |
|
22 |
#' character = letters, |
|
23 |
#' numeric = seq_along(letters), |
|
24 |
#' date = seq(Sys.Date(), length.out = length(letters), by = "1 day"), |
|
25 |
#' datetime = seq(Sys.time(), length.out = length(letters), by = "33.33 hours") |
|
26 |
#' ) |
|
27 |
#' rf <- teal.slice:::init_filter_states( |
|
28 |
#' data = df, |
|
29 |
#' dataname = "DF", |
|
30 |
#' varlabels = c( |
|
31 |
#' "character variable", "numeric variable", "date variable", "datetime variable" |
|
32 |
#' ) |
|
33 |
#' ) |
|
34 |
#' \dontrun{ |
|
35 |
#' shinyApp( |
|
36 |
#' ui = fluidPage( |
|
37 |
#' actionButton("clear", span(icon("xmark"), "Remove all filters")), |
|
38 |
#' rf$ui_add_filter_state(id = "add", data = df), |
|
39 |
#' rf$ui("states"), |
|
40 |
#' verbatimTextOutput("expr"), |
|
41 |
#' ), |
|
42 |
#' server = function(input, output, session) { |
|
43 |
#' rf$srv_add_filter_state(id = "add", data = df) |
|
44 |
#' rf$server(id = "states") |
|
45 |
#' output$expr <- renderText({ |
|
46 |
#' deparse1(rf$get_call(), collapse = "\n") |
|
47 |
#' }) |
|
48 |
#' observeEvent(input$clear, rf$state_list_empty()) |
|
49 |
#' } |
|
50 |
#' ) |
|
51 |
#' } |
|
52 |
init_filter_states <- function(data, |
|
53 |
dataname, |
|
54 |
datalabel = character(0), |
|
55 |
...) { |
|
56 | 275x |
UseMethod("init_filter_states") |
57 |
} |
|
58 | ||
59 |
#' @keywords internal |
|
60 |
#' @export |
|
61 |
init_filter_states.data.frame <- function(data, # nolint |
|
62 |
dataname, |
|
63 |
datalabel = character(0), |
|
64 |
varlabels = character(0), |
|
65 |
keys = character(0), |
|
66 |
...) { |
|
67 | 110x |
DFFilterStates$new( |
68 | 110x |
dataname = dataname, |
69 | 110x |
datalabel = datalabel, |
70 | 110x |
varlabels = varlabels, |
71 | 110x |
keys = keys |
72 |
) |
|
73 |
} |
|
74 | ||
75 |
#' @keywords internal |
|
76 |
#' @export |
|
77 |
init_filter_states.matrix <- function(data, # nolint |
|
78 |
dataname, |
|
79 |
datalabel = character(0), |
|
80 |
...) { |
|
81 | 28x |
MatrixFilterStates$new( |
82 | 28x |
dataname = dataname, |
83 | 28x |
datalabel = datalabel |
84 |
) |
|
85 |
} |
|
86 | ||
87 |
#' @keywords internal |
|
88 |
#' @export |
|
89 |
init_filter_states.MultiAssayExperiment <- function(data, # nolint |
|
90 |
dataname, |
|
91 |
datalabel = character(0), |
|
92 |
varlabels, |
|
93 |
keys = character(0), |
|
94 |
...) { |
|
95 | 28x |
if (!requireNamespace("MultiAssayExperiment", quietly = TRUE)) { |
96 | ! |
stop("Cannot load MultiAssayExperiment - please install the package or restart your session.") |
97 |
} |
|
98 | 28x |
MAEFilterStates$new( |
99 | 28x |
dataname = dataname, |
100 | 28x |
datalabel = datalabel, |
101 | 28x |
varlabels = varlabels, |
102 | 28x |
keys = keys |
103 |
) |
|
104 |
} |
|
105 | ||
106 |
#' @keywords internal |
|
107 |
#' @export |
|
108 |
init_filter_states.SummarizedExperiment <- function(data, # nolint |
|
109 |
dataname, |
|
110 |
datalabel = character(0), |
|
111 |
...) { |
|
112 | 109x |
if (!requireNamespace("SummarizedExperiment", quietly = TRUE)) { |
113 | ! |
stop("Cannot load SummarizedExperiment - please install the package or restart your session.") |
114 |
} |
|
115 | 109x |
SEFilterStates$new( |
116 | 109x |
dataname = dataname, |
117 | 109x |
datalabel = datalabel |
118 |
) |
|
119 |
} |
|
120 | ||
121 |
#' Gets supported filterable variable names |
|
122 |
#' |
|
123 |
#' Gets filterable variable names from a given object. The names match variables |
|
124 |
#' of classes in an array `teal.slice:::.filterable_class`. |
|
125 |
#' @param data (`object`)\cr |
|
126 |
#' the R object containing elements which class can be checked through `vapply` or `apply`. |
|
127 |
#' |
|
128 |
#' @examples |
|
129 |
#' df <- data.frame( |
|
130 |
#' a = letters[1:3], |
|
131 |
#' b = 1:3, |
|
132 |
#' c = Sys.Date() + 1:3, |
|
133 |
#' d = Sys.time() + 1:3, |
|
134 |
#' z = complex(3) |
|
135 |
#' ) |
|
136 |
#' teal.slice:::get_supported_filter_varnames(df) |
|
137 |
#' @return `character` the array of the matched element names |
|
138 |
#' @keywords internal |
|
139 |
get_supported_filter_varnames <- function(data) { |
|
140 | 133x |
UseMethod("get_supported_filter_varnames") |
141 |
} |
|
142 | ||
143 |
#' @keywords internal |
|
144 |
#' @export |
|
145 |
get_supported_filter_varnames.default <- function(data) { # nolint |
|
146 | 75x |
is_expected_class <- vapply( |
147 | 75x |
X = data, |
148 | 75x |
FUN = function(x) any(class(x) %in% .filterable_class), |
149 | 75x |
FUN.VALUE = logical(1) |
150 |
) |
|
151 | 75x |
names(is_expected_class[is_expected_class]) |
152 |
} |
|
153 | ||
154 |
#' @keywords internal |
|
155 |
#' @export |
|
156 |
get_supported_filter_varnames.matrix <- function(data) { # nolint |
|
157 |
# all columns are the same type in matrix |
|
158 | 3x |
is_expected_class <- class(data[, 1]) %in% .filterable_class |
159 | 3x |
if (is_expected_class && !is.null(names(data))) { |
160 | ! |
names(data) |
161 |
} else { |
|
162 | 3x |
character(0) |
163 |
} |
|
164 |
} |
|
165 | ||
166 |
#' @keywords internal |
|
167 |
#' @export |
|
168 |
get_supported_filter_varnames.FilteredDataset <- function(data) { # nolint |
|
169 | 54x |
get_supported_filter_varnames(data$get_dataset()) |
170 |
} |
|
171 | ||
172 |
#' @keywords internal |
|
173 |
#' @export |
|
174 |
get_supported_filter_varnames.MAEFilteredDataset <- function(data) { # nolint |
|
175 | 1x |
character(0) |
176 |
} |
|
177 | ||
178 | ||
179 |
#' @title Returns a `choices_labeled` object |
|
180 |
#' |
|
181 |
#' @param data (`data.frame`, `DFrame`, `list`)\cr |
|
182 |
#' where labels can be taken from in case when `varlabels` is not specified. |
|
183 |
#' `data` must be specified if `varlabels` is not specified. |
|
184 |
#' @param choices (`character`)\cr |
|
185 |
#' the array of chosen variables |
|
186 |
#' @param varlabels (`character`)\cr |
|
187 |
#' the labels of variables in data |
|
188 |
#' @param keys (`character`)\cr |
|
189 |
#' the names of the key columns in data |
|
190 |
#' @return `character(0)` if choices are empty; a `choices_labeled` object otherwise |
|
191 |
#' @keywords internal |
|
192 |
data_choices_labeled <- function(data, choices, varlabels = character(0), keys = character(0)) { |
|
193 | 10x |
if (length(choices) == 0) { |
194 | 1x |
return(character(0)) |
195 |
} |
|
196 | ||
197 | 9x |
choice_labels <- if (identical(varlabels, character(0))) { |
198 | 2x |
vapply( |
199 | 2x |
X = data, |
200 | 2x |
FUN.VALUE = character(1), |
201 | 2x |
FUN = function(x) { |
202 | 3x |
label <- attr(x, "label") |
203 | 3x |
if (length(label) != 1) { |
204 |
"" |
|
205 |
} else { |
|
206 | ! |
label |
207 |
} |
|
208 |
} |
|
209 | 2x |
)[choices] |
210 |
} else { |
|
211 | 7x |
varlabels |
212 |
} |
|
213 | ||
214 | 9x |
if (!identical(choice_labels, character(0))) { |
215 | 9x |
choice_labels[is.na(choice_labels) | choice_labels == ""] <- names( |
216 | 9x |
choice_labels[is.na(choice_labels) | choice_labels == ""] |
217 |
) |
|
218 | 9x |
choice_types <- setNames(variable_types(data = data, columns = choices), choices) |
219 | 9x |
choice_types[keys] <- "primary_key" |
220 | ||
221 | 9x |
choices_labeled( |
222 | 9x |
choices = choices, |
223 | 9x |
labels = unname(choice_labels[choices]), |
224 | 9x |
types = choice_types[choices] |
225 |
) |
|
226 |
} else { |
|
227 | ! |
choices |
228 |
} |
|
229 |
} |
1 |
#' Set "`<choice>:<label>`" type of Names |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' This is often useful for as it marks up the dropdown boxes for [shiny::selectInput()]. |
|
5 |
#' |
|
6 |
#' @param choices a character / numeric / logical vector |
|
7 |
#' @param labels character vector containing labels to be applied to `choices`. If `NA` then |
|
8 |
#' "Label Missing" will be used. |
|
9 |
#' @param subset a vector that is a subset of `choices`. This is useful if |
|
10 |
#' only a few variables need to be named. If this argument is used, the returned vector will |
|
11 |
#' match its order. |
|
12 |
#' @param types vector containing the types of the columns. |
|
13 |
#' @details If either `choices` or `labels` are factors, they are coerced to character. |
|
14 |
#' Duplicated elements from `choices` get removed. |
|
15 |
#' |
|
16 |
#' @return a named character vector |
|
17 |
choices_labeled <- function(choices, labels, subset = NULL, types = NULL) { |
|
18 | 9x |
if (is.factor(choices)) { |
19 | ! |
choices <- as.character(choices) |
20 |
} |
|
21 | ||
22 | 9x |
stopifnot( |
23 | 9x |
is.character(choices) || |
24 | 9x |
is.numeric(choices) || |
25 | 9x |
is.logical(choices) || |
26 | 9x |
(length(choices) == 1 && is.na(choices)) |
27 |
) |
|
28 | ||
29 | 9x |
if (is.factor(labels)) { |
30 | ! |
labels <- as.character(labels) |
31 |
} |
|
32 | ||
33 | 9x |
checkmate::assert_character(labels[!is.na(labels)], any.missing = FALSE) |
34 | 9x |
if (length(choices) != length(labels)) { |
35 | ! |
stop("length of choices must be the same as labels") |
36 |
} |
|
37 | 9x |
stopifnot(is.null(subset) || is.vector(subset)) |
38 | 9x |
stopifnot(is.null(types) || is.vector(types)) |
39 | ||
40 | 9x |
if (is.vector(types)) { |
41 | 9x |
stopifnot(length(choices) == length(types)) |
42 |
} |
|
43 | ||
44 | 9x |
if (!is.null(subset)) { |
45 | ! |
if (!all(subset %in% choices)) { |
46 | ! |
stop("all of subset variables must be in choices") |
47 |
} |
|
48 | ! |
labels <- labels[choices %in% subset] |
49 | ! |
types <- types[choices %in% subset] |
50 | ! |
choices <- choices[choices %in% subset] |
51 |
} |
|
52 | ||
53 | 9x |
is_dupl <- duplicated(choices) |
54 | 9x |
choices <- choices[!is_dupl] |
55 | 9x |
labels <- labels[!is_dupl] |
56 | 9x |
types <- types[!is_dupl] |
57 | 9x |
labels[is.na(labels)] <- "Label Missing" |
58 | 9x |
raw_labels <- labels |
59 | 9x |
combined_labels <- if (length(choices) > 0) { |
60 | 9x |
paste0(choices, ": ", labels) |
61 |
} else { |
|
62 | ! |
character(0) |
63 |
} |
|
64 | ||
65 | 9x |
if (!is.null(subset)) { |
66 | ! |
ord <- match(subset, choices) |
67 | ! |
choices <- choices[ord] |
68 | ! |
raw_labels <- raw_labels[ord] |
69 | ! |
combined_labels <- combined_labels[ord] |
70 | ! |
types <- types[ord] |
71 |
} |
|
72 | 9x |
choices <- structure( |
73 | 9x |
choices, |
74 | 9x |
names = combined_labels, |
75 | 9x |
raw_labels = raw_labels, |
76 | 9x |
combined_labels = combined_labels, |
77 | 9x |
class = c("choices_labeled", "character"), |
78 | 9x |
types = types |
79 |
) |
|
80 | ||
81 | 9x |
return(choices) |
82 |
} |
1 |
# DefaultFilteredDataset ------ |
|
2 |
#' @title The `DefaultFilteredDataset` R6 class |
|
3 |
#' @keywords internal |
|
4 |
#' @examples |
|
5 |
#' library(shiny) |
|
6 |
#' ds <- teal.slice:::DefaultFilteredDataset$new(iris, "iris") |
|
7 |
#' isolate( |
|
8 |
#' ds$set_filter_state( |
|
9 |
#' state = list( |
|
10 |
#' Species = list(selected = "virginica"), |
|
11 |
#' Petal.Length = list(selected = c(2.0, 5)) |
|
12 |
#' ) |
|
13 |
#' ) |
|
14 |
#' ) |
|
15 |
#' isolate(ds$get_filter_state()) |
|
16 |
#' isolate(ds$get_call()) |
|
17 |
DefaultFilteredDataset <- R6::R6Class( # nolint |
|
18 |
classname = "DefaultFilteredDataset", |
|
19 |
inherit = FilteredDataset, |
|
20 |
public = list( |
|
21 | ||
22 |
#' @description |
|
23 |
#' Initializes this `DefaultFilteredDataset` object |
|
24 |
#' |
|
25 |
#' @param dataset (`data.frame`)\cr |
|
26 |
#' single data.frame for which filters are rendered |
|
27 |
#' @param dataname (`character`)\cr |
|
28 |
#' A given name for the dataset it may not contain spaces |
|
29 |
#' @param keys optional, (`character`)\cr |
|
30 |
#' Vector with primary keys |
|
31 |
#' @param label (`character`)\cr |
|
32 |
#' Label to describe the dataset |
|
33 |
#' @param metadata (named `list` or `NULL`) \cr |
|
34 |
#' Field containing metadata about the dataset. Each element of the list |
|
35 |
#' should be atomic and length one. |
|
36 |
initialize = function(dataset, dataname, keys = character(0), label = character(0), metadata = NULL) { |
|
37 | 109x |
checkmate::assert_class(dataset, "data.frame") |
38 | 109x |
super$initialize(dataset, dataname, keys, label, metadata) |
39 | 109x |
dataname <- self$get_dataname() |
40 | ||
41 | 109x |
private$add_filter_states( |
42 | 109x |
filter_states = init_filter_states( |
43 | 109x |
data = self$get_dataset(), |
44 | 109x |
dataname = dataname, |
45 | 109x |
varlabels = self$get_varlabels(), |
46 | 109x |
keys = self$get_keys() |
47 |
), |
|
48 | 109x |
id = "filter" |
49 |
) |
|
50 | 109x |
invisible(self) |
51 |
}, |
|
52 | ||
53 |
#' @description |
|
54 |
#' Gets the filter expression |
|
55 |
#' |
|
56 |
#' This functions returns filter calls equivalent to selected items |
|
57 |
#' within each of `filter_states`. Configuration of the calls is constant and |
|
58 |
#' depends on `filter_states` type and order which are set during initialization. |
|
59 |
#' This class contains single `FilterStates` |
|
60 |
#' which contains single `state_list` and all `FilterState` objects |
|
61 |
#' applies to one argument (`...`) in `dplyr::filter` call. |
|
62 |
#' @return filter `call` or `list` of filter calls |
|
63 |
get_call = function() { |
|
64 | 26x |
filter_call <- Filter( |
65 | 26x |
f = Negate(is.null), |
66 | 26x |
x = lapply( |
67 | 26x |
self$get_filter_states(), |
68 | 26x |
function(x) x$get_call() |
69 |
) |
|
70 |
) |
|
71 | 26x |
if (length(filter_call) == 0) { |
72 | 17x |
return(NULL) |
73 |
} |
|
74 | 9x |
filter_call |
75 |
}, |
|
76 | ||
77 |
#' @description |
|
78 |
#' Gets the reactive values from the active `FilterState` objects. |
|
79 |
#' |
|
80 |
#' Get all active filters from this dataset in form of the nested list. |
|
81 |
#' The output list is a compatible input to `self$set_filter_state`. |
|
82 |
#' @return `list` with named elements corresponding to `FilterState` objects |
|
83 |
#' (active filters). |
|
84 |
get_filter_state = function() { |
|
85 | 36x |
self$get_filter_states("filter")$get_filter_state() |
86 |
}, |
|
87 | ||
88 |
#' @description |
|
89 |
#' Set filter state |
|
90 |
#' |
|
91 |
#' @param state (`named list`)\cr |
|
92 |
#' containing values of the initial filter. Values should be relevant |
|
93 |
#' to the referred column. |
|
94 |
#' @param ... Additional arguments. Note that this is currently not used |
|
95 |
#' @examples |
|
96 |
#' dataset <- teal.slice:::DefaultFilteredDataset$new(iris, "iris") |
|
97 |
#' fs <- list( |
|
98 |
#' Sepal.Length = list(selected = c(5.1, 6.4), keep_na = TRUE, keep_inf = TRUE), |
|
99 |
#' Species = list(selected = c("setosa", "versicolor"), keep_na = FALSE) |
|
100 |
#' ) |
|
101 |
#' shiny::isolate(dataset$set_filter_state(state = fs)) |
|
102 |
#' shiny::isolate(dataset$get_filter_state()) |
|
103 |
#' |
|
104 |
#' @return `NULL` |
|
105 |
set_filter_state = function(state, ...) { |
|
106 | 40x |
checkmate::assert_list(state) |
107 | 40x |
logger::log_trace( |
108 | 40x |
sprintf( |
109 | 40x |
"DefaultFilteredDataset$set_filter_state setting up filters of variables %s, dataname: %s", |
110 | 40x |
paste(names(state), collapse = ", "), |
111 | 40x |
self$get_dataname() |
112 |
) |
|
113 |
) |
|
114 | ||
115 | 40x |
data <- self$get_dataset() |
116 | 40x |
fs <- self$get_filter_states()[[1]] |
117 | 40x |
fs$set_filter_state(state = state, data = data, ...) |
118 | 38x |
logger::log_trace( |
119 | 38x |
sprintf( |
120 | 38x |
"DefaultFilteredDataset$set_filter_state done setting up filters of variables %s, dataname: %s", |
121 | 38x |
paste(names(state), collapse = ", "), |
122 | 38x |
self$get_dataname() |
123 |
) |
|
124 |
) |
|
125 | 38x |
NULL |
126 |
}, |
|
127 | ||
128 |
#' @description Remove one or more `FilterState` of a `FilteredDataset` |
|
129 |
#' |
|
130 |
#' @param state_id (`character`)\cr |
|
131 |
#' Vector of character names of variables to remove their `FilterState`. |
|
132 |
#' |
|
133 |
#' @return `NULL` |
|
134 |
remove_filter_state = function(state_id) { |
|
135 | 5x |
logger::log_trace( |
136 | 5x |
sprintf( |
137 | 5x |
"DefaultFilteredDataset$remove_filter_state removing filters of variable %s, dataname: %s", |
138 | 5x |
state_id, |
139 | 5x |
self$get_dataname() |
140 |
) |
|
141 |
) |
|
142 | ||
143 | 5x |
fdata_filter_state <- self$get_filter_states()[[1]] |
144 | 5x |
for (element in state_id) { |
145 | 7x |
fdata_filter_state$remove_filter_state(element) |
146 |
} |
|
147 | 5x |
logger::log_trace( |
148 | 5x |
sprintf( |
149 | 5x |
"DefaultFilteredDataset$remove_filter_state done removing filters of variable %s, dataname: %s", |
150 | 5x |
state_id, |
151 | 5x |
self$get_dataname() |
152 |
) |
|
153 |
) |
|
154 | 5x |
invisible(NULL) |
155 |
}, |
|
156 | ||
157 |
#' @description |
|
158 |
#' UI module to add filter variable for this dataset |
|
159 |
#' |
|
160 |
#' UI module to add filter variable for this dataset |
|
161 |
#' @param id (`character(1)`)\cr |
|
162 |
#' identifier of the element - preferably containing dataset name |
|
163 |
#' |
|
164 |
#' @return function - shiny UI module |
|
165 |
ui_add_filter_state = function(id) { |
|
166 | ! |
ns <- NS(id) |
167 | ! |
tagList( |
168 | ! |
tags$label("Add", tags$code(self$get_dataname()), "filter"), |
169 | ! |
self$get_filter_states(id = "filter")$ui_add_filter_state( |
170 | ! |
id = ns("filter"), |
171 | ! |
data = self$get_dataset() |
172 |
) |
|
173 |
) |
|
174 |
}, |
|
175 | ||
176 |
#' @description |
|
177 |
#' Server module to add filter variable for this dataset |
|
178 |
#' |
|
179 |
#' Server module to add filter variable for this dataset. |
|
180 |
#' For this class `srv_add_filter_state` calls single module |
|
181 |
#' `srv_add_filter_state` from `FilterStates` (`DefaultFilteredDataset` |
|
182 |
#' contains single `FilterStates`) |
|
183 |
#' |
|
184 |
#' @param id (`character(1)`)\cr |
|
185 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
186 |
#' @param ... other arguments passed on to child `FilterStates` methods. |
|
187 |
#' |
|
188 |
#' @return `moduleServer` function which returns `NULL` |
|
189 |
srv_add_filter_state = function(id, ...) { |
|
190 | 8x |
check_ellipsis(..., stop = FALSE, allowed_args = "vars_include") |
191 | 8x |
moduleServer( |
192 | 8x |
id = id, |
193 | 8x |
function(input, output, session) { |
194 | 8x |
logger::log_trace( |
195 | 8x |
"DefaultFilteredDataset$srv_add_filter_state initializing, dataname: { deparse1(self$get_dataname()) }" |
196 |
) |
|
197 | 8x |
data <- self$get_dataset() |
198 | 8x |
self$get_filter_states(id = "filter")$srv_add_filter_state( |
199 | 8x |
id = "filter", |
200 | 8x |
data = data, |
201 |
... |
|
202 |
) |
|
203 | 8x |
logger::log_trace( |
204 | 8x |
"DefaultFilteredDataset$srv_add_filter_state initialized, dataname: { deparse1(self$get_dataname()) }" |
205 |
) |
|
206 | 8x |
NULL |
207 |
} |
|
208 |
) |
|
209 |
}, |
|
210 | ||
211 |
#' @description |
|
212 |
#' Get number of observations based on given keys |
|
213 |
#' The output shows the comparison between `filtered_dataset` |
|
214 |
#' function parameter and the dataset inside self |
|
215 |
#' @param filtered_dataset comparison object, of the same class |
|
216 |
#' as `self$get_dataset()`, if `NULL` then `self$get_dataset()` |
|
217 |
#' is used. |
|
218 |
#' @param subject_keys (`character` or `NULL`) columns denoting unique subjects when |
|
219 |
#' calculating the filtering. |
|
220 |
#' @return `list` containing character `#filtered/#not_filtered` |
|
221 |
get_filter_overview_nsubjs = function(filtered_dataset = self$get_dataset(), subject_keys = NULL) { |
|
222 | 8x |
checkmate::assert_class(filtered_dataset, classes = class(self$get_dataset())) |
223 | 8x |
checkmate::assert_character(subject_keys, null.ok = TRUE, any.missing = FALSE) |
224 | ||
225 | 8x |
f_rows <- if (length(subject_keys) == 0) { |
226 | 4x |
dplyr::n_distinct(filtered_dataset) |
227 |
} else { |
|
228 | 4x |
dplyr::n_distinct(filtered_dataset[subject_keys]) |
229 |
} |
|
230 | ||
231 | 8x |
nf_rows <- if (length(subject_keys) == 0) { |
232 | 4x |
dplyr::n_distinct(self$get_dataset()) |
233 |
} else { |
|
234 | 4x |
dplyr::n_distinct(self$get_dataset()[subject_keys]) |
235 |
} |
|
236 | ||
237 | 8x |
list(paste0(f_rows, "/", nf_rows)) |
238 |
} |
|
239 |
), |
|
240 |
private = list( |
|
241 |
# Gets filter overview observations number and returns a |
|
242 |
# list of the number of observations of filtered/non-filtered datasets |
|
243 |
get_filter_overview_nobs = function(filtered_dataset) { |
|
244 | 10x |
f_rows <- nrow(filtered_dataset) |
245 | 10x |
nf_rows <- nrow(self$get_dataset()) |
246 | 10x |
list( |
247 | 10x |
paste0(f_rows, "/", nf_rows) |
248 |
) |
|
249 |
} |
|
250 |
) |
|
251 |
) |
1 |
#' Initializes `FilteredDataset` |
|
2 |
#' |
|
3 |
#' @keywords internal |
|
4 |
#' @examples |
|
5 |
#' # DefaultFilteredDataset example |
|
6 |
#' iris_fd <- teal.slice:::init_filtered_dataset( |
|
7 |
#' iris, |
|
8 |
#' dataname = "iris", |
|
9 |
#' metadata = list(type = "teal") |
|
10 |
#' ) |
|
11 |
#' \dontrun{ |
|
12 |
#' shinyApp( |
|
13 |
#' ui = fluidPage( |
|
14 |
#' iris_fd$ui_add_filter_state(id = "add"), |
|
15 |
#' iris_fd$ui("dataset"), |
|
16 |
#' verbatimTextOutput("call"), |
|
17 |
#' verbatimTextOutput("metadata") |
|
18 |
#' ), |
|
19 |
#' server = function(input, output, session) { |
|
20 |
#' iris_fd$srv_add_filter_state(id = "add") |
|
21 |
#' iris_fd$server(id = "dataset") |
|
22 |
#' |
|
23 |
#' output$metadata <- renderText({ |
|
24 |
#' paste("Type =", iris_fd$get_metadata()$type) |
|
25 |
#' }) |
|
26 |
#' |
|
27 |
#' output$call <- renderText({ |
|
28 |
#' paste( |
|
29 |
#' vapply(iris_fd$get_call(), deparse1, character(1), collapse = "\n"), |
|
30 |
#' collapse = "\n" |
|
31 |
#' ) |
|
32 |
#' }) |
|
33 |
#' } |
|
34 |
#' ) |
|
35 |
#' } |
|
36 |
#' |
|
37 |
#' # MAEFilteredDataset example |
|
38 |
#' library(MultiAssayExperiment) |
|
39 |
#' data(miniACC) |
|
40 |
#' MAE_fd <- teal.slice:::init_filtered_dataset(miniACC, "MAE", metadata = list(type = "MAE")) |
|
41 |
#' \dontrun{ |
|
42 |
#' shinyApp( |
|
43 |
#' ui = fluidPage( |
|
44 |
#' MAE_fd$ui_add_filter_state(id = "add"), |
|
45 |
#' MAE_fd$ui("dataset"), |
|
46 |
#' verbatimTextOutput("call"), |
|
47 |
#' verbatimTextOutput("metadata") |
|
48 |
#' ), |
|
49 |
#' server = function(input, output, session) { |
|
50 |
#' MAE_fd$srv_add_filter_state(id = "add") |
|
51 |
#' MAE_fd$server(id = "dataset") |
|
52 |
#' output$metadata <- renderText({ |
|
53 |
#' paste("Type =", MAE_fd$get_metadata()$type) |
|
54 |
#' }) |
|
55 |
#' output$call <- renderText({ |
|
56 |
#' paste( |
|
57 |
#' vapply(MAE_fd$get_call(), deparse1, character(1), collapse = "\n"), |
|
58 |
#' collapse = "\n" |
|
59 |
#' ) |
|
60 |
#' }) |
|
61 |
#' } |
|
62 |
#' ) |
|
63 |
#' } |
|
64 |
#' @param dataset (`data.frame` or `MultiAssayExperiment`)\cr |
|
65 |
#' @param dataname (`character`)\cr |
|
66 |
#' A given name for the dataset it may not contain spaces |
|
67 |
#' @param keys optional, (`character`)\cr |
|
68 |
#' Vector with primary keys |
|
69 |
#' @param label (`character`)\cr |
|
70 |
#' Label to describe the dataset |
|
71 |
#' @param metadata (named `list` or `NULL`) \cr |
|
72 |
#' Field containing metadata about the dataset. Each element of the list |
|
73 |
#' should be atomic and length one. |
|
74 |
#' @export |
|
75 |
#' @note Although this function is exported for use in other packages, it may be changed or removed in a future release |
|
76 |
#' at which point any code which relies on this exported function will need to be changed. |
|
77 |
init_filtered_dataset <- function(dataset, # nolint |
|
78 |
dataname, |
|
79 |
keys = character(0), |
|
80 |
label = attr(dataset, "label"), |
|
81 |
metadata = NULL) { |
|
82 | 109x |
UseMethod("init_filtered_dataset") |
83 |
} |
|
84 | ||
85 |
#' @keywords internal |
|
86 |
#' @export |
|
87 |
init_filtered_dataset.data.frame <- function(dataset, # nolint |
|
88 |
dataname, |
|
89 |
keys = character(0), |
|
90 |
label = attr(dataset, "label"), |
|
91 |
metadata = NULL) { |
|
92 | 98x |
DefaultFilteredDataset$new(dataset, dataname, keys, label, metadata) |
93 |
} |
|
94 | ||
95 |
#' @keywords internal |
|
96 |
#' @export |
|
97 |
init_filtered_dataset.MultiAssayExperiment <- function(dataset, # nolint |
|
98 |
dataname, |
|
99 |
keys = character(0), |
|
100 |
label = attr(dataset, "label"), |
|
101 |
metadata = NULL) { |
|
102 | 11x |
if (!requireNamespace("MultiAssayExperiment", quietly = TRUE)) { |
103 | ! |
stop("Cannot load MultiAssayExperiment - please install the package or restart your session.") |
104 |
} |
|
105 | 11x |
MAEFilteredDataset$new(dataset, dataname, keys, label, metadata) |
106 |
} |
|
107 | ||
108 |
# FilteredDataset abstract -------- |
|
109 |
#' @title `FilterStates` R6 class |
|
110 |
#' @description |
|
111 |
#' `FilteredDataset` is a class which renders/controls `FilterStates`(s) |
|
112 |
#' Each `FilteredDataset` contains `filter_states` field - a `list` which contains one |
|
113 |
#' (`data.frame`) or multiple (`MultiAssayExperiment`) `FilterStates` objects. |
|
114 |
#' Each `FilterStates` is responsible for one filter/subset expression applied for specific |
|
115 |
#' components of the dataset. |
|
116 |
#' @keywords internal |
|
117 |
FilteredDataset <- R6::R6Class( # nolint |
|
118 |
"FilteredDataset", |
|
119 |
## __Public Methods ==== |
|
120 |
public = list( |
|
121 |
#' @description |
|
122 |
#' Initializes this `FilteredDataset` object |
|
123 |
#' |
|
124 |
#' @param dataset (`data.frame` or `MultiAssayExperiment`)\cr |
|
125 |
#' single dataset for which filters are rendered |
|
126 |
#' @param dataname (`character(1)`)\cr |
|
127 |
#' A given name for the dataset it may not contain spaces |
|
128 |
#' @param keys optional, (`character`)\cr |
|
129 |
#' Vector with primary keys |
|
130 |
#' @param label (`character(1)`)\cr |
|
131 |
#' Label to describe the dataset |
|
132 |
#' @param metadata (named `list` or `NULL`) \cr |
|
133 |
#' Field containing metadata about the dataset. Each element of the list |
|
134 |
#' should be atomic and length one. |
|
135 |
initialize = function(dataset, dataname, keys = character(0), label = attr(dataset, "label"), metadata = NULL) { |
|
136 |
# dataset assertion in child classes |
|
137 | ||
138 | 153x |
check_simple_name(dataname) |
139 | 153x |
checkmate::assert_character(keys, any.missing = FALSE) |
140 | 153x |
checkmate::assert_character(label, null.ok = TRUE) |
141 | 153x |
teal.data::validate_metadata(metadata) |
142 | ||
143 | 153x |
logger::log_trace("Instantiating { class(self)[1] }, dataname: { dataname }") |
144 | 153x |
private$dataset <- dataset |
145 | 153x |
private$dataname <- dataname |
146 | 153x |
private$keys <- keys |
147 | 153x |
private$label <- if (is.null(label)) character(0) else label |
148 | 153x |
private$metadata <- metadata |
149 | 153x |
invisible(self) |
150 |
}, |
|
151 | ||
152 | ||
153 |
#' @description |
|
154 |
#' Returns a string representation of the filter state in this `FilteredDataset`. |
|
155 |
#' |
|
156 |
#' @return `character(1)` the formatted string representing the filter state or |
|
157 |
#' `NULL` if no filter state is present. |
|
158 |
#' |
|
159 |
get_formatted_filter_state = function() { |
|
160 | 46x |
out <- Filter( |
161 | 46x |
function(x) x != "", |
162 | 46x |
sapply( |
163 | 46x |
self$get_filter_states(), |
164 | 46x |
function(states) { |
165 | 66x |
states$format(indent = 2) |
166 |
} |
|
167 |
) |
|
168 |
) |
|
169 | 46x |
if (length(out) > 0) { |
170 | 25x |
header <- paste0("Filters for dataset: ", self$get_dataname()) |
171 | 25x |
paste(c(header, out), collapse = "\n") |
172 |
} |
|
173 |
}, |
|
174 | ||
175 |
#' @description |
|
176 |
#' Removes all active filter items applied to this dataset |
|
177 |
#' @return NULL |
|
178 |
state_lists_empty = function() { |
|
179 | 14x |
logger::log_trace("Removing all filters from FilteredDataset: { deparse1(self$get_dataname()) }") |
180 | 14x |
lapply( |
181 | 14x |
self$get_filter_states(), |
182 | 14x |
function(state_list) state_list$state_list_empty() |
183 |
) |
|
184 | 14x |
logger::log_trace("Removed all filters from FilteredDataset: { deparse1(self$get_dataname()) }") |
185 | 14x |
NULL |
186 |
}, |
|
187 |
# managing filter states ----- |
|
188 | ||
189 | ||
190 |
# getters ---- |
|
191 |
#' @description |
|
192 |
#' Gets a filter expression |
|
193 |
#' |
|
194 |
#' This functions returns filter calls equivalent to selected items |
|
195 |
#' within each of `filter_states`. Configuration of the calls is constant and |
|
196 |
#' depends on `filter_states` type and order which are set during initialization. |
|
197 |
#' @return filter `call` or `list` of filter calls |
|
198 |
get_call = function() { |
|
199 | ! |
stop("Pure virtual method.") |
200 |
}, |
|
201 | ||
202 |
#' Gets the reactive values from the active `FilterState` objects. |
|
203 |
#' |
|
204 |
#' Get all active filters from this dataset in form of the nested list. |
|
205 |
#' The output list is a compatible input to `self$set_filter_state`. |
|
206 |
#' @return `list` with named elements corresponding to `FilterStates` objects |
|
207 |
#' with active filters. |
|
208 |
get_filter_state = function() { |
|
209 | 9x |
states <- lapply(self$get_filter_states(), function(x) x$get_filter_state()) |
210 | 9x |
Filter(function(x) length(x) > 0, states) |
211 |
}, |
|
212 | ||
213 |
#' @description |
|
214 |
#' Gets the active `FilterStates` objects. |
|
215 |
#' @param id (`character(1)`, `character(0)`)\cr |
|
216 |
#' the id of the `private$filter_states` list element where `FilterStates` is kept. |
|
217 |
#' @return `FilterStates` or `list` of `FilterStates` objects. |
|
218 |
get_filter_states = function(id = character(0)) { |
|
219 | 526x |
if (length(id) == 0) { |
220 | 463x |
private$filter_states |
221 |
} else { |
|
222 | 63x |
private$filter_states[[id]] |
223 |
} |
|
224 |
}, |
|
225 | ||
226 |
#' @description |
|
227 |
#' Gets the number of active `FilterState` objects in all `FilterStates` in this `FilteredDataset`. |
|
228 |
#' @return `integer(1)` |
|
229 |
get_filter_count = function() { |
|
230 | 12x |
sum(vapply(private$filter_states, |
231 | 12x |
function(state) state$get_filter_count(), |
232 | 12x |
FUN.VALUE = integer(1) |
233 |
)) |
|
234 |
}, |
|
235 | ||
236 |
#' @description |
|
237 |
#' Get name of the dataset |
|
238 |
#' |
|
239 |
#' Get name of the dataset |
|
240 |
#' @return `character(1)` as a name of this dataset |
|
241 |
get_dataname = function() { |
|
242 | 160x |
private$dataname |
243 |
}, |
|
244 | ||
245 |
#' @description |
|
246 |
#' Gets the dataset object in this `FilteredDataset` |
|
247 |
#' @return `data.frame` or `MultiAssayExperiment` |
|
248 |
get_dataset = function() { |
|
249 | 301x |
private$dataset |
250 |
}, |
|
251 | ||
252 |
#' @description |
|
253 |
#' Gets the metadata for the dataset in this `FilteredDataset` |
|
254 |
#' @return named `list` or `NULL` |
|
255 |
get_metadata = function() { |
|
256 | 4x |
private$metadata |
257 |
}, |
|
258 | ||
259 |
#' @description |
|
260 |
#' Get filter overview rows of a dataset |
|
261 |
#' The output shows the comparison between `filtered_dataset` |
|
262 |
#' function parameter and the dataset inside self |
|
263 |
#' @param filtered_dataset comparison object, of the same class |
|
264 |
#' as `self$get_dataset()`, if `NULL` then `self$get_dataset()` |
|
265 |
#' is used. |
|
266 |
#' @return (`matrix`) matrix of observations and subjects |
|
267 |
get_filter_overview_info = function(filtered_dataset = self$get_dataset()) { |
|
268 | 10x |
checkmate::assert_class(filtered_dataset, classes = class(self$get_dataset())) |
269 | 10x |
df <- cbind(private$get_filter_overview_nobs(filtered_dataset), "") |
270 | 10x |
rownames(df) <- self$get_dataname() |
271 | 10x |
colnames(df) <- c("Obs", "Subjects") |
272 | 10x |
df |
273 |
}, |
|
274 | ||
275 |
#' @description |
|
276 |
#' Gets the keys for the dataset of this `FilteredDataset` |
|
277 |
#' @return (`character`) the keys of dataset |
|
278 |
get_keys = function() { |
|
279 | 151x |
private$keys |
280 |
}, |
|
281 | ||
282 |
#' @description |
|
283 |
#' Gets labels of variables in the data |
|
284 |
#' |
|
285 |
#' Variables are the column names of the data. |
|
286 |
#' Either, all labels must have been provided for all variables |
|
287 |
#' in `set_data` or `NULL`. |
|
288 |
#' |
|
289 |
#' @param variables (`character` vector) variables to get labels for; |
|
290 |
#' if `NULL`, for all variables in data |
|
291 |
#' @return (`character` or `NULL`) variable labels, `NULL` if `column_labels` |
|
292 |
#' attribute does not exist for the data |
|
293 |
get_varlabels = function(variables = NULL) { |
|
294 | 115x |
checkmate::assert_character(variables, null.ok = TRUE, any.missing = FALSE) |
295 | ||
296 | 115x |
labels <- formatters::var_labels(private$dataset, fill = FALSE) |
297 | 115x |
if (is.null(labels)) { |
298 | ! |
return(NULL) |
299 |
} |
|
300 | 2x |
if (!is.null(variables)) labels <- labels[intersect(self$get_varnames(), variables)] |
301 | 115x |
labels |
302 |
}, |
|
303 | ||
304 |
#' @description |
|
305 |
#' Gets the dataset label |
|
306 |
#' @return (`character`) the dataset label |
|
307 |
get_dataset_label = function() { |
|
308 | 3x |
private$label |
309 |
}, |
|
310 | ||
311 |
#' @description |
|
312 |
#' Gets variable names from dataset |
|
313 |
#' @return `character` the variable names |
|
314 |
get_varnames = function() { |
|
315 | 4x |
colnames(self$get_dataset()) |
316 |
}, |
|
317 | ||
318 |
#' @description |
|
319 |
#' Gets variable names for the filtering. |
|
320 |
#' |
|
321 |
#' It takes the intersection of the column names |
|
322 |
#' of the data and `private$filterable_varnames` if |
|
323 |
#' `private$filterable_varnames` has positive length |
|
324 |
#' |
|
325 |
#' @return (`character` vector) of variable names |
|
326 |
get_filterable_varnames = function() { |
|
327 | 49x |
varnames <- get_supported_filter_varnames(self) |
328 | 49x |
if (length(private$filterable_varnames) > 0) { |
329 | 4x |
return(intersect(private$filterable_varnames, varnames)) |
330 |
} |
|
331 | 45x |
return(varnames) |
332 |
}, |
|
333 | ||
334 |
# setters ------ |
|
335 |
#' @description |
|
336 |
#' Set the allowed filterable variables |
|
337 |
#' @param varnames (`character` or `NULL`) The variables which can be filtered |
|
338 |
#' See `self$get_filterable_varnames` for more details |
|
339 |
#' |
|
340 |
#' @details When retrieving the filtered variables only |
|
341 |
#' those which have filtering supported (i.e. are of the permitted types) |
|
342 |
#' are included. |
|
343 |
#' |
|
344 |
#' @return invisibly this `FilteredDataset` |
|
345 |
set_filterable_varnames = function(varnames) { |
|
346 | 10x |
checkmate::assert_character(varnames, any.missing = FALSE, null.ok = TRUE) |
347 | 7x |
private$filterable_varnames <- varnames |
348 | 7x |
return(invisible(self)) |
349 |
}, |
|
350 | ||
351 |
# modules ------ |
|
352 |
#' @description |
|
353 |
#' UI module for dataset active filters |
|
354 |
#' |
|
355 |
#' UI module containing dataset active filters along with |
|
356 |
#' title and remove button. |
|
357 |
#' @param id (`character(1)`)\cr |
|
358 |
#' identifier of the element - preferably containing dataset name |
|
359 |
#' |
|
360 |
#' @return function - shiny UI module |
|
361 |
ui = function(id) { |
|
362 | ! |
dataname <- self$get_dataname() |
363 | ! |
checkmate::assert_string(dataname) |
364 | ||
365 | ! |
ns <- NS(id) |
366 | ! |
if_multiple_filter_states <- length(self$get_filter_states()) > 1 |
367 | ! |
span( |
368 | ! |
id = id, |
369 | ! |
include_css_files("filter-panel"), |
370 | ! |
div( |
371 | ! |
id = ns("whole_ui"), # to hide it entirely |
372 | ! |
fluidRow( |
373 | ! |
column( |
374 | ! |
width = 8, |
375 | ! |
tags$span(dataname, class = "filter_panel_dataname") |
376 |
), |
|
377 | ! |
column( |
378 | ! |
width = 4, |
379 | ! |
tagList( |
380 | ! |
actionLink( |
381 | ! |
ns("remove_filters"), |
382 | ! |
label = "", |
383 | ! |
icon = icon("circle-xmark", lib = "font-awesome"), |
384 | ! |
class = "remove pull-right" |
385 |
), |
|
386 | ! |
actionLink( |
387 | ! |
ns("collapse"), |
388 | ! |
label = "", |
389 | ! |
icon = icon("angle-down", lib = "font-awesome"), |
390 | ! |
class = "remove pull-right" |
391 |
) |
|
392 |
) |
|
393 |
) |
|
394 |
), |
|
395 | ! |
shinyjs::hidden( |
396 | ! |
div( |
397 | ! |
id = ns("filter_count_ui"), |
398 | ! |
tagList( |
399 | ! |
textOutput(ns("filter_count")), |
400 | ! |
br() |
401 |
) |
|
402 |
) |
|
403 |
), |
|
404 | ! |
div( |
405 |
# id needed to insert and remove UI to filter single variable as needed |
|
406 |
# it is currently also used by the above module to entirely hide this panel |
|
407 | ! |
id = ns("filters"), |
408 | ! |
class = "parent-hideable-list-group", |
409 | ! |
tagList( |
410 | ! |
lapply( |
411 | ! |
names(self$get_filter_states()), |
412 | ! |
function(x) { |
413 | ! |
tagList(self$get_filter_states(id = x)$ui(id = ns(x))) |
414 |
} |
|
415 |
) |
|
416 |
) |
|
417 |
) |
|
418 |
) |
|
419 |
) |
|
420 |
}, |
|
421 | ||
422 |
#' @description |
|
423 |
#' Server module for a dataset active filters |
|
424 |
#' |
|
425 |
#' Server module managing a active filters. |
|
426 |
#' @param id (`character(1)`)\cr |
|
427 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
428 |
#' @return `moduleServer` function which returns `NULL` |
|
429 |
server = function(id) { |
|
430 | 9x |
moduleServer( |
431 | 9x |
id = id, |
432 | 9x |
function(input, output, session) { |
433 | 9x |
dataname <- self$get_dataname() |
434 | 9x |
logger::log_trace("FilteredDataset$server initializing, dataname: { dataname }") |
435 | 9x |
checkmate::assert_string(dataname) |
436 | 9x |
shiny::setBookmarkExclude("remove_filters") |
437 | ||
438 | 9x |
output$filter_count <- renderText( |
439 | 9x |
sprintf( |
440 | 9x |
"%d filter%s applied", |
441 | 9x |
self$get_filter_count(), |
442 | 9x |
if (self$get_filter_count() != 1) "s" else "" |
443 |
) |
|
444 |
) |
|
445 | ||
446 | 9x |
lapply( |
447 | 9x |
names(self$get_filter_states()), |
448 | 9x |
function(x) { |
449 | 14x |
self$get_filter_states(id = x)$server(id = x) |
450 |
} |
|
451 |
) |
|
452 | ||
453 | 9x |
shiny::observeEvent(self$get_filter_state(), { |
454 | 2x |
shinyjs::hide("filter_count_ui") |
455 | 2x |
shinyjs::show("filters") |
456 | 2x |
shinyjs::toggle("remove_filters", condition = length(self$get_filter_state()) != 0) |
457 | 2x |
shinyjs::toggle("collapse", condition = length(self$get_filter_state()) != 0) |
458 |
}) |
|
459 | ||
460 | 9x |
shiny::observeEvent(input$collapse, { |
461 | ! |
shinyjs::toggle("filter_count_ui") |
462 | ! |
shinyjs::toggle("filters") |
463 | ! |
toggle_icon(session$ns("collapse"), c("fa-angle-right", "fa-angle-down")) |
464 |
}) |
|
465 | ||
466 | 9x |
observeEvent(input$remove_filters, { |
467 | 1x |
logger::log_trace("FilteredDataset$server@1 removing filters, dataname: { dataname }") |
468 | 1x |
lapply( |
469 | 1x |
self$get_filter_states(), |
470 | 1x |
function(x) x$state_list_empty() |
471 |
) |
|
472 | 1x |
logger::log_trace("FilteredDataset$server@1 removed filters, dataname: { dataname }") |
473 |
}) |
|
474 | ||
475 | 9x |
logger::log_trace("FilteredDataset$initialized, dataname: { dataname }") |
476 | 9x |
NULL |
477 |
} |
|
478 |
) |
|
479 |
}, |
|
480 | ||
481 |
#' @description |
|
482 |
#' UI module to add filter variable for this dataset |
|
483 |
#' |
|
484 |
#' UI module to add filter variable for this dataset |
|
485 |
#' @param id (`character(1)`)\cr |
|
486 |
#' identifier of the element - preferably containing dataset name |
|
487 |
#' |
|
488 |
#' @return function - shiny UI module |
|
489 |
ui_add_filter_state = function(id) { |
|
490 | 1x |
stop("Pure virtual method") |
491 |
}, |
|
492 | ||
493 |
#' @description |
|
494 |
#' Server module to add filter variable for this dataset |
|
495 |
#' |
|
496 |
#' Server module to add filter variable for this dataset |
|
497 |
#' @param id (`character(1)`)\cr |
|
498 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
499 |
#' @param ... ignored |
|
500 |
#' @return `moduleServer` function. |
|
501 |
srv_add_filter_state = function(id, ...) { |
|
502 | ! |
check_ellipsis(..., stop = FALSE) |
503 | ! |
moduleServer( |
504 | ! |
id = id, |
505 | ! |
function(input, output, session) { |
506 | ! |
stop("Pure virtual method") |
507 |
} |
|
508 |
) |
|
509 |
} |
|
510 |
), |
|
511 |
## __Private Fields ==== |
|
512 |
private = list( |
|
513 |
dataset = NULL, |
|
514 |
filter_states = list(), |
|
515 |
dataname = character(0), |
|
516 |
keys = character(0), |
|
517 |
label = character(0), |
|
518 |
metadata = NULL, |
|
519 | ||
520 |
# if this has length > 0 then only varnames in this vector |
|
521 |
# can be filtered |
|
522 |
filterable_varnames = NULL, |
|
523 | ||
524 |
# Adds `FilterStates` to the `private$filter_states`. |
|
525 |
# `FilterStates` is added once for each element of the dataset. |
|
526 |
# @param filter_states (`FilterStates`) |
|
527 |
# @param id (`character(1)`) |
|
528 |
add_filter_states = function(filter_states, id) { |
|
529 | 271x |
stopifnot(is(filter_states, "FilterStates")) |
530 | 271x |
checkmate::assert_string(id) |
531 | ||
532 | 271x |
x <- setNames(list(filter_states), id) |
533 | 271x |
private$filter_states <- c(self$get_filter_states(), x) |
534 |
}, |
|
535 | ||
536 |
# @description |
|
537 |
# Checks if the dataname exists and |
|
538 |
# (if provided) that varname is a valid column in the dataset |
|
539 |
# |
|
540 |
# Stops when this is not the case. |
|
541 |
# |
|
542 |
# @param varname (`character`) column within the dataset; |
|
543 |
# if `NULL`, this check is not performed |
|
544 |
check_data_varname_exists = function(varname = NULL) { |
|
545 | ! |
checkmate::assert_string(varname, null.ok = TRUE) |
546 | ||
547 | ! |
isolate({ |
548 | ! |
if (!is.null(varname) && !(varname %in% self$get_varnames())) { |
549 | ! |
stop( |
550 | ! |
sprintf("variable '%s' does not exist in data '%s'", varname, dataname) |
551 |
) |
|
552 |
} |
|
553 |
}) |
|
554 | ||
555 | ! |
return(invisible(NULL)) |
556 |
} |
|
557 |
) |
|
558 |
) |
1 |
#' Initialize `FilteredData` |
|
2 |
#' |
|
3 |
#' Initialize `FilteredData` |
|
4 |
#' @param x (named `list` or `TealData`) In case of `TealData` see [teal.data::teal_data()]. |
|
5 |
#' If the list is provided, it should contain `list`(s) containing following fields: |
|
6 |
#' - `dataset` data object object supported by [`FilteredDataset`]. |
|
7 |
#' - `metatada` (optional) additional metadata attached to the `dataset`. |
|
8 |
#' - `keys` (optional) primary keys. |
|
9 |
#' - `datalabel` (optional) label describing the `dataset`. |
|
10 |
#' - `parent` (optional) which `dataset` is a parent of this one. |
|
11 |
#' @param join_keys (`JoinKeys`) see [teal.data::join_keys()]. |
|
12 |
#' @param code (`CodeClass`) see [`teal.data::CodeClass`]. |
|
13 |
#' @param cdisc (`logical(1)`) whether data is of `cdisc` type (relational). |
|
14 |
#' @param check (`logical(1)`) whether data has been check against reproducibility. |
|
15 |
#' @examples |
|
16 |
#' library(shiny) |
|
17 |
#' datasets <- teal.slice::init_filtered_data( |
|
18 |
#' x = list( |
|
19 |
#' iris = list(dataset = iris), |
|
20 |
#' mtcars = list(dataset = mtcars, metadata = list(type = "training")) |
|
21 |
#' ) |
|
22 |
#' ) |
|
23 |
#' @export |
|
24 |
init_filtered_data <- function(x, join_keys, code, cdisc, check) { |
|
25 | 35x |
UseMethod("init_filtered_data") |
26 |
} |
|
27 | ||
28 |
#' @keywords internal |
|
29 |
#' @export |
|
30 |
init_filtered_data.TealData <- function(x, # nolint |
|
31 |
join_keys = x$get_join_keys(), |
|
32 |
code = x$get_code_class(), |
|
33 |
cdisc = FALSE, |
|
34 |
check = x$get_check()) { |
|
35 | 12x |
cdisc <- length(join_keys$get_parents()) > 0 |
36 | 12x |
data_objects <- lapply(x$get_datanames(), function(dataname) { |
37 | 23x |
dataset <- x$get_dataset(dataname) |
38 | ||
39 | 23x |
parent <- if (cdisc) join_keys$get_parent(dataname) else NULL |
40 | ||
41 | 23x |
return_list <- list( |
42 | 23x |
dataset = dataset$get_raw_data(), |
43 | 23x |
keys = dataset$get_keys(), |
44 | 23x |
metadata = dataset$get_metadata(), |
45 | 23x |
label = dataset$get_dataset_label() |
46 |
) |
|
47 | ||
48 | 22x |
if (cdisc) return_list[["parent"]] <- parent |
49 | 23x |
return_list |
50 |
}) |
|
51 | ||
52 | 12x |
names(data_objects) <- x$get_datanames() |
53 | ||
54 | 12x |
init_filtered_data( |
55 | 12x |
x = data_objects, |
56 | 12x |
join_keys = join_keys, |
57 | 12x |
code = code, |
58 | 12x |
check = check, |
59 | 12x |
cdisc = cdisc |
60 |
) |
|
61 |
} |
|
62 | ||
63 |
#' @keywords internal |
|
64 |
#' @export |
|
65 |
init_filtered_data.default <- function(x, join_keys = NULL, code = NULL, cdisc = FALSE, check = FALSE) { # nolint |
|
66 | 23x |
checkmate::assert_list(x, any.missing = FALSE, names = "unique") |
67 | 22x |
mapply(validate_dataset_args, x, names(x), MoreArgs = list(allowed_parent = cdisc)) |
68 | 21x |
checkmate::assert_class(code, "CodeClass", null.ok = TRUE) |
69 | 20x |
checkmate::assert_class(join_keys, "JoinKeys", null.ok = TRUE) |
70 | 19x |
checkmate::assert_flag(check) |
71 | ||
72 | 18x |
datasets <- if (cdisc) { |
73 | 11x |
CDISCFilteredData$new(x, join_keys = join_keys, code = code, check = check) |
74 |
} else { |
|
75 | 7x |
FilteredData$new(x, join_keys = join_keys, code = code, check = check) |
76 |
} |
|
77 |
} |
|
78 | ||
79 |
#' Validate dataset arguments |
|
80 |
#' |
|
81 |
#' Validate dataset arguments |
|
82 |
#' @param dataset_args (`list`)\cr |
|
83 |
#' containing the arguments except (`dataname`) |
|
84 |
#' needed by `init_filtered_dataset` |
|
85 |
#' @param dataname (`character(1)`)\cr |
|
86 |
#' the name of the `dataset` to be added to this object |
|
87 |
#' @param allowed_parent (`logical(1)`)\cr |
|
88 |
#' whether `FilteredDataset` can have a parent - i.e. if it's a part of `CDISCFilteredData` |
|
89 |
#' @keywords internal |
|
90 |
#' @return (`NULL` or throws an error) |
|
91 |
validate_dataset_args <- function(dataset_args, dataname, allowed_parent = FALSE) { |
|
92 | 173x |
check_simple_name(dataname) |
93 | 173x |
checkmate::assert_flag(allowed_parent) |
94 | 172x |
checkmate::assert_list(dataset_args, names = "unique") |
95 | ||
96 | 172x |
allowed_names <- c("dataset", "keys", "label", "metadata") |
97 | 172x |
if (allowed_parent) { |
98 | 62x |
allowed_names <- c(allowed_names, "parent") |
99 |
} |
|
100 | ||
101 | 172x |
checkmate::assert_subset(names(dataset_args), choices = allowed_names) |
102 | ||
103 | 172x |
checkmate::assert_multi_class(dataset_args[["dataset"]], classes = c("data.frame", "MultiAssayExperiment")) |
104 | 172x |
checkmate::assert_character(dataset_args[["keys"]], null.ok = TRUE) |
105 | 172x |
teal.data::validate_metadata(dataset_args[["metadata"]]) |
106 | 172x |
checkmate::assert_character(dataset_args[["label"]], null.ok = TRUE, min.len = 0, max.len = 1) |
107 | 172x |
checkmate::assert_character(dataset_args[["parent"]], null.ok = TRUE, min.len = 0, max.len = 1) |
108 |
} |
|
109 | ||
110 |
#' Evaluate expression with meaningful message |
|
111 |
#' |
|
112 |
#' Method created for the `FilteredData` to execute filter call with |
|
113 |
#' meaningful message. After evaluation used environment should contain |
|
114 |
#' all necessary bindings. |
|
115 |
#' @param expr (`language`) |
|
116 |
#' @param env (`environment`) where expression is evaluated. |
|
117 |
#' @return invisible `NULL`. |
|
118 |
#' @keywords internal |
|
119 |
eval_expr_with_msg <- function(expr, env) { |
|
120 | 14x |
lapply( |
121 | 14x |
expr, |
122 | 14x |
function(x) { |
123 | 2x |
tryCatch( |
124 | 2x |
eval(x, envir = env), |
125 | 2x |
error = function(e) { |
126 | ! |
stop( |
127 | ! |
sprintf( |
128 | ! |
"Call execution failed:\n - call:\n %s\n - message:\n %s ", |
129 | ! |
deparse1(x, collapse = "\n"), e |
130 |
) |
|
131 |
) |
|
132 |
} |
|
133 |
) |
|
134 | 2x |
return(invisible(NULL)) |
135 |
} |
|
136 |
) |
|
137 |
} |
|
138 | ||
139 | ||
140 |
#' Toggle button properties. |
|
141 |
#' |
|
142 |
#' Switch between different icons or titles on a button. |
|
143 |
#' |
|
144 |
#' Wrapper functions that use `shinyjs::runjs` to change button properties in response to events, |
|
145 |
#' typically clicking those very buttons. |
|
146 |
#' `shiny`'s `actionButton` and `actionLink` create `<a>` tags, |
|
147 |
#' which may contain a child `<i>` tag that specifies an icon to be displayed. |
|
148 |
#' `toggle_icon` calls the `toggleClass` (when `one_way = FALSE`) or |
|
149 |
#' `removeClass` and `addClass` methods (when `one_way = TRUE`) to change icons. |
|
150 |
#' `toggle_title` calls the `attr` method to modify the `Title` attribute of the button. |
|
151 |
#' |
|
152 |
#' @param input_id `character(1)` (namespaced) id of the button |
|
153 |
#' @param icons,titles `character(2)` vector specifying values between which to toggle |
|
154 |
#' @param one_way `logical(1)` flag specifying whether to keep toggling; |
|
155 |
#' if TRUE, the target will be changed |
|
156 |
#' from the first element of `icons`/`titles` to the second |
|
157 |
#' |
|
158 |
#' @return Invisible NULL. |
|
159 |
#' |
|
160 |
#' @name toggle_button |
|
161 |
#' |
|
162 |
#' @examples |
|
163 |
#' \dontrun{ |
|
164 |
#' |
|
165 |
#' # continuously switch between right- and down-pointing chevrons |
|
166 |
#' toggle_icon("toggle_element", c("fa-angle-right", "fa-angle-down")) |
|
167 |
#' |
|
168 |
#' # switch right- to down-pointing chevron |
|
169 |
#' toggle_icon("toggle_element", c("fa-angle-right", "fa-angle-down"), one_way = TRUE) |
|
170 |
#' } |
|
171 |
#' |
|
172 |
#' library(shiny) |
|
173 |
#' |
|
174 |
#' ui <- fluidPage( |
|
175 |
#' shinyjs::useShinyjs(), |
|
176 |
#' actionButton("hide_content", label = "hide", icon = icon("xmark")), |
|
177 |
#' actionButton("show_content", label = "show", icon = icon("check")), |
|
178 |
#' actionButton("toggle_content", label = "toggle", icon = icon("angle-down")), |
|
179 |
#' br(), |
|
180 |
#' div( |
|
181 |
#' id = "content", |
|
182 |
#' verbatimTextOutput("printout") |
|
183 |
#' ) |
|
184 |
#' ) |
|
185 |
#' |
|
186 |
#' server <- function(input, output, session) { |
|
187 |
#' |
|
188 |
#' observeEvent(input$hide_content, { |
|
189 |
#' shinyjs::hide("content") |
|
190 |
#' toggle_icon("toggle_content", c("fa-angle-down", "fa-angle-right"), one_way = TRUE) |
|
191 |
#' }, ignoreInit = TRUE) |
|
192 |
#' |
|
193 |
#' observeEvent(input$show_content, { |
|
194 |
#' shinyjs::show("content") |
|
195 |
#' toggle_icon("toggle_content", c("fa-angle-right", "fa-angle-down"), one_way = TRUE) |
|
196 |
#' }, ignoreInit = TRUE) |
|
197 |
#' |
|
198 |
#' observeEvent(input$toggle_content, { |
|
199 |
#' shinyjs::toggle("content") |
|
200 |
#' toggle_icon("toggle_content", c("fa-angle-right", "fa-angle-down")) |
|
201 |
#' }, ignoreInit = TRUE) |
|
202 |
#' |
|
203 |
#' output$printout <- renderPrint({ |
|
204 |
#' head(faithful, 10) |
|
205 |
#' }) |
|
206 |
#' |
|
207 |
#' } |
|
208 |
#' |
|
209 |
#' if (interactive()) { |
|
210 |
#' shinyApp(ui, server) |
|
211 |
#' } |
|
212 |
#' |
|
213 |
#' @rdname toggle_button |
|
214 |
#' @keywords internal |
|
215 |
toggle_icon <- function(input_id, icons, one_way = FALSE) { |
|
216 | ! |
checkmate::assert_string(input_id) |
217 | ! |
checkmate::assert_character(icons, len = 2L) |
218 | ! |
checkmate::assert_flag(one_way) |
219 | ||
220 | ! |
expr <- |
221 | ! |
if (one_way) { |
222 | ! |
sprintf( |
223 | ! |
"$('#%s i').removeClass('%s').addClass('%s');", |
224 | ! |
input_id, icons[1], icons[2] |
225 |
) |
|
226 |
} else { |
|
227 | ! |
sprintf("$('#%s i').toggleClass('%s');", input_id, paste(icons, collapse = " ")) |
228 |
} |
|
229 | ||
230 | ! |
shinyjs::runjs(expr) |
231 | ||
232 | ! |
invisible(NULL) |
233 |
} |
|
234 | ||
235 |
#' @rdname toggle_button |
|
236 |
#' @keywords internal |
|
237 |
toggle_title <- function(input_id, titles, one_way = FALSE) { |
|
238 | ! |
checkmate::assert_string(input_id) |
239 | ! |
checkmate::assert_character(titles, len = 2L) |
240 | ! |
checkmate::assert_flag(one_way) |
241 | ||
242 | ! |
expr <- |
243 | ! |
if (one_way) { |
244 | ! |
sprintf( |
245 | ! |
"$('a#%s').attr('title', '%s');", |
246 | ! |
input_id, titles[2] |
247 |
) |
|
248 |
} else { |
|
249 | ! |
sprintf( |
250 | ! |
paste0( |
251 | ! |
"var button_id = 'a#%1$s';", |
252 | ! |
"var curr = $(button_id).attr('title');", |
253 | ! |
"if (curr == '%2$s') { $(button_id).attr('title', '%3$s');", |
254 | ! |
"} else { $(button_id).attr('title', '%2$s');", |
255 |
"}" |
|
256 |
), |
|
257 | ! |
input_id, titles[1], titles[2] |
258 |
) |
|
259 |
} |
|
260 | ||
261 | ! |
shinyjs::runjs(expr) |
262 | ||
263 | ! |
invisible(NULL) |
264 |
} |
1 |
#' @title `FilterStates` subclass for data frames |
|
2 |
#' @description Handles filter states in a `data.frame` |
|
3 |
#' @keywords internal |
|
4 |
#' |
|
5 |
#' |
|
6 |
#' @examples |
|
7 |
#' \dontrun{ |
|
8 |
#' # working filters in an app |
|
9 |
#' |
|
10 |
#' library(shiny) |
|
11 |
#' |
|
12 |
#' # create data frame to filter |
|
13 |
#' data_df <- data.frame( |
|
14 |
#' NUM1 = 1:100, |
|
15 |
#' NUM2 = round(runif(100, min = 20, max = 23)), |
|
16 |
#' CHAR1 = sample(LETTERS[1:6], size = 100, replace = TRUE), |
|
17 |
#' CHAR2 = sample(c("M", "F"), size = 100, replace = TRUE), |
|
18 |
#' DATE = seq(as.Date("2020-01-01"), by = 1, length.out = 100), |
|
19 |
#' DATETIME = as.POSIXct(seq(as.Date("2020-01-01"), by = 1, length.out = 100)) |
|
20 |
#' ) |
|
21 |
#' data_na <- data.frame( |
|
22 |
#' NUM1 = NA, |
|
23 |
#' NUM2 = NA, |
|
24 |
#' CHAR1 = NA, |
|
25 |
#' CHAR2 = NA, |
|
26 |
#' DATE = NA, |
|
27 |
#' DATETIME = NA |
|
28 |
#' ) |
|
29 |
#' data_df <- rbind(data_df, data_na) |
|
30 |
#' |
|
31 |
#' |
|
32 |
#' # initiate FilterStates object |
|
33 |
#' filter_states_df <- init_filter_states( |
|
34 |
#' data = data_df, |
|
35 |
#' dataname = "dataset", |
|
36 |
#' datalabel = ("label"), |
|
37 |
#' varlabels = c("long", "short", "long", "short", "long", "long") |
|
38 |
#' ) |
|
39 |
#' |
|
40 |
#' ui <- fluidPage( |
|
41 |
#' column(4, div( |
|
42 |
#' h4("Active filters"), |
|
43 |
#' filter_states_df$ui("fsdf") |
|
44 |
#' )), |
|
45 |
#' column(4, div( |
|
46 |
#' h4("Manual filter control"), |
|
47 |
#' filter_states_df$ui_add_filter_state("add_filters", data_df), br(), |
|
48 |
#' h4("Condition (i.e. call)"), # display the subsetting call generated by this FilterStates |
|
49 |
#' textOutput("call_df"), br(), |
|
50 |
#' h4("Formatted state"), # display human readable filter state |
|
51 |
#' textOutput("formatted_df"), br() |
|
52 |
#' )), |
|
53 |
#' column(4, div( |
|
54 |
#' h4("Programmatic filter control"), |
|
55 |
#' actionButton("button1_df", "set NUM1 < 30", width = "100%"), br(), |
|
56 |
#' actionButton("button2_df", "set NUM2 %in% c(20, 21)", width = "100%"), br(), |
|
57 |
#' actionButton("button3_df", "set CHAR1 %in% c(\"B\", \"C\", \"D\")", width = "100%"), br(), |
|
58 |
#' actionButton("button4_df", "set CHAR2 == \"F\"", width = "100%"), br(), |
|
59 |
#' actionButton("button5_df", "set DATE <= 2020-02-02", width = "100%"), br(), |
|
60 |
#' actionButton("button6_df", "set DATETIME <= 2020-02-02", width = "100%"), br(), |
|
61 |
#' hr(), |
|
62 |
#' actionButton("button7_df", "remove NUM1", width = "100%"), br(), |
|
63 |
#' actionButton("button8_df", "remove NUM2", width = "100%"), br(), |
|
64 |
#' actionButton("button9_df", "remove CHAR1", width = "100%"), br(), |
|
65 |
#' actionButton("button10_df", "remove CHAR2", width = "100%"), br(), |
|
66 |
#' actionButton("button11_df", "remove DATE", width = "100%"), br(), |
|
67 |
#' actionButton("button12_df", "remove DATETIME", width = "100%"), br(), |
|
68 |
#' hr(), |
|
69 |
#' actionButton("button0_df", "clear all filters", width = "100%"), br() |
|
70 |
#' )) |
|
71 |
#' ) |
|
72 |
#' |
|
73 |
#' server <- function(input, output, session) { |
|
74 |
#' filter_states_df$srv_add_filter_state("add_filters", data_df) |
|
75 |
#' filter_states_df$server("fsdf") |
|
76 |
#' |
|
77 |
#' output$call_df <- renderPrint(filter_states_df$get_call()) |
|
78 |
#' output$formatted_df <- renderText(filter_states_df$format()) |
|
79 |
#' |
|
80 |
#' observeEvent(input$button1_df, { |
|
81 |
#' filter_state <- list(NUM1 = list(selected = c(0, 30))) |
|
82 |
#' filter_states_df$set_filter_state(data = data_df, state = filter_state) |
|
83 |
#' }) |
|
84 |
#' observeEvent(input$button2_df, { |
|
85 |
#' filter_state <- list(NUM2 = list(selected = c(20, 21))) |
|
86 |
#' filter_states_df$set_filter_state(data = data_df, state = filter_state) |
|
87 |
#' }) |
|
88 |
#' observeEvent(input$button3_df, { |
|
89 |
#' filter_state <- list(CHAR1 = list(selected = c("B", "C", "D"))) |
|
90 |
#' filter_states_df$set_filter_state(data = data_df, state = filter_state) |
|
91 |
#' }) |
|
92 |
#' observeEvent(input$button4_df, { |
|
93 |
#' filter_state <- list(CHAR2 = list(selected = "F")) |
|
94 |
#' filter_states_df$set_filter_state(data = data_df, state = filter_state) |
|
95 |
#' }) |
|
96 |
#' observeEvent(input$button5_df, { |
|
97 |
#' filter_state <- list(DATE = list(selected = c("2020-01-01", "2020-02-02"))) |
|
98 |
#' filter_states_df$set_filter_state(data = data_df, state = filter_state) |
|
99 |
#' }) |
|
100 |
#' observeEvent(input$button6_df, { |
|
101 |
#' filter_state <- list(DATETIME = list(selected = as.POSIXct(c("2020-01-01", "2020-02-02")))) |
|
102 |
#' filter_states_df$set_filter_state(data = data_df, state = filter_state) |
|
103 |
#' }) |
|
104 |
#' observeEvent(input$button7_df, filter_states_df$state_list_remove(1, state_id = "NUM1")) |
|
105 |
#' observeEvent(input$button8_df, filter_states_df$state_list_remove(1, state_id = "NUM2")) |
|
106 |
#' observeEvent(input$button9_df, filter_states_df$state_list_remove(1, state_id = "CHAR1")) |
|
107 |
#' observeEvent(input$button10_df, filter_states_df$state_list_remove(1, state_id = "CHAR2")) |
|
108 |
#' observeEvent(input$button11_df, filter_states_df$state_list_remove(1, state_id = "DATE")) |
|
109 |
#' observeEvent(input$button12_df, filter_states_df$state_list_remove(1, state_id = "DATETIME")) |
|
110 |
#' observeEvent(input$button0_df, filter_states_df$state_list_empty()) |
|
111 |
#' } |
|
112 |
#' |
|
113 |
#' if (interactive()) { |
|
114 |
#' shinyApp(ui, server) |
|
115 |
#' } |
|
116 |
#' } |
|
117 |
#' |
|
118 |
DFFilterStates <- R6::R6Class( # nolint |
|
119 |
classname = "DFFilterStates", |
|
120 |
inherit = FilterStates, |
|
121 | ||
122 |
# public methods ---- |
|
123 |
public = list( |
|
124 |
#' @description Initializes `DFFilterStates` object. |
|
125 |
#' |
|
126 |
#' Initializes `DFFilterStates` object by setting `dataname` |
|
127 |
#' and initializing `state_list` (`shiny::reactiveVal`). |
|
128 |
#' This class contains a single `state_list` with no specified name, |
|
129 |
#' which means that when calling the subset function associated with this class |
|
130 |
#' (`dplyr::filter`), a list of conditions is passed to unnamed arguments (`...`). |
|
131 |
#' |
|
132 |
#' @param dataname (`character(1)`)\cr |
|
133 |
#' name of the data used in the \emph{subset expression} |
|
134 |
#' specified to the function argument attached to this `FilterStates` |
|
135 |
#' @param datalabel (`character(0)` or `character(1)`)\cr |
|
136 |
#' text label value |
|
137 |
#' @param varlabels (`character`)\cr |
|
138 |
#' labels of the variables used in this object |
|
139 |
#' @param keys (`character`)\cr |
|
140 |
#' key columns names |
|
141 |
#' |
|
142 |
initialize = function(dataname, datalabel, varlabels, keys) { |
|
143 | 140x |
super$initialize(dataname, datalabel) |
144 | 140x |
private$varlabels <- varlabels |
145 | 140x |
private$keys <- keys |
146 | 140x |
private$state_list <- list( |
147 | 140x |
reactiveVal() |
148 |
) |
|
149 |
}, |
|
150 | ||
151 |
#' @description |
|
152 |
#' Returns a formatted string representing this `FilterStates` object. |
|
153 |
#' |
|
154 |
#' @param indent (`numeric(1)`) the number of spaces prepended to each line of the output |
|
155 |
#' |
|
156 |
#' @return `character(1)` the formatted string |
|
157 |
#' |
|
158 |
format = function(indent = 0) { |
|
159 | 46x |
checkmate::assert_number(indent, finite = TRUE, lower = 0) |
160 | ||
161 | 45x |
formatted_states <- vapply( |
162 | 45x |
self$state_list_get(1L), function(state) state$format(indent = indent), |
163 | 45x |
USE.NAMES = FALSE, FUN.VALUE = character(1) |
164 |
) |
|
165 | 45x |
paste(formatted_states, collapse = "\n") |
166 |
}, |
|
167 | ||
168 |
#' @description |
|
169 |
#' Gets the name of the function used to filter the data in this `FilterStates`. |
|
170 |
#' |
|
171 |
#' Get name of function used to create the \emph{subset expression}. |
|
172 |
#' For `DFFilterStates` this is `dplyr::filter`. |
|
173 |
#' |
|
174 |
#' @return `character(1)` |
|
175 |
get_fun = function() { |
|
176 | 26x |
"dplyr::filter" |
177 |
}, |
|
178 | ||
179 |
#' @description |
|
180 |
#' Shiny server module. |
|
181 |
#' |
|
182 |
#' @param id (`character(1)`)\cr |
|
183 |
#' shiny module instance id |
|
184 |
#' |
|
185 |
#' @return `moduleServer` function which returns `NULL` |
|
186 |
#' |
|
187 |
server = function(id) { |
|
188 | 8x |
moduleServer( |
189 | 8x |
id = id, |
190 | 8x |
function(input, output, session) { |
191 | 8x |
previous_state <- reactiveVal(isolate(self$state_list_get(1L))) |
192 | 8x |
added_state_name <- reactiveVal(character(0)) |
193 | 8x |
removed_state_name <- reactiveVal(character(0)) |
194 | ||
195 | 8x |
observeEvent(self$state_list_get(1L), { |
196 | ! |
added_state_name(setdiff(names(self$state_list_get(1L)), names(previous_state()))) |
197 | ! |
removed_state_name(setdiff(names(previous_state()), names(self$state_list_get(1L)))) |
198 | ! |
previous_state(self$state_list_get(1L)) |
199 |
}) |
|
200 | ||
201 | 8x |
observeEvent(added_state_name(), ignoreNULL = TRUE, { |
202 | ! |
fstates <- self$state_list_get(1L) |
203 | ! |
html_ids <- private$map_vars_to_html_ids(names(fstates)) |
204 | ! |
for (fname in added_state_name()) { |
205 | ! |
private$insert_filter_state_ui( |
206 | ! |
id = html_ids[fname], |
207 | ! |
filter_state = fstates[[fname]], |
208 | ! |
state_list_index = 1L, |
209 | ! |
state_id = fname |
210 |
) |
|
211 |
} |
|
212 | ! |
added_state_name(character(0)) |
213 |
}) |
|
214 | ||
215 | 8x |
observeEvent(removed_state_name(), { |
216 | ! |
req(removed_state_name()) |
217 | ! |
for (fname in removed_state_name()) { |
218 | ! |
private$remove_filter_state_ui(1L, fname, .input = input) |
219 |
} |
|
220 | ! |
removed_state_name(character(0)) |
221 |
}) |
|
222 | 8x |
NULL |
223 |
} |
|
224 |
) |
|
225 |
}, |
|
226 | ||
227 |
#' @description |
|
228 |
#' Gets the reactive values from the active `FilterState` objects. |
|
229 |
#' |
|
230 |
#' Get active filter state from the `FilterState` objects kept in `state_list`. |
|
231 |
#' The output list is a compatible input to `self$set_filter_state`. |
|
232 |
#' |
|
233 |
#' @return `list` with named elements corresponding to `FilterState` in the `state_list`. |
|
234 |
#' |
|
235 |
get_filter_state = function() { |
|
236 | 38x |
lapply(self$state_list_get(1L), function(x) x$get_state()) |
237 |
}, |
|
238 | ||
239 |
#' @description |
|
240 |
#' Set filter state. |
|
241 |
#' |
|
242 |
#' @param data (`data.frame`)\cr |
|
243 |
#' data object for which to define a subset |
|
244 |
#' @param state (`named list`)\cr |
|
245 |
#' should contain values of initial selections in the `FilterState`; |
|
246 |
#' `list` names must correspond to column names in `data` |
|
247 |
#' @param vars_include (`character(n)`)\cr |
|
248 |
#' optional, vector of column names to be included |
|
249 |
#' @param ... ignored |
|
250 |
#' |
|
251 |
#' @examples |
|
252 |
#' dffs <- teal.slice:::DFFilterStates$new( |
|
253 |
#' dataname = "iris", |
|
254 |
#' datalabel = character(0), |
|
255 |
#' varlabels = character(0), |
|
256 |
#' keys = character(0) |
|
257 |
#' ) |
|
258 |
#' fs <- list( |
|
259 |
#' Sepal.Length = list(selected = c(5.1, 6.4), keep_na = TRUE, keep_inf = TRUE), |
|
260 |
#' Species = list(selected = c("setosa", "versicolor"), keep_na = FALSE) |
|
261 |
#' ) |
|
262 |
#' shiny::isolate(dffs$set_filter_state(state = fs, data = iris)) |
|
263 |
#' shiny::isolate(dffs$get_filter_state()) |
|
264 |
#' |
|
265 |
#' @return `NULL` |
|
266 |
#' |
|
267 |
set_filter_state = function(data, state, vars_include = get_supported_filter_varnames(data = data), ...) { |
|
268 | 50x |
checkmate::assert_data_frame(data) |
269 | 50x |
checkmate::assert( |
270 | 50x |
checkmate::check_subset(names(state), names(data)), |
271 | 50x |
checkmate::check_class(state, "default_filter"), |
272 | 50x |
combine = "or" |
273 |
) |
|
274 | 47x |
logger::log_trace( |
275 | 47x |
"{ class(self)[1] }$set_filter_state initializing, dataname: { private$dataname }" |
276 |
) |
|
277 | ||
278 | 47x |
filter_states <- self$state_list_get(1L) |
279 | 47x |
state_names <- names(state) |
280 | 47x |
excluded_vars <- setdiff(state_names, vars_include) |
281 | 47x |
if (length(excluded_vars) > 0) { |
282 | 1x |
warning( |
283 | 1x |
paste( |
284 | 1x |
"These columns filters were excluded:", |
285 | 1x |
paste(excluded_vars, collapse = ", "), |
286 | 1x |
"from dataset", |
287 | 1x |
private$dataname |
288 |
) |
|
289 |
) |
|
290 | 1x |
logger::log_warn( |
291 | 1x |
paste( |
292 | 1x |
"Columns filters { paste(excluded_vars, collapse = ', ') } were excluded", |
293 | 1x |
"from { private$dataname }" |
294 |
) |
|
295 |
) |
|
296 |
} |
|
297 | ||
298 | 47x |
filters_to_apply <- state_names[state_names %in% vars_include] |
299 | ||
300 | 47x |
for (varname in filters_to_apply) { |
301 | 80x |
value <- resolve_state(state[[varname]]) |
302 | 80x |
if (varname %in% names(filter_states)) { |
303 | 8x |
fstate <- filter_states[[varname]] |
304 | 8x |
fstate$set_state(value) |
305 |
} else { |
|
306 | 72x |
fstate <- init_filter_state( |
307 | 72x |
data[[varname]], |
308 | 72x |
varname = varname, |
309 | 72x |
varlabel = private$get_varlabels(varname), |
310 | 72x |
dataname = private$dataname |
311 |
) |
|
312 | 72x |
fstate$set_state(value) |
313 | 72x |
self$state_list_push(x = fstate, state_list_index = 1L, state_id = varname) |
314 |
} |
|
315 |
} |
|
316 | 47x |
logger::log_trace( |
317 | 47x |
"{ class(self)[1] }$set_filter_state initialized, dataname: { private$dataname }" |
318 |
) |
|
319 | 47x |
NULL |
320 |
}, |
|
321 | ||
322 |
#' @description Remove a `FilterState` from the `state_list`. |
|
323 |
#' |
|
324 |
#' @param state_id (`character(1)`)\cr name of `state_list` element |
|
325 |
#' |
|
326 |
#' @return `NULL` |
|
327 |
#' |
|
328 |
remove_filter_state = function(state_id) { |
|
329 | 9x |
logger::log_trace( |
330 | 9x |
sprintf( |
331 | 9x |
"%s$remove_filter_state for variable %s called, dataname: %s", |
332 | 9x |
class(self)[1], |
333 | 9x |
state_id, |
334 | 9x |
private$dataname |
335 |
) |
|
336 |
) |
|
337 | ||
338 | 9x |
if (!state_id %in% names(self$state_list_get(1L))) { |
339 | 1x |
warning(paste( |
340 | 1x |
"Variable:", state_id, |
341 | 1x |
"is not present in the actual active filters of dataset: { private$dataname }", |
342 | 1x |
"therefore no changes are applied." |
343 |
)) |
|
344 | 1x |
logger::log_warn( |
345 | 1x |
paste( |
346 | 1x |
"Variable:", state_id, "is not present in the actual active filters of dataset:", |
347 | 1x |
"{ private$dataname } therefore no changes are applied." |
348 |
) |
|
349 |
) |
|
350 |
} else { |
|
351 | 8x |
self$state_list_remove(state_list_index = 1L, state_id = state_id) |
352 | 8x |
logger::log_trace( |
353 | 8x |
sprintf( |
354 | 8x |
"%s$remove_filter_state for variable %s done, dataname: %s", |
355 | 8x |
class(self)[1], |
356 | 8x |
state_id, |
357 | 8x |
private$dataname |
358 |
) |
|
359 |
) |
|
360 |
} |
|
361 |
}, |
|
362 | ||
363 |
# shiny modules ---- |
|
364 | ||
365 |
#' @description |
|
366 |
#' Shiny UI module to add filter variable. |
|
367 |
#' |
|
368 |
#' @param id (`character(1)`)\cr |
|
369 |
#' shiny element (module instance) id |
|
370 |
#' @param data (`data.frame`)\cr |
|
371 |
#' data object for which to define a subset |
|
372 |
#' |
|
373 |
#' @return `shiny.tag` |
|
374 |
#' |
|
375 |
ui_add_filter_state = function(id, data) { |
|
376 | 2x |
checkmate::assert_string(id) |
377 | 2x |
checkmate::assert_data_frame(data) |
378 | ||
379 | 2x |
ns <- NS(id) |
380 | ||
381 | 2x |
if (ncol(data) == 0) { |
382 | 1x |
div("no sample variables available") |
383 | 1x |
} else if (nrow(data) == 0) { |
384 | 1x |
div("no samples available") |
385 |
} else { |
|
386 | ! |
div( |
387 | ! |
teal.widgets::optionalSelectInput( |
388 | ! |
ns("var_to_add"), |
389 | ! |
choices = NULL, |
390 | ! |
options = shinyWidgets::pickerOptions( |
391 | ! |
liveSearch = TRUE, |
392 | ! |
noneSelectedText = "Select variable to filter" |
393 |
) |
|
394 |
) |
|
395 |
) |
|
396 |
} |
|
397 |
}, |
|
398 | ||
399 |
#' @description |
|
400 |
#' Shiny server module to add filter variable. |
|
401 |
#' |
|
402 |
#' This module controls available choices to select as a filter variable. |
|
403 |
#' Once selected, a variable is removed from available choices. |
|
404 |
#' Removing a filter variable adds it back to available choices. |
|
405 |
#' |
|
406 |
#' @param id (`character(1)`)\cr |
|
407 |
#' shiny module instance id |
|
408 |
#' @param data (`data.frame`)\cr |
|
409 |
#' data object for which to define a subset |
|
410 |
#' @param vars_include (`character(n)`)\cr |
|
411 |
#' optional, vector of column names to be included |
|
412 |
#' @param ... ignored |
|
413 |
#' |
|
414 |
#' @return `moduleServer` function which returns `NULL` |
|
415 |
#' |
|
416 |
srv_add_filter_state = function(id, data, vars_include = get_supported_filter_varnames(data = data), ...) { |
|
417 | 11x |
stopifnot(is.data.frame(data)) |
418 | 11x |
check_ellipsis(..., stop = FALSE) |
419 | 11x |
moduleServer( |
420 | 11x |
id = id, |
421 | 11x |
function(input, output, session) { |
422 | 11x |
logger::log_trace( |
423 | 11x |
"DFFilterStates$srv_add_filter_state initializing, dataname: { private$dataname }" |
424 |
) |
|
425 | 11x |
shiny::setBookmarkExclude(c("var_to_add")) |
426 | 11x |
active_filter_vars <- reactive({ |
427 | 6x |
vapply( |
428 | 6x |
X = self$state_list_get(state_list_index = 1L), |
429 | 6x |
FUN.VALUE = character(1), |
430 | 6x |
FUN = function(x) x$get_varname() |
431 |
) |
|
432 |
}) |
|
433 | ||
434 |
# available choices to display |
|
435 | 11x |
avail_column_choices <- reactive({ |
436 | 6x |
choices <- setdiff(vars_include, active_filter_vars()) |
437 | ||
438 | 6x |
data_choices_labeled( |
439 | 6x |
data = data, |
440 | 6x |
choices = choices, |
441 | 6x |
varlabels = private$get_varlabels(choices), |
442 | 6x |
keys = private$keys |
443 |
) |
|
444 |
}) |
|
445 | 11x |
observeEvent( |
446 | 11x |
avail_column_choices(), |
447 | 11x |
ignoreNULL = TRUE, |
448 | 11x |
handlerExpr = { |
449 | 6x |
logger::log_trace(paste( |
450 | 6x |
"DFFilterStates$srv_add_filter_state@1 updating available column choices,", |
451 | 6x |
"dataname: { private$dataname }" |
452 |
)) |
|
453 | 6x |
if (is.null(avail_column_choices())) { |
454 | ! |
shinyjs::hide("var_to_add") |
455 |
} else { |
|
456 | 6x |
shinyjs::show("var_to_add") |
457 |
} |
|
458 | 6x |
teal.widgets::updateOptionalSelectInput( |
459 | 6x |
session, |
460 | 6x |
"var_to_add", |
461 | 6x |
choices = avail_column_choices() |
462 |
) |
|
463 | 6x |
logger::log_trace(paste( |
464 | 6x |
"DFFilterStates$srv_add_filter_state@1 updated available column choices,", |
465 | 6x |
"dataname: { private$dataname }" |
466 |
)) |
|
467 |
} |
|
468 |
) |
|
469 | ||
470 | 11x |
observeEvent( |
471 | 11x |
eventExpr = input$var_to_add, |
472 | 11x |
handlerExpr = { |
473 | 3x |
logger::log_trace( |
474 | 3x |
sprintf( |
475 | 3x |
"DFFilterStates$srv_add_filter_state@2 adding FilterState of variable %s, dataname: %s", |
476 | 3x |
input$var_to_add, |
477 | 3x |
private$dataname |
478 |
) |
|
479 |
) |
|
480 | 3x |
self$state_list_push( |
481 | 3x |
x = init_filter_state( |
482 | 3x |
data[[input$var_to_add]], |
483 | 3x |
varname = input$var_to_add, |
484 | 3x |
varlabel = private$get_varlabels(input$var_to_add), |
485 | 3x |
dataname = private$dataname |
486 |
), |
|
487 | 3x |
state_list_index = 1L, |
488 | 3x |
state_id = input$var_to_add |
489 |
) |
|
490 | 3x |
logger::log_trace( |
491 | 3x |
sprintf( |
492 | 3x |
"DFFilterStates$srv_add_filter_state@2 added FilterState of variable %s, dataname: %s", |
493 | 3x |
input$var_to_add, |
494 | 3x |
private$dataname |
495 |
) |
|
496 |
) |
|
497 |
} |
|
498 |
) |
|
499 | ||
500 | 11x |
logger::log_trace( |
501 | 11x |
"DFFilterStates$srv_add_filter_state initialized, dataname: { private$dataname }" |
502 |
) |
|
503 | 11x |
NULL |
504 |
} |
|
505 |
) |
|
506 |
} |
|
507 |
), |
|
508 | ||
509 |
# private members ---- |
|
510 |
private = list( |
|
511 |
varlabels = character(0), |
|
512 |
keys = character(0), |
|
513 |
# @description |
|
514 |
# Get label of specific variable. If variable label is missing, variable name is returned. |
|
515 |
# |
|
516 |
# @para variable (`character`)\cr |
|
517 |
# name of variable for which label should be returned |
|
518 |
# |
|
519 |
# @return `character` |
|
520 |
get_varlabels = function(variables = character(0)) { |
|
521 | 81x |
checkmate::assert_character(variables) |
522 | 81x |
if (identical(variables, character(0))) { |
523 | ! |
private$varlabels |
524 |
} else { |
|
525 | 81x |
varlabels <- private$varlabels[variables] |
526 | 81x |
missing_labels <- is.na(varlabels) | varlabels == "" |
527 | 81x |
varlabels[missing_labels] <- variables[missing_labels] |
528 | 81x |
varlabels |
529 |
} |
|
530 |
} |
|
531 |
) |
|
532 |
) |
1 |
#' @title `FilterStates` subclass for matrices |
|
2 |
#' @description Handles filter states in a `matrix` |
|
3 |
#' @keywords internal |
|
4 |
#' |
|
5 |
#' |
|
6 |
MatrixFilterStates <- R6::R6Class( # nolint |
|
7 |
classname = "MatrixFilterStates", |
|
8 |
inherit = FilterStates, |
|
9 | ||
10 |
# public methods ---- |
|
11 |
public = list( |
|
12 |
#' @description Initialize `MatrixFilterStates` object |
|
13 |
#' |
|
14 |
#' Initialize `MatrixFilterStates` object |
|
15 |
#' |
|
16 |
#' @param dataname (`character(1)`)\cr |
|
17 |
#' name of the data used in the expression |
|
18 |
#' specified to the function argument attached to this `FilterStates`. |
|
19 |
#' |
|
20 |
#' @param datalabel (`character(0)` or `character(1)`)\cr |
|
21 |
#' text label value. |
|
22 |
initialize = function(dataname, datalabel) { |
|
23 | 45x |
super$initialize(dataname, datalabel) |
24 | 45x |
private$state_list <- list( |
25 | 45x |
subset = reactiveVal() |
26 |
) |
|
27 |
}, |
|
28 | ||
29 |
#' @description |
|
30 |
#' Returns the formatted string representing this `MatrixFilterStates` object. |
|
31 |
#' |
|
32 |
#' @param indent (`numeric(1)`) the number of spaces before each line of the representation |
|
33 |
#' @return `character(1)` the formatted string |
|
34 |
format = function(indent = 0) { |
|
35 | 4x |
checkmate::assert_number(indent, finite = TRUE, lower = 0) |
36 | ||
37 | 4x |
formatted_states <- c() |
38 | 4x |
for (state in self$state_list_get(state_list_index = "subset")) { |
39 | ! |
formatted_states <- c(formatted_states, state$format(indent = indent + 2)) |
40 |
} |
|
41 | 4x |
paste(formatted_states, collapse = "\n") |
42 |
}, |
|
43 | ||
44 |
#' @description |
|
45 |
#' Server module |
|
46 |
#' @param id (`character(1)`)\cr |
|
47 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
48 |
#' @return `moduleServer` function which returns `NULL` |
|
49 |
server = function(id) { |
|
50 | 1x |
moduleServer( |
51 | 1x |
id = id, |
52 | 1x |
function(input, output, session) { |
53 | 1x |
previous_state <- reactiveVal(isolate(self$state_list_get("subset"))) |
54 | 1x |
added_state_name <- reactiveVal(character(0)) |
55 | 1x |
removed_state_name <- reactiveVal(character(0)) |
56 | ||
57 | 1x |
observeEvent(self$state_list_get("subset"), { |
58 | 1x |
added_state_name( |
59 | 1x |
setdiff(names(self$state_list_get("subset")), names(previous_state())) |
60 |
) |
|
61 | 1x |
removed_state_name( |
62 | 1x |
setdiff(names(previous_state()), names(self$state_list_get("subset"))) |
63 |
) |
|
64 | 1x |
previous_state(self$state_list_get("subset")) |
65 |
}) |
|
66 | ||
67 | 1x |
observeEvent(added_state_name(), ignoreNULL = TRUE, { |
68 | 1x |
fstates <- self$state_list_get("subset") |
69 | 1x |
html_ids <- private$map_vars_to_html_ids(keys = names(fstates)) |
70 | 1x |
for (fname in added_state_name()) { |
71 | ! |
private$insert_filter_state_ui( |
72 | ! |
id = html_ids[fname], |
73 | ! |
filter_state = fstates[[fname]], |
74 | ! |
state_list_index = "subset", |
75 | ! |
state_id = fname |
76 |
) |
|
77 |
} |
|
78 | 1x |
added_state_name(character(0)) |
79 |
}) |
|
80 | ||
81 | 1x |
observeEvent(removed_state_name(), { |
82 | 1x |
req(removed_state_name()) |
83 | ||
84 | ! |
for (fname in removed_state_name()) { |
85 | ! |
private$remove_filter_state_ui("subset", fname, .input = input) |
86 |
} |
|
87 | ! |
removed_state_name(character(0)) |
88 |
}) |
|
89 | 1x |
NULL |
90 |
} |
|
91 |
) |
|
92 |
}, |
|
93 | ||
94 |
#' @description |
|
95 |
#' Returns active `FilterState` objects. |
|
96 |
#' |
|
97 |
#' Gets all active filters from this dataset in form of the nested list. |
|
98 |
#' The output list can be used as input to `self$set_filter_state`. |
|
99 |
#' |
|
100 |
#' @return `list` containing `list` with selected values for each `FilterState`. |
|
101 |
get_filter_state = function() { |
|
102 | 9x |
lapply(self$state_list_get(state_list_index = "subset"), function(x) x$get_state()) |
103 |
}, |
|
104 | ||
105 |
#' @description |
|
106 |
#' Sets a filter state |
|
107 |
#' |
|
108 |
#' @param data (`matrix`)\cr |
|
109 |
#' data which are supposed to be filtered. |
|
110 |
#' @param state (`named list`)\cr |
|
111 |
#' should contain values which are initial selection in the `FilterState`. |
|
112 |
#' Names of the `list` element should correspond to the name of the |
|
113 |
#' column in `data`. |
|
114 |
#' @param ... ignored. |
|
115 |
#' @return `NULL` |
|
116 |
set_filter_state = function(data, state, ...) { |
|
117 | 4x |
checkmate::assert_class(data, "matrix") |
118 | 4x |
checkmate::assert( |
119 | 4x |
checkmate::assert( |
120 | 4x |
!checkmate::test_null(names(state)), |
121 | 4x |
checkmate::check_subset(names(state), colnames(data)), |
122 | 4x |
combine = "and" |
123 |
), |
|
124 | 4x |
checkmate::check_class(state, "default_filter"), |
125 | 4x |
combine = "or" |
126 |
) |
|
127 | 3x |
logger::log_trace(paste( |
128 | 3x |
"MatrixFilterState$set_filter_state initializing,", |
129 | 3x |
"dataname: { private$dataname }" |
130 |
)) |
|
131 | 3x |
filter_states <- self$state_list_get("subset") |
132 | 3x |
for (varname in names(state)) { |
133 | 3x |
value <- resolve_state(state[[varname]]) |
134 | 3x |
if (varname %in% names(filter_states)) { |
135 | ! |
fstate <- filter_states[[varname]] |
136 | ! |
fstate$set_state(value) |
137 |
} else { |
|
138 | 3x |
fstate <- init_filter_state( |
139 | 3x |
data[, varname], |
140 | 3x |
varname = varname, |
141 | 3x |
varlabel = varname, |
142 | 3x |
dataname = private$dataname, |
143 | 3x |
extract_type = "matrix" |
144 |
) |
|
145 | 3x |
fstate$set_state(value) |
146 | 3x |
self$state_list_push( |
147 | 3x |
x = fstate, |
148 | 3x |
state_list_index = "subset", |
149 | 3x |
state_id = varname |
150 |
) |
|
151 |
} |
|
152 |
} |
|
153 | 3x |
logger::log_trace(paste( |
154 | 3x |
"MatrixFilterState$set_filter_state initialized,", |
155 | 3x |
"dataname: { private$dataname }" |
156 |
)) |
|
157 | 3x |
NULL |
158 |
}, |
|
159 | ||
160 |
#' @description Remove a variable from the `state_list` and its corresponding UI element. |
|
161 |
#' |
|
162 |
#' @param state_id (`character(1)`)\cr name of `state_list` element. |
|
163 |
#' |
|
164 |
#' @return `NULL` |
|
165 |
remove_filter_state = function(state_id) { |
|
166 | 2x |
logger::log_trace( |
167 | 2x |
sprintf( |
168 | 2x |
"%s$remove_filter_state of variable %s, dataname: %s", |
169 | 2x |
class(self)[1], |
170 | 2x |
state_id, |
171 | 2x |
private$dataname |
172 |
) |
|
173 |
) |
|
174 | ||
175 | 2x |
if (!state_id %in% names(self$state_list_get("subset"))) { |
176 | 1x |
warning(paste( |
177 | 1x |
"Variable:", state_id, "is not present in the actual active filters of dataset:", |
178 | 1x |
"{ private$dataname } therefore no changes are applied." |
179 |
)) |
|
180 | 1x |
logger::log_warn( |
181 | 1x |
paste( |
182 | 1x |
"Variable:", state_id, "is not present in the actual active filters of dataset:", |
183 | 1x |
"{ private$dataname } therefore no changes are applied." |
184 |
) |
|
185 |
) |
|
186 |
} else { |
|
187 | 1x |
self$state_list_remove(state_list_index = "subset", state_id = state_id) |
188 | 1x |
logger::log_trace( |
189 | 1x |
sprintf( |
190 | 1x |
"%s$remove_filter_state of variable %s done, dataname: %s", |
191 | 1x |
class(self)[1], |
192 | 1x |
state_id, |
193 | 1x |
private$dataname |
194 |
) |
|
195 |
) |
|
196 |
} |
|
197 |
}, |
|
198 | ||
199 |
# shiny modules ---- |
|
200 | ||
201 |
#' @description |
|
202 |
#' Shiny UI module to add filter variable. |
|
203 |
#' |
|
204 |
#' @param id (`character(1)`)\cr |
|
205 |
#' id of shiny module |
|
206 |
#' @param data (`matrix`)\cr |
|
207 |
#' data object for which to define a subset |
|
208 |
#' |
|
209 |
#' @return `shiny.tag` |
|
210 |
#' |
|
211 |
ui_add_filter_state = function(id, data) { |
|
212 | 2x |
checkmate::assert_string(id) |
213 | 2x |
stopifnot(is.matrix(data)) |
214 | ||
215 | 2x |
ns <- NS(id) |
216 | ||
217 | 2x |
if (ncol(data) == 0) { |
218 | 1x |
div("no sample variables available") |
219 | 1x |
} else if (nrow(data) == 0) { |
220 | 1x |
div("no samples available") |
221 |
} else { |
|
222 | ! |
teal.widgets::optionalSelectInput( |
223 | ! |
ns("var_to_add"), |
224 | ! |
choices = NULL, |
225 | ! |
options = shinyWidgets::pickerOptions( |
226 | ! |
liveSearch = TRUE, |
227 | ! |
noneSelectedText = "Select variable to filter" |
228 |
) |
|
229 |
) |
|
230 |
} |
|
231 |
}, |
|
232 | ||
233 |
#' @description |
|
234 |
#' Shiny server module to add filter variable |
|
235 |
#' |
|
236 |
#' Module controls available choices to select as a filter variable. |
|
237 |
#' Selected filter variable is being removed from available choices. |
|
238 |
#' Removed filter variable gets back to available choices. |
|
239 |
#' |
|
240 |
#' @param id (`character(1)`)\cr |
|
241 |
#' shiny module instance id |
|
242 |
#' @param data (`matrix`)\cr |
|
243 |
#' data object for which to define a subset |
|
244 |
#' @param ... ignored |
|
245 |
#' |
|
246 |
#' @return `moduleServer` function which returns `NULL` |
|
247 |
#' |
|
248 |
srv_add_filter_state = function(id, data, ...) { |
|
249 | ! |
stopifnot(is.matrix(data)) |
250 | ! |
check_ellipsis(..., stop = FALSE) |
251 | ! |
moduleServer( |
252 | ! |
id = id, |
253 | ! |
function(input, output, session) { |
254 | ! |
logger::log_trace( |
255 | ! |
"MatrixFilterStates$srv_add_filter_state initializing, dataname: { private$dataname }" |
256 |
) |
|
257 | ! |
shiny::setBookmarkExclude("var_to_add") |
258 | ! |
active_filter_vars <- reactive({ |
259 | ! |
vapply( |
260 | ! |
X = self$state_list_get(state_list_index = "subset"), |
261 | ! |
FUN.VALUE = character(1), |
262 | ! |
FUN = function(x) x$get_varname() |
263 |
) |
|
264 |
}) |
|
265 | ||
266 |
# available choices to display |
|
267 | ! |
avail_column_choices <- reactive({ |
268 | ! |
choices <- setdiff( |
269 | ! |
get_supported_filter_varnames(data = data), |
270 | ! |
active_filter_vars() |
271 |
) |
|
272 | ! |
data_choices_labeled( |
273 | ! |
data = data, |
274 | ! |
choices = choices, |
275 | ! |
varlabels = character(0), |
276 | ! |
keys = NULL |
277 |
) |
|
278 |
}) |
|
279 | ! |
observeEvent( |
280 | ! |
avail_column_choices(), |
281 | ! |
ignoreNULL = TRUE, |
282 | ! |
handlerExpr = { |
283 | ! |
logger::log_trace(paste( |
284 | ! |
"MatrixFilterStates$srv_add_filter_state@1 updating column choices,", |
285 | ! |
"dataname: { private$dataname }" |
286 |
)) |
|
287 | ! |
if (length(avail_column_choices()) < 0) { |
288 | ! |
shinyjs::hide("var_to_add") |
289 |
} else { |
|
290 | ! |
shinyjs::show("var_to_add") |
291 |
} |
|
292 | ! |
teal.widgets::updateOptionalSelectInput( |
293 | ! |
session, |
294 | ! |
"var_to_add", |
295 | ! |
choices = avail_column_choices() |
296 |
) |
|
297 | ! |
logger::log_trace(paste( |
298 | ! |
"MatrixFilterStates$srv_add_filter_state@1 updated column choices,", |
299 | ! |
"dataname: { private$dataname }" |
300 |
)) |
|
301 |
} |
|
302 |
) |
|
303 | ||
304 | ! |
observeEvent( |
305 | ! |
eventExpr = input$var_to_add, |
306 | ! |
handlerExpr = { |
307 | ! |
logger::log_trace( |
308 | ! |
sprintf( |
309 | ! |
"MatrixFilterState$srv_add_filter_state@2 adding FilterState of variable %s, dataname: %s", |
310 | ! |
deparse1(input$var_to_add), |
311 | ! |
private$dataname |
312 |
) |
|
313 |
) |
|
314 | ! |
self$state_list_push( |
315 | ! |
x = init_filter_state( |
316 | ! |
subset(data, select = input$var_to_add), |
317 | ! |
varname = input$var_to_add, |
318 | ! |
varlabel = private$get_varlabel(input$var_to_add), |
319 | ! |
dataname = private$dataname, |
320 | ! |
extract_type = "matrix" |
321 |
), |
|
322 | ! |
state_list_index = "subset", |
323 | ! |
state_id = input$var_to_add |
324 |
) |
|
325 | ! |
logger::log_trace( |
326 | ! |
sprintf( |
327 | ! |
"MatrixFilterState$srv_add_filter_state@2 added FilterState of variable %s, dataname: %s", |
328 | ! |
deparse1(input$var_to_add), |
329 | ! |
private$dataname |
330 |
) |
|
331 |
) |
|
332 |
} |
|
333 |
) |
|
334 | ||
335 | ! |
logger::log_trace( |
336 | ! |
"MatrixFilterStates$srv_add_filter_state initialized, dataname: { private$dataname }" |
337 |
) |
|
338 | ! |
NULL |
339 |
} |
|
340 |
) |
|
341 |
} |
|
342 |
) |
|
343 |
) |
1 |
#' @name RangeFilterState |
|
2 |
#' @title `FilterState` object for numeric variable |
|
3 |
#' @description Manages choosing a numeric range |
|
4 |
#' @docType class |
|
5 |
#' @keywords internal |
|
6 |
#' |
|
7 |
#' |
|
8 |
#' @examples |
|
9 |
#' filter_state <- teal.slice:::RangeFilterState$new( |
|
10 |
#' c(NA, Inf, seq(1:10)), |
|
11 |
#' varname = "x", |
|
12 |
#' dataname = "data", |
|
13 |
#' extract_type = character(0) |
|
14 |
#' ) |
|
15 |
#' isolate(filter_state$get_call()) |
|
16 |
#' isolate(filter_state$set_selected(c(3L, 8L))) |
|
17 |
#' isolate(filter_state$set_keep_na(TRUE)) |
|
18 |
#' isolate(filter_state$set_keep_inf(TRUE)) |
|
19 |
#' isolate(filter_state$get_call()) |
|
20 |
#' |
|
21 |
#' \dontrun{ |
|
22 |
#' # working filter in an app |
|
23 |
#' library(shiny) |
|
24 |
#' |
|
25 |
#' data_range <- c(runif(100, 0, 1), NA, Inf) |
|
26 |
#' filter_state_range <- RangeFilterState$new( |
|
27 |
#' x = data_range, |
|
28 |
#' varname = "variable", |
|
29 |
#' varlabel = "label" |
|
30 |
#' ) |
|
31 |
#' filter_state_range$set_state(list(selected = c(0.15, 0.93), keep_na = TRUE, keep_inf = TRUE)) |
|
32 |
#' |
|
33 |
#' ui <- fluidPage( |
|
34 |
#' column(4, div( |
|
35 |
#' h4("RangeFilterState"), |
|
36 |
#' isolate(filter_state_range$ui("fs")) |
|
37 |
#' )), |
|
38 |
#' column(4, div( |
|
39 |
#' id = "outputs", # div id is needed for toggling the element |
|
40 |
#' h4("Condition (i.e. call)"), # display the subsetting call generated by this FilterState |
|
41 |
#' textOutput("condition_range"), br(), |
|
42 |
#' h4("Unformatted state"), # display raw filter state |
|
43 |
#' textOutput("unformatted_range"), br(), |
|
44 |
#' h4("Formatted state"), # display human readable filter state |
|
45 |
#' textOutput("formatted_range"), br() |
|
46 |
#' )), |
|
47 |
#' column(4, div( |
|
48 |
#' h4("Programmatic filter control"), |
|
49 |
#' actionButton("button1_range", "set drop NA", width = "100%"), br(), |
|
50 |
#' actionButton("button2_range", "set keep NA", width = "100%"), br(), |
|
51 |
#' actionButton("button3_range", "set drop Inf", width = "100%"), br(), |
|
52 |
#' actionButton("button4_range", "set keep Inf", width = "100%"), br(), |
|
53 |
#' actionButton("button5_range", "set a range", width = "100%"), br(), |
|
54 |
#' actionButton("button6_range", "set full range", width = "100%"), br(), |
|
55 |
#' actionButton("button0_range", "set initial state", width = "100%"), br() |
|
56 |
#' )) |
|
57 |
#' ) |
|
58 |
#' |
|
59 |
#' server <- function(input, output, session) { |
|
60 |
#' filter_state_range$server("fs") |
|
61 |
#' output$condition_range <- renderPrint(filter_state_range$get_call()) |
|
62 |
#' output$formatted_range <- renderText(filter_state_range$format()) |
|
63 |
#' output$unformatted_range <- renderPrint(filter_state_range$get_state()) |
|
64 |
#' # modify filter state programmatically |
|
65 |
#' observeEvent(input$button1_range, filter_state_range$set_keep_na(FALSE)) |
|
66 |
#' observeEvent(input$button2_range, filter_state_range$set_keep_na(TRUE)) |
|
67 |
#' observeEvent(input$button3_range, filter_state_range$set_keep_inf(FALSE)) |
|
68 |
#' observeEvent(input$button4_range, filter_state_range$set_keep_inf(TRUE)) |
|
69 |
#' observeEvent(input$button5_range, filter_state_range$set_selected(c(0.2, 0.74))) |
|
70 |
#' observeEvent(input$button6_range, filter_state_range$set_selected(c(0, 1))) |
|
71 |
#' observeEvent( |
|
72 |
#' input$button0_range, |
|
73 |
#' filter_state_range$set_state(list(selected = c(0.15, 0.93), keep_na = TRUE, keep_inf = TRUE)) |
|
74 |
#' ) |
|
75 |
#' } |
|
76 |
#' |
|
77 |
#' if (interactive()) { |
|
78 |
#' shinyApp(ui, server) |
|
79 |
#' } |
|
80 |
#' } |
|
81 |
#' |
|
82 |
RangeFilterState <- R6::R6Class( # nolint |
|
83 |
"RangeFilterState", |
|
84 |
inherit = FilterState, |
|
85 | ||
86 |
# public methods ---- |
|
87 |
public = list( |
|
88 | ||
89 |
#' @description |
|
90 |
#' Initialize a `FilterState` object |
|
91 |
#' @param x (`numeric`)\cr |
|
92 |
#' values of the variable used in filter |
|
93 |
#' @param varname (`character`, `name`)\cr |
|
94 |
#' name of the variable |
|
95 |
#' @param varlabel (`character(1)`)\cr |
|
96 |
#' label of the variable (optional). |
|
97 |
#' @param dataname (`character(1)`)\cr |
|
98 |
#' optional name of dataset where `x` is taken from |
|
99 |
#' @param extract_type (`character(0)`, `character(1)`)\cr |
|
100 |
#' whether condition calls should be prefixed by dataname. Possible values: |
|
101 |
#' \itemize{ |
|
102 |
#' \item{`character(0)` (default)}{ `varname` in the condition call will not be prefixed} |
|
103 |
#' \item{`"list"`}{ `varname` in the condition call will be returned as `<dataname>$<varname>`} |
|
104 |
#' \item{`"matrix"`}{ `varname` in the condition call will be returned as `<dataname>[, <varname>]`} |
|
105 |
#' } |
|
106 |
initialize = function(x, |
|
107 |
varname, |
|
108 |
varlabel = character(0), |
|
109 |
dataname = NULL, |
|
110 |
extract_type = character(0)) { |
|
111 | 88x |
checkmate::assert_numeric(x, all.missing = FALSE) |
112 | 2x |
if (!any(is.finite(x))) stop("\"x\" contains no finite values") |
113 | ||
114 | 86x |
super$initialize(x, varname, varlabel, dataname, extract_type) |
115 | 86x |
private$inf_count <- sum(is.infinite(x)) |
116 | 86x |
private$is_integer <- checkmate::test_integerish(x) |
117 | 86x |
private$keep_inf <- reactiveVal(FALSE) |
118 | ||
119 | 86x |
x_range <- range(x, finite = TRUE) |
120 | 86x |
x_pretty <- pretty(x_range, 100L) |
121 | ||
122 | 86x |
if (identical(diff(x_range), 0)) { |
123 | 11x |
private$set_choices(x_range) |
124 | 11x |
private$slider_ticks <- signif(x_range, digits = 10) |
125 | 11x |
private$slider_step <- NULL |
126 | 11x |
self$set_selected(x_range) |
127 |
} else { |
|
128 | 75x |
private$set_choices(range(x_pretty)) |
129 | 75x |
private$slider_ticks <- signif(x_pretty, digits = 10) |
130 | 75x |
private$slider_step <- signif(private$get_pretty_range_step(x_pretty), digits = 10) |
131 | 75x |
self$set_selected(range(x_pretty)) |
132 |
} |
|
133 | ||
134 | 86x |
private$histogram_data <- if (sum(is.finite(x)) >= 2) { |
135 | 75x |
as.data.frame( |
136 | 75x |
stats::density(x, na.rm = TRUE, n = 100)[c("x", "y")] # 100 bins only |
137 |
) |
|
138 |
} else { |
|
139 | 11x |
data.frame(x = NA_real_, y = NA_real_) |
140 |
} |
|
141 | ||
142 | 86x |
return(invisible(self)) |
143 |
}, |
|
144 | ||
145 |
#' @description |
|
146 |
#' Returns a formatted string representing this `RangeFilterState`. |
|
147 |
#' |
|
148 |
#' @param indent (`numeric(1)`) |
|
149 |
#' the number of spaces before after each new line character of the formatted string. |
|
150 |
#' Default: 0 |
|
151 |
#' @return `character(1)` the formatted string |
|
152 |
#' |
|
153 |
format = function(indent = 0) { |
|
154 | 33x |
checkmate::assert_number(indent, finite = TRUE, lower = 0) |
155 | ||
156 | 32x |
vals <- self$get_selected() |
157 | 32x |
sprintf( |
158 | 32x |
"%sFiltering on: %s\n%1$s Selected range: %s - %s\n%1$s Include missing values: %s", |
159 | 32x |
format("", width = indent), |
160 | 32x |
private$varname, |
161 | 32x |
format(vals[1], nsmall = 3), |
162 | 32x |
format(vals[2], nsmall = 3), |
163 | 32x |
format(self$get_keep_na()) |
164 |
) |
|
165 |
}, |
|
166 | ||
167 |
#' @description |
|
168 |
#' Answers the question of whether the current settings and values selected actually filters out any values. |
|
169 |
#' @return logical scalar |
|
170 |
is_any_filtered = function() { |
|
171 | 29x |
if (!isTRUE(all.equal(self$get_selected(), private$choices))) { |
172 | 20x |
TRUE |
173 | 9x |
} else if (!isTRUE(self$get_keep_inf()) && private$inf_count > 0) { |
174 | 2x |
TRUE |
175 | 7x |
} else if (!isTRUE(self$get_keep_na()) && private$na_count > 0) { |
176 | 2x |
TRUE |
177 |
} else { |
|
178 | 5x |
FALSE |
179 |
} |
|
180 |
}, |
|
181 | ||
182 |
#' @description |
|
183 |
#' Returns reproducible condition call for current selection. |
|
184 |
#' For this class returned call looks like |
|
185 |
#' `<varname> >= <min value> & <varname> <= <max value>` with |
|
186 |
#' optional `is.na(<varname>)` and `is.finite(<varname>)`. |
|
187 |
#' @return (`call`) |
|
188 |
get_call = function() { |
|
189 | 31x |
filter_call <- |
190 | 31x |
call( |
191 |
"&", |
|
192 | 31x |
call(">=", private$get_varname_prefixed(), self$get_selected()[1L]), |
193 | 31x |
call("<=", private$get_varname_prefixed(), self$get_selected()[2L]) |
194 |
) |
|
195 | 31x |
private$add_keep_na_call(private$add_keep_inf_call(filter_call)) |
196 |
}, |
|
197 | ||
198 |
#' @description |
|
199 |
#' Returns current `keep_inf` selection |
|
200 |
#' @return (`logical(1)`) |
|
201 |
get_keep_inf = function() { |
|
202 | 70x |
private$keep_inf() |
203 |
}, |
|
204 | ||
205 |
#' @description |
|
206 |
#' Returns the filtering state. |
|
207 |
#' |
|
208 |
#' @return `list` containing values taken from the reactive fields: |
|
209 |
#' * `selected` (`numeric(2)`) range of the filter. |
|
210 |
#' * `keep_na` (`logical(1)`) whether `NA` should be kept. |
|
211 |
#' * `keep_inf` (`logical(1)`) whether `Inf` should be kept. |
|
212 |
get_state = function() { |
|
213 | 28x |
list( |
214 | 28x |
selected = self$get_selected(), |
215 | 28x |
keep_na = self$get_keep_na(), |
216 | 28x |
keep_inf = self$get_keep_inf() |
217 |
) |
|
218 |
}, |
|
219 | ||
220 |
#' @description |
|
221 |
#' Set if `Inf` should be kept |
|
222 |
#' @param value (`logical(1)`)\cr |
|
223 |
#' Value(s) which come from the filter selection. Value is set in `server` |
|
224 |
#' modules after selecting check-box-input in the shiny interface. Values are set to |
|
225 |
#' `private$keep_inf` which is reactive. |
|
226 |
set_keep_inf = function(value) { |
|
227 | 36x |
checkmate::assert_flag(value) |
228 | 36x |
private$keep_inf(value) |
229 | 36x |
logger::log_trace( |
230 | 36x |
sprintf( |
231 | 36x |
"%s$set_keep_inf of variable %s set to %s, dataname: %s.", |
232 | 36x |
class(self)[1], |
233 | 36x |
private$varname, |
234 | 36x |
value, |
235 | 36x |
private$dataname |
236 |
) |
|
237 |
) |
|
238 |
}, |
|
239 | ||
240 |
#' @description |
|
241 |
#' Set state |
|
242 |
#' @param state (`list`)\cr |
|
243 |
#' contains fields relevant for a specific class |
|
244 |
#' \itemize{ |
|
245 |
#' \item{`selected`}{ defines initial selection} |
|
246 |
#' \item{`keep_na` (`logical`)}{ defines whether to keep or remove `NA` values} |
|
247 |
#' \item{`keep_inf` (`logical`)}{ defines whether to keep or remove `Inf` values} |
|
248 |
#' } |
|
249 |
set_state = function(state) { |
|
250 | 67x |
stopifnot(is.list(state) && all(names(state) %in% c("selected", "keep_na", "keep_inf"))) |
251 | 66x |
if (!is.null(state$keep_inf)) { |
252 | 27x |
self$set_keep_inf(state$keep_inf) |
253 |
} |
|
254 | 66x |
super$set_state(state[names(state) %in% c("selected", "keep_na")]) |
255 | 66x |
invisible(NULL) |
256 |
}, |
|
257 | ||
258 |
#' @description |
|
259 |
#' Sets the selected values of this `RangeFilterState`. |
|
260 |
#' |
|
261 |
#' @param value (`numeric(2)`) the two-elements array of the lower and upper bound |
|
262 |
#' of the selected range. Must not contain NA values. |
|
263 |
#' |
|
264 |
#' @returns invisibly `NULL` |
|
265 |
#' |
|
266 |
#' @note Casts the passed object to `numeric` before validating the input |
|
267 |
#' making it possible to pass any object coercible to `numeric` to this method. |
|
268 |
#' |
|
269 |
#' @examples |
|
270 |
#' filter <- teal.slice:::RangeFilterState$new(c(1, 2, 3, 4), varname = "name") |
|
271 |
#' filter$set_selected(c(2, 3)) |
|
272 |
#' |
|
273 |
set_selected = function(value) { |
|
274 | 158x |
super$set_selected(value) |
275 |
} |
|
276 |
), |
|
277 | ||
278 |
# private fields---- |
|
279 |
private = list( |
|
280 |
histogram_data = data.frame(), |
|
281 |
keep_inf = NULL, # because it holds reactiveVal |
|
282 |
inf_count = integer(0), |
|
283 |
is_integer = logical(0), |
|
284 |
slider_step = numeric(0), # step for the slider input widget, calculated from input data (x) |
|
285 |
slider_ticks = numeric(0), # allowed values for the slider input widget, calculated from input data (x) |
|
286 | ||
287 |
# private methods ---- |
|
288 |
# Adds is.infinite(varname) before existing condition calls if keep_inf is selected |
|
289 |
# returns a call |
|
290 |
add_keep_inf_call = function(filter_call) { |
|
291 | 31x |
if (isTRUE(self$get_keep_inf())) { |
292 | 3x |
call("|", call("is.infinite", private$get_varname_prefixed()), filter_call) |
293 |
} else { |
|
294 | 28x |
filter_call |
295 |
} |
|
296 |
}, |
|
297 | ||
298 |
# @description gets pretty step size for range slider |
|
299 |
# adaptation of shiny's method (see shiny/R/input-slider.R function findStepSize) |
|
300 |
# @param pretty_range (numeric(n)) vector of pretty values |
|
301 |
# @return numeric(1) pretty step size for the sliderInput |
|
302 |
get_pretty_range_step = function(pretty_range) { |
|
303 | 77x |
if (private$is_integer && diff(range(pretty_range) > 2)) { |
304 | 12x |
return(1L) |
305 |
} else { |
|
306 | 65x |
n_steps <- length(pretty_range) - 1 |
307 | 65x |
return( |
308 | 65x |
signif(digits = 10, (max(pretty_range) - min(pretty_range)) / n_steps) |
309 |
) |
|
310 |
} |
|
311 |
}, |
|
312 | ||
313 |
# overwrites superclass method |
|
314 |
validate_selection = function(value) { |
|
315 | 157x |
if (!is.numeric(value)) { |
316 | ! |
stop( |
317 | ! |
sprintf( |
318 | ! |
"value of the selection for `%s` in `%s` should be a numeric", |
319 | ! |
self$get_varname(), |
320 | ! |
self$get_dataname() |
321 |
) |
|
322 |
) |
|
323 |
} |
|
324 | 157x |
invisible(NULL) |
325 |
}, |
|
326 | ||
327 |
# overwrites superclass method |
|
328 |
# additionally adjusts progtammatic selection to existing slider ticks |
|
329 |
cast_and_validate = function(values) { |
|
330 | 1x |
if (!is.atomic(values)) stop("Values to set must be an atomic vector.") |
331 | 157x |
values <- as.numeric(values) |
332 | ! |
if (any(is.na(values))) stop("The array of set values must contain values coercible to numeric.") |
333 | ! |
if (length(values) != 2) stop("The array of set values must have length two.") |
334 | ||
335 | 157x |
values_adjusted <- contain_interval(values, private$slider_ticks) |
336 | 157x |
if (!isTRUE(all.equal(values, values_adjusted))) { |
337 | 1x |
logger::log_warn(sprintf( |
338 | 1x |
paste( |
339 | 1x |
"Programmatic range specification on %s was adjusted to existing slider ticks.", |
340 | 1x |
"It is now broader in order to contain the specified values." |
341 |
), |
|
342 | 1x |
private$varname |
343 |
)) |
|
344 |
} |
|
345 | 157x |
values_adjusted |
346 |
}, |
|
347 |
# for numeric ranges selecting out of bound values is allowed |
|
348 |
remove_out_of_bound_values = function(values) { |
|
349 | 157x |
values |
350 |
}, |
|
351 | ||
352 |
# shiny modules ---- |
|
353 | ||
354 |
# UI Module for `RangeFilterState`. |
|
355 |
# This UI element contains two values for `min` and `max` |
|
356 |
# of the range and two checkboxes whether to keep the `NA` or `Inf` values. |
|
357 |
# @param id (`character(1)`)\cr |
|
358 |
# id of shiny element |
|
359 |
ui_inputs = function(id) { |
|
360 | ! |
ns <- NS(id) |
361 | ! |
fluidRow( |
362 | ! |
div( |
363 | ! |
class = "filterPlotOverlayRange", |
364 | ! |
plotOutput(ns("plot"), height = "100%") |
365 |
), |
|
366 | ! |
div( |
367 | ! |
class = "filterRangeSlider", |
368 | ! |
teal.widgets::optionalSliderInput( |
369 | ! |
inputId = ns("selection"), |
370 | ! |
label = NULL, |
371 | ! |
min = private$choices[1], |
372 | ! |
max = private$choices[2], |
373 | ! |
value = isolate(private$selected()), |
374 | ! |
step = private$slider_step, |
375 | ! |
width = "100%" |
376 |
) |
|
377 |
), |
|
378 | ! |
private$keep_inf_ui(ns("keep_inf")), |
379 | ! |
private$keep_na_ui(ns("keep_na")) |
380 |
) |
|
381 |
}, |
|
382 | ||
383 |
# @description |
|
384 |
# Server module |
|
385 |
# @param id (`character(1)`)\cr |
|
386 |
# an ID string that corresponds with the ID used to call the module's UI function. |
|
387 |
# return `moduleServer` function which returns `NULL` |
|
388 |
server_inputs = function(id) { |
|
389 | ! |
moduleServer( |
390 | ! |
id = id, |
391 | ! |
function(input, output, session) { |
392 | ! |
logger::log_trace("RangeFilterState$server initializing, dataname: { private$dataname }") |
393 | ||
394 | ! |
output$plot <- renderPlot( |
395 | ! |
bg = "transparent", |
396 | ! |
height = 25, |
397 | ! |
expr = { |
398 | ! |
ggplot2::ggplot(private$histogram_data) + |
399 | ! |
ggplot2::aes_string(x = "x", y = "y") + |
400 | ! |
ggplot2::geom_area( |
401 | ! |
fill = grDevices::rgb(66 / 255, 139 / 255, 202 / 255), |
402 | ! |
color = NA, |
403 | ! |
alpha = 0.2 |
404 |
) + |
|
405 | ! |
ggplot2::theme_void() + |
406 | ! |
ggplot2::scale_y_continuous(expand = c(0, 0)) + |
407 | ! |
ggplot2::scale_x_continuous(expand = c(0, 0)) |
408 |
} |
|
409 |
) |
|
410 | ||
411 |
# this observer is needed in the situation when private$selected has been |
|
412 |
# changed directly by the api - then it's needed to rerender UI element |
|
413 |
# to show relevant values |
|
414 | ! |
private$observers$selection_api <- observeEvent( |
415 | ! |
ignoreNULL = FALSE, |
416 | ! |
ignoreInit = TRUE, |
417 | ! |
eventExpr = self$get_selected(), |
418 | ! |
handlerExpr = { |
419 | ! |
if (!isTRUE(all.equal(input$selection, self$get_selected()))) { |
420 | ! |
updateSliderInput( |
421 | ! |
session = session, |
422 | ! |
inputId = "selection", |
423 | ! |
value = private$selected() |
424 |
) |
|
425 |
} |
|
426 |
} |
|
427 |
) |
|
428 | ||
429 | ! |
private$observers$selection <- observeEvent( |
430 | ! |
ignoreNULL = FALSE, # ignoreNULL: we don't want to ignore NULL when nothing is selected in `selectInput` |
431 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
432 | ! |
eventExpr = input$selection, |
433 | ! |
handlerExpr = { |
434 | ! |
if (!isTRUE(all.equal(input$selection, self$get_selected()))) { |
435 | ! |
self$set_selected(input$selection) |
436 |
} |
|
437 | ! |
logger::log_trace( |
438 | ! |
sprintf( |
439 | ! |
"RangeFilterState$server@3 selection of variable %s changed, dataname: %s", |
440 | ! |
private$varname, |
441 | ! |
private$dataname |
442 |
) |
|
443 |
) |
|
444 |
} |
|
445 |
) |
|
446 | ||
447 | ! |
private$keep_inf_srv("keep_inf") |
448 | ! |
private$keep_na_srv("keep_na") |
449 | ||
450 | ! |
logger::log_trace("RangeFilterState$server initialized, dataname: { private$dataname }") |
451 | ! |
NULL |
452 |
} |
|
453 |
) |
|
454 |
}, |
|
455 | ||
456 |
# @description |
|
457 |
# module displaying input to keep or remove Inf in the FilterState call |
|
458 |
# @param id `shiny` id parameter |
|
459 |
# renders checkbox input only when variable from which FilterState has |
|
460 |
# been created has some Inf values. |
|
461 |
keep_inf_ui = function(id) { |
|
462 | ! |
ns <- NS(id) |
463 | ! |
if (private$inf_count > 0) { |
464 | ! |
checkboxInput( |
465 | ! |
ns("value"), |
466 | ! |
sprintf("Keep Inf (%s)", private$inf_count), |
467 | ! |
value = self$get_keep_inf() |
468 |
) |
|
469 |
} else { |
|
470 | ! |
NULL |
471 |
} |
|
472 |
}, |
|
473 | ||
474 |
# @description |
|
475 |
# module to handle Inf values in the FilterState |
|
476 |
# @param shiny `id` parametr passed to moduleServer |
|
477 |
# module sets `private$keep_inf` according to the selection. |
|
478 |
# Module also updates a UI element if the `private$keep_inf` has been |
|
479 |
# changed through the api |
|
480 |
keep_inf_srv = function(id) { |
|
481 | ! |
moduleServer(id, function(input, output, session) { |
482 |
# this observer is needed in the situation when private$keep_na has been |
|
483 |
# changed directly by the api - then it's needed to rerender UI element |
|
484 |
# to show relevant values |
|
485 | ! |
private$observers$keep_inf_api <- observeEvent( |
486 | ! |
ignoreNULL = TRUE, # its not possible for range that NULL is selected |
487 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
488 | ! |
eventExpr = self$get_keep_inf(), |
489 | ! |
handlerExpr = { |
490 | ! |
if (!setequal(self$get_keep_inf(), input$value)) { |
491 | ! |
updateCheckboxInput( |
492 | ! |
inputId = "value", |
493 | ! |
value = self$get_keep_inf() |
494 |
) |
|
495 |
} |
|
496 |
} |
|
497 |
) |
|
498 | ! |
private$observers$keep_inf <- observeEvent( |
499 | ! |
ignoreNULL = TRUE, # it's not possible for range that NULL is selected |
500 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
501 | ! |
eventExpr = input$value, |
502 | ! |
handlerExpr = { |
503 | ! |
keep_inf <- input$value |
504 | ! |
self$set_keep_inf(keep_inf) |
505 | ! |
logger::log_trace( |
506 | ! |
sprintf( |
507 | ! |
"%s$server keep_inf of variable %s set to: %s, dataname: %s", |
508 | ! |
class(self)[1], |
509 | ! |
private$varname, |
510 | ! |
deparse1(input$value), |
511 | ! |
private$dataname |
512 |
) |
|
513 |
) |
|
514 |
} |
|
515 |
) |
|
516 | ! |
invisible(NULL) |
517 |
}) |
|
518 |
} |
|
519 |
) |
|
520 |
) |
1 |
#' @rdname DatetimeFilterState |
|
2 |
#' @title `FilterState` object for `POSIXct` variable |
|
3 |
#' @description Manages choosing a range of date-times |
|
4 |
#' @docType class |
|
5 |
#' @keywords internal |
|
6 |
#' |
|
7 |
#' |
|
8 |
#' @examples |
|
9 |
#' filter_state <- teal.slice:::DatetimeFilterState$new( |
|
10 |
#' c(Sys.time() + seq(0, by = 3600, length.out = 10), NA), |
|
11 |
#' varname = "x", |
|
12 |
#' dataname = "data", |
|
13 |
#' extract_type = character(0) |
|
14 |
#' ) |
|
15 |
#' |
|
16 |
#' isolate(filter_state$get_call()) |
|
17 |
#' isolate(filter_state$set_selected(c(Sys.time() + 3L, Sys.time() + 8L))) |
|
18 |
#' isolate(filter_state$set_keep_na(TRUE)) |
|
19 |
#' isolate(filter_state$get_call()) |
|
20 |
#' |
|
21 |
#' \dontrun{ |
|
22 |
#' # working filter in an app |
|
23 |
#' library(shiny) |
|
24 |
#' |
|
25 |
#' datetimes <- as.POSIXct(c("2012-01-01 12:00:00", "2020-01-01 12:00:00")) |
|
26 |
#' data_datetime <- c(seq(from = datetimes[1], to = datetimes[2], length.out = 100), NA) |
|
27 |
#' filter_state_datetime <- DatetimeFilterState$new( |
|
28 |
#' x = data_datetime, |
|
29 |
#' varname = "variable", |
|
30 |
#' varlabel = "label" |
|
31 |
#' ) |
|
32 |
#' filter_state_datetime$set_state(list(selected = data_datetime[c(47, 98)], keep_na = TRUE)) |
|
33 |
#' |
|
34 |
#' ui <- fluidPage( |
|
35 |
#' column(4, div( |
|
36 |
#' h4("DatetimeFilterState"), |
|
37 |
#' isolate(filter_state_datetime$ui("fs")) |
|
38 |
#' )), |
|
39 |
#' column(4, div( |
|
40 |
#' id = "outputs", # div id is needed for toggling the element |
|
41 |
#' h4("Condition (i.e. call)"), # display the subsetting call generated by this FilterState |
|
42 |
#' textOutput("condition_datetime"), br(), |
|
43 |
#' h4("Unformatted state"), # display raw filter state |
|
44 |
#' textOutput("unformatted_datetime"), br(), |
|
45 |
#' h4("Formatted state"), # display human readable filter state |
|
46 |
#' textOutput("formatted_datetime"), br() |
|
47 |
#' )), |
|
48 |
#' column(4, div( |
|
49 |
#' h4("Programmatic filter control"), |
|
50 |
#' actionButton("button1_datetime", "set drop NA", width = "100%"), br(), |
|
51 |
#' actionButton("button2_datetime", "set keep NA", width = "100%"), br(), |
|
52 |
#' actionButton("button3_datetime", "set a range", width = "100%"), br(), |
|
53 |
#' actionButton("button4_datetime", "set full range", width = "100%"), br(), |
|
54 |
#' actionButton("button0_datetime", "set initial state", width = "100%"), br() |
|
55 |
#' )) |
|
56 |
#' ) |
|
57 |
#' |
|
58 |
#' server <- function(input, output, session) { |
|
59 |
#' filter_state_datetime$server("fs") |
|
60 |
#' output$condition_datetime <- renderPrint(filter_state_datetime$get_call()) |
|
61 |
#' output$formatted_datetime <- renderText(filter_state_datetime$format()) |
|
62 |
#' output$unformatted_datetime <- renderPrint(filter_state_datetime$get_state()) |
|
63 |
#' # modify filter state programmatically |
|
64 |
#' observeEvent(input$button1_datetime, filter_state_datetime$set_keep_na(FALSE)) |
|
65 |
#' observeEvent(input$button2_datetime, filter_state_datetime$set_keep_na(TRUE)) |
|
66 |
#' observeEvent( |
|
67 |
#' input$button3_datetime, |
|
68 |
#' filter_state_datetime$set_selected(data_datetime[c(34, 56)]) |
|
69 |
#' ) |
|
70 |
#' observeEvent(input$button4_datetime, filter_state_datetime$set_selected(datetimes)) |
|
71 |
#' observeEvent( |
|
72 |
#' input$button0_datetime, |
|
73 |
#' filter_state_datetime$set_state(list(selected = data_datetime[c(47, 98)], keep_na = TRUE)) |
|
74 |
#' ) |
|
75 |
#' } |
|
76 |
#' |
|
77 |
#' if (interactive()) { |
|
78 |
#' shinyApp(ui, server) |
|
79 |
#' } |
|
80 |
#' } |
|
81 |
#' |
|
82 |
DatetimeFilterState <- R6::R6Class( # nolint |
|
83 |
"DatetimeFilterState", |
|
84 |
inherit = FilterState, |
|
85 | ||
86 |
# public methods ---- |
|
87 | ||
88 |
public = list( |
|
89 | ||
90 |
#' @description |
|
91 |
#' Initialize a `FilterState` object. This class |
|
92 |
#' has an extra field, `private$timezone`, which is set to `Sys.timezone()` by |
|
93 |
#' default. However, in case when using this module in `teal` app, one needs |
|
94 |
#' timezone of the app user. App user timezone is taken from `session$userData$timezone` |
|
95 |
#' and is set only if object is initialized in `shiny`. |
|
96 |
#' @param x (`POSIXct` or `POSIXlt`)\cr |
|
97 |
#' values of the variable used in filter |
|
98 |
#' @param varname (`character`, `name`)\cr |
|
99 |
#' name of the variable |
|
100 |
#' @param varlabel (`character(1)`)\cr |
|
101 |
#' label of the variable (optional). |
|
102 |
#' @param dataname (`character(1)`)\cr |
|
103 |
#' optional name of dataset where `x` is taken from |
|
104 |
#' @param extract_type (`character(0)`, `character(1)`)\cr |
|
105 |
#' whether condition calls should be prefixed by dataname. Possible values: |
|
106 |
#' \itemize{ |
|
107 |
#' \item{`character(0)` (default)}{ `varname` in the condition call will not be prefixed} |
|
108 |
#' \item{`"list"`}{ `varname` in the condition call will be returned as `<dataname>$<varname>`} |
|
109 |
#' \item{`"matrix"`}{ `varname` in the condition call will be returned as `<dataname>[, <varname>]`} |
|
110 |
#' } |
|
111 |
initialize = function(x, |
|
112 |
varname, |
|
113 |
varlabel = character(0), |
|
114 |
dataname = NULL, |
|
115 |
extract_type = character(0)) { |
|
116 | 23x |
checkmate::assert_multi_class(x, c("POSIXct", "POSIXlt")) |
117 | 23x |
super$initialize(x, varname, varlabel, dataname, extract_type) |
118 | ||
119 | 23x |
var_range <- as.POSIXct(trunc(range(x, na.rm = TRUE), units = "secs")) |
120 | 23x |
private$set_choices(var_range) |
121 | 23x |
self$set_selected(var_range) |
122 | ||
123 | 23x |
return(invisible(self)) |
124 |
}, |
|
125 | ||
126 |
#' @description |
|
127 |
#' Returns a formatted string representing this `DatetimeFilterState`. |
|
128 |
#' |
|
129 |
#' @param indent (`numeric(1)`) the number of spaces before after each new line character of the formatted string. |
|
130 |
#' Default: 0 |
|
131 |
#' @return `character(1)` the formatted string |
|
132 |
#' |
|
133 |
format = function(indent = 0) { |
|
134 | 6x |
checkmate::assert_number(indent, finite = TRUE, lower = 0) |
135 | ||
136 | ||
137 | 5x |
vals <- self$get_selected() |
138 | 5x |
sprintf( |
139 | 5x |
"%sFiltering on: %s\n%1$s Selected range: %s - %s\n%1$s Include missing values: %s", |
140 | 5x |
format("", width = indent), |
141 | 5x |
private$varname, |
142 | 5x |
format(vals[1], nsmall = 3), |
143 | 5x |
format(vals[2], nsmall = 3), |
144 | 5x |
format(self$get_keep_na()) |
145 |
) |
|
146 |
}, |
|
147 | ||
148 |
#' @description |
|
149 |
#' Answers the question of whether the current settings and values selected actually filters out any values. |
|
150 |
#' @return logical scalar |
|
151 |
is_any_filtered = function() { |
|
152 | 6x |
if (!setequal(self$get_selected(), private$choices)) { |
153 | 4x |
TRUE |
154 | 2x |
} else if (!isTRUE(self$get_keep_na()) && private$na_count > 0) { |
155 | 1x |
TRUE |
156 |
} else { |
|
157 | 1x |
FALSE |
158 |
} |
|
159 |
}, |
|
160 | ||
161 |
#' @description |
|
162 |
#' Returns reproducible condition call for current selection. |
|
163 |
#' For this class returned call looks like |
|
164 |
#' `<varname> >= as.POSIXct(<min>) & <varname> <= <max>)` |
|
165 |
#' with optional `is.na(<varname>)`. |
|
166 |
get_call = function() { |
|
167 | 10x |
choices <- self$get_selected() |
168 | 10x |
tzone <- Find(function(x) x != "", attr(as.POSIXlt(choices), "tzone")) |
169 | 10x |
class <- class(choices)[1L] |
170 | 10x |
date_fun <- as.name(switch(class, |
171 | 10x |
"POSIXct" = "as.POSIXct", |
172 | 10x |
"POSIXlt" = "as.POSIXlt" |
173 |
)) |
|
174 | 10x |
choices <- as.character(choices + c(0, 1)) |
175 | 10x |
filter_call <- |
176 | 10x |
call( |
177 |
"&", |
|
178 | 10x |
call( |
179 |
">=", |
|
180 | 10x |
private$get_varname_prefixed(), |
181 | 10x |
as.call(list(date_fun, choices[1L], tz = tzone)) |
182 |
), |
|
183 | 10x |
call( |
184 |
"<", |
|
185 | 10x |
private$get_varname_prefixed(), |
186 | 10x |
as.call(list(date_fun, choices[2L], tz = tzone)) |
187 |
) |
|
188 |
) |
|
189 | 10x |
private$add_keep_na_call(filter_call) |
190 |
}, |
|
191 | ||
192 |
#' @description |
|
193 |
#' Sets the selected time frame of this `DatetimeFilterState`. |
|
194 |
#' |
|
195 |
#' @param value (`POSIX(2)`) the lower and the upper bound of the selected |
|
196 |
#' time frame. Must not contain NA values. |
|
197 |
#' |
|
198 |
#' @return invisibly `NULL`. |
|
199 |
#' |
|
200 |
#' @note Casts the passed object to `POSIXct` before validating the input |
|
201 |
#' making it possible to pass any object coercible to `POSIXct` to this method. |
|
202 |
#' |
|
203 |
#' @examples |
|
204 |
#' date <- as.POSIXct(1, origin = "01/01/1970") |
|
205 |
#' filter <- teal.slice:::DatetimeFilterState$new( |
|
206 |
#' c(date, date + 1, date + 2, date + 3), |
|
207 |
#' varname = "name" |
|
208 |
#' ) |
|
209 |
#' filter$set_selected(c(date + 1, date + 2)) |
|
210 |
set_selected = function(value) { |
|
211 | 41x |
super$set_selected(value) |
212 |
} |
|
213 |
), |
|
214 | ||
215 |
# private members ---- |
|
216 | ||
217 |
private = list( |
|
218 |
# private methods ---- |
|
219 |
validate_selection = function(value) { |
|
220 | 40x |
if (!(is(value, "POSIXct") || is(value, "POSIXlt"))) { |
221 | ! |
stop( |
222 | ! |
sprintf( |
223 | ! |
"value of the selection for `%s` in `%s` should be a POSIXct or POSIXlt", |
224 | ! |
self$get_varname(), |
225 | ! |
self$get_dataname() |
226 |
) |
|
227 |
) |
|
228 |
} |
|
229 | ||
230 | 40x |
pre_msg <- sprintf( |
231 | 40x |
"dataset '%s', variable '%s': ", |
232 | 40x |
self$get_dataname(), |
233 | 40x |
self$get_varname() |
234 |
) |
|
235 | 40x |
check_in_range(value, private$choices, pre_msg = pre_msg) |
236 |
}, |
|
237 |
cast_and_validate = function(values) { |
|
238 | 41x |
tryCatch( |
239 | 41x |
expr = { |
240 | 41x |
values <- as.POSIXct(values) |
241 | ! |
if (any(is.na(values))) stop() |
242 |
}, |
|
243 | 41x |
error = function(error) stop("The array of set values must contain values coercible to POSIX.") |
244 |
) |
|
245 | ! |
if (length(values) != 2) stop("The array of set values must have length two.") |
246 | 40x |
values |
247 |
}, |
|
248 |
remove_out_of_bound_values = function(values) { |
|
249 | 40x |
if (values[1] < private$choices[1]) { |
250 | 4x |
warning(paste( |
251 | 4x |
"Value:", values[1], "is outside of the possible range for column", private$varname, |
252 | 4x |
"of dataset", private$dataname, "." |
253 |
)) |
|
254 | 4x |
values[1] <- private$choices[1] |
255 |
} |
|
256 | ||
257 | 40x |
if (values[2] > private$choices[2]) { |
258 | 5x |
warning(paste( |
259 | 5x |
"Value:", values[2], "is outside of the possible range for column", private$varname, |
260 | 5x |
"of dataset", private$dataname, "." |
261 |
)) |
|
262 | 5x |
values[2] <- private$choices[2] |
263 |
} |
|
264 | 40x |
values |
265 |
}, |
|
266 | ||
267 |
# shiny modules ---- |
|
268 | ||
269 |
# @description |
|
270 |
# UI Module for `DatetimeFilterState`. |
|
271 |
# This UI element contains two date-time selections for `min` and `max` |
|
272 |
# of the range and a checkbox whether to keep the `NA` values. |
|
273 |
# @param id (`character(1)`)\cr |
|
274 |
# id of shiny element |
|
275 |
ui_inputs = function(id) { |
|
276 | ! |
ns <- NS(id) |
277 | ! |
div( |
278 | ! |
div( |
279 | ! |
class = "flex", |
280 | ! |
actionButton( |
281 | ! |
class = "date_reset_button", |
282 | ! |
inputId = ns("start_date_reset"), |
283 | ! |
label = NULL, |
284 | ! |
icon = icon("fas fa-undo") |
285 |
), |
|
286 | ! |
div( |
287 | ! |
class = "flex w-80 filter_datelike_input", |
288 | ! |
div(class = "w-45 text-center", { |
289 | ! |
x <- shinyWidgets::airDatepickerInput( |
290 | ! |
inputId = ns("selection_start"), |
291 | ! |
value = self$get_selected()[1], |
292 | ! |
startView = self$get_selected()[1], |
293 | ! |
timepicker = TRUE, |
294 | ! |
minDate = private$choices[1], |
295 | ! |
maxDate = private$choices[2], |
296 | ! |
update_on = "close", |
297 | ! |
addon = "none", |
298 | ! |
position = "bottom right" |
299 |
) |
|
300 | ! |
x$children[[2]]$attribs <- c(x$children[[2]]$attribs, list(class = "input-sm")) |
301 | ! |
x |
302 |
}), |
|
303 | ! |
span( |
304 | ! |
class = "input-group-addon w-10", |
305 | ! |
span(class = "input-group-text w-100 justify-content-center", "to"), |
306 | ! |
title = "Times are displayed in the local timezone and are converted to UTC in the analysis" |
307 |
), |
|
308 | ! |
div(class = "w-45 text-center", { |
309 | ! |
x <- shinyWidgets::airDatepickerInput( |
310 | ! |
inputId = ns("selection_end"), |
311 | ! |
value = self$get_selected()[2], |
312 | ! |
startView = self$get_selected()[2], |
313 | ! |
timepicker = TRUE, |
314 | ! |
minDate = private$choices[1], |
315 | ! |
maxDate = private$choices[2], |
316 | ! |
update_on = "close", |
317 | ! |
addon = "none", |
318 | ! |
position = "bottom right" |
319 |
) |
|
320 | ! |
x$children[[2]]$attribs <- c(x$children[[2]]$attribs, list(class = "input-sm")) |
321 | ! |
x |
322 |
}) |
|
323 |
), |
|
324 | ! |
actionButton( |
325 | ! |
class = "date_reset_button", |
326 | ! |
inputId = ns("end_date_reset"), |
327 | ! |
label = NULL, |
328 | ! |
icon = icon("fas fa-undo") |
329 |
) |
|
330 |
), |
|
331 | ! |
private$keep_na_ui(ns("keep_na")) |
332 |
) |
|
333 |
}, |
|
334 | ||
335 |
# @description |
|
336 |
# Server module |
|
337 |
# @param id (`character(1)`)\cr |
|
338 |
# an ID string that corresponds with the ID used to call the module's UI function. |
|
339 |
# @return `moduleServer` function which returns `NULL` |
|
340 |
server_inputs = function(id) { |
|
341 | ! |
moduleServer( |
342 | ! |
id = id, |
343 | ! |
function(input, output, session) { |
344 | ! |
logger::log_trace("DatetimeFilterState$server initializing, dataname: { private$dataname }") |
345 | ||
346 |
# this observer is needed in the situation when private$selected has been |
|
347 |
# changed directly by the api - then it's needed to rerender UI element |
|
348 |
# to show relevant values |
|
349 | ! |
private$observers$selection_api <- observeEvent( |
350 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
351 | ! |
ignoreInit = TRUE, # on init selected == default, so no need to trigger |
352 | ! |
eventExpr = self$get_selected(), |
353 | ! |
handlerExpr = { |
354 | ! |
start_date <- input$selection_start |
355 | ! |
end_date <- input$selection_end |
356 | ! |
if (!all(self$get_selected() == c(start_date, end_date))) { |
357 | ! |
if (self$get_selected()[1] != start_date) { |
358 | ! |
shinyWidgets::updateAirDateInput( |
359 | ! |
session = session, |
360 | ! |
inputId = "selection_start", |
361 | ! |
value = self$get_selected()[1] |
362 |
) |
|
363 |
} |
|
364 | ||
365 | ! |
if (self$get_selected()[2] != end_date) { |
366 | ! |
shinyWidgets::updateAirDateInput( |
367 | ! |
session = session, |
368 | ! |
inputId = "selection_end", |
369 | ! |
value = self$get_selected()[2] |
370 |
) |
|
371 |
} |
|
372 | ||
373 | ! |
logger::log_trace(sprintf( |
374 | ! |
"DatetimeFilterState$server@1 selection of variable %s changed, dataname: %s", |
375 | ! |
private$varname, |
376 | ! |
private$dataname |
377 |
)) |
|
378 |
} |
|
379 |
} |
|
380 |
) |
|
381 | ||
382 | ||
383 | ! |
private$observers$selection <- observeEvent( |
384 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
385 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
386 | ! |
eventExpr = { |
387 | ! |
input$selection_start |
388 | ! |
input$selection_end |
389 |
}, |
|
390 | ! |
handlerExpr = { |
391 | ! |
start_date <- input$selection_start |
392 | ! |
end_date <- input$selection_end |
393 | ! |
tzone <- Find(function(x) x != "", attr(as.POSIXlt(private$choices), "tzone")) |
394 | ! |
attr(start_date, "tzone") <- tzone |
395 | ! |
attr(end_date, "tzone") <- tzone |
396 | ||
397 | ! |
if (start_date < private$choices[1]) { |
398 | ! |
start_date <- private$choices[1] |
399 |
} |
|
400 | ||
401 | ! |
if (end_date > private$choices[2]) { |
402 | ! |
end_date <- private$choices[2] |
403 |
} |
|
404 | ||
405 | ! |
self$set_selected(c(start_date, end_date)) |
406 | ! |
logger::log_trace(sprintf( |
407 | ! |
"DatetimeFilterState$server@2 selection of variable %s changed, dataname: %s", |
408 | ! |
private$varname, |
409 | ! |
private$dataname |
410 |
)) |
|
411 |
} |
|
412 |
) |
|
413 | ||
414 | ! |
private$keep_na_srv("keep_na") |
415 | ||
416 | ! |
private$observers$reset1 <- observeEvent( |
417 | ! |
ignoreInit = TRUE, # reset button shouldn't be trigger on init |
418 | ! |
ignoreNULL = TRUE, # it's impossible and wrong to set default to NULL |
419 | ! |
input$start_date_reset, |
420 |
{ |
|
421 | ! |
shinyWidgets::updateAirDateInput( |
422 | ! |
session = session, |
423 | ! |
inputId = "selection_start", |
424 | ! |
value = private$choices[1] |
425 |
) |
|
426 | ! |
logger::log_trace(sprintf( |
427 | ! |
"DatetimeFilterState$server@2 reset start date of variable %s, dataname: %s", |
428 | ! |
private$varname, |
429 | ! |
private$dataname |
430 |
)) |
|
431 |
} |
|
432 |
) |
|
433 | ! |
private$observers$reset2 <- observeEvent( |
434 | ! |
ignoreInit = TRUE, # reset button shouldn't be trigger on init |
435 | ! |
ignoreNULL = TRUE, # it's impossible and wrong to set default to NULL |
436 | ! |
input$end_date_reset, |
437 |
{ |
|
438 | ! |
shinyWidgets::updateAirDateInput( |
439 | ! |
session = session, |
440 | ! |
inputId = "selection_end", |
441 | ! |
value = private$choices[2] |
442 |
) |
|
443 | ! |
logger::log_trace(sprintf( |
444 | ! |
"DatetimeFilterState$server@3 reset end date of variable %s, dataname: %s", |
445 | ! |
private$varname, |
446 | ! |
private$dataname |
447 |
)) |
|
448 |
} |
|
449 |
) |
|
450 | ! |
logger::log_trace("DatetimeFilterState$server initialized, dataname: { private$dataname }") |
451 | ! |
NULL |
452 |
} |
|
453 |
) |
|
454 |
} |
|
455 |
) |
|
456 |
) |
1 |
#' @name ChoicesFilterState |
|
2 |
#' @title `FilterState` object for factor or character variable |
|
3 |
#' @description Manages choosing elements from a set |
|
4 |
#' @docType class |
|
5 |
#' @keywords internal |
|
6 |
#' |
|
7 |
#' |
|
8 |
#' @examples |
|
9 |
#' filter_state <- teal.slice:::ChoicesFilterState$new( |
|
10 |
#' c(LETTERS, NA), |
|
11 |
#' varname = "x", |
|
12 |
#' dataname = "data", |
|
13 |
#' extract_type = character(0) |
|
14 |
#' ) |
|
15 |
#' isolate(filter_state$get_call()) |
|
16 |
#' isolate(filter_state$set_selected("B")) |
|
17 |
#' isolate(filter_state$set_keep_na(TRUE)) |
|
18 |
#' isolate(filter_state$get_call()) |
|
19 |
#' |
|
20 |
#' \dontrun{ |
|
21 |
#' # working filter in an app |
|
22 |
#' library(shiny) |
|
23 |
#' |
|
24 |
#' data_choices <- c(sample(letters[1:4], 100, replace = TRUE), NA) |
|
25 |
#' filter_state_choices <- ChoicesFilterState$new( |
|
26 |
#' x = data_choices, |
|
27 |
#' varname = "variable", |
|
28 |
#' varlabel = "label" |
|
29 |
#' ) |
|
30 |
#' filter_state_choices$set_state(list(selected = c("a", "c"), keep_na = TRUE)) |
|
31 |
#' |
|
32 |
#' ui <- fluidPage( |
|
33 |
#' column(4, div( |
|
34 |
#' h4("ChoicesFilterState"), |
|
35 |
#' isolate(filter_state_choices$ui("fs")) |
|
36 |
#' )), |
|
37 |
#' column(4, div( |
|
38 |
#' h4("Condition (i.e. call)"), # display the subsetting call generated by this FilterState |
|
39 |
#' textOutput("condition_choices"), br(), |
|
40 |
#' h4("Unformatted state"), # display raw filter state |
|
41 |
#' textOutput("unformatted_choices"), br(), |
|
42 |
#' h4("Formatted state"), # display human readable filter state |
|
43 |
#' textOutput("formatted_choices"), br() |
|
44 |
#' )), |
|
45 |
#' column(4, div( |
|
46 |
#' h4("Programmatic filter control"), |
|
47 |
#' actionButton("button1_choices", "set drop NA", width = "100%"), br(), |
|
48 |
#' actionButton("button2_choices", "set keep NA", width = "100%"), br(), |
|
49 |
#' actionButton("button3_choices", "set a selection", width = "100%"), br(), |
|
50 |
#' actionButton("button4_choices", "deselect all", width = "100%"), br(), |
|
51 |
#' actionButton("button0_choices", "set initial state", width = "100%"), br() |
|
52 |
#' )) |
|
53 |
#' ) |
|
54 |
#' |
|
55 |
#' server <- function(input, output, session) { |
|
56 |
#' filter_state_choices$server("fs") |
|
57 |
#' output$condition_choices <- renderPrint(filter_state_choices$get_call()) |
|
58 |
#' output$formatted_choices <- renderText(filter_state_choices$format()) |
|
59 |
#' output$unformatted_choices <- renderPrint(filter_state_choices$get_state()) |
|
60 |
#' # modify filter state programmatically |
|
61 |
#' observeEvent(input$button1_choices, filter_state_choices$set_keep_na(FALSE)) |
|
62 |
#' observeEvent(input$button2_choices, filter_state_choices$set_keep_na(TRUE)) |
|
63 |
#' observeEvent( |
|
64 |
#' input$button3_choices, |
|
65 |
#' filter_state_choices$set_selected(c("b", "c")) |
|
66 |
#' ) |
|
67 |
#' observeEvent(input$button4_choices, filter_state_choices$set_selected(c())) |
|
68 |
#' observeEvent( |
|
69 |
#' input$button0_choices, |
|
70 |
#' filter_state_choices$set_state(list(selected = c("a", "c"), keep_na = TRUE)) |
|
71 |
#' ) |
|
72 |
#' } |
|
73 |
#' |
|
74 |
#' if (interactive()) { |
|
75 |
#' shinyApp(ui, server) |
|
76 |
#' } |
|
77 |
#' } |
|
78 |
#' |
|
79 |
ChoicesFilterState <- R6::R6Class( # nolint |
|
80 |
"ChoicesFilterState", |
|
81 |
inherit = FilterState, |
|
82 | ||
83 |
# public methods ---- |
|
84 | ||
85 |
public = list( |
|
86 | ||
87 |
#' @description |
|
88 |
#' Initialize a `FilterState` object |
|
89 |
#' @param x (`character` or `factor`)\cr |
|
90 |
#' values of the variable used in filter |
|
91 |
#' @param varname (`character`)\cr |
|
92 |
#' name of the variable |
|
93 |
#' @param varlabel (`character(1)`)\cr |
|
94 |
#' label of the variable (optional). |
|
95 |
#' @param dataname (`character(1)`)\cr |
|
96 |
#' optional name of dataset where `x` is taken from |
|
97 |
#' @param extract_type (`character(0)`, `character(1)`)\cr |
|
98 |
#' whether condition calls should be prefixed by dataname. Possible values: |
|
99 |
#' \itemize{ |
|
100 |
#' \item{`character(0)` (default)}{ `varname` in the condition call will not be prefixed} |
|
101 |
#' \item{`"list"`}{ `varname` in the condition call will be returned as `<dataname>$<varname>`} |
|
102 |
#' \item{`"matrix"`}{ `varname` in the condition call will be returned as `<dataname>[, <varname>]`} |
|
103 |
#' } |
|
104 |
initialize = function(x, |
|
105 |
varname, |
|
106 |
varlabel = character(0), |
|
107 |
dataname = NULL, |
|
108 |
extract_type = character(0)) { |
|
109 | 144x |
checkmate::assert( |
110 | 144x |
is.character(x), |
111 | 144x |
is.factor(x), |
112 | 144x |
length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup"), |
113 | 144x |
combine = "or" |
114 |
) |
|
115 | 144x |
super$initialize(x, varname, varlabel, dataname, extract_type) |
116 | ||
117 | 144x |
private$data_class <- class(x)[1L] |
118 | 144x |
if (inherits(x, "POSIXt")) { |
119 | 12x |
private$tzone <- Find(function(x) x != "", attr(as.POSIXlt(x), "tzone")) |
120 |
} |
|
121 | ||
122 | 144x |
if (!is.factor(x)) { |
123 | 113x |
x <- factor(as.character(x), levels = as.character(sort(unique(x)))) |
124 |
} |
|
125 | 144x |
x <- droplevels(x) |
126 | 144x |
tbl <- table(x) |
127 | 144x |
choices <- names(tbl) |
128 | 144x |
names(choices) <- tbl |
129 | ||
130 | ||
131 | 144x |
private$set_choices(as.list(choices)) |
132 | 144x |
self$set_selected(unname(choices)) |
133 | 144x |
private$histogram_data <- data.frame( |
134 | 144x |
x = levels(x), |
135 | 144x |
y = tabulate(x) |
136 |
) |
|
137 | ||
138 | 144x |
return(invisible(self)) |
139 |
}, |
|
140 | ||
141 |
#' @description |
|
142 |
#' Answers the question of whether the current settings and values selected actually filters out any values. |
|
143 |
#' @return logical scalar |
|
144 |
is_any_filtered = function() { |
|
145 | 44x |
if (!setequal(self$get_selected(), private$choices)) { |
146 | 37x |
TRUE |
147 | 7x |
} else if (!isTRUE(self$get_keep_na()) && private$na_count > 0) { |
148 | 3x |
TRUE |
149 |
} else { |
|
150 | 4x |
FALSE |
151 |
} |
|
152 |
}, |
|
153 | ||
154 |
#' @description |
|
155 |
#' Returns reproducible condition call for current selection. |
|
156 |
#' For this class returned call looks like |
|
157 |
#' `<varname> %in% c(<values selected>)` with |
|
158 |
#' optional `is.na(<varname>)`. |
|
159 |
#' @return (`call`) |
|
160 |
get_call = function() { |
|
161 | 61x |
varname <- private$get_varname_prefixed() |
162 | 61x |
choices <- self$get_selected() |
163 | 61x |
if (private$data_class != "factor") { |
164 | 50x |
choices <- do.call(sprintf("as.%s", private$data_class), list(x = choices)) |
165 |
} |
|
166 | 61x |
fun_compare <- if (length(choices) == 1L) "==" else "%in%" |
167 | 61x |
filter_call <- |
168 | 61x |
if (inherits(choices, "Date")) { |
169 | 4x |
call(fun_compare, varname, call("as.Date", as.character(choices))) |
170 | 61x |
} else if (inherits(choices, c("POSIXct", "POSIXlt"))) { |
171 | 8x |
class <- class(choices)[1L] |
172 | 8x |
date_fun <- as.name(switch(class, |
173 | 8x |
"POSIXct" = "as.POSIXct", |
174 | 8x |
"POSIXlt" = "as.POSIXlt" |
175 |
)) |
|
176 | 8x |
call(fun_compare, varname, as.call(list(date_fun, as.character(choices), tz = private$tzone))) |
177 |
} else { |
|
178 |
# This handles numerics, characters, and factors. |
|
179 | 49x |
call(fun_compare, varname, choices) |
180 |
} |
|
181 | 61x |
private$add_keep_na_call(filter_call) |
182 |
}, |
|
183 | ||
184 |
#' @description |
|
185 |
#' Set state |
|
186 |
#' @param state (`list`)\cr |
|
187 |
#' contains fields relevant for a specific class |
|
188 |
#' \itemize{ |
|
189 |
#' \item{`selected`}{ defines initial selection} |
|
190 |
#' \item{`keep_na` (`logical`)}{ defines whether to keep or remove `NA` values} |
|
191 |
#' } |
|
192 |
set_state = function(state) { |
|
193 | 100x |
if (!is.null(state$selected)) { |
194 | 100x |
state$selected <- as.character(state$selected) |
195 |
} |
|
196 | 100x |
super$set_state(state) |
197 | 99x |
invisible(NULL) |
198 |
}, |
|
199 | ||
200 |
#' @description |
|
201 |
#' Sets the selected values of this `ChoicesFilterState`. |
|
202 |
#' |
|
203 |
#' @param value (`character`) the array of the selected choices. |
|
204 |
#' Must not contain NA values. |
|
205 |
#' |
|
206 |
#' @return invisibly `NULL` |
|
207 |
#' |
|
208 |
#' @note Casts the passed object to `character` before validating the input |
|
209 |
#' making it possible to pass any object coercible to `character` to this method. |
|
210 |
#' |
|
211 |
#' @examples |
|
212 |
#' filter <- teal.slice:::ChoicesFilterState$new(c("a", "b", "c"), varname = "name") |
|
213 |
#' filter$set_selected(c("c", "a")) |
|
214 |
set_selected = function(value) { |
|
215 | 282x |
super$set_selected(value) |
216 |
} |
|
217 |
), |
|
218 | ||
219 |
# private members ---- |
|
220 | ||
221 |
private = list( |
|
222 |
histogram_data = data.frame(), |
|
223 |
data_class = character(0), # stores class of filtered variable so that it can be restored in $get_call |
|
224 |
tzone = character(0), # if x is a datetime, stores time zone so that it can be restored in $get_call |
|
225 | ||
226 |
# private methods ---- |
|
227 |
validate_selection = function(value) { |
|
228 | 282x |
if (!is.character(value)) { |
229 | ! |
stop( |
230 | ! |
sprintf( |
231 | ! |
"Values of the selection for `%s` in `%s` should be an array of character.", |
232 | ! |
self$get_varname(), |
233 | ! |
self$get_dataname() |
234 |
) |
|
235 |
) |
|
236 |
} |
|
237 | 282x |
pre_msg <- sprintf( |
238 | 282x |
"data '%s', variable '%s': ", |
239 | 282x |
self$get_dataname(), |
240 | 282x |
self$get_varname() |
241 |
) |
|
242 | 282x |
check_in_subset(value, private$choices, pre_msg = pre_msg) |
243 |
}, |
|
244 |
cast_and_validate = function(values) { |
|
245 | 282x |
tryCatch( |
246 | 282x |
expr = { |
247 | 282x |
values <- as.character(values) |
248 | ! |
if (any(is.na(values))) stop() |
249 |
}, |
|
250 | 282x |
error = function(error) stop("The array of set values must contain values coercible to character.") |
251 |
) |
|
252 | 282x |
values |
253 |
}, |
|
254 |
remove_out_of_bound_values = function(values) { |
|
255 | 282x |
in_choices_mask <- values %in% private$choices |
256 | 282x |
if (length(values[!in_choices_mask]) > 0) { |
257 | 18x |
warning(paste( |
258 | 18x |
"Values:", strtrim(paste(values[!in_choices_mask], collapse = ", "), 360), |
259 | 18x |
"are not in choices of column", private$varname, "in dataset", private$dataname, "." |
260 |
)) |
|
261 |
} |
|
262 | 282x |
values[in_choices_mask] |
263 |
}, |
|
264 | ||
265 |
# shiny modules ---- |
|
266 | ||
267 |
# @description |
|
268 |
# UI Module for `ChoicesFilterState`. |
|
269 |
# This UI element contains available choices selection and |
|
270 |
# checkbox whether to keep or not keep the `NA` values. |
|
271 |
# @param id (`character(1)`)\cr |
|
272 |
# id of shiny element |
|
273 |
ui_inputs = function(id) { |
|
274 | ! |
ns <- NS(id) |
275 | ! |
div( |
276 | ! |
if (length(private$choices) <= getOption("teal.threshold_slider_vs_checkboxgroup")) { |
277 | ! |
l_counts <- as.numeric(names(private$choices)) |
278 | ! |
l_counts[is.na(l_counts)] <- 0 |
279 | ! |
l_freqs <- l_counts / sum(l_counts) |
280 | ! |
labels <- lapply(seq_along(private$choices), function(i) { |
281 | ! |
div( |
282 | ! |
class = "choices_state_label", |
283 | ! |
style = sprintf("width:%s%%", l_freqs[i] * 100), |
284 | ! |
span( |
285 | ! |
class = "choices_state_label_text", |
286 | ! |
sprintf( |
287 | ! |
"%s (%s)", |
288 | ! |
private$choices[i], |
289 | ! |
l_counts[i] |
290 |
) |
|
291 |
) |
|
292 |
) |
|
293 |
}) |
|
294 | ! |
div( |
295 | ! |
class = "choices_state", |
296 | ! |
checkboxGroupInput( |
297 | ! |
ns("selection"), |
298 | ! |
label = NULL, |
299 | ! |
selected = self$get_selected(), |
300 | ! |
choiceNames = labels, |
301 | ! |
choiceValues = as.character(private$choices), |
302 | ! |
width = "100%" |
303 |
) |
|
304 |
) |
|
305 |
} else { |
|
306 | ! |
teal.widgets::optionalSelectInput( |
307 | ! |
inputId = ns("selection"), |
308 | ! |
choices = stats::setNames(private$choices, sprintf("%s (%s)", private$choices, names(private$choices))), |
309 | ! |
selected = self$get_selected(), |
310 | ! |
multiple = TRUE, |
311 | ! |
options = shinyWidgets::pickerOptions( |
312 | ! |
actionsBox = TRUE, |
313 | ! |
liveSearch = (length(private$choices) > 10), |
314 | ! |
noneSelectedText = "Select a value" |
315 |
) |
|
316 |
) |
|
317 |
}, |
|
318 | ! |
private$keep_na_ui(ns("keep_na")) |
319 |
) |
|
320 |
}, |
|
321 | ||
322 |
# @description |
|
323 |
# Server module |
|
324 |
# @param id (`character(1)`)\cr |
|
325 |
# an ID string that corresponds with the ID used to call the module's UI function. |
|
326 |
# @return `moduleServer` function which returns `NULL` |
|
327 |
server_inputs = function(id) { |
|
328 | ! |
moduleServer( |
329 | ! |
id = id, |
330 | ! |
function(input, output, session) { |
331 | ! |
logger::log_trace("ChoicesFilterState$server initializing, dataname: { private$dataname }") |
332 | ||
333 |
# this observer is needed in the situation when private$selected has been |
|
334 |
# changed directly by the api - then it's needed to rerender UI element |
|
335 |
# to show relevant values |
|
336 | ! |
private$observers$selection_api <- observeEvent( |
337 | ! |
ignoreNULL = FALSE, # it's possible that nothing is selected |
338 | ! |
ignoreInit = TRUE, |
339 | ! |
eventExpr = self$get_selected(), |
340 | ! |
handlerExpr = { |
341 | ! |
if (!setequal(self$get_selected(), input$selection)) { |
342 | ! |
updateCheckboxInput( |
343 | ! |
session = session, |
344 | ! |
inputId = "selection", |
345 | ! |
value = self$get_selected() |
346 |
) |
|
347 | ! |
logger::log_trace(sprintf( |
348 | ! |
"ChoicesFilterState$server@1 selection of variable %s changed, dataname: %s", |
349 | ! |
private$varname, |
350 | ! |
private$dataname |
351 |
)) |
|
352 |
} |
|
353 |
} |
|
354 |
) |
|
355 | ||
356 | ! |
private$observers$selection <- observeEvent( |
357 | ! |
ignoreNULL = FALSE, # it's possible that nothing is selected |
358 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
359 | ! |
eventExpr = input$selection, |
360 | ! |
handlerExpr = { |
361 | ! |
selection <- if (is.null(input$selection)) character(0) else input$selection |
362 | ! |
self$set_selected(selection) |
363 | ! |
logger::log_trace(sprintf( |
364 | ! |
"ChoicesFilterState$server@2 selection of variable %s changed, dataname: %s", |
365 | ! |
private$varname, |
366 | ! |
private$dataname |
367 |
)) |
|
368 |
} |
|
369 |
) |
|
370 | ! |
private$keep_na_srv("keep_na") |
371 | ||
372 | ! |
logger::log_trace("ChoicesFilterState$server initialized, dataname: { private$dataname }") |
373 | ! |
NULL |
374 |
} |
|
375 |
) |
|
376 |
} |
|
377 |
) |
|
378 |
) |
1 |
#' @name LogicalFilterState |
|
2 |
#' @title `FilterState` object for logical variable |
|
3 |
#' @description Manages choosing a logical state |
|
4 |
#' @docType class |
|
5 |
#' @keywords internal |
|
6 |
#' |
|
7 |
#' |
|
8 |
#' @examples |
|
9 |
#' filter_state <- teal.slice:::LogicalFilterState$new( |
|
10 |
#' sample(c(TRUE, FALSE, NA), 10, replace = TRUE), |
|
11 |
#' varname = "x", |
|
12 |
#' dataname = "data", |
|
13 |
#' extract_type = character(0) |
|
14 |
#' ) |
|
15 |
#' isolate(filter_state$get_call()) |
|
16 |
#' |
|
17 |
#' isolate(filter_state$set_selected(TRUE)) |
|
18 |
#' isolate(filter_state$set_keep_na(TRUE)) |
|
19 |
#' isolate(filter_state$get_call()) |
|
20 |
#' |
|
21 |
#' \dontrun{ |
|
22 |
#' # working filter in an app |
|
23 |
#' library(shiny) |
|
24 |
#' |
|
25 |
#' data_logical <- c(sample(c(TRUE, FALSE), 10, replace = TRUE), NA) |
|
26 |
#' filter_state_logical <- LogicalFilterState$new( |
|
27 |
#' x = data_logical, |
|
28 |
#' varname = "variable", |
|
29 |
#' varlabel = "label" |
|
30 |
#' ) |
|
31 |
#' filter_state_logical$set_state(list(selected = FALSE, keep_na = TRUE)) |
|
32 |
#' |
|
33 |
#' ui <- fluidPage( |
|
34 |
#' column(4, div( |
|
35 |
#' h4("LogicalFilterState"), |
|
36 |
#' isolate(filter_state_logical$ui("fs")) |
|
37 |
#' )), |
|
38 |
#' column(4, div( |
|
39 |
#' id = "outputs", # div id is needed for toggling the element |
|
40 |
#' h4("Condition (i.e. call)"), # display the subsetting call generated by this FilterState |
|
41 |
#' textOutput("condition_logical"), br(), |
|
42 |
#' h4("Unformatted state"), # display raw filter state |
|
43 |
#' textOutput("unformatted_logical"), br(), |
|
44 |
#' h4("Formatted state"), # display human readable filter state |
|
45 |
#' textOutput("formatted_logical"), br() |
|
46 |
#' )), |
|
47 |
#' column(4, div( |
|
48 |
#' h4("Programmatic filter control"), |
|
49 |
#' actionButton("button1_logical", "set drop NA", width = "100%"), br(), |
|
50 |
#' actionButton("button2_logical", "set keep NA", width = "100%"), br(), |
|
51 |
#' actionButton("button3_logical", "set a selection", width = "100%"), br(), |
|
52 |
#' actionButton("button0_logical", "set initial state", width = "100%"), br() |
|
53 |
#' )) |
|
54 |
#' ) |
|
55 |
#' |
|
56 |
#' server <- function(input, output, session) { |
|
57 |
#' filter_state_logical$server("fs") |
|
58 |
#' output$condition_logical <- renderPrint(filter_state_logical$get_call()) |
|
59 |
#' output$formatted_logical <- renderText(filter_state_logical$format()) |
|
60 |
#' output$unformatted_logical <- renderPrint(filter_state_logical$get_state()) |
|
61 |
#' # modify filter state programmatically |
|
62 |
#' observeEvent(input$button1_logical, filter_state_logical$set_keep_na(FALSE)) |
|
63 |
#' observeEvent(input$button2_logical, filter_state_logical$set_keep_na(TRUE)) |
|
64 |
#' observeEvent(input$button3_logical, filter_state_logical$set_selected(TRUE)) |
|
65 |
#' observeEvent( |
|
66 |
#' input$button0_logical, |
|
67 |
#' filter_state_logical$set_state(list(selected = FALSE, keep_na = TRUE)) |
|
68 |
#' ) |
|
69 |
#' } |
|
70 |
#' |
|
71 |
#' if (interactive()) { |
|
72 |
#' shinyApp(ui, server) |
|
73 |
#' } |
|
74 |
#' } |
|
75 |
#' |
|
76 |
LogicalFilterState <- R6::R6Class( # nolint |
|
77 |
"LogicalFilterState", |
|
78 |
inherit = FilterState, |
|
79 | ||
80 |
# public methods ---- |
|
81 |
public = list( |
|
82 | ||
83 |
#' @description |
|
84 |
#' Initialize a `FilterState` object |
|
85 |
#' @param x (`logical`)\cr |
|
86 |
#' values of the variable used in filter |
|
87 |
#' @param varname (`character`, `name`)\cr |
|
88 |
#' label of the variable (optional). |
|
89 |
#' @param varlabel (`character(1)`)\cr |
|
90 |
#' label of the variable (optional). |
|
91 |
#' @param dataname (`character(1)`)\cr |
|
92 |
#' optional name of dataset where `x` is taken from |
|
93 |
#' @param extract_type (`character(0)`, `character(1)`)\cr |
|
94 |
#' whether condition calls should be prefixed by dataname. Possible values: |
|
95 |
#' \itemize{ |
|
96 |
#' \item{`character(0)` (default)}{ `varname` in the condition call will not be prefixed} |
|
97 |
#' \item{`"list"`}{ `varname` in the condition call will be returned as `<dataname>$<varname>`} |
|
98 |
#' \item{`"matrix"`}{ `varname` in the condition call will be returned as `<dataname>[, <varname>]`} |
|
99 |
#' } |
|
100 |
initialize = function(x, |
|
101 |
varname, |
|
102 |
varlabel = character(0), |
|
103 |
dataname = NULL, |
|
104 |
extract_type = character(0)) { |
|
105 | 16x |
stopifnot(is.logical(x)) |
106 | 16x |
super$initialize(x, varname, varlabel, dataname, extract_type) |
107 | 16x |
df <- as.factor(x) |
108 | 16x |
if (length(levels(df)) != 2) { |
109 | 6x |
if (levels(df) %in% c(TRUE, FALSE)) { |
110 | 6x |
choices_not_included <- c(TRUE, FALSE)[!c(TRUE, FALSE) %in% levels(df)] |
111 | 6x |
levels(df) <- c(levels(df), choices_not_included) |
112 |
} |
|
113 |
} |
|
114 | ||
115 | 16x |
tbl <- table(df) |
116 | ||
117 | 16x |
choices <- as.logical(names(tbl)) |
118 | 16x |
names(choices) <- tbl |
119 | 16x |
private$set_choices(as.list(choices)) |
120 | 16x |
self$set_selected(unname(choices)[1]) |
121 | 16x |
private$histogram_data <- data.frame( |
122 | 16x |
x = sprintf( |
123 | 16x |
"%s (%s)", |
124 | 16x |
choices, |
125 | 16x |
names(choices) |
126 |
), |
|
127 | 16x |
y = as.vector(tbl) |
128 |
) |
|
129 | ||
130 | 16x |
invisible(self) |
131 |
}, |
|
132 | ||
133 |
#' @description |
|
134 |
#' Answers the question of whether the current settings and values selected actually filters out any values. |
|
135 |
#' @return logical scalar |
|
136 |
is_any_filtered = function() { |
|
137 | 20x |
if (!isTRUE(self$get_keep_na()) && private$na_count > 0) { |
138 | 9x |
TRUE |
139 | 11x |
} else if (all(private$histogram_data$y > 0)) { |
140 | 6x |
TRUE |
141 | 5x |
} else if (self$get_selected() == FALSE && "FALSE (0)" %in% private$histogram_data$x) { |
142 | 1x |
TRUE |
143 | 4x |
} else if (self$get_selected() == TRUE && "TRUE (0)" %in% private$histogram_data$x) { |
144 | 1x |
TRUE |
145 |
} else { |
|
146 | 3x |
FALSE |
147 |
} |
|
148 |
}, |
|
149 | ||
150 |
#' @description |
|
151 |
#' Returns reproducible condition call for current selection. |
|
152 |
#' For `LogicalFilterState` it's a `!<varname>` or `<varname>` and optionally |
|
153 |
#' `is.na(<varname>)` |
|
154 |
get_call = function() { |
|
155 | 8x |
filter_call <- |
156 | 8x |
if (self$get_selected()) { |
157 | 2x |
private$get_varname_prefixed() |
158 |
} else { |
|
159 | 6x |
call("!", private$get_varname_prefixed()) |
160 |
} |
|
161 | 8x |
private$add_keep_na_call(filter_call) |
162 |
}, |
|
163 | ||
164 |
#' @description |
|
165 |
#' Sets the selected values of this `LogicalFilterState`. |
|
166 |
#' |
|
167 |
#' @param value (`logical(1)`)\cr |
|
168 |
#' the value to set. Must not contain the NA value. |
|
169 |
#' |
|
170 |
#' @returns invisibly `NULL`. |
|
171 |
#' |
|
172 |
#' @note Casts the passed object to `logical` before validating the input |
|
173 |
#' making it possible to pass any object coercible to `logical` to this method. |
|
174 |
#' |
|
175 |
#' @examples |
|
176 |
#' filter <- teal.slice:::LogicalFilterState$new(c(TRUE), varname = "name") |
|
177 |
#' filter$set_selected(TRUE) |
|
178 |
set_selected = function(value) { |
|
179 | 44x |
super$set_selected(value) |
180 |
} |
|
181 |
), |
|
182 | ||
183 |
# private fields ---- |
|
184 | ||
185 |
private = list( |
|
186 |
histogram_data = data.frame(), |
|
187 | ||
188 |
# private methods ---- |
|
189 | ||
190 |
validate_selection = function(value) { |
|
191 | 44x |
if (!(checkmate::test_logical(value, max.len = 1, any.missing = FALSE))) { |
192 | 2x |
stop( |
193 | 2x |
sprintf( |
194 | 2x |
"value of the selection for `%s` in `%s` should be a logical scalar (TRUE or FALSE)", |
195 | 2x |
self$get_varname(), |
196 | 2x |
self$get_dataname() |
197 |
) |
|
198 |
) |
|
199 |
} |
|
200 | ||
201 | 42x |
pre_msg <- sprintf( |
202 | 42x |
"dataset '%s', variable '%s': ", |
203 | 42x |
self$get_dataname(), |
204 | 42x |
self$get_varname() |
205 |
) |
|
206 | 42x |
check_in_subset(value, private$choices, pre_msg = pre_msg) |
207 |
}, |
|
208 |
cast_and_validate = function(values) { |
|
209 | 44x |
tryCatch( |
210 | 44x |
expr = { |
211 | 44x |
values_logical <- as.logical(values) |
212 | ! |
if (any(is.na(values_logical))) stop() |
213 |
}, |
|
214 | 44x |
error = function(cond) stop("The array of set values must contain values coercible to logical.") |
215 |
) |
|
216 | 44x |
values_logical |
217 |
}, |
|
218 | ||
219 |
# shiny modules ---- |
|
220 | ||
221 |
# @description |
|
222 |
# UI Module for `EmptyFilterState`. |
|
223 |
# This UI element contains available choices selection and |
|
224 |
# checkbox whether to keep or not keep the `NA` values. |
|
225 |
# @param id (`character(1)`)\cr |
|
226 |
# id of shiny element |
|
227 |
ui_inputs = function(id) { |
|
228 | ! |
ns <- NS(id) |
229 | ! |
l_counts <- as.numeric(names(private$choices)) |
230 | ! |
l_counts[is.na(l_counts)] <- 0 |
231 | ! |
l_freqs <- l_counts / sum(l_counts) |
232 | ! |
labels <- lapply(seq_along(private$choices), function(i) { |
233 | ! |
div( |
234 | ! |
class = "choices_state_label", |
235 | ! |
style = sprintf("width:%s%%", l_freqs[i] * 100), |
236 | ! |
span( |
237 | ! |
class = "choices_state_label_text", |
238 | ! |
sprintf( |
239 | ! |
"%s (%s)", |
240 | ! |
private$choices[i], |
241 | ! |
l_counts[i] |
242 |
) |
|
243 |
) |
|
244 |
) |
|
245 |
}) |
|
246 | ! |
div( |
247 | ! |
div( |
248 | ! |
class = "choices_state", |
249 | ! |
radioButtons( |
250 | ! |
ns("selection"), |
251 | ! |
label = NULL, |
252 | ! |
choiceNames = labels, |
253 | ! |
choiceValues = as.character(private$choices), |
254 | ! |
selected = as.character(self$get_selected()), |
255 | ! |
width = "100%" |
256 |
) |
|
257 |
), |
|
258 | ! |
private$keep_na_ui(ns("keep_na")) |
259 |
) |
|
260 |
}, |
|
261 | ||
262 |
# @description |
|
263 |
# Server module |
|
264 |
# |
|
265 |
# @param id (`character(1)`)\cr |
|
266 |
# an ID string that corresponds with the ID used to call the module's UI function. |
|
267 |
# @return `moduleServer` function which returns `NULL` |
|
268 |
server_inputs = function(id) { |
|
269 | ! |
moduleServer( |
270 | ! |
id = id, |
271 | ! |
function(input, output, session) { |
272 |
# this observer is needed in the situation when private$selected has been |
|
273 |
# changed directly by the api - then it's needed to rerender UI element |
|
274 |
# to show relevant values |
|
275 | ! |
private$observers$seleted_api <- observeEvent( |
276 | ! |
ignoreNULL = TRUE, # this is radio button so something have to be selected |
277 | ! |
ignoreInit = TRUE, |
278 | ! |
eventExpr = self$get_selected(), |
279 | ! |
handlerExpr = { |
280 | ! |
if (!setequal(self$get_selected(), input$selection)) { |
281 | ! |
updateRadioButtons( |
282 | ! |
session = session, |
283 | ! |
inputId = "selection", |
284 | ! |
selected = self$get_selected() |
285 |
) |
|
286 | ! |
logger::log_trace(sprintf( |
287 | ! |
"LogicalFilterState$server@1 selection of variable %s changed, dataname: %s", |
288 | ! |
private$varname, |
289 | ! |
private$dataname |
290 |
)) |
|
291 |
} |
|
292 |
} |
|
293 |
) |
|
294 | ||
295 | ! |
private$observers$selection <- observeEvent( |
296 | ! |
ignoreNULL = TRUE, # in radio button something has to be selected to input$selection can't be NULL |
297 | ! |
ignoreInit = TRUE, |
298 | ! |
eventExpr = input$selection, |
299 | ! |
handlerExpr = { |
300 | ! |
selection_state <- as.logical(input$selection) |
301 | ! |
if (is.null(selection_state)) { |
302 | ! |
selection_state <- logical(0) |
303 |
} |
|
304 | ! |
self$set_selected(selection_state) |
305 | ! |
logger::log_trace( |
306 | ! |
sprintf( |
307 | ! |
"LogicalFilterState$server@2 selection of variable %s changed, dataname: %s", |
308 | ! |
private$varname, |
309 | ! |
private$dataname |
310 |
) |
|
311 |
) |
|
312 |
} |
|
313 |
) |
|
314 | ||
315 | ! |
private$keep_na_srv("keep_na") |
316 | ||
317 | ! |
logger::log_trace("LogicalFilterState$server initialized, dataname: { private$dataname }") |
318 | ! |
NULL |
319 |
} |
|
320 |
) |
|
321 |
} |
|
322 |
) |
|
323 |
) |
1 |
#' @name FilterPanelAPI |
|
2 |
#' @docType class |
|
3 |
#' |
|
4 |
#' @title Class to encapsulate the API of the filter panel of a teal app |
|
5 |
#' |
|
6 |
#' @details |
|
7 |
#' The purpose of this class is to encapsulate the API of the filter panel in a new class `FilterPanelAPI` so |
|
8 |
#' that it can be passed and used in the `server` call of any module instead of passing the whole `FilteredData` |
|
9 |
#' object. |
|
10 |
#' |
|
11 |
#' This class is supported by methods to set, get, remove filter states in the filter panel API. |
|
12 |
#' |
|
13 |
#' @export |
|
14 |
#' |
|
15 |
#' @examples |
|
16 |
#' library(teal.slice) |
|
17 |
#' fd <- teal.slice::init_filtered_data(list(iris = list(dataset = iris))) |
|
18 |
#' fpa <- FilterPanelAPI$new(fd) |
|
19 |
#' |
|
20 |
#' # get the actual filter state --> empty named list |
|
21 |
#' isolate(fpa$get_filter_state()) |
|
22 |
#' |
|
23 |
#' # set a filter state |
|
24 |
#' isolate( |
|
25 |
#' set_filter_state( |
|
26 |
#' fpa, |
|
27 |
#' list(iris = list(Species = list(selected = "setosa", keep_na = TRUE))) |
|
28 |
#' ) |
|
29 |
#' ) |
|
30 |
#' |
|
31 |
#' # get the actual filter state --> named list with filters |
|
32 |
#' isolate(fpa$get_filter_state()) |
|
33 |
#' |
|
34 |
#' # remove all_filter_states |
|
35 |
#' fpa$remove_all_filter_states() |
|
36 |
#' |
|
37 |
#' # get the actual filter state --> empty named list |
|
38 |
#' isolate(fpa$get_filter_state()) |
|
39 |
#' |
|
40 |
FilterPanelAPI <- R6::R6Class( # nolint |
|
41 |
"FilterPanelAPI", |
|
42 |
## __Public Methods ==== |
|
43 |
public = list( |
|
44 |
#' @description |
|
45 |
#' Initialize a `FilterPanelAPI` object |
|
46 |
#' @param datasets (`FilteredData`) object. |
|
47 |
initialize = function(datasets) { |
|
48 | 10x |
checkmate::assert_class(datasets, "FilteredData") |
49 | 8x |
private$filtered_data <- datasets |
50 |
}, |
|
51 | ||
52 |
#' @description |
|
53 |
#' Gets the reactive values from the active `FilterState` objects of the `FilteredData` object. |
|
54 |
#' |
|
55 |
#' Gets all active filters in the form of a nested list. |
|
56 |
#' The output list is a compatible input to `set_filter_state`. |
|
57 |
#' |
|
58 |
#' @return `list` with named elements corresponding to `FilteredDataset` objects with active filters. |
|
59 |
get_filter_state = function() { |
|
60 | 7x |
private$filtered_data$get_filter_state() |
61 |
}, |
|
62 | ||
63 |
#' @description |
|
64 |
#' Sets active filter states. |
|
65 |
#' @param filter (`named list`)\cr |
|
66 |
#' nested list of filter selections applied to datasets. |
|
67 |
#' |
|
68 |
#' @return `NULL` |
|
69 |
set_filter_state = function(filter) { |
|
70 | 8x |
if (private$filtered_data$get_filter_panel_active()) { |
71 | 6x |
private$filtered_data$set_filter_state(filter) |
72 |
} else { |
|
73 | 2x |
warning(private$deactivated_msg) |
74 |
} |
|
75 | 8x |
invisible(NULL) |
76 |
}, |
|
77 | ||
78 |
#' @description |
|
79 |
#' Remove one or more `FilterState` of a `FilteredDataset` in the `FilteredData` object. |
|
80 |
#' @param filter (`named list`)\cr |
|
81 |
#' nested list of filter selections applied to datasets. |
|
82 |
#' |
|
83 |
#' @return `NULL` |
|
84 |
remove_filter_state = function(filter) { |
|
85 | 1x |
if (private$filtered_data$get_filter_panel_active()) { |
86 | 1x |
private$filtered_data$remove_filter_state(filter) |
87 |
} else { |
|
88 | ! |
warning(private$deactivated_msg) |
89 |
} |
|
90 | 1x |
invisible(NULL) |
91 |
}, |
|
92 | ||
93 |
#' @description Remove all `FilterStates` of the `FilteredData` object. |
|
94 |
#' |
|
95 |
#' @param datanames (`character`)\cr |
|
96 |
#' datanames to remove their `FilterStates`; |
|
97 |
#' omit to remove all `FilterStates` in the `FilteredData` object |
|
98 |
#' |
|
99 |
#' @return `NULL` |
|
100 |
#' |
|
101 |
remove_all_filter_states = function(datanames) { |
|
102 | 5x |
if (private$filtered_data$get_filter_panel_active()) { |
103 | 3x |
datanames_to_remove <- if (missing(datanames)) private$filtered_data$datanames() else datanames |
104 | 3x |
private$filtered_data$remove_all_filter_states(datanames = datanames_to_remove) |
105 |
} else { |
|
106 | 2x |
warning(private$deactivated_msg) |
107 |
} |
|
108 | 5x |
invisible(NULL) |
109 |
}, |
|
110 |
#' @description |
|
111 |
#' Toggle the state of the global Filter Panel button by running `javascript` code |
|
112 |
#' to click the toggle button with the `filter_panel_active` id suffix. |
|
113 |
#' The button id is prefixed with the Filter Panel shiny namespace. |
|
114 |
#' This button is observed in `srv_filter_panel` method that executes |
|
115 |
#' `filter_panel_enable()` or `filter_panel_disable()` method depending on the toggle state. |
|
116 |
#' |
|
117 |
#' @return `NULL` |
|
118 |
filter_panel_toggle = function() { |
|
119 | ! |
shinyjs::runjs( |
120 | ! |
sprintf( |
121 | ! |
'$("#%s-filter_turn_onoff").click();', |
122 | ! |
private$filtered_data$get_filter_panel_ui_id() |
123 |
) |
|
124 |
) |
|
125 | ! |
invisible(NULL) |
126 |
} |
|
127 |
), |
|
128 |
## __Private Methods ==== |
|
129 |
private = list( |
|
130 |
filtered_data = NULL, |
|
131 |
deactivated_msg = "Filter Panel is deactivated so the action can not be applied with api." |
|
132 |
) |
|
133 |
) |
1 |
# MAEFilteredDataset ------ |
|
2 |
#' @keywords internal |
|
3 |
#' @title `MAEFilteredDataset` R6 class |
|
4 |
MAEFilteredDataset <- R6::R6Class( # nolint |
|
5 |
classname = "MAEFilteredDataset", |
|
6 |
inherit = FilteredDataset, |
|
7 | ||
8 |
# public methods ---- |
|
9 |
public = list( |
|
10 | ||
11 |
#' @description |
|
12 |
#' Initialize `MAEFilteredDataset` object |
|
13 |
#' |
|
14 |
#' @param dataset (`MulitiAssayExperiment`)\cr |
|
15 |
#' a single `MultiAssayExperiment` for which to define a subset |
|
16 |
#' @param dataname (`character`)\cr |
|
17 |
#' a given name for the dataset it may not contain spaces |
|
18 |
#' @param keys optional, (`character`)\cr |
|
19 |
#' vector with primary keys |
|
20 |
#' @param label (`character`)\cr |
|
21 |
#' label to describe the dataset |
|
22 |
#' @param metadata (named `list` or `NULL`) \cr |
|
23 |
#' field containing metadata about the dataset; |
|
24 |
#' each element of the list must be atomic and length one |
|
25 |
#' |
|
26 |
initialize = function(dataset, dataname, keys = character(0), label = character(0), metadata = NULL) { |
|
27 | 28x |
if (!requireNamespace("MultiAssayExperiment", quietly = TRUE)) { |
28 | ! |
stop("Cannot load MultiAssayExperiment - please install the package or restart your session.") |
29 |
} |
|
30 | 28x |
checkmate::assert_class(dataset, "MultiAssayExperiment") |
31 | 27x |
super$initialize(dataset, dataname, keys, label, metadata) |
32 | 27x |
experiment_names <- names(dataset) |
33 | ||
34 |
# subsetting by subjects means subsetting by colData(MAE) |
|
35 | 27x |
private$add_filter_states( |
36 | 27x |
filter_states = init_filter_states( |
37 | 27x |
data = dataset, |
38 | 27x |
dataname = dataname, |
39 | 27x |
varlabels = self$get_varlabels(), |
40 | 27x |
datalabel = "subjects", |
41 | 27x |
keys = self$get_keys() |
42 |
), |
|
43 | 27x |
id = "subjects" |
44 |
) |
|
45 |
# elements of the list (experiments) are unknown |
|
46 |
# dispatch needed because we can't hardcode methods otherwise: |
|
47 |
# if (matrix) else if (SummarizedExperiment) else if ... |
|
48 | 27x |
lapply( |
49 | 27x |
experiment_names, |
50 | 27x |
function(experiment_name) { |
51 | 135x |
private$add_filter_states( |
52 | 135x |
filter_states = init_filter_states( |
53 | 135x |
data = dataset[[experiment_name]], |
54 | 135x |
dataname = sprintf('%s[["%s"]]', dataname, experiment_name), |
55 | 135x |
datalabel = experiment_name |
56 |
), |
|
57 | 135x |
id = experiment_name |
58 |
) |
|
59 |
} |
|
60 |
) |
|
61 |
}, |
|
62 | ||
63 |
#' @description |
|
64 |
#' Get filter expression |
|
65 |
#' |
|
66 |
#' This functions returns filter calls equivalent to selected items |
|
67 |
#' within each of `filter_states`. Configuration of the calls is constant and |
|
68 |
#' depends on `filter_states` type and order which are set during initialization. |
|
69 |
#' This class contains multiple `FilterStates`: |
|
70 |
#' \itemize{ |
|
71 |
#' \item{`colData(dataset)`}{for this object single `MAEFilterStates` |
|
72 |
#' which returns `subsetByColData` call} |
|
73 |
#' \item{experiments}{for each experiment single `SEFilterStates` and |
|
74 |
#' `FilterStates_matrix`, both returns `subset` call} |
|
75 |
#' } |
|
76 |
#' @return filter `call` or `list` of filter calls |
|
77 |
get_call = function() { |
|
78 | 10x |
filter_call <- Filter( |
79 | 10x |
f = Negate(is.null), |
80 | 10x |
x = lapply( |
81 | 10x |
self$get_filter_states(), |
82 | 10x |
function(x) x$get_call() |
83 |
) |
|
84 |
) |
|
85 | 10x |
if (length(filter_call) == 0) { |
86 | 5x |
return(NULL) |
87 |
} |
|
88 | 5x |
filter_call |
89 |
}, |
|
90 | ||
91 |
#' @description |
|
92 |
#' Gets labels of variables in the data |
|
93 |
#' |
|
94 |
#' Variables are the column names of the data. |
|
95 |
#' Either, all labels must have been provided for all variables |
|
96 |
#' in `set_data` or `NULL`. |
|
97 |
#' |
|
98 |
#' @param variables (`character` vector) variables to get labels for; |
|
99 |
#' if `NULL`, for all variables in data |
|
100 |
#' @return (`character` or `NULL`) variable labels, `NULL` if `column_labels` |
|
101 |
#' attribute does not exist for the data |
|
102 |
get_varlabels = function(variables = NULL) { |
|
103 | 28x |
checkmate::assert_character(variables, null.ok = TRUE, any.missing = FALSE) |
104 | ||
105 | 28x |
labels <- vapply( |
106 | 28x |
X = SummarizedExperiment::colData(private$dataset), |
107 | 28x |
FUN.VALUE = character(1), |
108 | 28x |
FUN = function(x) { |
109 | 840x |
label <- attr(x, "label") |
110 | 840x |
if (length(label) != 1) { |
111 | 28x |
NA_character_ |
112 |
} else { |
|
113 | 2x |
label |
114 |
} |
|
115 |
} |
|
116 |
) |
|
117 | ||
118 | 28x |
if (is.null(labels)) { |
119 | ! |
return(NULL) |
120 |
} |
|
121 | 1x |
if (!is.null(variables)) labels <- labels[names(labels) %in% variables] |
122 | 28x |
labels |
123 |
}, |
|
124 | ||
125 |
#' @description |
|
126 |
#' Get filter overview rows of a dataset |
|
127 |
#' @param filtered_dataset (`MultiAssayExperiment`) object to calculate filter overview statistics on. |
|
128 |
#' @return (`matrix`) matrix of observations and subjects |
|
129 |
get_filter_overview_info = function(filtered_dataset = self$get_dataset()) { |
|
130 | 6x |
names_exps <- paste0("- ", names(self$get_dataset())) |
131 | 6x |
mae_and_exps <- c(self$get_dataname(), names_exps) |
132 | ||
133 | 6x |
df <- cbind( |
134 | 6x |
private$get_filter_overview_nobs(filtered_dataset), |
135 | 6x |
self$get_filter_overview_nsubjs(filtered_dataset) |
136 |
) |
|
137 | ||
138 | 6x |
rownames(df) <- mae_and_exps |
139 | 6x |
colnames(df) <- c("Obs", "Subjects") |
140 | ||
141 | 6x |
df |
142 |
}, |
|
143 | ||
144 |
#' @description |
|
145 |
#' Gets variable names for the filtering. |
|
146 |
#' |
|
147 |
#' @return (`character(0)`) |
|
148 |
get_filterable_varnames = function() { |
|
149 | 1x |
character(0) |
150 |
}, |
|
151 | ||
152 |
#' @description |
|
153 |
#' Set filter state |
|
154 |
#' |
|
155 |
#' @param state (`named list`)\cr |
|
156 |
#' names of the list should correspond to the names of the initialized `FilterStates` |
|
157 |
#' kept in `private$filter_states`. For this object they are `"subjects"` and |
|
158 |
#' names of the experiments. Values of initial state should be relevant |
|
159 |
#' to the referred column. |
|
160 |
#' @param ... ignored. |
|
161 |
#' @examples |
|
162 |
#' utils::data(miniACC, package = "MultiAssayExperiment") |
|
163 |
#' dataset <- teal.slice:::MAEFilteredDataset$new(miniACC, "MAE") |
|
164 |
#' fs <- list( |
|
165 |
#' subjects = list( |
|
166 |
#' years_to_birth = list(selected = c(30, 50), keep_na = TRUE, keep_inf = FALSE), |
|
167 |
#' vital_status = list(selected = "1", keep_na = FALSE), |
|
168 |
#' gender = list(selected = "female", keep_na = TRUE) |
|
169 |
#' ), |
|
170 |
#' RPPAArray = list( |
|
171 |
#' subset = list(ARRAY_TYPE = list(selected = "", keep_na = TRUE)) |
|
172 |
#' ) |
|
173 |
#' ) |
|
174 |
#' shiny::isolate(dataset$set_filter_state(state = fs)) |
|
175 |
#' shiny::isolate(dataset$get_filter_state()) |
|
176 |
#' @return `NULL` |
|
177 |
set_filter_state = function(state, ...) { |
|
178 | 11x |
checkmate::assert_list(state) |
179 | 10x |
checkmate::assert_subset(names(state), c(names(self$get_filter_states()))) |
180 | ||
181 | 9x |
logger::log_trace( |
182 | 9x |
sprintf( |
183 | 9x |
"MAEFilteredDataset$set_filter_state setting up filters of variable %s, dataname: %s", |
184 | 9x |
paste(names(state), collapse = ", "), |
185 | 9x |
self$get_dataname() |
186 |
) |
|
187 |
) |
|
188 | 9x |
data <- self$get_dataset() |
189 | 9x |
for (fs_name in names(state)) { |
190 | 17x |
fs <- self$get_filter_states()[[fs_name]] |
191 | 17x |
fs$set_filter_state( |
192 | 17x |
state = state[[fs_name]], |
193 | 17x |
data = `if`(fs_name == "subjects", data, data[[fs_name]]) |
194 |
) |
|
195 |
} |
|
196 | ||
197 | 8x |
logger::log_trace( |
198 | 8x |
sprintf( |
199 | 8x |
"MAEFilteredDataset$set_filter_state done setting filters of variable %s, dataname: %s", |
200 | 8x |
paste(names(state), collapse = ", "), |
201 | 8x |
self$get_dataname() |
202 |
) |
|
203 |
) |
|
204 | 8x |
NULL |
205 |
}, |
|
206 | ||
207 |
#' @description Remove one or more `FilterState` of a `MAEFilteredDataset` |
|
208 |
#' |
|
209 |
#' @param state_id (`list`)\cr |
|
210 |
#' Named list of variables to remove their `FilterState`. |
|
211 |
#' |
|
212 |
#' @return `NULL` |
|
213 |
#' |
|
214 |
remove_filter_state = function(state_id) { |
|
215 | 2x |
checkmate::assert_list(state_id, names = "unique") |
216 | 1x |
checkmate::assert_subset(names(state_id), c(names(self$get_filter_states()))) |
217 | ||
218 | 1x |
logger::log_trace( |
219 | 1x |
sprintf( |
220 | 1x |
"MAEFilteredDataset$remove_filter_state removing filters of variable %s, dataname: %s", |
221 | 1x |
state_id, |
222 | 1x |
self$get_dataname() |
223 |
) |
|
224 |
) |
|
225 | ||
226 | 1x |
for (fs_name in names(state_id)) { |
227 | 1x |
fdata_filter_state <- self$get_filter_states()[[fs_name]] |
228 | 1x |
fdata_filter_state$remove_filter_state( |
229 | 1x |
`if`(fs_name == "subjects", state_id[[fs_name]][[1]], state_id[[fs_name]]) |
230 |
) |
|
231 |
} |
|
232 | 1x |
logger::log_trace( |
233 | 1x |
sprintf( |
234 | 1x |
"MAEFilteredDataset$remove_filter_state done removing filters of variable %s, dataname: %s", |
235 | 1x |
state_id, |
236 | 1x |
self$get_dataname() |
237 |
) |
|
238 |
) |
|
239 | 1x |
invisible(NULL) |
240 |
}, |
|
241 | ||
242 |
#' @description |
|
243 |
#' UI module to add filter variable for this dataset |
|
244 |
#' |
|
245 |
#' UI module to add filter variable for this dataset |
|
246 |
#' @param id (`character(1)`)\cr |
|
247 |
#' identifier of the element - preferably containing dataset name |
|
248 |
#' |
|
249 |
#' @return function - shiny UI module |
|
250 |
ui_add_filter_state = function(id) { |
|
251 | ! |
ns <- NS(id) |
252 | ! |
data <- self$get_dataset() |
253 | ! |
experiment_names <- names(data) |
254 | ||
255 | ! |
div( |
256 | ! |
tags$label("Add", tags$code(self$get_dataname()), "filter"), |
257 | ! |
br(), |
258 | ! |
HTML("►"), |
259 | ! |
tags$label("Add subjects filter"), |
260 | ! |
self$get_filter_states("subjects")$ui_add_filter_state( |
261 | ! |
id = ns("subjects"), |
262 | ! |
data = data |
263 |
), |
|
264 | ! |
tagList( |
265 | ! |
lapply( |
266 | ! |
experiment_names, |
267 | ! |
function(experiment_name) { |
268 | ! |
tagList( |
269 | ! |
HTML("►"), |
270 | ! |
tags$label("Add", tags$code(experiment_name), "filter"), |
271 | ! |
self$get_filter_states(experiment_name)$ui_add_filter_state( |
272 | ! |
id = ns(experiment_name), |
273 | ! |
data = data[[experiment_name]] |
274 |
) |
|
275 |
) |
|
276 |
} |
|
277 |
) |
|
278 |
) |
|
279 |
) |
|
280 |
}, |
|
281 | ||
282 |
#' @description |
|
283 |
#' Server module to add filter variable for this dataset |
|
284 |
#' |
|
285 |
#' Server module to add filter variable for this dataset. |
|
286 |
#' For this class `srv_add_filter_state` calls multiple modules |
|
287 |
#' of the same name from `FilterStates` as `MAEFilteredDataset` |
|
288 |
#' contains one `FilterStates` object for `colData` and one for each |
|
289 |
#' experiment. |
|
290 |
#' |
|
291 |
#' @param id (`character(1)`)\cr |
|
292 |
#' an ID string that corresponds with the ID used to call the module's UI function. |
|
293 |
#' @param ... ignored. |
|
294 |
#' @return `moduleServer` function which returns `NULL` |
|
295 |
srv_add_filter_state = function(id, ...) { |
|
296 | ! |
moduleServer( |
297 | ! |
id = id, |
298 | ! |
function(input, output, session) { |
299 | ! |
logger::log_trace(paste( |
300 | ! |
"MAEFilteredDataset$srv_add_filter_state initializing,", |
301 | ! |
"dataname: { deparse1(self$get_dataname()) }" |
302 |
)) |
|
303 | ! |
data <- self$get_dataset() |
304 | ! |
self$get_filter_states("subjects")$srv_add_filter_state( |
305 | ! |
id = "subjects", |
306 | ! |
data = data # MultiAssayExperiment |
307 |
# ignoring vars_include |
|
308 |
) |
|
309 | ||
310 | ! |
experiment_names <- names(data) |
311 | ! |
lapply( |
312 | ! |
experiment_names, |
313 | ! |
function(experiment_name) { |
314 | ! |
self$get_filter_states(experiment_name)$srv_add_filter_state( |
315 | ! |
id = experiment_name, |
316 | ! |
data = data[[experiment_name]] # SummarizedExperiment or matrix |
317 |
# ignoring vars_include |
|
318 |
) |
|
319 |
} |
|
320 |
) |
|
321 | ! |
logger::log_trace(paste( |
322 | ! |
"MAEFilteredDataset$srv_add_filter_state initialized,", |
323 | ! |
"dataname: { deparse1(self$get_dataname()) }" |
324 |
)) |
|
325 | ! |
NULL |
326 |
} |
|
327 |
) |
|
328 |
}, |
|
329 | ||
330 |
#' @description |
|
331 |
#' Gets filter overview subjects number |
|
332 |
#' @param filtered_dataset (`MultiAssayExperiment`) object to calculate filter overview statistics on. |
|
333 |
#' @param subject_keys (unused) in `MultiAssayExperiment` unique subjects are the rows of `colData` slot. |
|
334 |
#' @return `list` with the number of subjects of filtered/non-filtered datasets. |
|
335 |
get_filter_overview_nsubjs = function(filtered_dataset = self$get_dataset(), subject_keys) { |
|
336 | 10x |
data_f <- filtered_dataset |
337 | 10x |
data_nf <- self$get_dataset() |
338 | 10x |
experiment_names <- names(data_nf) |
339 | ||
340 | 10x |
data_f_subjects_info <- nrow(SummarizedExperiment::colData(data_f)) |
341 | 10x |
data_nf_subjects_info <- nrow(SummarizedExperiment::colData(data_nf)) |
342 | 10x |
mae_total_subjects_info <- paste0(data_f_subjects_info, "/", data_nf_subjects_info) |
343 | ||
344 | 10x |
get_experiment_rows <- function(mae, experiment) { |
345 | 100x |
sample_subset <- subset(MultiAssayExperiment::sampleMap(mae), colname %in% colnames(experiment)) |
346 | 100x |
length(unique(sample_subset$primary)) |
347 |
} |
|
348 | ||
349 | 10x |
subjects_info <- lapply( |
350 | 10x |
experiment_names, |
351 | 10x |
function(experiment_name) { |
352 | 50x |
subjects_f_rows <- get_experiment_rows(data_f, data_f[[experiment_name]]) |
353 | 50x |
subjects_nf_rows <- get_experiment_rows(data_nf, data_nf[[experiment_name]]) |
354 | ||
355 | 50x |
subjects_info <- paste0(subjects_f_rows, "/", subjects_nf_rows) |
356 | 50x |
subjects_info |
357 |
} |
|
358 |
) |
|
359 | ||
360 | 10x |
append( |
361 | 10x |
list(mae_total_subjects_info), |
362 | 10x |
subjects_info |
363 |
) |
|
364 |
} |
|
365 |
), |
|
366 | ||
367 |
# private members ---- |
|
368 |
private = list( |
|
369 | ||
370 |
# Gets filter overview observations number and returns a |
|
371 |
# list of the number of observations of filtered/non-filtered datasets |
|
372 |
get_filter_overview_nobs = function(filtered_dataset) { |
|
373 | 6x |
data_f <- filtered_dataset |
374 | 6x |
data_nf <- self$get_dataset() |
375 | 6x |
experiment_names <- names(data_nf) |
376 | 6x |
mae_total_data_info <- "" |
377 | ||
378 | 6x |
data_info <- lapply( |
379 | 6x |
experiment_names, |
380 | 6x |
function(experiment_name) { |
381 | 30x |
data_f_rows <- ncol(data_f[[experiment_name]]) |
382 | 30x |
data_nf_rows <- ncol(data_nf[[experiment_name]]) |
383 | ||
384 | 30x |
data_info <- paste0(data_f_rows, "/", data_nf_rows) |
385 | 30x |
data_info |
386 |
} |
|
387 |
) |
|
388 | ||
389 | 6x |
append( |
390 | 6x |
list(mae_total_data_info), |
391 | 6x |
data_info |
392 |
) |
|
393 |
} |
|
394 |
) |
|
395 |
) |
1 |
#' @name EmptyFilterState |
|
2 |
#' @title `FilterState` object for empty variable |
|
3 |
#' @description `FilterState` subclass representing an empty variable |
|
4 |
#' @docType class |
|
5 |
#' @keywords internal |
|
6 |
#' |
|
7 |
#' |
|
8 |
#' @examples |
|
9 |
#' filter_state <- teal.slice:::EmptyFilterState$new( |
|
10 |
#' NA, |
|
11 |
#' varname = "x", |
|
12 |
#' dataname = "data", |
|
13 |
#' extract_type = character(0) |
|
14 |
#' ) |
|
15 |
#' isolate(filter_state$get_call()) |
|
16 |
#' isolate(filter_state$set_selected(TRUE)) |
|
17 |
#' isolate(filter_state$set_keep_na(TRUE)) |
|
18 |
#' isolate(filter_state$get_call()) |
|
19 |
#' |
|
20 |
EmptyFilterState <- R6::R6Class( # nolint |
|
21 |
"EmptyFilterState", |
|
22 |
inherit = FilterState, |
|
23 | ||
24 |
# public methods ---- |
|
25 |
public = list( |
|
26 |
#' @description |
|
27 |
#' Initialize `EmptyFilterState` object. |
|
28 |
#' |
|
29 |
#' @param x (`vector`)\cr |
|
30 |
#' values of the variable used in filter |
|
31 |
#' @param varname (`character`, `name`)\cr |
|
32 |
#' name of the variable |
|
33 |
#' @param varlabel (`character(1)`)\cr |
|
34 |
#' label of the variable (optional). |
|
35 |
#' @param dataname (`character(1)`)\cr |
|
36 |
#' optional name of dataset where `x` is taken from |
|
37 |
#' @param extract_type (`character(0)`, `character(1)`)\cr |
|
38 |
#' whether condition calls should be prefixed by dataname. Possible values: |
|
39 |
#' \itemize{ |
|
40 |
#' \item{`character(0)` (default)}{ `varname` in the condition call will not be prefixed} |
|
41 |
#' \item{`"list"`}{ `varname` in the condition call will be returned as `<dataname>$<varname>`} |
|
42 |
#' \item{`"matrix"`}{ `varname` in the condition call will be returned as `<dataname>[, <varname>]`} |
|
43 |
#' } |
|
44 |
#' |
|
45 |
initialize = function(x, |
|
46 |
varname, |
|
47 |
varlabel = character(0), |
|
48 |
dataname = NULL, |
|
49 |
extract_type = character(0)) { |
|
50 | 6x |
super$initialize(x, varname, varlabel, dataname, extract_type) |
51 | 6x |
private$set_choices(list()) |
52 | 6x |
self$set_selected(list()) |
53 | ||
54 | 6x |
return(invisible(self)) |
55 |
}, |
|
56 | ||
57 |
#' @description |
|
58 |
#' Reports whether the current state filters out any values.(?) |
|
59 |
#' |
|
60 |
#' @return `logical(1)` |
|
61 |
#' |
|
62 |
is_any_filtered = function() { |
|
63 | 2x |
!isTRUE(self$get_keep_na()) |
64 |
}, |
|
65 | ||
66 |
#' @description |
|
67 |
#' Returns reproducible condition call for current selection relevant |
|
68 |
#' for selected variable type. |
|
69 |
#' Uses internal reactive values, hence must be called |
|
70 |
#' in reactive or isolated context. |
|
71 |
#' |
|
72 |
#' @return `logical(1)` |
|
73 |
#' |
|
74 |
get_call = function() { |
|
75 | 2x |
filter_call <- if (isTRUE(self$get_keep_na())) { |
76 | 1x |
call("is.na", private$get_varname_prefixed()) |
77 |
} else { |
|
78 | 1x |
FALSE |
79 |
} |
|
80 |
}, |
|
81 | ||
82 |
#' @description |
|
83 |
#' Returns the filtering state. |
|
84 |
#' |
|
85 |
#' @return `list` containing values taken from the reactive fields: |
|
86 |
#' * `keep_na` (`logical(1)`) whether `NA` should be kept. |
|
87 |
#' |
|
88 |
get_state = function() { |
|
89 | 1x |
list( |
90 | 1x |
keep_na = self$get_keep_na() |
91 |
) |
|
92 |
}, |
|
93 | ||
94 |
#' @description |
|
95 |
#' Set state. |
|
96 |
#' |
|
97 |
#' @param state (`list`)\cr |
|
98 |
#' contains fields relevant for specific class: |
|
99 |
#' \itemize{ |
|
100 |
#' \item{`keep_na` (`logical`)}{ defines whether to keep or remove `NA` values} |
|
101 |
#' } |
|
102 |
#' |
|
103 |
#' @return NULL invisibly |
|
104 |
set_state = function(state) { |
|
105 | 4x |
if (!is.null(state$selected)) { |
106 | 1x |
stop( |
107 | 1x |
sprintf( |
108 | 1x |
"All values in variable '%s' are `NA`. Unable to apply filter values \n %s", |
109 | 1x |
private$varname, |
110 | 1x |
paste(state$selected, collapse = ", ") |
111 |
) |
|
112 |
) |
|
113 |
} |
|
114 | 3x |
stopifnot(is.list(state) && all(names(state) == "keep_na")) |
115 | 2x |
if (!is.null(state$keep_na)) { |
116 | 2x |
self$set_keep_na(state$keep_na) |
117 |
} |
|
118 | 2x |
invisible(NULL) |
119 |
} |
|
120 |
), |
|
121 | ||
122 |
# private members ---- |
|
123 |
private = list( |
|
124 |
# @description |
|
125 |
# UI Module for `EmptyFilterState`. |
|
126 |
# This UI element contains a checkbox input to filter or keep missing values. |
|
127 |
# |
|
128 |
# @param id (`character(1)`)\cr |
|
129 |
# shiny element (module instance) id |
|
130 |
# |
|
131 |
ui_inputs = function(id) { |
|
132 | ! |
ns <- NS(id) |
133 | ! |
fluidRow( |
134 | ! |
div( |
135 | ! |
class = "relative", |
136 | ! |
div( |
137 | ! |
span("Variable contains missing values only"), |
138 | ! |
private$keep_na_ui(ns("keep_na")) |
139 |
) |
|
140 |
) |
|
141 |
) |
|
142 |
}, |
|
143 | ||
144 |
# @description |
|
145 |
# Controls state of the `keep_na` checkbox input. |
|
146 |
# |
|
147 |
# @param id (`character(1)`)\cr |
|
148 |
# shiny module instance id |
|
149 |
# |
|
150 |
# @return `moduleServer` function which returns `NULL` |
|
151 |
# |
|
152 |
server_inputs = function(id) { |
|
153 | ! |
moduleServer( |
154 | ! |
id = id, |
155 | ! |
function(input, output, session) { |
156 | ! |
private$keep_na_srv("keep_na") |
157 |
} |
|
158 |
) |
|
159 |
} |
|
160 |
) |
|
161 |
) |
1 |
#' @name DateFilterState |
|
2 |
#' @title `FilterState` object for Date variable |
|
3 |
#' @description Manages choosing a range of Dates |
|
4 |
#' @docType class |
|
5 |
#' @keywords internal |
|
6 |
#' |
|
7 |
#' |
|
8 |
#' @examples |
|
9 |
#' filter_state <- teal.slice:::DateFilterState$new( |
|
10 |
#' c(Sys.Date() + seq(1:10), NA), |
|
11 |
#' varname = "x", |
|
12 |
#' dataname = "data", |
|
13 |
#' extract_type = character(0) |
|
14 |
#' ) |
|
15 |
#' isolate(filter_state$get_call()) |
|
16 |
#' |
|
17 |
#' isolate(filter_state$set_selected(c(Sys.Date() + 3L, Sys.Date() + 8L))) |
|
18 |
#' isolate(filter_state$set_keep_na(TRUE)) |
|
19 |
#' isolate(filter_state$get_call()) |
|
20 |
#' |
|
21 |
#' \dontrun{ |
|
22 |
#' # working filter in an app |
|
23 |
#' library(shiny) |
|
24 |
#' |
|
25 |
#' dates <- c(Sys.Date() - 100, Sys.Date()) |
|
26 |
#' data_date <- c(seq(from = dates[1], to = dates[2], length.out = 100), NA) |
|
27 |
#' filter_state_date <- DateFilterState$new( |
|
28 |
#' x = data_date, |
|
29 |
#' varname = "variable", |
|
30 |
#' varlabel = "label" |
|
31 |
#' ) |
|
32 |
#' filter_state_date$set_state(list(selected = data_date[c(47, 98)], keep_na = TRUE)) |
|
33 |
#' |
|
34 |
#' ui <- fluidPage( |
|
35 |
#' column(4, div( |
|
36 |
#' h4("DateFilterState"), |
|
37 |
#' isolate(filter_state_date$ui("fs")) |
|
38 |
#' )), |
|
39 |
#' column(4, div( |
|
40 |
#' id = "outputs", # div id is needed for toggling the element |
|
41 |
#' h4("Condition (i.e. call)"), # display the subsetting call generated by this FilterState |
|
42 |
#' textOutput("condition_date"), br(), |
|
43 |
#' h4("Unformatted state"), # display raw filter state |
|
44 |
#' textOutput("unformatted_date"), br(), |
|
45 |
#' h4("Formatted state"), # display human readable filter state |
|
46 |
#' textOutput("formatted_date"), br() |
|
47 |
#' )), |
|
48 |
#' column(4, div( |
|
49 |
#' h4("Programmatic filter control"), |
|
50 |
#' actionButton("button1_date", "set drop NA", width = "100%"), br(), |
|
51 |
#' actionButton("button2_date", "set keep NA", width = "100%"), br(), |
|
52 |
#' actionButton("button3_date", "set a range", width = "100%"), br(), |
|
53 |
#' actionButton("button4_date", "set full range", width = "100%"), br(), |
|
54 |
#' actionButton("button0_date", "set initial state", width = "100%"), br() |
|
55 |
#' )) |
|
56 |
#' ) |
|
57 |
#' |
|
58 |
#' server <- function(input, output, session) { |
|
59 |
#' filter_state_date$server("fs") |
|
60 |
#' output$condition_date <- renderPrint(filter_state_date$get_call()) |
|
61 |
#' output$formatted_date <- renderText(filter_state_date$format()) |
|
62 |
#' output$unformatted_date <- renderPrint(filter_state_date$get_state()) |
|
63 |
#' # modify filter state programmatically |
|
64 |
#' observeEvent(input$button1_date, filter_state_date$set_keep_na(FALSE)) |
|
65 |
#' observeEvent(input$button2_date, filter_state_date$set_keep_na(TRUE)) |
|
66 |
#' observeEvent( |
|
67 |
#' input$button3_date, |
|
68 |
#' filter_state_date$set_selected(data_date[c(34, 56)]) |
|
69 |
#' ) |
|
70 |
#' observeEvent(input$button4_date, filter_state_date$set_selected(dates)) |
|
71 |
#' observeEvent( |
|
72 |
#' input$button0_date, |
|
73 |
#' filter_state_date$set_state(list(selected = data_date[c(47, 98)], keep_na = TRUE)) |
|
74 |
#' ) |
|
75 |
#' } |
|
76 |
#' |
|
77 |
#' if (interactive()) { |
|
78 |
#' shinyApp(ui, server) |
|
79 |
#' } |
|
80 |
#' } |
|
81 |
#' |
|
82 |
DateFilterState <- R6::R6Class( # nolint |
|
83 |
"DateFilterState", |
|
84 |
inherit = FilterState, |
|
85 | ||
86 |
# public methods ---- |
|
87 | ||
88 |
public = list( |
|
89 | ||
90 |
#' @description |
|
91 |
#' Initialize a `FilterState` object |
|
92 |
#' @param x (`Date`)\cr |
|
93 |
#' values of the variable used in filter |
|
94 |
#' @param varname (`character`, `name`)\cr |
|
95 |
#' name of the variable |
|
96 |
#' @param varlabel (`character(1)`)\cr |
|
97 |
#' label of the variable (optional). |
|
98 |
#' @param dataname (`character(1)`)\cr |
|
99 |
#' optional name of dataset where `x` is taken from |
|
100 |
#' @param extract_type (`character(0)`, `character(1)`)\cr |
|
101 |
#' whether condition calls should be prefixed by dataname. Possible values: |
|
102 |
#' \itemize{ |
|
103 |
#' \item{`character(0)` (default)}{ `varname` in the condition call will not be prefixed} |
|
104 |
#' \item{`"list"`}{ `varname` in the condition call will be returned as `<dataname>$<varname>`} |
|
105 |
#' \item{`"matrix"`}{ `varname` in the condition call will be returned as `<dataname>[, <varname>]`} |
|
106 |
#' } |
|
107 |
initialize = function(x, |
|
108 |
varname, |
|
109 |
varlabel = character(0), |
|
110 |
dataname = NULL, |
|
111 |
extract_type = character(0)) { |
|
112 | 20x |
stopifnot(is(x, "Date")) |
113 | 20x |
super$initialize(x, varname, varlabel, dataname, extract_type) |
114 | ||
115 | 20x |
var_range <- range(x, na.rm = TRUE) |
116 | 20x |
private$set_choices(var_range) |
117 | 20x |
self$set_selected(var_range) |
118 | ||
119 | 20x |
return(invisible(self)) |
120 |
}, |
|
121 | ||
122 |
#' @description |
|
123 |
#' Returns a formatted string representing this `DateFilterState`. |
|
124 |
#' |
|
125 |
#' @param indent (`numeric(1)`) the number of spaces before after each new line character of the formatted string. |
|
126 |
#' Default: 0 |
|
127 |
#' @return `character(1)` the formatted string |
|
128 |
#' |
|
129 |
format = function(indent = 0) { |
|
130 | 6x |
checkmate::assert_number(indent, finite = TRUE, lower = 0) |
131 | ||
132 | 5x |
vals <- self$get_selected() |
133 | 5x |
sprintf( |
134 | 5x |
"%sFiltering on: %s\n%1$s Selected range: %s - %s\n%1$s Include missing values: %s", |
135 | 5x |
format("", width = indent), |
136 | 5x |
private$varname, |
137 | 5x |
format(vals[1], nsmall = 3), |
138 | 5x |
format(vals[2], nsmall = 3), |
139 | 5x |
format(self$get_keep_na()) |
140 |
) |
|
141 |
}, |
|
142 | ||
143 |
#' @description |
|
144 |
#' Answers the question of whether the current settings and values selected actually filters out any values. |
|
145 |
#' @return logical scalar |
|
146 |
is_any_filtered = function() { |
|
147 | 8x |
if (!setequal(self$get_selected(), private$choices)) { |
148 | 6x |
TRUE |
149 | 2x |
} else if (!isTRUE(self$get_keep_na()) && private$na_count > 0) { |
150 | 1x |
TRUE |
151 |
} else { |
|
152 | 1x |
FALSE |
153 |
} |
|
154 |
}, |
|
155 | ||
156 |
#' @description |
|
157 |
#' Returns reproducible condition call for current selection. |
|
158 |
#' For this class returned call looks like |
|
159 |
#' `<varname> >= <min value> & <varname> <= <max value>` with |
|
160 |
#' optional `is.na(<varname>)`. |
|
161 |
#' @return (`call`) |
|
162 |
get_call = function() { |
|
163 | 9x |
choices <- as.character(self$get_selected()) |
164 | 9x |
filter_call <- |
165 | 9x |
call( |
166 |
"&", |
|
167 | 9x |
call(">=", private$get_varname_prefixed(), call("as.Date", choices[1L])), |
168 | 9x |
call("<=", private$get_varname_prefixed(), call("as.Date", choices[2L])) |
169 |
) |
|
170 | 9x |
private$add_keep_na_call(filter_call) |
171 |
}, |
|
172 | ||
173 |
#' @description |
|
174 |
#' Sets the selected time frame of this `DateFilterState`. |
|
175 |
#' |
|
176 |
#' @param value (`Date(2)`) the lower and the upper bound of the selected |
|
177 |
#' time frame. Must not contain NA values. |
|
178 |
#' |
|
179 |
#' @return invisibly `NULL`. |
|
180 |
#' |
|
181 |
#' @note Casts the passed object to `Date` before validating the input |
|
182 |
#' making it possible to pass any object coercible to `Date` to this method. |
|
183 |
#' |
|
184 |
#' @examples |
|
185 |
#' date <- as.Date("13/09/2021") |
|
186 |
#' filter <- teal.slice:::DateFilterState$new( |
|
187 |
#' c(date, date + 1, date + 2, date + 3), |
|
188 |
#' varname = "name" |
|
189 |
#' ) |
|
190 |
#' filter$set_selected(c(date + 1, date + 2)) |
|
191 |
set_selected = function(value) { |
|
192 | 42x |
super$set_selected(value) |
193 |
} |
|
194 |
), |
|
195 | ||
196 |
# private methods ---- |
|
197 | ||
198 |
private = list( |
|
199 |
validate_selection = function(value) { |
|
200 | 41x |
if (!is(value, "Date")) { |
201 | ! |
stop( |
202 | ! |
sprintf( |
203 | ! |
"value of the selection for `%s` in `%s` should be a Date", |
204 | ! |
self$get_varname(), |
205 | ! |
self$get_dataname() |
206 |
) |
|
207 |
) |
|
208 |
} |
|
209 | 41x |
pre_msg <- sprintf( |
210 | 41x |
"dataset '%s', variable '%s': ", |
211 | 41x |
self$get_dataname(), |
212 | 41x |
self$get_varname() |
213 |
) |
|
214 | 41x |
check_in_range(value, private$choices, pre_msg = pre_msg) |
215 |
}, |
|
216 |
cast_and_validate = function(values) { |
|
217 | 42x |
tryCatch( |
218 | 42x |
expr = { |
219 | 42x |
values <- as.Date(values) |
220 | ! |
if (any(is.na(values))) stop() |
221 |
}, |
|
222 | 42x |
error = function(error) stop("The array of set values must contain values coercible to Date.") |
223 |
) |
|
224 | ! |
if (length(values) != 2) stop("The array of set values must have length two.") |
225 | 41x |
values |
226 |
}, |
|
227 |
remove_out_of_bound_values = function(values) { |
|
228 | 41x |
if (values[1] < private$choices[1]) { |
229 | 4x |
warning(paste( |
230 | 4x |
"Value:", values[1], "is outside of the possible range for column", private$varname, |
231 | 4x |
"of dataset", private$dataname, "." |
232 |
)) |
|
233 | 4x |
values[1] <- private$choices[1] |
234 |
} |
|
235 | ||
236 | 41x |
if (values[2] > private$choices[2]) { |
237 | 4x |
warning(paste( |
238 | 4x |
"Value:", values[2], "is outside of the possible range for column", private$varname, |
239 | 4x |
"of dataset", private$dataname, "." |
240 |
)) |
|
241 | 4x |
values[2] <- private$choices[2] |
242 |
} |
|
243 | 41x |
values |
244 |
}, |
|
245 | ||
246 |
# shiny modules ---- |
|
247 | ||
248 |
# @description |
|
249 |
# UI Module for `DateFilterState`. |
|
250 |
# This UI element contains two date selections for `min` and `max` |
|
251 |
# of the range and a checkbox whether to keep the `NA` values. |
|
252 |
# @param id (`character(1)`)\cr |
|
253 |
# id of shiny element |
|
254 |
ui_inputs = function(id) { |
|
255 | ! |
ns <- NS(id) |
256 | ! |
div( |
257 | ! |
div( |
258 | ! |
class = "flex", |
259 | ! |
actionButton( |
260 | ! |
class = "date_reset_button", |
261 | ! |
inputId = ns("start_date_reset"), |
262 | ! |
label = NULL, |
263 | ! |
icon = icon("fas fa-undo") |
264 |
), |
|
265 | ! |
div( |
266 | ! |
class = "w-80 filter_datelike_input", |
267 | ! |
dateRangeInput( |
268 | ! |
inputId = ns("selection"), |
269 | ! |
label = NULL, |
270 | ! |
start = self$get_selected()[1], |
271 | ! |
end = self$get_selected()[2], |
272 | ! |
min = private$choices[1], |
273 | ! |
max = private$choices[2], |
274 | ! |
width = "100%" |
275 |
) |
|
276 |
), |
|
277 | ! |
actionButton( |
278 | ! |
class = "date_reset_button", |
279 | ! |
inputId = ns("end_date_reset"), |
280 | ! |
label = NULL, |
281 | ! |
icon = icon("fas fa-undo") |
282 |
) |
|
283 |
), |
|
284 | ! |
private$keep_na_ui(ns("keep_na")) |
285 |
) |
|
286 |
}, |
|
287 | ||
288 |
# @description |
|
289 |
# Server module |
|
290 |
# @param id (`character(1)`)\cr |
|
291 |
# an ID string that corresponds with the ID used to call the module's UI function. |
|
292 |
# @return `moduleServer` function which returns `NULL` |
|
293 |
server_inputs = function(id) { |
|
294 | ! |
moduleServer( |
295 | ! |
id = id, |
296 | ! |
function(input, output, session) { |
297 | ! |
logger::log_trace("DateFilterState$server initializing, dataname: { private$dataname }") |
298 | ||
299 |
# this observer is needed in the situation when private$selected has been |
|
300 |
# changed directly by the api - then it's needed to rerender UI element |
|
301 |
# to show relevant values |
|
302 | ! |
private$observers$seletion_api <- observeEvent( |
303 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
304 | ! |
ignoreInit = TRUE, |
305 | ! |
eventExpr = self$get_selected(), |
306 | ! |
handlerExpr = { |
307 | ! |
if (!setequal(self$get_selected(), input$selection)) { |
308 | ! |
updateDateRangeInput( |
309 | ! |
session = session, |
310 | ! |
inputId = "selection", |
311 | ! |
start = self$get_selected()[1], |
312 | ! |
end = self$get_selected()[2] |
313 |
) |
|
314 | ! |
logger::log_trace(sprintf( |
315 | ! |
"DateFilterState$server@1 selection of variable %s changed, dataname: %s", |
316 | ! |
private$varname, |
317 | ! |
private$dataname |
318 |
)) |
|
319 |
} |
|
320 |
} |
|
321 |
) |
|
322 | ||
323 | ! |
private$observers$selection <- observeEvent( |
324 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
325 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
326 | ! |
eventExpr = input$selection, |
327 | ! |
handlerExpr = { |
328 | ! |
start_date <- input$selection[1] |
329 | ! |
end_date <- input$selection[2] |
330 | ||
331 | ! |
self$set_selected(c(start_date, end_date)) |
332 | ! |
logger::log_trace(sprintf( |
333 | ! |
"DateFilterState$server@2 selection of variable %s changed, dataname: %s", |
334 | ! |
private$varname, |
335 | ! |
private$dataname |
336 |
)) |
|
337 |
} |
|
338 |
) |
|
339 | ||
340 | ||
341 | ! |
private$keep_na_srv("keep_na") |
342 | ||
343 | ! |
private$observers$reset1 <- observeEvent(input$start_date_reset, { |
344 | ! |
updateDateRangeInput( |
345 | ! |
session = session, |
346 | ! |
inputId = "selection", |
347 | ! |
start = private$choices[1] |
348 |
) |
|
349 | ! |
logger::log_trace(sprintf( |
350 | ! |
"DateFilterState$server@3 reset start date of variable %s, dataname: %s", |
351 | ! |
private$varname, |
352 | ! |
private$dataname |
353 |
)) |
|
354 |
}) |
|
355 | ||
356 | ! |
private$observers$reset2 <- observeEvent(input$end_date_reset, { |
357 | ! |
updateDateRangeInput( |
358 | ! |
session = session, |
359 | ! |
inputId = "selection", |
360 | ! |
end = private$choices[2] |
361 |
) |
|
362 | ! |
logger::log_trace(sprintf( |
363 | ! |
"DateFilterState$server@4 reset end date of variable %s, dataname: %s", |
364 | ! |
private$varname, |
365 | ! |
private$dataname |
366 |
)) |
|
367 |
}) |
|
368 | ! |
logger::log_trace("DateFilterState$server initialized, dataname: { private$dataname }") |
369 | ! |
NULL |
370 |
} |
|
371 |
) |
|
372 |
} |
|
373 |
) |
|
374 |
) |
1 |
#' Ensure the ellipsis, ..., in method arguments are empty |
|
2 |
#' |
|
3 |
#' Ellipsis, ..., are needed as part of method arguments to allow for its arguments to be different from its generic's |
|
4 |
#' arguments and for this to pass check(). Hence, ..., should always be empty. This function will check for this |
|
5 |
#' condition. |
|
6 |
#' |
|
7 |
#' @param ... it should literally just be ... |
|
8 |
#' @param stop TRUE to raise an error; FALSE will output warning message |
|
9 |
#' @param allowed_args character vector naming arguments that are allowed in the \code{...}. |
|
10 |
#' to allow for unnamed arguments, let "" be one of the elements in this character vector. |
|
11 |
#' |
|
12 |
#' @return \code{NULL} if ... is empty |
|
13 |
#' |
|
14 |
#' @keywords internal |
|
15 |
#' |
|
16 |
#' @examples |
|
17 |
#' method.class <- function(a, b, c, ...) { |
|
18 |
#' check_ellipsis(...) |
|
19 |
#' } |
|
20 |
#' method.class <- function(a, b, c, ...) { |
|
21 |
#' check_ellipsis(..., allowed_args = c("y", "z")) |
|
22 |
#' } |
|
23 |
check_ellipsis <- function(..., stop = FALSE, allowed_args = character(0)) { |
|
24 | 37x |
if (!missing(...)) { |
25 | 24x |
checkmate::assert_flag(stop) |
26 | 24x |
checkmate::assert_character(allowed_args, min.len = 0, null.ok = TRUE, any.missing = FALSE) |
27 | 24x |
args <- list(...) |
28 | 24x |
arg_names <- names(args) |
29 | 24x |
if (is.null(arg_names)) { |
30 | 4x |
arg_names <- rep("", length(args)) |
31 |
} |
|
32 | 24x |
extra_args <- arg_names[!is.element(arg_names, allowed_args)] |
33 | 24x |
if (length(extra_args) == 0) { |
34 | 12x |
return(invisible(NULL)) |
35 |
} |
|
36 | 12x |
message <- paste(length(extra_args), "total unused argument(s).") |
37 | ||
38 | 12x |
named_extra_args <- extra_args[!vapply(extra_args, identical, logical(1), "")] |
39 | 12x |
if (length(named_extra_args) > 0) { |
40 | 9x |
message <- paste0( |
41 | 9x |
message, |
42 |
" ", |
|
43 | 9x |
length(named_extra_args), |
44 | 9x |
" with name(s): ", |
45 | 9x |
paste(named_extra_args, collapse = ", "), |
46 |
"." |
|
47 |
) |
|
48 |
} |
|
49 | 12x |
if (stop) { |
50 | 8x |
stop(message) |
51 |
} else { |
|
52 | 4x |
warning(message) |
53 |
} |
|
54 |
} |
|
55 |
} |
|
56 | ||
57 |
#' Whether the variable name is good to use within Show R Code |
|
58 |
#' |
|
59 |
#' Spaces are problematic because the variables must be escaped with backticks. |
|
60 |
#' Also, they should not start with a number as R may silently make it valid by changing it. |
|
61 |
#' Therefore, we only allow alphanumeric characters with underscores. |
|
62 |
#' The first character of the `name` must be an alphabetic character and can be followed by alphanumeric characters. |
|
63 |
#' |
|
64 |
#' @md |
|
65 |
#' |
|
66 |
#' @param name `character, single or vector` name to check |
|
67 |
#' @keywords internal |
|
68 |
#' |
|
69 |
#' @examples |
|
70 |
#' teal.slice:::check_simple_name("aas2df") |
|
71 |
#' teal.slice:::check_simple_name("ADSL") |
|
72 |
#' teal.slice:::check_simple_name("ADSLmodified") |
|
73 |
#' teal.slice:::check_simple_name("ADSL_modified") |
|
74 |
#' teal.slice:::check_simple_name("ADSL_2") |
|
75 |
#' teal.slice:::check_simple_name("a1") |
|
76 |
#' # the following fail |
|
77 |
#' \dontrun{ |
|
78 |
#' teal.slice:::check_simple_name("1a") |
|
79 |
#' teal.slice:::check_simple_name("ADSL.modified") |
|
80 |
#' teal.slice:::check_simple_name("a1...") |
|
81 |
#' } |
|
82 |
check_simple_name <- function(name) { |
|
83 | 448x |
checkmate::assert_character(name, min.len = 1, any.missing = FALSE) |
84 | 448x |
if (!grepl("^[[:alpha:]][a-zA-Z0-9_]*$", name, perl = TRUE)) { |
85 | 5x |
stop( |
86 | 5x |
"name '", |
87 | 5x |
name, |
88 | 5x |
"' must only contain alphanumeric characters (with underscores)", |
89 | 5x |
" and the first character must be an alphabetic character" |
90 |
) |
|
91 |
} |
|
92 |
} |
|
93 | ||
94 |
#' Resolve the expected bootstrap theme |
|
95 |
#' @keywords internal |
|
96 |
get_teal_bs_theme <- function() { |
|
97 | 2x |
bs_theme <- getOption("teal.bs_theme") |
98 | 2x |
if (is.null(bs_theme)) { |
99 | 1x |
NULL |
100 | 1x |
} else if (!inherits(bs_theme, "bs_theme")) { |
101 | ! |
warning("teal.bs_theme has to be of a bslib::bs_theme class, the default shiny bootstrap is used.") |
102 | ! |
NULL |
103 |
} else { |
|
104 | 1x |
bs_theme |
105 |
} |
|
106 |
} |
1 | ||
2 |
#' Get classes of selected columns from dataset |
|
3 |
#' |
|
4 |
#' @param data (`data.frame`) data to determine variable types from |
|
5 |
#' @param columns (atomic vector of `character` or `NULL`) column names chosen from `data`. |
|
6 |
#' The value of `NULL` will be interpreted to mean all columns. |
|
7 |
#' |
|
8 |
#' @return (atomic vector of `character`) classes of `columns` from provided `data` |
|
9 |
#' @keywords internal |
|
10 |
#' @examples |
|
11 |
#' teal.slice:::variable_types( |
|
12 |
#' data.frame( |
|
13 |
#' x = 1:3, y = factor(c("a", "b", "a")), z = c("h1", "h2", "h3"), |
|
14 |
#' stringsAsFactors = FALSE |
|
15 |
#' ), |
|
16 |
#' "x" |
|
17 |
#' ) |
|
18 |
#' |
|
19 |
#' teal.slice:::variable_types( |
|
20 |
#' data.frame( |
|
21 |
#' x = 1:3, y = factor(c("a", "b", "a")), z = c("h1", "h2", "h3"), |
|
22 |
#' stringsAsFactors = FALSE |
|
23 |
#' ), |
|
24 |
#' c("x", "z") |
|
25 |
#' ) |
|
26 |
#' |
|
27 |
#' teal.slice:::variable_types( |
|
28 |
#' data.frame( |
|
29 |
#' x = 1:3, y = factor(c("a", "b", "a")), z = c("h1", "h2", "h3"), |
|
30 |
#' stringsAsFactors = FALSE |
|
31 |
#' ) |
|
32 |
#' ) |
|
33 |
variable_types <- function(data, columns = NULL) { |
|
34 | 9x |
UseMethod("variable_types") |
35 |
} |
|
36 | ||
37 |
#' @export |
|
38 |
variable_types.default <- function(data, columns = NULL) { |
|
39 | 9x |
checkmate::assert_character(columns, null.ok = TRUE, any.missing = FALSE) |
40 | ||
41 | 9x |
res <- if (is.null(columns)) { |
42 | ! |
vapply( |
43 | ! |
data, |
44 | ! |
function(x) class(x)[[1]], |
45 | ! |
character(1), |
46 | ! |
USE.NAMES = FALSE |
47 |
) |
|
48 | 9x |
} else if (checkmate::test_character(columns, any.missing = FALSE)) { |
49 | 9x |
stopifnot(all(columns %in% names(data) | vapply(columns, identical, logical(1L), ""))) |
50 | 9x |
vapply( |
51 | 9x |
columns, |
52 | 9x |
function(x) ifelse(x == "", "", class(data[[x]])[[1]]), |
53 | 9x |
character(1), |
54 | 9x |
USE.NAMES = FALSE |
55 |
) |
|
56 |
} else { |
|
57 | ! |
character(0) |
58 |
} |
|
59 | ||
60 | 9x |
return(res) |
61 |
} |
|
62 | ||
63 |
#' @export |
|
64 |
variable_types.data.frame <- function(data, columns = NULL) { # nolint: object_name_linter. |
|
65 | 8x |
variable_types.default(data, columns) |
66 |
} |
|
67 | ||
68 |
#' @export |
|
69 |
variable_types.DataTable <- function(data, columns = NULL) { |
|
70 | ! |
variable_types.default(data, columns) |
71 |
} |
|
72 | ||
73 |
#' @export |
|
74 |
variable_types.DFrame <- function(data, columns = NULL) { |
|
75 | ! |
variable_types.default(data, columns) |
76 |
} |
|
77 | ||
78 |
#' @export |
|
79 |
variable_types.matrix <- function(data, columns = NULL) { |
|
80 | ! |
checkmate::assert_character(columns, null.ok = TRUE, any.missing = FALSE) |
81 | ||
82 | ! |
res <- if (is.null(columns)) { |
83 | ! |
apply( |
84 | ! |
data, |
85 | ! |
2, |
86 | ! |
function(x) class(x)[1] |
87 |
) |
|
88 | ! |
} else if (checkmate::test_character(columns, any.missing = FALSE)) { |
89 | ! |
stopifnot( |
90 | ! |
all( |
91 | ! |
columns %in% colnames(data) | |
92 | ! |
vapply(columns, identical, logical(1L), "") |
93 |
) |
|
94 |
) |
|
95 | ! |
vapply( |
96 | ! |
columns, |
97 | ! |
function(x) ifelse(x == "", "", class(data[, x])[1]), |
98 | ! |
character(1), |
99 | ! |
USE.NAMES = FALSE |
100 |
) |
|
101 |
} else { |
|
102 | ! |
character(0) |
103 |
} |
|
104 | ||
105 | ! |
return(res) |
106 |
} |
1 |
#' Resolve list of filter states |
|
2 |
#' |
|
3 |
#' Resolves the state values for `FilterState$set_state()`, which accepts only a list |
|
4 |
#' with `selected`,`keep_na` and `keep_inf` fields. In case of the `default_filter` function, |
|
5 |
#' it returns `NULL` as during the initialization of `FilterState` values are set to |
|
6 |
#' default. |
|
7 |
#' |
|
8 |
#' @param x (`list`,`vector`)\cr |
|
9 |
#' values of the variable used in filter. Depending on the `FilterState` type |
|
10 |
#' list must contain these fields: |
|
11 |
#' \itemize{ |
|
12 |
#' \item{`selected`}{ defines initial selection. See notes for more details} |
|
13 |
#' \item{`keep_na` (`logical`)}{ defines whether to keep or remove `NA` values} |
|
14 |
#' \item{`keep_inf` (`logical`)}{ defines whether to keep or remove `Inf` values} |
|
15 |
#' } |
|
16 |
#' If `vector` is provided then `keep_na` and `keep_inf` can be specified |
|
17 |
#' adding `NA` and `Inf` to the selection vector. |
|
18 |
#' |
|
19 |
#' @note |
|
20 |
#' The value of the `x$selected` needs to be modified according to the type |
|
21 |
#' of the passed `filter_state`. |
|
22 |
#' |
|
23 |
#' @seealso |
|
24 |
#' - [LogicalFilterState] |
|
25 |
#' - [ChoicesFilterState] |
|
26 |
#' - [RangeFilterState] |
|
27 |
#' - [DateFilterState] |
|
28 |
#' - [DatetimeFilterState] |
|
29 |
#' |
|
30 |
#' @return `list` containing `selected`, `keep_na` and `keep_inf` |
|
31 |
#' |
|
32 |
#' @keywords internal |
|
33 |
#' |
|
34 |
#' @examples |
|
35 |
#' teal.slice:::resolve_state(list(c(1, 2), keep_na = FALSE, keep_inf = TRUE)) |
|
36 |
#' teal.slice:::resolve_state(c(1, 2, Inf)) |
|
37 |
#' teal.slice:::resolve_state(list()) |
|
38 |
resolve_state <- function(x) { |
|
39 | 165x |
UseMethod("resolve_state") |
40 |
} |
|
41 | ||
42 |
#' @rdname resolve_state |
|
43 |
#' @keywords internal |
|
44 |
#' @export |
|
45 |
resolve_state.default <- function(x) { |
|
46 | 86x |
state <- list() |
47 | 86x |
if (length(x[!(is.infinite(x) | is.na(x))]) > 0) { |
48 | 86x |
state$selected <- x[!(is.infinite(x) | is.na(x))] |
49 |
} |
|
50 | ||
51 | 86x |
if (any(is.na(x))) { |
52 | 1x |
state$keep_na <- TRUE |
53 |
} |
|
54 | ||
55 | 86x |
if (any(is.infinite(x))) { |
56 | 1x |
state$keep_inf <- TRUE |
57 |
} |
|
58 | ||
59 | 86x |
state |
60 |
} |
|
61 | ||
62 |
#' @rdname resolve_state |
|
63 |
#' @keywords internal |
|
64 |
#' @export |
|
65 |
resolve_state.default_filter <- function(x, filter_state) { # nolint |
|
66 | 1x |
list() |
67 |
} |
|
68 | ||
69 |
#' @rdname resolve_state |
|
70 |
#' @keywords internal |
|
71 |
#' @export |
|
72 |
resolve_state.list <- function(x) { |
|
73 | 78x |
if (identical(x, list())) { |
74 | 6x |
return(x) |
75 |
} |
|
76 | 72x |
if (is.null(names(x))) { |
77 | 10x |
names(x) <- rep("", length(x)) |
78 |
} |
|
79 | 72x |
x_names <- names(x) |
80 | 72x |
if (sum(x_names == "") > 1) { |
81 | 1x |
stop("More than one element of filter state is unnamed.") |
82 | 71x |
} else if (sum(x_names == "") == 1) { |
83 | 11x |
if ("selected" %in% x_names) { |
84 | 1x |
stop("Unnamed element of filter state cannot be intepreted as 'selected' because it already exists.") |
85 |
} else { |
|
86 | 10x |
x_idx <- which(x_names == "") |
87 | 10x |
names(x)[[x_idx]] <- "selected" |
88 |
} |
|
89 |
} |
|
90 | 70x |
x |
91 |
} |
1 |
#' Include `CSS` files from `/inst/css/` package directory to application header |
|
2 |
#' |
|
3 |
#' `system.file` should not be used to access files in other packages, it does |
|
4 |
#' not work with `devtools`. Therefore, we redefine this method in each package |
|
5 |
#' as needed. Thus, we do not export this method |
|
6 |
#' |
|
7 |
#' @param pattern (`character`) pattern of files to be included |
|
8 |
#' |
|
9 |
#' @return HTML code that includes `CSS` files |
|
10 |
#' @keywords internal |
|
11 |
include_css_files <- function(pattern = "*") { |
|
12 | ! |
css_files <- list.files( |
13 | ! |
system.file("css", package = "teal.slice", mustWork = TRUE), |
14 | ! |
pattern = pattern, full.names = TRUE |
15 |
) |
|
16 | ! |
return(shiny::singleton(shiny::tags$head(lapply(css_files, shiny::includeCSS)))) |
17 |
} |
1 |
#' Combine calls by operator |
|
2 |
#' |
|
3 |
#' Combine list of calls by specific operator |
|
4 |
#' |
|
5 |
#' @param calls (`list` of calls)\cr |
|
6 |
#' list containing calls to be combined by `operator`; |
|
7 |
#' if empty, NULL is returned |
|
8 |
#' @param operator (`character(1)`)\cr |
|
9 |
#' name/symbol of the operator passed as character string |
|
10 |
#' |
|
11 |
#' @return call or NULL, if `calls` is an empty list |
|
12 |
#' |
|
13 |
#' @examples |
|
14 |
#' \dontrun{ |
|
15 |
#' calls <- list( |
|
16 |
#' quote(SEX == "F"), # subsetting on factor |
|
17 |
#' quote(AGE >= 20 & AGE <= 50), # subsetting on range |
|
18 |
#' quote(!SURV) # subsetting on logical |
|
19 |
#' ) |
|
20 |
#' calls_combine_by(calls, "&") |
|
21 |
#' } |
|
22 |
#' @return a combined `call` |
|
23 |
#' @keywords internal |
|
24 |
calls_combine_by <- function(calls, operator) { |
|
25 | 174x |
checkmate::assert_list(calls) |
26 | 53x |
if (length(calls) > 0L) checkmate::assert_list(calls, types = c("call", "name")) |
27 | 173x |
checkmate::assert_string(operator) |
28 | ||
29 | 171x |
Reduce( |
30 | 171x |
x = calls, |
31 | 171x |
f = function(x, y) call(operator, x, y) |
32 |
) |
|
33 |
} |
1 |
.onLoad <- function(libname, pkgname) { # nolint |
|
2 |
# adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R |
|
3 | ! |
teal_default_options <- list(teal.threshold_slider_vs_checkboxgroup = 5) |
4 | ! |
op <- options() |
5 | ! |
toset <- !(names(teal_default_options) %in% names(op)) |
6 | ! |
if (any(toset)) options(teal_default_options[toset]) |
7 | ||
8 |
# Set up the teal logger instance |
|
9 | ! |
teal.logger::register_logger("teal.slice") |
10 | ||
11 | ! |
invisible() |
12 |
} |
1 |
#' Managing `FilteredData` states |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("experimental")` |
|
4 |
#' Set, get and remove filter states of `FilteredData` object |
|
5 |
#' |
|
6 |
#' @name filter_state_api |
|
7 |
#' |
|
8 |
#' @param datasets (`FilteredData`)\cr |
|
9 |
#' object to store filter state and filtered datasets, shared across modules. For more |
|
10 |
#' details see [`FilteredData`] |
|
11 |
#' |
|
12 |
#' @param filter (`list`)\cr |
|
13 |
#' You can define filters that show when the app starts. List names should be |
|
14 |
#' named according to datanames passed to the `data` argument. |
|
15 |
#' In case of data.frame` the list should be composed as follows: |
|
16 |
#' ``` |
|
17 |
#' list(<dataname1> = list(<varname1> = ..., <varname2> = ...), |
|
18 |
#' <dataname2> = list(...), |
|
19 |
#' ...) |
|
20 |
#' |
|
21 |
#' ``` |
|
22 |
#' |
|
23 |
#' For example, filters for variable `Sepal.Length` in `iris` can be specified as |
|
24 |
#' follows: |
|
25 |
#' ``` |
|
26 |
#' list(iris = list(Sepal.Length = list(selected = c(5.0, 7.0)))) |
|
27 |
#' # or |
|
28 |
#' list(iris = list(Sepal.Length = c(5.0, 7.0))) |
|
29 |
#' ``` |
|
30 |
#' |
|
31 |
#' In case developer would like to include `NA` and `Inf` values in the |
|
32 |
#' filtered dataset. |
|
33 |
#' ``` |
|
34 |
#' list(Species = list(selected = c(5.0, 7.0), keep_na = TRUE, keep_inf = TRUE)) |
|
35 |
#' list(Species = c(c(5.0, 7.0), NA, Inf)) |
|
36 |
#' ``` |
|
37 |
#' |
|
38 |
#' To initialize with specific variable filter with all values on start, one |
|
39 |
#' can use |
|
40 |
#' ``` |
|
41 |
#' list(Species = list()) |
|
42 |
#' ``` |
|
43 |
#' `filter` should be set with respect to the class of the column: |
|
44 |
#' * `numeric`: `selected` should be a two elements vector defining the range |
|
45 |
#' of the filter. |
|
46 |
#' * `Date`: `selected` should be a two elements vector defining the date-range |
|
47 |
#' of the filter |
|
48 |
#' * `POSIXct`: `selected` should be a two elements vector defining the |
|
49 |
#' `datetime` range of the filter |
|
50 |
#' * `character` and `factor`: `selected` should be a vector of any length |
|
51 |
#' defining initial values selected to filter. |
|
52 |
#' \cr |
|
53 |
#' `MultiAssayExperiment` `filter` should be specified in slightly different |
|
54 |
#' way. Since [MultiAssayExperiment::MultiAssayExperiment()] contains |
|
55 |
#' patient data ([SummarizedExperiment::colData()]) with list of experiments |
|
56 |
#' ([MultiAssayExperiment::ExperimentList()]), `filter` list should be named |
|
57 |
#' in the following name. |
|
58 |
#' \cr |
|
59 |
#' |
|
60 |
#' ``` |
|
61 |
#' list( |
|
62 |
#' <MAE dataname> = list( |
|
63 |
#' subjects = list(<column in colData> = ..., <column in colData> = ...), |
|
64 |
#' <experiment name> = list( |
|
65 |
#' subset = list(<column in rowData of experiment> = ..., |
|
66 |
#' <column in rowData of experiment> = ...), |
|
67 |
#' select = list(<column in colData of experiment> = ..., |
|
68 |
#' <column in colData of experiment> = ...) |
|
69 |
#' ) |
|
70 |
#' ) |
|
71 |
#' ) |
|
72 |
#' ``` |
|
73 |
#' `filter` is ignored if the app is restored from a bookmarked state. |
|
74 |
#' |
|
75 |
#' @return |
|
76 |
#' - set, remove and clear returns `NULL` |
|
77 |
#' - get returns named `list` of the same structure as described in `filter` argument. |
|
78 |
#' |
|
79 |
#' @examples |
|
80 |
#' utils::data(miniACC, package = "MultiAssayExperiment") |
|
81 |
#' |
|
82 |
#' datasets <- init_filtered_data( |
|
83 |
#' x = list( |
|
84 |
#' iris = list(dataset = iris), |
|
85 |
#' mae = list(dataset = miniACC) |
|
86 |
#' ) |
|
87 |
#' ) |
|
88 |
#' fs <- list( |
|
89 |
#' iris = list( |
|
90 |
#' Sepal.Length = list(selected = c(5.1, 6.4), keep_na = TRUE, keep_inf = FALSE), |
|
91 |
#' Species = list(selected = c("setosa", "versicolor"), keep_na = FALSE) |
|
92 |
#' ), |
|
93 |
#' mae = list( |
|
94 |
#' subjects = list( |
|
95 |
#' years_to_birth = list(selected = c(30, 50), keep_na = TRUE, keep_inf = FALSE), |
|
96 |
#' vital_status = list(selected = "1", keep_na = FALSE), |
|
97 |
#' gender = list(selected = "female", keep_na = TRUE) |
|
98 |
#' ), |
|
99 |
#' RPPAArray = list( |
|
100 |
#' subset = list(ARRAY_TYPE = list(selected = "", keep_na = TRUE)) |
|
101 |
#' ) |
|
102 |
#' ) |
|
103 |
#' ) |
|
104 |
#' |
|
105 |
#' # set initial filter state |
|
106 |
#' isolate(set_filter_state(datasets, filter = fs)) |
|
107 |
#' |
|
108 |
#' # get filter state |
|
109 |
#' get_filter_state(datasets) |
|
110 |
#' |
|
111 |
#' # modify filter state |
|
112 |
#' isolate( |
|
113 |
#' set_filter_state( |
|
114 |
#' datasets, |
|
115 |
#' filter = list(iris = list(Species = list(selected = "setosa", keep_na = TRUE))) |
|
116 |
#' ) |
|
117 |
#' ) |
|
118 |
#' |
|
119 |
#' # remove specific filters |
|
120 |
#' isolate( |
|
121 |
#' remove_filter_state(datasets, |
|
122 |
#' filter = list( |
|
123 |
#' iris = "Species", |
|
124 |
#' mae = list( |
|
125 |
#' subjects = c("years_to_birth", "vital_status") |
|
126 |
#' ) |
|
127 |
#' ) |
|
128 |
#' ) |
|
129 |
#' ) |
|
130 |
#' # remove all states |
|
131 |
#' clear_filter_states(datasets) |
|
132 |
NULL |
|
133 | ||
134 |
#' @rdname filter_state_api |
|
135 |
#' @export |
|
136 |
set_filter_state <- function(datasets, filter) { |
|
137 | ! |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
138 | ! |
checkmate::assert_list(filter, min.len = 0, null.ok = TRUE) |
139 | ! |
if (length(filter) > 0) { |
140 | ! |
datasets$set_filter_state(filter) |
141 |
} |
|
142 | ! |
invisible(NULL) |
143 |
} |
|
144 | ||
145 |
#' @rdname filter_state_api |
|
146 |
#' @export |
|
147 |
get_filter_state <- function(datasets) { |
|
148 | ! |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
149 | ! |
if (shiny::isRunning()) { |
150 | ! |
datasets$get_filter_state() |
151 |
} else { |
|
152 | ! |
isolate(datasets$get_filter_state()) |
153 |
} |
|
154 |
} |
|
155 | ||
156 |
#' @rdname filter_state_api |
|
157 |
#' @export |
|
158 |
remove_filter_state <- function(datasets, filter) { |
|
159 | ! |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
160 | ! |
checkmate::assert_list(filter, min.len = 0, null.ok = TRUE) |
161 | ! |
if (length(filter) > 0) { |
162 | ! |
datasets$remove_filter_state(filter) |
163 |
} |
|
164 | ! |
invisible(NULL) |
165 |
} |
|
166 | ||
167 |
#' @rdname filter_state_api |
|
168 |
#' @export |
|
169 |
clear_filter_states <- function(datasets) { |
|
170 | ! |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
171 | ! |
datasets$remove_all_filter_states() |
172 | ! |
invisible(NULL) |
173 |
} |