1 |
# FilterStates ------
|
|
2 | ||
3 |
#' @name FilterStates
|
|
4 |
#' @docType class
|
|
5 |
#' @title `FilterStates` `R6` class
|
|
6 |
#'
|
|
7 |
#' @description
|
|
8 |
#' Abstract class that manages adding and removing `FilterState` objects
|
|
9 |
#' and builds a *subset expression*.
|
|
10 |
#'
|
|
11 |
#' A `FilterStates` object tracks all condition calls
|
|
12 |
#' (logical predicates that limit observations) associated with a given dataset
|
|
13 |
#' and composes them into a single reproducible `R` expression
|
|
14 |
#' that will assign a subset of the original data to a new variable.
|
|
15 |
#' This expression is hereafter referred to as *subset expression*.
|
|
16 |
#'
|
|
17 |
#' The *subset expression* is constructed differently for different
|
|
18 |
#' classes of the underlying data object and `FilterStates` sub-classes.
|
|
19 |
#' Currently implemented for `data.frame`, `matrix`,
|
|
20 |
#' `SummarizedExperiment`, and `MultiAssayExperiment`.
|
|
21 |
#'
|
|
22 |
#' @keywords internal
|
|
23 |
#'
|
|
24 |
FilterStates <- R6::R6Class( # nolint |
|
25 |
classname = "FilterStates", |
|
26 | ||
27 |
# public members ----
|
|
28 |
public = list( |
|
29 |
#' @description
|
|
30 |
#' Initializes `FilterStates` object by setting
|
|
31 |
#' `dataname`, and `datalabel`.
|
|
32 |
#'
|
|
33 |
#' @param data (`data.frame` or `MultiAssayExperiment` or `SummarizedExperiment` or `matrix`)
|
|
34 |
#' the `R` object which `subset` function is applied on.
|
|
35 |
#' @param data_reactive (`function(sid)`)
|
|
36 |
#' should return an object of the same type as `data` object or `NULL`.
|
|
37 |
#' This object is needed for the `FilterState` counts being updated
|
|
38 |
#' on a change in filters. If function returns `NULL` then filtered counts are not shown.
|
|
39 |
#' Function has to have `sid` argument being a character.
|
|
40 |
#' @param dataname (`character(1)`)
|
|
41 |
#' name of the dataset, used in the subset expression.
|
|
42 |
#' Passed to the function argument attached to this `FilterStates`.
|
|
43 |
#' @param datalabel (`character(1)`) optional
|
|
44 |
#' text label.
|
|
45 |
#'
|
|
46 |
#' @return
|
|
47 |
#' Object of class `FilterStates`, invisibly.
|
|
48 |
#'
|
|
49 |
initialize = function(data, |
|
50 |
data_reactive = function(sid = "") NULL, |
|
51 |
dataname,
|
|
52 |
datalabel = NULL) { |
|
53 | 258x |
checkmate::assert_string(dataname) |
54 | 256x |
logger::log_debug("Instantiating { class(self)[1] }, dataname: { dataname }") |
55 | 256x |
checkmate::assert_function(data_reactive, args = "sid") |
56 | 256x |
checkmate::assert_string(datalabel, null.ok = TRUE) |
57 | ||
58 | 256x |
private$dataname <- dataname |
59 | 256x |
private$datalabel <- datalabel |
60 | 256x |
private$dataname_prefixed <- if (identical(dataname, make.names(dataname))) { |
61 | 256x |
dataname
|
62 |
} else { |
|
63 | ! |
sprintf("`%s`", dataname) |
64 |
}
|
|
65 | 256x |
private$data <- data |
66 | 256x |
private$data_reactive <- data_reactive |
67 | 256x |
private$state_list <- reactiveVal() |
68 | ||
69 |
# Clears state list when finalizing the object
|
|
70 | 256x |
private$session_bindings[["clear_state_list"]] <- list( |
71 | 256x |
destroy = function() { |
72 | 52x |
private$state_list_empty(force = TRUE) |
73 | 52x |
isolate(private$state_list(NULL)) |
74 |
}
|
|
75 |
)
|
|
76 | ||
77 | 256x |
invisible(self) |
78 |
},
|
|
79 | ||
80 |
#' @description
|
|
81 |
#' Returns a formatted string representing this `FilterStates` object.
|
|
82 |
#'
|
|
83 |
#' @param show_all (`logical(1)`) passed to `format.teal_slices`
|
|
84 |
#' @param trim_lines (`logical(1)`) passed to `format.teal_slices`
|
|
85 |
#'
|
|
86 |
#' @return `character(1)` the formatted string
|
|
87 |
#'
|
|
88 |
format = function(show_all = FALSE, trim_lines = TRUE) { |
|
89 | ! |
sprintf( |
90 | ! |
"%s:\n%s",
|
91 | ! |
class(self)[1], |
92 | ! |
format(self$get_filter_state(), show_all = show_all, trim_lines = trim_lines) |
93 |
)
|
|
94 |
},
|
|
95 | ||
96 |
#' @description
|
|
97 |
#' Filter call
|
|
98 |
#'
|
|
99 |
#' Builds *subset expression* from condition calls generated by `FilterState`.
|
|
100 |
#' The `lhs` of the expression is a `dataname_prefixed`, where word prefixed refers to
|
|
101 |
#' situation when call is evaluated on elements of the original data, for example `dataname[[x]]`.
|
|
102 |
#' By default `dataname_prefixed = dataname` and it's not alterable through class methods.
|
|
103 |
#' Customization of `private$dataname_prefixed` is done through inheriting classes.
|
|
104 |
#'
|
|
105 |
#' The `rhs` is a call to `private$fun` with following arguments:
|
|
106 |
#' - `dataname_prefixed`
|
|
107 |
#' - list of logical expressions generated by `FilterState` objects
|
|
108 |
#' stored in `private$state_list`. Each logical predicate is combined with `&` operator.
|
|
109 |
#' Variables in these logical expressions by default are not prefixed but this can be changed
|
|
110 |
#' by setting `private$extract_type` (change in the similar way as `dataname_prefixed`)
|
|
111 |
#' Possible call outputs depending on a custom fields/options:
|
|
112 |
#' ```
|
|
113 |
#' # default
|
|
114 |
#' dataname <- subset(dataname, col == "x")
|
|
115 |
#'
|
|
116 |
#' # fun = dplyr::filter
|
|
117 |
#' dataname <- dplyr::filter(dataname, col == "x")
|
|
118 |
#'
|
|
119 |
#' # fun = MultiAssayExperiment::subsetByColData; extract_type = "list"
|
|
120 |
#' dataname <- MultiAssayExperiment::subsetByColData(dataname, dataname$col == "x")
|
|
121 |
#'
|
|
122 |
#' # teal_slice objects having `arg = "subset"` and `arg = "select"`
|
|
123 |
#' dataname <- subset(dataname, subset = row_col == "x", select = col_col == "x")
|
|
124 |
#'
|
|
125 |
#' # dataname = dataname[[element]]
|
|
126 |
#' dataname[[element]] <- subset(dataname[[element]], subset = col == "x")
|
|
127 |
#' ```
|
|
128 |
#'
|
|
129 |
#' If no filters are applied, `NULL` is returned to avoid no-op calls such as `dataname <- dataname`.
|
|
130 |
#'
|
|
131 |
#' @param sid (`character`)
|
|
132 |
#' when specified then method returns code containing condition calls (logical predicates) of
|
|
133 |
#' `FilterState` objects which `"sid"` attribute is different than this `sid` argument.
|
|
134 |
#'
|
|
135 |
#' @return `call` or `NULL`
|
|
136 |
#'
|
|
137 |
get_call = function(sid = "") { |
|
138 |
# `arg` must be the same as argument of the function where
|
|
139 |
# predicate is passed to.
|
|
140 |
# For unnamed arguments state_list should have `arg = NULL`
|
|
141 | 88x |
states_list <- private$state_list_get() |
142 | 88x |
if (length(states_list) == 0) { |
143 | 52x |
return(NULL) |
144 |
}
|
|
145 | 36x |
args <- vapply( |
146 | 36x |
states_list,
|
147 | 36x |
function(x) { |
148 | 57x |
arg <- x$get_state()$arg |
149 | 7x |
`if`(is.null(arg), "", arg) # converting NULL -> "" to enable tapply. |
150 |
},
|
|
151 | 36x |
character(1) |
152 |
)
|
|
153 | ||
154 | 36x |
filter_items <- tapply( |
155 | 36x |
X = states_list, |
156 | 36x |
INDEX = args, |
157 | 36x |
simplify = FALSE, |
158 | 36x |
function(items) { |
159 |
# removing filters identified by sid
|
|
160 | 38x |
other_filter_idx <- !names(items) %in% sid |
161 | 38x |
filtered_items <- items[other_filter_idx] |
162 | ||
163 | 38x |
calls <- Filter( |
164 | 38x |
Negate(is.null), |
165 | 38x |
lapply( |
166 | 38x |
filtered_items,
|
167 | 38x |
function(state) { |
168 | 51x |
state$get_call(dataname = private$dataname_prefixed) |
169 |
}
|
|
170 |
)
|
|
171 |
)
|
|
172 | 38x |
calls_combine_by(calls, operator = "&") |
173 |
}
|
|
174 |
)
|
|
175 | 36x |
filter_items <- Filter( |
176 | 36x |
x = filter_items, |
177 | 36x |
f = Negate(is.null) |
178 |
)
|
|
179 | 36x |
if (length(filter_items) > 0L) { |
180 | 35x |
filter_function <- private$fun |
181 | 35x |
data_name <- tryCatch( |
182 |
{
|
|
183 | 35x |
str2lang(private$dataname_prefixed) |
184 |
},
|
|
185 | 35x |
error = function(e) str2lang(paste0("`", private$dataname_prefixed, "`")) |
186 |
)
|
|
187 | 35x |
substitute( |
188 | 35x |
env = list( |
189 | 35x |
lhs = data_name, |
190 | 35x |
rhs = as.call(c(filter_function, c(list(data_name), filter_items))) |
191 |
),
|
|
192 | 35x |
expr = lhs <- rhs |
193 |
)
|
|
194 |
} else { |
|
195 |
# return NULL to avoid no-op call
|
|
196 | 1x |
NULL
|
197 |
}
|
|
198 |
},
|
|
199 | ||
200 |
#' @description
|
|
201 |
#' Prints this `FilterStates` object.
|
|
202 |
#'
|
|
203 |
#' @param ... additional arguments passed to `format`.
|
|
204 |
print = function(...) { |
|
205 | ! |
cat(isolate(self$format(...)), "\n") |
206 |
},
|
|
207 | ||
208 |
#' @description
|
|
209 |
#' Remove one or more `FilterState`s from the `state_list` along with their UI elements.
|
|
210 |
#'
|
|
211 |
#' @param state (`teal_slices`)
|
|
212 |
#' specifying `FilterState` objects to remove;
|
|
213 |
#' `teal_slice`s may contain only `dataname` and `varname`, other elements are ignored
|
|
214 |
#'
|
|
215 |
#' @return `NULL`, invisibly.
|
|
216 |
#'
|
|
217 |
remove_filter_state = function(state) { |
|
218 | 17x |
isolate({ |
219 | 17x |
logger::log_debug("{ class(self)[1] }$remove_filter_state removing filters, state_id: { private$dataname }") |
220 | 17x |
checkmate::assert_class(state, "teal_slices") |
221 | 17x |
state_ids <- vapply(state, `[[`, character(1L), "id") |
222 | 17x |
private$state_list_remove(state_ids) |
223 |
}) |
|
224 | 17x |
invisible(NULL) |
225 |
},
|
|
226 | ||
227 |
#' @description
|
|
228 |
#' Gets reactive values from active `FilterState` objects.
|
|
229 |
#'
|
|
230 |
#' Get active filter state from `FilterState` objects stored in `state_list`(s).
|
|
231 |
#' The output is a list compatible with input to `self$set_filter_state`.
|
|
232 |
#'
|
|
233 |
#' @return Object of class `teal_slices`.
|
|
234 |
#'
|
|
235 |
get_filter_state = function() { |
|
236 | 306x |
slices <- unname(lapply(private$state_list(), function(x) x$get_state())) |
237 | 306x |
fs <- do.call(teal_slices, c(slices, list(count_type = private$count_type))) |
238 | ||
239 | 306x |
include_varnames <- private$include_varnames |
240 | 306x |
if (length(include_varnames)) { |
241 | 180x |
attr(fs, "include_varnames") <- structure( |
242 | 180x |
list(include_varnames), |
243 | 180x |
names = private$dataname |
244 |
)
|
|
245 |
}
|
|
246 | ||
247 | 306x |
exclude_varnames <- private$exclude_varnames |
248 | 306x |
if (length(exclude_varnames)) { |
249 | 9x |
attr(fs, "exclude_varnames") <- structure( |
250 | 9x |
list(exclude_varnames), |
251 | 9x |
names = private$dataname |
252 |
)
|
|
253 |
}
|
|
254 | ||
255 | 306x |
fs
|
256 |
},
|
|
257 | ||
258 |
#' @description
|
|
259 |
#' Sets active `FilterState` objects.
|
|
260 |
#' @param state (`teal_slices`)
|
|
261 |
#' @return Function that raises an error.
|
|
262 |
set_filter_state = function(state) { |
|
263 | 127x |
isolate({ |
264 | 127x |
logger::log_debug("{ class(self)[1] }$set_filter_state initializing, dataname: { private$dataname }") |
265 | 127x |
checkmate::assert_class(state, "teal_slices") |
266 | 127x |
lapply(state, function(x) { |
267 | 170x |
checkmate::assert_true( |
268 | 170x |
x$dataname == private$dataname, |
269 | 170x |
.var.name = "dataname matches private$dataname" |
270 |
)
|
|
271 |
}) |
|
272 | ||
273 | 127x |
private$set_filterable_varnames( |
274 | 127x |
include_varnames = attr(state, "include_varnames")[[private$dataname]], |
275 | 127x |
exclude_varnames = attr(state, "exclude_varnames")[[private$dataname]] |
276 |
)
|
|
277 | 127x |
count_type <- attr(state, "count_type") |
278 | 127x |
if (length(count_type)) { |
279 | 21x |
private$count_type <- count_type |
280 |
}
|
|
281 | ||
282 |
# Drop teal_slices that refer to excluded variables.
|
|
283 | 127x |
varnames <- unique(unlist(lapply(state, "[[", "varname"))) |
284 | 127x |
excluded_varnames <- setdiff(varnames, private$get_filterable_varnames()) |
285 | 127x |
if (length(excluded_varnames)) { |
286 | 3x |
state <- Filter(function(x) !isTRUE(x$varname %in% excluded_varnames), state) |
287 | 3x |
warning(sprintf("filters for columns: %s excluded from %s", toString(excluded_varnames), private$dataname)) |
288 |
}
|
|
289 | ||
290 | 127x |
if (length(state) > 0) { |
291 | 90x |
private$set_filter_state_impl( |
292 | 90x |
state = state, |
293 | 90x |
data = private$data, |
294 | 90x |
data_reactive = private$data_reactive |
295 |
)
|
|
296 |
}
|
|
297 |
}) |
|
298 | ||
299 | 127x |
invisible(NULL) |
300 |
},
|
|
301 | ||
302 |
#' @description
|
|
303 |
#' Remove all `FilterState` objects from this `FilterStates` object.
|
|
304 |
#'
|
|
305 |
#' @param force (`logical(1)`)
|
|
306 |
#' flag specifying whether to include anchored filter states.
|
|
307 |
#'
|
|
308 |
#' @return `NULL`, invisibly.
|
|
309 |
#'
|
|
310 |
clear_filter_states = function(force = FALSE) { |
|
311 | 25x |
private$state_list_empty(force) |
312 | 25x |
invisible(NULL) |
313 |
},
|
|
314 | ||
315 |
# shiny modules ----
|
|
316 | ||
317 |
#' @description
|
|
318 |
#' `shiny` UI definition that stores `FilterState` UI elements.
|
|
319 |
#' Populated with elements created with `renderUI` in the module server.
|
|
320 |
#'
|
|
321 |
#' @param id (`character(1)`)
|
|
322 |
#' `shiny` module instance id.
|
|
323 |
#'
|
|
324 |
#' @return `shiny.tag`
|
|
325 |
#'
|
|
326 |
ui_active = function(id) { |
|
327 | ! |
ns <- NS(id) |
328 | ! |
tagList( |
329 | ! |
include_css_files(pattern = "filter-panel"), |
330 | ! |
uiOutput(ns("trigger_visible_state_change"), inline = TRUE), |
331 | ! |
uiOutput( |
332 | ! |
ns("cards"), |
333 | ! |
class = "accordion", |
334 | ! |
`data-label` = ifelse(length(private$datalabel), paste0("> ", private$datalabel), ""), |
335 |
)
|
|
336 |
)
|
|
337 |
},
|
|
338 | ||
339 |
#' @description
|
|
340 |
#' `shiny` server module.
|
|
341 |
#'
|
|
342 |
#' @param id (`character(1)`)
|
|
343 |
#' `shiny` module instance id.
|
|
344 |
#'
|
|
345 |
#' @return `NULL`.
|
|
346 |
#'
|
|
347 |
srv_active = function(id) { |
|
348 | 12x |
moduleServer( |
349 | 12x |
id = id, |
350 | 12x |
function(input, output, session) { |
351 | 12x |
logger::log_debug("{ class(self)[1] }$srv_active initializing, dataname: { private$dataname }") |
352 | 12x |
current_state <- reactive(private$state_list_get()) |
353 | 12x |
previous_state <- reactiveVal(NULL) # FilterState list |
354 | 12x |
added_states <- reactiveVal(NULL) # FilterState list |
355 | ||
356 |
# gives a valid shiny ns based on a default slice id
|
|
357 | 12x |
fs_to_shiny_ns <- function(x) { |
358 | 24x |
checkmate::assert_multi_class(x, c("FilterState", "FilterStateExpr")) |
359 | 24x |
gsub("[^[:alnum:]]+", "_", get_default_slice_id(x$get_state())) |
360 |
}
|
|
361 | ||
362 | 12x |
output$trigger_visible_state_change <- renderUI({ |
363 | 14x |
current_state() |
364 | 14x |
isolate({ |
365 | 14x |
logger::log_debug("{ class(self)[1] }$srv_active@1 determining added and removed filter states") |
366 |
# Be aware this returns a list because `current_state` is a list and not `teal_slices`.
|
|
367 | 14x |
added_states(setdiff_teal_slices(current_state(), previous_state())) |
368 | 14x |
previous_state(current_state()) |
369 | 14x |
NULL
|
370 |
}) |
|
371 |
}) |
|
372 | ||
373 | 12x |
output[["cards"]] <- renderUI({ |
374 | 14x |
lapply( |
375 | 14x |
current_state(), # observes only if added/removed |
376 | 14x |
function(state) { |
377 | 12x |
isolate( # isolates when existing state changes |
378 | 12x |
state$ui(id = session$ns(fs_to_shiny_ns(state)), parent_id = session$ns("cards")) |
379 |
)
|
|
380 |
}
|
|
381 |
)
|
|
382 |
}) |
|
383 | ||
384 | 12x |
private$session_bindings[[session$ns("added_states")]] <- observeEvent( |
385 | 12x |
added_states(), # we want to call FilterState module only once when it's added |
386 | 12x |
ignoreNULL = TRUE, |
387 |
{
|
|
388 | 10x |
added_state_names <- vapply(added_states(), function(x) x$get_state()$id, character(1L)) |
389 | 10x |
logger::log_debug( |
390 | 10x |
"{ class(self)[1] }$srv_active@2 triggered by added states: { toString(added_state_names) }"
|
391 |
)
|
|
392 | 10x |
lapply(added_states(), function(state) { |
393 | 12x |
state$server( |
394 | 12x |
id = fs_to_shiny_ns(state), |
395 | 12x |
remove_callback = function() private$state_list_remove(state$get_state()$id) |
396 |
)
|
|
397 |
}) |
|
398 | 10x |
added_states(NULL) |
399 |
}
|
|
400 |
)
|
|
401 | ||
402 | 12x |
NULL
|
403 |
}
|
|
404 |
)
|
|
405 |
},
|
|
406 | ||
407 |
#' @description
|
|
408 |
#' `shiny` UI module to add filter variable.
|
|
409 |
#'
|
|
410 |
#' @param id (`character(1)`)
|
|
411 |
#' `shiny` module instance id.
|
|
412 |
#'
|
|
413 |
#' @return `shiny.tag`
|
|
414 |
#'
|
|
415 |
ui_add = function(id) { |
|
416 | 1x |
checkmate::assert_string(id) |
417 | 1x |
data <- private$data |
418 | ||
419 | 1x |
ns <- NS(id) |
420 | ||
421 | 1x |
if (ncol(data) == 0) { |
422 | 1x |
tags$div("no sample variables available") |
423 | ! |
} else if (nrow(data) == 0) { |
424 | ! |
tags$div("no samples available") |
425 |
} else { |
|
426 | ! |
uiOutput(ns("add_filter")) |
427 |
}
|
|
428 |
},
|
|
429 | ||
430 |
#' @description
|
|
431 |
#' `shiny` server module to add filter variable.
|
|
432 |
#'
|
|
433 |
#' This module controls available choices to select as a filter variable.
|
|
434 |
#' Once selected, a variable is removed from available choices.
|
|
435 |
#' Removing a filter variable adds it back to available choices.
|
|
436 |
#'
|
|
437 |
#' @param id (`character(1)`)
|
|
438 |
#' `shiny` module instance id.
|
|
439 |
#'
|
|
440 |
#' @return `NULL`.
|
|
441 |
srv_add = function(id) { |
|
442 | 14x |
moduleServer( |
443 | 14x |
id = id, |
444 | 14x |
function(input, output, session) { |
445 | 14x |
logger::log_debug("{ class(self)[1] }$srv_add initializing, dataname: { private$dataname }") |
446 | ||
447 |
# available choices to display
|
|
448 | 14x |
avail_column_choices <- reactive({ |
449 | 17x |
data <- private$data |
450 | 17x |
vars_include <- private$get_filterable_varnames() |
451 | 17x |
active_filter_vars <- unique(unlist(lapply(self$get_filter_state(), "[[", "varname"))) |
452 | 17x |
choices <- setdiff(vars_include, active_filter_vars) |
453 | 17x |
varlabels <- get_varlabels(data) |
454 | ||
455 | 17x |
data_choices_labeled( |
456 | 17x |
data = data, |
457 | 17x |
choices = choices, |
458 | 17x |
varlabels = varlabels, |
459 | 17x |
keys = private$keys |
460 |
)
|
|
461 |
}) |
|
462 | ||
463 | 14x |
output$add_filter <- renderUI({ |
464 | 14x |
logger::log_debug( |
465 | 14x |
"{ class(self)[1] }$srv_add@1 updating available column choices, dataname: { private$dataname }"
|
466 |
)
|
|
467 | 14x |
if (length(avail_column_choices()) == 0) { |
468 |
# because input UI is not rendered on this condition but shiny still holds latest selected value
|
|
469 | ! |
tags$span("No available columns to add.") |
470 |
} else { |
|
471 | 14x |
tags$div( |
472 | 14x |
teal.widgets::optionalSelectInput( |
473 | 14x |
session$ns("var_to_add"), |
474 | 14x |
choices = avail_column_choices(), |
475 | 14x |
selected = NULL, |
476 | 14x |
options = shinyWidgets::pickerOptions( |
477 | 14x |
liveSearch = TRUE, |
478 | 14x |
noneSelectedText = "Select variable to filter" |
479 |
)
|
|
480 |
)
|
|
481 |
)
|
|
482 |
}
|
|
483 |
}) |
|
484 | ||
485 | 14x |
private$session_bindings[[session$ns("var_to_add")]] <- observeEvent( |
486 | 14x |
eventExpr = input$var_to_add, |
487 | 14x |
handlerExpr = { |
488 | 3x |
logger::log_debug( |
489 | 3x |
"{ class(self)[1] }$srv_add@2 adding FilterState for variable { input$var_to_add }, ",
|
490 | 3x |
"dataname {private$dataname}"
|
491 |
)
|
|
492 | 3x |
self$set_filter_state( |
493 | 3x |
teal_slices( |
494 | 3x |
teal_slice(dataname = private$dataname, varname = input$var_to_add) |
495 |
)
|
|
496 |
)
|
|
497 |
}
|
|
498 |
)
|
|
499 | ||
500 |
# Extra observer that clears all input values in session
|
|
501 | 14x |
private$session_bindings[[session$ns("inputs")]] <- list( |
502 | 14x |
destroy = function() { |
503 | 12x |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
504 |
}
|
|
505 |
)
|
|
506 | ||
507 | 14x |
NULL
|
508 |
}
|
|
509 |
)
|
|
510 |
},
|
|
511 | ||
512 |
#' @description
|
|
513 |
#' Object cleanup.
|
|
514 |
#'
|
|
515 |
#' - Destroy inputs and observers stored in `private$session_bindings`
|
|
516 |
#' - Clean `state_list`
|
|
517 |
#'
|
|
518 |
#' @return `NULL`, invisibly.
|
|
519 |
#'
|
|
520 |
finalize = function() { |
|
521 | 567x |
.finalize_session_bindings(self, private) |
522 | 567x |
invisible(NULL) |
523 |
}
|
|
524 |
),
|
|
525 |
private = list( |
|
526 |
# private fields ----
|
|
527 |
count_type = "none", # specifies how observation numbers are displayed in filter cards, |
|
528 |
data = NULL, # data.frame, MAE, SE or matrix |
|
529 |
data_reactive = NULL, # reactive |
|
530 |
datalabel = NULL, # to follow default `experiment = NULL` in `teal_slice` |
|
531 |
dataname = NULL, # because it holds object of class name |
|
532 |
dataname_prefixed = character(0), # name used in call returned from get_call |
|
533 |
exclude_varnames = character(0), # holds column names |
|
534 |
include_varnames = character(0), # holds column names |
|
535 |
extract_type = character(0), # type of the prefix in a subset call (eg. "list": x$var; "matrix": x[["var"]]) |
|
536 |
fun = quote(subset), # function used to generate subset call |
|
537 |
keys = character(0), |
|
538 |
ns = NULL, # shiny ns() |
|
539 |
session_bindings = list(), # inputs and observers |
|
540 |
state_list = NULL, # list of `reactiveVal`s initialized by init methods of child classes, |
|
541 | ||
542 |
# private methods ----
|
|
543 | ||
544 |
# @description
|
|
545 |
# Set the allowed filterable variables
|
|
546 |
# @param include_varnames (`character`) Names of variables included in filtering.
|
|
547 |
# @param exclude_varnames (`character`) Names of variables excluded from filtering.
|
|
548 |
#
|
|
549 |
# @details When retrieving the filtered variables only
|
|
550 |
# those which have filtering supported (i.e. are of the permitted types).
|
|
551 |
# Only one from `include_varnames` and `exclude_varnames` can be used in one call. When `exclude_varnames`
|
|
552 |
# is called `include_varnames` is cleared - same otherwise.
|
|
553 |
# are included.
|
|
554 |
#
|
|
555 |
# @return `NULL`, invisibly.
|
|
556 |
set_filterable_varnames = function(include_varnames = character(0), exclude_varnames = character(0)) { |
|
557 | 272x |
if ((length(include_varnames) + length(exclude_varnames)) == 0L) { |
558 | 105x |
return(invisible(NULL)) |
559 |
}
|
|
560 | 167x |
checkmate::assert_character(include_varnames, any.missing = FALSE, min.len = 0L, null.ok = TRUE) |
561 | 167x |
checkmate::assert_character(exclude_varnames, any.missing = FALSE, min.len = 0L, null.ok = TRUE) |
562 | 167x |
if (length(include_varnames) && length(exclude_varnames)) { |
563 | ! |
stop( |
564 | ! |
"`include_varnames` and `exclude_varnames` has been both specified for",
|
565 | ! |
private$dataname, |
566 | ! |
". Only one per dataset is allowed.",
|
567 |
)
|
|
568 |
}
|
|
569 | 167x |
supported_vars <- get_supported_filter_varnames(private$data) |
570 | 167x |
if (length(include_varnames)) { |
571 | 153x |
private$include_varnames <- intersect(include_varnames, supported_vars) |
572 | 153x |
private$exclude_varnames <- character(0) |
573 |
} else { |
|
574 | 14x |
private$exclude_varnames <- exclude_varnames |
575 | 14x |
private$include_varnames <- character(0) |
576 |
}
|
|
577 | 167x |
invisible(NULL) |
578 |
},
|
|
579 | ||
580 |
# @description
|
|
581 |
# Get vector of filterable varnames
|
|
582 |
#
|
|
583 |
# @details
|
|
584 |
# These are the only columns which can be used in the filter panel
|
|
585 |
#
|
|
586 |
# @return character vector with names of the columns
|
|
587 |
get_filterable_varnames = function() { |
|
588 | 144x |
if (length(private$include_varnames)) { |
589 | 97x |
private$include_varnames |
590 |
} else { |
|
591 | 47x |
supported_varnames <- get_supported_filter_varnames(private$data) |
592 | 47x |
setdiff(supported_varnames, private$exclude_varnames) |
593 |
}
|
|
594 |
},
|
|
595 | ||
596 |
# state_list methods ----
|
|
597 | ||
598 |
# @description
|
|
599 |
# Returns a list of `FilterState` objects stored in this `FilterStates`.
|
|
600 |
#
|
|
601 |
# @param state_id (`character(1)`)
|
|
602 |
# name of element in a filter state (which is a `reactiveVal` containing a list)
|
|
603 |
#
|
|
604 |
# @return `list` of `FilterState` objects
|
|
605 |
#
|
|
606 |
state_list_get = function(state_id = NULL) { |
|
607 | 208x |
checkmate::assert_string(state_id, null.ok = TRUE) |
608 | ||
609 | 208x |
if (is.null(state_id)) { |
610 | 208x |
private$state_list() |
611 |
} else { |
|
612 | ! |
private$state_list()[[state_id]] |
613 |
}
|
|
614 |
},
|
|
615 | ||
616 |
# @description
|
|
617 |
# Adds a new `FilterState` object to this `FilterStates`.
|
|
618 |
# Raises error if the length of `x` does not match the length of `state_id`.
|
|
619 |
#
|
|
620 |
# @param x (`FilterState`)
|
|
621 |
# object to be added to filter state list
|
|
622 |
# @param state_id (`character(1)`)
|
|
623 |
# name of element in a filter state (which is a `reactiveVal` containing a list)
|
|
624 |
#
|
|
625 |
# @return `NULL`.
|
|
626 |
#
|
|
627 |
state_list_push = function(x, state_id) { |
|
628 | 175x |
isolate({ |
629 | 175x |
logger::log_debug("{ class(self)[1] }$state_list_push pushing into state_list, state_id: { state_id }") |
630 | 175x |
checkmate::assert_string(state_id) |
631 | 175x |
checkmate::assert_multi_class(x, c("FilterState", "FilterStateExpr")) |
632 | 175x |
state <- stats::setNames(list(x), state_id) |
633 | 175x |
new_state_list <- c(private$state_list(), state) |
634 | 175x |
private$state_list(new_state_list) |
635 | 175x |
invisible(NULL) |
636 |
}) |
|
637 |
},
|
|
638 | ||
639 |
# @description
|
|
640 |
# Removes a single filter state with all associated shiny elements:
|
|
641 |
# * specified `FilterState` from `private$state_list`
|
|
642 |
# * UI card created for this filter
|
|
643 |
# * observers tracking the selection and remove button
|
|
644 |
#
|
|
645 |
# @param state_id (`character`)
|
|
646 |
# identifiers of elements in a filter state (which is a `reactiveVal` containing a list).
|
|
647 |
# @param force (`logical(1)`)
|
|
648 |
# flag specifying whether to include anchored filter states.
|
|
649 |
#
|
|
650 |
# @return `NULL`, invisibly.
|
|
651 |
#
|
|
652 |
state_list_remove = function(state_id, force = FALSE) { |
|
653 | 42x |
isolate({ |
654 | 42x |
checkmate::assert_character(state_id) |
655 | 42x |
logger::log_debug("{ class(self)[1] }$state_list_remove removing a filter, state_id: { toString(state_id) }") |
656 | 42x |
current_state_ids <- vapply(private$state_list(), function(x) x$get_state()$id, character(1)) |
657 | 42x |
to_remove <- state_id %in% current_state_ids |
658 | 42x |
if (any(to_remove)) { |
659 | 41x |
new_state_list <- Filter( |
660 | 41x |
function(state) { |
661 | 86x |
if (state$get_state()$id %in% state_id) { |
662 | 72x |
if (state$get_state()$anchored && !force) { |
663 | 7x |
TRUE
|
664 |
} else { |
|
665 | 65x |
state$finalize() |
666 | 65x |
FALSE
|
667 |
}
|
|
668 |
} else { |
|
669 | 14x |
TRUE
|
670 |
}
|
|
671 |
},
|
|
672 | 41x |
private$state_list() |
673 |
)
|
|
674 | 41x |
private$state_list(new_state_list) |
675 |
} else { |
|
676 | 1x |
warning(sprintf("\"%s\" not found in state list", state_id)) |
677 |
}
|
|
678 |
}) |
|
679 | ||
680 | 42x |
invisible(NULL) |
681 |
},
|
|
682 | ||
683 |
# @description
|
|
684 |
# Remove all `FilterState` objects from this `FilterStates` object.
|
|
685 |
# @param force (`logical(1)`)
|
|
686 |
# flag specifying whether to include anchored filter states.
|
|
687 |
# @return `NULL`, invisibly.
|
|
688 |
#
|
|
689 |
state_list_empty = function(force = FALSE) { |
|
690 | 77x |
isolate({ |
691 | 77x |
logger::log_debug( |
692 | 77x |
"{ class(self)[1] }$state_list_empty removing all non-anchored filters for dataname: { private$dataname }"
|
693 |
)
|
|
694 | ||
695 | 77x |
state_list <- private$state_list() |
696 | 77x |
if (length(state_list)) { |
697 | 25x |
state_ids <- vapply(state_list, function(x) x$get_state()$id, character(1L)) |
698 | 25x |
private$state_list_remove(state_ids, force) |
699 |
}
|
|
700 |
}) |
|
701 | ||
702 | 77x |
invisible(NULL) |
703 |
},
|
|
704 | ||
705 |
# @description
|
|
706 |
# Set filter state
|
|
707 |
#
|
|
708 |
# Utility method for `set_filter_state` to create or modify `FilterState` using a single
|
|
709 |
# `teal_slice`.
|
|
710 |
# @param state (`teal_slices`)
|
|
711 |
# @param data (`data.frame`, `matrix` or `DataFrame`)
|
|
712 |
# @param data_reactive (`function`)
|
|
713 |
# function having `sid` as argument.
|
|
714 |
#
|
|
715 |
# @return `NULL`, invisibly.
|
|
716 |
#
|
|
717 |
set_filter_state_impl = function(state, |
|
718 |
data,
|
|
719 |
data_reactive) { |
|
720 | 192x |
isolate({ |
721 | 192x |
checkmate::assert_class(state, "teal_slices") |
722 | 192x |
checkmate::assert_multi_class(data, c("data.frame", "matrix", "DataFrame", "HermesData")) |
723 | 192x |
checkmate::assert_function(data_reactive, args = "sid") |
724 | 192x |
if (length(state) == 0L) { |
725 | 86x |
return(invisible(NULL)) |
726 |
}
|
|
727 | ||
728 | 106x |
slices_hashed <- vapply(state, `[[`, character(1L), "id") |
729 | 106x |
if (any(duplicated(slices_hashed))) { |
730 | ! |
stop( |
731 | ! |
"Some of the teal_slice objects refer to the same filter. ",
|
732 | ! |
"Please specify different 'id' when calling teal_slice"
|
733 |
)
|
|
734 |
}
|
|
735 | ||
736 | 106x |
state_list <- private$state_list_get() |
737 | 106x |
lapply(state, function(slice) { |
738 | 183x |
state_id <- slice$id |
739 | 183x |
if (state_id %in% names(state_list)) { |
740 |
# Modify existing filter states.
|
|
741 | 8x |
state_list[[state_id]]$set_state(slice) |
742 |
} else { |
|
743 | 175x |
if (inherits(slice, "teal_slice_expr")) { |
744 |
# create a new FilterStateExpr
|
|
745 | 6x |
fstate <- init_filter_state_expr(slice) |
746 |
} else { |
|
747 |
# create a new FilterState
|
|
748 | 169x |
fstate <- init_filter_state( |
749 | 169x |
x = data[, slice$varname, drop = TRUE], |
750 |
# data_reactive is a function which eventually calls get_call(sid).
|
|
751 |
# This chain of calls returns column from the data filtered by everything
|
|
752 |
# but filter identified by the sid argument. FilterState then get x_reactive
|
|
753 |
# and this no longer needs to be a function to pass sid. reactive in the FilterState
|
|
754 |
# is also beneficial as it can be cached and retriger filter counts only if
|
|
755 |
# returned vector is different.
|
|
756 | 169x |
x_reactive = if (private$count_type == "none") { |
757 | 163x |
reactive(NULL) |
758 |
} else { |
|
759 | 6x |
reactive(data_reactive(state_id)[, slice$varname, drop = TRUE]) |
760 |
},
|
|
761 | 169x |
slice = slice, |
762 | 169x |
extract_type = private$extract_type |
763 |
)
|
|
764 |
}
|
|
765 | 175x |
private$state_list_push(x = fstate, state_id = state_id) |
766 |
}
|
|
767 |
}) |
|
768 | ||
769 | 106x |
invisible(NULL) |
770 |
}) |
|
771 |
}
|
|
772 |
)
|
|
773 |
)
|
1 |
# FilteredDataset abstract --------
|
|
2 | ||
3 |
#' @name FilteredDataset
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title `FilteredDataset` `R6` class
|
|
7 |
#' @description
|
|
8 |
#' `FilteredDataset` is a class which renders/controls `FilterStates`(s)
|
|
9 |
#' Each `FilteredDataset` contains `filter_states` field - a `list` which contains one
|
|
10 |
#' (`data.frame`) or multiple (`MultiAssayExperiment`) `FilterStates` objects.
|
|
11 |
#' Each `FilterStates` is responsible for one filter/subset expression applied for specific
|
|
12 |
#' components of the dataset.
|
|
13 |
#'
|
|
14 |
#' @keywords internal
|
|
15 |
FilteredDataset <- R6::R6Class( # nolint |
|
16 |
"FilteredDataset",
|
|
17 |
# public methods ----
|
|
18 |
public = list( |
|
19 |
#' @description
|
|
20 |
#' Initializes this `FilteredDataset` object.
|
|
21 |
#'
|
|
22 |
#' @param dataset any object
|
|
23 |
#' @param dataname (`character(1)`)
|
|
24 |
#' syntactically valid name given to the dataset.
|
|
25 |
#' @param keys (`character`) optional
|
|
26 |
#' vector of primary key column names.
|
|
27 |
#' @param label (`character(1)`)
|
|
28 |
#' label to describe the dataset.
|
|
29 |
#'
|
|
30 |
#' @return Object of class `FilteredDataset`, invisibly.
|
|
31 |
#'
|
|
32 |
initialize = function(dataset, dataname, keys = character(0), label = attr(dataset, "label", exact = TRUE)) { |
|
33 | 147x |
checkmate::assert_string(dataname) |
34 | 145x |
logger::log_debug("Instantiating { class(self)[1] }, dataname: { dataname }") |
35 | 145x |
checkmate::assert_character(keys, any.missing = FALSE) |
36 | 145x |
checkmate::assert_character(label, null.ok = TRUE) |
37 | 145x |
private$dataset <- dataset |
38 | 145x |
private$dataname <- dataname |
39 | 145x |
private$keys <- keys |
40 | 145x |
private$label <- if (is.null(label)) character(0) else label |
41 | ||
42 |
# function executing reactive call and returning data
|
|
43 | 145x |
private$data_filtered_fun <- function(sid = "") { |
44 | 24x |
checkmate::assert_character(sid) |
45 | 24x |
if (length(sid)) { |
46 | 24x |
logger::log_debug("filtering data dataname: { dataname }, sid: { sid }") |
47 |
} else { |
|
48 | ! |
logger::log_debug("filtering data dataname: { dataname }") |
49 |
}
|
|
50 | 24x |
env <- new.env(parent = parent.env(globalenv())) |
51 | 24x |
env[[dataname]] <- private$dataset |
52 | 24x |
filter_call <- self$get_call(sid) |
53 | 24x |
eval_expr_with_msg(filter_call, env) |
54 | 24x |
get(x = dataname, envir = env) |
55 |
}
|
|
56 | ||
57 | 145x |
private$data_filtered <- reactive(private$data_filtered_fun()) |
58 | 145x |
invisible(self) |
59 |
},
|
|
60 | ||
61 |
#' @description
|
|
62 |
#' Returns a formatted string representing this `FilteredDataset` object.
|
|
63 |
#'
|
|
64 |
#' @param show_all (`logical(1)`) passed to `format.teal_slice`.
|
|
65 |
#' @param trim_lines (`logical(1)`) passed to `format.teal_slice`.
|
|
66 |
#'
|
|
67 |
#' @return The formatted character string.
|
|
68 |
#'
|
|
69 |
format = function(show_all = FALSE, trim_lines = TRUE) { |
|
70 | 24x |
sprintf( |
71 | 24x |
"%s:\n%s",
|
72 | 24x |
class(self)[1], |
73 | 24x |
format(self$get_filter_state(), show_all = show_all, trim_lines = trim_lines) |
74 |
)
|
|
75 |
},
|
|
76 | ||
77 |
#' @description
|
|
78 |
#' Prints this `FilteredDataset` object.
|
|
79 |
#'
|
|
80 |
#' @param ... additional arguments passed to `format`.
|
|
81 |
#'
|
|
82 |
print = function(...) { |
|
83 | 10x |
cat(isolate(self$format(...)), "\n") |
84 |
},
|
|
85 | ||
86 |
#' @description
|
|
87 |
#' Removes all filter items applied to this dataset.
|
|
88 |
#'
|
|
89 |
#' @param force (`logical(1)`)
|
|
90 |
#' flag specifying whether to include anchored filter states.
|
|
91 |
#'
|
|
92 |
#' @return `NULL`.
|
|
93 |
clear_filter_states = function(force = FALSE) { |
|
94 | 14x |
logger::log_debug("Removing filters from FilteredDataset: { private$dataname }") |
95 | 14x |
lapply( |
96 | 14x |
private$get_filter_states(), |
97 | 14x |
function(filter_states) filter_states$clear_filter_states(force) |
98 |
)
|
|
99 | 14x |
NULL
|
100 |
},
|
|
101 | ||
102 |
# managing filter states -----
|
|
103 | ||
104 |
# getters ----
|
|
105 |
#' @description
|
|
106 |
#' Gets a filter expression.
|
|
107 |
#'
|
|
108 |
#' This function returns filter calls equivalent to selected items
|
|
109 |
#' within each of `filter_states`. Configuration of the calls is constant and
|
|
110 |
#' depends on `filter_states` type and order which are set during initialization.
|
|
111 |
#'
|
|
112 |
#' @param sid (`character`)
|
|
113 |
#' when specified, the method returns code containing conditions calls of
|
|
114 |
#' `FilterState` objects with `sid` different to this `sid` argument.
|
|
115 |
#'
|
|
116 |
#' @return Either a `list` of filter `call`s, or `NULL`.
|
|
117 |
get_call = function(sid = "") { |
|
118 | 47x |
filter_call <- Filter( |
119 | 47x |
f = Negate(is.null), |
120 | 47x |
x = lapply(private$get_filter_states(), function(x) x$get_call(sid)) |
121 |
)
|
|
122 | 47x |
if (length(filter_call) == 0) { |
123 | 29x |
return(NULL) |
124 |
}
|
|
125 | 18x |
filter_call
|
126 |
},
|
|
127 | ||
128 |
#' @description
|
|
129 |
#' Gets states of all contained `FilterState` objects.
|
|
130 |
#'
|
|
131 |
#' @return A `teal_slices` object.
|
|
132 |
#'
|
|
133 |
get_filter_state = function() { |
|
134 | 150x |
states <- unname(lapply(private$get_filter_states(), function(x) x$get_filter_state())) |
135 | 150x |
do.call(c, states) |
136 |
},
|
|
137 | ||
138 |
#' @description
|
|
139 |
#' Set filter state.
|
|
140 |
#'
|
|
141 |
#' @param state (`teal_slices`)
|
|
142 |
#'
|
|
143 |
#' @return Virtual method, returns nothing and raises error.
|
|
144 |
#'
|
|
145 |
set_filter_state = function(state) { |
|
146 | ! |
stop("set_filter_state is an abstract class method.") |
147 |
},
|
|
148 | ||
149 |
#' @description
|
|
150 |
#' Gets the name of the dataset.
|
|
151 |
#'
|
|
152 |
#' @return A character string.
|
|
153 |
get_dataname = function() { |
|
154 | 8x |
private$dataname |
155 |
},
|
|
156 | ||
157 |
#' @description
|
|
158 |
#' Gets the dataset object in this `FilteredDataset`.
|
|
159 |
#'
|
|
160 |
#' @param filtered (`logical(1)`)
|
|
161 |
#'
|
|
162 |
#' @return
|
|
163 |
#' The stored dataset. If `data.frame` or `MultiAssayExperiment`,
|
|
164 |
#' either raw or as a reactive with current filters applied (depending on `filtered`).
|
|
165 |
#'
|
|
166 |
get_dataset = function(filtered = FALSE) { |
|
167 | 51x |
if (filtered) { |
168 | 33x |
private$data_filtered |
169 |
} else { |
|
170 | 18x |
private$dataset |
171 |
}
|
|
172 |
},
|
|
173 | ||
174 |
#' @description
|
|
175 |
#' Get filter overview of a dataset.
|
|
176 |
#' @return Virtual method, returns nothing and raises an error.
|
|
177 |
get_filter_overview = function() { |
|
178 | ! |
stop("get_filter_overview is an abstract class method") |
179 |
},
|
|
180 | ||
181 |
#' @description
|
|
182 |
#' Gets the key columns for this dataset.
|
|
183 |
#' @return Character vector of variable names
|
|
184 |
get_keys = function() { |
|
185 | 127x |
private$keys |
186 |
},
|
|
187 | ||
188 |
#' @description
|
|
189 |
#' Gets the dataset label.
|
|
190 |
#' @return Character string.
|
|
191 |
get_dataset_label = function() { |
|
192 | 2x |
private$label |
193 |
},
|
|
194 | ||
195 |
# modules ------
|
|
196 |
#' @description
|
|
197 |
#' `shiny` module containing active filters for a dataset, along with a title and a remove button.
|
|
198 |
#' @param id (`character(1)`)
|
|
199 |
#' `shiny` module instance id.
|
|
200 |
#' @param allow_add (`logical(1)`)
|
|
201 |
#' logical flag specifying whether the user will be able to add new filters
|
|
202 |
#'
|
|
203 |
#' @return `shiny.tag`
|
|
204 |
ui_active = function(id, allow_add = TRUE) { |
|
205 | ! |
dataname <- self$get_dataname() |
206 | ! |
checkmate::assert_string(dataname) |
207 | ||
208 | ! |
ns <- NS(id) |
209 | ! |
if_multiple_filter_states <- length(private$get_filter_states()) > 1 |
210 | ! |
tags$span( |
211 | ! |
id = id, |
212 | ! |
class = "teal-slice", |
213 | ! |
include_css_files("filter-panel"), |
214 | ! |
include_js_files(pattern = "icons"), |
215 | ! |
bslib::accordion( |
216 | ! |
id = ns("dataset_filter_accordian"), |
217 | ! |
class = "teal-slice-dataset-filter", |
218 | ! |
bslib::accordion_panel( |
219 | ! |
dataname,
|
220 | ! |
style = "padding: 0; margin: 0;", |
221 | ! |
bslib::page_fluid( |
222 | ! |
id = ns("whole_ui"), |
223 | ! |
style = "margin: 0; padding: 0;", |
224 | ! |
uiOutput(ns("active_filter_badge")), |
225 | ! |
div( |
226 | ! |
id = ns("filter_util_icons"), |
227 | ! |
class = "teal-slice filter-util-icons", |
228 | ! |
if (allow_add) { |
229 | ! |
tags$a( |
230 | ! |
class = "teal-slice filter-icon", |
231 | ! |
tags$i( |
232 | ! |
id = ns("add_filter_icon"), |
233 | ! |
class = "fa fa-plus", |
234 | ! |
title = "fold/expand transform panel", |
235 | ! |
onclick = sprintf( |
236 | ! |
"togglePanelItems(this, '%s', 'fa-plus', 'fa-minus'); |
237 | ! |
if ($(this).hasClass('fa-minus')) { |
238 | ! |
$('#%s .accordion-button.collapsed').click(); |
239 |
}", |
|
240 | ! |
ns("add_panel"), |
241 | ! |
ns("dataset_filter_accordian") |
242 |
)
|
|
243 |
)
|
|
244 |
)
|
|
245 |
},
|
|
246 | ! |
uiOutput(ns("remove_filters_ui")) |
247 |
),
|
|
248 | ! |
bslib::page_fluid( |
249 | ! |
style = "padding: 0px 15px 0px 15px; margin: 0;", |
250 | ! |
if (allow_add) { |
251 | ! |
tags$div( |
252 | ! |
id = ns("add_panel"), |
253 | ! |
class = "add-panel", |
254 | ! |
style = "display: none;", |
255 | ! |
self$ui_add(ns(private$dataname)) |
256 |
)
|
|
257 |
}
|
|
258 |
),
|
|
259 | ! |
tags$div( |
260 | ! |
id = ns("filter_count_ui"), |
261 | ! |
style = "display: none;", |
262 | ! |
tagList( |
263 | ! |
textOutput(ns("filter_count")) |
264 |
)
|
|
265 |
),
|
|
266 | ! |
tags$div( |
267 |
# id needed to insert and remove UI to filter single variable as needed
|
|
268 |
# it is currently also used by the above module to entirely hide this panel
|
|
269 | ! |
id = ns("filters"), |
270 | ! |
class = "parent-hideable-list-group", |
271 | ! |
tagList( |
272 | ! |
lapply( |
273 | ! |
names(private$get_filter_states()), |
274 | ! |
function(x) { |
275 | ! |
tagList(private$get_filter_states()[[x]]$ui_active(id = ns(x))) |
276 |
}
|
|
277 |
)
|
|
278 |
)
|
|
279 |
)
|
|
280 |
)
|
|
281 |
)
|
|
282 |
),
|
|
283 | ! |
tags$script( |
284 | ! |
HTML( |
285 | ! |
sprintf( |
286 |
" |
|
287 | ! |
$(document).ready(function() { |
288 | ! |
$('#%s').appendTo('#%s > .accordion-item > .accordion-header'); |
289 | ! |
$('#%s > .accordion-item > .accordion-header').css({ |
290 | ! |
'display': 'flex' |
291 |
}); |
|
292 | ! |
$('#%s').appendTo('#%s .accordion-header .accordion-title'); |
293 |
}); |
|
294 |
", |
|
295 | ! |
ns("filter_util_icons"), |
296 | ! |
ns("dataset_filter_accordian"), |
297 | ! |
ns("dataset_filter_accordian"), |
298 | ! |
ns("active_filter_badge"), |
299 | ! |
ns("dataset_filter_accordian") |
300 |
)
|
|
301 |
)
|
|
302 |
)
|
|
303 |
)
|
|
304 |
},
|
|
305 | ||
306 |
#' @description
|
|
307 |
#' Server module for a dataset active filters.
|
|
308 |
#'
|
|
309 |
#' @param id (`character(1)`)
|
|
310 |
#' `shiny` module instance id.
|
|
311 |
#' @return `NULL`.
|
|
312 |
srv_active = function(id) { |
|
313 | 7x |
moduleServer( |
314 | 7x |
id = id, |
315 | 7x |
function(input, output, session) { |
316 | 7x |
dataname <- self$get_dataname() |
317 | 7x |
logger::log_debug("FilteredDataset$srv_active initializing, dataname: { dataname }") |
318 | 7x |
checkmate::assert_string(dataname) |
319 | ||
320 | 7x |
filter_count <- reactive({ |
321 | 8x |
length(self$get_filter_state()) |
322 |
}) |
|
323 | ||
324 | 7x |
output$active_filter_badge <- renderUI({ |
325 | 8x |
if (filter_count() == 0) { |
326 | 3x |
return(NULL) |
327 |
}
|
|
328 | 5x |
tags$span( |
329 | 5x |
filter_count(), |
330 | 5x |
class = "teal-slice data-filter-badge-count" |
331 |
)
|
|
332 |
}) |
|
333 | ||
334 | 7x |
output$filter_count <- renderText( |
335 | 7x |
sprintf( |
336 | 7x |
"%d filter%s applied",
|
337 | 7x |
filter_count(), |
338 | 7x |
if (filter_count() != 1) "s" else "" |
339 |
)
|
|
340 |
)
|
|
341 | ||
342 | 7x |
lapply( |
343 | 7x |
names(private$get_filter_states()), |
344 | 7x |
function(x) { |
345 | 12x |
private$get_filter_states()[[x]]$srv_active(id = x) |
346 |
}
|
|
347 |
)
|
|
348 | ||
349 | 7x |
is_filter_removable <- reactive({ |
350 | 8x |
non_anchored <- Filter(function(x) !x$anchored, self$get_filter_state()) |
351 | 8x |
isTRUE(length(non_anchored) > 0) |
352 |
}) |
|
353 | ||
354 | 7x |
private$session_bindings[[session$ns("get_filter_state")]] <- observeEvent( |
355 | 7x |
self$get_filter_state(), |
356 | 7x |
ignoreInit = TRUE, |
357 |
{
|
|
358 | 3x |
shinyjs::hide("filter_count_ui") |
359 | 3x |
shinyjs::show("filters") |
360 | 3x |
shinyjs::toggle("remove_filters_ui", condition = is_filter_removable()) |
361 | 3x |
shinyjs::runjs( |
362 | 3x |
sprintf( |
363 | 3x |
"setAndRemoveClass('#%s', 'fa-angle-down', 'fa-angle-right')",
|
364 | 3x |
session$ns("collapse_icon") |
365 |
)
|
|
366 |
)
|
|
367 |
}
|
|
368 |
)
|
|
369 | ||
370 | 7x |
output$remove_filters_ui <- renderUI({ |
371 | 8x |
req(is_filter_removable()) |
372 | 5x |
tags$div( |
373 | 5x |
style = "display: flex;", |
374 | 5x |
actionLink( |
375 | 5x |
session$ns("remove_filters"), |
376 | 5x |
label = "", |
377 | 5x |
icon = icon("far fa-circle-xmark"), |
378 | 5x |
class = "teal-slice filter-icon" |
379 |
)
|
|
380 |
)
|
|
381 |
}) |
|
382 | ||
383 |
# If the accordion input is `NULL` it is being collapsed.
|
|
384 |
# It has the accordion panel label if it is expanded.
|
|
385 | 7x |
observeEvent(input$dataset_filter_accordian, ignoreNULL = FALSE, { |
386 | 5x |
if (is.null(input$dataset_filter_accordian)) { |
387 |
# Hiding the `add_panel` dropdown and changing the minus icon to plus
|
|
388 |
# TODO: simplify this implementation. This is done in multiple places
|
|
389 | 5x |
shinyjs::runjs( |
390 | 5x |
sprintf( |
391 | 5x |
"var element = $('#%s.fa-minus'); |
392 | 5x |
if (element.length) { |
393 | 5x |
element.click(); |
394 | 5x |
$('#%s').hide(); |
395 |
}", |
|
396 | 5x |
session$ns("add_filter_icon"), |
397 | 5x |
session$ns("add_panel") |
398 |
)
|
|
399 |
)
|
|
400 |
}
|
|
401 |
}) |
|
402 | ||
403 | 7x |
private$session_bindings[[session$ns("remove_filters")]] <- observeEvent(input$remove_filters, { |
404 | 1x |
logger::log_debug("FilteredDataset$srv_active@1 removing all non-anchored filters, dataname: { dataname }") |
405 | 1x |
self$clear_filter_states() |
406 |
}) |
|
407 | ||
408 | 7x |
private$session_bindings[[session$ns("inputs")]] <- list( |
409 | 7x |
destroy = function() { |
410 | 8x |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
411 |
}
|
|
412 |
)
|
|
413 | ||
414 | 7x |
self$srv_add(private$dataname) |
415 | ||
416 | 7x |
NULL
|
417 |
}
|
|
418 |
)
|
|
419 |
},
|
|
420 | ||
421 |
#' @description
|
|
422 |
#' UI module to add filter variable for this dataset.
|
|
423 |
#'
|
|
424 |
#' @param id (`character(1)`)
|
|
425 |
#' `shiny` module instance id.
|
|
426 |
#'
|
|
427 |
#' @return Virtual method, returns nothing and raises error.
|
|
428 |
ui_add = function(id) { |
|
429 | 1x |
stop("Pure virtual method") |
430 |
},
|
|
431 | ||
432 |
#' @description
|
|
433 |
#' Server module to add filter variable for this dataset.
|
|
434 |
#' For this class `srv_add` calls multiple modules
|
|
435 |
#' of the same name from `FilterStates` as `MAEFilteredDataset`
|
|
436 |
#' contains one `FilterStates` object for `colData` and one for each experiment.
|
|
437 |
#'
|
|
438 |
#' @param id (`character(1)`)
|
|
439 |
#' `shiny` module instance id.
|
|
440 |
#'
|
|
441 |
#' @return `NULL`.
|
|
442 |
srv_add = function(id) { |
|
443 | 7x |
moduleServer( |
444 | 7x |
id = id, |
445 | 7x |
function(input, output, session) { |
446 | 7x |
logger::log_debug("FilteredDataset$srv_add initializing, dataname: { private$dataname }") |
447 | 7x |
elems <- private$get_filter_states() |
448 | 7x |
elem_names <- names(private$get_filter_states()) |
449 | 7x |
lapply( |
450 | 7x |
elem_names,
|
451 | 7x |
function(elem_name) elems[[elem_name]]$srv_add(elem_name) |
452 |
)
|
|
453 | ||
454 | 7x |
NULL
|
455 |
}
|
|
456 |
)
|
|
457 |
},
|
|
458 | ||
459 |
#' @description
|
|
460 |
#' Object and dependencies cleanup.
|
|
461 |
#'
|
|
462 |
#' - Destroy inputs and observers stored in `private$session_bindings`
|
|
463 |
#' - Finalize `FilterStates` stored in `private$filter_states`
|
|
464 |
#'
|
|
465 |
#' @return `NULL`, invisibly.
|
|
466 |
finalize = function() { |
|
467 | 244x |
.finalize_session_bindings(self, private) |
468 | 244x |
lapply(private$filter_states, function(x) x$finalize()) |
469 | 244x |
invisible(NULL) |
470 |
}
|
|
471 |
),
|
|
472 |
# private fields ----
|
|
473 |
private = list( |
|
474 |
dataset = NULL, # data.frame or MultiAssayExperiment |
|
475 |
data_filtered = NULL, |
|
476 |
data_filtered_fun = NULL, # function |
|
477 |
filter_states = list(), |
|
478 |
dataname = character(0), |
|
479 |
keys = character(0), |
|
480 |
label = character(0), |
|
481 |
session_bindings = list(), |
|
482 | ||
483 |
# Adds `FilterStates` to the `private$filter_states`.
|
|
484 |
# `FilterStates` is added once for each element of the dataset.
|
|
485 |
# @param filter_states (`FilterStates`)
|
|
486 |
# @param id (`character(1)`)
|
|
487 |
add_filter_states = function(filter_states, id) { |
|
488 | 209x |
checkmate::assert_class(filter_states, "FilterStates") |
489 | 209x |
checkmate::assert_string(id) |
490 | 209x |
x <- stats::setNames(list(filter_states), id) |
491 | 209x |
private$filter_states <- c(private$get_filter_states(), x) |
492 |
},
|
|
493 | ||
494 |
# @description
|
|
495 |
# Gets `FilterStates` objects in this `FilteredDataset`.
|
|
496 |
# @return list of `FilterStates` objects.
|
|
497 |
get_filter_states = function() { |
|
498 | 626x |
private$filter_states |
499 |
}
|
|
500 |
)
|
|
501 |
)
|
1 |
#' Complete filter specification
|
|
2 |
#'
|
|
3 |
#' Create `teal_slices` object to package multiple filters and additional settings.
|
|
4 |
#' Check out [`teal_slices-utilities`] functions for working with `teal_slices` object.
|
|
5 |
#'
|
|
6 |
#' `teal_slices()` collates multiple `teal_slice` objects into a `teal_slices` object,
|
|
7 |
#' a complete filter specification. This is used by all classes above `FilterState`
|
|
8 |
#' as well as `filter_panel_api` wrapper functions.
|
|
9 |
#' `teal_slices` has attributes that modify the behavior of the filter panel, which are resolved by different classes.
|
|
10 |
#'
|
|
11 |
#' `include_varnames` and `exclude_varnames` determine which variables can have filters assigned.
|
|
12 |
#' The former enumerates allowed variables, the latter enumerates forbidden values.
|
|
13 |
#' Since these could be mutually exclusive, it is impossible to set both allowed and forbidden
|
|
14 |
#' variables for one data set in one `teal_slices`.
|
|
15 |
#'
|
|
16 |
#' @param ... any number of `teal_slice` objects.
|
|
17 |
#' @param include_varnames,exclude_varnames (`named list`s of `character`) where list names
|
|
18 |
#' match names of data sets and vector elements match variable names in respective data sets;
|
|
19 |
#' specify which variables are allowed to be filtered; see `Details`.
|
|
20 |
#' @param count_type `r lifecycle::badge("experimental")`
|
|
21 |
#' _This is a new feature. Do kindly share your opinions on
|
|
22 |
#' [`teal.slice`'s GitHub repository](https://github.com/insightsengineering/teal.slice/)._
|
|
23 |
#'
|
|
24 |
#' (`character(1)`) string specifying how observations are tallied by these filter states.
|
|
25 |
#' Possible options:
|
|
26 |
#' - `"none"` (default) to have counts of single `FilterState` to show unfiltered number only.
|
|
27 |
#' - `"all"` to have counts of single `FilterState` to show number of observation in filtered
|
|
28 |
#' and unfiltered dataset. Note, that issues were reported when using this option with `MultiAssayExperiment`.
|
|
29 |
#' Please make sure that adding new filters doesn't fail on target platform before deploying for production.
|
|
30 |
#' @param allow_add (`logical(1)`) logical flag specifying whether the user will be able to add new filters
|
|
31 |
#'
|
|
32 |
#' @return
|
|
33 |
#' `teal_slices`, which is an unnamed list of `teal_slice` objects.
|
|
34 |
#'
|
|
35 |
#' @examples
|
|
36 |
#' filter_1 <- teal_slice(
|
|
37 |
#' dataname = "dataname1",
|
|
38 |
#' varname = "varname1",
|
|
39 |
#' choices = letters,
|
|
40 |
#' selected = "b",
|
|
41 |
#' keep_na = TRUE,
|
|
42 |
#' fixed = FALSE,
|
|
43 |
#' extra1 = "extraone"
|
|
44 |
#' )
|
|
45 |
#' filter_2 <- teal_slice(
|
|
46 |
#' dataname = "dataname1",
|
|
47 |
#' varname = "varname2",
|
|
48 |
#' choices = 1:10,
|
|
49 |
#' keep_na = TRUE,
|
|
50 |
#' selected = 2,
|
|
51 |
#' fixed = TRUE,
|
|
52 |
#' anchored = FALSE,
|
|
53 |
#' extra2 = "extratwo"
|
|
54 |
#' )
|
|
55 |
#' filter_3 <- teal_slice(
|
|
56 |
#' dataname = "dataname2",
|
|
57 |
#' varname = "varname3",
|
|
58 |
#' choices = 1:10 / 10,
|
|
59 |
#' keep_na = TRUE,
|
|
60 |
#' selected = 0.2,
|
|
61 |
#' fixed = TRUE,
|
|
62 |
#' anchored = FALSE,
|
|
63 |
#' extra1 = "extraone",
|
|
64 |
#' extra2 = "extratwo"
|
|
65 |
#' )
|
|
66 |
#'
|
|
67 |
#' all_filters <- teal_slices(
|
|
68 |
#' filter_1,
|
|
69 |
#' filter_2,
|
|
70 |
#' filter_3,
|
|
71 |
#' exclude_varnames = list(
|
|
72 |
#' "dataname1" = "varname2"
|
|
73 |
#' )
|
|
74 |
#' )
|
|
75 |
#'
|
|
76 |
#' is.teal_slices(all_filters)
|
|
77 |
#' all_filters[1:2]
|
|
78 |
#' c(all_filters[1], all_filters[2])
|
|
79 |
#' print(all_filters)
|
|
80 |
#' print(all_filters, trim_lines = FALSE)
|
|
81 |
#'
|
|
82 |
#' @seealso
|
|
83 |
#' - [`teal_slice`] for creating constituent elements of `teal_slices`
|
|
84 |
#' - `teal::slices_store` for robust utilities for saving and loading `teal_slices` in `JSON` format
|
|
85 |
#' - [`is.teal_slices`], [`as.teal_slices`], [`as.list.teal_slices`], [`[.teal_slices`], [`c.teal_slices`]
|
|
86 |
#' [`print.teal_slices`], [`format.teal_slices`]
|
|
87 |
#'
|
|
88 |
#' @export
|
|
89 |
#'
|
|
90 |
teal_slices <- function(..., |
|
91 |
exclude_varnames = NULL, |
|
92 |
include_varnames = NULL, |
|
93 |
count_type = NULL, |
|
94 |
allow_add = TRUE) { |
|
95 | 664x |
slices <- list(...) |
96 | 664x |
checkmate::assert_list(slices, types = "teal_slice", any.missing = FALSE) |
97 | 663x |
slices_id <- isolate(vapply(slices, `[[`, character(1L), "id")) |
98 | 663x |
if (any(duplicated(slices_id))) { |
99 | 1x |
stop( |
100 | 1x |
"Some teal_slice objects have the same id:\n",
|
101 | 1x |
toString(unique(slices_id[duplicated(slices_id)])) |
102 |
)
|
|
103 |
}
|
|
104 | 662x |
checkmate::assert_list(exclude_varnames, names = "named", types = "character", null.ok = TRUE, min.len = 1) |
105 | 661x |
checkmate::assert_list(include_varnames, names = "named", types = "character", null.ok = TRUE, min.len = 1) |
106 | 660x |
checkmate::assert_character(count_type, len = 1, null.ok = TRUE) |
107 | 658x |
checkmate::assert_subset(count_type, choices = c("all", "none"), empty.ok = TRUE) |
108 | 657x |
checkmate::assert_logical(allow_add) |
109 | ||
110 | 656x |
duplicated_datasets <- intersect(names(include_varnames), names(exclude_varnames)) |
111 | 656x |
if (length(duplicated_datasets)) { |
112 | 1x |
stop( |
113 | 1x |
"Some datasets are specified in both, include_varnames and exclude_varnames:\n",
|
114 | 1x |
toString(duplicated_datasets) |
115 |
)
|
|
116 |
}
|
|
117 | ||
118 | 655x |
structure( |
119 | 655x |
slices,
|
120 | 655x |
exclude_varnames = exclude_varnames, |
121 | 655x |
include_varnames = include_varnames, |
122 | 655x |
count_type = count_type, |
123 | 655x |
allow_add = allow_add, |
124 | 655x |
class = c("teal_slices", class(slices)) |
125 |
)
|
|
126 |
}
|
|
127 | ||
128 |
#' `teal_slices` utility functions
|
|
129 |
#'
|
|
130 |
#' Helper functions for working with [`teal_slices`] object.
|
|
131 |
#' @param x object to test for `teal_slices`, object to convert to `teal_slices` or a `teal_slices` object
|
|
132 |
#' @param i (`character` or `numeric` or `logical`) indicating which elements to extract
|
|
133 |
#' @param recursive (`logical(1)`) flag specifying whether to also convert to list the elements of this `teal_slices`
|
|
134 |
#' @param ... additional arguments passed to other functions.
|
|
135 |
#' @name teal_slices-utilities
|
|
136 |
#' @inherit teal_slices examples
|
|
137 |
#' @keywords internal
|
|
138 | ||
139 |
#' @rdname teal_slices-utilities
|
|
140 |
#' @export
|
|
141 |
#'
|
|
142 |
is.teal_slices <- function(x) { # nolint |
|
143 | 373x |
inherits(x, "teal_slices") |
144 |
}
|
|
145 | ||
146 |
#' @rdname teal_slices-utilities
|
|
147 |
#' @export
|
|
148 |
#'
|
|
149 |
as.teal_slices <- function(x) { # nolint |
|
150 | ! |
checkmate::assert_list(x) |
151 | ! |
lapply(x, checkmate::assert_list, names = "named", .var.name = "list element") |
152 | ||
153 | ! |
attrs <- attributes(unclass(x)) |
154 | ! |
ans <- lapply(x, function(x) if (is.teal_slice(x)) x else as.teal_slice(x)) |
155 | ! |
do.call(teal_slices, c(ans, attrs)) |
156 |
}
|
|
157 | ||
158 | ||
159 |
#' @rdname teal_slices-utilities
|
|
160 |
#' @export
|
|
161 |
#'
|
|
162 |
as.list.teal_slices <- function(x, recursive = FALSE, ...) { # nolint |
|
163 | 1047x |
ans <- unclass(x) |
164 | 46x |
if (recursive) ans[] <- lapply(ans, as.list) |
165 | 1047x |
ans
|
166 |
}
|
|
167 | ||
168 | ||
169 |
#' @rdname teal_slices-utilities
|
|
170 |
#' @export
|
|
171 |
#'
|
|
172 |
`[.teal_slices` <- function(x, i) { |
|
173 | 3x |
if (missing(i)) i <- seq_along(x) |
174 | 475x |
if (length(i) == 0L) { |
175 | 162x |
return(x[0]) |
176 |
}
|
|
177 | 1x |
if (is.logical(i) && length(i) > length(x)) stop("subscript out of bounds") |
178 | 1x |
if (is.numeric(i) && max(i) > length(x)) stop("subscript out of bounds") |
179 | 311x |
if (is.character(i)) { |
180 | 1x |
if (!all(is.element(i, names(x)))) stop("subscript out of bounds") |
181 | 2x |
i <- which(is.element(i, names(x))) |
182 |
}
|
|
183 | ||
184 | 310x |
y <- NextMethod("[") |
185 | 310x |
attrs <- attributes(x) |
186 | 310x |
attrs$names <- attrs$names[i] |
187 | 310x |
attributes(y) <- attrs |
188 | 310x |
y
|
189 |
}
|
|
190 | ||
191 | ||
192 |
#' @rdname teal_slices-utilities
|
|
193 |
#' @export
|
|
194 |
#'
|
|
195 |
c.teal_slices <- function(...) { |
|
196 | 211x |
x <- list(...) |
197 | 211x |
checkmate::assert_true(all(vapply(x, is.teal_slices, logical(1L))), .var.name = "all arguments are teal_slices") |
198 | ||
199 | 210x |
all_attributes <- lapply(x, attributes) |
200 | 210x |
all_attributes <- coalesce_r(all_attributes) |
201 | 210x |
all_attributes <- all_attributes[names(all_attributes) != "class"] |
202 | ||
203 | 210x |
do.call( |
204 | 210x |
teal_slices,
|
205 | 210x |
c( |
206 | 210x |
unique(unlist(x, recursive = FALSE)), |
207 | 210x |
all_attributes
|
208 |
)
|
|
209 |
)
|
|
210 |
}
|
|
211 | ||
212 | ||
213 |
#' @rdname teal_slices-utilities
|
|
214 |
#' @param show_all (`logical(1)`) whether to display non-null elements of constituent `teal_slice` objects
|
|
215 |
#' @param trim_lines (`logical(1)`) whether to trim lines
|
|
216 |
#' @export
|
|
217 |
#'
|
|
218 |
format.teal_slices <- function(x, show_all = FALSE, trim_lines = TRUE, ...) { |
|
219 | 46x |
checkmate::assert_flag(show_all) |
220 | 46x |
checkmate::assert_flag(trim_lines) |
221 | ||
222 | 46x |
x <- as.list(x, recursive = TRUE) |
223 | 46x |
attrs <- attributes(x) |
224 | 46x |
attributes(x) <- NULL |
225 | 46x |
slices_list <- list(slices = x, attributes = attrs) |
226 | 46x |
slices_list <- Filter(Negate(is.null), slices_list) # drop attributes if empty |
227 | ||
228 | 21x |
if (!show_all) slices_list$slices <- lapply(slices_list$slices, function(slice) Filter(Negate(is.null), slice)) |
229 | ||
230 | 46x |
jsonify(slices_list, trim_lines) |
231 |
}
|
|
232 | ||
233 |
#' @rdname teal_slices-utilities
|
|
234 |
#' @export
|
|
235 |
#'
|
|
236 |
print.teal_slices <- function(x, ...) { |
|
237 | 3x |
cat(format(x, ...), "\n") |
238 | 3x |
invisible(x) |
239 |
}
|
|
240 | ||
241 | ||
242 |
#' `setdiff` method for `teal_slices`
|
|
243 |
#'
|
|
244 |
#' Compare two teal slices objects and return `teal_slices` containing slices present in `x` but not in `y`.
|
|
245 |
#' @param x,y (`teal_slices`)
|
|
246 |
#' @return `teal_slices`
|
|
247 |
#' @keywords internal
|
|
248 |
#'
|
|
249 |
setdiff_teal_slices <- function(x, y) { |
|
250 | 14x |
Filter( |
251 | 14x |
function(xx) { |
252 | 12x |
!any(vapply(y, function(yy) identical(yy, xx), logical(1))) |
253 |
},
|
|
254 | 14x |
x
|
255 |
)
|
|
256 |
}
|
|
257 | ||
258 |
#' Recursively coalesce list elements.
|
|
259 |
#'
|
|
260 |
#' Returns first element of list that it not `NULL`, recursively.
|
|
261 |
#'
|
|
262 |
#' Given a list of atomic vectors, the first non-null element is returned.
|
|
263 |
#' Given a list of lists, for all `names` found in all elements of the list
|
|
264 |
#' the first non-null element of a given name is returned.
|
|
265 |
#'
|
|
266 |
#' This function is used internally in `c.teal_slices` to manage `teal_slices` attributes.
|
|
267 |
#'
|
|
268 |
#' @param x (`list`), either of atomic vectors or of named lists
|
|
269 |
#' @return
|
|
270 |
#' Either an atomic vector of length 1 or a (potentially nested) list.
|
|
271 |
#'
|
|
272 |
#' @keywords internal
|
|
273 |
#'
|
|
274 |
coalesce_r <- function(x) { |
|
275 | 1312x |
checkmate::assert_list(x) |
276 | 1311x |
xnn <- Filter(Negate(is.null), x) |
277 | 1311x |
if (all(vapply(xnn, is.atomic, logical(1L)))) { |
278 | 884x |
return(xnn[[1L]]) |
279 |
}
|
|
280 | 427x |
lapply(x, checkmate::assert_list, names = "named", null.ok = TRUE, .var.name = "list element") |
281 | 426x |
all_names <- unique(unlist(lapply(x, names))) |
282 | 426x |
sapply(all_names, function(nm) coalesce_r(lapply(x, `[[`, nm)), simplify = FALSE) |
283 |
}
|
1 |
# FilterState ------
|
|
2 | ||
3 |
#' @name FilterState
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title `FilterState` abstract class
|
|
7 |
#'
|
|
8 |
#' @description Abstract class to encapsulate single filter state.
|
|
9 |
#'
|
|
10 |
#' @details
|
|
11 |
#' This class is responsible for managing a single filter item within a `FilteredData` object
|
|
12 |
#' and outputs a condition call (logical predicate) for subsetting one variable.
|
|
13 |
#' Filter states depend on the variable type:
|
|
14 |
#' (`logical`, `integer`, `numeric`, `character`, `factor`, `Date`, `POSIXct`, `POSIXlt`)
|
|
15 |
#' and `FilterState` subclasses exist that correspond to those types.
|
|
16 |
#' - `logical`: `class = LogicalFilterState`
|
|
17 |
#' - `integer`: `class = RangeFilterState`
|
|
18 |
#' - `numeric`: `class = RangeFilterState`
|
|
19 |
#' - `character`: `class = ChoicesFilterState`
|
|
20 |
#' - `factor`: `class = ChoicesFilterState`
|
|
21 |
#' - `Date`: `class = DateFilterState`
|
|
22 |
#' - `POSIXct`, `POSIXlt`: `class = DatetimeFilterState`
|
|
23 |
#' - all `NA` entries: `class: FilterState`, cannot be filtered
|
|
24 |
#' - default: `FilterState`, cannot be filtered
|
|
25 |
#'
|
|
26 |
#' Each variable's filter state is an `R6` object keeps the variable that is filtered,
|
|
27 |
#' a `teal_slice` object that describes the filter state, as well as a `shiny` module (UI and server)
|
|
28 |
#' that allows the user to alter the filter state.
|
|
29 |
#' Changes to the filter state that cause some observations to be omitted
|
|
30 |
#' trigger the `get_call` method and every `R` function call up in the reactive chain.
|
|
31 |
#'
|
|
32 |
#' @section Modifying state:
|
|
33 |
#' Modifying a `FilterState` object is possible in three scenarios:
|
|
34 |
#' - In an interactive session, by passing an appropriate `teal_slice` to the `set_state` method.
|
|
35 |
#' - In a running application, by changing appropriate inputs.
|
|
36 |
#' - In a running application, by using [filter_state_api] which directly uses
|
|
37 |
#' `set_state` method of the `FilterState` object.
|
|
38 |
#'
|
|
39 |
#' @keywords internal
|
|
40 |
#'
|
|
41 |
FilterState <- R6::R6Class( # nolint |
|
42 |
"FilterState",
|
|
43 | ||
44 |
# public methods ----
|
|
45 |
public = list( |
|
46 | ||
47 |
#' @description
|
|
48 |
#' Initialize a `FilterState` object.
|
|
49 |
#'
|
|
50 |
#' @param x (`vector`)
|
|
51 |
#' variable to be filtered.
|
|
52 |
#' @param x_reactive (`reactive`)
|
|
53 |
#' returning vector of the same type as `x`. Is used to update
|
|
54 |
#' counts following the change in values of the filtered dataset.
|
|
55 |
#' If it is set to `reactive(NULL)` then counts based on filtered
|
|
56 |
#' dataset are not shown.
|
|
57 |
#' @param slice (`teal_slice`)
|
|
58 |
#' specification of this filter state.
|
|
59 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`.
|
|
60 |
#' `get_state` returns `teal_slice` object which can be reused in other places.
|
|
61 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e.
|
|
62 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`.
|
|
63 |
#' @param extract_type (`character`)
|
|
64 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values:
|
|
65 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed
|
|
66 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>`
|
|
67 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]`
|
|
68 |
#'
|
|
69 |
#' @return Object of class `FilterState`, invisibly.
|
|
70 |
#'
|
|
71 |
initialize = function(x, |
|
72 |
x_reactive = reactive(NULL), |
|
73 |
slice,
|
|
74 |
extract_type = character(0)) { |
|
75 | 364x |
isolate({ |
76 | 364x |
checkmate::assert_class(x_reactive, "reactive") |
77 | 363x |
checkmate::assert_class(slice, "teal_slice") |
78 | 361x |
checkmate::assert_character(extract_type, max.len = 1, any.missing = FALSE) |
79 | 361x |
if (length(extract_type) == 1) { |
80 | 50x |
checkmate::assert_choice(extract_type, choices = c("list", "matrix")) |
81 |
}
|
|
82 | 360x |
logger::log_debug("{ class(self)[1] } initializing for slice: { slice$id }") |
83 | ||
84 |
# Set data properties.
|
|
85 | 360x |
private$x <- x |
86 | 360x |
private$x_reactive <- x_reactive |
87 |
# Set derived data properties.
|
|
88 | 360x |
private$na_count <- sum(is.na(x)) |
89 | 360x |
private$filtered_na_count <- reactive( |
90 | 360x |
if (!is.null(private$x_reactive())) { |
91 | ! |
sum(is.na(private$x_reactive())) |
92 |
}
|
|
93 |
)
|
|
94 |
# Set extract type.
|
|
95 | 360x |
private$extract_type <- extract_type |
96 | ||
97 |
# Set state properties.
|
|
98 | 19x |
if (is.null(isolate(slice$keep_na)) && anyNA(x)) slice$keep_na <- TRUE |
99 | 360x |
private$teal_slice <- slice |
100 |
# Obtain variable label.
|
|
101 | 360x |
varlabel <- attr(x, "label", exact = TRUE) |
102 |
# Display only when different from varname.
|
|
103 | 360x |
private$varlabel <- |
104 | 360x |
if (is.null(varlabel) || identical(varlabel, private$get_varname())) { |
105 | 359x |
character(0) |
106 |
} else { |
|
107 | 1x |
varlabel
|
108 |
}
|
|
109 | ||
110 | 360x |
private$state_history <- reactiveVal(list()) |
111 | ||
112 | 360x |
invisible(self) |
113 |
}) |
|
114 |
},
|
|
115 | ||
116 |
#' @description
|
|
117 |
#' Returns a formatted string representing this `FilterState` object.
|
|
118 |
#'
|
|
119 |
#' @param show_all (`logical(1)`) passed to `format.teal_slice`
|
|
120 |
#' @param trim_lines (`logical(1)`) passed to `format.teal_slice`
|
|
121 |
#'
|
|
122 |
#' @return `character(1)` the formatted string
|
|
123 |
#'
|
|
124 |
format = function(show_all = FALSE, trim_lines = TRUE) { |
|
125 | 68x |
sprintf( |
126 | 68x |
"%s:\n%s",
|
127 | 68x |
class(self)[1], |
128 | 68x |
format(self$get_state(), show_all = show_all, trim_lines = trim_lines) |
129 |
)
|
|
130 |
},
|
|
131 | ||
132 |
#' @description
|
|
133 |
#' Prints this `FilterState` object.
|
|
134 |
#'
|
|
135 |
#' @param ... additional arguments
|
|
136 |
#'
|
|
137 |
print = function(...) { |
|
138 | 14x |
cat(isolate(self$format(...))) |
139 |
},
|
|
140 | ||
141 |
#' @description
|
|
142 |
#' Sets mutable parameters of the filter state.
|
|
143 |
#' - `fixed` state is prevented from changing state
|
|
144 |
#' - `anchored` state is prevented from removing state
|
|
145 |
#'
|
|
146 |
#' @param state (`teal_slice`)
|
|
147 |
#'
|
|
148 |
#' @return `self` invisibly
|
|
149 |
#'
|
|
150 |
set_state = function(state) { |
|
151 | 89x |
if (identical(state, private$slice)) { |
152 | ! |
return(self) |
153 |
}
|
|
154 | 88x |
isolate({ |
155 | 88x |
checkmate::assert_class(state, "teal_slice") |
156 | 88x |
if (private$is_fixed()) { |
157 | 1x |
warning("attempt to set state on fixed filter aborted id: ", private$get_id()) |
158 |
} else { |
|
159 | 87x |
logger::log_debug("{ class(self)[1] }$set_state setting state of filter id: { private$get_id() }") |
160 | 87x |
if (!is.null(state$selected)) { |
161 | 78x |
private$set_selected(state$selected) |
162 |
}
|
|
163 | 75x |
if (!is.null(state$keep_na)) { |
164 | 16x |
private$set_keep_na(state$keep_na) |
165 |
}
|
|
166 | 75x |
if (!is.null(state$keep_inf)) { |
167 | 9x |
private$set_keep_inf(state$keep_inf) |
168 |
}
|
|
169 | 75x |
current_state <- sprintf( |
170 | 75x |
"selected: %s; keep_na: %s; keep_inf: %s",
|
171 | 75x |
toString(private$get_selected()), |
172 | 75x |
private$get_keep_na(), |
173 | 75x |
private$get_keep_inf() |
174 |
)
|
|
175 |
}
|
|
176 | ||
177 | 76x |
invisible(self) |
178 |
}) |
|
179 |
},
|
|
180 | ||
181 | ||
182 |
#' @description
|
|
183 |
#' Returns a complete description of the filter state.
|
|
184 |
#'
|
|
185 |
#' @return A `teal_slice` object.
|
|
186 |
#'
|
|
187 |
get_state = function() { |
|
188 | 785x |
private$teal_slice |
189 |
},
|
|
190 | ||
191 |
#' @description
|
|
192 |
#' Returns reproducible condition call for current selection relevant
|
|
193 |
#' for selected variable type.
|
|
194 |
#' Method is using internal reactive values which makes it reactive
|
|
195 |
#' and must be executed in reactive or isolated context.
|
|
196 |
#'
|
|
197 |
get_call = function() { |
|
198 | 1x |
stop("this is a virtual method") |
199 |
},
|
|
200 | ||
201 |
#' @description
|
|
202 |
#' `shiny` module server.
|
|
203 |
#'
|
|
204 |
#' @param id (`character(1)`)
|
|
205 |
#' `shiny` module instance id.
|
|
206 |
#'
|
|
207 |
#' @param remove_callback (`function`)
|
|
208 |
#' callback to handle removal of this `FilterState` object from `state_list`
|
|
209 |
#'
|
|
210 |
#' @return Reactive expression signaling that remove button has been clicked.
|
|
211 |
#'
|
|
212 |
server = function(id, remove_callback) { |
|
213 | 12x |
moduleServer( |
214 | 12x |
id = id, |
215 | 12x |
function(input, output, session) { |
216 | 12x |
logger::log_debug("{ class(self)[1] }$server initializing module for slice: { private$get_id() } ") |
217 | 12x |
private$server_summary("summary") |
218 | 12x |
if (private$is_fixed()) { |
219 | ! |
private$server_inputs_fixed("inputs") |
220 |
} else { |
|
221 | 12x |
private$server_inputs("inputs") |
222 |
}
|
|
223 | ||
224 | 12x |
private$session_bindings[[session$ns("state")]] <- observeEvent( |
225 | 12x |
eventExpr = list(private$get_selected(), private$get_keep_na(), private$get_keep_inf()), |
226 | 12x |
handlerExpr = { |
227 | 4x |
current_state <- as.list(self$get_state()) |
228 | 4x |
history <- private$state_history() |
229 | 4x |
history_update <- c(history, list(current_state)) |
230 | 4x |
private$state_history(history_update) |
231 |
}
|
|
232 |
)
|
|
233 | ||
234 | 12x |
private$session_bindings[[session$ns("back")]] <- observeEvent( |
235 | 12x |
eventExpr = input$back, |
236 | 12x |
handlerExpr = { |
237 | ! |
history <- rev(private$state_history()) |
238 | ! |
slice <- history[[2L]] |
239 | ! |
history_update <- rev(history[-(1:2)]) |
240 | ! |
private$state_history(history_update) |
241 | ! |
self$set_state(as.teal_slice(slice)) |
242 |
}
|
|
243 |
)
|
|
244 | ||
245 | 12x |
private$session_bindings[[session$ns("reset")]] <- observeEvent( |
246 | 12x |
eventExpr = input$reset, |
247 | 12x |
handlerExpr = { |
248 | ! |
slice <- private$state_history()[[1L]] |
249 | ! |
self$set_state(as.teal_slice(slice)) |
250 |
}
|
|
251 |
)
|
|
252 | ||
253 |
# Buttons for rewind/reset are disabled upon change in history to prevent double-clicking.
|
|
254 |
# Re-enabling occurs after 100 ms, after they are potentially hidden when no history is present.
|
|
255 | 12x |
private$session_bindings[[session$ns("state_history")]] <- observeEvent( |
256 | 12x |
eventExpr = private$state_history(), |
257 | 12x |
handlerExpr = { |
258 | 4x |
shinyjs::disable(id = "back") |
259 | 4x |
shinyjs::disable(id = "reset") |
260 | 4x |
shinyjs::delay( |
261 | 4x |
ms = 100, |
262 | 4x |
expr = { |
263 | ! |
shinyjs::toggleElement(id = "back", condition = length(private$state_history()) > 1L) |
264 | ! |
shinyjs::enable(id = "back") |
265 |
}
|
|
266 |
)
|
|
267 | 4x |
shinyjs::delay( |
268 | 4x |
ms = 100, |
269 | 4x |
expr = { |
270 | ! |
shinyjs::toggleElement(id = "reset", condition = length(private$state_history()) > 1L) |
271 | ! |
shinyjs::enable(id = "reset") |
272 |
}
|
|
273 |
)
|
|
274 |
}
|
|
275 |
)
|
|
276 | ||
277 | 12x |
private$session_bindings[[session$ns("remove")]] <- observeEvent( |
278 | 12x |
once = TRUE, # remove button can be called once, should be destroyed afterwards |
279 | 12x |
ignoreInit = TRUE, # ignoreInit: should not matter because we destroy the previous input set of the UI |
280 | 12x |
eventExpr = input$remove, # when remove button is clicked in the FilterState ui |
281 | 12x |
handlerExpr = remove_callback() |
282 |
)
|
|
283 | ||
284 | 12x |
private$session_bindings[[session$ns("inputs")]] <- list( |
285 | 12x |
destroy = function() { |
286 | 16x |
logger::log_debug("Destroying { class(self)[1] } inputs and observers; id: { private$get_id() }") |
287 | 16x |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
288 |
}
|
|
289 |
)
|
|
290 | ||
291 | 12x |
private$state_history <- reactiveVal(list()) |
292 | ||
293 | 12x |
NULL
|
294 |
}
|
|
295 |
)
|
|
296 |
},
|
|
297 | ||
298 |
#' @description
|
|
299 |
#' `shiny` UI module.
|
|
300 |
#' The UI for this class contains simple message stating that it is not supported.
|
|
301 |
#' @param id (`character(1)`)
|
|
302 |
#' `shiny` module instance id.
|
|
303 |
#' @param parent_id (`character(1)`) id of the `FilterStates` card container
|
|
304 |
ui = function(id, parent_id = "cards") { |
|
305 | 12x |
ns <- NS(id) |
306 | ||
307 |
# Filter card consists of header and body, arranged in a single column.
|
|
308 |
# Body is hidden and is toggled by clicking on header.
|
|
309 |
## Header consists of title and summary, arranged in a column.
|
|
310 |
### Title consists of conditional icon, varname, conditional varlabel, and controls, arranged in a row.
|
|
311 |
### Summary consists of value and controls, arranged in a row.
|
|
312 | ||
313 | 12x |
tags$div( |
314 | 12x |
id = id, |
315 | 12x |
class = "panel filter-card", |
316 | 12x |
include_js_files("count-bar-labels.js"), |
317 | 12x |
tags$div( |
318 | 12x |
class = "filter-card-header", |
319 | 12x |
`data-toggle` = "collapse", |
320 | 12x |
`data-bs-toggle` = "collapse", |
321 | 12x |
href = paste0("#", ns("body")), |
322 | 12x |
tags$div( |
323 | 12x |
class = "filter-card-title", |
324 | 12x |
if (private$is_anchored() && private$is_fixed()) { |
325 | ! |
icon("anchor-lock", class = "filter-card-icon") |
326 | 12x |
} else if (private$is_anchored() && !private$is_fixed()) { |
327 | ! |
icon("anchor", class = "filter-card-icon") |
328 | 12x |
} else if (!private$is_anchored() && private$is_fixed()) { |
329 | ! |
icon("lock", class = "filter-card-icon") |
330 |
},
|
|
331 | 12x |
tags$div(class = "filter-card-varname", tags$strong(private$get_varname())), |
332 | 12x |
tags$div(class = "filter-card-varlabel", private$get_varlabel()), |
333 | 12x |
tags$div( |
334 | 12x |
class = "filter-card-controls", |
335 |
# Suppress toggling body when clicking on this div.
|
|
336 |
# This is for bootstrap 3 and 4. Causes page to scroll to top, prevented by setting href on buttons.
|
|
337 | 12x |
onclick = "event.stopPropagation();event.preventDefault();", |
338 |
# This is for bootstrap 5.
|
|
339 | 12x |
`data-bs-toggle` = "collapse", |
340 | 12x |
`data-bs-target` = NULL, |
341 | 12x |
if (isFALSE(private$is_fixed())) { |
342 | 12x |
actionLink( |
343 | 12x |
inputId = ns("back"), |
344 | 12x |
label = NULL, |
345 | 12x |
icon = icon("far fa-circle-arrow-left"), |
346 | 12x |
title = "Rewind state", |
347 | 12x |
class = "filter-card-back", |
348 | 12x |
style = "display: none" |
349 |
)
|
|
350 |
},
|
|
351 | 12x |
if (isFALSE(private$is_fixed())) { |
352 | 12x |
actionLink( |
353 | 12x |
inputId = ns("reset"), |
354 | 12x |
label = NULL, |
355 | 12x |
icon = icon("far fa-circle-arrow-up"), |
356 | 12x |
title = "Restore original state", |
357 | 12x |
class = "filter-card-back", |
358 | 12x |
style = "display: none" |
359 |
)
|
|
360 |
},
|
|
361 | 12x |
if (isFALSE(private$is_anchored())) { |
362 | 12x |
actionLink( |
363 | 12x |
inputId = ns("remove"), |
364 | 12x |
label = icon("far fa-circle-xmark"), |
365 | 12x |
title = "Remove filter asdfasdfaksdfk", |
366 | 12x |
class = "teal-slice filter-icon" |
367 |
)
|
|
368 |
}
|
|
369 |
)
|
|
370 |
),
|
|
371 | 12x |
tags$div(class = "filter-card-summary", private$ui_summary(ns("summary"))) |
372 |
),
|
|
373 | 12x |
tags$div( |
374 | 12x |
id = ns("body"), |
375 | 12x |
class = "collapse out", |
376 | 12x |
`data-parent` = paste0("#", parent_id), |
377 | 12x |
`data-bs-parent` = paste0("#", parent_id), |
378 | 12x |
tags$div( |
379 | 12x |
class = "filter-card-body", |
380 | 12x |
if (private$is_fixed()) { |
381 | ! |
private$ui_inputs_fixed(ns("inputs")) |
382 |
} else { |
|
383 | 12x |
private$ui_inputs(ns("inputs")) |
384 |
}
|
|
385 |
)
|
|
386 |
)
|
|
387 |
)
|
|
388 |
},
|
|
389 | ||
390 |
#' @description
|
|
391 |
#' Destroy inputs and observers stored in `private$session_bindings`.
|
|
392 |
#'
|
|
393 |
#'
|
|
394 |
#' @return `NULL`, invisibly.
|
|
395 |
#'
|
|
396 |
finalize = function() { |
|
397 | 416x |
.finalize_session_bindings(self, private) |
398 | 416x |
invisible(NULL) |
399 |
}
|
|
400 |
),
|
|
401 | ||
402 |
# private members ----
|
|
403 |
private = list( |
|
404 |
# set by constructor
|
|
405 |
x = NULL, # the filtered variable |
|
406 |
x_reactive = NULL, # reactive containing the filtered variable, used for updating counts and histograms |
|
407 |
teal_slice = NULL, # stores all transferable properties of this filter state |
|
408 |
extract_type = character(0), # used by private$get_varname_prefixed |
|
409 |
na_count = integer(0), |
|
410 |
filtered_na_count = NULL, # reactive containing the count of NA in the filtered dataset |
|
411 |
varlabel = character(0), # taken from variable labels in data; displayed in filter cards |
|
412 |
# other
|
|
413 |
is_choice_limited = FALSE, # flag whether number of possible choices was limited when specifying filter |
|
414 |
session_bindings = list(), # stores observers and inputs to destroy afterwards |
|
415 |
state_history = NULL, # reactiveVal holding a list storing states this FilterState has had since instantiation |
|
416 | ||
417 |
# private methods ----
|
|
418 | ||
419 |
# setters for state features ----
|
|
420 | ||
421 |
# @description
|
|
422 |
# Set values that can be selected from.
|
|
423 |
set_choices = function(choices) { |
|
424 | ! |
stop("this is a virtual method") |
425 |
},
|
|
426 | ||
427 |
# @description
|
|
428 |
# Set selection.
|
|
429 |
#
|
|
430 |
# @param value (`vector`)
|
|
431 |
# value(s) that come from filter selection; values are set in the
|
|
432 |
# module server after a selection is made in the app interface;
|
|
433 |
# values are stored in `teal_slice$selected` which is reactive;
|
|
434 |
# value types have to be the same as `private$get_choices()`
|
|
435 |
#
|
|
436 |
# @return `NULL`, invisibly.
|
|
437 |
set_selected = function(value) { |
|
438 | 416x |
isolate({ |
439 | 416x |
value <- private$cast_and_validate(value) |
440 | 405x |
value <- private$check_length(value) |
441 | 399x |
value <- private$remove_out_of_bounds_values(value) |
442 | 399x |
if (!identical(value, private$teal_slice$selected)) { |
443 | 62x |
logger::log_debug("{ class(self)[1] }$set_selected setting selection for id: { private$get_id() }") |
444 | 62x |
private$teal_slice$selected <- value |
445 |
}
|
|
446 | 399x |
invisible(NULL) |
447 |
}) |
|
448 |
},
|
|
449 | ||
450 |
# @description
|
|
451 |
# Sets `value` in `private$teal_slice$keep_na`.
|
|
452 |
#
|
|
453 |
# @param value (`logical(1)`)
|
|
454 |
# corresponding to the state of a checkbox input in the `shiny` interface.
|
|
455 |
#
|
|
456 |
# @return `NULL`, invisibly.
|
|
457 |
#
|
|
458 |
set_keep_na = function(value) { |
|
459 | 16x |
isolate({ |
460 | 16x |
checkmate::assert_flag(value) |
461 | 16x |
logger::log_debug("{ class(self)[1] }$set_keep_na sets for filter { private$get_id() } to { value }.") |
462 | 16x |
if (!identical(value, private$teal_slice$keep_na)) { |
463 | 6x |
private$teal_slice$keep_na <- value |
464 |
}
|
|
465 | 16x |
invisible(NULL) |
466 |
}) |
|
467 |
},
|
|
468 | ||
469 |
# @description
|
|
470 |
# Sets `value` in `private$teal_slice$keep_inf`.
|
|
471 |
#
|
|
472 |
# @param value (`logical(1)`)
|
|
473 |
# corresponding to the state of a checkbox input in the `shiny` interface.
|
|
474 |
#
|
|
475 |
# @return `NULL`, invisibly.
|
|
476 |
#
|
|
477 |
set_keep_inf = function(value) { |
|
478 | 9x |
isolate({ |
479 | 9x |
if (!identical(value, private$teal_slice$keep_inf)) { |
480 | 4x |
checkmate::assert_flag(value) |
481 | 4x |
logger::log_debug( |
482 | 4x |
sprintf( |
483 | 4x |
"%s$set_keep_inf of filter %s set to %s",
|
484 | 4x |
class(self)[1], |
485 | 4x |
private$get_id(), |
486 | 4x |
value
|
487 |
)
|
|
488 |
)
|
|
489 | 4x |
private$teal_slice$keep_inf <- value |
490 |
}
|
|
491 | 9x |
invisible(NULL) |
492 |
}) |
|
493 |
},
|
|
494 | ||
495 |
# getters for state features ----
|
|
496 | ||
497 |
# @description
|
|
498 |
# Returns dataname.
|
|
499 |
# @return `character(1)`
|
|
500 |
get_dataname = function() { |
|
501 | 88x |
isolate(private$teal_slice$dataname) |
502 |
},
|
|
503 | ||
504 |
# @description
|
|
505 |
# Get variable name.
|
|
506 |
# @return `character(1)`
|
|
507 |
get_varname = function() { |
|
508 | 166x |
isolate(private$teal_slice$varname) |
509 |
},
|
|
510 | ||
511 |
# @description
|
|
512 |
# Get id of the teal_slice.
|
|
513 |
# @return `character(1)`
|
|
514 |
get_id = function() { |
|
515 | 4x |
isolate(private$teal_slice$id) |
516 |
},
|
|
517 | ||
518 |
# @description
|
|
519 |
# Get allowed values from `FilterState`.
|
|
520 |
# @return
|
|
521 |
# Vector describing the available choices. Return type depends on the `FilterState` subclass.
|
|
522 |
get_choices = function() { |
|
523 | 776x |
isolate(private$teal_slice$choices) |
524 |
},
|
|
525 | ||
526 |
# @description
|
|
527 |
# Get selected values from `FilterState`.
|
|
528 |
# @return
|
|
529 |
# Vector describing the current selection. Return type depends on the `FilterState` subclass.
|
|
530 |
get_selected = function() { |
|
531 | 367x |
private$teal_slice$selected |
532 |
},
|
|
533 | ||
534 |
# @description
|
|
535 |
# Returns current `keep_na` selection.
|
|
536 |
# @return `logical(1)`
|
|
537 |
get_keep_na = function() { |
|
538 | 129x |
private$teal_slice$keep_na |
539 |
},
|
|
540 | ||
541 |
# @description
|
|
542 |
# Returns current `keep_inf` selection.
|
|
543 |
# @return (`logical(1)`)
|
|
544 |
get_keep_inf = function() { |
|
545 | 117x |
private$teal_slice$keep_inf |
546 |
},
|
|
547 | ||
548 |
# Check whether this filter is fixed (cannot be changed).
|
|
549 |
# @return `logical(1)`
|
|
550 |
is_fixed = function() { |
|
551 | 148x |
isolate(isTRUE(private$teal_slice$fixed)) |
552 |
},
|
|
553 | ||
554 |
# Check whether this filter is anchored (cannot be removed).
|
|
555 |
# @return `logical(1)`
|
|
556 |
is_anchored = function() { |
|
557 | 48x |
isolate(isTRUE(private$teal_slice$anchored)) |
558 |
},
|
|
559 | ||
560 |
# Check whether this filter is capable of selecting multiple values.
|
|
561 |
# @return `logical(1)`
|
|
562 |
is_multiple = function() { |
|
563 | 218x |
isolate(isTRUE(private$teal_slice$multiple)) |
564 |
},
|
|
565 | ||
566 |
# other ----
|
|
567 | ||
568 |
# @description
|
|
569 |
# Returns variable label.
|
|
570 |
# @return `character(1)`
|
|
571 |
get_varlabel = function() { |
|
572 | 12x |
private$varlabel |
573 |
},
|
|
574 | ||
575 |
# @description
|
|
576 |
# Return variable name prefixed by `dataname` to be evaluated as extracted object, for example `data$var`
|
|
577 |
# @return Call that extracts the variable from the dataset.
|
|
578 |
get_varname_prefixed = function(dataname) { |
|
579 | 109x |
varname <- private$get_varname() |
580 | 109x |
varname_backticked <- sprintf("`%s`", varname) |
581 | 109x |
ans <- |
582 | 109x |
if (isTRUE(private$extract_type == "list")) { |
583 | 16x |
sprintf("%s$%s", dataname, varname_backticked) |
584 | 109x |
} else if (isTRUE(private$extract_type == "matrix")) { |
585 | 7x |
sprintf("%s[, \"%s\"]", dataname, varname) |
586 |
} else { |
|
587 | 86x |
varname_backticked
|
588 |
}
|
|
589 | 109x |
str2lang(ans) |
590 |
},
|
|
591 | ||
592 |
# @description
|
|
593 |
# Adds `is.na(varname)` moiety to the existing condition call, according to `keep_na` status.
|
|
594 |
# @param filter_call `call` raw filter call, as defined by selection
|
|
595 |
# @param varname `character(1)` name of a variable
|
|
596 |
# @return `call`
|
|
597 |
add_keep_na_call = function(filter_call, varname) { |
|
598 |
# No need to deal with NAs.
|
|
599 | 108x |
if (private$na_count == 0L) { |
600 | 87x |
return(filter_call) |
601 |
}
|
|
602 | ||
603 | 21x |
if (is.null(filter_call) && isFALSE(private$get_keep_na())) { |
604 | 2x |
call("!", call("is.na", varname)) |
605 | 19x |
} else if (!is.null(filter_call) && isTRUE(private$get_keep_na())) { |
606 | 12x |
call("|", call("is.na", varname), filter_call) |
607 | 7x |
} else if (!is.null(filter_call) && isFALSE(private$get_keep_na())) { |
608 | 7x |
call("&", call("!", call("is.na", varname)), filter_call) |
609 |
}
|
|
610 |
},
|
|
611 | ||
612 |
# Converts values to the type fitting this `FilterState` and validates the conversion.
|
|
613 |
# Raises error if casting does not execute successfully.
|
|
614 |
#
|
|
615 |
# @param values vector of values
|
|
616 |
#
|
|
617 |
# @return vector converted to appropriate class
|
|
618 |
cast_and_validate = function(values) { |
|
619 | 11x |
values
|
620 |
},
|
|
621 | ||
622 |
# Checks length of selection.
|
|
623 |
check_length = function(values) { |
|
624 | 11x |
values
|
625 |
},
|
|
626 | ||
627 |
# Filters out erroneous values from vector.
|
|
628 |
#
|
|
629 |
# @param values vector of values
|
|
630 |
#
|
|
631 |
# @return vector in which values that cannot be set in this FilterState have been dropped
|
|
632 |
remove_out_of_bounds_values = function(values) { |
|
633 | 31x |
values
|
634 |
},
|
|
635 | ||
636 |
# Checks if the selection is valid in terms of class and length.
|
|
637 |
# It should not return anything but raise an error if selection
|
|
638 |
# has a wrong class or is outside of possible choices
|
|
639 |
validate_selection = function(value) { |
|
640 | ! |
invisible(NULL) |
641 |
},
|
|
642 | ||
643 |
# @description
|
|
644 |
# Checks whether the current settings actually cause any values to be omitted.
|
|
645 |
# @return logical scalar
|
|
646 |
is_any_filtered = function() { |
|
647 | 75x |
if (private$is_choice_limited) { |
648 | 3x |
TRUE
|
649 | 72x |
} else if (!setequal(private$get_selected(), private$get_choices())) { |
650 | 59x |
TRUE
|
651 | 13x |
} else if (!isTRUE(private$get_keep_na()) && private$na_count > 0) { |
652 | 4x |
TRUE
|
653 |
} else { |
|
654 | 9x |
FALSE
|
655 |
}
|
|
656 |
},
|
|
657 | ||
658 |
# shiny modules -----
|
|
659 | ||
660 |
# @description
|
|
661 |
# Server module to display filter summary
|
|
662 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
663 |
ui_summary = function(id) { |
|
664 | 12x |
ns <- NS(id) |
665 | 12x |
uiOutput(ns("summary"), class = "filter-card-summary") |
666 |
},
|
|
667 | ||
668 |
# @description
|
|
669 |
# UI module to display filter summary
|
|
670 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
671 |
# @return Nothing. Renders the UI.
|
|
672 |
server_summary = function(id) { |
|
673 | 12x |
moduleServer( |
674 | 12x |
id = id, |
675 | 12x |
function(input, output, session) { |
676 | 12x |
output$summary <- renderUI(private$content_summary()) |
677 |
}
|
|
678 |
)
|
|
679 |
},
|
|
680 | ||
681 |
# module with inputs
|
|
682 |
ui_inputs = function(id) { |
|
683 | ! |
stop("abstract class") |
684 |
},
|
|
685 |
# module with inputs
|
|
686 |
server_inputs = function(id) { |
|
687 | ! |
stop("abstract class") |
688 |
},
|
|
689 | ||
690 |
# @description
|
|
691 |
# Module displaying inputs in a fixed filter state.
|
|
692 |
# There are no input widgets, only selection visualizations.
|
|
693 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
694 |
ui_inputs_fixed = function(id) { |
|
695 | ! |
ns <- NS(id) |
696 | ! |
tags$div( |
697 | ! |
class = "choices_state", |
698 | ! |
uiOutput(ns("selection")) |
699 |
)
|
|
700 |
},
|
|
701 | ||
702 |
# @description
|
|
703 |
# Module creating the display of a fixed filter state.
|
|
704 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
705 |
server_inputs_fixed = function(id) { |
|
706 | ! |
stop("abstract class") |
707 |
},
|
|
708 | ||
709 |
# @description
|
|
710 |
# Module UI function displaying input to keep or remove NA in the `FilterState` call.
|
|
711 |
# Renders a checkbox input only when variable with which `FilterState` has been created contains NAs.
|
|
712 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
713 |
keep_na_ui = function(id) { |
|
714 | 14x |
ns <- NS(id) |
715 | 14x |
if (private$na_count > 0) { |
716 | ! |
isolate({ |
717 | ! |
countmax <- private$na_count |
718 | ! |
countnow <- private$filtered_na_count() |
719 | ! |
ui_input <- checkboxInput( |
720 | ! |
inputId = ns("value"), |
721 | ! |
label = tags$span( |
722 | ! |
id = ns("count_label"), |
723 | ! |
make_count_text( |
724 | ! |
label = "Keep NA", |
725 | ! |
countmax = countmax, |
726 | ! |
countnow = countnow |
727 |
)
|
|
728 |
),
|
|
729 | ! |
value = private$get_keep_na() |
730 |
)
|
|
731 | ! |
tags$div( |
732 | ! |
uiOutput(ns("trigger_visible"), inline = TRUE), |
733 | ! |
ui_input
|
734 |
)
|
|
735 |
}) |
|
736 |
} else { |
|
737 | 14x |
NULL
|
738 |
}
|
|
739 |
},
|
|
740 | ||
741 |
# @description
|
|
742 |
# Module server function to handle NA values in the `FilterState`.
|
|
743 |
# Sets `private$slice$keep_na` according to the selection
|
|
744 |
# and updates the relevant UI element if `private$slice$keep_na` has been changed by the api.
|
|
745 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
746 |
# @return `NULL`, invisibly.
|
|
747 |
keep_na_srv = function(id) { |
|
748 | 12x |
moduleServer(id, function(input, output, session) { |
749 |
# 1. renderUI is used here as an observer which triggers only if output is visible
|
|
750 |
# and if the reactive changes - reactive triggers only if the output is visible.
|
|
751 |
# 2. We want to trigger change of the labels only if reactive count changes (not underlying data)
|
|
752 | 12x |
output$trigger_visible <- renderUI({ |
753 | 12x |
updateCountText( |
754 | 12x |
inputId = "count_label", |
755 | 12x |
label = "Keep NA", |
756 | 12x |
countmax = private$na_count, |
757 | 12x |
countnow = private$filtered_na_count() |
758 |
)
|
|
759 | 12x |
NULL
|
760 |
}) |
|
761 | ||
762 |
# this observer is needed in the situation when private$keep_inf has been
|
|
763 |
# changed directly by the api - then it's needed to rerender UI element
|
|
764 |
# to show relevant values
|
|
765 | 12x |
private$session_bindings[[session$ns("keep_na_api")]] <- observeEvent( |
766 | 12x |
ignoreNULL = FALSE, # nothing selected is possible for NA |
767 | 12x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
768 | 12x |
eventExpr = private$get_keep_na(), |
769 | 12x |
handlerExpr = { |
770 | ! |
if (!setequal(private$get_keep_na(), input$value)) { |
771 | ! |
logger::log_debug("{ class(self)[1] }$keep_na_srv@1 changed reactive value, id: { private$get_id() }") |
772 | ! |
updateCheckboxInput( |
773 | ! |
inputId = "value", |
774 | ! |
label = sprintf("Keep NA (%s/%s)", private$filtered_na_count(), private$na_count), |
775 | ! |
value = private$get_keep_na() |
776 |
)
|
|
777 |
}
|
|
778 |
}
|
|
779 |
)
|
|
780 | 12x |
private$session_bindings[[session$ns("keep_na")]] <- observeEvent( |
781 | 12x |
ignoreNULL = FALSE, # ignoreNULL: we don't want to ignore NULL when nothing is selected in the `selectInput` |
782 | 12x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
783 | 12x |
eventExpr = input$value, |
784 | 12x |
handlerExpr = { |
785 | ! |
logger::log_debug("{ class(self)[1] }$keep_na_srv@2 changed input, id: { private$get_id() }") |
786 | ! |
keep_na <- if (is.null(input$value)) { |
787 | ! |
FALSE
|
788 |
} else { |
|
789 | ! |
input$value |
790 |
}
|
|
791 | ! |
private$set_keep_na(keep_na) |
792 |
}
|
|
793 |
)
|
|
794 | 12x |
invisible(NULL) |
795 |
}) |
|
796 |
}
|
|
797 |
)
|
|
798 |
)
|
1 |
# DatetimeFilterState ------
|
|
2 | ||
3 |
#' @rdname DatetimeFilterState
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title `FilterState` object for date time data
|
|
7 |
#'
|
|
8 |
#' @description Manages choosing a range of date-times.
|
|
9 |
#'
|
|
10 |
#' @examples
|
|
11 |
#' # use non-exported function from teal.slice
|
|
12 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice")
|
|
13 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice")
|
|
14 |
#' DatetimeFilterState <- getFromNamespace("DatetimeFilterState", "teal.slice")
|
|
15 |
#'
|
|
16 |
#' library(shiny)
|
|
17 |
#'
|
|
18 |
#' filter_state <- DatetimeFilterState$new(
|
|
19 |
#' x = c(Sys.time() + seq(0, by = 3600, length.out = 10), NA),
|
|
20 |
#' slice = teal_slice(varname = "x", dataname = "data"),
|
|
21 |
#' extract_type = character(0)
|
|
22 |
#' )
|
|
23 |
#' isolate(filter_state$get_call())
|
|
24 |
#' filter_state$set_state(
|
|
25 |
#' teal_slice(
|
|
26 |
#' dataname = "data",
|
|
27 |
#' varname = "x",
|
|
28 |
#' selected = c(Sys.time() + 3L, Sys.time() + 8L),
|
|
29 |
#' keep_na = TRUE
|
|
30 |
#' )
|
|
31 |
#' )
|
|
32 |
#' isolate(filter_state$get_call())
|
|
33 |
#'
|
|
34 |
#' # working filter in an app
|
|
35 |
#' library(shinyjs)
|
|
36 |
#'
|
|
37 |
#' datetimes <- as.POSIXct(c("2012-01-01 12:00:00", "2020-01-01 12:00:00"))
|
|
38 |
#' data_datetime <- c(seq(from = datetimes[1], to = datetimes[2], length.out = 100), NA)
|
|
39 |
#' fs <- DatetimeFilterState$new(
|
|
40 |
#' x = data_datetime,
|
|
41 |
#' slice = teal_slice(
|
|
42 |
#' varname = "x", dataname = "data", selected = data_datetime[c(47, 98)], keep_na = TRUE
|
|
43 |
#' )
|
|
44 |
#' )
|
|
45 |
#'
|
|
46 |
#' ui <- fluidPage(
|
|
47 |
#' useShinyjs(),
|
|
48 |
#' include_css_files(pattern = "filter-panel"),
|
|
49 |
#' include_js_files(pattern = "count-bar-labels"),
|
|
50 |
#' column(4, tags$div(
|
|
51 |
#' tags$h4("DatetimeFilterState"),
|
|
52 |
#' fs$ui("fs")
|
|
53 |
#' )),
|
|
54 |
#' column(4, tags$div(
|
|
55 |
#' id = "outputs", # div id is needed for toggling the element
|
|
56 |
#' tags$h4("Condition (i.e. call)"), # display the condition call generated by this FilterState
|
|
57 |
#' textOutput("condition_datetime"), tags$br(),
|
|
58 |
#' tags$h4("Unformatted state"), # display raw filter state
|
|
59 |
#' textOutput("unformatted_datetime"), tags$br(),
|
|
60 |
#' tags$h4("Formatted state"), # display human readable filter state
|
|
61 |
#' textOutput("formatted_datetime"), tags$br()
|
|
62 |
#' )),
|
|
63 |
#' column(4, tags$div(
|
|
64 |
#' tags$h4("Programmatic filter control"),
|
|
65 |
#' actionButton("button1_datetime", "set drop NA", width = "100%"), tags$br(),
|
|
66 |
#' actionButton("button2_datetime", "set keep NA", width = "100%"), tags$br(),
|
|
67 |
#' actionButton("button3_datetime", "set a range", width = "100%"), tags$br(),
|
|
68 |
#' actionButton("button4_datetime", "set full range", width = "100%"), tags$br(),
|
|
69 |
#' actionButton("button0_datetime", "set initial state", width = "100%"), tags$br()
|
|
70 |
#' ))
|
|
71 |
#' )
|
|
72 |
#'
|
|
73 |
#' server <- function(input, output, session) {
|
|
74 |
#' fs$server("fs")
|
|
75 |
#' output$condition_datetime <- renderPrint(fs$get_call())
|
|
76 |
#' output$formatted_datetime <- renderText(fs$format())
|
|
77 |
#' output$unformatted_datetime <- renderPrint(fs$get_state())
|
|
78 |
#' # modify filter state programmatically
|
|
79 |
#' observeEvent(
|
|
80 |
#' input$button1_datetime,
|
|
81 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = FALSE))
|
|
82 |
#' )
|
|
83 |
#' observeEvent(
|
|
84 |
#' input$button2_datetime,
|
|
85 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = TRUE))
|
|
86 |
#' )
|
|
87 |
#' observeEvent(
|
|
88 |
#' input$button3_datetime,
|
|
89 |
#' fs$set_state(
|
|
90 |
#' teal_slice(dataname = "data", varname = "x", selected = data_datetime[c(34, 56)])
|
|
91 |
#' )
|
|
92 |
#' )
|
|
93 |
#' observeEvent(
|
|
94 |
#' input$button4_datetime,
|
|
95 |
#' fs$set_state(
|
|
96 |
#' teal_slice(dataname = "data", varname = "x", selected = datetimes)
|
|
97 |
#' )
|
|
98 |
#' )
|
|
99 |
#' observeEvent(
|
|
100 |
#' input$button0_datetime,
|
|
101 |
#' fs$set_state(
|
|
102 |
#' teal_slice(
|
|
103 |
#' dataname = "data", varname = "x", selected = data_datetime[c(47, 98)], keep_na = TRUE
|
|
104 |
#' )
|
|
105 |
#' )
|
|
106 |
#' )
|
|
107 |
#' }
|
|
108 |
#'
|
|
109 |
#' if (interactive()) {
|
|
110 |
#' shinyApp(ui, server)
|
|
111 |
#' }
|
|
112 |
#'
|
|
113 |
#' @keywords internal
|
|
114 |
#'
|
|
115 |
DatetimeFilterState <- R6::R6Class( # nolint |
|
116 |
"DatetimeFilterState",
|
|
117 |
inherit = FilterState, |
|
118 | ||
119 |
# public methods ----
|
|
120 | ||
121 |
public = list( |
|
122 | ||
123 |
#' @description
|
|
124 |
#' Initialize a `FilterState` object. This class
|
|
125 |
#' has an extra field, `private$timezone`, which is set to `Sys.timezone()` by
|
|
126 |
#' default. However, in case when using this module in `teal` app, one needs
|
|
127 |
#' timezone of the app user. App user timezone is taken from `session$userData$timezone`
|
|
128 |
#' and is set only if object is initialized in `shiny`.
|
|
129 |
#'
|
|
130 |
#' @param x (`POSIXct` or `POSIXlt`)
|
|
131 |
#' variable to be filtered.
|
|
132 |
#' @param x_reactive (`reactive`)
|
|
133 |
#' returning vector of the same type as `x`. Is used to update
|
|
134 |
#' counts following the change in values of the filtered dataset.
|
|
135 |
#' If it is set to `reactive(NULL)` then counts based on filtered
|
|
136 |
#' dataset are not shown.
|
|
137 |
#' @param slice (`teal_slice`)
|
|
138 |
#' specification of this filter state.
|
|
139 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`.
|
|
140 |
#' `get_state` returns `teal_slice` object which can be reused in other places.
|
|
141 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e.
|
|
142 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`.
|
|
143 |
#' @param extract_type (`character`)
|
|
144 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values:
|
|
145 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed
|
|
146 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>`
|
|
147 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]`
|
|
148 |
#'
|
|
149 |
#' @return Object of class `DatetimeFilterState`, invisibly.
|
|
150 |
#'
|
|
151 |
initialize = function(x, |
|
152 |
x_reactive = reactive(NULL), |
|
153 |
extract_type = character(0), |
|
154 |
slice) { |
|
155 | 25x |
isolate({ |
156 | 25x |
checkmate::assert_multi_class(x, c("POSIXct", "POSIXlt")) |
157 | 24x |
checkmate::assert_class(x_reactive, "reactive") |
158 | ||
159 | 24x |
super$initialize( |
160 | 24x |
x = x, |
161 | 24x |
x_reactive = x_reactive, |
162 | 24x |
slice = slice, |
163 | 24x |
extract_type = extract_type |
164 |
)
|
|
165 | 24x |
checkmate::assert_multi_class(slice$choices, c("POSIXct", "POSIXlt"), null.ok = TRUE) |
166 | 23x |
private$set_choices(slice$choices) |
167 | 15x |
if (is.null(slice$selected)) slice$selected <- slice$choices |
168 | 23x |
private$set_selected(slice$selected) |
169 |
}) |
|
170 | ||
171 | 22x |
invisible(self) |
172 |
},
|
|
173 | ||
174 |
#' @description
|
|
175 |
#' Returns reproducible condition call for current selection.
|
|
176 |
#' For this class returned call looks like
|
|
177 |
#' `<varname> >= as.POSIXct(<min>) & <varname> <= <max>)` with optional `is.na(<varname>)`.
|
|
178 |
#' @param dataname name of data set; defaults to `private$get_dataname()`
|
|
179 |
#' @return `call`
|
|
180 |
#'
|
|
181 |
get_call = function(dataname) { |
|
182 | 7x |
if (isFALSE(private$is_any_filtered())) { |
183 | 1x |
return(NULL) |
184 |
}
|
|
185 | 4x |
if (missing(dataname)) dataname <- private$get_dataname() |
186 | 6x |
varname <- private$get_varname_prefixed(dataname) |
187 | 6x |
choices <- private$get_selected() |
188 | 6x |
tzone <- Find(function(x) x != "", attr(as.POSIXlt(choices), "tzone")) |
189 | 6x |
class <- class(choices)[1L] |
190 | 6x |
date_fun <- as.name( |
191 | 6x |
switch(class, |
192 | 6x |
"POSIXct" = "as.POSIXct", |
193 | 6x |
"POSIXlt" = "as.POSIXlt" |
194 |
)
|
|
195 |
)
|
|
196 | 6x |
choices <- as.character(choices + c(0, 1)) |
197 | 6x |
filter_call <- |
198 | 6x |
call( |
199 |
"&",
|
|
200 | 6x |
call( |
201 |
">=",
|
|
202 | 6x |
varname,
|
203 | 6x |
as.call(list(date_fun, choices[1L], tz = tzone)) |
204 |
),
|
|
205 | 6x |
call( |
206 |
"<",
|
|
207 | 6x |
varname,
|
208 | 6x |
as.call(list(date_fun, choices[2L], tz = tzone)) |
209 |
)
|
|
210 |
)
|
|
211 | 6x |
private$add_keep_na_call(filter_call, varname) |
212 |
}
|
|
213 |
),
|
|
214 | ||
215 |
# private members ----
|
|
216 | ||
217 |
private = list( |
|
218 |
# private methods ----
|
|
219 |
set_choices = function(choices) { |
|
220 | 23x |
if (is.null(choices)) { |
221 | 20x |
choices <- as.POSIXct(trunc(range(private$x, na.rm = TRUE), units = "secs")) |
222 |
} else { |
|
223 | 3x |
choices <- as.POSIXct(choices, units = "secs") |
224 | 3x |
choices_adjusted <- c( |
225 | 3x |
max(choices[1L], min(as.POSIXct(private$x), na.rm = TRUE)), |
226 | 3x |
min(choices[2L], max(as.POSIXct(private$x), na.rm = TRUE)) |
227 |
)
|
|
228 | 3x |
if (any(choices != choices_adjusted)) { |
229 | 1x |
warning(sprintf( |
230 | 1x |
"Choices adjusted (some values outside of variable range). Varname: %s, dataname: %s.",
|
231 | 1x |
private$get_varname(), private$get_dataname() |
232 |
)) |
|
233 | 1x |
choices <- choices_adjusted |
234 |
}
|
|
235 | 3x |
if (choices[1L] >= choices[2L]) { |
236 | 1x |
warning(sprintf( |
237 | 1x |
"Invalid choices: lower is higher / equal to upper, or not in range of variable values. |
238 | 1x |
Setting defaults. Varname: %s, dataname: %s.", |
239 | 1x |
private$get_varname(), private$get_dataname() |
240 |
)) |
|
241 | 1x |
choices <- range(private$x, na.rm = TRUE) |
242 |
}
|
|
243 |
}
|
|
244 | ||
245 | 23x |
private$set_is_choice_limited(private$x, choices) |
246 | 23x |
private$x <- private$x[ |
247 |
(
|
|
248 | 23x |
as.POSIXct(trunc(private$x, units = "secs")) >= choices[1L] & |
249 | 23x |
as.POSIXct(trunc(private$x, units = "secs")) <= choices[2L] |
250 | 23x |
) | is.na(private$x) |
251 |
]
|
|
252 | 23x |
private$teal_slice$choices <- choices |
253 | 23x |
invisible(NULL) |
254 |
},
|
|
255 | ||
256 |
# @description
|
|
257 |
# Check whether the initial choices filter out some values of x and set the flag in case.
|
|
258 |
set_is_choice_limited = function(xl, choices = NULL) { |
|
259 | 23x |
private$is_choice_limited <- (any(xl < choices[1L], na.rm = TRUE) | any(xl > choices[2L], na.rm = TRUE)) |
260 | 23x |
invisible(NULL) |
261 |
},
|
|
262 |
cast_and_validate = function(values) { |
|
263 | 34x |
tryCatch( |
264 | 34x |
expr = { |
265 | 34x |
values <- as.POSIXct(values, origin = "1970-01-01 00:00:00") |
266 | ! |
if (anyNA(values)) stop() |
267 | 31x |
values
|
268 |
},
|
|
269 | 34x |
error = function(e) stop("Vector of set values must contain values coercible to POSIX.") |
270 |
)
|
|
271 |
},
|
|
272 |
check_length = function(values) { |
|
273 | 1x |
if (length(values) != 2) stop("Vector of set values must have length two.") |
274 | 30x |
if (values[1] > values[2]) { |
275 | 1x |
warning( |
276 | 1x |
sprintf( |
277 | 1x |
"Start date '%s' is set after the end date '%s', the values will be replaced by a default datetime range.",
|
278 | 1x |
values[1], values[2] |
279 |
)
|
|
280 |
)
|
|
281 | 1x |
values <- isolate(private$get_choices()) |
282 |
}
|
|
283 | 30x |
values
|
284 |
},
|
|
285 |
remove_out_of_bounds_values = function(values) { |
|
286 | 30x |
choices <- private$get_choices() |
287 | 30x |
if (values[1] < choices[1L] || values[1] > choices[2L]) { |
288 | 5x |
warning( |
289 | 5x |
sprintf( |
290 | 5x |
"Value: %s is outside of the range for the column '%s' in dataset '%s', setting minimum possible value.",
|
291 | 5x |
values[1], private$get_varname(), toString(private$get_dataname()) |
292 |
)
|
|
293 |
)
|
|
294 | 5x |
values[1] <- choices[1L] |
295 |
}
|
|
296 | ||
297 | 30x |
if (values[2] > choices[2L] | values[2] < choices[1L]) { |
298 | 5x |
warning( |
299 | 5x |
sprintf( |
300 | 5x |
"Value: '%s' is outside of the range for the column '%s' in dataset '%s', setting maximum possible value.",
|
301 | 5x |
values[2], private$get_varname(), toString(private$get_dataname()) |
302 |
)
|
|
303 |
)
|
|
304 | 5x |
values[2] <- choices[2L] |
305 |
}
|
|
306 | ||
307 | 30x |
values
|
308 |
},
|
|
309 | ||
310 |
# shiny modules ----
|
|
311 | ||
312 |
# @description
|
|
313 |
# UI Module for `DatetimeFilterState`.
|
|
314 |
# This UI element contains two date-time selections for `min` and `max`
|
|
315 |
# of the range and a checkbox whether to keep the `NA` values.
|
|
316 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
317 |
ui_inputs = function(id) { |
|
318 | ! |
ns <- NS(id) |
319 | ||
320 | ! |
isolate({ |
321 | ! |
ui_input_1 <- shinyWidgets::airDatepickerInput( |
322 | ! |
inputId = ns("selection_start"), |
323 | ! |
value = private$get_selected()[1], |
324 | ! |
startView = private$get_selected()[1], |
325 | ! |
timepicker = TRUE, |
326 | ! |
minDate = private$get_choices()[1L], |
327 | ! |
maxDate = private$get_choices()[2L], |
328 | ! |
update_on = "close", |
329 | ! |
addon = "none", |
330 | ! |
position = "bottom right" |
331 |
)
|
|
332 | ! |
ui_input_2 <- shinyWidgets::airDatepickerInput( |
333 | ! |
inputId = ns("selection_end"), |
334 | ! |
value = private$get_selected()[2], |
335 | ! |
startView = private$get_selected()[2], |
336 | ! |
timepicker = TRUE, |
337 | ! |
minDate = private$get_choices()[1L], |
338 | ! |
maxDate = private$get_choices()[2L], |
339 | ! |
update_on = "close", |
340 | ! |
addon = "none", |
341 | ! |
position = "bottom right" |
342 |
)
|
|
343 | ! |
ui_reset_1 <- actionButton( |
344 | ! |
class = "date_reset_button", |
345 | ! |
inputId = ns("start_date_reset"), |
346 | ! |
label = NULL, |
347 | ! |
icon = icon("fas fa-undo") |
348 |
)
|
|
349 | ! |
ui_reset_2 <- actionButton( |
350 | ! |
class = "date_reset_button", |
351 | ! |
inputId = ns("end_date_reset"), |
352 | ! |
label = NULL, |
353 | ! |
icon = icon("fas fa-undo") |
354 |
)
|
|
355 | ! |
ui_input_1$children[[2]]$attribs <- c(ui_input_1$children[[2]]$attribs, list(class = "input-sm")) |
356 | ! |
ui_input_2$children[[2]]$attribs <- c(ui_input_2$children[[2]]$attribs, list(class = "input-sm")) |
357 | ||
358 | ! |
tags$div( |
359 | ! |
tags$div( |
360 | ! |
class = "flex", |
361 | ! |
ui_reset_1,
|
362 | ! |
tags$div( |
363 | ! |
class = "flex w-80 filter_datelike_input", |
364 | ! |
tags$div(class = "w-45 text-center", ui_input_1), |
365 | ! |
tags$span( |
366 | ! |
class = "input-group-addon w-10", |
367 | ! |
tags$span(class = "input-group-text w-100 justify-content-center", "to"), |
368 | ! |
title = "Times are displayed in the local timezone and are converted to UTC in the analysis" |
369 |
),
|
|
370 | ! |
tags$div(class = "w-45 text-center", ui_input_2) |
371 |
),
|
|
372 | ! |
ui_reset_2
|
373 |
),
|
|
374 | ! |
private$keep_na_ui(ns("keep_na")) |
375 |
)
|
|
376 |
}) |
|
377 |
},
|
|
378 | ||
379 |
# @description
|
|
380 |
# Server module
|
|
381 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
382 |
# @return `NULL`.
|
|
383 |
server_inputs = function(id) { |
|
384 | ! |
moduleServer( |
385 | ! |
id = id, |
386 | ! |
function(input, output, session) { |
387 | ! |
logger::log_debug("DatetimeFilterState$server initializing, id: { private$get_id() }") |
388 |
# this observer is needed in the situation when teal_slice$selected has been
|
|
389 |
# changed directly by the api - then it's needed to rerender UI element
|
|
390 |
# to show relevant values
|
|
391 | ! |
private$session_bindings[[session$ns("selection_api")]] <- observeEvent( |
392 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
393 | ! |
ignoreInit = TRUE, # on init selected == default, so no need to trigger |
394 | ! |
eventExpr = private$get_selected(), |
395 | ! |
handlerExpr = { |
396 | ! |
start_date <- input$selection_start |
397 | ! |
end_date <- input$selection_end |
398 | ! |
if (!all(private$get_selected() == c(start_date, end_date))) { |
399 | ! |
logger::log_debug("DatetimeFilterState$server@1 state changed, id: { private$get_id() }") |
400 | ! |
if (private$get_selected()[1] != start_date) { |
401 | ! |
shinyWidgets::updateAirDateInput( |
402 | ! |
session = session, |
403 | ! |
inputId = "selection_start", |
404 | ! |
value = private$get_selected()[1] |
405 |
)
|
|
406 |
}
|
|
407 | ||
408 | ! |
if (private$get_selected()[2] != end_date) { |
409 | ! |
shinyWidgets::updateAirDateInput( |
410 | ! |
session = session, |
411 | ! |
inputId = "selection_end", |
412 | ! |
value = private$get_selected()[2] |
413 |
)
|
|
414 |
}
|
|
415 |
}
|
|
416 |
}
|
|
417 |
)
|
|
418 | ||
419 | ||
420 | ! |
private$session_bindings[[session$ns("selection_start")]] <- observeEvent( |
421 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
422 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
423 | ! |
eventExpr = input$selection_start, |
424 | ! |
handlerExpr = { |
425 | ! |
logger::log_debug("DatetimeFilterState$server@2 selection changed, id: { private$get_id() }") |
426 | ! |
start_date <- input$selection_start |
427 | ! |
end_date <- private$get_selected()[[2]] |
428 | ! |
tzone <- Find(function(x) x != "", attr(as.POSIXlt(private$get_choices()), "tzone")) |
429 | ! |
attr(start_date, "tzone") <- tzone |
430 | ||
431 | ! |
if (start_date > end_date) { |
432 | ! |
showNotification( |
433 | ! |
"Start date must not be greater than the end date. Ignoring selection.",
|
434 | ! |
type = "warning" |
435 |
)
|
|
436 | ! |
shinyWidgets::updateAirDateInput( |
437 | ! |
session = session, |
438 | ! |
inputId = "selection_start", |
439 | ! |
value = private$get_selected()[1] # sets back to latest selected value |
440 |
)
|
|
441 | ! |
return(NULL) |
442 |
}
|
|
443 | ||
444 | ! |
private$set_selected(c(start_date, end_date)) |
445 |
}
|
|
446 |
)
|
|
447 | ||
448 | ! |
private$session_bindings[[session$ns("selection_end")]] <- observeEvent( |
449 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
450 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
451 | ! |
eventExpr = input$selection_end, |
452 | ! |
handlerExpr = { |
453 | ! |
start_date <- private$get_selected()[1] |
454 | ! |
end_date <- input$selection_end |
455 | ! |
tzone <- Find(function(x) x != "", attr(as.POSIXlt(private$get_choices()), "tzone")) |
456 | ! |
attr(end_date, "tzone") <- tzone |
457 | ||
458 | ! |
if (start_date > end_date) { |
459 | ! |
showNotification( |
460 | ! |
"End date must not be lower than the start date. Ignoring selection.",
|
461 | ! |
type = "warning" |
462 |
)
|
|
463 | ! |
shinyWidgets::updateAirDateInput( |
464 | ! |
session = session, |
465 | ! |
inputId = "selection_end", |
466 | ! |
value = private$get_selected()[2] # sets back to latest selected value |
467 |
)
|
|
468 | ! |
return(NULL) |
469 |
}
|
|
470 | ||
471 | ! |
private$set_selected(c(start_date, end_date)) |
472 | ! |
logger::log_debug("DatetimeFilterState$server@2 selection changed, id: { private$get_id() }") |
473 |
}
|
|
474 |
)
|
|
475 | ||
476 | ! |
private$keep_na_srv("keep_na") |
477 | ||
478 | ! |
private$session_bindings[[session$ns("reset1")]] <- observeEvent( |
479 | ! |
ignoreInit = TRUE, # reset button shouldn't be trigger on init |
480 | ! |
ignoreNULL = TRUE, # it's impossible and wrong to set default to NULL |
481 | ! |
input$start_date_reset, |
482 |
{
|
|
483 | ! |
shinyWidgets::updateAirDateInput( |
484 | ! |
session = session, |
485 | ! |
inputId = "selection_start", |
486 | ! |
value = private$get_choices()[1L] |
487 |
)
|
|
488 | ! |
logger::log_debug("DatetimeFilterState$server@2 reset start date, id: { private$get_id() }") |
489 |
}
|
|
490 |
)
|
|
491 | ! |
private$session_bindings[[session$ns("reset2")]] <- observeEvent( |
492 | ! |
ignoreInit = TRUE, # reset button shouldn't be trigger on init |
493 | ! |
ignoreNULL = TRUE, # it's impossible and wrong to set default to NULL |
494 | ! |
input$end_date_reset, |
495 |
{
|
|
496 | ! |
shinyWidgets::updateAirDateInput( |
497 | ! |
session = session, |
498 | ! |
inputId = "selection_end", |
499 | ! |
value = private$get_choices()[2L] |
500 |
)
|
|
501 | ! |
logger::log_debug("DatetimeFilterState$server@3 reset end date, id: { private$get_id() }") |
502 |
}
|
|
503 |
)
|
|
504 | ||
505 | ! |
NULL
|
506 |
}
|
|
507 |
)
|
|
508 |
},
|
|
509 |
server_inputs_fixed = function(id) { |
|
510 | ! |
moduleServer( |
511 | ! |
id = id, |
512 | ! |
function(input, output, session) { |
513 | ! |
logger::log_debug("DatetimeFilterState$server initializing, id: { private$get_id() }") |
514 | ||
515 | ! |
output$selection <- renderUI({ |
516 | ! |
vals <- format(private$get_selected(), usetz = TRUE, nsmall = 3) |
517 | ! |
tags$div( |
518 | ! |
tags$div(icon("clock"), vals[1]), |
519 | ! |
tags$div(span(" - "), icon("clock"), vals[2]) |
520 |
)
|
|
521 |
}) |
|
522 | ||
523 | ! |
NULL
|
524 |
}
|
|
525 |
)
|
|
526 |
},
|
|
527 | ||
528 |
# @description
|
|
529 |
# UI module to display filter summary
|
|
530 |
# renders text describing selected date range and
|
|
531 |
# if NA are included also
|
|
532 |
content_summary = function(id) { |
|
533 | ! |
selected <- format(private$get_selected(), "%Y-%m-%d %H:%M:%S") |
534 | ! |
min <- selected[1] |
535 | ! |
max <- selected[2] |
536 | ! |
tagList( |
537 | ! |
tags$span( |
538 | ! |
class = "filter-card-summary-value", |
539 | ! |
HTML(min, "–", max) |
540 |
),
|
|
541 | ! |
tags$span( |
542 | ! |
class = "filter-card-summary-controls", |
543 | ! |
if (private$na_count > 0) { |
544 | ! |
tags$span("NA", if (isTRUE(private$get_keep_na())) icon("check") else icon("xmark")) |
545 |
}
|
|
546 |
)
|
|
547 |
)
|
|
548 |
}
|
|
549 |
)
|
|
550 |
)
|
1 |
#' Initialize `FilterState`
|
|
2 |
#'
|
|
3 |
#' Initializes a `FilterState` object corresponding to the class of the filtered variable.
|
|
4 |
#'
|
|
5 |
#' @param x (`vector`)
|
|
6 |
#' variable to be filtered.
|
|
7 |
#' @param x_reactive (`reactive`)
|
|
8 |
#' returning vector of the same type as `x`. Is used to update
|
|
9 |
#' counts following the change in values of the filtered dataset.
|
|
10 |
#' If it is set to `reactive(NULL)` then counts based on filtered
|
|
11 |
#' dataset are not shown.
|
|
12 |
#' @param slice (`teal_slice`)
|
|
13 |
#' specification of this filter state.
|
|
14 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`.
|
|
15 |
#' `get_state` returns `teal_slice` object which can be reused in other places.
|
|
16 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e.
|
|
17 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`.
|
|
18 |
#' @param extract_type (`character`)
|
|
19 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values:
|
|
20 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed
|
|
21 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>`
|
|
22 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]`
|
|
23 |
#'
|
|
24 |
#' @examples
|
|
25 |
#' # use non-exported function from teal.slice
|
|
26 |
#' init_filter_state <- getFromNamespace("init_filter_state", "teal.slice")
|
|
27 |
#'
|
|
28 |
#' library(shiny)
|
|
29 |
#'
|
|
30 |
#' filter_state <- init_filter_state(
|
|
31 |
#' x = c(1:10, NA, Inf),
|
|
32 |
#' x_reactive = reactive(c(1:10, NA, Inf)),
|
|
33 |
#' slice = teal_slice(
|
|
34 |
#' varname = "varname",
|
|
35 |
#' dataname = "dataname"
|
|
36 |
#' ),
|
|
37 |
#' extract_type = "matrix"
|
|
38 |
#' )
|
|
39 |
#'
|
|
40 |
#' isolate(filter_state$get_call())
|
|
41 |
#'
|
|
42 |
#' # working filter in an app
|
|
43 |
#'
|
|
44 |
#' ui <- fluidPage(
|
|
45 |
#' filter_state$ui(id = "app"),
|
|
46 |
#' verbatimTextOutput("call")
|
|
47 |
#' )
|
|
48 |
#' server <- function(input, output, session) {
|
|
49 |
#' filter_state$server("app")
|
|
50 |
#'
|
|
51 |
#' output$call <- renderText(
|
|
52 |
#' deparse1(filter_state$get_call(), collapse = "\n")
|
|
53 |
#' )
|
|
54 |
#' }
|
|
55 |
#'
|
|
56 |
#' if (interactive()) {
|
|
57 |
#' shinyApp(ui, server)
|
|
58 |
#' }
|
|
59 |
#'
|
|
60 |
#' @return `FilterState` object
|
|
61 |
#' @keywords internal
|
|
62 |
init_filter_state <- function(x, |
|
63 |
x_reactive = reactive(NULL), |
|
64 |
slice,
|
|
65 |
extract_type = character(0)) { |
|
66 | 187x |
checkmate::assert_class(x_reactive, "reactive") |
67 | 186x |
checkmate::assert_character(extract_type, max.len = 1, any.missing = FALSE) |
68 | 186x |
checkmate::assert_class(slice, "teal_slice") |
69 | 185x |
if (length(extract_type) == 1) { |
70 | 40x |
checkmate::assert_choice(extract_type, choices = c("list", "matrix")) |
71 |
}
|
|
72 | ||
73 | 184x |
if (all(is.na(x))) { |
74 | 1x |
EmptyFilterState$new( |
75 | 1x |
x = x, |
76 | 1x |
x_reactive = x_reactive, |
77 | 1x |
slice = slice, |
78 | 1x |
extract_type = extract_type |
79 |
)
|
|
80 |
} else { |
|
81 | 183x |
UseMethod("init_filter_state") |
82 |
}
|
|
83 |
}
|
|
84 | ||
85 |
#' @keywords internal
|
|
86 |
#' @export
|
|
87 |
init_filter_state.default <- function(x, |
|
88 |
x_reactive = reactive(NULL), |
|
89 |
slice,
|
|
90 |
extract_type = character(0)) { |
|
91 | 1x |
args <- list( |
92 | 1x |
x = x, |
93 | 1x |
x_reactive = x_reactive, |
94 | 1x |
extract_type = extract_type, |
95 | 1x |
slice
|
96 |
)
|
|
97 | ||
98 | 1x |
do.call(FilterState$new, args) |
99 |
}
|
|
100 | ||
101 |
#' @keywords internal
|
|
102 |
#' @export
|
|
103 |
init_filter_state.logical <- function(x, |
|
104 |
x_reactive = reactive(NULL), |
|
105 |
slice,
|
|
106 |
extract_type = character(0)) { |
|
107 | 1x |
LogicalFilterState$new( |
108 | 1x |
x = x, |
109 | 1x |
x_reactive = x_reactive, |
110 | 1x |
slice = slice, |
111 | 1x |
extract_type = extract_type |
112 |
)
|
|
113 |
}
|
|
114 | ||
115 |
#' @keywords internal
|
|
116 |
#' @export
|
|
117 |
init_filter_state.numeric <- function(x, |
|
118 |
x_reactive = reactive(NULL), |
|
119 |
slice,
|
|
120 |
extract_type = character(0)) { |
|
121 | 115x |
args <- list( |
122 | 115x |
x = x, |
123 | 115x |
x_reactive = x_reactive, |
124 | 115x |
slice = slice, |
125 | 115x |
extract_type = extract_type |
126 |
)
|
|
127 | ||
128 | 115x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
129 | 26x |
do.call(ChoicesFilterState$new, args) |
130 |
} else { |
|
131 | 89x |
do.call(RangeFilterState$new, args) |
132 |
}
|
|
133 |
}
|
|
134 | ||
135 |
#' @keywords internal
|
|
136 |
#' @export
|
|
137 |
init_filter_state.factor <- function(x, |
|
138 |
x_reactive = reactive(NULL), |
|
139 |
slice,
|
|
140 |
extract_type = character(0)) { |
|
141 | 30x |
ChoicesFilterState$new( |
142 | 30x |
x = x, |
143 | 30x |
x_reactive = x_reactive, |
144 | 30x |
slice = slice, |
145 | 30x |
extract_type = extract_type |
146 |
)
|
|
147 |
}
|
|
148 | ||
149 |
#' @keywords internal
|
|
150 |
#' @export
|
|
151 |
init_filter_state.character <- function(x, |
|
152 |
x_reactive = reactive(NULL), |
|
153 |
slice,
|
|
154 |
extract_type = character(0)) { |
|
155 | 30x |
ChoicesFilterState$new( |
156 | 30x |
x = x, |
157 | 30x |
x_reactive = x_reactive, |
158 | 30x |
slice = slice, |
159 | 30x |
extract_type = extract_type |
160 |
)
|
|
161 |
}
|
|
162 | ||
163 |
#' @keywords internal
|
|
164 |
#' @export
|
|
165 |
init_filter_state.Date <- function(x, |
|
166 |
x_reactive = reactive(NULL), |
|
167 |
slice,
|
|
168 |
extract_type = character(0)) { |
|
169 | 2x |
args <- list( |
170 | 2x |
x = x, |
171 | 2x |
x_reactive = x_reactive, |
172 | 2x |
slice = slice, |
173 | 2x |
extract_type = extract_type |
174 |
)
|
|
175 | ||
176 | 2x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
177 | 1x |
do.call(ChoicesFilterState$new, args) |
178 |
} else { |
|
179 | 1x |
do.call(DateFilterState$new, args) |
180 |
}
|
|
181 |
}
|
|
182 | ||
183 |
#' @keywords internal
|
|
184 |
#' @export
|
|
185 |
init_filter_state.POSIXct <- function(x, |
|
186 |
x_reactive = reactive(NULL), |
|
187 |
slice,
|
|
188 |
extract_type = character(0)) { |
|
189 | 2x |
args <- list( |
190 | 2x |
x = x, |
191 | 2x |
x_reactive = x_reactive, |
192 | 2x |
slice = slice, |
193 | 2x |
extract_type = extract_type |
194 |
)
|
|
195 | ||
196 | 2x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
197 | 1x |
do.call(ChoicesFilterState$new, args) |
198 |
} else { |
|
199 | 1x |
do.call(DatetimeFilterState$new, args) |
200 |
}
|
|
201 |
}
|
|
202 | ||
203 |
#' @keywords internal
|
|
204 |
#' @export
|
|
205 |
init_filter_state.POSIXlt <- function(x, |
|
206 |
x_reactive = reactive(NULL), |
|
207 |
slice,
|
|
208 |
extract_type = character(0)) { |
|
209 | 2x |
args <- list( |
210 | 2x |
x = x, |
211 | 2x |
x_reactive = x_reactive, |
212 | 2x |
slice = slice, |
213 | 2x |
extract_type = extract_type |
214 |
)
|
|
215 | ||
216 | 2x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
217 | 1x |
do.call(ChoicesFilterState$new, args) |
218 |
} else { |
|
219 | 1x |
do.call(DatetimeFilterState$new, args) |
220 |
}
|
|
221 |
}
|
|
222 | ||
223 | ||
224 |
#' Initialize a `FilterStateExpr` object
|
|
225 |
#'
|
|
226 |
#' @param slice (`teal_slice_expr`)
|
|
227 |
#' specifying this filter state.
|
|
228 |
#'
|
|
229 |
#' @return `FilterStateExpr` object
|
|
230 |
#' @keywords internal
|
|
231 |
init_filter_state_expr <- function(slice) { |
|
232 | 6x |
FilterStateExpr$new(slice) |
233 |
}
|
|
234 | ||
235 | ||
236 |
#' Get hex code of the current Bootstrap theme color.
|
|
237 |
#'
|
|
238 |
#' Determines the color specification for the currently active Bootstrap color theme and returns one queried color.
|
|
239 |
#'
|
|
240 |
#' @param color (`character(1)`) naming one of the available theme colors
|
|
241 |
#' @param alpha either a `numeric(1)` or `character(1)` specifying transparency
|
|
242 |
#' in the range of `0-1` or a hexadecimal value `00-ff`, respectively;
|
|
243 |
#' set to NULL to omit adding the alpha channel
|
|
244 |
#'
|
|
245 |
#' @return Named `character(1)` containing a hexadecimal color representation.
|
|
246 |
#'
|
|
247 |
#' @examples
|
|
248 |
#' fetch_bs_color <- getFromNamespace("fetch_bs_color", "teal.slice")
|
|
249 |
#' fetch_bs_color("primary")
|
|
250 |
#' fetch_bs_color("danger", 0.35)
|
|
251 |
#' fetch_bs_color("danger", "80")
|
|
252 |
#'
|
|
253 |
#' @keywords internal
|
|
254 |
#'
|
|
255 |
fetch_bs_color <- function(color, alpha = NULL) { |
|
256 | 116x |
checkmate::assert_string(color) |
257 | 116x |
checkmate::assert( |
258 | 116x |
checkmate::check_number(alpha, lower = 0, upper = 1, null.ok = TRUE), |
259 | 116x |
checkmate::check_string(alpha, pattern = "[0-9a-f]{2}", null.ok = TRUE) |
260 |
)
|
|
261 | ||
262 |
# locate file that describes the current theme
|
|
263 | 116x |
sass_file <- if (utils::packageVersion("bslib") < as.package_version("0.5.1.9000")) { |
264 | ! |
bslib::bs_theme()[["layers"]][[2]][["defaults"]][[1]] |
265 |
} else { |
|
266 | 116x |
bslib::bs_theme()[["layers"]][[2]][["defaults"]][[1]][[1]] |
267 |
}
|
|
268 | 116x |
sass_file <- attr(sass_file, "sass_file_path") |
269 | ||
270 |
# load scss file that encodes variables
|
|
271 | 116x |
variables_file <- readLines(sass_file) |
272 |
# locate theme color variables
|
|
273 | 116x |
ind <- grep("// scss-docs-(start|end) theme-color-variables", variables_file) |
274 | 116x |
color_definitions <- variables_file[(ind[1] + 1L):(ind[2] - 1L)] |
275 | ||
276 |
# extract colors names
|
|
277 | 116x |
color_names <- sub("(\\$)(\\w.+)(:.+)", "\\2", color_definitions) |
278 | ||
279 |
# verify that an available color was requested
|
|
280 | 116x |
checkmate::assert_choice(color, color_names) |
281 | ||
282 |
# extract color references
|
|
283 | 116x |
color_references <- sub("(\\$)(\\w.+)(:\\s.+\\$)(\\w.+)(\\s.+)", "\\4", color_definitions) |
284 | ||
285 |
# translate references to color codes
|
|
286 | 116x |
color_specification <- structure(color_references, names = color_names) |
287 | 116x |
color_specification <- vapply(color_specification, function(x) { |
288 | 928x |
line <- grep(sprintf("^\\$%s:\\s+#\\w{6}\\s+!default", x), variables_file, value = TRUE) |
289 | 928x |
code <- sub("(.+)(#\\w{6})(\\s+.+)", "\\2", line) |
290 | 928x |
code
|
291 | 116x |
}, character(1L)) |
292 | ||
293 | 116x |
if (!is.null(alpha)) { |
294 | ! |
if (is.numeric(alpha)) alpha <- as.hexmode(ceiling(255 * alpha)) |
295 |
}
|
|
296 | ||
297 | 116x |
paste0(color_specification[color], alpha) |
298 |
}
|
1 |
#' Specify single filter
|
|
2 |
#'
|
|
3 |
#' Create a `teal_slice` object that holds complete information on filtering one variable.
|
|
4 |
#' Check out [`teal_slice-utilities`] functions for working with `teal_slice` object.
|
|
5 |
#'
|
|
6 |
#' `teal_slice` object fully describes filter state and can be used to create,
|
|
7 |
#' modify, and delete a filter state. A `teal_slice` contains a number of common fields
|
|
8 |
#' (all named arguments of `teal_slice`), some of which are mandatory, but only
|
|
9 |
#' `dataname` and either `varname` or `expr` must be specified, while the others have default
|
|
10 |
#' values.
|
|
11 |
#'
|
|
12 |
#' Setting any of the other values to `NULL` means that those properties will not be modified
|
|
13 |
#' (when setting an existing state) or that they will be determined by data (when creating new a new one).
|
|
14 |
#' Entire object is `FilterState` class member and can be accessed with `FilterState$get_state()`.
|
|
15 |
#'
|
|
16 |
#' A `teal_slice` can come in two flavors:
|
|
17 |
#' 1. `teal_slice_var` -
|
|
18 |
#' this describes a typical interactive filter that refers to a single variable, managed by the `FilterState` class.
|
|
19 |
#' This class is created when `varname` is specified.
|
|
20 |
#' The object retains all fields specified in the call. `id` can be created by default and need not be specified.
|
|
21 |
#' 2. `teal_slice_expr` -
|
|
22 |
#' this describes a filter state that refers to an expression, which can potentially include multiple variables,
|
|
23 |
#' managed by the `FilterStateExpr` class.
|
|
24 |
#' This class is created when `expr` is specified.
|
|
25 |
#' `dataname` and `anchored` are retained, `fixed` is set to `TRUE`, `id` becomes mandatory, `title`
|
|
26 |
#' remains optional, while other arguments are disregarded.
|
|
27 |
#'
|
|
28 |
#' A teal_slice can be passed `FilterState`/`FilterStateExpr` constructors to instantiate an object.
|
|
29 |
#' It can also be passed to `FilterState$set_state` to modify the state.
|
|
30 |
#' However, once a `FilterState` is created, only the mutable features can be set with a teal_slice:
|
|
31 |
#' `selected`, `keep_na` and `keep_inf`.
|
|
32 |
#'
|
|
33 |
#' Special consideration is given to two fields: `fixed` and `anchored`.
|
|
34 |
#' These are always immutable logical flags that default to `FALSE`.
|
|
35 |
#' In a `FilterState` instantiated with `fixed = TRUE` the features
|
|
36 |
#' `selected`, `keep_na`, `keep_inf` cannot be changed.
|
|
37 |
#' Note that a `FilterStateExpr` is always considered to have `fixed = TRUE`.
|
|
38 |
#' A `FilterState` instantiated with `anchored = TRUE` cannot be removed.
|
|
39 |
#'
|
|
40 |
#' @section Filters in `SumarizedExperiment` and `MultiAssayExperiment` objects:
|
|
41 |
#'
|
|
42 |
#' To establish a filter on a column in a `data.frame`, `dataname` and `varname` are sufficient.
|
|
43 |
#' `MultiAssayExperiment` objects can be filtered either on their `colData` slot (which contains subject information)
|
|
44 |
#' or on their experiments, which are stored in the `experimentList` slot.
|
|
45 |
#' For filters referring to `colData` no extra arguments are needed.
|
|
46 |
#' If a filter state is created for an experiment, that experiment name must be specified in the `experiment` argument.
|
|
47 |
#' Furthermore, to specify filter for an `SummarizedExperiment` one must also set `arg`
|
|
48 |
#' (`"subset"` or `"select"`, arguments in the [subset()] function for `SummarizedExperiment`)
|
|
49 |
#' in order to determine whether the filter refers to the `SE`'s `rowData` or `colData`.
|
|
50 |
#'
|
|
51 |
#' @param dataname (`character(1)`) name of data set
|
|
52 |
#' @param varname (`character(1)`) name of variable
|
|
53 |
#' @param id (`character(1)`) identifier of the filter. Must be specified when `expr` is set.
|
|
54 |
#' When `varname` is specified then `id` is set to `"{dataname} {varname}"` by default.
|
|
55 |
#' @param expr (`character(1)`) string providing a logical expression.
|
|
56 |
#' Must be a valid `R` expression which can be evaluated in the context of the data set.
|
|
57 |
#' For a `data.frame` `var == "x"` is sufficient, but `MultiAssayExperiment::subsetByColData`
|
|
58 |
#' requires `dataname` prefix, *e.g.* `data$var == "x"`.
|
|
59 |
#' @param choices (`vector`) optional, specifies allowed choices;
|
|
60 |
#' When specified it should be a subset of values in variable denoted by `varname`;
|
|
61 |
#' Type and size depends on variable type. Factors are coerced to character.
|
|
62 |
#' @param selected (`vector`) optional, specifies selected values from `choices`;
|
|
63 |
#' Type and size depends on variable type. Factors are coerced to character.
|
|
64 |
#' @param multiple (`logical(1)`) optional flag specifying whether more than one value can be selected;
|
|
65 |
#' only applicable to `ChoicesFilterState` and `LogicalFilterState`
|
|
66 |
#' @param keep_na (`logical(1)`) optional flag specifying whether to keep missing values
|
|
67 |
#' @param keep_inf (`logical(1)`) optional flag specifying whether to keep infinite values
|
|
68 |
#' @param fixed (`logical(1)`) flag specifying whether to fix this filter state (forbid setting state)
|
|
69 |
#' @param anchored (`logical(1)`) flag specifying whether to lock this filter state (forbid removing and inactivating)
|
|
70 |
#' @param title (`character(1)`) optional title of the filter. Ignored when `varname` is set.
|
|
71 |
#' @param ... additional arguments which can be handled by extensions of `teal.slice` classes.
|
|
72 |
#'
|
|
73 |
#' @return A `teal.slice` object. Depending on whether `varname` or `expr` was specified, the resulting
|
|
74 |
#' `teal_slice` also receives class `teal_slice_var` or `teal_slice_expr`, respectively.
|
|
75 |
#'
|
|
76 |
#' @note Date time objects of `POSIX*t` classes are printed as strings after converting to UTC timezone.
|
|
77 |
#'
|
|
78 |
#' @examples
|
|
79 |
#' x1 <- teal_slice(
|
|
80 |
#' dataname = "data",
|
|
81 |
#' id = "Female adults",
|
|
82 |
#' expr = "SEX == 'F' & AGE >= 18",
|
|
83 |
#' title = "Female adults"
|
|
84 |
#' )
|
|
85 |
#' x2 <- teal_slice(
|
|
86 |
#' dataname = "data",
|
|
87 |
#' varname = "var",
|
|
88 |
#' choices = c("F", "M", "U"),
|
|
89 |
#' selected = "F",
|
|
90 |
#' keep_na = TRUE,
|
|
91 |
#' keep_inf = TRUE,
|
|
92 |
#' fixed = FALSE,
|
|
93 |
#' anchored = FALSE,
|
|
94 |
#' multiple = TRUE,
|
|
95 |
#' id = "Gender",
|
|
96 |
#' extra_arg = "extra"
|
|
97 |
#' )
|
|
98 |
#'
|
|
99 |
#' is.teal_slice(x1)
|
|
100 |
#' as.list(x1)
|
|
101 |
#' as.teal_slice(list(dataname = "a", varname = "var"))
|
|
102 |
#' format(x1)
|
|
103 |
#' format(x1, show_all = TRUE, trim_lines = FALSE)
|
|
104 |
#' print(x1)
|
|
105 |
#' print(x1, show_all = TRUE, trim_lines = FALSE)
|
|
106 |
#'
|
|
107 |
#' @seealso [`teal_slices`],
|
|
108 |
#' [`is.teal_slice`], [`as.teal_slice`], [`as.list.teal_slice`], [`print.teal_slice`], [`format.teal_slice`]
|
|
109 |
#'
|
|
110 |
#' @export
|
|
111 |
teal_slice <- function(dataname, |
|
112 |
varname,
|
|
113 |
id,
|
|
114 |
expr,
|
|
115 |
choices = NULL, |
|
116 |
selected = NULL, |
|
117 |
keep_na = NULL, |
|
118 |
keep_inf = NULL, |
|
119 |
fixed = FALSE, |
|
120 |
anchored = FALSE, |
|
121 |
multiple = TRUE, |
|
122 |
title = NULL, |
|
123 |
...) { |
|
124 | 578x |
checkmate::assert_string(dataname) |
125 | 571x |
checkmate::assert_flag(fixed) |
126 | 569x |
checkmate::assert_flag(anchored) |
127 | ||
128 | 567x |
formal_args <- as.list(environment()) |
129 | ||
130 | 567x |
if (!missing(expr) && !missing(varname)) { |
131 | ! |
stop("Must provide either `expr` or `varname`.") |
132 | 567x |
} else if (!missing(expr)) { |
133 | 30x |
checkmate::assert_string(id) |
134 | 27x |
checkmate::assert_string(title) |
135 | 24x |
checkmate::assert_string(expr) |
136 | ||
137 | 23x |
formal_args$fixed <- TRUE |
138 | 23x |
ts_expr_args <- c("dataname", "id", "expr", "fixed", "anchored", "title") |
139 | 23x |
formal_args <- formal_args[ts_expr_args] |
140 | 23x |
ans <- do.call(reactiveValues, c(formal_args, list(...))) |
141 | 23x |
class(ans) <- c("teal_slice_expr", "teal_slice", class(ans)) |
142 | 537x |
} else if (!missing(varname)) { |
143 | 536x |
checkmate::assert_string(varname) |
144 | 533x |
checkmate::assert_multi_class(choices, .filterable_class, null.ok = TRUE) |
145 | 532x |
checkmate::assert_multi_class(selected, .filterable_class, null.ok = TRUE) |
146 | 530x |
checkmate::assert_flag(keep_na, null.ok = TRUE) |
147 | 529x |
checkmate::assert_flag(keep_inf, null.ok = TRUE) |
148 | 528x |
checkmate::assert_flag(multiple) |
149 | ||
150 | 528x |
ts_var_args <- c( |
151 | 528x |
"dataname", "varname", "id", "choices", "selected", "keep_na", "keep_inf", |
152 | 528x |
"fixed", "anchored", "multiple" |
153 |
)
|
|
154 | 528x |
formal_args <- formal_args[ts_var_args] |
155 | 528x |
args <- c(formal_args, list(...)) |
156 | 528x |
args[c("choices", "selected")] <- |
157 | 528x |
lapply(args[c("choices", "selected")], function(x) if (is.factor(x)) as.character(x) else x) |
158 | 528x |
if (missing(id)) { |
159 | 519x |
args$id <- get_default_slice_id(args) |
160 |
} else { |
|
161 | 9x |
checkmate::assert_string(id) |
162 |
}
|
|
163 | 525x |
ans <- do.call(reactiveValues, args) |
164 | 525x |
class(ans) <- c("teal_slice_var", "teal_slice", class(ans)) |
165 |
} else { |
|
166 | 1x |
stop("Must provide either `expr` or `varname`.") |
167 |
}
|
|
168 | ||
169 | 548x |
ans
|
170 |
}
|
|
171 | ||
172 |
#' `teal_slice` utility functions
|
|
173 |
#'
|
|
174 |
#' Helper functions for working with [`teal_slice`] object.
|
|
175 |
#' @param x (`teal.slice`)
|
|
176 |
#' @param show_all (`logical(1)`) indicating whether to show all fields. If set to `FALSE`,
|
|
177 |
#' only non-NULL elements will be printed.
|
|
178 |
#' @param trim_lines (`logical(1)`) indicating whether to trim lines when printing.
|
|
179 |
#' @param ... additional arguments passed to other functions.
|
|
180 |
#' @name teal_slice-utilities
|
|
181 |
#' @inherit teal_slice examples
|
|
182 |
#' @keywords internal
|
|
183 | ||
184 |
#' @rdname teal_slice-utilities
|
|
185 |
#' @export
|
|
186 |
#'
|
|
187 |
is.teal_slice <- function(x) { # nolint |
|
188 | 4x |
inherits(x, "teal_slice") |
189 |
}
|
|
190 | ||
191 |
#' @rdname teal_slice-utilities
|
|
192 |
#' @export
|
|
193 |
#'
|
|
194 |
as.teal_slice <- function(x) { # nolint |
|
195 | ! |
checkmate::assert_list(x, names = "named") |
196 | ! |
do.call(teal_slice, x) |
197 |
}
|
|
198 | ||
199 |
#' @rdname teal_slice-utilities
|
|
200 |
#' @export
|
|
201 |
#'
|
|
202 |
as.list.teal_slice <- function(x, ...) { |
|
203 | 286x |
formal_args <- setdiff(names(formals(teal_slice)), "...") |
204 | ||
205 | 286x |
x <- if (isRunning()) { |
206 | ! |
reactiveValuesToList(x) |
207 |
} else { |
|
208 | 286x |
isolate(reactiveValuesToList(x)) |
209 |
}
|
|
210 | ||
211 | 286x |
formal_args <- intersect(formal_args, names(x)) |
212 | 286x |
extra_args <- rev(setdiff(names(x), formal_args)) |
213 | ||
214 | 286x |
x[c(formal_args, extra_args)] |
215 |
}
|
|
216 | ||
217 | ||
218 |
#' @rdname teal_slice-utilities
|
|
219 |
#' @export
|
|
220 |
#'
|
|
221 |
format.teal_slice <- function(x, show_all = FALSE, trim_lines = TRUE, ...) { |
|
222 | 117x |
checkmate::assert_flag(show_all) |
223 | 93x |
checkmate::assert_flag(trim_lines) |
224 | ||
225 | 87x |
x_list <- as.list(x) |
226 | 48x |
if (!show_all) x_list <- Filter(Negate(is.null), x_list) |
227 | ||
228 | 87x |
jsonify(x_list, trim_lines) |
229 |
}
|
|
230 | ||
231 |
#' @rdname teal_slice-utilities
|
|
232 |
#' @export
|
|
233 |
#'
|
|
234 |
print.teal_slice <- function(x, ...) { |
|
235 | 16x |
cat(format(x, ...)) |
236 | 16x |
invisible(x) |
237 |
}
|
|
238 | ||
239 | ||
240 |
# format utils -----
|
|
241 | ||
242 |
#' Convert a list to a justified `JSON` string
|
|
243 |
#'
|
|
244 |
#' This function takes a list and converts it to a `JSON` string.
|
|
245 |
#' The resulting `JSON` string is then optionally justified to improve readability
|
|
246 |
#' and trimmed to easier fit in the console when printing.
|
|
247 |
#'
|
|
248 |
#' @param x (`list`), possibly recursive, obtained from `teal_slice` or `teal_slices`.
|
|
249 |
#' @param trim_lines (`logical(1)`) flag specifying whether to trim lines of the `JSON` string.
|
|
250 |
#' @return A `JSON` string representation of the input list.
|
|
251 |
#' @keywords internal
|
|
252 |
#'
|
|
253 |
jsonify <- function(x, trim_lines) { |
|
254 | 133x |
checkmate::assert_list(x) |
255 | ||
256 | 133x |
x_json <- to_json(x) |
257 | 133x |
x_json_justified <- justify_json(x_json) |
258 | 123x |
if (trim_lines) x_json_justified <- trim_lines_json(x_json_justified) |
259 | 133x |
paste(x_json_justified, collapse = "\n") |
260 |
}
|
|
261 | ||
262 |
#' Converts a list to a `JSON` string
|
|
263 |
#'
|
|
264 |
#' Converts a list representation of `teal_slice` or `teal_slices` into a `JSON` string.
|
|
265 |
#' Ensures proper unboxing of list elements.
|
|
266 |
#' This function is used by the `format` methods for `teal_slice` and `teal_slices`.
|
|
267 |
#' @param x (`list`) possibly recursive, obtained from `teal_slice` or `teal_slices`.
|
|
268 |
#' @return A `JSON` string.
|
|
269 |
#
|
|
270 |
#' @param x (`list`) representation of `teal_slices` object.
|
|
271 |
#' @keywords internal
|
|
272 |
#'
|
|
273 |
to_json <- function(x) { |
|
274 | 133x |
no_unbox <- function(x) { |
275 | 2415x |
vars <- c("selected", "choices") |
276 | 2415x |
if (is.list(x)) { |
277 | 391x |
for (var in vars) { |
278 | 307x |
if (!is.null(x[[var]])) x[[var]] <- I(format_time(x[[var]])) |
279 |
}
|
|
280 | 391x |
lapply(x, no_unbox) |
281 |
} else { |
|
282 | 2024x |
x
|
283 |
}
|
|
284 |
}
|
|
285 | ||
286 | 133x |
jsonlite::toJSON(no_unbox(x), pretty = TRUE, auto_unbox = TRUE, digits = 16, null = "null") |
287 |
}
|
|
288 | ||
289 |
#' Format `POSIXt` for storage
|
|
290 |
#'
|
|
291 |
#' Convert `POSIXt` date time object to character representation in UTC time zone.
|
|
292 |
#'
|
|
293 |
#' Date times are stored as string representations expressed in the UTC time zone.
|
|
294 |
#' The storage format is `YYYY-MM-DD HH:MM:SS`.
|
|
295 |
#'
|
|
296 |
#' @param x (`POSIXt`) vector of date time values or anything else
|
|
297 |
#'
|
|
298 |
#' @return If `x` is of class `POSIXt`, a character vector, otherwise `x` itself.
|
|
299 |
#'
|
|
300 |
#' @keywords internal
|
|
301 |
format_time <- function(x) { |
|
302 | 307x |
if ("POSIXt" %in% class(x)) { |
303 | 20x |
format(x, format = "%Y-%m-%d %H:%M:%S", usetz = TRUE, tz = "UTC") |
304 |
} else { |
|
305 | 287x |
x
|
306 |
}
|
|
307 |
}
|
|
308 | ||
309 |
#' Justify colons in `JSON` string
|
|
310 |
#'
|
|
311 |
#' This function takes a `JSON` string as input, splits it into lines, and pads element names
|
|
312 |
#' with spaces so that colons are justified between lines.
|
|
313 |
#'
|
|
314 |
#' @param json (`character(1)`) a `JSON` string.
|
|
315 |
#'
|
|
316 |
#' @return A list of character strings, which can be collapsed into a `JSON` string.
|
|
317 |
#'
|
|
318 |
#' @keywords internal
|
|
319 |
justify_json <- function(json) { |
|
320 | 133x |
format_name <- function(name, name_width) { |
321 | 2806x |
if (nchar(name) == 1 || nchar(gsub("\\s", "", name)) <= 2) { |
322 | 640x |
return(name) |
323 | 2166x |
} else if (grepl("slices|attributes", name)) { |
324 | 92x |
paste0(name, ":") |
325 |
} else { |
|
326 | 2074x |
paste(format(name, width = name_width), ":") |
327 |
}
|
|
328 |
}
|
|
329 | 133x |
json_lines <- strsplit(json, "\n")[[1]] |
330 | 133x |
json_lines_split <- regmatches(json_lines, regexpr(":", json_lines), invert = TRUE) |
331 | 133x |
name_width <- max(unlist(regexpr(":", json_lines))) - 1 |
332 | 133x |
vapply(json_lines_split, function(x) paste0(format_name(x[1], name_width), stats::na.omit(x[2])), character(1)) |
333 |
}
|
|
334 | ||
335 |
#' Trim lines in `JSON` string
|
|
336 |
#'
|
|
337 |
#' This function takes a `JSON` string as input and returns a modified version of the
|
|
338 |
#' input where the values portion of each line is trimmed for a less messy console output.
|
|
339 |
#'
|
|
340 |
#' @param x (`character`)
|
|
341 |
#'
|
|
342 |
#' @return A character string trimmed after a certain hard-coded number of characters in the value portion.
|
|
343 |
#'
|
|
344 |
#' @keywords internal
|
|
345 |
#'
|
|
346 |
trim_lines_json <- function(x) { |
|
347 | 123x |
name_width <- max(unlist(gregexpr(":", x))) - 1 |
348 | 123x |
trim_position <- name_width + 37L |
349 | 123x |
x_trim <- substr(x, 1, trim_position) |
350 | 123x |
substr(x_trim, trim_position - 2, trim_position) <- "..." |
351 | 123x |
x_trim
|
352 |
}
|
|
353 | ||
354 |
#' Default `teal_slice` id
|
|
355 |
#'
|
|
356 |
#' Create a slice id if none provided.
|
|
357 |
#'
|
|
358 |
#' Function returns a default `id` for a `teal_slice` object which needs
|
|
359 |
#' to be distinct from other `teal_slice` objects created for any
|
|
360 |
#' `FilterStates` object. Returned `id` can be treated as a location of
|
|
361 |
#' a vector on which `FilterState` is built:
|
|
362 |
#' - for a `data.frame` `id` concatenates `dataname` and `varname`.
|
|
363 |
#' - for a `MultiAssayExperiment` `id` concatenates `dataname`, `varname`,
|
|
364 |
#' `experiment` and `arg`, so that one can add `teal_slice` for a `varname`
|
|
365 |
#' which exists in multiple `SummarizedExperiment`s or exists in both `colData`
|
|
366 |
#' and `rowData` of given experiment.
|
|
367 |
#' For such a vector `teal.slice` doesn't allow to activate more than one filters.
|
|
368 |
#' In case of `teal_slice_expr` `id` is mandatory and must be unique.
|
|
369 |
#'
|
|
370 |
#' @param x (`teal_slice` or `list`)
|
|
371 |
#' @return (`character(1)`) `id` for a `teal_slice` object.
|
|
372 |
#'
|
|
373 |
#' @keywords internal
|
|
374 |
get_default_slice_id <- function(x) { |
|
375 | 608x |
checkmate::assert_multi_class(x, c("teal_slice", "list")) |
376 | 608x |
isolate({ |
377 | 608x |
if (inherits(x, "teal_slice_expr") || is.null(x$varname)) { |
378 | 10x |
x$id |
379 |
} else { |
|
380 | 598x |
paste( |
381 | 598x |
Filter( |
382 | 598x |
length,
|
383 | 598x |
as.list(x)[c("dataname", "varname", "experiment", "arg")] |
384 |
),
|
|
385 | 598x |
collapse = " " |
386 |
)
|
|
387 |
}
|
|
388 |
}) |
|
389 |
}
|
1 |
# LogicalFilterState ------
|
|
2 | ||
3 |
#' @name LogicalFilterState
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title `FilterState` object for logical data
|
|
7 |
#'
|
|
8 |
#' @description Manages choosing a logical state.
|
|
9 |
#'
|
|
10 |
#' @examples
|
|
11 |
#' # use non-exported function from teal.slice
|
|
12 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice")
|
|
13 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice")
|
|
14 |
#' LogicalFilterState <- getFromNamespace("LogicalFilterState", "teal.slice")
|
|
15 |
#'
|
|
16 |
#' library(shiny)
|
|
17 |
#'
|
|
18 |
#' filter_state <- LogicalFilterState$new(
|
|
19 |
#' x = sample(c(TRUE, FALSE, NA), 10, replace = TRUE),
|
|
20 |
#' slice = teal_slice(varname = "x", dataname = "data")
|
|
21 |
#' )
|
|
22 |
#' isolate(filter_state$get_call())
|
|
23 |
#' filter_state$set_state(
|
|
24 |
#' teal_slice(dataname = "data", varname = "x", selected = TRUE, keep_na = TRUE)
|
|
25 |
#' )
|
|
26 |
#' isolate(filter_state$get_call())
|
|
27 |
#'
|
|
28 |
#' # working filter in an app
|
|
29 |
#' library(shinyjs)
|
|
30 |
#'
|
|
31 |
#' data_logical <- c(sample(c(TRUE, FALSE), 10, replace = TRUE), NA)
|
|
32 |
#' fs <- LogicalFilterState$new(
|
|
33 |
#' x = data_logical,
|
|
34 |
#' slice = teal_slice(dataname = "data", varname = "x", selected = FALSE, keep_na = TRUE)
|
|
35 |
#' )
|
|
36 |
#'
|
|
37 |
#' ui <- fluidPage(
|
|
38 |
#' useShinyjs(),
|
|
39 |
#' include_css_files(pattern = "filter-panel"),
|
|
40 |
#' include_js_files(pattern = "count-bar-labels"),
|
|
41 |
#' column(4, tags$div(
|
|
42 |
#' tags$h4("LogicalFilterState"),
|
|
43 |
#' fs$ui("fs")
|
|
44 |
#' )),
|
|
45 |
#' column(4, tags$div(
|
|
46 |
#' id = "outputs", # div id is needed for toggling the element
|
|
47 |
#' tags$h4("Condition (i.e. call)"), # display the condition call generated by this FilterState
|
|
48 |
#' textOutput("condition_logical"), tags$br(),
|
|
49 |
#' tags$h4("Unformatted state"), # display raw filter state
|
|
50 |
#' textOutput("unformatted_logical"), tags$br(),
|
|
51 |
#' tags$h4("Formatted state"), # display human readable filter state
|
|
52 |
#' textOutput("formatted_logical"), tags$br()
|
|
53 |
#' )),
|
|
54 |
#' column(4, tags$div(
|
|
55 |
#' tags$h4("Programmatic filter control"),
|
|
56 |
#' actionButton("button1_logical", "set drop NA", width = "100%"), tags$br(),
|
|
57 |
#' actionButton("button2_logical", "set keep NA", width = "100%"), tags$br(),
|
|
58 |
#' actionButton("button3_logical", "set a selection", width = "100%"), tags$br(),
|
|
59 |
#' actionButton("button0_logical", "set initial state", width = "100%"), tags$br()
|
|
60 |
#' ))
|
|
61 |
#' )
|
|
62 |
#'
|
|
63 |
#' server <- function(input, output, session) {
|
|
64 |
#' fs$server("fs")
|
|
65 |
#' output$condition_logical <- renderPrint(fs$get_call())
|
|
66 |
#' output$formatted_logical <- renderText(fs$format())
|
|
67 |
#' output$unformatted_logical <- renderPrint(fs$get_state())
|
|
68 |
#' # modify filter state programmatically
|
|
69 |
#' observeEvent(
|
|
70 |
#' input$button1_logical,
|
|
71 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = FALSE))
|
|
72 |
#' )
|
|
73 |
#' observeEvent(
|
|
74 |
#' input$button2_logical,
|
|
75 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = TRUE))
|
|
76 |
#' )
|
|
77 |
#' observeEvent(
|
|
78 |
#' input$button3_logical,
|
|
79 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", selected = TRUE))
|
|
80 |
#' )
|
|
81 |
#' observeEvent(
|
|
82 |
#' input$button0_logical,
|
|
83 |
#' fs$set_state(
|
|
84 |
#' teal_slice(dataname = "data", varname = "x", selected = FALSE, keep_na = TRUE)
|
|
85 |
#' )
|
|
86 |
#' )
|
|
87 |
#' }
|
|
88 |
#'
|
|
89 |
#' if (interactive()) {
|
|
90 |
#' shinyApp(ui, server)
|
|
91 |
#' }
|
|
92 |
#'
|
|
93 |
#' @keywords internal
|
|
94 |
#'
|
|
95 |
LogicalFilterState <- R6::R6Class( # nolint |
|
96 |
"LogicalFilterState",
|
|
97 |
inherit = FilterState, |
|
98 | ||
99 |
# public methods ----
|
|
100 |
public = list( |
|
101 | ||
102 |
#' @description
|
|
103 |
#' Initialize a `FilterState` object.
|
|
104 |
#'
|
|
105 |
#' @param x (`logical`)
|
|
106 |
#' variable to be filtered.
|
|
107 |
#' @param x_reactive (`reactive`)
|
|
108 |
#' returning vector of the same type as `x`. Is used to update
|
|
109 |
#' counts following the change in values of the filtered dataset.
|
|
110 |
#' If it is set to `reactive(NULL)` then counts based on filtered
|
|
111 |
#' dataset are not shown.
|
|
112 |
#' @param slice (`teal_slice`)
|
|
113 |
#' specification of this filter state.
|
|
114 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`.
|
|
115 |
#' `get_state` returns `teal_slice` object which can be reused in other places.
|
|
116 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e.
|
|
117 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`.
|
|
118 |
#' @param extract_type (`character`)
|
|
119 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values:
|
|
120 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed
|
|
121 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>`
|
|
122 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]`
|
|
123 |
#'
|
|
124 |
#' @return Object of class `LogicalFilterState`, invisibly.
|
|
125 |
#'
|
|
126 |
initialize = function(x, |
|
127 |
x_reactive = reactive(NULL), |
|
128 |
extract_type = character(0), |
|
129 |
slice) { |
|
130 | 16x |
isolate({ |
131 | 16x |
checkmate::assert_logical(x) |
132 | 15x |
checkmate::assert_logical(slice$selected, null.ok = TRUE) |
133 | 14x |
super$initialize(x = x, x_reactive = x_reactive, slice = slice, extract_type = extract_type) |
134 | ||
135 | 14x |
private$set_choices(slice$choices) |
136 | ! |
if (is.null(slice$multiple)) slice$multiple <- FALSE |
137 | 14x |
if (is.null(slice$selected) && slice$multiple) { |
138 | 7x |
slice$selected <- private$get_choices() |
139 | 7x |
} else if (length(slice$selected) != 1 && !slice$multiple) { |
140 | 3x |
slice$selected <- TRUE |
141 |
}
|
|
142 | 14x |
private$set_selected(slice$selected) |
143 | 14x |
df <- factor(x, levels = c(TRUE, FALSE)) |
144 | 14x |
tbl <- table(df) |
145 | 14x |
private$set_choices_counts(tbl) |
146 |
}) |
|
147 | 14x |
invisible(self) |
148 |
},
|
|
149 | ||
150 |
#' @description
|
|
151 |
#' Returns reproducible condition call for current selection.
|
|
152 |
#' For `LogicalFilterState` it's a `!<varname>` or `<varname>` and optionally `is.na(<varname>)`
|
|
153 |
#' @param dataname name of data set; defaults to `private$get_dataname()`
|
|
154 |
#' @return `call`
|
|
155 |
#'
|
|
156 |
get_call = function(dataname) { |
|
157 | 6x |
if (isFALSE(private$is_any_filtered())) { |
158 | ! |
return(NULL) |
159 |
}
|
|
160 | 4x |
if (missing(dataname)) dataname <- private$get_dataname() |
161 | 6x |
varname <- private$get_varname_prefixed(dataname) |
162 | 6x |
choices <- private$get_selected() |
163 | 6x |
n_choices <- length(choices) |
164 | ||
165 | 6x |
filter_call <- |
166 | 6x |
if (n_choices == 1 && choices) { |
167 | 1x |
varname
|
168 | 6x |
} else if (n_choices == 1 && !choices) { |
169 | 4x |
call("!", varname) |
170 |
} else { |
|
171 | 1x |
call("%in%", varname, make_c_call(choices)) |
172 |
}
|
|
173 | 6x |
private$add_keep_na_call(filter_call, varname) |
174 |
}
|
|
175 |
),
|
|
176 | ||
177 |
# private members ----
|
|
178 |
private = list( |
|
179 |
choices_counts = integer(0), |
|
180 | ||
181 |
# private methods ----
|
|
182 |
set_choices = function(choices) { |
|
183 | 14x |
private$teal_slice$choices <- c(TRUE, FALSE) |
184 | 14x |
invisible(NULL) |
185 |
},
|
|
186 |
# @description
|
|
187 |
# Sets choices_counts private field
|
|
188 |
set_choices_counts = function(choices_counts) { |
|
189 | 14x |
private$choices_counts <- choices_counts |
190 | 14x |
invisible(NULL) |
191 |
},
|
|
192 |
cast_and_validate = function(values) { |
|
193 | 21x |
tryCatch( |
194 | 21x |
expr = { |
195 | 21x |
values <- as.logical(values) |
196 | 1x |
if (anyNA(values)) stop() |
197 | 20x |
values
|
198 |
},
|
|
199 | 21x |
error = function(e) stop("Vector of set values must contain values coercible to logical.") |
200 |
)
|
|
201 |
},
|
|
202 |
# If multiple forbidden but selected, restores previous selection with warning.
|
|
203 |
check_length = function(values) { |
|
204 | 20x |
if (!private$is_multiple() && length(values) > 1) { |
205 | 1x |
warning( |
206 | 1x |
sprintf("Selection: %s is not a vector of length one. ", toString(values, width = 360)), |
207 | 1x |
"Maintaining previous selection."
|
208 |
)
|
|
209 | 1x |
values <- isolate(private$get_selected()) |
210 |
}
|
|
211 | 20x |
values
|
212 |
},
|
|
213 | ||
214 |
# Answers the question of whether the current settings and values selected actually filters out any values.
|
|
215 |
# @return logical scalar
|
|
216 |
is_any_filtered = function() { |
|
217 | 6x |
if (private$is_choice_limited) { |
218 | ! |
TRUE
|
219 | 6x |
} else if (all(private$choices_counts > 0)) { |
220 | 6x |
TRUE
|
221 |
} else if ( |
|
222 | ! |
setequal(private$get_selected(), private$get_choices()) && |
223 | ! |
!anyNA(private$get_selected(), private$get_choices()) |
224 |
) { |
|
225 | ! |
TRUE
|
226 | ! |
} else if (!isTRUE(private$get_keep_na()) && private$na_count > 0) { |
227 | ! |
TRUE
|
228 |
} else { |
|
229 | ! |
FALSE
|
230 |
}
|
|
231 |
},
|
|
232 | ||
233 |
# shiny modules ----
|
|
234 | ||
235 |
# @description
|
|
236 |
# UI Module for `EmptyFilterState`.
|
|
237 |
# This UI element contains available choices selection and
|
|
238 |
# checkbox whether to keep or not keep the `NA` values.
|
|
239 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
240 |
ui_inputs = function(id) { |
|
241 | ! |
ns <- NS(id) |
242 | ! |
isolate({ |
243 | ! |
countsmax <- private$choices_counts |
244 | ! |
countsnow <- if (!is.null(private$x_reactive())) { |
245 | ! |
unname(table(factor(private$x_reactive(), levels = private$get_choices()))) |
246 |
} else { |
|
247 | ! |
NULL
|
248 |
}
|
|
249 | ||
250 | ! |
labels <- countBars( |
251 | ! |
inputId = ns("labels"), |
252 | ! |
choices = as.character(private$get_choices()), |
253 | ! |
countsnow = countsnow, |
254 | ! |
countsmax = countsmax |
255 |
)
|
|
256 | ! |
ui_input <- if (private$is_multiple()) { |
257 | ! |
checkboxGroupInput( |
258 | ! |
inputId = ns("selection"), |
259 | ! |
label = NULL, |
260 | ! |
selected = isolate(as.character(private$get_selected())), |
261 | ! |
choiceNames = labels, |
262 | ! |
choiceValues = factor(as.character(private$get_choices()), levels = c("TRUE", "FALSE")), |
263 | ! |
width = "100%" |
264 |
)
|
|
265 |
} else { |
|
266 | ! |
radioButtons( |
267 | ! |
inputId = ns("selection"), |
268 | ! |
label = NULL, |
269 | ! |
selected = isolate(as.character(private$get_selected())), |
270 | ! |
choiceNames = labels, |
271 | ! |
choiceValues = factor(as.character(private$get_choices()), levels = c("TRUE", "FALSE")), |
272 | ! |
width = "100%" |
273 |
)
|
|
274 |
}
|
|
275 | ! |
tags$div( |
276 | ! |
tags$div( |
277 | ! |
class = "choices_state", |
278 | ! |
uiOutput(ns("trigger_visible"), inline = TRUE), |
279 | ! |
ui_input
|
280 |
),
|
|
281 | ! |
private$keep_na_ui(ns("keep_na")) |
282 |
)
|
|
283 |
}) |
|
284 |
},
|
|
285 | ||
286 |
# @description
|
|
287 |
# Server module
|
|
288 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
289 |
# @return `NULL`.
|
|
290 |
server_inputs = function(id) { |
|
291 | ! |
moduleServer( |
292 | ! |
id = id, |
293 | ! |
function(input, output, session) { |
294 |
# this observer is needed in the situation when teal_slice$selected has been
|
|
295 |
# changed directly by the api - then it's needed to rerender UI element
|
|
296 |
# to show relevant values
|
|
297 | ! |
non_missing_values <- reactive(Filter(Negate(is.na), private$x_reactive())) |
298 | ! |
output$trigger_visible <- renderUI({ |
299 | ! |
logger::log_debug("LogicalFilterState$server@1 updating count labels, id: { private$get_id() }") |
300 | ||
301 | ! |
countsnow <- if (!is.null(private$x_reactive())) { |
302 | ! |
unname(table(factor(non_missing_values(), levels = private$get_choices()))) |
303 |
} else { |
|
304 | ! |
NULL
|
305 |
}
|
|
306 | ||
307 | ! |
updateCountBars( |
308 | ! |
inputId = "labels", |
309 | ! |
choices = as.character(private$get_choices()), |
310 | ! |
countsmax = private$choices_counts, |
311 | ! |
countsnow = countsnow |
312 |
)
|
|
313 | ! |
NULL
|
314 |
}) |
|
315 | ||
316 | ! |
private$session_bindings[[session$ns("selected_api")]] <- observeEvent( |
317 | ! |
ignoreNULL = !private$is_multiple(), |
318 | ! |
ignoreInit = TRUE, |
319 | ! |
eventExpr = private$get_selected(), |
320 | ! |
handlerExpr = { |
321 | ! |
if (!setequal(private$get_selected(), input$selection)) { |
322 | ! |
logger::log_debug("LogicalFilterState$server@1 state changed, id: { private$get_id() }") |
323 | ! |
if (private$is_multiple()) { |
324 | ! |
updateCheckboxGroupInput( |
325 | ! |
inputId = "selection", |
326 | ! |
selected = private$get_selected() |
327 |
)
|
|
328 |
} else { |
|
329 | ! |
updateRadioButtons( |
330 | ! |
inputId = "selection", |
331 | ! |
selected = private$get_selected() |
332 |
)
|
|
333 |
}
|
|
334 |
}
|
|
335 |
}
|
|
336 |
)
|
|
337 | ||
338 | ! |
private$session_bindings[[session$ns("selection")]] <- observeEvent( |
339 | ! |
ignoreNULL = FALSE, |
340 | ! |
ignoreInit = TRUE, |
341 | ! |
eventExpr = input$selection, |
342 | ! |
handlerExpr = { |
343 | ! |
logger::log_debug("LogicalFilterState$server@2 selection changed, id: { private$get_id() }") |
344 |
# for private$is_multiple() == TRUE input$selection will always have value
|
|
345 | ! |
if (is.null(input$selection) && isFALSE(private$is_multiple())) { |
346 | ! |
selection_state <- private$get_selected() |
347 |
} else { |
|
348 | ! |
selection_state <- as.logical(input$selection) |
349 |
}
|
|
350 | ||
351 | ! |
if (is.null(selection_state)) { |
352 | ! |
selection_state <- logical(0) |
353 |
}
|
|
354 | ! |
private$set_selected(selection_state) |
355 |
}
|
|
356 |
)
|
|
357 | ||
358 | ! |
private$keep_na_srv("keep_na") |
359 | ||
360 | ! |
NULL
|
361 |
}
|
|
362 |
)
|
|
363 |
},
|
|
364 |
server_inputs_fixed = function(id) { |
|
365 | ! |
moduleServer( |
366 | ! |
id = id, |
367 | ! |
function(input, output, session) { |
368 | ! |
logger::log_debug("LogicalFilterState$server initializing, id: { private$get_id() }") |
369 | ||
370 | ! |
output$selection <- renderUI({ |
371 | ! |
countsnow <- unname(table(factor(private$x_reactive(), levels = private$get_choices()))) |
372 | ! |
countsmax <- private$choices_counts |
373 | ||
374 | ! |
ind <- private$get_choices() %in% private$get_selected() |
375 | ! |
countBars( |
376 | ! |
inputId = session$ns("labels"), |
377 | ! |
choices = private$get_selected(), |
378 | ! |
countsnow = countsnow[ind], |
379 | ! |
countsmax = countsmax[ind] |
380 |
)
|
|
381 |
}) |
|
382 | ||
383 | ! |
NULL
|
384 |
}
|
|
385 |
)
|
|
386 |
},
|
|
387 | ||
388 |
# @description
|
|
389 |
# Server module to display filter summary
|
|
390 |
# renders text describing whether TRUE or FALSE is selected
|
|
391 |
# and if NA are included also
|
|
392 |
content_summary = function(id) { |
|
393 | ! |
tagList( |
394 | ! |
tags$span( |
395 | ! |
class = "filter-card-summary-value", |
396 | ! |
toString(private$get_selected()) |
397 |
),
|
|
398 | ! |
tags$span( |
399 | ! |
class = "filter-card-summary-controls", |
400 | ! |
if (private$na_count > 0) { |
401 | ! |
tags$span("NA", if (isTRUE(private$get_keep_na())) icon("check") else icon("xmark")) |
402 |
}
|
|
403 |
)
|
|
404 |
)
|
|
405 |
}
|
|
406 |
)
|
|
407 |
)
|
1 |
# DataframeFilteredDataset ------
|
|
2 | ||
3 |
#' @name DataframeFilteredDataset
|
|
4 |
#' @docType class
|
|
5 |
#' @title The `DataframeFilteredDataset` `R6` class
|
|
6 |
#'
|
|
7 |
#' @examples
|
|
8 |
#' # use non-exported function from teal.slice
|
|
9 |
#' DataframeFilteredDataset <- getFromNamespace("DataframeFilteredDataset", "teal.slice")
|
|
10 |
#'
|
|
11 |
#' library(shiny)
|
|
12 |
#'
|
|
13 |
#' ds <- DataframeFilteredDataset$new(iris, "iris")
|
|
14 |
#' ds$set_filter_state(
|
|
15 |
#' teal_slices(
|
|
16 |
#' teal_slice(dataname = "iris", varname = "Species", selected = "virginica"),
|
|
17 |
#' teal_slice(dataname = "iris", varname = "Petal.Length", selected = c(2.0, 5))
|
|
18 |
#' )
|
|
19 |
#' )
|
|
20 |
#' isolate(ds$get_filter_state())
|
|
21 |
#' isolate(ds$get_call())
|
|
22 |
#'
|
|
23 |
#' ## set_filter_state
|
|
24 |
#' dataset <- DataframeFilteredDataset$new(iris, "iris")
|
|
25 |
#' fs <- teal_slices(
|
|
26 |
#' teal_slice(dataname = "iris", varname = "Species", selected = "virginica"),
|
|
27 |
#' teal_slice(dataname = "iris", varname = "Petal.Length", selected = c(2.0, 5))
|
|
28 |
#' )
|
|
29 |
#' dataset$set_filter_state(state = fs)
|
|
30 |
#' isolate(dataset$get_filter_state())
|
|
31 |
#'
|
|
32 |
#' @keywords internal
|
|
33 |
#'
|
|
34 |
DataframeFilteredDataset <- R6::R6Class( # nolint |
|
35 |
classname = "DataframeFilteredDataset", |
|
36 |
inherit = FilteredDataset, |
|
37 | ||
38 |
# public fields ----
|
|
39 |
public = list( |
|
40 | ||
41 |
#' @description
|
|
42 |
#' Initializes this `DataframeFilteredDataset` object.
|
|
43 |
#'
|
|
44 |
#' @param dataset (`data.frame`)
|
|
45 |
#' single `data.frame` for which filters are rendered.
|
|
46 |
#' @param dataname (`character(1)`)
|
|
47 |
#' syntactically valid name given to the dataset.
|
|
48 |
#' @param keys (`character`) optional
|
|
49 |
#' vector of primary key column names.
|
|
50 |
#' @param parent_name (`character(1)`)
|
|
51 |
#' name of the parent dataset.
|
|
52 |
#' @param parent (`reactive`)
|
|
53 |
#' that returns a filtered `data.frame` from other `FilteredDataset` named `parent_name`.
|
|
54 |
#' Passing `parent` results in a `reactive` link that causes re-filtering of this `dataset`
|
|
55 |
#' based on the changes in `parent`.
|
|
56 |
#' @param join_keys (`character`)
|
|
57 |
#' vector of names of columns in this dataset to join with `parent` dataset.
|
|
58 |
#' If column names in the parent do not match these, they should be given as the names of this vector.
|
|
59 |
#' @param label (`character(1)`)
|
|
60 |
#' label to describe the dataset.
|
|
61 |
#'
|
|
62 |
#' @return Object of class `DataframeFilteredDataset`, invisibly.
|
|
63 |
#'
|
|
64 |
initialize = function(dataset, |
|
65 |
dataname,
|
|
66 |
keys = character(0), |
|
67 |
parent_name = character(0), |
|
68 |
parent = NULL, |
|
69 |
join_keys = character(0), |
|
70 |
label = character(0)) { |
|
71 | 99x |
checkmate::assert_data_frame(dataset) |
72 | 97x |
super$initialize(dataset, dataname, keys, label) |
73 | ||
74 |
# overwrite filtered_data if there is relationship with parent dataset
|
|
75 | 95x |
if (!is.null(parent)) { |
76 | 10x |
checkmate::assert_character(parent_name, len = 1) |
77 | 10x |
checkmate::assert_character(join_keys, min.len = 1) |
78 | ||
79 | 10x |
private$parent_name <- parent_name |
80 | 10x |
private$join_keys <- join_keys |
81 | ||
82 | 10x |
private$data_filtered_fun <- function(sid = "") { |
83 | 8x |
checkmate::assert_character(sid) |
84 | 8x |
if (length(sid)) { |
85 | 8x |
logger::log_debug("filtering data dataname: { dataname }, sid: { sid }") |
86 |
} else { |
|
87 | ! |
logger::log_debug("filtering data dataname: { dataname }") |
88 |
}
|
|
89 | 8x |
env <- new.env(parent = parent.env(globalenv())) |
90 | 8x |
env[[dataname]] <- private$dataset |
91 | 8x |
env[[parent_name]] <- parent() |
92 | 8x |
filter_call <- self$get_call(sid) |
93 | 8x |
eval_expr_with_msg(filter_call, env) |
94 | 8x |
get(x = dataname, envir = env) |
95 |
}
|
|
96 |
}
|
|
97 | ||
98 | 95x |
private$add_filter_states( |
99 | 95x |
filter_states = init_filter_states( |
100 | 95x |
data = dataset, |
101 | 95x |
data_reactive = private$data_filtered_fun, |
102 | 95x |
dataname = dataname, |
103 | 95x |
keys = self$get_keys() |
104 |
),
|
|
105 | 95x |
id = "filter" |
106 |
)
|
|
107 | ||
108 |
# todo: Should we make these defaults? It could be handled by the app developer
|
|
109 | 95x |
if (!is.null(parent)) { |
110 | 10x |
logger::log_debug("Excluding { parent_name } columns from possible teal_slices for dataname: { dataname }") |
111 | 10x |
fs <- teal_slices( |
112 | 10x |
exclude_varnames = structure( |
113 | 10x |
list(intersect(colnames(dataset), colnames(isolate(parent())))), |
114 | 10x |
names = private$dataname |
115 |
)
|
|
116 |
)
|
|
117 | 10x |
self$set_filter_state(fs) |
118 |
}
|
|
119 | ||
120 | 95x |
invisible(self) |
121 |
},
|
|
122 | ||
123 |
#' @description
|
|
124 |
#' Gets the subset expression.
|
|
125 |
#'
|
|
126 |
#' This function returns subset expressions equivalent to selected items
|
|
127 |
#' within each of `filter_states`. Configuration of the expressions is constant and
|
|
128 |
#' depends on `filter_states` type and order which are set during initialization.
|
|
129 |
#' This class contains single `FilterStates` which contains single `state_list`
|
|
130 |
#' and all `FilterState` objects apply to one argument (`...`) in a `dplyr::filter` call.
|
|
131 |
#'
|
|
132 |
#' @param sid (`character`)
|
|
133 |
#' when specified, the method returns code containing conditions calls of
|
|
134 |
#' `FilterState` objects with `sid` different to that of this `sid` argument.
|
|
135 |
#'
|
|
136 |
#' @return Either a `list` of length 1 containing a filter `call`, or `NULL`.
|
|
137 |
get_call = function(sid = "") { |
|
138 | 42x |
filter_call <- super$get_call(sid) |
139 | 42x |
dataname <- private$dataname |
140 | 42x |
parent_dataname <- private$parent_name |
141 | ||
142 | 42x |
if (!identical(parent_dataname, character(0))) { |
143 | 9x |
join_keys <- private$join_keys |
144 | 9x |
parent_keys <- unname(join_keys) |
145 | 9x |
dataset_keys <- names(join_keys) |
146 | ||
147 | 9x |
y_arg <- if (length(parent_keys) == 0L) { |
148 | ! |
parent_dataname
|
149 |
} else { |
|
150 | 9x |
sprintf( |
151 | 9x |
"%s[, c(%s), drop = FALSE]",
|
152 | 9x |
parent_dataname,
|
153 | 9x |
toString(dQuote(parent_keys, q = FALSE)) |
154 |
)
|
|
155 |
}
|
|
156 | ||
157 | 9x |
more_args <- if (length(parent_keys) == 0 || length(dataset_keys) == 0) { |
158 | ! |
list() |
159 | 9x |
} else if (identical(parent_keys, dataset_keys)) { |
160 | 7x |
list(by = parent_keys) |
161 |
} else { |
|
162 | 2x |
list(by = stats::setNames(parent_keys, dataset_keys)) |
163 |
}
|
|
164 | ||
165 | 9x |
merge_call <- call( |
166 |
"<-",
|
|
167 | 9x |
as.name(dataname), |
168 | 9x |
as.call( |
169 | 9x |
c( |
170 | 9x |
str2lang("dplyr::inner_join"), |
171 | 9x |
x = as.name(dataname), |
172 | 9x |
y = str2lang(y_arg), |
173 | 9x |
more_args
|
174 |
)
|
|
175 |
)
|
|
176 |
)
|
|
177 | ||
178 | 9x |
filter_call <- c(filter_call, merge_call) |
179 |
}
|
|
180 | 42x |
filter_call
|
181 |
},
|
|
182 | ||
183 |
#' @description
|
|
184 |
#' Set filter state.
|
|
185 |
#'
|
|
186 |
#' @param state (`teal_slices`)
|
|
187 |
#' @return `NULL`, invisibly.
|
|
188 |
#'
|
|
189 |
set_filter_state = function(state) { |
|
190 | 77x |
isolate({ |
191 | 77x |
logger::log_debug("FilteredDatasetDataframe$set_filter_state initializing, dataname: { private$dataname }") |
192 | 77x |
checkmate::assert_class(state, "teal_slices") |
193 | 76x |
state_datanames <- unique(vapply(state, `[[`, character(1L), "dataname")) |
194 | 76x |
checkmate::assert_subset(state_datanames, private$dataname) |
195 | 76x |
private$get_filter_states()[[1L]]$set_filter_state(state = state) |
196 | 76x |
invisible(NULL) |
197 |
}) |
|
198 |
},
|
|
199 | ||
200 |
#' @description
|
|
201 |
#' Remove one or more `FilterState` form a `FilteredDataset`.
|
|
202 |
#'
|
|
203 |
#' @param state (`teal_slices`)
|
|
204 |
#' specifying `FilterState` objects to remove;
|
|
205 |
#' `teal_slice`s may contain only `dataname` and `varname`, other elements are ignored
|
|
206 |
#'
|
|
207 |
#' @return `NULL`, invisibly.
|
|
208 |
#'
|
|
209 |
remove_filter_state = function(state) { |
|
210 | 11x |
checkmate::assert_class(state, "teal_slices") |
211 | ||
212 | 11x |
isolate({ |
213 | 11x |
logger::log_debug( |
214 | 11x |
"FilteredDatasetDataframe$remove_filter_state removing filter(s), dataname: { private$dataname }"
|
215 |
)
|
|
216 | ||
217 | 11x |
varnames <- unique(unlist(lapply(state, "[[", "varname"))) |
218 | 11x |
private$get_filter_states()[[1]]$remove_filter_state(state) |
219 |
}) |
|
220 | ||
221 | 11x |
invisible(NULL) |
222 |
},
|
|
223 | ||
224 |
#' @description
|
|
225 |
#' UI module to add filter variable for this dataset.
|
|
226 |
#'
|
|
227 |
#' @param id (`character(1)`)
|
|
228 |
#' `shiny` module instance id.
|
|
229 |
#'
|
|
230 |
#' @return `shiny.tag`
|
|
231 |
ui_add = function(id) { |
|
232 | ! |
ns <- NS(id) |
233 | ! |
tagList( |
234 | ! |
tags$div( |
235 | ! |
tags$label("Add", tags$code(self$get_dataname()), "filter") |
236 |
),
|
|
237 | ! |
private$get_filter_states()[["filter"]]$ui_add(id = ns("filter")) |
238 |
)
|
|
239 |
},
|
|
240 | ||
241 |
#' @description
|
|
242 |
#' Creates row for filter overview in the form of \cr
|
|
243 |
#' `dataname -- observations (remaining/total)` - data.frame
|
|
244 |
#' @return A `data.frame`.
|
|
245 |
get_filter_overview = function() { |
|
246 | 12x |
logger::log_debug("FilteredDataset$srv_filter_overview initializing for dataname: { private$dataname }") |
247 |
# Gets filter overview subjects number and returns a list
|
|
248 |
# of the number of subjects of filtered/non-filtered datasets
|
|
249 | 12x |
subject_keys <- if (length(private$parent_name) > 0) { |
250 | 1x |
names(private$join_keys) |
251 |
} else { |
|
252 | 11x |
self$get_keys() |
253 |
}
|
|
254 | 12x |
dataset <- self$get_dataset() |
255 | 12x |
data_filtered <- self$get_dataset(TRUE) |
256 | 12x |
if (length(subject_keys) == 0) { |
257 | 10x |
data.frame( |
258 | 10x |
dataname = private$dataname, |
259 | 10x |
obs = nrow(dataset), |
260 | 10x |
obs_filtered = nrow(data_filtered()) |
261 |
)
|
|
262 |
} else { |
|
263 | 2x |
data.frame( |
264 | 2x |
dataname = private$dataname, |
265 | 2x |
obs = nrow(dataset), |
266 | 2x |
obs_filtered = nrow(data_filtered()), |
267 | 2x |
subjects = nrow(unique(dataset[subject_keys])), |
268 | 2x |
subjects_filtered = nrow(unique(data_filtered()[subject_keys])) |
269 |
)
|
|
270 |
}
|
|
271 |
}
|
|
272 |
),
|
|
273 | ||
274 |
# private fields ----
|
|
275 |
private = list( |
|
276 |
parent_name = character(0), |
|
277 |
join_keys = character(0) |
|
278 |
)
|
|
279 |
)
|
1 |
# FilteredData ------
|
|
2 | ||
3 |
#' @name FilteredData
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title Class to encapsulate filtered datasets
|
|
7 |
#'
|
|
8 |
#' @description
|
|
9 |
#' Manages filtering of all datasets in the application or module.
|
|
10 |
#'
|
|
11 |
#' @details
|
|
12 |
#' The main purpose of this class is to provide a collection of reactive datasets,
|
|
13 |
#' each dataset having a filter state that determines how it is filtered.
|
|
14 |
#'
|
|
15 |
#' For each dataset, `get_filter_expr` returns the call to filter the dataset according
|
|
16 |
#' to the filter state. The data itself can be obtained through `get_data`.
|
|
17 |
#'
|
|
18 |
#' The datasets are filtered lazily, i.e. only when requested / needed in a `shiny` app.
|
|
19 |
#'
|
|
20 |
#' By design, any `dataname` set through `set_dataset` cannot be removed because
|
|
21 |
#' other code may already depend on it. As a workaround, the underlying
|
|
22 |
#' data can be set to `NULL`.
|
|
23 |
#'
|
|
24 |
#' The class currently supports variables of the following types within datasets:
|
|
25 |
#' - `choices`: variable of type `factor`, e.g. `ADSL$COUNTRY`, `iris$Species`
|
|
26 |
#' zero or more options can be selected, when the variable is a factor
|
|
27 |
#' - `logical`: variable of type `logical`, e.g. `ADSL$TRT_FLAG`
|
|
28 |
#' exactly one option must be selected, `TRUE` or `FALSE`
|
|
29 |
#' - `ranges`: variable of type `numeric`, e.g. `ADSL$AGE`, `iris$Sepal.Length`
|
|
30 |
#' numerical range, a range within this range can be selected
|
|
31 |
#' - `dates`: variable of type `Date`, `POSIXlt`
|
|
32 |
#' Other variables cannot be used for filtering the data in this class.
|
|
33 |
#'
|
|
34 |
#' Common arguments are:
|
|
35 |
#' 1. `filtered`: whether to return a filtered result or not
|
|
36 |
#' 2. `dataname`: the name of one of the datasets in this `FilteredData` object
|
|
37 |
#' 3. `varname`: one of the columns in a dataset
|
|
38 |
#'
|
|
39 |
#' @examples
|
|
40 |
#' # use non-exported function from teal.slice
|
|
41 |
#' FilteredData <- getFromNamespace("FilteredData", "teal.slice")
|
|
42 |
#'
|
|
43 |
#' library(shiny)
|
|
44 |
#'
|
|
45 |
#' datasets <- FilteredData$new(list(iris = iris, mtcars = mtcars))
|
|
46 |
#'
|
|
47 |
#' # get datanames
|
|
48 |
#' datasets$datanames()
|
|
49 |
#'
|
|
50 |
#' datasets$set_filter_state(
|
|
51 |
#' teal_slices(teal_slice(dataname = "iris", varname = "Species", selected = "virginica"))
|
|
52 |
#' )
|
|
53 |
#'
|
|
54 |
#' datasets$set_filter_state(
|
|
55 |
#' teal_slices(teal_slice(dataname = "mtcars", varname = "mpg", selected = c(15, 20)))
|
|
56 |
#' )
|
|
57 |
#'
|
|
58 |
#' isolate(datasets$get_filter_state())
|
|
59 |
#' isolate(datasets$get_call("iris"))
|
|
60 |
#' isolate(datasets$get_call("mtcars"))
|
|
61 |
#'
|
|
62 |
#' @examplesIf requireNamespace("MultiAssayExperiment")
|
|
63 |
#' ### set_filter_state
|
|
64 |
#' library(shiny)
|
|
65 |
#'
|
|
66 |
#' data(miniACC, package = "MultiAssayExperiment")
|
|
67 |
#' datasets <- FilteredData$new(list(iris = iris, mae = miniACC))
|
|
68 |
#' fs <- teal_slices(
|
|
69 |
#' teal_slice(
|
|
70 |
#' dataname = "iris", varname = "Sepal.Length", selected = c(5.1, 6.4),
|
|
71 |
#' keep_na = TRUE, keep_inf = FALSE
|
|
72 |
#' ),
|
|
73 |
#' teal_slice(
|
|
74 |
#' dataname = "iris", varname = "Species", selected = c("setosa", "versicolor"),
|
|
75 |
#' keep_na = FALSE
|
|
76 |
#' ),
|
|
77 |
#' teal_slice(
|
|
78 |
#' dataname = "mae", varname = "years_to_birth", selected = c(30, 50),
|
|
79 |
#' keep_na = TRUE, keep_inf = FALSE
|
|
80 |
#' ),
|
|
81 |
#' teal_slice(dataname = "mae", varname = "vital_status", selected = "1", keep_na = FALSE),
|
|
82 |
#' teal_slice(dataname = "mae", varname = "gender", selected = "female", keep_na = TRUE),
|
|
83 |
#' teal_slice(
|
|
84 |
#' dataname = "mae", varname = "ARRAY_TYPE",
|
|
85 |
#' selected = "", keep_na = TRUE, experiment = "RPPAArray", arg = "subset"
|
|
86 |
#' )
|
|
87 |
#' )
|
|
88 |
#' datasets$set_filter_state(state = fs)
|
|
89 |
#' isolate(datasets$get_filter_state())
|
|
90 |
#'
|
|
91 |
#' @keywords internal
|
|
92 |
#'
|
|
93 |
FilteredData <- R6::R6Class( # nolint |
|
94 |
"FilteredData",
|
|
95 |
# public methods ----
|
|
96 |
public = list( |
|
97 |
#' @description
|
|
98 |
#' Initialize a `FilteredData` object.
|
|
99 |
#' @param data_objects (`named list`)
|
|
100 |
#' List of data objects.
|
|
101 |
#' Names of the list will be used as `dataname` for respective datasets.
|
|
102 |
#' @param join_keys (`join_keys`) optional joining keys, see [`teal.data::join_keys()`].
|
|
103 |
#'
|
|
104 |
initialize = function(data_objects, join_keys = teal.data::join_keys()) { |
|
105 | 65x |
checkmate::assert_list(data_objects, any.missing = FALSE, min.len = 0, names = "unique") |
106 |
# unpack data.object from the nested list
|
|
107 | 65x |
data_objects <- lapply(data_objects, function(dataset) { |
108 | 95x |
if (is.list(dataset) && "dataset" %in% names(dataset)) { |
109 | ! |
dataset$dataset |
110 |
} else { |
|
111 | 95x |
dataset
|
112 |
}
|
|
113 |
}) |
|
114 | ||
115 |
# Note the internals of data_objects are checked in set_dataset
|
|
116 | 65x |
checkmate::assert_class(join_keys, "join_keys") |
117 | 64x |
self$set_join_keys(join_keys) |
118 | 64x |
child_parent <- sapply( |
119 | 64x |
names(data_objects), |
120 | 64x |
function(i) teal.data::parent(join_keys, i), |
121 | 64x |
USE.NAMES = TRUE, |
122 | 64x |
simplify = FALSE |
123 |
)
|
|
124 | 64x |
ordered_datanames <- topological_sort(child_parent) |
125 | 64x |
ordered_datanames <- intersect(ordered_datanames, names(data_objects)) |
126 | ||
127 | 64x |
for (dataname in ordered_datanames) { |
128 | 94x |
ds_object <- data_objects[[dataname]] |
129 | 94x |
self$set_dataset(data = ds_object, dataname = dataname) |
130 |
}
|
|
131 | ||
132 | 64x |
self$set_available_teal_slices(x = reactive(NULL)) |
133 | ||
134 | 64x |
invisible(self) |
135 |
},
|
|
136 | ||
137 |
#' @description
|
|
138 |
#' Gets `datanames`.
|
|
139 |
#' @details
|
|
140 |
#' The `datanames` are returned in the order in which they must be evaluated (in case of dependencies).
|
|
141 |
#' @return Character vector.
|
|
142 |
datanames = function() { |
|
143 | 115x |
names(private$filtered_datasets) |
144 |
},
|
|
145 | ||
146 |
#' @description
|
|
147 |
#' Gets data label for the dataset.
|
|
148 |
#' Useful to display in `Show R Code`.
|
|
149 |
#'
|
|
150 |
#' @param dataname (`character(1)`) name of the dataset
|
|
151 |
#' @return Character string.
|
|
152 |
get_datalabel = function(dataname) { |
|
153 | 1x |
private$get_filtered_dataset(dataname)$get_dataset_label() |
154 |
},
|
|
155 | ||
156 |
#' @description
|
|
157 |
#' Set list of external filter states available for activation.
|
|
158 |
#' @details
|
|
159 |
#' Unlike adding new filter from the column, these filters can come with some prespecified settings.
|
|
160 |
#' `teal_slices` are wrapped in a `reactive` so they can be updated from elsewhere in the app.
|
|
161 |
#' Filters passed in `x` are limited to those that can be set for this `FilteredData` object,
|
|
162 |
#' i.e. they have the correct `dataname` and `varname` (waived `teal_slice_fixed` as they do not have `varname`).
|
|
163 |
#' List is accessible in `ui/srv_active` through `ui/srv_available_filters`.
|
|
164 |
#' @param x (`reactive`)
|
|
165 |
#' should return `teal_slices`
|
|
166 |
#' @return `NULL`, invisibly.
|
|
167 |
set_available_teal_slices = function(x) { |
|
168 | 65x |
checkmate::assert_class(x, "reactive") |
169 | 65x |
private$available_teal_slices <- reactive({ |
170 |
# Available filters should be limited to the ones relevant for this FilteredData.
|
|
171 | 4x |
current_state <- isolate(self$get_filter_state()) |
172 | 4x |
allowed <- attr(current_state, "include_varnames") |
173 | 4x |
forbidden <- attr(current_state, "exclude_varnames") |
174 | 4x |
foo <- function(slice) { |
175 | 13x |
if (slice$dataname %in% self$datanames()) { |
176 | 13x |
if (slice$fixed) { |
177 | 4x |
TRUE
|
178 |
} else { |
|
179 | 9x |
isTRUE(slice$varname %in% allowed[[slice$dataname]]) || |
180 | 9x |
isFALSE(slice$varname %in% forbidden[[slice$dataname]]) |
181 |
}
|
|
182 |
} else { |
|
183 | ! |
FALSE
|
184 |
}
|
|
185 |
}
|
|
186 | 4x |
Filter(foo, x()) |
187 |
}) |
|
188 | 65x |
invisible(NULL) |
189 |
},
|
|
190 | ||
191 |
#' @description
|
|
192 |
#' Get list of filter states available for this object.
|
|
193 |
#' @details
|
|
194 |
#' All `teal_slice` objects that have been created since the beginning of the app session
|
|
195 |
#' are stored in one `teal_slices` object. This returns a subset of that `teal_slices`,
|
|
196 |
#' describing filter states that can be set for this object.
|
|
197 |
#' @return `reactive` that returns `teal_slices`.
|
|
198 |
get_available_teal_slices = function() { |
|
199 | 4x |
private$available_teal_slices |
200 |
},
|
|
201 | ||
202 |
# datasets methods ----
|
|
203 | ||
204 |
#' @description
|
|
205 |
#' Gets a `call` to filter the dataset according to the filter state.
|
|
206 |
#' @details
|
|
207 |
#' It returns a `call` to filter the dataset only, assuming the
|
|
208 |
#' other (filtered) datasets it depends on are available.
|
|
209 |
#'
|
|
210 |
#' Together with `self$datanames()` which returns the datasets in the correct
|
|
211 |
#' evaluation order, this generates the whole filter code, see the function
|
|
212 |
#' `FilteredData$get_filter_code`.
|
|
213 |
#'
|
|
214 |
#' For the return type, note that `rlang::is_expression` returns `TRUE` on the
|
|
215 |
#' return type, both for base `R` expressions and calls (single expression,
|
|
216 |
#' capturing a function call).
|
|
217 |
#'
|
|
218 |
#' The filtered dataset has the name given by `self$filtered_dataname(dataname)`
|
|
219 |
#'
|
|
220 |
#' This can be used for the `Show R Code` generation.
|
|
221 |
#'
|
|
222 |
#' @param dataname (`character(1)`) name of the dataset
|
|
223 |
#'
|
|
224 |
#' @return A list of `call`s.
|
|
225 |
#'
|
|
226 |
get_call = function(dataname) { |
|
227 | 10x |
checkmate::assert_subset(dataname, self$datanames()) |
228 | 9x |
private$get_filtered_dataset(dataname)$get_call() |
229 |
},
|
|
230 | ||
231 |
#' @description
|
|
232 |
#' Gets filtered or unfiltered dataset.
|
|
233 |
#'
|
|
234 |
#' For `filtered = FALSE`, the original data set with `set_data` is returned including all attributes.
|
|
235 |
#'
|
|
236 |
#' @param dataname (`character(1)`) name of the dataset.
|
|
237 |
#' @param filtered (`logical(1)`) whether to return a filtered or unfiltered dataset.
|
|
238 |
#'
|
|
239 |
#' @return A data object, a `data.frame` or a `MultiAssayExperiment`.
|
|
240 |
#'
|
|
241 |
get_data = function(dataname, filtered = TRUE) { |
|
242 | 24x |
checkmate::assert_subset(dataname, self$datanames()) |
243 | 23x |
checkmate::assert_flag(filtered) |
244 | 22x |
data <- private$get_filtered_dataset(dataname)$get_dataset(filtered) |
245 | 3x |
if (filtered) data() else data |
246 |
},
|
|
247 | ||
248 |
#' @description
|
|
249 |
#' Get join keys between two datasets.
|
|
250 |
#'
|
|
251 |
#' @return `join_keys`
|
|
252 |
#'
|
|
253 |
get_join_keys = function() { |
|
254 | 2x |
private$join_keys |
255 |
},
|
|
256 | ||
257 |
#' @description
|
|
258 |
#' Creates filter overview table to be displayed in the application.
|
|
259 |
#' One row is created per dataset, according to the `get_filter_overview` methods
|
|
260 |
#' of the contained `FilteredDataset` objects.
|
|
261 |
#'
|
|
262 |
#' @param datanames (`character`) vector of dataset names.
|
|
263 |
#'
|
|
264 |
#' @return A `data.frame` listing the numbers of observations in all datasets.
|
|
265 |
#'
|
|
266 |
get_filter_overview = function(datanames) { |
|
267 | 9x |
rows <- lapply( |
268 | 9x |
datanames,
|
269 | 9x |
function(dataname) { |
270 | 11x |
private$get_filtered_dataset(dataname)$get_filter_overview() |
271 |
}
|
|
272 |
)
|
|
273 | 5x |
unssuported_idx <- vapply(rows, function(x) all(is.na(x[-1])), logical(1)) |
274 | 5x |
dplyr::bind_rows(c(rows[!unssuported_idx], rows[unssuported_idx])) |
275 |
},
|
|
276 | ||
277 |
#' @description
|
|
278 |
#' Get keys for the dataset.
|
|
279 |
#'
|
|
280 |
#' @param dataname (`character(1)`) name of the dataset.
|
|
281 |
#'
|
|
282 |
#' @return Character vector of key column names.
|
|
283 |
#'
|
|
284 |
get_keys = function(dataname) { |
|
285 | 1x |
private$get_filtered_dataset(dataname)$get_keys() |
286 |
},
|
|
287 | ||
288 |
#' @description
|
|
289 |
#' Adds a dataset to this `FilteredData`.
|
|
290 |
#'
|
|
291 |
#' @details
|
|
292 |
#' `set_dataset` creates a `FilteredDataset` object which keeps `dataset` for the filtering purpose.
|
|
293 |
#' If this data has a parent specified in the `join_keys` object stored in `private$join_keys`
|
|
294 |
#' then created `FilteredDataset` (child) gets linked with other `FilteredDataset` (parent).
|
|
295 |
#' "Child" dataset return filtered data then dependent on the reactive filtered data of the
|
|
296 |
#' "parent". See more in documentation of `parent` argument in `DataframeFilteredDataset` constructor.
|
|
297 |
#'
|
|
298 |
#' @param data (`data.frame` or `MultiAssayExperiment`)
|
|
299 |
#' data to be filtered.
|
|
300 |
#'
|
|
301 |
#' @param dataname (`character(1)`)
|
|
302 |
#' the name of the `dataset` to be added to this object.
|
|
303 |
#'
|
|
304 |
#' @return `self`, invisibly.
|
|
305 |
#'
|
|
306 |
set_dataset = function(data, dataname) { |
|
307 | 99x |
checkmate::assert_string(dataname) |
308 | 99x |
logger::log_debug("FilteredData$set_dataset setting dataset, name: { dataname }") |
309 | ||
310 | 99x |
parent_dataname <- teal.data::parent(private$join_keys, dataname) |
311 | 99x |
keys <- private$join_keys[dataname, dataname] |
312 | 98x |
if (is.null(keys)) keys <- character(0) |
313 | ||
314 | 99x |
if (length(parent_dataname) == 0) { |
315 | 89x |
private$filtered_datasets[[dataname]] <- init_filtered_dataset( |
316 | 89x |
dataset = data, |
317 | 89x |
dataname = dataname, |
318 | 89x |
keys = keys |
319 |
)
|
|
320 |
} else { |
|
321 | 10x |
join_keys <- private$join_keys[dataname, parent_dataname] |
322 | ! |
if (is.null(join_keys)) join_keys <- character(0) |
323 | 10x |
private$filtered_datasets[[dataname]] <- init_filtered_dataset( |
324 | 10x |
dataset = data, |
325 | 10x |
dataname = dataname, |
326 | 10x |
keys = keys, |
327 | 10x |
parent_name = parent_dataname, |
328 | 10x |
parent = reactive(self$get_data(parent_dataname, filtered = TRUE)), |
329 | 10x |
join_keys = join_keys |
330 |
)
|
|
331 |
}
|
|
332 | ||
333 | 99x |
invisible(self) |
334 |
},
|
|
335 | ||
336 |
#' @description
|
|
337 |
#' Set the `join_keys`.
|
|
338 |
#'
|
|
339 |
#' @param join_keys (`join_keys`), see [`teal.data::join_keys()`].
|
|
340 |
#'
|
|
341 |
#' @return `self`, invisibly.
|
|
342 |
#'
|
|
343 |
set_join_keys = function(join_keys) { |
|
344 | 64x |
checkmate::assert_class(join_keys, "join_keys") |
345 | 64x |
private$join_keys <- join_keys |
346 | 64x |
invisible(self) |
347 |
},
|
|
348 | ||
349 |
# Functions useful for restoring from another dataset ----
|
|
350 | ||
351 |
#' @description
|
|
352 |
#' Gets states of all contained `FilterState` objects.
|
|
353 |
#'
|
|
354 |
#' @return A `teal_slices` object.
|
|
355 |
#'
|
|
356 |
get_filter_state = function() { |
|
357 | 46x |
states <- unname(lapply(private$filtered_datasets, function(x) x$get_filter_state())) |
358 | 46x |
slices <- Filter(Negate(is.null), states) |
359 | 46x |
slices <- do.call(c, slices) |
360 | 46x |
if (!is.null(slices)) { |
361 | 46x |
attr(slices, "allow_add") <- private$allow_add |
362 |
}
|
|
363 | 46x |
slices
|
364 |
},
|
|
365 | ||
366 |
#' @description
|
|
367 |
#' Returns a formatted string representing this `FilteredData` object.
|
|
368 |
#'
|
|
369 |
#' @param show_all (`logical(1)`) passed to `format.teal_slice`.
|
|
370 |
#' @param trim_lines (`logical(1)`) passed to `format.teal_slice`.
|
|
371 |
#'
|
|
372 |
#' @return `character(1)` the formatted string.
|
|
373 |
#'
|
|
374 |
format = function(show_all = FALSE, trim_lines = TRUE) { |
|
375 | 5x |
datasets <- lapply(self$datanames(), private$get_filtered_dataset) |
376 | 5x |
ind <- vapply(datasets, inherits, logical(1L), what = "DefaultFilteredDataset") |
377 | 5x |
states <- do.call(c, lapply(datasets[!ind], function(ds) ds$get_filter_state())) |
378 | 5x |
states_fmt <- format(states, show_all = show_all, trim_lines = trim_lines) |
379 | 5x |
holders_fmt <- vapply(datasets[ind], format, character(1L), show_all = show_all, trim_lines = trim_lines) |
380 | ||
381 | 5x |
sprintf( |
382 | 5x |
"%s:\n%s",
|
383 | 5x |
class(self)[1], |
384 | 5x |
paste(c(states_fmt, holders_fmt), collapse = "\n") |
385 |
)
|
|
386 |
},
|
|
387 | ||
388 |
#' @description
|
|
389 |
#' Prints this `FilteredData` object.
|
|
390 |
#'
|
|
391 |
#' @param ... additional arguments passed to `format`.
|
|
392 |
#'
|
|
393 |
print = function(...) { |
|
394 | ! |
cat(isolate(self$format(...)), "\n") |
395 |
},
|
|
396 | ||
397 |
#' @description
|
|
398 |
#' Sets active filter states.
|
|
399 |
#'
|
|
400 |
#' @param state (`teal_slices`)
|
|
401 |
#'
|
|
402 |
#' @return `NULL`, invisibly.
|
|
403 |
set_filter_state = function(state) { |
|
404 | 29x |
isolate({ |
405 | 29x |
logger::log_debug("{ class(self)[1] }$set_filter_state initializing") |
406 | 29x |
checkmate::assert_class(state, "teal_slices") |
407 | 29x |
allow_add <- attr(state, "allow_add") |
408 | 29x |
if (!is.null(allow_add)) { |
409 | 29x |
private$allow_add <- allow_add |
410 |
}
|
|
411 | ||
412 | 29x |
lapply(self$datanames(), function(dataname) { |
413 | 57x |
states <- Filter(function(x) identical(x$dataname, dataname), state) |
414 | 57x |
private$get_filtered_dataset(dataname)$set_filter_state(states) |
415 |
}) |
|
416 | ||
417 | 29x |
invisible(NULL) |
418 |
}) |
|
419 | ||
420 | 29x |
invisible(NULL) |
421 |
},
|
|
422 | ||
423 |
#' @description
|
|
424 |
#' Removes one or more `FilterState` from a `FilteredData` object.
|
|
425 |
#'
|
|
426 |
#' @param state (`teal_slices`)
|
|
427 |
#' specifying `FilterState` objects to remove;
|
|
428 |
#' `teal_slice`s may contain only `dataname` and `varname`, other elements are ignored.
|
|
429 |
#'
|
|
430 |
#' @return `NULL`, invisibly.
|
|
431 |
#'
|
|
432 |
remove_filter_state = function(state) { |
|
433 | 8x |
isolate({ |
434 | 8x |
checkmate::assert_class(state, "teal_slices") |
435 | 8x |
state_ids <- unique(vapply(state, "[[", character(1L), "id")) |
436 | 8x |
logger::log_debug("{ class(self)[1] }$remove_filter_state removing filter(s), dataname: { state_ids }") |
437 | 8x |
datanames <- unique(vapply(state, "[[", character(1L), "dataname")) |
438 | 8x |
checkmate::assert_subset(datanames, self$datanames()) |
439 | 8x |
lapply(datanames, function(dataname) { |
440 | 9x |
slices <- Filter(function(x) identical(x$dataname, dataname), state) |
441 | 9x |
private$get_filtered_dataset(dataname)$remove_filter_state(slices) |
442 |
}) |
|
443 |
}) |
|
444 | ||
445 | 8x |
invisible(NULL) |
446 |
},
|
|
447 | ||
448 |
#' @description
|
|
449 |
#' Remove all `FilterStates` of a `FilteredDataset` or all `FilterStates` of a `FilteredData` object.
|
|
450 |
#'
|
|
451 |
#' @param datanames (`character`)
|
|
452 |
#' names of datasets for which to remove all filter states.
|
|
453 |
#' Defaults to all datasets in this `FilteredData` object.
|
|
454 |
#' @param force (`logical(1)`)
|
|
455 |
#' flag specifying whether to include anchored filter states.
|
|
456 |
#'
|
|
457 |
#' @return `NULL`, invisibly.
|
|
458 |
#'
|
|
459 |
clear_filter_states = function(datanames = self$datanames(), force = FALSE) { |
|
460 | 7x |
logger::log_debug("FilteredData$clear_filter_states called, datanames: { toString(datanames) }") |
461 | 7x |
for (dataname in datanames) { |
462 | 12x |
fdataset <- private$get_filtered_dataset(dataname = dataname) |
463 | 12x |
fdataset$clear_filter_states(force) |
464 |
}
|
|
465 | 7x |
invisible(NULL) |
466 |
},
|
|
467 | ||
468 | ||
469 |
# shiny modules -----
|
|
470 | ||
471 |
#' @description
|
|
472 |
#' top-level `shiny` module for the filter panel in the `teal` app.
|
|
473 |
#' Contains 1) filter overview panel, 2) filter active panel, and 3) add filters panel.
|
|
474 |
#'
|
|
475 |
#' @param id (`character(1)`)
|
|
476 |
#' `shiny` module instance id.
|
|
477 |
#' @param active_datanames (`reactive`)
|
|
478 |
#' defining subset of `self$datanames()` to be displayed.
|
|
479 |
#' @return `shiny.tag`
|
|
480 |
ui_filter_panel = function(id, active_datanames = self$datanames) { |
|
481 | ! |
ns <- NS(id) |
482 | ! |
bslib::page_fluid( |
483 | ! |
id = ns(NULL), # used for hiding / showing |
484 | ! |
class = "teal-slice filter-panel", |
485 | ! |
include_css_files(pattern = "filter-panel"), |
486 | ! |
include_js_files(pattern = "togglePanelItems"), |
487 | ! |
shinyjs::useShinyjs(), |
488 | ! |
self$ui_overview(ns("overview")), |
489 | ! |
self$ui_active(ns("active"), active_datanames = active_datanames) |
490 |
)
|
|
491 |
},
|
|
492 | ||
493 |
#' @description
|
|
494 |
#' Server function for filter panel.
|
|
495 |
#'
|
|
496 |
#' @param id (`character(1)`)
|
|
497 |
#' `shiny` module instance id.
|
|
498 |
#' @param active_datanames (`function` or `reactive`)
|
|
499 |
#' returning `datanames` that should be shown on the filter panel.
|
|
500 |
#' Must be a subset of the `datanames` in this `FilteredData`.
|
|
501 |
#' If the function returns `NULL` (as opposed to `character(0)`),
|
|
502 |
#' the filter panel will be hidden.
|
|
503 |
#' @return `NULL`.
|
|
504 |
srv_filter_panel = function(id, active_datanames = self$datanames) { |
|
505 | 1x |
checkmate::assert_function(active_datanames) |
506 | 1x |
moduleServer(id = id, function(input, output, session) { |
507 | 1x |
logger::log_debug("FilteredData$srv_filter_panel initializing") |
508 | ||
509 | 1x |
active_datanames_resolved <- reactive({ |
510 | 1x |
checkmate::assert_subset(active_datanames(), self$datanames()) |
511 | ! |
active_datanames() |
512 |
}) |
|
513 | ||
514 | 1x |
self$srv_overview("overview", active_datanames_resolved) |
515 | 1x |
self$srv_active("active", active_datanames_resolved) |
516 | ||
517 | 1x |
NULL
|
518 |
}) |
|
519 |
},
|
|
520 | ||
521 |
#' @description
|
|
522 |
#' Server module responsible for displaying active filters.
|
|
523 |
#' @param id (`character(1)`)
|
|
524 |
#' `shiny` module instance id.
|
|
525 |
#' @param active_datanames (`reactive`)
|
|
526 |
#' defining subset of `self$datanames()` to be displayed.
|
|
527 |
#' @return `shiny.tag`
|
|
528 |
ui_active = function(id, active_datanames = self$datanames) { |
|
529 | ! |
ns <- NS(id) |
530 | ! |
tags$div( |
531 | ! |
id = id, # not used, can be used to customize CSS behavior |
532 | ! |
include_js_files(pattern = "togglePanelItems"), |
533 | ! |
class = "teal-slice", |
534 | ! |
bslib::accordion( |
535 | ! |
id = ns("main_filter_accordian"), |
536 | ! |
bslib::accordion_panel( |
537 | ! |
"Filter Data",
|
538 | ! |
tags$div( |
539 | ! |
div( |
540 | ! |
id = ns("additional_filter_helpers"), |
541 | ! |
class = "teal-slice available-filters", |
542 | ! |
private$ui_available_filters(ns("available_filters")), |
543 | ! |
uiOutput(ns("remove_all_filters_ui")) |
544 |
),
|
|
545 | ! |
tags$div( |
546 | ! |
id = ns("filter_active_vars_contents"), |
547 | ! |
tagList( |
548 | ! |
lapply( |
549 | ! |
isolate(active_datanames()), |
550 | ! |
function(dataname) { |
551 | ! |
fdataset <- private$get_filtered_dataset(dataname) |
552 | ! |
fdataset$ui_active(id = ns(dataname), allow_add = private$allow_add) |
553 |
}
|
|
554 |
)
|
|
555 |
)
|
|
556 |
),
|
|
557 | ! |
tags$div( |
558 | ! |
id = ns("filters_active_count"), |
559 | ! |
style = "display: none;", |
560 | ! |
textOutput(ns("teal_filters_count")) |
561 |
)
|
|
562 |
)
|
|
563 |
)
|
|
564 |
),
|
|
565 | ! |
tags$script( |
566 | ! |
HTML( |
567 | ! |
sprintf( |
568 |
" |
|
569 | ! |
$(document).ready(function() { |
570 | ! |
$('#%s').appendTo('#%s > .accordion-item > .accordion-header'); |
571 | ! |
$('#%s > .accordion-item > .accordion-header').css({ |
572 | ! |
'display': 'flex' |
573 |
}); |
|
574 | ! |
$('#%s i').css({ |
575 | ! |
'color': 'var(--bs-accordion-color)', |
576 | ! |
'font-size': '1rem' |
577 |
}); |
|
578 |
}); |
|
579 |
", |
|
580 | ! |
ns("additional_filter_helpers"), |
581 | ! |
ns("main_filter_accordian"), |
582 | ! |
ns("main_filter_accordian"), |
583 | ! |
ns("additional_filter_helpers") |
584 |
)
|
|
585 |
)
|
|
586 |
)
|
|
587 |
)
|
|
588 |
},
|
|
589 | ||
590 |
#' @description
|
|
591 |
#' Server module responsible for displaying active filters.
|
|
592 |
#' @param id (`character(1)`)
|
|
593 |
#' `shiny` module instance id.
|
|
594 |
#' @param active_datanames (`reactive`)
|
|
595 |
#' defining subset of `self$datanames()` to be displayed.
|
|
596 |
#' @return `NULL`.
|
|
597 |
srv_active = function(id, active_datanames = self$datanames) { |
|
598 | 3x |
checkmate::assert_function(active_datanames) |
599 | 3x |
moduleServer(id, function(input, output, session) { |
600 | 3x |
logger::log_debug("FilteredData$srv_active initializing") |
601 | ||
602 | 3x |
private$srv_available_filters("available_filters") |
603 | ||
604 | 3x |
private$session_bindings[[session$ns("minimise_filter_active")]] <- observeEvent( |
605 | 3x |
eventExpr = input$minimise_filter_active, |
606 | 3x |
handlerExpr = { |
607 | ! |
shinyjs::toggle("filter_active_vars_contents") |
608 | ! |
shinyjs::toggle("filters_active_count") |
609 | ! |
toggle_icon(session$ns("minimise_filter_active"), c("fa-angle-right", "fa-angle-down")) |
610 | ! |
toggle_title(session$ns("minimise_filter_active"), c("Restore panel", "Minimise Panel")) |
611 |
}
|
|
612 |
)
|
|
613 | ||
614 | 3x |
filter_count <- reactive({ |
615 | 3x |
length(self$get_filter_state()) |
616 |
}) |
|
617 | ||
618 | 3x |
is_filter_removable <- reactive({ |
619 | 3x |
non_anchored <- Filter(function(x) !x$anchored, self$get_filter_state()) |
620 | 3x |
isTRUE(length(non_anchored) > 0) |
621 |
}) |
|
622 | ||
623 | 3x |
output$remove_all_filters_ui <- renderUI({ |
624 | 3x |
req(is_filter_removable()) |
625 | 2x |
tags$div( |
626 | 2x |
style = "display: flex;", |
627 | 2x |
actionLink( |
628 | 2x |
inputId = session$ns("remove_all_filters"), |
629 | 2x |
label = "", |
630 | 2x |
title = "Remove active filters", |
631 | 2x |
icon = icon("far fa-circle-xmark"), |
632 | 2x |
class = "teal-slice filter-icon remove-all" |
633 |
)
|
|
634 |
)
|
|
635 |
}) |
|
636 | ||
637 | 3x |
private$session_bindings[[session$ns("is_filter_removable")]] <- observeEvent( |
638 | 3x |
eventExpr = is_filter_removable(), |
639 | 3x |
handlerExpr = { |
640 | 3x |
shinyjs::toggle("remove_all_filters", condition = is_filter_removable()) |
641 | 3x |
shinyjs::show("filter_active_vars_contents") |
642 | 3x |
shinyjs::hide("filters_active_count") |
643 | 3x |
toggle_icon(session$ns("minimise_filter_active"), c("fa-angle-right", "fa-angle-down"), TRUE) |
644 | 3x |
toggle_title(session$ns("minimise_filter_active"), c("Restore panel", "Minimise Panel"), TRUE) |
645 |
}
|
|
646 |
)
|
|
647 | ||
648 | 3x |
private$session_bindings[[session$ns("active_datanames")]] <- observeEvent( |
649 | 3x |
eventExpr = active_datanames(), |
650 | 3x |
handlerExpr = lapply(self$datanames(), function(dataname) { |
651 | 4x |
if (dataname %in% active_datanames()) { |
652 | 4x |
shinyjs::show(dataname) |
653 |
} else { |
|
654 | ! |
shinyjs::hide(dataname) |
655 |
}
|
|
656 |
}) |
|
657 |
)
|
|
658 | ||
659 |
# should not use for-loop as variables are otherwise only bound by reference
|
|
660 |
# and last dataname would be used
|
|
661 | 3x |
lapply( |
662 | 3x |
self$datanames(), |
663 | 3x |
function(dataname) { |
664 | 6x |
fdataset <- private$get_filtered_dataset(dataname) |
665 | 6x |
fdataset$srv_active(id = dataname) |
666 |
}
|
|
667 |
)
|
|
668 | ||
669 | 3x |
output$teal_filters_count <- renderText({ |
670 | 3x |
n_filters_active <- filter_count() |
671 | 3x |
req(n_filters_active > 0L) |
672 | 2x |
sprintf( |
673 | 2x |
"%s filter%s applied across datasets",
|
674 | 2x |
n_filters_active,
|
675 | 2x |
ifelse(n_filters_active == 1, "", "s") |
676 |
)
|
|
677 |
}) |
|
678 | ||
679 | 3x |
private$session_bindings[[session$ns("remove_all_filters")]] <- observeEvent( |
680 | 3x |
eventExpr = input$remove_all_filters, |
681 | 3x |
handlerExpr = { |
682 | 1x |
logger::log_debug("FilteredData$srv_filter_panel@1 removing all non-anchored filters") |
683 | 1x |
self$clear_filter_states() |
684 |
}
|
|
685 |
)
|
|
686 | ||
687 | 3x |
private$session_bindings[[session$ns("inputs")]] <- list( |
688 | 3x |
destroy = function() { |
689 | 2x |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
690 |
}
|
|
691 |
)
|
|
692 | ||
693 | 3x |
NULL
|
694 |
}) |
|
695 |
},
|
|
696 | ||
697 |
#' @description
|
|
698 |
#' Creates the UI definition for the module showing counts for each dataset
|
|
699 |
#' contrasting the filtered to the full unfiltered dataset.
|
|
700 |
#'
|
|
701 |
#' Per dataset, it displays
|
|
702 |
#' the number of rows/observations in each dataset,
|
|
703 |
#' the number of unique subjects.
|
|
704 |
#'
|
|
705 |
#' @param id (`character(1)`)
|
|
706 |
#' `shiny` module instance id.
|
|
707 |
#'
|
|
708 |
ui_overview = function(id) { |
|
709 | ! |
ns <- NS(id) |
710 | ! |
tags$div( |
711 | ! |
class = "teal-slice", |
712 | ! |
bslib::accordion( |
713 | ! |
id = ns("main_filter_accordian"), |
714 | ! |
bslib::accordion_panel( |
715 | ! |
title = "Active Filter Summary", |
716 | ! |
tags$div( |
717 | ! |
id = ns("filters_overview_contents"), |
718 | ! |
tags$div( |
719 | ! |
class = "teal_active_summary_filter_panel", |
720 | ! |
tableOutput(ns("table")) |
721 |
)
|
|
722 |
)
|
|
723 |
)
|
|
724 |
)
|
|
725 |
)
|
|
726 |
},
|
|
727 | ||
728 |
#' @description
|
|
729 |
#' Server function to display the number of records in the filtered and unfiltered
|
|
730 |
#' data.
|
|
731 |
#'
|
|
732 |
#' @param id (`character(1)`)
|
|
733 |
#' `shiny` module instance id.
|
|
734 |
#' @param active_datanames (`reactive`)
|
|
735 |
#' returning `datanames` that should be shown on the filter panel,
|
|
736 |
#' must be a subset of the `datanames` argument provided to `ui_filter_panel`;
|
|
737 |
#' if the function returns `NULL` (as opposed to `character(0)`), the filter
|
|
738 |
#' panel will be hidden.
|
|
739 |
#' @return `NULL`.
|
|
740 |
srv_overview = function(id, active_datanames = self$datanames) { |
|
741 | 1x |
checkmate::assert_class(active_datanames, "reactive") |
742 | 1x |
moduleServer( |
743 | 1x |
id = id, |
744 | 1x |
function(input, output, session) { |
745 | 1x |
logger::log_debug("FilteredData$srv_filter_overview initializing") |
746 | ||
747 | 1x |
output$table <- renderUI({ |
748 | ! |
logger::log_debug("FilteredData$srv_filter_overview@1 updating counts") |
749 | ! |
if (length(active_datanames()) == 0) { |
750 | ! |
return(NULL) |
751 |
}
|
|
752 | ||
753 | ! |
datasets_df <- self$get_filter_overview(datanames = active_datanames()) |
754 | ||
755 | ! |
attr(datasets_df$dataname, "label") <- "Data Name" |
756 | ||
757 | ! |
if (!is.null(datasets_df$obs)) { |
758 |
# some datasets (MAE colData) doesn't return obs column
|
|
759 | ! |
datasets_df <- transform( |
760 | ! |
datasets_df,
|
761 | ! |
obs_str_summary = ifelse( |
762 | ! |
!is.na(obs), |
763 | ! |
sprintf("%s/%s", obs_filtered, obs), |
764 |
""
|
|
765 |
)
|
|
766 |
)
|
|
767 | ! |
attr(datasets_df$obs_str_summary, "label") <- "Obs" |
768 |
}
|
|
769 | ||
770 | ||
771 | ! |
if (!is.null(datasets_df$subjects)) { |
772 |
# some datasets (without keys) doesn't return subjects
|
|
773 | ! |
datasets_df <- transform( |
774 | ! |
datasets_df,
|
775 | ! |
subjects_summary = ifelse( |
776 | ! |
!is.na(subjects), |
777 | ! |
sprintf("%s/%s", subjects_filtered, subjects), |
778 |
""
|
|
779 |
)
|
|
780 |
)
|
|
781 | ! |
attr(datasets_df$subjects_summary, "label") <- "Subjects" |
782 |
}
|
|
783 | ||
784 | ! |
all_names <- c("dataname", "obs_str_summary", "subjects_summary") |
785 | ! |
datasets_df <- datasets_df[, colnames(datasets_df) %in% all_names] |
786 | ||
787 | ! |
body_html <- apply( |
788 | ! |
datasets_df,
|
789 | ! |
1,
|
790 | ! |
function(x) { |
791 | ! |
tags$tr( |
792 | ! |
tagList( |
793 | ! |
tags$td( |
794 | ! |
if (all(x[-1] == "")) { |
795 | ! |
icon( |
796 | ! |
name = "exclamation-triangle", |
797 | ! |
title = "Unsupported dataset", |
798 | ! |
`data-container` = "body", |
799 | ! |
`data-toggle` = "popover", |
800 | ! |
`data-content` = "object not supported by the filter panel" |
801 |
)
|
|
802 |
},
|
|
803 | ! |
x[1] |
804 |
),
|
|
805 | ! |
lapply(x[-1], tags$td) |
806 |
)
|
|
807 |
)
|
|
808 |
}
|
|
809 |
)
|
|
810 | ||
811 | ! |
header_labels <- vapply( |
812 | ! |
seq_along(datasets_df), |
813 | ! |
function(i) { |
814 | ! |
label <- attr(datasets_df[[i]], "label") |
815 | ! |
ifelse(!is.null(label), label, names(datasets_df)[[i]]) |
816 |
},
|
|
817 | ! |
character(1) |
818 |
)
|
|
819 | ! |
header_html <- tags$tr(tagList(lapply(header_labels, tags$td))) |
820 | ||
821 | ! |
table_html <- tags$table( |
822 | ! |
class = "table custom-table", |
823 | ! |
tags$thead(header_html), |
824 | ! |
tags$tbody(body_html) |
825 |
)
|
|
826 | ! |
table_html
|
827 |
}) |
|
828 | ||
829 | 1x |
NULL
|
830 |
}
|
|
831 |
)
|
|
832 |
},
|
|
833 | ||
834 |
#' @description
|
|
835 |
#' Object and dependencies cleanup.
|
|
836 |
#'
|
|
837 |
#' - Destroy inputs and observers stored in `private$session_bindings`
|
|
838 |
#' - Finalize `FilteredData` stored in `private$filtered_datasets`
|
|
839 |
#'
|
|
840 |
#' @return `NULL`, invisibly.
|
|
841 |
finalize = function() { |
|
842 | 64x |
.finalize_session_bindings(self, private) |
843 | 64x |
lapply(private$filtered_datasets, function(x) x$finalize()) |
844 | 64x |
invisible(NULL) |
845 |
}
|
|
846 |
),
|
|
847 | ||
848 |
# private members ----
|
|
849 |
private = list( |
|
850 |
# selectively hide / show to only show `active_datanames` out of all datanames
|
|
851 | ||
852 |
# private attributes ----
|
|
853 |
filtered_datasets = list(), |
|
854 | ||
855 |
# activate/deactivate filter panel
|
|
856 |
filter_panel_active = TRUE, |
|
857 | ||
858 |
# `reactive` containing teal_slices that can be selected; only active in module-specific mode
|
|
859 |
available_teal_slices = NULL, |
|
860 | ||
861 |
# keys used for joining/filtering data a join_keys object (see teal.data)
|
|
862 |
join_keys = NULL, |
|
863 | ||
864 |
# flag specifying whether the user may add filters
|
|
865 |
allow_add = TRUE, |
|
866 | ||
867 |
# observers and inputs list
|
|
868 |
session_bindings = list(), |
|
869 | ||
870 |
# private methods ----
|
|
871 | ||
872 |
# @description
|
|
873 |
# Gets `FilteredDataset` object which contains all information
|
|
874 |
# pertaining to the specified dataset.
|
|
875 |
#
|
|
876 |
# @param dataname (`character(1)`)
|
|
877 |
# name of the dataset
|
|
878 |
#
|
|
879 |
# @return `FilteredDataset` object or list of `FilteredDataset`s
|
|
880 |
#
|
|
881 |
get_filtered_dataset = function(dataname = character(0)) { |
|
882 | 139x |
if (length(dataname) == 0) { |
883 | ! |
private$filtered_datasets |
884 |
} else { |
|
885 | 139x |
private$filtered_datasets[[dataname]] |
886 |
}
|
|
887 |
},
|
|
888 | ||
889 |
# @description
|
|
890 |
# Activate available filters.
|
|
891 |
# Module is composed from plus button and dropdown menu. Menu is shown when
|
|
892 |
# the button is clicked. Menu contains available/active filters list
|
|
893 |
# passed via `set_available_teal_slice`.
|
|
894 |
ui_available_filters = function(id) { |
|
895 | ! |
ns <- NS(id) |
896 | ||
897 | ! |
active_slices_id <- isolate(vapply(self$get_filter_state(), `[[`, character(1), "id")) |
898 | ! |
tags$div( |
899 | ! |
id = ns("available_menu"), |
900 | ! |
shinyWidgets::dropMenu( |
901 | ! |
tags$a( |
902 | ! |
id = ns("show"), |
903 | ! |
class = "available-menu", |
904 | ! |
bsicons::bs_icon("plus-square", size = "1.4rem", class = "teal-slice filter-icon"), |
905 |
),
|
|
906 | ! |
tags$div( |
907 | ! |
class = "menu-content", |
908 | ! |
shinycssloaders::withSpinner( |
909 | ! |
uiOutput(ns("checkbox")), |
910 | ! |
type = 4, |
911 | ! |
size = 0.25 |
912 |
)
|
|
913 |
)
|
|
914 |
)
|
|
915 |
)
|
|
916 |
},
|
|
917 |
# @description
|
|
918 |
# Activate available filters. When a filter is selected or removed,
|
|
919 |
# `set_filter_state` or `remove_filter_state` is executed for
|
|
920 |
# the appropriate filter state id.
|
|
921 |
srv_available_filters = function(id) { |
|
922 | 4x |
moduleServer(id, function(input, output, session) { |
923 | 4x |
slices_available <- self$get_available_teal_slices() |
924 | 4x |
slices_interactive <- reactive( |
925 | 4x |
Filter(function(slice) isFALSE(slice$fixed), slices_available()) |
926 |
)
|
|
927 | 4x |
slices_fixed <- reactive( |
928 | 4x |
Filter(function(slice) isTRUE(slice$fixed), slices_available()) |
929 |
)
|
|
930 | 4x |
available_slices_id <- reactive(vapply(slices_available(), `[[`, character(1), "id")) |
931 | 4x |
active_slices_id <- reactive(vapply(self$get_filter_state(), `[[`, character(1), "id")) |
932 | 4x |
duplicated_slice_references <- reactive({ |
933 |
# slice refers to a particular column
|
|
934 | 8x |
slice_reference <- vapply(slices_available(), get_default_slice_id, character(1)) |
935 | 8x |
is_duplicated_reference <- duplicated(slice_reference) | duplicated(slice_reference, fromLast = TRUE) |
936 | 8x |
is_active <- available_slices_id() %in% active_slices_id() |
937 | 8x |
is_not_expr <- !vapply(slices_available(), inherits, logical(1), "teal_slice_expr") |
938 | 8x |
slice_reference[is_duplicated_reference & is_active & is_not_expr] |
939 |
}) |
|
940 | ||
941 | 4x |
checkbox_group_element <- function(name, value, label, checked, disabled = FALSE) { |
942 | 35x |
tags$div( |
943 | 35x |
class = "checkbox available-filters", |
944 | 35x |
tags$label( |
945 | 35x |
tags$input( |
946 | 35x |
type = "checkbox", |
947 | 35x |
name = name, |
948 | 35x |
value = value, |
949 | 35x |
checked = checked, |
950 | 35x |
disabled = if (disabled) "disabled" |
951 |
),
|
|
952 | 35x |
tags$span(label, disabled = if (disabled) disabled) |
953 |
)
|
|
954 |
)
|
|
955 |
}
|
|
956 | ||
957 | 4x |
output$checkbox <- renderUI({ |
958 | 8x |
checkbox <- checkboxGroupInput( |
959 | 8x |
session$ns("available_slices_id"), |
960 | 8x |
label = NULL, |
961 | 8x |
choices = NULL, |
962 | 8x |
selected = NULL |
963 |
)
|
|
964 | 8x |
active_slices_ids <- active_slices_id() |
965 | 8x |
duplicated_slice_refs <- duplicated_slice_references() |
966 | ||
967 | 8x |
checkbox_group_slice <- function(slice) { |
968 |
# we need to isolate changes in the fields of the slice (teal_slice)
|
|
969 | 35x |
isolate({ |
970 | 35x |
checkbox_group_element( |
971 | 35x |
name = session$ns("available_slices_id"), |
972 | 35x |
value = slice$id, |
973 | 35x |
label = slice$id, |
974 | 35x |
checked = if (slice$id %in% active_slices_ids) "checked", |
975 | 35x |
disabled = slice$anchored || |
976 | 35x |
get_default_slice_id(slice) %in% duplicated_slice_refs && |
977 | 35x |
!slice$id %in% active_slices_ids |
978 |
)
|
|
979 |
}) |
|
980 |
}
|
|
981 | ||
982 | 8x |
interactive_choice_mock <- lapply(slices_interactive(), checkbox_group_slice) |
983 | 8x |
non_interactive_choice_mock <- lapply(slices_fixed(), checkbox_group_slice) |
984 | ||
985 | 8x |
htmltools::tagInsertChildren( |
986 | 8x |
checkbox,
|
987 | 8x |
tags$br(), |
988 | 8x |
if (length(non_interactive_choice_mock)) tags$strong("Fixed filters"), |
989 | 8x |
non_interactive_choice_mock,
|
990 | 8x |
if (length(interactive_choice_mock)) tags$strong("Interactive filters"), |
991 | 8x |
interactive_choice_mock,
|
992 | 8x |
.cssSelector = "div.shiny-options-group", |
993 | 8x |
after = 0 |
994 |
)
|
|
995 |
}) |
|
996 | ||
997 | 4x |
private$session_bindings[[session$ns("available_slices_id")]] <- observeEvent( |
998 | 4x |
eventExpr = input$available_slices_id, |
999 | 4x |
ignoreNULL = FALSE, |
1000 | 4x |
ignoreInit = TRUE, |
1001 | 4x |
handlerExpr = { |
1002 | 5x |
new_slices_id <- setdiff(input$available_slices_id, active_slices_id()) |
1003 | 5x |
removed_slices_id <- setdiff(active_slices_id(), input$available_slices_id) |
1004 | 5x |
if (length(new_slices_id)) { |
1005 | 3x |
new_teal_slices <- Filter( |
1006 | 3x |
function(slice) slice$id %in% new_slices_id, |
1007 | 3x |
private$available_teal_slices() |
1008 |
)
|
|
1009 | 3x |
self$set_filter_state(new_teal_slices) |
1010 |
}
|
|
1011 | ||
1012 | 5x |
if (length(removed_slices_id)) { |
1013 | 4x |
removed_teal_slices <- Filter( |
1014 | 4x |
function(slice) slice$id %in% removed_slices_id, |
1015 | 4x |
self$get_filter_state() |
1016 |
)
|
|
1017 | 4x |
self$remove_filter_state(removed_teal_slices) |
1018 |
}
|
|
1019 |
}
|
|
1020 |
)
|
|
1021 | ||
1022 | 4x |
private$session_bindings[[session$ns("available_teal_slices")]] <- observeEvent( |
1023 | 4x |
eventExpr = private$available_teal_slices(), |
1024 | 4x |
ignoreNULL = FALSE, |
1025 | 4x |
handlerExpr = { |
1026 | 3x |
if (length(private$available_teal_slices())) { |
1027 | 1x |
shinyjs::show("available_menu") |
1028 |
} else { |
|
1029 | 2x |
shinyjs::hide("available_menu") |
1030 |
}
|
|
1031 |
}
|
|
1032 |
)
|
|
1033 |
}) |
|
1034 |
}
|
|
1035 |
)
|
|
1036 |
)
|
1 |
# RangeFilterState ------
|
|
2 | ||
3 |
#' @name RangeFilterState
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title `FilterState` object for numeric data
|
|
7 |
#'
|
|
8 |
#' @description Manages choosing a numeric range.
|
|
9 |
#'
|
|
10 |
#' @examples
|
|
11 |
#' # use non-exported function from teal.slice
|
|
12 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice")
|
|
13 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice")
|
|
14 |
#' RangeFilterState <- getFromNamespace("RangeFilterState", "teal.slice")
|
|
15 |
#'
|
|
16 |
#' library(shiny)
|
|
17 |
#'
|
|
18 |
#' filter_state <- RangeFilterState$new(
|
|
19 |
#' x = c(NA, Inf, seq(1:10)),
|
|
20 |
#' slice = teal_slice(varname = "x", dataname = "data")
|
|
21 |
#' )
|
|
22 |
#' isolate(filter_state$get_call())
|
|
23 |
#' filter_state$set_state(
|
|
24 |
#' teal_slice(
|
|
25 |
#' dataname = "data",
|
|
26 |
#' varname = "x",
|
|
27 |
#' selected = c(3L, 8L),
|
|
28 |
#' keep_na = TRUE,
|
|
29 |
#' keep_inf = TRUE
|
|
30 |
#' )
|
|
31 |
#' )
|
|
32 |
#' isolate(filter_state$get_call())
|
|
33 |
#'
|
|
34 |
#' # working filter in an app
|
|
35 |
#' library(shinyjs)
|
|
36 |
#'
|
|
37 |
#' data_range <- c(runif(100, 0, 1), NA, Inf)
|
|
38 |
#' fs <- RangeFilterState$new(
|
|
39 |
#' x = data_range,
|
|
40 |
#' slice = teal_slice(
|
|
41 |
#' dataname = "data",
|
|
42 |
#' varname = "x",
|
|
43 |
#' selected = c(0.15, 0.93),
|
|
44 |
#' keep_na = TRUE,
|
|
45 |
#' keep_inf = TRUE
|
|
46 |
#' )
|
|
47 |
#' )
|
|
48 |
#'
|
|
49 |
#' ui <- fluidPage(
|
|
50 |
#' useShinyjs(),
|
|
51 |
#' include_css_files(pattern = "filter-panel"),
|
|
52 |
#' include_js_files(pattern = "count-bar-labels"),
|
|
53 |
#' column(4, tags$div(
|
|
54 |
#' tags$h4("RangeFilterState"),
|
|
55 |
#' fs$ui("fs")
|
|
56 |
#' )),
|
|
57 |
#' column(4, tags$div(
|
|
58 |
#' id = "outputs", # div id is needed for toggling the element
|
|
59 |
#' tags$h4("Condition (i.e. call)"), # display the condition call generated by this FilterState
|
|
60 |
#' textOutput("condition_range"), tags$br(),
|
|
61 |
#' tags$h4("Unformatted state"), # display raw filter state
|
|
62 |
#' textOutput("unformatted_range"), tags$br(),
|
|
63 |
#' tags$h4("Formatted state"), # display human readable filter state
|
|
64 |
#' textOutput("formatted_range"), tags$br()
|
|
65 |
#' )),
|
|
66 |
#' column(4, tags$div(
|
|
67 |
#' tags$h4("Programmatic filter control"),
|
|
68 |
#' actionButton("button1_range", "set drop NA", width = "100%"), tags$br(),
|
|
69 |
#' actionButton("button2_range", "set keep NA", width = "100%"), tags$br(),
|
|
70 |
#' actionButton("button3_range", "set drop Inf", width = "100%"), tags$br(),
|
|
71 |
#' actionButton("button4_range", "set keep Inf", width = "100%"), tags$br(),
|
|
72 |
#' actionButton("button5_range", "set a range", width = "100%"), tags$br(),
|
|
73 |
#' actionButton("button6_range", "set full range", width = "100%"), tags$br(),
|
|
74 |
#' actionButton("button0_range", "set initial state", width = "100%"), tags$br()
|
|
75 |
#' ))
|
|
76 |
#' )
|
|
77 |
#'
|
|
78 |
#' server <- function(input, output, session) {
|
|
79 |
#' fs$server("fs")
|
|
80 |
#' output$condition_range <- renderPrint(fs$get_call())
|
|
81 |
#' output$formatted_range <- renderText(fs$format())
|
|
82 |
#' output$unformatted_range <- renderPrint(fs$get_state())
|
|
83 |
#' # modify filter state programmatically
|
|
84 |
#' observeEvent(
|
|
85 |
#' input$button1_range,
|
|
86 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = FALSE))
|
|
87 |
#' )
|
|
88 |
#' observeEvent(
|
|
89 |
#' input$button2_range,
|
|
90 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = TRUE))
|
|
91 |
#' )
|
|
92 |
#' observeEvent(
|
|
93 |
#' input$button3_range,
|
|
94 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_inf = FALSE))
|
|
95 |
#' )
|
|
96 |
#' observeEvent(
|
|
97 |
#' input$button4_range,
|
|
98 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_inf = TRUE))
|
|
99 |
#' )
|
|
100 |
#' observeEvent(
|
|
101 |
#' input$button5_range,
|
|
102 |
#' fs$set_state(
|
|
103 |
#' teal_slice(dataname = "data", varname = "x", selected = c(0.2, 0.74))
|
|
104 |
#' )
|
|
105 |
#' )
|
|
106 |
#' observeEvent(
|
|
107 |
#' input$button6_range,
|
|
108 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", selected = c(0, 1)))
|
|
109 |
#' )
|
|
110 |
#' observeEvent(
|
|
111 |
#' input$button0_range,
|
|
112 |
#' fs$set_state(
|
|
113 |
#' teal_slice("data", "variable", selected = c(0.15, 0.93), keep_na = TRUE, keep_inf = TRUE)
|
|
114 |
#' )
|
|
115 |
#' )
|
|
116 |
#' }
|
|
117 |
#'
|
|
118 |
#' if (interactive()) {
|
|
119 |
#' shinyApp(ui, server)
|
|
120 |
#' }
|
|
121 |
#' @keywords internal
|
|
122 |
#'
|
|
123 |
RangeFilterState <- R6::R6Class( # nolint |
|
124 |
"RangeFilterState",
|
|
125 |
inherit = FilterState, |
|
126 | ||
127 |
# public methods ----
|
|
128 |
public = list( |
|
129 | ||
130 |
#' @description
|
|
131 |
#' Initialize a `FilterState` object for range selection.
|
|
132 |
#' @param x (`numeric`)
|
|
133 |
#' variable to be filtered.
|
|
134 |
#' @param x_reactive (`reactive`)
|
|
135 |
#' returning vector of the same type as `x`. Is used to update
|
|
136 |
#' counts following the change in values of the filtered dataset.
|
|
137 |
#' If it is set to `reactive(NULL)` then counts based on filtered
|
|
138 |
#' dataset are not shown.
|
|
139 |
#' @param slice (`teal_slice`)
|
|
140 |
#' specification of this filter state.
|
|
141 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`.
|
|
142 |
#' `get_state` returns `teal_slice` object which can be reused in other places.
|
|
143 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e.
|
|
144 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`.
|
|
145 |
#' @param extract_type (`character`)
|
|
146 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values:
|
|
147 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed
|
|
148 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>`
|
|
149 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]`
|
|
150 |
#'
|
|
151 |
#' @return Object of class `RangeFilterState`, invisibly.
|
|
152 |
#'
|
|
153 |
initialize = function(x, |
|
154 |
x_reactive = reactive(NULL), |
|
155 |
extract_type = character(0), |
|
156 |
slice) { |
|
157 | 118x |
isolate({ |
158 | 118x |
checkmate::assert_numeric(x, all.missing = FALSE) |
159 | 2x |
if (!any(is.finite(x))) stop("\"x\" contains no finite values") |
160 | 115x |
super$initialize(x = x, x_reactive = x_reactive, slice = slice, extract_type = extract_type) |
161 | 115x |
private$is_integer <- checkmate::test_integerish(x) |
162 | 115x |
private$inf_count <- sum(is.infinite(x)) |
163 | 115x |
private$inf_filtered_count <- reactive( |
164 | 115x |
if (!is.null(private$x_reactive())) sum(is.infinite(private$x_reactive())) |
165 |
)
|
|
166 | ||
167 | 115x |
checkmate::assert_numeric(slice$choices, null.ok = TRUE) |
168 | 3x |
if (is.null(slice$keep_inf) && any(is.infinite(x))) slice$keep_inf <- TRUE |
169 | ||
170 | 114x |
private$set_choices(slice$choices) |
171 | 41x |
if (is.null(slice$selected)) slice$selected <- slice$choices |
172 | 114x |
private$set_selected(slice$selected) |
173 | ||
174 | 111x |
private$is_integer <- checkmate::test_integerish(x) |
175 | 111x |
private$inf_filtered_count <- reactive( |
176 | 111x |
if (!is.null(private$x_reactive())) sum(is.infinite(private$x_reactive())) |
177 |
)
|
|
178 | 111x |
private$inf_count <- sum(is.infinite(x)) |
179 | ||
180 | 111x |
private$plot_data <- list( |
181 | 111x |
type = "histogram", |
182 | 111x |
nbinsx = 50, |
183 | 111x |
x = Filter(Negate(is.na), Filter(is.finite, private$x)), |
184 | 111x |
color = I(fetch_bs_color("secondary")), |
185 | 111x |
alpha = 0.2, |
186 | 111x |
bingroup = 1, |
187 | 111x |
showlegend = FALSE, |
188 | 111x |
hoverinfo = "none" |
189 |
)
|
|
190 | 111x |
private$plot_mask <- list(list( |
191 | 111x |
type = "rect", fillcolor = rgb(1, 1, 1, .65), line = list(width = 0), |
192 | 111x |
x0 = -0.5, x1 = 1.5, y0 = -0.5, y1 = 1.5, xref = "paper", yref = "paper" |
193 |
)) |
|
194 | 111x |
private$plot_layout <- reactive({ |
195 | 5x |
shapes <- private$get_shape_properties(private$get_selected()) |
196 | 5x |
list( |
197 | 5x |
barmode = "overlay", |
198 | 5x |
xaxis = list( |
199 | 5x |
range = private$get_choices() * c(0.995, 1.005), |
200 | 5x |
rangeslider = list(thickness = 0), |
201 | 5x |
showticklabels = TRUE, |
202 | 5x |
ticks = "outside", |
203 | 5x |
ticklen = 1.5, |
204 | 5x |
tickmode = "auto", |
205 | 5x |
nticks = 10 |
206 |
),
|
|
207 | 5x |
yaxis = list(showgrid = FALSE, showticklabels = FALSE), |
208 | 5x |
margin = list(b = 17, l = 0, r = 0, t = 0, autoexpand = FALSE), |
209 | 5x |
plot_bgcolor = "#FFFFFF00", |
210 | 5x |
paper_bgcolor = "#FFFFFF00", |
211 | 5x |
shapes = shapes |
212 |
)
|
|
213 |
}) |
|
214 | 111x |
private$plot_config <- reactive({ |
215 | 5x |
list( |
216 | 5x |
doubleClick = "reset", |
217 | 5x |
displayModeBar = FALSE, |
218 | 5x |
edits = list(shapePosition = TRUE) |
219 |
)
|
|
220 |
}) |
|
221 | 111x |
private$plot_filtered <- reactive({ |
222 | 5x |
finite_values <- Filter(is.finite, private$x_reactive()) |
223 | 5x |
if (!identical(finite_values, numeric(0))) { |
224 | 5x |
list( |
225 | 5x |
x = finite_values, |
226 | 5x |
bingroup = 1, |
227 | 5x |
color = I(fetch_bs_color("primary")) |
228 |
)
|
|
229 |
}
|
|
230 |
}) |
|
231 | 111x |
invisible(self) |
232 |
}) |
|
233 |
},
|
|
234 | ||
235 |
#' @description
|
|
236 |
#' Returns reproducible condition call for current selection.
|
|
237 |
#' For this class returned call looks like
|
|
238 |
#' `<varname> >= <min value> & <varname> <= <max value>` with
|
|
239 |
#' optional `is.na(<varname>)` and `is.finite(<varname>)`.
|
|
240 |
#' @param dataname name of data set; defaults to `private$get_dataname()`
|
|
241 |
#' @return `call`
|
|
242 |
#'
|
|
243 |
get_call = function(dataname) { |
|
244 | 35x |
if (isFALSE(private$is_any_filtered())) { |
245 | 1x |
return(NULL) |
246 |
}
|
|
247 | 4x |
if (missing(dataname)) dataname <- private$get_dataname() |
248 | 34x |
varname <- private$get_varname_prefixed(dataname) |
249 | 34x |
filter_call <- |
250 | 34x |
call( |
251 |
"&",
|
|
252 | 34x |
call(">=", varname, private$get_selected()[1L]), |
253 | 34x |
call("<=", varname, private$get_selected()[2L]) |
254 |
)
|
|
255 | 34x |
private$add_keep_na_call(private$add_keep_inf_call(filter_call, varname), varname) |
256 |
},
|
|
257 | ||
258 |
#' @description
|
|
259 |
#' Returns current `keep_inf` selection.
|
|
260 |
#' @return `logical(1)`
|
|
261 |
get_keep_inf = function() { |
|
262 | ! |
private$teal_slice$keep_inf |
263 |
}
|
|
264 |
),
|
|
265 | ||
266 |
# private fields----
|
|
267 |
private = list( |
|
268 |
inf_count = integer(0), |
|
269 |
inf_filtered_count = NULL, |
|
270 |
is_integer = logical(0), |
|
271 |
numeric_step = numeric(0), # step for the slider input widget, calculated from input data (x) |
|
272 |
plot_data = NULL, |
|
273 |
plot_mask = list(), |
|
274 |
plot_layout = NULL, |
|
275 |
plot_config = NULL, |
|
276 |
plot_filtered = NULL, |
|
277 | ||
278 |
# private methods ----
|
|
279 | ||
280 |
set_choices = function(choices) { |
|
281 | 114x |
x <- private$x[is.finite(private$x)] |
282 | 114x |
if (is.null(choices)) { |
283 | 102x |
choices <- range(x) |
284 |
} else { |
|
285 | 12x |
choices_adjusted <- c(max(choices[1L], min(x)), min(choices[2L], max(x))) |
286 | 12x |
if (any(choices != choices_adjusted)) { |
287 | 1x |
warning(sprintf( |
288 | 1x |
"Choices adjusted (some values outside of variable range). Varname: %s, dataname: %s.",
|
289 | 1x |
private$get_varname(), private$get_dataname() |
290 |
)) |
|
291 | 1x |
choices <- choices_adjusted |
292 |
}
|
|
293 | 12x |
if (choices[1L] > choices[2L]) { |
294 | 1x |
warning(sprintf( |
295 | 1x |
"Invalid choices: lower is higher / equal to upper, or not in range of variable values. |
296 | 1x |
Setting defaults. Varname: %s, dataname: %s.", |
297 | 1x |
private$get_varname(), private$get_dataname() |
298 |
)) |
|
299 | 1x |
choices <- range(x) |
300 |
}
|
|
301 |
}
|
|
302 | ||
303 | 114x |
private$set_is_choice_limited(private$x, choices) |
304 | 114x |
private$x <- private$x[ |
305 | 114x |
(private$x >= choices[1L] & private$x <= choices[2L]) | is.na(private$x) | !is.finite(private$x) |
306 |
]
|
|
307 | ||
308 | 114x |
x_range <- range(private$x, finite = TRUE) |
309 | ||
310 |
# Required for displaying ticks on the slider, can modify choices!
|
|
311 | 114x |
if (identical(diff(x_range), 0)) { |
312 | 2x |
choices <- x_range |
313 |
} else { |
|
314 | 112x |
x_pretty <- pretty(x_range, 100L) |
315 | 112x |
choices <- range(x_pretty) |
316 | 112x |
private$numeric_step <- signif(private$get_pretty_range_step(x_pretty), digits = 10) |
317 |
}
|
|
318 | 114x |
private$teal_slice$choices <- choices |
319 | 114x |
invisible(NULL) |
320 |
},
|
|
321 | ||
322 |
# @description
|
|
323 |
# Check whether the initial choices filter out some values of x and set the flag in case.
|
|
324 |
set_is_choice_limited = function(xl, choices) { |
|
325 | 114x |
xl <- xl[!is.na(xl)] |
326 | 114x |
xl <- xl[is.finite(xl)] |
327 | 114x |
private$is_choice_limited <- (any(xl < choices[1L]) | any(xl > choices[2L])) |
328 | 114x |
invisible(NULL) |
329 |
},
|
|
330 | ||
331 |
# Adds is.infinite(varname) before existing condition calls if keep_inf is selected
|
|
332 |
# returns a call
|
|
333 |
add_keep_inf_call = function(filter_call, varname) { |
|
334 | 34x |
if (isTRUE(private$get_keep_inf())) { |
335 | 2x |
call("|", call("is.infinite", varname), filter_call) |
336 |
} else { |
|
337 | 32x |
filter_call
|
338 |
}
|
|
339 |
},
|
|
340 | ||
341 |
# @description gets pretty step size for range slider
|
|
342 |
# adaptation of shiny's method (see shiny/R/input-slider.R function findStepSize)
|
|
343 |
# @param pretty_range (numeric(n)) vector of pretty values
|
|
344 |
# @return numeric(1) pretty step size for the sliderInput
|
|
345 |
get_pretty_range_step = function(pretty_range) { |
|
346 | 114x |
if (private$is_integer && diff(range(pretty_range) > 2)) { |
347 | 46x |
return(1L) |
348 |
} else { |
|
349 | 68x |
n_steps <- length(pretty_range) - 1 |
350 | 68x |
return(signif(digits = 10, (max(pretty_range) - min(pretty_range)) / n_steps)) |
351 |
}
|
|
352 |
},
|
|
353 |
cast_and_validate = function(values) { |
|
354 | 130x |
tryCatch( |
355 | 130x |
expr = { |
356 | 130x |
values <- as.numeric(values) |
357 | 4x |
if (anyNA(values)) stop() |
358 | 126x |
values
|
359 |
},
|
|
360 | 130x |
error = function(e) stop("Vector of set values must contain values coercible to numeric") |
361 |
)
|
|
362 |
},
|
|
363 |
# Also validates that selection is sorted.
|
|
364 |
check_length = function(values) { |
|
365 | 2x |
if (length(values) != 2L) stop("Vector of set values must have length two.") |
366 | 2x |
if (values[1L] > values[2L]) stop("Vector of set values must be sorted.") |
367 | 122x |
values
|
368 |
},
|
|
369 |
# Trim selection to limits imposed by private$get_choices()
|
|
370 |
remove_out_of_bounds_values = function(values) { |
|
371 | 2x |
if (values[1L] < private$get_choices()[1L]) values[1L] <- private$get_choices()[1L] |
372 | 2x |
if (values[2L] > private$get_choices()[2L]) values[2L] <- private$get_choices()[2L] |
373 | 122x |
values
|
374 |
},
|
|
375 | ||
376 |
# Answers the question of whether the current settings and values selected actually filters out any values.
|
|
377 |
# @return logical scalar
|
|
378 |
is_any_filtered = function() { |
|
379 | 35x |
if (private$is_choice_limited) { |
380 | 1x |
TRUE
|
381 | 34x |
} else if (!isTRUE(all.equal(private$get_selected(), private$get_choices()))) { |
382 | 32x |
TRUE
|
383 | 2x |
} else if (!isTRUE(private$get_keep_inf()) && private$inf_count > 0) { |
384 | ! |
TRUE
|
385 | 2x |
} else if (!isTRUE(private$get_keep_na()) && private$na_count > 0) { |
386 | 1x |
TRUE
|
387 |
} else { |
|
388 | 1x |
FALSE
|
389 |
}
|
|
390 |
},
|
|
391 | ||
392 |
# obtain shape determination for histogram
|
|
393 |
# returns a list that is passed to plotly's layout.shapes property
|
|
394 |
get_shape_properties = function(values) { |
|
395 | 5x |
list( |
396 | 5x |
list(type = "line", x0 = values[1], x1 = values[1], y0 = -100, y1 = 100, yref = "paper"), |
397 | 5x |
list(type = "line", x0 = values[2], x1 = values[2], y0 = -100, y1 = 100, yref = "paper") |
398 |
)
|
|
399 |
},
|
|
400 | ||
401 |
# shiny modules ----
|
|
402 | ||
403 |
# UI Module for `RangeFilterState`.
|
|
404 |
# This UI element contains two values for `min` and `max`
|
|
405 |
# of the range and two checkboxes whether to keep the `NA` or `Inf` values.
|
|
406 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
407 |
ui_inputs = function(id) { |
|
408 | 5x |
ns <- NS(id) |
409 | 5x |
isolate({ |
410 | 5x |
ui_input <- shinyWidgets::numericRangeInput( |
411 | 5x |
inputId = ns("selection_manual"), |
412 | 5x |
label = NULL, |
413 | 5x |
min = private$get_choices()[1L], |
414 | 5x |
max = private$get_choices()[2L], |
415 | 5x |
value = private$get_selected(), |
416 | 5x |
step = private$numeric_step, |
417 | 5x |
width = "100%" |
418 |
)
|
|
419 | 5x |
tagList( |
420 | 5x |
tags$div( |
421 | 5x |
class = "choices_state", |
422 | 5x |
tags$head(tags$script( |
423 |
# Inline JS code for popover functionality.
|
|
424 |
# Adding the script inline because when added from a file with include_js_files(),
|
|
425 |
# it only works in the first info_button instance and not others.
|
|
426 | 5x |
HTML( |
427 | 5x |
'$(document).ready(function() { |
428 | 5x |
$("[data-toggle=\'popover\']").popover(); |
429 | ||
430 | 5x |
$(document).on("click", function (e) { |
431 | 5x |
if (!$("[data-toggle=\'popover\']").is(e.target) && |
432 | 5x |
$("[data-toggle=\'popover\']").has(e.target).length === 0 && |
433 | 5x |
$(".popover").has(e.target).length === 0) { |
434 | 5x |
$("[data-toggle=\'popover\']").popover("hide"); |
435 |
}
|
|
436 |
}); |
|
437 |
});' |
|
438 |
)
|
|
439 |
)), |
|
440 | 5x |
tags$div( |
441 | 5x |
actionLink( |
442 | 5x |
ns("plotly_info"), |
443 | 5x |
label = NULL, |
444 | 5x |
icon = icon("question-circle"), |
445 | 5x |
"data-toggle" = "popover", |
446 | 5x |
"data-html" = "true", |
447 | 5x |
"data-placement" = "left", |
448 | 5x |
"data-trigger" = "click", |
449 | 5x |
"data-title" = "Plot actions", |
450 | 5x |
"data-content" = "<p> |
451 | 5x |
Drag vertical lines to set selection.<br> |
452 | 5x |
Drag across plot to zoom in.<br> |
453 | 5x |
Drag axis to pan.<br> |
454 | 5x |
Double click to zoom out." |
455 |
),
|
|
456 | 5x |
style = "text-align: right; font-size: 0.7em; margin-bottom: -1em; position: relative; z-index: 9;" |
457 |
),
|
|
458 | 5x |
shinycssloaders::withSpinner( |
459 | 5x |
plotly::plotlyOutput(ns("plot"), height = "50px"), |
460 | 5x |
type = 4, |
461 | 5x |
size = 0.25, |
462 | 5x |
hide.ui = FALSE |
463 |
),
|
|
464 | 5x |
ui_input
|
465 |
),
|
|
466 | 5x |
tags$div( |
467 | 5x |
class = "filter-card-body-keep-na-inf", |
468 | 5x |
private$keep_inf_ui(ns("keep_inf")), |
469 | 5x |
private$keep_na_ui(ns("keep_na")) |
470 |
)
|
|
471 |
)
|
|
472 |
}) |
|
473 |
},
|
|
474 | ||
475 |
# @description
|
|
476 |
# Server module
|
|
477 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
478 |
# return `NULL`.
|
|
479 |
server_inputs = function(id) { |
|
480 | 5x |
moduleServer( |
481 | 5x |
id = id, |
482 | 5x |
function(input, output, session) { |
483 | 5x |
logger::log_debug("RangeFilterState$server initializing, id: { private$get_id() }") |
484 | ||
485 |
# Capture manual input with debounce.
|
|
486 | 5x |
selection_manual <- debounce(reactive(input$selection_manual), 200) |
487 | ||
488 |
# Prepare for histogram construction.
|
|
489 | 5x |
plot_data <- c(private$plot_data, source = session$ns("histogram_plot")) |
490 | ||
491 | 5x |
trigger_event_data <- reactiveVal(NULL) |
492 | ||
493 |
# Display histogram, adding a second trace that contains filtered data.
|
|
494 | 5x |
output$plot <- plotly::renderPlotly({ |
495 | 5x |
histogram <- do.call(plotly::plot_ly, plot_data) |
496 | 5x |
histogram <- do.call(plotly::layout, c(list(p = histogram), private$plot_layout())) |
497 | 5x |
histogram <- do.call(plotly::config, c(list(p = histogram), private$plot_config())) |
498 | 5x |
histogram <- do.call(plotly::add_histogram, c(list(p = histogram), private$plot_filtered())) |
499 | 5x |
trigger_event_data(TRUE) |
500 | 5x |
histogram
|
501 |
}) |
|
502 | ||
503 | 5x |
relayout_data <- reactive({ |
504 | 3x |
req(trigger_event_data()) |
505 | 3x |
plotly::event_data("plotly_relayout", source = session$ns("histogram_plot")) |
506 |
}) |
|
507 | ||
508 |
# Dragging shapes (lines) on plot updates selection.
|
|
509 | 5x |
private$session_bindings[[session$ns("relayout")]] <- observeEvent( |
510 | 5x |
ignoreNULL = FALSE, |
511 | 5x |
ignoreInit = TRUE, |
512 | 5x |
eventExpr = relayout_data(), |
513 | 5x |
handlerExpr = { |
514 | 1x |
logger::log_debug("RangeFilterState$server@1 selection changed, id: { private$get_id() }") |
515 | 1x |
event <- relayout_data() |
516 | 1x |
if (any(grepl("shapes", names(event)))) { |
517 | ! |
line_positions <- private$get_selected() |
518 | ! |
if (any(grepl("shapes[0]", names(event), fixed = TRUE))) { |
519 | ! |
line_positions[1] <- event[["shapes[0].x0"]] |
520 | ! |
} else if (any(grepl("shapes[1]", names(event), fixed = TRUE))) { |
521 | ! |
line_positions[2] <- event[["shapes[1].x0"]] |
522 |
}
|
|
523 |
# If one line was dragged past the other, abort action and reset lines.
|
|
524 | ! |
if (line_positions[1] > line_positions[2]) { |
525 | ! |
showNotification( |
526 | ! |
"Numeric range start value must be less than end value.",
|
527 | ! |
type = "warning" |
528 |
)
|
|
529 | ! |
plotly::plotlyProxyInvoke( |
530 | ! |
plotly::plotlyProxy("plot"), |
531 | ! |
"relayout",
|
532 | ! |
shapes = private$get_shape_properties(private$get_selected()) |
533 |
)
|
|
534 | ! |
return(NULL) |
535 |
}
|
|
536 | ||
537 | ! |
private$set_selected(signif(line_positions, digits = 4L)) |
538 |
}
|
|
539 |
}
|
|
540 |
)
|
|
541 | ||
542 |
# Change in selection updates shapes (lines) on plot and numeric input.
|
|
543 | 5x |
private$session_bindings[[session$ns("selection_api")]] <- observeEvent( |
544 | 5x |
ignoreNULL = FALSE, |
545 | 5x |
ignoreInit = TRUE, |
546 | 5x |
eventExpr = private$get_selected(), |
547 | 5x |
handlerExpr = { |
548 | ! |
if (!isTRUE(all.equal(private$get_selected(), selection_manual()))) { |
549 | ! |
logger::log_debug("RangeFilterState$server@2 state changed, id: {private$get_id() }") |
550 | ! |
shinyWidgets::updateNumericRangeInput( |
551 | ! |
session = session, |
552 | ! |
inputId = "selection_manual", |
553 | ! |
value = private$get_selected() |
554 |
)
|
|
555 |
}
|
|
556 |
}
|
|
557 |
)
|
|
558 | ||
559 |
# Manual input updates selection.
|
|
560 | 5x |
private$session_bindings[[session$ns("selection_manual")]] <- observeEvent( |
561 | 5x |
ignoreNULL = FALSE, |
562 | 5x |
ignoreInit = TRUE, |
563 | 5x |
eventExpr = selection_manual(), |
564 | 5x |
handlerExpr = { |
565 | ! |
selection <- selection_manual() |
566 |
# Abort and reset if non-numeric values is entered.
|
|
567 | ! |
if (any(is.na(selection))) { |
568 | ! |
showNotification( |
569 | ! |
"Numeric range values must be numbers.",
|
570 | ! |
type = "warning" |
571 |
)
|
|
572 | ! |
shinyWidgets::updateNumericRangeInput( |
573 | ! |
session = session, |
574 | ! |
inputId = "selection_manual", |
575 | ! |
value = private$get_selected() |
576 |
)
|
|
577 | ! |
return(NULL) |
578 |
}
|
|
579 | ||
580 |
# Abort and reset if reversed choices are specified.
|
|
581 | ! |
if (selection[1] > selection[2]) { |
582 | ! |
showNotification( |
583 | ! |
"Numeric range start value must be less than end value.",
|
584 | ! |
type = "warning" |
585 |
)
|
|
586 | ! |
shinyWidgets::updateNumericRangeInput( |
587 | ! |
session = session, |
588 | ! |
inputId = "selection_manual", |
589 | ! |
value = private$get_selected() |
590 |
)
|
|
591 | ! |
return(NULL) |
592 |
}
|
|
593 | ||
594 | ||
595 | ! |
if (!isTRUE(all.equal(selection, private$get_selected()))) { |
596 | ! |
logger::log_debug("RangeFilterState$server@3 manual selection changed, id: { private$get_id() }") |
597 | ! |
private$set_selected(selection) |
598 |
}
|
|
599 |
}
|
|
600 |
)
|
|
601 | ||
602 | 5x |
private$keep_inf_srv("keep_inf") |
603 | 5x |
private$keep_na_srv("keep_na") |
604 | ||
605 | 5x |
NULL
|
606 |
}
|
|
607 |
)
|
|
608 |
},
|
|
609 |
server_inputs_fixed = function(id) { |
|
610 | ! |
moduleServer( |
611 | ! |
id = id, |
612 | ! |
function(input, output, session) { |
613 | ! |
logger::log_debug("RangeFilterState$server initializing, id: { private$get_id() }") |
614 | ||
615 | ! |
plot_config <- private$plot_config() |
616 | ! |
plot_config$staticPlot <- TRUE |
617 | ||
618 | ! |
output$plot <- plotly::renderPlotly({ |
619 | ! |
histogram <- do.call(plotly::plot_ly, private$plot_data) |
620 | ! |
histogram <- do.call(plotly::layout, c(list(p = histogram), private$plot_layout())) |
621 | ! |
histogram <- do.call(plotly::config, c(list(p = histogram), plot_config)) |
622 | ! |
histogram <- do.call(plotly::add_histogram, c(list(p = histogram), private$plot_filtered())) |
623 | ! |
histogram
|
624 |
}) |
|
625 | ||
626 | ! |
output$selection <- renderUI({ |
627 | ! |
shinycssloaders::withSpinner( |
628 | ! |
plotly::plotlyOutput(session$ns("plot"), height = "50px"), |
629 | ! |
type = 4, |
630 | ! |
size = 0.25 |
631 |
)
|
|
632 |
}) |
|
633 | ||
634 | ! |
NULL
|
635 |
}
|
|
636 |
)
|
|
637 |
},
|
|
638 | ||
639 |
# @description
|
|
640 |
# Server module to display filter summary
|
|
641 |
# renders text describing selected range and
|
|
642 |
# if NA or Inf are included also
|
|
643 |
# @return `shiny.tag` to include in the `ui_summary`
|
|
644 |
content_summary = function() { |
|
645 | 5x |
selection <- private$get_selected() |
646 | 5x |
tagList( |
647 | 5x |
tags$span(HTML(selection[1], "–", selection[2]), class = "filter-card-summary-value"), |
648 | 5x |
tags$span( |
649 | 5x |
class = "filter-card-summary-controls", |
650 | 5x |
if (private$na_count > 0) { |
651 | ! |
tags$span("NA", if (isTRUE(private$get_keep_na())) icon("check") else icon("xmark")) |
652 |
},
|
|
653 | 5x |
if (private$inf_count > 0) { |
654 | ! |
tags$span("Inf", if (isTRUE(private$get_keep_inf())) icon("check") else icon("xmark")) |
655 |
}
|
|
656 |
)
|
|
657 |
)
|
|
658 |
},
|
|
659 | ||
660 |
# @description
|
|
661 |
# Module displaying input to keep or remove NA in the `FilterState` call.
|
|
662 |
# Renders a checkbox input only when variable with which the `FilterState` has been created contains Infs.
|
|
663 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
664 |
keep_inf_ui = function(id) { |
|
665 | 5x |
ns <- NS(id) |
666 | ||
667 | 5x |
if (private$inf_count > 0) { |
668 | ! |
countmax <- private$na_count |
669 | ! |
countnow <- isolate(private$filtered_na_count()) |
670 | ! |
ui_input <- checkboxInput( |
671 | ! |
inputId = ns("value"), |
672 | ! |
label = tags$span( |
673 | ! |
id = ns("count_label"), |
674 | ! |
make_count_text( |
675 | ! |
label = "Keep Inf", |
676 | ! |
countmax = countmax, |
677 | ! |
countnow = countnow |
678 |
)
|
|
679 |
),
|
|
680 | ! |
value = isolate(private$get_keep_inf()) |
681 |
)
|
|
682 | ! |
tags$div( |
683 | ! |
uiOutput(ns("trigger_visible"), inline = TRUE), |
684 | ! |
ui_input
|
685 |
)
|
|
686 |
} else { |
|
687 | 5x |
NULL
|
688 |
}
|
|
689 |
},
|
|
690 | ||
691 |
# @description
|
|
692 |
# Module to handle Inf values in the FilterState
|
|
693 |
# Sets `private$slice$keep_inf` according to the selection
|
|
694 |
# and updates the relevant UI element if `private$slice$keep_inf` has been changed by the api.
|
|
695 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
696 |
# @return `NULL`.
|
|
697 |
keep_inf_srv = function(id) { |
|
698 | 5x |
moduleServer(id, function(input, output, session) { |
699 |
# 1. renderUI is used here as an observer which triggers only if output is visible
|
|
700 |
# and if the reactive changes - reactive triggers only if the output is visible.
|
|
701 |
# 2. We want to trigger change of the labels only if reactive count changes (not underlying data)
|
|
702 | 5x |
output$trigger_visible <- renderUI({ |
703 | 5x |
updateCountText( |
704 | 5x |
inputId = "count_label", |
705 | 5x |
label = "Keep Inf", |
706 | 5x |
countmax = private$inf_count, |
707 | 5x |
countnow = private$inf_filtered_count() |
708 |
)
|
|
709 | 5x |
NULL
|
710 |
}) |
|
711 | ||
712 |
# this observer is needed in the situation when private$teal_slice$keep_inf has been
|
|
713 |
# changed directly by the api - then it's needed to rerender UI element
|
|
714 |
# to show relevant values
|
|
715 | 5x |
private$session_bindings[[session$ns("keep_inf_api")]] <- observeEvent( |
716 | 5x |
ignoreNULL = TRUE, # its not possible for range that NULL is selected |
717 | 5x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
718 | 5x |
eventExpr = private$get_keep_inf(), |
719 | 5x |
handlerExpr = { |
720 | ! |
if (!setequal(private$get_keep_inf(), input$value)) { |
721 | ! |
logger::log_debug("RangeFilterState$keep_inf_srv@1 changed reactive value, id: { private$get_id() }") |
722 | ! |
updateCheckboxInput( |
723 | ! |
inputId = "value", |
724 | ! |
value = private$get_keep_inf() |
725 |
)
|
|
726 |
}
|
|
727 |
}
|
|
728 |
)
|
|
729 | ||
730 | 5x |
private$session_bindings[[session$ns("keep_inf")]] <- observeEvent( |
731 | 5x |
ignoreNULL = TRUE, # it's not possible for range that NULL is selected |
732 | 5x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
733 | 5x |
eventExpr = input$value, |
734 | 5x |
handlerExpr = { |
735 | ! |
logger::log_debug("FilterState$keep_na_srv@2 changed input, id: { private$get_id() }") |
736 | ! |
keep_inf <- input$value |
737 | ! |
private$set_keep_inf(keep_inf) |
738 |
}
|
|
739 |
)
|
|
740 | ||
741 | 5x |
invisible(NULL) |
742 |
}) |
|
743 |
}
|
|
744 |
)
|
|
745 |
)
|
1 |
#' Compose predicates
|
|
2 |
#'
|
|
3 |
#' Combines calls with a logical operator.
|
|
4 |
#'
|
|
5 |
#' This function is used to combine logical predicates produced by `FilterState` objects
|
|
6 |
#' to build a complete subset expression.
|
|
7 |
#'
|
|
8 |
#' @param calls (`list`)
|
|
9 |
#' containing calls (or symbols) to be combined by `operator`
|
|
10 |
#' @param operator (`character(1)`)
|
|
11 |
#' infix operator to use in predicate composition, _e.g._ `"&"`
|
|
12 |
#'
|
|
13 |
#' @return
|
|
14 |
#' A `call` where elements of `calls` are composed with `operator` or `NULL` if `calls` is an empty list.
|
|
15 |
#'
|
|
16 |
#' @examples
|
|
17 |
#' # use non-exported function from teal.slice
|
|
18 |
#' calls_combine_by <- getFromNamespace("calls_combine_by", "teal.slice")
|
|
19 |
#'
|
|
20 |
#' calls <- list(
|
|
21 |
#' quote(SEX == "F"), # subsetting on factor
|
|
22 |
#' quote(AGE >= 20 & AGE <= 50), # subsetting on range
|
|
23 |
#' quote(!SURV) # subsetting on logical
|
|
24 |
#' )
|
|
25 |
#' calls_combine_by(calls, "&")
|
|
26 |
#'
|
|
27 |
#' @keywords internal
|
|
28 |
#'
|
|
29 |
calls_combine_by <- function(calls, operator) { |
|
30 | 47x |
checkmate::assert_list(calls) |
31 | 45x |
if (length(calls) > 0L) checkmate::assert_list(calls, types = c("call", "name")) |
32 | 46x |
checkmate::assert_string(operator) |
33 | ||
34 | 44x |
Reduce( |
35 | 44x |
x = calls, |
36 | 44x |
f = function(x, y) call(operator, x, y) |
37 |
)
|
|
38 |
}
|
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 | ! |
teal.logger::register_handlers("teal.slice") |
11 | ||
12 | ! |
invisible() |
13 |
}
|
|
14 | ||
15 |
### GLOBAL VARIABLES ###
|
|
16 | ||
17 |
.filterable_class <- c("logical", "integer", "numeric", "factor", "character", "Date", "POSIXct", "POSIXlt") |
|
18 | ||
19 | ||
20 |
### END GLOBAL VARIABLES ###
|
|
21 | ||
22 | ||
23 |
### ENSURE CHECK PASSES
|
|
24 | ||
25 |
# This function is necessary for check to properly process code dependencies within R6 classes.
|
|
26 |
# If `package` is listed in `Imports` in `DESCRIPTION`,
|
|
27 |
# (1) check goes through `NAMESPACE` looking for any `importFrom(package,<foo>)` statements
|
|
28 |
# or an `import(package)` statement. If none are found,
|
|
29 |
# (2) check looks for `package::*` calls in the code. If none are found again,
|
|
30 |
# (3) check throws a NOTE;
|
|
31 |
# # Namespaces in Imports field not imported from:
|
|
32 |
# # 'package'
|
|
33 |
# # All declared Imports should be used.
|
|
34 |
# This note is banned by our CI.
|
|
35 |
# When package::* statements are made within an R6 class, they are not registered.
|
|
36 |
# This function provides single references to the imported namespaces for check to notice.
|
|
37 |
.rectify_dependencies_check <- function() { |
|
38 | ! |
dplyr::filter |
39 | ! |
grDevices::rgb |
40 | ! |
htmltools::tagInsertChildren |
41 | ! |
lifecycle::badge |
42 | ! |
logger::log_debug |
43 | ! |
plotly::plot_ly |
44 | ! |
shinycssloaders::withSpinner |
45 | ! |
shinyWidgets::pickerOptions |
46 | ! |
teal.data::datanames |
47 | ! |
teal.widgets::optionalSelectInput |
48 |
}
|
|
49 | ||
50 | ||
51 |
### END ENSURE CHECK PASSES
|
1 |
# FilterStateExpr ------
|
|
2 | ||
3 |
#' @name FilterStateExpr
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title `FilterStateExpr` `R6` class
|
|
7 |
#'
|
|
8 |
#' @description Sister class to `FilterState` that handles arbitrary filter expressions.
|
|
9 |
#'
|
|
10 |
#' @details
|
|
11 |
#' Creates a filter state around a predefined condition call (logical predicate).
|
|
12 |
#' The condition call is independent of the data
|
|
13 |
#' and the filter card allows no interaction (the filter is always fixed).
|
|
14 |
#'
|
|
15 |
#' @examples
|
|
16 |
#' # use non-exported function from teal.slice
|
|
17 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice")
|
|
18 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice")
|
|
19 |
#' FilterStateExpr <- getFromNamespace("FilterStateExpr", "teal.slice")
|
|
20 |
#'
|
|
21 |
#' filter_state <- FilterStateExpr$new(
|
|
22 |
#' slice = teal_slice(
|
|
23 |
#' dataname = "x",
|
|
24 |
#' id = "FA",
|
|
25 |
#' title = "Adult females",
|
|
26 |
#' expr = "sex == 'F' & age >= 18"
|
|
27 |
#' )
|
|
28 |
#' )
|
|
29 |
#' filter_state$get_call()
|
|
30 |
#'
|
|
31 |
#' # working filter in an app
|
|
32 |
#' library(shiny)
|
|
33 |
#' library(shinyjs)
|
|
34 |
#'
|
|
35 |
#' ui <- fluidPage(
|
|
36 |
#' useShinyjs(),
|
|
37 |
#' include_css_files(pattern = "filter-panel"),
|
|
38 |
#' include_js_files(pattern = "count-bar-labels"),
|
|
39 |
#' column(4, tags$div(
|
|
40 |
#' tags$h4("ChoicesFilterState"),
|
|
41 |
#' filter_state$ui("fs")
|
|
42 |
#' )),
|
|
43 |
#' column(8, tags$div(
|
|
44 |
#' tags$h4("Condition (i.e. call)"), # display the condition call generated by this FilterState
|
|
45 |
#' textOutput("condition_choices"), tags$br(),
|
|
46 |
#' tags$h4("Unformatted state"), # display raw filter state
|
|
47 |
#' textOutput("unformatted_choices"), tags$br(),
|
|
48 |
#' tags$h4("Formatted state"), # display human readable filter state
|
|
49 |
#' textOutput("formatted_choices"), tags$br()
|
|
50 |
#' ))
|
|
51 |
#' )
|
|
52 |
#'
|
|
53 |
#' server <- function(input, output, session) {
|
|
54 |
#' filter_state$server("fs")
|
|
55 |
#' output$condition_choices <- renderPrint(filter_state$get_call())
|
|
56 |
#' output$formatted_choices <- renderText(filter_state$format())
|
|
57 |
#' output$unformatted_choices <- renderPrint(filter_state$get_state())
|
|
58 |
#' }
|
|
59 |
#'
|
|
60 |
#' if (interactive()) {
|
|
61 |
#' shinyApp(ui, server)
|
|
62 |
#' }
|
|
63 |
#'
|
|
64 |
#' @keywords internal
|
|
65 |
#'
|
|
66 |
FilterStateExpr <- R6::R6Class( # nolint |
|
67 |
classname = "FilterStateExpr", |
|
68 |
# public methods ----
|
|
69 |
public = list( |
|
70 |
#' @description
|
|
71 |
#' Initialize a `FilterStateExpr` object.
|
|
72 |
#' @param slice (`teal_slice_expr`)
|
|
73 |
#' @return Object of class `FilterStateExpr`, invisibly.
|
|
74 |
#'
|
|
75 |
initialize = function(slice) { |
|
76 | 15x |
checkmate::assert_class(slice, "teal_slice_expr") |
77 | 14x |
private$teal_slice <- slice |
78 | 14x |
invisible(self) |
79 |
},
|
|
80 | ||
81 |
#' @description
|
|
82 |
#' Returns a formatted string representing this `FilterStateExpr` object.
|
|
83 |
#'
|
|
84 |
#' @param show_all (`logical(1)`) passed to `format.teal_slice`
|
|
85 |
#' @param trim_lines (`logical(1)`) passed to `format.teal_slice`
|
|
86 |
#'
|
|
87 |
#' @return `character(1)` the formatted string
|
|
88 |
#'
|
|
89 |
format = function(show_all = FALSE, trim_lines = TRUE) { |
|
90 | 12x |
sprintf( |
91 | 12x |
"%s:\n%s",
|
92 | 12x |
class(self)[1], |
93 | 12x |
format(self$get_state(), show_all = show_all, trim_lines = trim_lines) |
94 |
)
|
|
95 |
},
|
|
96 | ||
97 |
#' @description
|
|
98 |
#' Prints this `FilterStateExpr` object.
|
|
99 |
#' @param ... arguments passed to the `format` method
|
|
100 |
#' @return `NULL`, invisibly.
|
|
101 |
#'
|
|
102 |
print = function(...) { |
|
103 | 1x |
cat(isolate(self$format(...))) |
104 |
},
|
|
105 | ||
106 |
#' @description
|
|
107 |
#' Returns a complete description of this filter state.
|
|
108 |
#'
|
|
109 |
#' @return A `teal_slice` object.
|
|
110 |
#'
|
|
111 |
get_state = function() { |
|
112 | 30x |
private$teal_slice |
113 |
},
|
|
114 | ||
115 |
#' @description
|
|
116 |
#' Does nothing. Exists for compatibility.
|
|
117 |
#'
|
|
118 |
#' @param state (`teal_slice`)
|
|
119 |
#'
|
|
120 |
#' @return `self`, invisibly.
|
|
121 |
#'
|
|
122 |
set_state = function(state) { |
|
123 | 1x |
checkmate::assert_class(state, "teal_slice_expr") |
124 | 1x |
invisible(self) |
125 |
},
|
|
126 | ||
127 |
#' @description
|
|
128 |
#' Get reproducible call.
|
|
129 |
#'
|
|
130 |
#' @param dataname (`ignored`) for a consistency with `FilterState`
|
|
131 |
#'
|
|
132 |
#' Returns reproducible condition call for current selection relevant
|
|
133 |
#' for selected variable type.
|
|
134 |
#' Method is using internal reactive values which makes it reactive
|
|
135 |
#' and must be executed in reactive or isolated context.
|
|
136 |
#'
|
|
137 |
#' @return `call` or `NULL`
|
|
138 |
#'
|
|
139 |
get_call = function(dataname) { |
|
140 | 2x |
isolate(str2lang(private$teal_slice$expr)) |
141 |
},
|
|
142 | ||
143 |
#' @description
|
|
144 |
#' Destroy inputs and observers stored in `private$session_bindings`.
|
|
145 |
#'
|
|
146 |
#' @return `NULL`, invisibly.
|
|
147 |
#'
|
|
148 |
finalize = function() { |
|
149 | 15x |
.finalize_session_bindings(self, private) |
150 | 15x |
invisible(NULL) |
151 |
},
|
|
152 | ||
153 |
# public shiny modules ----
|
|
154 | ||
155 |
#' @description
|
|
156 |
#' `shiny` module server.
|
|
157 |
#'
|
|
158 |
#' @param id (`character(1)`)
|
|
159 |
#' `shiny` module instance id.
|
|
160 |
#'
|
|
161 |
#' @param remove_callback (`function`)
|
|
162 |
#' callback to handle removal of this `FilterState` object from `state_list`
|
|
163 |
#'
|
|
164 |
#' @return Reactive expression signaling that the remove button has been clicked.
|
|
165 |
#'
|
|
166 |
server = function(id, remove_callback) { |
|
167 | ! |
moduleServer( |
168 | ! |
id = id, |
169 | ! |
function(input, output, session) { |
170 | ! |
private$server_summary("summary") |
171 | ||
172 | ! |
private$session_bindings[[session$ns("remove")]] <- observeEvent( |
173 | ! |
once = TRUE, # remove button can be called once, should be destroyed afterwards |
174 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we destroy the previous input set of the UI |
175 | ! |
eventExpr = input$remove, # when remove button is clicked in the FilterState ui |
176 | ! |
handlerExpr = remove_callback() |
177 |
)
|
|
178 | ||
179 | ! |
private$session_bindings[[session$ns("inputs")]] <- list( |
180 | ! |
destroy = function() { |
181 | ! |
logger::log_debug("Destroying FilterState inputs and observers; id: { private$get_id() }") |
182 | ! |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
183 |
}
|
|
184 |
)
|
|
185 | ||
186 | ! |
NULL
|
187 |
}
|
|
188 |
)
|
|
189 |
},
|
|
190 | ||
191 |
#' @description
|
|
192 |
#' `shiny` module UI.
|
|
193 |
#' The UI for this class contains simple message stating that it is not supported.
|
|
194 |
#' @param id (`character(1)`)
|
|
195 |
#' `shiny` module instance id.
|
|
196 |
#' @param parent_id (`character(1)`)
|
|
197 |
#' id of the `FilterStates` card container.
|
|
198 |
ui = function(id, parent_id = "cards") { |
|
199 | ! |
ns <- NS(id) |
200 | ! |
isolate({ |
201 | ! |
tags$div( |
202 | ! |
id = id, |
203 | ! |
class = "panel filter-card", |
204 | ! |
include_js_files("count-bar-labels.js"), |
205 | ! |
tags$div( |
206 | ! |
class = "filter-card-header", |
207 | ! |
tags$div( |
208 | ! |
class = "filter-card-title", |
209 | ! |
if (private$is_anchored()) { |
210 | ! |
icon("anchor-lock", class = "filter-card-icon") |
211 |
} else { |
|
212 | ! |
icon("lock", class = "filter-card-icon") |
213 |
},
|
|
214 | ! |
tags$div(class = "filter-card-varname", tags$strong(private$teal_slice$id)), |
215 | ! |
tags$div(class = "filter-card-varlabel", private$teal_slice$title), |
216 | ! |
tags$div( |
217 | ! |
class = "filter-card-controls", |
218 | ! |
if (isFALSE(private$is_anchored())) { |
219 | ! |
actionLink( |
220 | ! |
inputId = ns("remove"), |
221 | ! |
label = icon("circle-xmark", lib = "font-awesome"), |
222 | ! |
title = "Remove filter", |
223 | ! |
class = "filter-card-remove" |
224 |
)
|
|
225 |
}
|
|
226 |
)
|
|
227 |
),
|
|
228 | ! |
tags$div( |
229 | ! |
class = "filter-card-summary", |
230 | ! |
private$ui_summary(ns("summary")) |
231 |
)
|
|
232 |
)
|
|
233 |
)
|
|
234 |
}) |
|
235 |
}
|
|
236 |
),
|
|
237 | ||
238 |
# private members ----
|
|
239 | ||
240 |
private = list( |
|
241 |
session_bindings = list(), # stores observers and inputs to destroy afterwards |
|
242 |
teal_slice = NULL, # stores reactiveValues |
|
243 | ||
244 |
# @description
|
|
245 |
# Get id of the teal_slice.
|
|
246 |
# @return `character(1)`
|
|
247 |
get_id = function() { |
|
248 | ! |
isolate(private$teal_slice$id) |
249 |
},
|
|
250 | ||
251 |
# Check whether this filter is anchored (cannot be removed).
|
|
252 |
# @return `logical(1)`
|
|
253 |
is_anchored = function() { |
|
254 | ! |
isolate(isTRUE(private$teal_slice$anchored)) |
255 |
},
|
|
256 | ||
257 |
# @description
|
|
258 |
# Server module to display filter summary
|
|
259 |
# @param id `shiny` id parameter
|
|
260 |
ui_summary = function(id) { |
|
261 | ! |
ns <- NS(id) |
262 | ! |
uiOutput(ns("summary"), class = "filter-card-summary") |
263 |
},
|
|
264 | ||
265 |
# @description
|
|
266 |
# UI module to display filter summary
|
|
267 |
# @param shiny `id` parameter passed to moduleServer
|
|
268 |
# renders text describing current state
|
|
269 |
server_summary = function(id) { |
|
270 | ! |
moduleServer( |
271 | ! |
id = id, |
272 | ! |
function(input, output, session) { |
273 | ! |
output$summary <- renderUI(private$content_summary()) |
274 |
}
|
|
275 |
)
|
|
276 |
},
|
|
277 |
content_summary = function() { |
|
278 | ! |
isolate(private$teal_slice$expr) |
279 |
}
|
|
280 |
)
|
|
281 |
)
|
1 |
# SEFilterStates ------
|
|
2 | ||
3 |
#' @name SEFilterStates
|
|
4 |
#' @docType class
|
|
5 |
#' @title `FilterStates` subclass for `SummarizedExperiment`s
|
|
6 |
#' @description Handles filter states in a `SummaryExperiment`.
|
|
7 |
#' @keywords internal
|
|
8 |
#'
|
|
9 |
SEFilterStates <- R6::R6Class( # nolint |
|
10 |
classname = "SEFilterStates", |
|
11 |
inherit = FilterStates, |
|
12 | ||
13 |
# public methods ----
|
|
14 |
public = list( |
|
15 |
#' @description
|
|
16 |
#' Initialize `SEFilterStates` object.
|
|
17 |
#'
|
|
18 |
#' @param data (`SummarizedExperiment`)
|
|
19 |
#' the `R` object which `subset` function is applied on.
|
|
20 |
#' @param data_reactive (`function(sid)`)
|
|
21 |
#' should return a `SummarizedExperiment` object or `NULL`.
|
|
22 |
#' This object is needed for the `FilterState` counts being updated on a change in filters.
|
|
23 |
#' If function returns `NULL` then filtered counts are not shown.
|
|
24 |
#' Function has to have `sid` argument being a character.
|
|
25 |
#' @param dataname (`character(1)`)
|
|
26 |
#' name of the data used in the expression
|
|
27 |
#' specified to the function argument attached to this `FilterStates`.
|
|
28 |
#' @param datalabel (`character(1)`) optional
|
|
29 |
#' text label. Should be the name of experiment.
|
|
30 |
#'
|
|
31 |
initialize = function(data, |
|
32 |
data_reactive = function(sid = "") NULL, |
|
33 |
dataname,
|
|
34 |
datalabel = NULL) { |
|
35 | 84x |
if (!requireNamespace("SummarizedExperiment", quietly = TRUE)) { |
36 | ! |
stop("Cannot load SummarizedExperiment - please install the package or restart your session.") |
37 |
}
|
|
38 | 84x |
checkmate::assert_function(data_reactive, args = "sid") |
39 | 84x |
checkmate::assert_class(data, "SummarizedExperiment") |
40 | 83x |
super$initialize(data, data_reactive, dataname, datalabel) |
41 | 83x |
if (!is.null(datalabel)) { |
42 | 76x |
private$dataname_prefixed <- sprintf( |
43 | 76x |
"%s[['%s']]", private$dataname_prefixed, datalabel |
44 |
)
|
|
45 |
}
|
|
46 |
},
|
|
47 | ||
48 |
#' @description
|
|
49 |
#' Set filter state.
|
|
50 |
#'
|
|
51 |
#' @param state (`teal_slices`)
|
|
52 |
#' `teal_slice` objects should contain the field `arg %in% c("subset", "select")`
|
|
53 |
#'
|
|
54 |
#' @return `NULL`, invisibly.
|
|
55 |
#'
|
|
56 |
set_filter_state = function(state) { |
|
57 | 53x |
isolate({ |
58 | 53x |
logger::log_debug("SEFilterState$set_filter_state initializing, dataname: { private$dataname }") |
59 | 53x |
checkmate::assert_class(state, "teal_slices") |
60 | 51x |
lapply(state, function(x) { |
61 | 16x |
checkmate::assert_choice(x$arg, choices = c("subset", "select"), null.ok = TRUE, .var.name = "teal_slice$arg") |
62 |
}) |
|
63 | 51x |
count_type <- attr(state, "count_type") |
64 | 51x |
if (length(count_type)) { |
65 | 8x |
private$count_type <- count_type |
66 |
}
|
|
67 | ||
68 | 51x |
subset_states <- Filter(function(x) x$arg == "subset", state) |
69 | 51x |
private$set_filter_state_impl( |
70 | 51x |
state = subset_states, |
71 | 51x |
data = SummarizedExperiment::rowData(private$data), |
72 | 51x |
data_reactive = function(sid = "") { |
73 | ! |
data <- private$data_reactive() |
74 | ! |
if (!is.null(data)) { |
75 | ! |
SummarizedExperiment::rowData(data) |
76 |
}
|
|
77 |
}
|
|
78 |
)
|
|
79 | ||
80 | 51x |
select_states <- Filter(function(x) x$arg == "select", state) |
81 | 51x |
private$set_filter_state_impl( |
82 | 51x |
state = select_states, |
83 | 51x |
data = SummarizedExperiment::colData(private$data), |
84 | 51x |
data_reactive = function(sid = "") { |
85 | ! |
data <- private$data_reactive() |
86 | ! |
if (!is.null(data)) { |
87 | ! |
SummarizedExperiment::colData(data) |
88 |
}
|
|
89 |
}
|
|
90 |
)
|
|
91 | ||
92 | 51x |
invisible(NULL) |
93 |
}) |
|
94 |
},
|
|
95 | ||
96 |
#' @description
|
|
97 |
#' `shiny` UI module to add filter variable.
|
|
98 |
#' @param id (`character(1)`)
|
|
99 |
#' `shiny` module instance id.
|
|
100 |
#' @return `shiny.tag`
|
|
101 |
ui_add = function(id) { |
|
102 | 2x |
data <- private$data |
103 | 2x |
checkmate::assert_string(id) |
104 | 2x |
ns <- NS(id) |
105 | 2x |
row_input <- if (ncol(SummarizedExperiment::rowData(data)) == 0) { |
106 | 1x |
tags$div("no sample variables available") |
107 | 2x |
} else if (nrow(SummarizedExperiment::rowData(data)) == 0) { |
108 | 1x |
tags$div("no samples available") |
109 |
} else { |
|
110 | ! |
teal.widgets::optionalSelectInput( |
111 | ! |
ns("row_to_add"), |
112 | ! |
choices = NULL, |
113 | ! |
options = shinyWidgets::pickerOptions( |
114 | ! |
liveSearch = TRUE, |
115 | ! |
noneSelectedText = "Select gene variable" |
116 |
)
|
|
117 |
)
|
|
118 |
}
|
|
119 | ||
120 | 2x |
col_input <- if (ncol(SummarizedExperiment::colData(data)) == 0) { |
121 | 1x |
tags$div("no sample variables available") |
122 | 2x |
} else if (nrow(SummarizedExperiment::colData(data)) == 0) { |
123 | 1x |
tags$div("no samples available") |
124 |
} else { |
|
125 | ! |
teal.widgets::optionalSelectInput( |
126 | ! |
ns("col_to_add"), |
127 | ! |
choices = NULL, |
128 | ! |
options = shinyWidgets::pickerOptions( |
129 | ! |
liveSearch = TRUE, |
130 | ! |
noneSelectedText = "Select sample variable" |
131 |
)
|
|
132 |
)
|
|
133 |
}
|
|
134 | ||
135 | 2x |
tags$div( |
136 | 2x |
row_input,
|
137 | 2x |
col_input
|
138 |
)
|
|
139 |
},
|
|
140 | ||
141 |
#' @description
|
|
142 |
#' `shiny` server module to add filter variable.
|
|
143 |
#'
|
|
144 |
#' Module controls available choices to select as a filter variable.
|
|
145 |
#' Selected filter variable is being removed from available choices.
|
|
146 |
#' Removed filter variable gets back to available choices.
|
|
147 |
#' This module unlike other `FilterStates` classes manages two
|
|
148 |
#' sets of filter variables - one for `colData` and another for
|
|
149 |
#' `rowData`.
|
|
150 |
#'
|
|
151 |
#' @param id (`character(1)`)
|
|
152 |
#' `shiny` module instance id.
|
|
153 |
#' @return `NULL`
|
|
154 |
srv_add = function(id) { |
|
155 | 4x |
data <- private$data |
156 | 4x |
data_reactive <- private$data_reactive |
157 | 4x |
moduleServer( |
158 | 4x |
id = id, |
159 | 4x |
function(input, output, session) { |
160 | 4x |
logger::log_debug("SEFilterState$srv_add initializing, dataname: { private$dataname }") |
161 | ||
162 | 4x |
row_data <- SummarizedExperiment::rowData(data) |
163 | 4x |
col_data <- SummarizedExperiment::colData(data) |
164 | ||
165 | 4x |
avail_row_data_choices <- reactive({ |
166 | 4x |
slices_for_subset <- Filter(function(x) x$arg == "subset", self$get_filter_state()) |
167 | 4x |
active_filter_row_vars <- unique(unlist(lapply(slices_for_subset, "[[", "varname"))) |
168 | ||
169 | 4x |
choices <- setdiff( |
170 | 4x |
get_supported_filter_varnames(data = row_data), |
171 | 4x |
active_filter_row_vars
|
172 |
)
|
|
173 | ||
174 | 4x |
data_choices_labeled( |
175 | 4x |
data = row_data, |
176 | 4x |
choices = choices, |
177 | 4x |
varlabels = character(0), |
178 | 4x |
keys = NULL |
179 |
)
|
|
180 |
}) |
|
181 | ||
182 | 4x |
avail_col_data_choices <- reactive({ |
183 | 4x |
slices_for_select <- Filter(function(x) x$arg == "select", self$get_filter_state()) |
184 | 4x |
active_filter_col_vars <- unique(unlist(lapply(slices_for_select, "[[", "varname"))) |
185 | ||
186 | 4x |
choices <- setdiff( |
187 | 4x |
get_supported_filter_varnames(data = col_data), |
188 | 4x |
active_filter_col_vars
|
189 |
)
|
|
190 | ||
191 | 4x |
data_choices_labeled( |
192 | 4x |
data = col_data, |
193 | 4x |
choices = choices, |
194 | 4x |
varlabels = character(0), |
195 | 4x |
keys = NULL |
196 |
)
|
|
197 |
}) |
|
198 | ||
199 | 4x |
private$session_bindings[[session$ns("avail_row_data_choices")]] <- observeEvent( |
200 | 4x |
avail_row_data_choices(), |
201 | 4x |
ignoreNULL = TRUE, |
202 | 4x |
handlerExpr = { |
203 | 4x |
logger::log_debug( |
204 | 4x |
"SEFilterStates$srv_add@1 updating available row data choices,",
|
205 | 4x |
"dataname: { private$dataname }"
|
206 |
)
|
|
207 | 4x |
if (is.null(avail_row_data_choices())) { |
208 | ! |
shinyjs::hide("row_to_add") |
209 |
} else { |
|
210 | 4x |
shinyjs::show("row_to_add") |
211 |
}
|
|
212 | 4x |
teal.widgets::updateOptionalSelectInput( |
213 | 4x |
session,
|
214 | 4x |
"row_to_add",
|
215 | 4x |
choices = avail_row_data_choices() |
216 |
)
|
|
217 |
}
|
|
218 |
)
|
|
219 | ||
220 | 4x |
private$session_bindings[[session$ns("avail_col_data_choices")]] <- observeEvent( |
221 | 4x |
avail_col_data_choices(), |
222 | 4x |
ignoreNULL = TRUE, |
223 | 4x |
handlerExpr = { |
224 | 4x |
logger::log_debug( |
225 | 4x |
"SEFilterStates$srv_add@2 updating available col data choices,",
|
226 | 4x |
"dataname: { private$dataname }"
|
227 |
)
|
|
228 | 4x |
if (is.null(avail_col_data_choices())) { |
229 | ! |
shinyjs::hide("col_to_add") |
230 |
} else { |
|
231 | 4x |
shinyjs::show("col_to_add") |
232 |
}
|
|
233 | 4x |
teal.widgets::updateOptionalSelectInput( |
234 | 4x |
session,
|
235 | 4x |
"col_to_add",
|
236 | 4x |
choices = avail_col_data_choices() |
237 |
)
|
|
238 |
}
|
|
239 |
)
|
|
240 | ||
241 | 4x |
private$session_bindings[[session$ns("col_to_add")]] <- observeEvent( |
242 | 4x |
eventExpr = input$col_to_add, |
243 | 4x |
handlerExpr = { |
244 | ! |
logger::log_debug( |
245 | ! |
"SEFilterStates$srv_add@3 adding FilterState of column { input$col_to_add }",
|
246 | ! |
" to col data, dataname: { private$dataname }"
|
247 |
)
|
|
248 | ! |
varname <- input$col_to_add |
249 | ! |
self$set_filter_state(teal_slices( |
250 | ! |
teal_slice(private$dataname, varname, experiment = private$datalabel, arg = "select") |
251 |
)) |
|
252 |
}
|
|
253 |
)
|
|
254 | ||
255 | ||
256 | 4x |
private$session_bindings[[session$ns("row_to_add")]] <- observeEvent( |
257 | 4x |
eventExpr = input$row_to_add, |
258 | 4x |
handlerExpr = { |
259 | ! |
logger::log_debug( |
260 | ! |
"SEFilterStates$srv_add@4 adding FilterState of variable { input$row_to_add }",
|
261 | ! |
" to row data, dataname: { private$dataname }"
|
262 |
)
|
|
263 | ! |
varname <- input$row_to_add |
264 | ! |
self$set_filter_state(teal_slices( |
265 | ! |
teal_slice(private$dataname, varname, experiment = private$datalabel, arg = "subset") |
266 |
)) |
|
267 |
}
|
|
268 |
)
|
|
269 | ||
270 |
# Extra observer that clears all input values in session
|
|
271 | 4x |
private$session_bindings[[session$ns("inputs")]] <- list( |
272 | 4x |
destroy = function() { |
273 | ! |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
274 |
}
|
|
275 |
)
|
|
276 | ||
277 | 4x |
NULL
|
278 |
}
|
|
279 |
)
|
|
280 |
}
|
|
281 |
)
|
|
282 |
)
|
1 |
#' Progress bars with labels
|
|
2 |
#'
|
|
3 |
#' `shiny` element displaying a series of progress bars and observation counts.
|
|
4 |
#'
|
|
5 |
#' @param inputId (`character(1)`) `shiny` id of the parent element (e.g. a check-box group input).
|
|
6 |
#' @param choices (`vector`) Available values. Used to determine label text.
|
|
7 |
#' @param countsmax (`numeric`) Maximum counts of each element. Must be the same length `choices`.
|
|
8 |
#' @param countsnow (`numeric`) Current counts of each element. Must be the same length `choices`.
|
|
9 |
#' @param session (`session`) `shiny` `session` object passed to function given to `shinyServer`.
|
|
10 |
#'
|
|
11 |
#' @return List of `shiny.tag`s.
|
|
12 |
#'
|
|
13 |
#' Creates a number of progress bar elements, one for each value of `choices`.
|
|
14 |
#' The widths of all progress bars add up to the full width of the container.
|
|
15 |
#' Each progress bar has a text label that contains the name of the value and the number of counts.
|
|
16 |
#'
|
|
17 |
#' If the filter panel is used with `count_type = "all"`, the progress bars will be filled
|
|
18 |
#' according to the number of counts remaining in the current selection and the label will show
|
|
19 |
#' both the current and the total number of counts.
|
|
20 |
#'
|
|
21 |
#' Each child element can have a unique `id` attribute to be used independently.
|
|
22 |
#'
|
|
23 |
#' @examples
|
|
24 |
#' # use non-exported function from teal.slice
|
|
25 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice")
|
|
26 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice")
|
|
27 |
#' countBars <- getFromNamespace("countBars", "teal.slice")
|
|
28 |
#' updateCountBars <- getFromNamespace("updateCountBars", "teal.slice")
|
|
29 |
#'
|
|
30 |
#' library(shiny)
|
|
31 |
#'
|
|
32 |
#' choices <- sample(as.factor(c("a", "b", "c")), size = 20, replace = TRUE)
|
|
33 |
#' counts <- table(choices)
|
|
34 |
#' labels <- countBars(
|
|
35 |
#' inputId = "counts",
|
|
36 |
#' choices = c("a", "b", "c"),
|
|
37 |
#' countsmax = counts,
|
|
38 |
#' countsnow = unname(counts)
|
|
39 |
#' )
|
|
40 |
#'
|
|
41 |
#' ui <- fluidPage(
|
|
42 |
#' tags$div(
|
|
43 |
#' class = "choices_state",
|
|
44 |
#' include_js_files("count-bar-labels.js"),
|
|
45 |
#' include_css_files(pattern = "filter-panel"),
|
|
46 |
#' checkboxGroupInput(
|
|
47 |
#' inputId = "choices",
|
|
48 |
#' selected = levels(choices),
|
|
49 |
#' choiceNames = labels,
|
|
50 |
#' choiceValues = levels(choices),
|
|
51 |
#' label = NULL
|
|
52 |
#' )
|
|
53 |
#' )
|
|
54 |
#' )
|
|
55 |
#' server <- function(input, output, session) {
|
|
56 |
#' observeEvent(input$choices, {
|
|
57 |
#' new_counts <- counts
|
|
58 |
#' new_counts[!names(new_counts) %in% input$choices] <- 0
|
|
59 |
#' updateCountBars(
|
|
60 |
#' inputId = "counts",
|
|
61 |
#' choices = levels(choices),
|
|
62 |
#' countsmax = counts,
|
|
63 |
#' countsnow = unname(new_counts)
|
|
64 |
#' )
|
|
65 |
#' })
|
|
66 |
#' }
|
|
67 |
#'
|
|
68 |
#' if (interactive()) {
|
|
69 |
#' shinyApp(ui, server)
|
|
70 |
#' }
|
|
71 |
#'
|
|
72 |
#' @keywords internal
|
|
73 |
#'
|
|
74 |
countBars <- function(inputId, choices, countsmax, countsnow = NULL) { # nolint |
|
75 | 27x |
checkmate::assert_string(inputId) |
76 | 23x |
checkmate::assert_vector(choices) |
77 | 22x |
checkmate::assert_numeric(countsmax, len = length(choices)) |
78 | 19x |
checkmate::assert_numeric(countsnow, len = length(choices), null.ok = TRUE) |
79 | 17x |
if (!is.null(countsnow)) { |
80 | 9x |
checkmate::assert_true(all(countsnow <= countsmax)) |
81 |
}
|
|
82 | ||
83 | 16x |
ns <- NS(inputId) |
84 | ||
85 | 16x |
mapply( |
86 | 16x |
countBar,
|
87 | 16x |
inputId = ns(seq_along(choices)), |
88 | 16x |
label = as.character(choices), |
89 | 16x |
countmax = countsmax, |
90 | 16x |
countnow = if (is.null(countsnow)) rep(list(NULL), length(choices)) else countsnow, |
91 | 16x |
MoreArgs = list( |
92 | 16x |
counttotal = sum(countsmax) |
93 |
),
|
|
94 | 16x |
SIMPLIFY = FALSE, USE.NAMES = FALSE |
95 |
)
|
|
96 |
}
|
|
97 | ||
98 |
#' Progress bar with label
|
|
99 |
#'
|
|
100 |
#' `shiny` element displaying a progress bar and observation count.
|
|
101 |
#'
|
|
102 |
#' A progress bar is created to visualize the number of counts in a variable, with filling and a text label.
|
|
103 |
#' - progress bar width is derived as a fraction of the container width: `style = "width: <countmax> / <counttotal>%"`,
|
|
104 |
#' - progress bar is filled up to the fraction `<countnow> / <countmax>`,
|
|
105 |
#' - text label is obtained by `<label> (<countnow> / <countmax>)`.
|
|
106 |
#'
|
|
107 |
#' @param inputId (`character(1)`) `shiny` id of the parent element (e.g. a check-box group input).
|
|
108 |
#' @param label (`character(1)`) Text to display followed by counts.
|
|
109 |
#' @param countmax (`numeric(1)`) Maximum count for a single element.
|
|
110 |
#' @param countnow (`numeric(1)`) Current count for a single element.
|
|
111 |
#' @param counttotal (`numeric(1)`) Sum total of maximum counts of all elements, see `Details`.
|
|
112 |
#' @param session (`session`) `shiny` `session` object passed to function given to `shinyServer`.
|
|
113 |
#'
|
|
114 |
#' @return `shiny.tag` object with a progress bar and a label.
|
|
115 |
#'
|
|
116 |
#' @keywords internal
|
|
117 |
#'
|
|
118 |
countBar <- function(inputId, label, countmax, countnow = NULL, counttotal = countmax) { # nolint |
|
119 | 70x |
checkmate::assert_string(inputId) |
120 | 66x |
checkmate::assert_string(label) |
121 | 63x |
checkmate::assert_number(countmax) |
122 | 61x |
checkmate::assert_number(countnow, null.ok = TRUE, upper = countmax) |
123 | 59x |
checkmate::assert_number(counttotal, lower = countmax) |
124 | ||
125 | 57x |
label <- make_count_text(label, countmax = countmax, countnow = countnow) |
126 | 57x |
ns <- NS(inputId) |
127 | 26x |
if (is.null(countnow)) countnow <- 0 |
128 | 57x |
tags$div( |
129 | 57x |
class = "progress state-count-container", |
130 |
# * .9 to not exceed width of the parent html element
|
|
131 | 57x |
tags$div( |
132 | 57x |
id = ns("count_bar_filtered"), |
133 | 57x |
class = "progress-bar state-count-bar-filtered", |
134 | 57x |
style = sprintf("width: %s%%", countnow / counttotal * 100), |
135 | 57x |
role = "progressbar", |
136 | 57x |
label
|
137 |
),
|
|
138 | 57x |
tags$div( |
139 | 57x |
id = ns("count_bar_unfiltered"), |
140 | 57x |
class = "progress-bar state-count-bar-unfiltered", |
141 | 57x |
style = sprintf("width: %s%%", (countmax - countnow) / counttotal * 100), |
142 | 57x |
role = "progressbar" |
143 |
)
|
|
144 |
)
|
|
145 |
}
|
|
146 | ||
147 |
#' @rdname countBars
|
|
148 |
updateCountBars <- function(session = getDefaultReactiveDomain(), inputId, choices, countsmax, countsnow = NULL) { # nolint |
|
149 | 7x |
checkmate::assert_string(inputId) |
150 | 7x |
checkmate::assert_vector(choices) |
151 | 7x |
checkmate::assert_numeric(countsmax, len = length(choices)) |
152 | 7x |
checkmate::assert_numeric(countsnow, len = length(choices), null.ok = TRUE) |
153 | ||
154 | 7x |
ns <- NS(inputId) |
155 | 7x |
mapply( |
156 | 7x |
updateCountBar,
|
157 | 7x |
inputId = ns(seq_along(choices)), |
158 | 7x |
label = choices, |
159 | 7x |
countmax = countsmax, |
160 | 7x |
countnow = if (is.null(countsnow)) rep(list(NULL), length(choices)) else countsnow, |
161 | 7x |
MoreArgs = list( |
162 | 7x |
counttotal = sum(countsmax) |
163 |
)
|
|
164 |
)
|
|
165 | 7x |
invisible(NULL) |
166 |
}
|
|
167 | ||
168 |
#' @rdname countBar
|
|
169 |
updateCountBar <- function(session = getDefaultReactiveDomain(), inputId, label, countmax, countnow = NULL, counttotal) { # nolint |
|
170 | 18x |
checkmate::assert_string(inputId) |
171 | 18x |
checkmate::assert_string(label) |
172 | 18x |
checkmate::assert_number(countmax) |
173 | 18x |
checkmate::assert_number(countnow, null.ok = TRUE) |
174 | 18x |
checkmate::assert_number(counttotal) |
175 | ||
176 | 18x |
label <- make_count_text(label, countmax = countmax, countnow = countnow) |
177 | 18x |
if (is.null(countnow)) countnow <- countmax |
178 | 18x |
session$sendCustomMessage( |
179 | 18x |
type = "updateCountBar", |
180 | 18x |
message = list( |
181 | 18x |
id = session$ns(inputId), |
182 | 18x |
label = label, |
183 | 18x |
countmax = countmax, |
184 | 18x |
countnow = countnow, |
185 | 18x |
counttotal = counttotal |
186 |
)
|
|
187 |
)
|
|
188 | ||
189 | 18x |
invisible(NULL) |
190 |
}
|
|
191 | ||
192 |
#' @rdname countBar
|
|
193 |
updateCountText <- function(session = getDefaultReactiveDomain(), inputId, label, countmax, countnow) { # nolint |
|
194 | 17x |
checkmate::assert_string(inputId) |
195 | 17x |
checkmate::assert_string(label) |
196 | 17x |
checkmate::assert_number(countmax) |
197 | 17x |
checkmate::assert_number(countnow, null.ok = TRUE) |
198 | 17x |
label <- make_count_text(label, countmax = countmax, countnow = countnow) |
199 | 17x |
session$sendCustomMessage( |
200 | 17x |
type = "updateCountText", |
201 | 17x |
message = list( |
202 | 17x |
id = session$ns(inputId), |
203 | 17x |
label = label |
204 |
)
|
|
205 |
)
|
|
206 |
}
|
|
207 | ||
208 |
#' Build count text
|
|
209 |
#'
|
|
210 |
#' Returns a text label describing filtered counts. The text is composed in the following way:
|
|
211 |
#' - when `countnow` is not `NULL`: `<label> (<countnow>/<countmax>)`
|
|
212 |
#' - when `countnow` is `NULL`: `<label> (<countmax>)`
|
|
213 |
#'
|
|
214 |
#' @param label (`character(1)`) Text displayed before counts.
|
|
215 |
#' @param countnow (`numeric(1)`) Number of filtered counts.
|
|
216 |
#' @param countmax (`numeric(1)`) Number of unfiltered counts.
|
|
217 |
#'
|
|
218 |
#' @return A character string.
|
|
219 |
#'
|
|
220 |
#' @keywords internal
|
|
221 |
#'
|
|
222 |
make_count_text <- function(label, countmax, countnow = NULL) { |
|
223 | 104x |
checkmate::assert_string(label) |
224 | 102x |
checkmate::assert_number(countmax) |
225 | 100x |
checkmate::assert_number(countnow, null.ok = TRUE) |
226 | 98x |
sprintf( |
227 | 98x |
"%s (%s%s)",
|
228 | 98x |
label,
|
229 | 98x |
if (is.null(countnow)) "" else sprintf("%s/", countnow), |
230 | 98x |
countmax
|
231 |
)
|
|
232 |
}
|
|
233 | ||
234 | ||
235 |
#' Adjust counts to match choices
|
|
236 |
#'
|
|
237 |
#' @param choices (`character`) Choices to match.
|
|
238 |
#' @param counts (`named numeric`) Counts to adjust.
|
|
239 |
#' @keywords internal
|
|
240 |
pair_counts <- function(choices, counts) { |
|
241 | 160x |
checkmate::assert_numeric(counts) |
242 | 160x |
counts <- counts[match(choices, names(counts))] |
243 | 160x |
counts[is.na(counts)] <- 0 |
244 | 160x |
names(counts) <- choices |
245 | 160x |
counts
|
246 |
}
|
1 |
#' Include `JS` files from `/inst/js/` 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, passed to `system.file`
|
|
8 |
#'
|
|
9 |
#' @return HTML code that includes `JS` files
|
|
10 |
#' @keywords internal
|
|
11 |
include_js_files <- function(pattern) { |
|
12 | 12x |
checkmate::assert_character(pattern, min.len = 1, null.ok = TRUE) |
13 | 12x |
js_files <- list.files( |
14 | 12x |
system.file("js", package = "teal.slice", mustWork = TRUE), |
15 | 12x |
pattern = pattern, |
16 | 12x |
full.names = TRUE |
17 |
)
|
|
18 | 12x |
singleton(lapply(js_files, includeScript)) |
19 |
}
|
|
20 | ||
21 |
#' Build concatenating call
|
|
22 |
#'
|
|
23 |
#' This function takes a vector of values and returns a `c` call. If the vector
|
|
24 |
#' has only one element, the element is returned directly.
|
|
25 |
#'
|
|
26 |
#' @param choices A vector of values.
|
|
27 |
#'
|
|
28 |
#' @return A `c` call.
|
|
29 |
#'
|
|
30 |
#' @examples
|
|
31 |
#' # use non-exported function from teal.slice
|
|
32 |
#' make_c_call <- getFromNamespace("make_c_call", "teal.slice")
|
|
33 |
#' make_c_call(1:3)
|
|
34 |
#' make_c_call(1)
|
|
35 |
#'
|
|
36 |
#' @keywords internal
|
|
37 |
make_c_call <- function(choices) { |
|
38 | 55x |
if (length(choices) > 1) { |
39 | 27x |
do.call("call", append(list("c"), choices)) |
40 |
} else { |
|
41 | 28x |
choices
|
42 |
}
|
|
43 |
}
|
|
44 | ||
45 |
#' Destroys inputs and observers stored in `private$session_bindings`
|
|
46 |
#'
|
|
47 |
#' @description
|
|
48 |
#' Call a `destroy` method to remove `observer` and `input` from obsolete session which happens
|
|
49 |
#' when `filter_panel_srv` is called again in new `FilteredData` object.
|
|
50 |
#' Inputs are not stored directly in a field as they don't have `destroy` method. Instead, we
|
|
51 |
#' store callback `destroy` function for inputs which removes bindings from a `session`.
|
|
52 |
#' @param self,private slots of a `R6` class
|
|
53 |
#' @return `NULL` invisibly
|
|
54 |
#' @keywords internal
|
|
55 |
.finalize_session_bindings <- function(self, private) { |
|
56 |
# Only finalize shiny session binding when there is an active session
|
|
57 |
if ( |
|
58 | 1306x |
!is.null(getDefaultReactiveDomain()) && |
59 | 1306x |
!getDefaultReactiveDomain()$isEnded() |
60 |
) { |
|
61 | 140x |
lapply(private$session_bindings, function(x) x$destroy()) |
62 |
}
|
|
63 | 1306x |
invisible(NULL) |
64 |
}
|
|
65 | ||
66 | ||
67 | ||
68 |
#' Encodes ids to be used in JavaScript and Shiny
|
|
69 |
#'
|
|
70 |
#' @description
|
|
71 |
#' Replaces non-ASCII characters into a format that can be used in HTML,
|
|
72 |
#' JavaScript and Shiny.
|
|
73 |
#'
|
|
74 |
#' When the id has a character that is not allowed, it is replaced with `"_"`
|
|
75 |
#' and a 4 character hash of the original id is added to the start of the
|
|
76 |
#' resulting id.
|
|
77 |
#'
|
|
78 |
#'
|
|
79 |
#' @param id (`character(1)`) The id string.
|
|
80 |
#'
|
|
81 |
#' @return Sanitized string that removes special characters and spaces.
|
|
82 |
#'
|
|
83 |
#' @keywords internal
|
|
84 |
sanitize_id <- function(id) { |
|
85 | 392x |
pattern_escape <- "[^0-9A-Za-z_]" |
86 | ||
87 | 392x |
id_new <- gsub(pattern_escape, "_", id, perl = TRUE) |
88 | 392x |
hashes <- vapply(id[id != id_new], rlang::hash, character(1), USE.NAMES = FALSE) |
89 | ||
90 | 392x |
id[id != id_new] <- paste0("h", substr(hashes, 1, 4), "_", id_new[id != id_new]) |
91 | 392x |
id
|
92 |
}
|
|
93 | ||
94 |
#' `NS` wrapper to sanitize ids for shiny
|
|
95 |
#'
|
|
96 |
#' Special characters and spaces are not allowed in shiny ids (in JS)
|
|
97 |
#'
|
|
98 |
#' @noRd
|
|
99 |
NS <- function(namespace, id = NULL) { # nolint: object_name. |
|
100 | 148x |
if (!missing(id)) { |
101 | 3x |
return(shiny::NS(namespace, sanitize_id(id))) |
102 |
}
|
|
103 | ||
104 | 145x |
function(id) { |
105 | 282x |
shiny::NS(namespace, sanitize_id(id)) |
106 |
}
|
|
107 |
}
|
|
108 | ||
109 |
#' `moduleServer` wrapper to sanitize ids for shiny
|
|
110 |
#'
|
|
111 |
#' Special characters and spaces are not allowed in shiny ids (in JS)
|
|
112 |
#'
|
|
113 |
#' @noRd
|
|
114 |
moduleServer <- function(id, module, session = getDefaultReactiveDomain()) { # nolint: object_name. |
|
115 | 107x |
id <- sanitize_id(id) |
116 | 107x |
shiny::moduleServer(id, module, session) |
117 |
}
|
1 |
#' Get classes of selected columns from dataset
|
|
2 |
#'
|
|
3 |
#' @param data (`data.frame` or `DataFrame` or `matrix`) Object in which to determine variable types.
|
|
4 |
#' @param columns (`character`) Vector of columns in `data` for which to get types.
|
|
5 |
#' Set to `NULL` to get types of all columns.
|
|
6 |
#'
|
|
7 |
#' @return Character vector of classes of `columns` from provided `data`.
|
|
8 |
#'
|
|
9 |
#' @examples
|
|
10 |
#' # use non-exported function from teal.slice
|
|
11 |
#' variable_types <- getFromNamespace("variable_types", "teal.slice")
|
|
12 |
#'
|
|
13 |
#' variable_types(
|
|
14 |
#' data.frame(
|
|
15 |
#' x = 1:3, y = factor(c("a", "b", "a")), z = c("h1", "h2", "h3"),
|
|
16 |
#' stringsAsFactors = FALSE
|
|
17 |
#' ),
|
|
18 |
#' "x"
|
|
19 |
#' )
|
|
20 |
#'
|
|
21 |
#' variable_types(
|
|
22 |
#' data.frame(
|
|
23 |
#' x = 1:3, y = factor(c("a", "b", "a")), z = c("h1", "h2", "h3"),
|
|
24 |
#' stringsAsFactors = FALSE
|
|
25 |
#' ),
|
|
26 |
#' c("x", "z")
|
|
27 |
#' )
|
|
28 |
#'
|
|
29 |
#' variable_types(
|
|
30 |
#' data.frame(
|
|
31 |
#' x = 1:3, y = factor(c("a", "b", "a")), z = c("h1", "h2", "h3"),
|
|
32 |
#' stringsAsFactors = FALSE
|
|
33 |
#' )
|
|
34 |
#' )
|
|
35 |
#'
|
|
36 |
#' @keywords internal
|
|
37 |
#'
|
|
38 |
variable_types <- function(data, columns = NULL) { |
|
39 | 19x |
checkmate::assert_multi_class(data, c("data.frame", "DataFrame", "matrix")) |
40 | 19x |
checkmate::assert_character(columns, any.missing = FALSE, null.ok = TRUE) |
41 | 19x |
checkmate::assert_subset(columns, colnames(data)) |
42 | ||
43 | 19x |
if (is.matrix(data)) { |
44 | 1x |
type <- typeof(data) |
45 | 1x |
if (type == "double") type <- "numeric" |
46 | 1x |
types <- |
47 | 1x |
if (is.null(columns)) { |
48 | ! |
stats::setNames(rep_len(type, ncol(data)), nm = colnames(data)) |
49 |
} else { |
|
50 | 1x |
stats::setNames(rep_len(type, length(columns)), nm = columns) |
51 |
}
|
|
52 |
} else { |
|
53 | 18x |
types <- vapply(data, function(x) class(x)[1L], character(1L)) |
54 | 18x |
if (!is.null(columns)) types <- types[columns] |
55 |
# alternative after R 4.4.0: `types <- types[columns %||% TRUE]`
|
|
56 |
}
|
|
57 | 19x |
types
|
58 |
}
|
1 |
# MAEFilteredDataset ------
|
|
2 | ||
3 |
#' @name MAEFilteredDataset
|
|
4 |
#' @docType class
|
|
5 |
#' @title `MAEFilteredDataset` `R6` class
|
|
6 |
#'
|
|
7 |
#' @examplesIf requireNamespace("MultiAssayExperiment")
|
|
8 |
#' # use non-exported function from teal.slice
|
|
9 |
#' MAEFilteredDataset <- getFromNamespace("MAEFilteredDataset", "teal.slice")
|
|
10 |
#'
|
|
11 |
#' data(miniACC, package = "MultiAssayExperiment")
|
|
12 |
#' dataset <- MAEFilteredDataset$new(miniACC, "MAE")
|
|
13 |
#' fs <- teal_slices(
|
|
14 |
#' teal_slice(
|
|
15 |
#' dataname = "MAE", varname = "years_to_birth", selected = c(30, 50), keep_na = TRUE
|
|
16 |
#' ),
|
|
17 |
#' teal_slice(
|
|
18 |
#' dataname = "MAE", varname = "vital_status", selected = "1", keep_na = FALSE
|
|
19 |
#' ),
|
|
20 |
#' teal_slice(
|
|
21 |
#' dataname = "MAE", varname = "gender", selected = "female", keep_na = TRUE
|
|
22 |
#' ),
|
|
23 |
#' teal_slice(
|
|
24 |
#' dataname = "MAE", varname = "ARRAY_TYPE", selected = "", keep_na = TRUE
|
|
25 |
#' )
|
|
26 |
#' )
|
|
27 |
#' dataset$set_filter_state(state = fs)
|
|
28 |
#'
|
|
29 |
#' library(shiny)
|
|
30 |
#' isolate(dataset$get_filter_state())
|
|
31 |
#'
|
|
32 |
#' @keywords internal
|
|
33 |
#'
|
|
34 |
MAEFilteredDataset <- R6::R6Class( # nolint |
|
35 |
classname = "MAEFilteredDataset", |
|
36 |
inherit = FilteredDataset, |
|
37 | ||
38 |
# public methods ----
|
|
39 |
public = list( |
|
40 |
#' @description
|
|
41 |
#' Initialize `MAEFilteredDataset` object.
|
|
42 |
#'
|
|
43 |
#' @param dataset (`MulitiAssayExperiment`)
|
|
44 |
#' single `MulitiAssayExperiment` for which filters are rendered.
|
|
45 |
#' @param dataname (`character(1)`)
|
|
46 |
#' syntactically valid name given to the dataset.
|
|
47 |
#' @param keys (`character`) optional
|
|
48 |
#' vector of primary key column names.
|
|
49 |
#' @param label (`character(1)`)
|
|
50 |
#' label to describe the dataset.
|
|
51 |
#'
|
|
52 |
#' @return Object of class `MAEFilteredDataset`, invisibly.
|
|
53 |
#'
|
|
54 |
initialize = function(dataset, dataname, keys = character(0), label = character(0)) { |
|
55 | 21x |
if (!requireNamespace("MultiAssayExperiment", quietly = TRUE)) { |
56 | ! |
stop("Cannot load MultiAssayExperiment - please install the package or restart your session.") |
57 |
}
|
|
58 | 21x |
checkmate::assert_class(dataset, "MultiAssayExperiment") |
59 | 19x |
super$initialize(dataset, dataname, keys, label) |
60 | 19x |
experiment_names <- names(dataset) |
61 | ||
62 |
# subsetting by subjects means subsetting by colData(MAE)
|
|
63 | 19x |
private$add_filter_states( |
64 | 19x |
filter_states = init_filter_states( |
65 | 19x |
data = dataset, |
66 | 19x |
data_reactive = private$data_filtered_fun, |
67 | 19x |
dataname = dataname, |
68 | 19x |
datalabel = "subjects", |
69 | 19x |
keys = self$get_keys() |
70 |
),
|
|
71 | 19x |
id = "subjects" |
72 |
)
|
|
73 |
# elements of the list (experiments) are unknown
|
|
74 |
# dispatch needed because we can't hardcode methods otherwise:
|
|
75 |
# if (matrix) else if (SummarizedExperiment) else if ...
|
|
76 | 19x |
lapply( |
77 | 19x |
experiment_names,
|
78 | 19x |
function(experiment_name) { |
79 | 95x |
data_reactive <- function(sid = "") private$data_filtered_fun(sid)[[experiment_name]] |
80 | 95x |
private$add_filter_states( |
81 | 95x |
filter_states = init_filter_states( |
82 | 95x |
data = dataset[[experiment_name]], |
83 | 95x |
data_reactive = data_reactive, |
84 | 95x |
dataname = dataname, |
85 | 95x |
datalabel = experiment_name |
86 |
),
|
|
87 | 95x |
id = experiment_name |
88 |
)
|
|
89 |
}
|
|
90 |
)
|
|
91 |
},
|
|
92 | ||
93 |
#' @description
|
|
94 |
#' Set filter state.
|
|
95 |
#'
|
|
96 |
#' @param state (`teal_slices`)
|
|
97 |
#' @return `NULL`, invisibly.
|
|
98 |
#'
|
|
99 |
set_filter_state = function(state) { |
|
100 | 13x |
isolate({ |
101 | 13x |
logger::log_debug("FilteredDatasetMAE$set_filter_state initializing, dataname: { private$dataname }") |
102 | 13x |
checkmate::assert_class(state, "teal_slices") |
103 | 12x |
lapply(state, function(x) { |
104 | 48x |
checkmate::assert_true(x$dataname == private$dataname, .var.name = "dataname matches private$dataname") |
105 |
}) |
|
106 | ||
107 |
# set state on subjects
|
|
108 | 12x |
subject_state <- Filter(function(x) is.null(x$experiment), state) |
109 | 12x |
private$get_filter_states()[["subjects"]]$set_filter_state(subject_state) |
110 | ||
111 |
# set state on experiments
|
|
112 |
# determine target experiments (defined in teal_slices)
|
|
113 | 12x |
experiments <- unique(unlist(lapply(state, "[[", "experiment"))) |
114 | 12x |
available_experiments <- setdiff(names(private$get_filter_states()), "subjects") |
115 | 12x |
excluded_filters <- setdiff(experiments, available_experiments) |
116 | 12x |
if (length(excluded_filters)) { |
117 | ! |
stop(sprintf( |
118 | ! |
"%s doesn't contain elements specified in 'experiment': %s\n'experiment' should be a subset of: %s",
|
119 | ! |
private$dataname, |
120 | ! |
toString(excluded_filters), |
121 | ! |
toString(available_experiments) |
122 |
)) |
|
123 |
}
|
|
124 | ||
125 |
# set states on state_lists with corresponding experiments
|
|
126 | 12x |
lapply(available_experiments, function(experiment) { |
127 | 60x |
slices <- Filter(function(x) identical(x$experiment, experiment), state) |
128 | 60x |
private$get_filter_states()[[experiment]]$set_filter_state(slices) |
129 |
}) |
|
130 | ||
131 | 12x |
invisible(NULL) |
132 |
}) |
|
133 |
},
|
|
134 | ||
135 |
#' @description
|
|
136 |
#' Remove one or more `FilterState` of a `MAEFilteredDataset`.
|
|
137 |
#'
|
|
138 |
#' @param state (`teal_slices`)
|
|
139 |
#' specifying `FilterState` objects to remove;
|
|
140 |
#' `teal_slice`s may contain only `dataname` and `varname`, other elements are ignored.
|
|
141 |
#'
|
|
142 |
#' @return `NULL`, invisibly.
|
|
143 |
#'
|
|
144 |
remove_filter_state = function(state) { |
|
145 | 1x |
checkmate::assert_class(state, "teal_slices") |
146 | ||
147 | 1x |
isolate({ |
148 | 1x |
logger::log_debug("FilteredDatasetMAE$remove_filter_state removing filter(s), dataname: { private$dataname }") |
149 |
# remove state on subjects
|
|
150 | 1x |
subject_state <- Filter(function(x) is.null(x$experiment), state) |
151 | 1x |
private$get_filter_states()[["subjects"]]$remove_filter_state(subject_state) |
152 | ||
153 |
# remove state on experiments
|
|
154 |
# determine target experiments (defined in teal_slices)
|
|
155 | 1x |
experiments <- unique(unlist(lapply(state, "[[", "experiment"))) |
156 | 1x |
available_experiments <- setdiff(names(private$get_filter_states()), "subjects") |
157 | 1x |
excluded_filters <- setdiff(experiments, available_experiments) |
158 | 1x |
if (length(excluded_filters)) { |
159 | ! |
stop(sprintf( |
160 | ! |
"%s doesn't contain elements specified in 'experiment': %s\n'experiment' should be a subset of: %s",
|
161 | ! |
private$dataname, |
162 | ! |
toString(excluded_filters), |
163 | ! |
toString(available_experiments) |
164 |
)) |
|
165 |
}
|
|
166 |
# remove states on state_lists with corresponding experiments
|
|
167 | 1x |
lapply(experiments, function(experiment) { |
168 | ! |
slices <- Filter(function(x) identical(x$experiment, experiment), state) |
169 | ! |
private$get_filter_states()[[experiment]]$remove_filter_state(slices) |
170 |
}) |
|
171 |
}) |
|
172 | ||
173 | 1x |
invisible(NULL) |
174 |
},
|
|
175 | ||
176 |
#' @description
|
|
177 |
#' UI module to add filter variable for this dataset.
|
|
178 |
#' @param id (`character(1)`)
|
|
179 |
#' `shiny` module instance id.
|
|
180 |
#'
|
|
181 |
#' @return `shiny.tag`
|
|
182 |
#'
|
|
183 |
ui_add = function(id) { |
|
184 | ! |
ns <- NS(id) |
185 | ! |
data <- self$get_dataset() |
186 | ! |
experiment_names <- names(data) |
187 | ||
188 | ! |
tags$div( |
189 | ! |
tags$label("Add", tags$code(self$get_dataname()), "filter"), |
190 | ! |
tags$br(), |
191 | ! |
HTML("►"), |
192 | ! |
tags$label("Add subjects filter"), |
193 | ! |
private$get_filter_states()[["subjects"]]$ui_add(id = ns("subjects")), |
194 | ! |
tagList( |
195 | ! |
lapply( |
196 | ! |
experiment_names,
|
197 | ! |
function(experiment_name) { |
198 | ! |
tagList( |
199 | ! |
HTML("►"), |
200 | ! |
tags$label("Add", tags$code(experiment_name), "filter"), |
201 | ! |
private$get_filter_states()[[experiment_name]]$ui_add(id = ns(experiment_name)) |
202 |
)
|
|
203 |
}
|
|
204 |
)
|
|
205 |
)
|
|
206 |
)
|
|
207 |
},
|
|
208 | ||
209 |
#' @description
|
|
210 |
#' Creates row for filter overview in the form of \cr
|
|
211 |
#' `dataname -- observations (remaining/total) -- subjects (remaining/total)` - MAE
|
|
212 |
#' @return A `data.frame`.
|
|
213 |
get_filter_overview = function() { |
|
214 | 2x |
data <- self$get_dataset() |
215 | 2x |
data_filtered <- self$get_dataset(TRUE) |
216 | 2x |
experiment_names <- names(data) |
217 | ||
218 | 2x |
mae_info <- data.frame( |
219 | 2x |
dataname = private$dataname, |
220 | 2x |
subjects = nrow(SummarizedExperiment::colData(data)), |
221 | 2x |
subjects_filtered = nrow(SummarizedExperiment::colData(data_filtered())) |
222 |
)
|
|
223 | ||
224 | 2x |
experiment_obs_info <- do.call("rbind", lapply( |
225 | 2x |
experiment_names,
|
226 | 2x |
function(experiment_name) { |
227 | 10x |
data.frame( |
228 | 10x |
dataname = sprintf("- %s", experiment_name), |
229 | 10x |
obs = nrow(data[[experiment_name]]), |
230 | 10x |
obs_filtered = nrow(data_filtered()[[experiment_name]]) |
231 |
)
|
|
232 |
}
|
|
233 |
)) |
|
234 | ||
235 | 2x |
get_experiment_keys <- function(mae, experiment) { |
236 | 20x |
sample_subset <- subset(MultiAssayExperiment::sampleMap(mae), colname %in% colnames(experiment)) |
237 | 20x |
length(unique(sample_subset$primary)) |
238 |
}
|
|
239 | ||
240 | 2x |
experiment_subjects_info <- do.call("rbind", lapply( |
241 | 2x |
experiment_names,
|
242 | 2x |
function(experiment_name) { |
243 | 10x |
data.frame( |
244 | 10x |
subjects = get_experiment_keys(data, data[[experiment_name]]), |
245 | 10x |
subjects_filtered = get_experiment_keys(data_filtered(), data_filtered()[[experiment_name]]) |
246 |
)
|
|
247 |
}
|
|
248 |
)) |
|
249 | ||
250 | 2x |
experiment_info <- cbind(experiment_obs_info, experiment_subjects_info) |
251 | 2x |
dplyr::bind_rows(mae_info, experiment_info) |
252 |
}
|
|
253 |
)
|
|
254 |
)
|
1 |
# ChoicesFilterState ------
|
|
2 | ||
3 |
#' @name ChoicesFilterState
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title `FilterState` object for categorical data
|
|
7 |
#'
|
|
8 |
#' @description Manages choosing elements from a set.
|
|
9 |
#'
|
|
10 |
#' @examples
|
|
11 |
#' # use non-exported function from teal.slice
|
|
12 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice")
|
|
13 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice")
|
|
14 |
#' ChoicesFilterState <- getFromNamespace("ChoicesFilterState", "teal.slice")
|
|
15 |
#'
|
|
16 |
#' library(shiny)
|
|
17 |
#'
|
|
18 |
#' filter_state <- ChoicesFilterState$new(
|
|
19 |
#' x = c(LETTERS, NA),
|
|
20 |
#' slice = teal_slice(varname = "var", dataname = "data")
|
|
21 |
#' )
|
|
22 |
#' isolate(filter_state$get_call())
|
|
23 |
#' filter_state$set_state(
|
|
24 |
#' teal_slice(
|
|
25 |
#' dataname = "data",
|
|
26 |
#' varname = "var",
|
|
27 |
#' selected = "A",
|
|
28 |
#' keep_na = TRUE
|
|
29 |
#' )
|
|
30 |
#' )
|
|
31 |
#' isolate(filter_state$get_call())
|
|
32 |
#'
|
|
33 |
#' # working filter in an app
|
|
34 |
#' library(shinyjs)
|
|
35 |
#'
|
|
36 |
#' data_choices <- c(sample(letters[1:4], 100, replace = TRUE), NA)
|
|
37 |
#' attr(data_choices, "label") <- "lowercase letters"
|
|
38 |
#' fs <- ChoicesFilterState$new(
|
|
39 |
#' x = data_choices,
|
|
40 |
#' slice = teal_slice(
|
|
41 |
#' dataname = "data", varname = "variable", selected = c("a", "c"), keep_na = TRUE
|
|
42 |
#' )
|
|
43 |
#' )
|
|
44 |
#'
|
|
45 |
#' ui <- fluidPage(
|
|
46 |
#' useShinyjs(),
|
|
47 |
#' include_css_files(pattern = "filter-panel"),
|
|
48 |
#' include_js_files(pattern = "count-bar-labels"),
|
|
49 |
#' column(4, tags$div(
|
|
50 |
#' tags$h4("ChoicesFilterState"),
|
|
51 |
#' fs$ui("fs")
|
|
52 |
#' )),
|
|
53 |
#' column(4, tags$div(
|
|
54 |
#' tags$h4("Condition (i.e. call)"), # display the condition call generated by this FilterState
|
|
55 |
#' textOutput("condition_choices"), tags$br(),
|
|
56 |
#' tags$h4("Unformatted state"), # display raw filter state
|
|
57 |
#' textOutput("unformatted_choices"), tags$br(),
|
|
58 |
#' tags$h4("Formatted state"), # display human readable filter state
|
|
59 |
#' textOutput("formatted_choices"), tags$br()
|
|
60 |
#' )),
|
|
61 |
#' column(4, tags$div(
|
|
62 |
#' tags$h4("Programmatic filter control"),
|
|
63 |
#' actionButton("button1_choices", "set drop NA", width = "100%"), tags$br(),
|
|
64 |
#' actionButton("button2_choices", "set keep NA", width = "100%"), tags$br(),
|
|
65 |
#' actionButton("button3_choices", "set selection: a, b", width = "100%"), tags$br(),
|
|
66 |
#' actionButton("button4_choices", "deselect all", width = "100%"), tags$br(),
|
|
67 |
#' actionButton("button0_choices", "set initial state", width = "100%"), tags$br()
|
|
68 |
#' ))
|
|
69 |
#' )
|
|
70 |
#'
|
|
71 |
#' server <- function(input, output, session) {
|
|
72 |
#' fs$server("fs")
|
|
73 |
#' output$condition_choices <- renderPrint(fs$get_call())
|
|
74 |
#' output$formatted_choices <- renderText(fs$format())
|
|
75 |
#' output$unformatted_choices <- renderPrint(fs$get_state())
|
|
76 |
#' # modify filter state programmatically
|
|
77 |
#' observeEvent(
|
|
78 |
#' input$button1_choices,
|
|
79 |
#' fs$set_state(
|
|
80 |
#' teal_slice(dataname = "data", varname = "variable", keep_na = FALSE)
|
|
81 |
#' )
|
|
82 |
#' )
|
|
83 |
#' observeEvent(
|
|
84 |
#' input$button2_choices,
|
|
85 |
#' fs$set_state(
|
|
86 |
#' teal_slice(dataname = "data", varname = "variable", keep_na = TRUE)
|
|
87 |
#' )
|
|
88 |
#' )
|
|
89 |
#' observeEvent(
|
|
90 |
#' input$button3_choices,
|
|
91 |
#' fs$set_state(
|
|
92 |
#' teal_slice(dataname = "data", varname = "variable", selected = c("a", "b"))
|
|
93 |
#' )
|
|
94 |
#' )
|
|
95 |
#' observeEvent(
|
|
96 |
#' input$button4_choices,
|
|
97 |
#' fs$set_state(
|
|
98 |
#' teal_slice(dataname = "data", varname = "variable", selected = character(0), keep_na = TRUE)
|
|
99 |
#' )
|
|
100 |
#' )
|
|
101 |
#' observeEvent(
|
|
102 |
#' input$button0_choices,
|
|
103 |
#' fs$set_state(
|
|
104 |
#' teal_slice(dataname = "data", varname = "variable", selected = c("a", "c"), keep_na = TRUE)
|
|
105 |
#' )
|
|
106 |
#' )
|
|
107 |
#' }
|
|
108 |
#'
|
|
109 |
#' if (interactive()) {
|
|
110 |
#' shinyApp(ui, server)
|
|
111 |
#' }
|
|
112 |
#'
|
|
113 |
#' @keywords internal
|
|
114 |
#'
|
|
115 |
ChoicesFilterState <- R6::R6Class( # nolint |
|
116 |
"ChoicesFilterState",
|
|
117 |
inherit = FilterState, |
|
118 | ||
119 |
# public methods ----
|
|
120 | ||
121 |
public = list( |
|
122 | ||
123 |
#' @description
|
|
124 |
#' Initialize a `FilterState` object.
|
|
125 |
#'
|
|
126 |
#' @param x (`character`)
|
|
127 |
#' variable to be filtered.
|
|
128 |
#' @param x_reactive (`reactive`)
|
|
129 |
#' returning vector of the same type as `x`. Is used to update
|
|
130 |
#' counts following the change in values of the filtered dataset.
|
|
131 |
#' If it is set to `reactive(NULL)` then counts based on filtered
|
|
132 |
#' dataset are not shown.
|
|
133 |
#' @param slice (`teal_slice`)
|
|
134 |
#' specification of this filter state.
|
|
135 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`.
|
|
136 |
#' `get_state` returns `teal_slice` object which can be reused in other places.
|
|
137 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e.
|
|
138 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`.
|
|
139 |
#' @param extract_type (`character`)
|
|
140 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values:
|
|
141 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed
|
|
142 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>`
|
|
143 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]`
|
|
144 |
#'
|
|
145 |
#' @return Object of class `ChoicesFilterState`, invisibly.
|
|
146 |
#'
|
|
147 |
initialize = function(x, |
|
148 |
x_reactive = reactive(NULL), |
|
149 |
slice,
|
|
150 |
extract_type = character(0)) { |
|
151 | 158x |
isolate({ |
152 | 158x |
checkmate::assert( |
153 | 158x |
is.character(x), |
154 | 158x |
is.factor(x), |
155 | 158x |
length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup"), |
156 | 158x |
combine = "or" |
157 |
)
|
|
158 | 158x |
if (is.factor(x)) { |
159 | 37x |
x <- droplevels(x) |
160 |
}
|
|
161 | 158x |
super$initialize( |
162 | 158x |
x = x, |
163 | 158x |
x_reactive = x_reactive, |
164 | 158x |
slice = slice, |
165 | 158x |
extract_type = extract_type |
166 |
)
|
|
167 | 158x |
private$set_choices(slice$choices) |
168 | 158x |
if (is.null(slice$selected) && slice$multiple) { |
169 | 45x |
slice$selected <- private$get_choices() |
170 | 113x |
} else if (is.null(slice$selected)) { |
171 | 1x |
slice$selected <- private$get_choices()[1] |
172 | 112x |
} else if (length(slice$selected) > 1 && !slice$multiple) { |
173 | 1x |
warning( |
174 | 1x |
"ChoicesFilterState allows \"selected\" to be of length 1 when \"multiple\" is FALSE. ",
|
175 | 1x |
"Only the first value will be used."
|
176 |
)
|
|
177 | 1x |
slice$selected <- slice$selected[1] |
178 |
}
|
|
179 | 158x |
private$set_selected(slice$selected) |
180 | 158x |
if (inherits(x, "POSIXt")) { |
181 | 9x |
private$tzone <- Find(function(x) x != "", attr(as.POSIXlt(x), "tzone")) |
182 |
}
|
|
183 |
}) |
|
184 | 158x |
invisible(self) |
185 |
},
|
|
186 | ||
187 |
#' @description
|
|
188 |
#' Returns reproducible condition call for current selection.
|
|
189 |
#' For this class returned call looks like
|
|
190 |
#' `<varname> %in% c(<values selected>)` with optional `is.na(<varname>)`.
|
|
191 |
#' @param dataname (`character(1)`) name of data set; defaults to `private$get_dataname()`
|
|
192 |
#' @return `call` or `NULL`
|
|
193 |
#'
|
|
194 |
get_call = function(dataname) { |
|
195 | 61x |
if (isFALSE(private$is_any_filtered())) { |
196 | 7x |
return(NULL) |
197 |
}
|
|
198 | 30x |
if (missing(dataname)) dataname <- private$get_dataname() |
199 | 54x |
varname <- private$get_varname_prefixed(dataname) |
200 | 54x |
choices <- private$get_choices() |
201 | 54x |
selected <- private$get_selected() |
202 | 54x |
fun_compare <- if (length(selected) == 1L) "==" else "%in%" |
203 | 54x |
filter_call <- if (length(selected) == 0) { |
204 | 6x |
call("!", call(fun_compare, varname, make_c_call(as.character(choices)))) |
205 |
} else { |
|
206 | 48x |
if (setequal(selected, choices) && !private$is_choice_limited) { |
207 | 2x |
NULL
|
208 | 46x |
} else if (inherits(private$x, "Date")) { |
209 | 1x |
call(fun_compare, varname, call("as.Date", make_c_call(as.character(selected)))) |
210 | 45x |
} else if (inherits(private$x, c("POSIXct", "POSIXlt"))) { |
211 | 2x |
class <- class(private$x)[1L] |
212 | 2x |
date_fun <- as.name( |
213 | 2x |
switch(class, |
214 | 2x |
"POSIXct" = "as.POSIXct", |
215 | 2x |
"POSIXlt" = "as.POSIXlt" |
216 |
)
|
|
217 |
)
|
|
218 | 2x |
call( |
219 | 2x |
fun_compare,
|
220 | 2x |
varname,
|
221 | 2x |
as.call(list(date_fun, make_c_call(as.character(selected)), tz = private$tzone)) |
222 |
)
|
|
223 | 43x |
} else if (is.numeric(private$x)) { |
224 | 7x |
call(fun_compare, varname, make_c_call(as.numeric(selected))) |
225 |
} else { |
|
226 |
# This handles numerics, characters, and factors.
|
|
227 | 36x |
call(fun_compare, varname, make_c_call(selected)) |
228 |
}
|
|
229 |
}
|
|
230 | 54x |
private$add_keep_na_call(filter_call, varname) |
231 |
}
|
|
232 |
),
|
|
233 | ||
234 |
# private members ----
|
|
235 |
private = list( |
|
236 |
x = NULL, |
|
237 |
choices_counts = integer(0), |
|
238 |
tzone = character(0), # if x is a datetime, stores time zone so that it can be restored in $get_call |
|
239 | ||
240 |
# private methods ----
|
|
241 | ||
242 |
# @description
|
|
243 |
# Checks validity of the choices, adjust if neccessary and sets the flag for the case where choices
|
|
244 |
# are limited by default from the start.
|
|
245 |
set_choices = function(choices) { |
|
246 | 158x |
named_counts <- .table(private$x) |
247 | 158x |
possible_choices <- names(named_counts) |
248 | 158x |
if (is.null(choices)) { |
249 | 143x |
choices <- possible_choices |
250 |
} else { |
|
251 | 15x |
choices <- as.character(choices) |
252 | 15x |
choices_adjusted <- choices[choices %in% possible_choices] |
253 | 15x |
if (length(setdiff(choices, choices_adjusted)) > 0L) { |
254 | 2x |
warning( |
255 | 2x |
sprintf( |
256 | 2x |
"Some choices not found in data. Adjusting. Filter id: %s.",
|
257 | 2x |
private$get_id() |
258 |
)
|
|
259 |
)
|
|
260 | 2x |
choices <- choices_adjusted |
261 |
}
|
|
262 | 15x |
if (length(choices) == 0) { |
263 | 1x |
warning( |
264 | 1x |
sprintf( |
265 | 1x |
"None of the choices were found in data. Setting defaults. Filter id: %s.",
|
266 | 1x |
private$get_id() |
267 |
)
|
|
268 |
)
|
|
269 | 1x |
choices <- possible_choices |
270 |
}
|
|
271 |
}
|
|
272 | 158x |
private$set_choices_counts( |
273 | 158x |
pair_counts(choices, named_counts) |
274 |
)
|
|
275 | 158x |
private$set_is_choice_limited(possible_choices, choices) |
276 | 158x |
private$teal_slice$choices <- choices |
277 | 158x |
invisible(NULL) |
278 |
},
|
|
279 |
# @description
|
|
280 |
# Check whether the initial choices filter out some values of x and set the flag in case.
|
|
281 |
set_is_choice_limited = function(x, choices) { |
|
282 | 158x |
xl <- x[!is.na(x)] |
283 | 158x |
private$is_choice_limited <- length(setdiff(xl, choices)) > 0L |
284 | 158x |
invisible(NULL) |
285 |
},
|
|
286 |
# @description
|
|
287 |
# Sets choices_counts private field.
|
|
288 |
set_choices_counts = function(choices_counts) { |
|
289 | 158x |
private$choices_counts <- choices_counts |
290 | 158x |
invisible(NULL) |
291 |
},
|
|
292 |
# @description
|
|
293 |
# Checks whether the input should be rendered as a checkboxgroup/radiobutton or a drop-down.
|
|
294 |
is_checkboxgroup = function() { |
|
295 | 25x |
length(private$get_choices()) <= getOption("teal.threshold_slider_vs_checkboxgroup") |
296 |
},
|
|
297 |
cast_and_validate = function(values) { |
|
298 | 187x |
tryCatch( |
299 | 187x |
expr = { |
300 | 187x |
values <- as.character(values) |
301 | ! |
if (anyNA(values)) stop() |
302 |
},
|
|
303 | 187x |
error = function(e) stop("The vector of set values must contain values coercible to character.") |
304 |
)
|
|
305 | 187x |
values
|
306 |
},
|
|
307 |
# If multiple forbidden but selected, restores previous selection with warning.
|
|
308 |
check_length = function(values) { |
|
309 | 187x |
if (!private$is_multiple() && length(values) > 1) { |
310 | 1x |
warning( |
311 | 1x |
sprintf("Selection: %s is not a vector of length one. ", toString(values, width = 360)), |
312 | 1x |
"Maintaining previous selection."
|
313 |
)
|
|
314 | 1x |
values <- isolate(private$get_selected()) |
315 |
}
|
|
316 | 187x |
values
|
317 |
},
|
|
318 |
remove_out_of_bounds_values = function(values) { |
|
319 | 187x |
in_choices_mask <- values %in% private$get_choices() |
320 | 187x |
if (length(values[!in_choices_mask]) > 0) { |
321 | 17x |
warning(paste( |
322 | 17x |
"Values:", toString(values[!in_choices_mask], width = 360), |
323 | 17x |
"are not in choices of column", private$get_varname(), "in dataset", private$get_dataname(), "." |
324 |
)) |
|
325 |
}
|
|
326 | 187x |
values[in_choices_mask] |
327 |
},
|
|
328 | ||
329 |
# shiny modules ----
|
|
330 | ||
331 |
# @description
|
|
332 |
# UI Module for `ChoicesFilterState`.
|
|
333 |
# This UI element contains available choices selection and
|
|
334 |
# checkbox whether to keep or not keep the `NA` values.
|
|
335 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
336 |
ui_inputs = function(id) { |
|
337 | 9x |
ns <- NS(id) |
338 | ||
339 |
# we need to isolate UI to not retrigger renderUI
|
|
340 | 9x |
isolate({ |
341 | 9x |
countsmax <- private$choices_counts |
342 | 9x |
countsnow <- if (!is.null(private$x_reactive())) { |
343 | 2x |
pair_counts( |
344 | 2x |
private$get_choices(), |
345 | 2x |
.table(private$x_reactive()) |
346 |
)
|
|
347 |
}
|
|
348 | ||
349 | 9x |
ui_input <- if (private$is_checkboxgroup()) { |
350 | 9x |
labels <- countBars( |
351 | 9x |
inputId = ns("labels"), |
352 | 9x |
choices = private$get_choices(), |
353 | 9x |
countsnow = countsnow, |
354 | 9x |
countsmax = countsmax |
355 |
)
|
|
356 | 9x |
tags$div( |
357 | 9x |
class = "choices_state", |
358 | 9x |
if (private$is_multiple()) { |
359 | 9x |
checkboxGroupInput( |
360 | 9x |
inputId = ns("selection"), |
361 | 9x |
label = NULL, |
362 | 9x |
selected = private$get_selected(), |
363 | 9x |
choiceNames = labels, |
364 | 9x |
choiceValues = private$get_choices(), |
365 | 9x |
width = "100%" |
366 |
)
|
|
367 |
} else { |
|
368 | ! |
radioButtons( |
369 | ! |
inputId = ns("selection"), |
370 | ! |
label = NULL, |
371 | ! |
selected = private$get_selected(), |
372 | ! |
choiceNames = labels, |
373 | ! |
choiceValues = private$get_choices(), |
374 | ! |
width = "100%" |
375 |
)
|
|
376 |
}
|
|
377 |
)
|
|
378 |
} else { |
|
379 | ! |
labels <- mapply( |
380 | ! |
FUN = make_count_text, |
381 | ! |
label = private$get_choices(), |
382 | ! |
countnow = if (is.null(countsnow)) rep(list(NULL), length(private$get_choices())) else countsnow, |
383 | ! |
countmax = countsmax |
384 |
)
|
|
385 | ||
386 | ! |
teal.widgets::optionalSelectInput( |
387 | ! |
inputId = ns("selection"), |
388 | ! |
choices = stats::setNames(private$get_choices(), labels), |
389 | ! |
selected = private$get_selected(), |
390 | ! |
multiple = private$is_multiple(), |
391 | ! |
options = shinyWidgets::pickerOptions( |
392 | ! |
actionsBox = TRUE, |
393 | ! |
liveSearch = (length(private$get_choices()) > 10), |
394 | ! |
noneSelectedText = "Select a value" |
395 |
)
|
|
396 |
)
|
|
397 |
}
|
|
398 | 9x |
tags$div( |
399 | 9x |
uiOutput(ns("trigger_visible")), |
400 | 9x |
ui_input,
|
401 | 9x |
private$keep_na_ui(ns("keep_na")) |
402 |
)
|
|
403 |
}) |
|
404 |
},
|
|
405 | ||
406 |
# @description
|
|
407 |
# Server module
|
|
408 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
409 |
# @return `NULL`.
|
|
410 |
server_inputs = function(id) { |
|
411 | 7x |
moduleServer( |
412 | 7x |
id = id, |
413 | 7x |
function(input, output, session) { |
414 | 7x |
logger::log_debug("ChoicesFilterState$server_inputs initializing, id: { private$get_id() }") |
415 |
# 1. renderUI is used here as an observer which triggers only if output is visible
|
|
416 |
# and if the reactive changes - reactive triggers only if the output is visible.
|
|
417 |
# 2. We want to trigger change of the labels only if reactive count changes (not underlying data)
|
|
418 | 7x |
non_missing_values <- reactive(Filter(Negate(is.na), private$x_reactive())) |
419 | 7x |
output$trigger_visible <- renderUI({ |
420 | 7x |
logger::log_debug("ChoicesFilterState$server_inputs@1 updating count labels, id: { private$get_id() }") |
421 | ||
422 | 7x |
countsnow <- if (!is.null(private$x_reactive())) { |
423 | ! |
pair_counts( |
424 | ! |
private$get_choices(), |
425 | ! |
.table(non_missing_values()) |
426 |
)
|
|
427 |
}
|
|
428 | ||
429 |
# update should be based on a change of counts only
|
|
430 | 7x |
isolate({ |
431 | 7x |
if (private$is_checkboxgroup()) { |
432 | 7x |
updateCountBars( |
433 | 7x |
inputId = "labels", |
434 | 7x |
choices = private$get_choices(), |
435 | 7x |
countsmax = private$choices_counts, |
436 | 7x |
countsnow = countsnow |
437 |
)
|
|
438 |
} else { |
|
439 | ! |
labels <- mapply( |
440 | ! |
FUN = make_count_text, |
441 | ! |
label = private$get_choices(), |
442 | ! |
countnow = if (is.null(countsnow)) rep(list(NULL), length(private$get_choices())) else countsnow, |
443 | ! |
countmax = private$choices_counts |
444 |
)
|
|
445 | ! |
teal.widgets::updateOptionalSelectInput( |
446 | ! |
session = session, |
447 | ! |
inputId = "selection", |
448 | ! |
choices = stats::setNames(private$get_choices(), labels), |
449 | ! |
selected = private$get_selected() |
450 |
)
|
|
451 |
}
|
|
452 | 7x |
NULL
|
453 |
}) |
|
454 |
}) |
|
455 | ||
456 | 7x |
private$session_bindings[[session$ns("selection")]] <- if (private$is_checkboxgroup()) { |
457 | 7x |
observeEvent( |
458 | 7x |
ignoreNULL = FALSE, |
459 | 7x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
460 | 7x |
eventExpr = input$selection, |
461 | 7x |
handlerExpr = { |
462 | ! |
logger::log_debug("ChoicesFilterState$server_inputs@2 changed selection, id: { private$get_id() }") |
463 | ||
464 | ! |
selection <- if (is.null(input$selection) && private$is_multiple()) { |
465 | ! |
character(0) |
466 |
} else { |
|
467 | ! |
input$selection |
468 |
}
|
|
469 | ||
470 | ! |
private$set_selected(selection) |
471 |
}
|
|
472 |
)
|
|
473 |
} else { |
|
474 | ! |
observeEvent( |
475 | ! |
ignoreNULL = FALSE, |
476 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
477 | ! |
eventExpr = input$selection_open, # observe click on a dropdown |
478 | ! |
handlerExpr = { |
479 | ! |
if (!isTRUE(input$selection_open)) { # only when the dropdown got closed |
480 | ! |
logger::log_debug("ChoicesFilterState$server_inputs@2 changed selection, id: { private$get_id() }") |
481 | ||
482 | ! |
selection <- if (is.null(input$selection) && private$is_multiple()) { |
483 | ! |
character(0) |
484 | ! |
} else if (isTRUE(length(input$selection) != 1) && !private$is_multiple()) { |
485 |
# In optionalSelectInput user is able to select mutliple options. But if FilterState is not multiple
|
|
486 |
# we should prevent this selection to be processed further.
|
|
487 |
# This is why notification is thrown and dropdown is changed back to latest selected.
|
|
488 | ! |
showNotification(paste( |
489 | ! |
"This filter exclusively supports single selection.",
|
490 | ! |
"Any additional choices made will be disregarded."
|
491 |
)) |
|
492 | ! |
teal.widgets::updateOptionalSelectInput( |
493 | ! |
session, "selection", |
494 | ! |
selected = private$get_selected() |
495 |
)
|
|
496 | ! |
return(NULL) |
497 |
} else { |
|
498 | ! |
input$selection |
499 |
}
|
|
500 | ! |
private$set_selected(selection) |
501 |
}
|
|
502 |
}
|
|
503 |
)
|
|
504 |
}
|
|
505 | ||
506 | 7x |
private$keep_na_srv("keep_na") |
507 | ||
508 |
# this observer is needed in the situation when teal_slice$selected has been
|
|
509 |
# changed directly by the api - then it's needed to rerender UI element
|
|
510 |
# to show relevant values
|
|
511 | 7x |
private$session_bindings[[session$ns("selection_api")]] <- observeEvent(private$get_selected(), { |
512 |
# it's important to not retrigger when the input$selection is the same as reactive values
|
|
513 |
# kept in the teal_slice$selected
|
|
514 | 2x |
if (!setequal(input$selection, private$get_selected())) { |
515 | 2x |
logger::log_debug("ChoicesFilterState$server@1 state changed, id: { private$get_id() }") |
516 | 2x |
if (private$is_checkboxgroup()) { |
517 | 2x |
if (private$is_multiple()) { |
518 | 2x |
updateCheckboxGroupInput( |
519 | 2x |
inputId = "selection", |
520 | 2x |
selected = private$get_selected() |
521 |
)
|
|
522 |
} else { |
|
523 | ! |
updateRadioButtons( |
524 | ! |
inputId = "selection", |
525 | ! |
selected = private$get_selected() |
526 |
)
|
|
527 |
}
|
|
528 |
} else { |
|
529 | ! |
teal.widgets::updateOptionalSelectInput( |
530 | ! |
session, "selection", |
531 | ! |
selected = private$get_selected() |
532 |
)
|
|
533 |
}
|
|
534 |
}
|
|
535 |
}) |
|
536 | ||
537 | 7x |
NULL
|
538 |
}
|
|
539 |
)
|
|
540 |
},
|
|
541 |
server_inputs_fixed = function(id) { |
|
542 | ! |
moduleServer( |
543 | ! |
id = id, |
544 | ! |
function(input, output, session) { |
545 | ! |
logger::log_debug("ChoicesFilterState$server_inputs_fixed initializing, id: { private$get_id() }") |
546 | ||
547 | ! |
output$selection <- renderUI({ |
548 | ! |
countsnow <- if (!is.null(private$x_reactive())) { |
549 | ! |
pair_counts( |
550 | ! |
private$get_choices(), |
551 | ! |
.table(private$x_reactive()) |
552 |
)
|
|
553 |
}
|
|
554 | ! |
countsmax <- private$choices_counts |
555 | ||
556 | ! |
ind <- private$get_choices() %in% isolate(private$get_selected()) |
557 | ! |
countBars( |
558 | ! |
inputId = session$ns("labels"), |
559 | ! |
choices = isolate(private$get_selected()), |
560 | ! |
countsnow = countsnow[ind], |
561 | ! |
countsmax = countsmax[ind] |
562 |
)
|
|
563 |
}) |
|
564 | ||
565 | ! |
NULL
|
566 |
}
|
|
567 |
)
|
|
568 |
},
|
|
569 | ||
570 |
# @description
|
|
571 |
# UI module to display filter summary
|
|
572 |
# renders text describing number of selected levels
|
|
573 |
# and if NA are included also
|
|
574 |
content_summary = function(id) { |
|
575 | 7x |
selected <- private$get_selected() |
576 | 7x |
selected_text <- |
577 | 7x |
if (length(selected) == 0L) { |
578 | ! |
"no selection"
|
579 |
} else { |
|
580 | 7x |
if (sum(nchar(selected)) <= 40L) { |
581 | 7x |
paste(selected, collapse = ", ") |
582 |
} else { |
|
583 | ! |
paste(length(selected), "levels selected") |
584 |
}
|
|
585 |
}
|
|
586 | 7x |
tagList( |
587 | 7x |
tags$span( |
588 | 7x |
class = "filter-card-summary-value", |
589 | 7x |
selected_text
|
590 |
),
|
|
591 | 7x |
tags$span( |
592 | 7x |
class = "filter-card-summary-controls", |
593 | 7x |
if (private$na_count > 0) { |
594 | ! |
tags$span("NA", if (isTRUE(private$get_keep_na())) icon("check") else icon("xmark")) |
595 |
}
|
|
596 |
)
|
|
597 |
)
|
|
598 |
}
|
|
599 |
)
|
|
600 |
)
|
|
601 | ||
602 |
#' `table` handling `POSIXlt`
|
|
603 |
#'
|
|
604 |
#' @param x (`vector`) variable to get counts from.
|
|
605 |
#' @return vector of counts named by unique values of `x`.
|
|
606 |
#'
|
|
607 |
#' @keywords internal
|
|
608 |
.table <- function(x) { |
|
609 | 160x |
tbl <- table( |
610 | 160x |
if (is.factor(x)) { |
611 | 37x |
x
|
612 |
} else { |
|
613 | 123x |
as.character(x) |
614 |
}
|
|
615 |
)
|
|
616 |
# tbl returns an array with dimnames instead of a simple vector
|
|
617 |
# we need to convert it to a vector so the object is simpler to handle
|
|
618 | 160x |
stats::setNames( |
619 | 160x |
as.vector(tbl), |
620 | 160x |
names(tbl) |
621 |
)
|
|
622 |
}
|
1 |
# DateFilterState ------
|
|
2 | ||
3 |
#' @name DateFilterState
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title `FilterState` object for `Date` data
|
|
7 |
#'
|
|
8 |
#' @description Manages choosing a range of `Date`s.
|
|
9 |
#'
|
|
10 |
#' @examples
|
|
11 |
#' # use non-exported function from teal.slice
|
|
12 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice")
|
|
13 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice")
|
|
14 |
#' DateFilterState <- getFromNamespace("DateFilterState", "teal.slice")
|
|
15 |
#'
|
|
16 |
#' library(shiny)
|
|
17 |
#'
|
|
18 |
#' filter_state <- DateFilterState$new(
|
|
19 |
#' x = c(Sys.Date() + seq(1:10), NA),
|
|
20 |
#' slice = teal_slice(varname = "x", dataname = "data"),
|
|
21 |
#' extract_type = character(0)
|
|
22 |
#' )
|
|
23 |
#' isolate(filter_state$get_call())
|
|
24 |
#' filter_state$set_state(
|
|
25 |
#' teal_slice(
|
|
26 |
#' dataname = "data",
|
|
27 |
#' varname = "x",
|
|
28 |
#' selected = c(Sys.Date() + 3L, Sys.Date() + 8L),
|
|
29 |
#' keep_na = TRUE
|
|
30 |
#' )
|
|
31 |
#' )
|
|
32 |
#' isolate(filter_state$get_call())
|
|
33 |
#'
|
|
34 |
#' # working filter in an app
|
|
35 |
#' library(shinyjs)
|
|
36 |
#'
|
|
37 |
#' dates <- c(Sys.Date() - 100, Sys.Date())
|
|
38 |
#' data_date <- c(seq(from = dates[1], to = dates[2], length.out = 100), NA)
|
|
39 |
#' fs <- DateFilterState$new(
|
|
40 |
#' x = data_date,
|
|
41 |
#' slice = teal_slice(
|
|
42 |
#' dataname = "data", varname = "x", selected = data_date[c(47, 98)], keep_na = TRUE
|
|
43 |
#' )
|
|
44 |
#' )
|
|
45 |
#'
|
|
46 |
#' ui <- fluidPage(
|
|
47 |
#' useShinyjs(),
|
|
48 |
#' include_css_files(pattern = "filter-panel"),
|
|
49 |
#' include_js_files(pattern = "count-bar-labels"),
|
|
50 |
#' column(4, tags$div(
|
|
51 |
#' tags$h4("DateFilterState"),
|
|
52 |
#' fs$ui("fs")
|
|
53 |
#' )),
|
|
54 |
#' column(4, tags$div(
|
|
55 |
#' id = "outputs", # div id is needed for toggling the element
|
|
56 |
#' tags$h4("Condition (i.e. call)"), # display the condition call generated by this FilterState
|
|
57 |
#' textOutput("condition_date"), tags$br(),
|
|
58 |
#' tags$h4("Unformatted state"), # display raw filter state
|
|
59 |
#' textOutput("unformatted_date"), tags$br(),
|
|
60 |
#' tags$h4("Formatted state"), # display human readable filter state
|
|
61 |
#' textOutput("formatted_date"), tags$br()
|
|
62 |
#' )),
|
|
63 |
#' column(4, tags$div(
|
|
64 |
#' tags$h4("Programmatic filter control"),
|
|
65 |
#' actionButton("button1_date", "set drop NA", width = "100%"), tags$br(),
|
|
66 |
#' actionButton("button2_date", "set keep NA", width = "100%"), tags$br(),
|
|
67 |
#' actionButton("button3_date", "set a range", width = "100%"), tags$br(),
|
|
68 |
#' actionButton("button4_date", "set full range", width = "100%"), tags$br(),
|
|
69 |
#' actionButton("button0_date", "set initial state", width = "100%"), tags$br()
|
|
70 |
#' ))
|
|
71 |
#' )
|
|
72 |
#'
|
|
73 |
#' server <- function(input, output, session) {
|
|
74 |
#' fs$server("fs")
|
|
75 |
#' output$condition_date <- renderPrint(fs$get_call())
|
|
76 |
#' output$formatted_date <- renderText(fs$format())
|
|
77 |
#' output$unformatted_date <- renderPrint(fs$get_state())
|
|
78 |
#' # modify filter state programmatically
|
|
79 |
#' observeEvent(
|
|
80 |
#' input$button1_date,
|
|
81 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = FALSE))
|
|
82 |
#' )
|
|
83 |
#' observeEvent(
|
|
84 |
#' input$button2_date,
|
|
85 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = TRUE))
|
|
86 |
#' )
|
|
87 |
#' observeEvent(
|
|
88 |
#' input$button3_date,
|
|
89 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", selected = data_date[c(34, 56)]))
|
|
90 |
#' )
|
|
91 |
#' observeEvent(
|
|
92 |
#' input$button4_date,
|
|
93 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", selected = dates))
|
|
94 |
#' )
|
|
95 |
#' observeEvent(
|
|
96 |
#' input$button0_date,
|
|
97 |
#' fs$set_state(
|
|
98 |
#' teal_slice("data", "variable", selected = data_date[c(47, 98)], keep_na = TRUE)
|
|
99 |
#' )
|
|
100 |
#' )
|
|
101 |
#' }
|
|
102 |
#'
|
|
103 |
#' if (interactive()) {
|
|
104 |
#' shinyApp(ui, server)
|
|
105 |
#' }
|
|
106 |
#'
|
|
107 |
#' @keywords internal
|
|
108 |
#'
|
|
109 |
DateFilterState <- R6::R6Class( # nolint |
|
110 |
"DateFilterState",
|
|
111 |
inherit = FilterState, |
|
112 | ||
113 |
# public methods ----
|
|
114 | ||
115 |
public = list( |
|
116 | ||
117 |
#' @description
|
|
118 |
#' Initialize a `FilterState` object.
|
|
119 |
#'
|
|
120 |
#' @param x (`Date`)
|
|
121 |
#' variable to be filtered.
|
|
122 |
#' @param x_reactive (`reactive`)
|
|
123 |
#' returning vector of the same type as `x`. Is used to update
|
|
124 |
#' counts following the change in values of the filtered dataset.
|
|
125 |
#' If it is set to `reactive(NULL)` then counts based on filtered
|
|
126 |
#' dataset are not shown.
|
|
127 |
#' @param slice (`teal_slice`)
|
|
128 |
#' specification of this filter state.
|
|
129 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`.
|
|
130 |
#' `get_state` returns `teal_slice` object which can be reused in other places.
|
|
131 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e.
|
|
132 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`.
|
|
133 |
#' @param extract_type (`character`)
|
|
134 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values:
|
|
135 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed
|
|
136 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>`
|
|
137 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]`
|
|
138 |
#'
|
|
139 |
#' @return Object of class `DateFilterState`, invisibly.
|
|
140 |
#'
|
|
141 |
initialize = function(x, |
|
142 |
x_reactive = reactive(NULL), |
|
143 |
slice,
|
|
144 |
extract_type = character(0)) { |
|
145 | 24x |
isolate({ |
146 | 24x |
checkmate::assert_date(x) |
147 | 23x |
checkmate::assert_class(x_reactive, "reactive") |
148 | ||
149 | 23x |
super$initialize( |
150 | 23x |
x = x, |
151 | 23x |
x_reactive = x_reactive, |
152 | 23x |
slice = slice, |
153 | 23x |
extract_type = extract_type |
154 |
)
|
|
155 | 23x |
checkmate::assert_date(slice$choices, null.ok = TRUE) |
156 | 22x |
private$set_choices(slice$choices) |
157 | 14x |
if (is.null(slice$selected)) slice$selected <- slice$choices |
158 | 22x |
private$set_selected(slice$selected) |
159 |
}) |
|
160 | ||
161 | 21x |
invisible(self) |
162 |
},
|
|
163 | ||
164 |
#' @description
|
|
165 |
#' Returns reproducible condition call for current selection.
|
|
166 |
#' For this class returned call looks like
|
|
167 |
#' `<varname> >= <min value> & <varname> <= <max value>` with optional `is.na(<varname>)`.
|
|
168 |
#' @param dataname (`character(1)`) containing possibly prefixed name of data set
|
|
169 |
#' @return `call` or `NULL`
|
|
170 |
#'
|
|
171 |
get_call = function(dataname) { |
|
172 | 7x |
if (isFALSE(private$is_any_filtered())) { |
173 | 1x |
return(NULL) |
174 |
}
|
|
175 | 6x |
choices <- as.character(private$get_selected()) |
176 | 6x |
varname <- private$get_varname_prefixed(dataname) |
177 | 6x |
filter_call <- |
178 | 6x |
call( |
179 |
"&",
|
|
180 | 6x |
call(">=", varname, call("as.Date", choices[1L])), |
181 | 6x |
call("<=", varname, call("as.Date", choices[2L])) |
182 |
)
|
|
183 | 6x |
private$add_keep_na_call(filter_call, varname) |
184 |
}
|
|
185 |
),
|
|
186 | ||
187 |
# private methods ----
|
|
188 | ||
189 |
private = list( |
|
190 |
set_choices = function(choices) { |
|
191 | 22x |
if (is.null(choices)) { |
192 | 19x |
choices <- range(private$x, na.rm = TRUE) |
193 |
} else { |
|
194 | 3x |
choices_adjusted <- c( |
195 | 3x |
max(choices[1L], min(private$x, na.rm = TRUE)), |
196 | 3x |
min(choices[2L], max(private$x, na.rm = TRUE)) |
197 |
)
|
|
198 | 3x |
if (any(choices != choices_adjusted)) { |
199 | 1x |
warning(sprintf( |
200 | 1x |
"Choices adjusted (some values outside of variable range). Varname: %s, dataname: %s.",
|
201 | 1x |
private$get_varname(), private$get_dataname() |
202 |
)) |
|
203 | 1x |
choices <- choices_adjusted |
204 |
}
|
|
205 | 3x |
if (choices[1L] >= choices[2L]) { |
206 | 1x |
warning(sprintf( |
207 | 1x |
"Invalid choices: lower is higher / equal to upper, or not in range of variable values. |
208 | 1x |
Setting defaults. Varname: %s, dataname: %s.", |
209 | 1x |
private$get_varname(), private$get_dataname() |
210 |
)) |
|
211 | 1x |
choices <- range(private$x, na.rm = TRUE) |
212 |
}
|
|
213 |
}
|
|
214 | 22x |
private$set_is_choice_limited(private$x, choices) |
215 | 22x |
private$x <- private$x[(private$x >= choices[1L] & private$x <= choices[2L]) | is.na(private$x)] |
216 | 22x |
private$teal_slice$choices <- choices |
217 | 22x |
invisible(NULL) |
218 |
},
|
|
219 | ||
220 |
# @description
|
|
221 |
# Check whether the initial choices filter out some values of x and set the flag in case.
|
|
222 |
set_is_choice_limited = function(xl, choices) { |
|
223 | 22x |
private$is_choice_limited <- (any(xl < choices[1L], na.rm = TRUE) | any(xl > choices[2L], na.rm = TRUE)) |
224 | 22x |
invisible(NULL) |
225 |
},
|
|
226 |
cast_and_validate = function(values) { |
|
227 | 33x |
tryCatch( |
228 | 33x |
expr = { |
229 | 33x |
values <- as.Date(values, origin = "1970-01-01") |
230 | ! |
if (anyNA(values)) stop() |
231 | 30x |
values
|
232 |
},
|
|
233 | 33x |
error = function(e) stop("Vector of set values must contain values coercible to Date.") |
234 |
)
|
|
235 |
},
|
|
236 |
check_length = function(values) { |
|
237 | 1x |
if (length(values) != 2) stop("Vector of set values must have length two.") |
238 | 29x |
if (values[1] > values[2]) { |
239 | 1x |
warning( |
240 | 1x |
sprintf( |
241 | 1x |
"Start date %s is set after the end date %s, the values will be replaced with a default date range.",
|
242 | 1x |
values[1], values[2] |
243 |
)
|
|
244 |
)
|
|
245 | 1x |
values <- isolate(private$get_choices()) |
246 |
}
|
|
247 | 29x |
values
|
248 |
},
|
|
249 |
remove_out_of_bounds_values = function(values) { |
|
250 | 29x |
choices <- private$get_choices() |
251 | 29x |
if (values[1] < choices[1L] | values[1] > choices[2L]) { |
252 | 5x |
warning( |
253 | 5x |
sprintf( |
254 | 5x |
"Value: %s is outside of the possible range for column %s of dataset %s, setting minimum possible value.",
|
255 | 5x |
values[1], private$get_varname(), private$get_dataname() |
256 |
)
|
|
257 |
)
|
|
258 | 5x |
values[1] <- choices[1L] |
259 |
}
|
|
260 | ||
261 | 29x |
if (values[2] > choices[2L] | values[2] < choices[1L]) { |
262 | 5x |
warning( |
263 | 5x |
sprintf( |
264 | 5x |
"Value: %s is outside of the possible range for column %s of dataset %s, setting maximum possible value.",
|
265 | 5x |
values[2], private$get_varname(), private$get_dataname() |
266 |
)
|
|
267 |
)
|
|
268 | 5x |
values[2] <- choices[2L] |
269 |
}
|
|
270 | ||
271 | 29x |
values
|
272 |
},
|
|
273 | ||
274 |
# shiny modules ----
|
|
275 | ||
276 |
# @description
|
|
277 |
# UI Module for `DateFilterState`.
|
|
278 |
# This UI element contains two date selections for `min` and `max`
|
|
279 |
# of the range and a checkbox whether to keep the `NA` values.
|
|
280 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
281 |
ui_inputs = function(id) { |
|
282 | ! |
ns <- NS(id) |
283 | ! |
isolate({ |
284 | ! |
tags$div( |
285 | ! |
tags$div( |
286 | ! |
class = "flex", |
287 | ! |
actionButton( |
288 | ! |
class = "date_reset_button", |
289 | ! |
inputId = ns("start_date_reset"), |
290 | ! |
label = NULL, |
291 | ! |
icon = icon("fas fa-undo") |
292 |
),
|
|
293 | ! |
tags$div( |
294 | ! |
class = "w-80 filter_datelike_input", |
295 | ! |
dateRangeInput( |
296 | ! |
inputId = ns("selection"), |
297 | ! |
label = NULL, |
298 | ! |
start = private$get_selected()[1], |
299 | ! |
end = private$get_selected()[2], |
300 | ! |
min = private$get_choices()[1L], |
301 | ! |
max = private$get_choices()[2L], |
302 | ! |
width = "100%" |
303 |
)
|
|
304 |
),
|
|
305 | ! |
actionButton( |
306 | ! |
class = "date_reset_button", |
307 | ! |
inputId = ns("end_date_reset"), |
308 | ! |
label = NULL, |
309 | ! |
icon = icon("fas fa-undo") |
310 |
)
|
|
311 |
),
|
|
312 | ! |
private$keep_na_ui(ns("keep_na")) |
313 |
)
|
|
314 |
}) |
|
315 |
},
|
|
316 | ||
317 |
# @description
|
|
318 |
# Server module
|
|
319 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
320 |
# @return `NULL`.
|
|
321 |
server_inputs = function(id) { |
|
322 | ! |
moduleServer( |
323 | ! |
id = id, |
324 | ! |
function(input, output, session) { |
325 | ! |
logger::log_debug("DateFilterState$server initializing, id: { private$get_id() }") |
326 | ||
327 |
# this observer is needed in the situation when teal_slice$selected has been
|
|
328 |
# changed directly by the api - then it's needed to rerender UI element
|
|
329 |
# to show relevant values
|
|
330 | ! |
private$session_bindings[[session$ns("selection_api")]] <- observeEvent( |
331 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
332 | ! |
ignoreInit = TRUE, |
333 | ! |
eventExpr = private$get_selected(), |
334 | ! |
handlerExpr = { |
335 | ! |
if (!setequal(private$get_selected(), input$selection)) { |
336 | ! |
logger::log_debug("DateFilterState$server@1 state changed, id: { private$get_id() }") |
337 | ! |
updateDateRangeInput( |
338 | ! |
session = session, |
339 | ! |
inputId = "selection", |
340 | ! |
start = private$get_selected()[1], |
341 | ! |
end = private$get_selected()[2] |
342 |
)
|
|
343 |
}
|
|
344 |
}
|
|
345 |
)
|
|
346 | ||
347 | ! |
private$session_bindings[[session$ns("selection")]] <- observeEvent( |
348 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
349 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
350 | ! |
eventExpr = input$selection, |
351 | ! |
handlerExpr = { |
352 | ! |
logger::log_debug("DateFilterState$server@2 selection changed, id: { private$get_id() }") |
353 | ! |
start_date <- input$selection[1] |
354 | ! |
end_date <- input$selection[2] |
355 | ||
356 | ! |
if (is.na(start_date) || is.na(end_date) || start_date > end_date) { |
357 | ! |
updateDateRangeInput( |
358 | ! |
session = session, |
359 | ! |
inputId = "selection", |
360 | ! |
start = private$get_selected()[1], |
361 | ! |
end = private$get_selected()[2] |
362 |
)
|
|
363 | ! |
showNotification( |
364 | ! |
"Start date must not be greater than the end date. Setting back to previous value.",
|
365 | ! |
type = "warning" |
366 |
)
|
|
367 | ! |
return(NULL) |
368 |
}
|
|
369 | ||
370 | ! |
private$set_selected(c(start_date, end_date)) |
371 |
}
|
|
372 |
)
|
|
373 | ||
374 | ||
375 | ! |
private$keep_na_srv("keep_na") |
376 | ||
377 | ! |
private$session_bindings[[session$ns("reset1")]] <- observeEvent(input$start_date_reset, { |
378 | ! |
logger::log_debug("DateFilterState$server@3 reset start date, id: { private$get_id() }") |
379 | ! |
updateDateRangeInput( |
380 | ! |
session = session, |
381 | ! |
inputId = "selection", |
382 | ! |
start = private$get_choices()[1L] |
383 |
)
|
|
384 |
}) |
|
385 | ||
386 | ! |
private$session_bindings[[session$ns("reset2")]] <- observeEvent(input$end_date_reset, { |
387 | ! |
logger::log_debug("DateFilterState$server@4 reset end date, id: { private$get_id() }") |
388 | ! |
updateDateRangeInput( |
389 | ! |
session = session, |
390 | ! |
inputId = "selection", |
391 | ! |
end = private$get_choices()[2L] |
392 |
)
|
|
393 |
}) |
|
394 | ||
395 | ! |
NULL
|
396 |
}
|
|
397 |
)
|
|
398 |
},
|
|
399 |
server_inputs_fixed = function(id) { |
|
400 | ! |
moduleServer( |
401 | ! |
id = id, |
402 | ! |
function(input, output, session) { |
403 | ! |
logger::log_debug("DateFilterState$server initializing, id: { private$get_id() }") |
404 | ||
405 | ! |
output$selection <- renderUI({ |
406 | ! |
vals <- format(private$get_selected(), nsmall = 3) |
407 | ! |
tags$div( |
408 | ! |
tags$div(icon("calendar-days"), vals[1]), |
409 | ! |
tags$div(span(" - "), icon("calendar-days"), vals[2]) |
410 |
)
|
|
411 |
}) |
|
412 | ||
413 | ! |
NULL
|
414 |
}
|
|
415 |
)
|
|
416 |
},
|
|
417 | ||
418 |
# @description
|
|
419 |
# Server module to display filter summary
|
|
420 |
# renders text describing selected date range and
|
|
421 |
# if NA are included also
|
|
422 |
content_summary = function(id) { |
|
423 | ! |
selected <- as.character(private$get_selected()) |
424 | ! |
min <- selected[1] |
425 | ! |
max <- selected[2] |
426 | ! |
tagList( |
427 | ! |
tags$span( |
428 | ! |
class = "filter-card-summary-value", |
429 | ! |
HTML(min, "–", max) |
430 |
),
|
|
431 | ! |
tags$span( |
432 | ! |
class = "filter-card-summary-controls", |
433 | ! |
if (private$na_count > 0) { |
434 | ! |
tags$span("NA", if (isTRUE(private$get_keep_na())) icon("check") else icon("xmark")) |
435 |
}
|
|
436 |
)
|
|
437 |
)
|
|
438 |
}
|
|
439 |
)
|
|
440 |
)
|
1 |
# EmptyFilterState ------
|
|
2 | ||
3 |
#' @name EmptyFilterState
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title `FilterState` object for empty variables
|
|
7 |
#'
|
|
8 |
#' @description `FilterState` subclass representing an empty variable.
|
|
9 |
#'
|
|
10 |
#' @examples
|
|
11 |
#' # use non-exported function from teal.slice
|
|
12 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice")
|
|
13 |
#' EmptyFilterState <- getFromNamespace("EmptyFilterState", "teal.slice")
|
|
14 |
#'
|
|
15 |
#' library(shiny)
|
|
16 |
#'
|
|
17 |
#' filter_state <- EmptyFilterState$new(
|
|
18 |
#' x = NA,
|
|
19 |
#' slice = teal_slice(varname = "x", dataname = "data"),
|
|
20 |
#' extract_type = character(0)
|
|
21 |
#' )
|
|
22 |
#' isolate(filter_state$get_call())
|
|
23 |
#' filter_state$set_state(teal_slice(dataname = "data", varname = "x", keep_na = TRUE))
|
|
24 |
#' isolate(filter_state$get_call())
|
|
25 |
#'
|
|
26 |
#' @keywords internal
|
|
27 |
#'
|
|
28 |
EmptyFilterState <- R6::R6Class( # nolint |
|
29 |
"EmptyFilterState",
|
|
30 |
inherit = FilterState, |
|
31 | ||
32 |
# public methods ----
|
|
33 |
public = list( |
|
34 | ||
35 |
#' @description
|
|
36 |
#' Initialize `EmptyFilterState` object.
|
|
37 |
#'
|
|
38 |
#' @param x (`vector`)
|
|
39 |
#' variable to be filtered,
|
|
40 |
#' @param x_reactive (`reactive`)
|
|
41 |
#' returning vector of the same type as `x`. Is used to update
|
|
42 |
#' counts following the change in values of the filtered dataset.
|
|
43 |
#' If it is set to `reactive(NULL)` then counts based on filtered
|
|
44 |
#' dataset are not shown.
|
|
45 |
#' @param slice (`teal_slice`)
|
|
46 |
#' specification of this filter state.
|
|
47 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`.
|
|
48 |
#' `get_state` returns `teal_slice` object which can be reused in other places.
|
|
49 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e.
|
|
50 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`.
|
|
51 |
#' @param extract_type (`character`)
|
|
52 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values:
|
|
53 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed
|
|
54 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>`
|
|
55 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]`
|
|
56 |
#'
|
|
57 |
#' @return Object of class `EmptyFilterState`, invisibly.
|
|
58 |
#'
|
|
59 |
initialize = function(x, |
|
60 |
x_reactive = reactive(NULL), |
|
61 |
extract_type = character(0), |
|
62 |
slice) { |
|
63 | 6x |
isolate({ |
64 | 6x |
super$initialize( |
65 | 6x |
x = x, |
66 | 6x |
x_reactive = x_reactive, |
67 | 6x |
slice = slice, |
68 | 6x |
extract_type = extract_type |
69 |
)
|
|
70 | 6x |
private$set_choices(slice$choices) |
71 | 6x |
private$set_selected(slice$selected) |
72 |
}) |
|
73 | ||
74 | 6x |
invisible(self) |
75 |
},
|
|
76 | ||
77 |
#' @description
|
|
78 |
#' Returns reproducible condition call for current selection relevant for selected variable type.
|
|
79 |
#' Uses internal reactive values, hence must be called in reactive or isolated context.
|
|
80 |
#' @param dataname name of data set; defaults to `private$get_dataname()`
|
|
81 |
#' @return `logical(1)`
|
|
82 |
#'
|
|
83 |
get_call = function(dataname) { |
|
84 | 2x |
if (isFALSE(private$is_any_filtered())) { |
85 | 1x |
return(NULL) |
86 |
}
|
|
87 | 1x |
if (missing(dataname)) dataname <- private$get_dataname() |
88 | 1x |
filter_call <- if (isTRUE(private$get_keep_na())) { |
89 | ! |
call("is.na", private$get_varname_prefixed(dataname)) |
90 |
} else { |
|
91 | 1x |
substitute(!is.na(varname), list(varname = private$get_varname_prefixed(dataname))) |
92 |
}
|
|
93 |
}
|
|
94 |
),
|
|
95 | ||
96 |
# private members ----
|
|
97 |
private = list( |
|
98 |
cache_state = function() { |
|
99 | ! |
private$cache <- private$get_state() |
100 | ! |
self$set_state( |
101 | ! |
list( |
102 | ! |
keep_na = NULL |
103 |
)
|
|
104 |
)
|
|
105 |
},
|
|
106 |
set_choices = function(choices) { |
|
107 | 6x |
private$teal_slice$choices <- choices |
108 | 6x |
invisible(NULL) |
109 |
},
|
|
110 | ||
111 | ||
112 |
# Reports whether the current state filters out any values.(?)
|
|
113 |
# @return `logical(1)`
|
|
114 |
#
|
|
115 |
is_any_filtered = function() { |
|
116 | 2x |
if (private$is_choice_limited) { |
117 | ! |
TRUE
|
118 |
} else { |
|
119 | 2x |
!isTRUE(private$get_keep_na()) |
120 |
}
|
|
121 |
},
|
|
122 | ||
123 |
# @description
|
|
124 |
# UI Module for `EmptyFilterState`.
|
|
125 |
# This UI element contains a checkbox input to filter or keep missing values.
|
|
126 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
127 |
#
|
|
128 |
ui_inputs = function(id) { |
|
129 | ! |
ns <- NS(id) |
130 | ! |
isolate({ |
131 | ! |
tags$div( |
132 | ! |
tags$span("Variable contains missing values only"), |
133 | ! |
private$keep_na_ui(ns("keep_na")) |
134 |
)
|
|
135 |
}) |
|
136 |
},
|
|
137 | ||
138 |
# @description
|
|
139 |
# Controls state of the `keep_na` checkbox input.
|
|
140 |
#
|
|
141 |
# @param id (`character(1)`) `shiny` module instance id.
|
|
142 |
#
|
|
143 |
# @return `NULL`.
|
|
144 |
#
|
|
145 |
server_inputs = function(id) { |
|
146 | ! |
moduleServer( |
147 | ! |
id = id, |
148 | ! |
function(input, output, session) { |
149 | ! |
private$keep_na_srv("keep_na") |
150 |
}
|
|
151 |
)
|
|
152 |
},
|
|
153 |
server_inputs_fixed = function(id) { |
|
154 | ! |
moduleServer( |
155 | ! |
id = id, |
156 | ! |
function(input, output, session) { |
157 | ! |
output$selection <- renderUI({ |
158 | ! |
tags$span("Variable contains missing values only") |
159 |
}) |
|
160 | ! |
NULL
|
161 |
}
|
|
162 |
)
|
|
163 |
},
|
|
164 | ||
165 |
# @description
|
|
166 |
# Server module to display filter summary
|
|
167 |
# Doesn't render anything
|
|
168 |
content_summary = function(id) { |
|
169 | ! |
tags$span("All empty") |
170 |
}
|
|
171 |
)
|
|
172 |
)
|
1 |
# DefaultFilteredDataset ----
|
|
2 | ||
3 |
#' @name DefaultFilteredDataset
|
|
4 |
#' @docType class
|
|
5 |
#' @title `DefaultFilteredDataset` `R6` class
|
|
6 |
#'
|
|
7 |
#' @description Stores any object as inert entity. Filtering is not supported.
|
|
8 |
#'
|
|
9 |
#' @examples
|
|
10 |
#' # use non-exported function from teal.slice
|
|
11 |
#' DefaultFilteredDataset <- getFromNamespace("DefaultFilteredDataset", "teal.slice")
|
|
12 |
#'
|
|
13 |
#' library(shiny)
|
|
14 |
#'
|
|
15 |
#' ds <- DefaultFilteredDataset$new(letters, "letters")
|
|
16 |
#' isolate(ds$get_filter_state())
|
|
17 |
#' isolate(ds$get_call())
|
|
18 |
#'
|
|
19 |
#' @keywords internal
|
|
20 |
#'
|
|
21 |
DefaultFilteredDataset <- R6::R6Class( # nolint |
|
22 |
classname = "DefaultFilteredDataset", |
|
23 |
inherit = FilteredDataset, |
|
24 | ||
25 |
# public methods ----
|
|
26 |
public = list( |
|
27 | ||
28 |
#' @description
|
|
29 |
#' Initializes this `DefaultFilteredDataset` object.
|
|
30 |
#'
|
|
31 |
#' @param dataset
|
|
32 |
#' any type of object; will not be filtered.
|
|
33 |
#' @param dataname (`character(1)`)
|
|
34 |
#' syntactically valid name given to the dataset.
|
|
35 |
#' @param label (`character(1)`)
|
|
36 |
#' label to describe the dataset.
|
|
37 |
#'
|
|
38 |
#' @return Object of class `DefaultFilteredDataset`, invisibly.
|
|
39 |
#'
|
|
40 |
initialize = function(dataset, dataname, label = character(0)) { |
|
41 | 24x |
super$initialize(dataset = dataset, dataname = dataname, label = label) |
42 |
},
|
|
43 | ||
44 |
#' @description
|
|
45 |
#' Returns a formatted string representing this `DefaultFilteredDataset` object.
|
|
46 |
#'
|
|
47 |
#' @param show_all (`logical(1)`) for method consistency, ignored.
|
|
48 |
#' @param trim_lines (`logical(1)`) flag specifying whether to trim lines if class names are too long.
|
|
49 |
#'
|
|
50 |
#' @return The formatted string.
|
|
51 |
#'
|
|
52 |
format = function(show_all, trim_lines = FALSE) { |
|
53 | 4x |
class_string <- toString(class(private$dataset)) |
54 | 4x |
if (trim_lines) { |
55 | 2x |
trim_position <- 37L |
56 | 2x |
class_string <- strtrim(class_string, trim_position) |
57 | 2x |
substr(class_string, 35L, 37L) <- "..." |
58 |
}
|
|
59 | 4x |
sprintf(" - unfiltered dataset:\t\"%s\": %s", private$dataname, class_string) |
60 |
},
|
|
61 | ||
62 |
#' @param sid (`character(1)`) for method consistency, ignored.
|
|
63 |
#' @return `NULL`, invisibly.
|
|
64 |
get_call = function(sid) { |
|
65 | 1x |
invisible(NULL) |
66 |
},
|
|
67 |
#' @return `NULL`, invisibly.
|
|
68 |
get_filter_state = function() { |
|
69 | 2x |
invisible(NULL) |
70 |
},
|
|
71 |
#' @param state (`teal_slices`) for method consistency, ignored.
|
|
72 |
#' @return `NULL`, invisibly.
|
|
73 |
set_filter_state = function(state) { |
|
74 | 3x |
if (length(state) != 0L) { |
75 | 1x |
warning("DefaultFilterState cannot set state") |
76 |
}
|
|
77 | 3x |
invisible(NULL) |
78 |
},
|
|
79 |
#' @param force (`logical(1)`) for method consistency, ignored.
|
|
80 |
#' @return `NULL`, invisibly.
|
|
81 |
clear_filter_states = function(force) { |
|
82 | 1x |
invisible(NULL) |
83 |
},
|
|
84 | ||
85 |
#' @description
|
|
86 |
#' Creates row for filter overview in the form of \cr
|
|
87 |
#' `dataname` - unsupported data class
|
|
88 |
#' @return A `data.frame`.
|
|
89 |
get_filter_overview = function() { |
|
90 | 1x |
data.frame(dataname = private$dataname, obs = NA, obs_filtered = NA) |
91 |
},
|
|
92 | ||
93 |
# shiny modules ----
|
|
94 | ||
95 |
#' @description
|
|
96 |
#' Overwrites parent method.
|
|
97 |
#' @details
|
|
98 |
#' Blank UI module that would list active filter states for this dataset.
|
|
99 |
#' @param id (`character(1)`)
|
|
100 |
#' `shiny` module instance id.
|
|
101 |
#' @param allow_add (ignored)
|
|
102 |
#' @return An empty `div`.
|
|
103 |
ui_active = function(id, allow_add) { |
|
104 | ! |
ns <- NS(id) |
105 | ! |
tags$div() |
106 |
},
|
|
107 | ||
108 |
#' @description
|
|
109 |
#' Overwrites parent method.
|
|
110 |
#' @details
|
|
111 |
#' Blank UI module that would list active filter states for this dataset.
|
|
112 |
#' @param id (`character(1)`)
|
|
113 |
#' `shiny` module instance id.
|
|
114 |
#' @return An empty `div`.
|
|
115 |
ui_add = function(id) { |
|
116 | ! |
ns <- NS(id) |
117 | ! |
tags$div() |
118 |
}
|
|
119 |
),
|
|
120 |
private = list( |
|
121 |
# private methods ----
|
|
122 |
# private fields ----
|
|
123 |
)
|
|
124 |
)
|
1 |
#' Set "`<choice>:<label>`" type of names
|
|
2 |
#'
|
|
3 |
#' @description `r lifecycle::badge("stable")`
|
|
4 |
#'
|
|
5 |
#' This is often useful for as it marks up the drop-down boxes for [shiny::selectInput()].
|
|
6 |
#'
|
|
7 |
#' @details
|
|
8 |
#' If either `choices` or `labels` are factors, they are coerced to character.
|
|
9 |
#' Duplicated elements from `choices` get removed.
|
|
10 |
#'
|
|
11 |
#' @param choices (`character` or `numeric` or `logical`) vector
|
|
12 |
#' @param labels (`character`) vector containing labels to be applied to `choices`. If `NA` then
|
|
13 |
#' "Label Missing" will be used.
|
|
14 |
#' @param subset a vector that is a subset of `choices`. This is useful if
|
|
15 |
#' only a few variables need to be named. If this argument is used, the returned vector will
|
|
16 |
#' match its order.
|
|
17 |
#' @param types vector containing the types of the columns.
|
|
18 |
#'
|
|
19 |
#' @return A named character vector.
|
|
20 |
#'
|
|
21 |
#' @keywords internal
|
|
22 |
#'
|
|
23 |
choices_labeled <- function(choices, labels, subset = NULL, types = NULL) { |
|
24 | 19x |
if (is.factor(choices)) { |
25 | ! |
choices <- as.character(choices) |
26 |
}
|
|
27 | ||
28 | 19x |
stopifnot( |
29 | 19x |
is.character(choices) || |
30 | 19x |
is.numeric(choices) || |
31 | 19x |
is.logical(choices) || |
32 | 19x |
(length(choices) == 1 && is.na(choices)) |
33 |
)
|
|
34 | ||
35 | 19x |
if (is.factor(labels)) { |
36 | ! |
labels <- as.character(labels) |
37 |
}
|
|
38 | ||
39 | 19x |
checkmate::assert_character(labels[!is.na(labels)], any.missing = FALSE) |
40 | 19x |
if (length(choices) != length(labels)) { |
41 | ! |
stop("length of choices must be the same as labels") |
42 |
}
|
|
43 | 19x |
stopifnot(is.null(subset) || is.vector(subset)) |
44 | 19x |
stopifnot(is.null(types) || is.vector(types)) |
45 | ||
46 | 19x |
if (is.vector(types)) { |
47 | 19x |
stopifnot(length(choices) == length(types)) |
48 |
}
|
|
49 | ||
50 | 19x |
if (!is.null(subset)) { |
51 | ! |
if (!all(subset %in% choices)) { |
52 | ! |
stop("all of subset variables must be in choices") |
53 |
}
|
|
54 | ! |
labels <- labels[choices %in% subset] |
55 | ! |
types <- types[choices %in% subset] |
56 | ! |
choices <- choices[choices %in% subset] |
57 |
}
|
|
58 | ||
59 | 19x |
is_dupl <- duplicated(choices) |
60 | 19x |
choices <- choices[!is_dupl] |
61 | 19x |
labels <- labels[!is_dupl] |
62 | 19x |
types <- types[!is_dupl] |
63 | 19x |
labels[is.na(labels)] <- "Label Missing" |
64 | 19x |
raw_labels <- labels |
65 | 19x |
combined_labels <- if (length(choices) > 0) { |
66 | 19x |
paste0(choices, ": ", labels) |
67 |
} else { |
|
68 | ! |
character(0) |
69 |
}
|
|
70 | ||
71 | 19x |
if (!is.null(subset)) { |
72 | ! |
ord <- match(subset, choices) |
73 | ! |
choices <- choices[ord] |
74 | ! |
raw_labels <- raw_labels[ord] |
75 | ! |
combined_labels <- combined_labels[ord] |
76 | ! |
types <- types[ord] |
77 |
}
|
|
78 | 19x |
choices <- structure( |
79 | 19x |
choices,
|
80 | 19x |
names = combined_labels, |
81 | 19x |
raw_labels = raw_labels, |
82 | 19x |
combined_labels = combined_labels, |
83 | 19x |
class = c("choices_labeled", "character"), |
84 | 19x |
types = types |
85 |
)
|
|
86 | ||
87 | 19x |
choices
|
88 |
}
|
1 |
#' Initialize `FilteredData`
|
|
2 |
#'
|
|
3 |
#' Function creates a `FilteredData` object.
|
|
4 |
#'
|
|
5 |
#' @param x (`named list`) of datasets.
|
|
6 |
#' @param join_keys (`join_keys`) see [`teal.data::join_keys()`].
|
|
7 |
#'
|
|
8 |
#' @return Object of class `FilteredData`.
|
|
9 |
#'
|
|
10 |
#' @examples
|
|
11 |
#' datasets <- init_filtered_data(list(iris = iris, mtcars = mtcars))
|
|
12 |
#' datasets
|
|
13 |
#'
|
|
14 |
#' @export
|
|
15 |
init_filtered_data <- function(x, join_keys = teal.data::join_keys()) { # nolint |
|
16 | 7x |
checkmate::assert_list(x, any.missing = FALSE, names = "unique") |
17 | 6x |
checkmate::assert_class(join_keys, "join_keys") |
18 | 5x |
FilteredData$new(x, join_keys = join_keys) |
19 |
}
|
|
20 | ||
21 |
#' Evaluate expression with meaningful message
|
|
22 |
#'
|
|
23 |
#' Method created for the `FilteredData` object to execute filter call with
|
|
24 |
#' meaningful message. After evaluation used environment should contain
|
|
25 |
#' all necessary bindings.
|
|
26 |
#'
|
|
27 |
#' @param expr (`language`)
|
|
28 |
#' @param env (`environment`) where expression is evaluated.
|
|
29 |
#' @return `NULL`, invisibly.
|
|
30 |
#' @keywords internal
|
|
31 |
eval_expr_with_msg <- function(expr, env) { |
|
32 | 32x |
lapply( |
33 | 32x |
expr,
|
34 | 32x |
function(x) { |
35 | 19x |
tryCatch( |
36 | 19x |
eval(x, envir = env), |
37 | 19x |
error = function(e) { |
38 | ! |
stop( |
39 | ! |
sprintf( |
40 | ! |
"Filter call execution failed:\n - call:\n %s\n - message:\n %s ",
|
41 | ! |
deparse1(x, collapse = "\n"), e |
42 |
)
|
|
43 |
)
|
|
44 |
}
|
|
45 |
)
|
|
46 |
}
|
|
47 |
)
|
|
48 | 32x |
invisible(NULL) |
49 |
}
|
|
50 | ||
51 | ||
52 |
#' Toggle button properties.
|
|
53 |
#'
|
|
54 |
#' Switch between different icons or titles on a button.
|
|
55 |
#'
|
|
56 |
#' Wrapper functions that use `shinyjs::runjs` to change button properties in response to events,
|
|
57 |
#' typically clicking those very buttons.
|
|
58 |
#' `shiny`'s `actionButton` and `actionLink` create `<a>` tags,
|
|
59 |
#' which may contain a child `<i>` tag that specifies an icon to be displayed.
|
|
60 |
#' `toggle_icon` calls the `toggleClass` (when `one_way = FALSE`) or
|
|
61 |
#' `removeClass` and `addClass` methods (when `one_way = TRUE`) to change icons.
|
|
62 |
#' `toggle_title` calls the `attr` method to modify the `Title` attribute of the button.
|
|
63 |
#'
|
|
64 |
#' @param input_id (`character(1)`) (name-spaced) id of the button
|
|
65 |
#' @param icons,titles (`character(2)`) vector specifying values between which to toggle
|
|
66 |
#' @param one_way (`logical(1)`) flag specifying whether to keep toggling;
|
|
67 |
#' if TRUE, the target will be changed
|
|
68 |
#' from the first element of `icons`/`titles` to the second
|
|
69 |
#'
|
|
70 |
#' @return `NULL`, invisibly.
|
|
71 |
#'
|
|
72 |
#' @examples
|
|
73 |
#' # use non-exported function from teal.slice
|
|
74 |
#' toggle_icon <- getFromNamespace("toggle_icon", "teal.slice")
|
|
75 |
#'
|
|
76 |
#' library(shiny)
|
|
77 |
#' library(shinyjs)
|
|
78 |
#'
|
|
79 |
#' ui <- fluidPage(
|
|
80 |
#' useShinyjs(),
|
|
81 |
#' actionButton("hide_content", label = "hide", icon = icon("xmark")),
|
|
82 |
#' actionButton("show_content", label = "show", icon = icon("check")),
|
|
83 |
#' actionButton("toggle_content", label = "toggle", icon = icon("angle-down")),
|
|
84 |
#' tags$br(),
|
|
85 |
#' tags$div(
|
|
86 |
#' id = "content",
|
|
87 |
#' verbatimTextOutput("printout")
|
|
88 |
#' )
|
|
89 |
#' )
|
|
90 |
#'
|
|
91 |
#' server <- function(input, output, session) {
|
|
92 |
#' observeEvent(input$hide_content,
|
|
93 |
#' {
|
|
94 |
#' hide("content")
|
|
95 |
#' toggle_icon("toggle_content", c("fa-angle-down", "fa-angle-right"), one_way = TRUE)
|
|
96 |
#' },
|
|
97 |
#' ignoreInit = TRUE
|
|
98 |
#' )
|
|
99 |
#'
|
|
100 |
#' observeEvent(input$show_content,
|
|
101 |
#' {
|
|
102 |
#' show("content")
|
|
103 |
#' toggle_icon("toggle_content", c("fa-angle-right", "fa-angle-down"), one_way = TRUE)
|
|
104 |
#' },
|
|
105 |
#' ignoreInit = TRUE
|
|
106 |
#' )
|
|
107 |
#'
|
|
108 |
#' observeEvent(input$toggle_content,
|
|
109 |
#' {
|
|
110 |
#' toggle("content")
|
|
111 |
#' toggle_icon("toggle_content", c("fa-angle-right", "fa-angle-down"))
|
|
112 |
#' },
|
|
113 |
#' ignoreInit = TRUE
|
|
114 |
#' )
|
|
115 |
#'
|
|
116 |
#' output$printout <- renderPrint({
|
|
117 |
#' head(faithful, 10)
|
|
118 |
#' })
|
|
119 |
#' }
|
|
120 |
#' if (interactive()) {
|
|
121 |
#' shinyApp(ui, server)
|
|
122 |
#' }
|
|
123 |
#'
|
|
124 |
#' @name toggle_button
|
|
125 |
#' @rdname toggle_button
|
|
126 |
#' @keywords internal
|
|
127 |
toggle_icon <- function(input_id, icons, one_way = FALSE) { |
|
128 | 3x |
checkmate::assert_string(input_id) |
129 | 3x |
checkmate::assert_character(icons, len = 2L) |
130 | 3x |
checkmate::assert_flag(one_way) |
131 | ||
132 | 3x |
expr <- |
133 | 3x |
if (one_way) { |
134 | 3x |
sprintf( |
135 | 3x |
"$('#%s i').removeClass('%s').addClass('%s');",
|
136 | 3x |
input_id, icons[1], icons[2] |
137 |
)
|
|
138 |
} else { |
|
139 | ! |
sprintf("$('#%s i').toggleClass('%s');", input_id, paste(icons, collapse = " ")) |
140 |
}
|
|
141 | ||
142 | 3x |
shinyjs::runjs(expr) |
143 | ||
144 | 3x |
invisible(NULL) |
145 |
}
|
|
146 | ||
147 |
#' @rdname toggle_button
|
|
148 |
#' @keywords internal
|
|
149 |
toggle_title <- function(input_id, titles, one_way = FALSE) { |
|
150 | 3x |
checkmate::assert_string(input_id) |
151 | 3x |
checkmate::assert_character(titles, len = 2L) |
152 | 3x |
checkmate::assert_flag(one_way) |
153 | ||
154 | 3x |
expr <- |
155 | 3x |
if (one_way) { |
156 | 3x |
sprintf( |
157 | 3x |
"$('a#%s').attr('title', '%s');",
|
158 | 3x |
input_id, titles[2] |
159 |
)
|
|
160 |
} else { |
|
161 | ! |
sprintf( |
162 | ! |
paste0( |
163 | ! |
"var button_id = 'a#%1$s';",
|
164 | ! |
"var curr = $(button_id).attr('title');",
|
165 | ! |
"if (curr == '%2$s') { $(button_id).attr('title', '%3$s');",
|
166 | ! |
"} else { $(button_id).attr('title', '%2$s');",
|
167 |
"}"
|
|
168 |
),
|
|
169 | ! |
input_id, titles[1], titles[2] |
170 |
)
|
|
171 |
}
|
|
172 | ||
173 | 3x |
shinyjs::runjs(expr) |
174 | ||
175 | 3x |
invisible(NULL) |
176 |
}
|
|
177 | ||
178 |
#' @inherit teal.data::topological_sort description details params title
|
|
179 |
#' @examples
|
|
180 |
#' # use non-exported function from teal.slice
|
|
181 |
#' topological_sort <- getFromNamespace("topological_sort", "teal.slice")
|
|
182 |
#'
|
|
183 |
#' topological_sort(list(A = c(), B = c("A"), C = c("B"), D = c("A")))
|
|
184 |
#' topological_sort(list(D = c("A"), A = c(), B = c("A"), C = c("B")))
|
|
185 |
#' topological_sort(list(D = c("A"), B = c("A"), C = c("B"), A = c()))
|
|
186 |
#' @keywords internal
|
|
187 |
topological_sort <- function(graph) { |
|
188 | 64x |
utils::getFromNamespace("topological_sort", ns = "teal.data")(graph) |
189 |
}
|
1 |
# FilterPanelAPI ------
|
|
2 | ||
3 |
#' @name FilterPanelAPI
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title Class to encapsulate the API of the filter panel of a teal app
|
|
7 |
#'
|
|
8 |
#' @description
|
|
9 |
#' An API class for managing filter states in a teal application's filter panel.
|
|
10 |
#'
|
|
11 |
#' @details
|
|
12 |
#' The purpose of this class is to encapsulate the API of the filter panel in a
|
|
13 |
#' new class `FilterPanelAPI` so that it can be passed and used in the server
|
|
14 |
#' call of any module instead of passing the whole `FilteredData` object.
|
|
15 |
#'
|
|
16 |
#' This class is supported by methods to set, get, remove filter states in the
|
|
17 |
#' filter panel API.
|
|
18 |
#'
|
|
19 |
#' @examples
|
|
20 |
#' library(shiny)
|
|
21 |
#'
|
|
22 |
#' fd <- init_filtered_data(list(iris = iris))
|
|
23 |
#' fpa <- FilterPanelAPI$new(fd)
|
|
24 |
#'
|
|
25 |
#' # get the actual filter state --> empty named list
|
|
26 |
#' isolate(fpa$get_filter_state())
|
|
27 |
#'
|
|
28 |
#' # set a filter state
|
|
29 |
#' set_filter_state(
|
|
30 |
#' fpa,
|
|
31 |
#' teal_slices(
|
|
32 |
#' teal_slice(dataname = "iris", varname = "Species", selected = "setosa", keep_na = TRUE)
|
|
33 |
#' )
|
|
34 |
#' )
|
|
35 |
#'
|
|
36 |
#' # get the actual filter state --> named list with filters
|
|
37 |
#' isolate(fpa$get_filter_state())
|
|
38 |
#'
|
|
39 |
#' # remove all_filter_states
|
|
40 |
#' fpa$clear_filter_states()
|
|
41 |
#'
|
|
42 |
#' # get the actual filter state --> empty named list
|
|
43 |
#' isolate(fpa$get_filter_state())
|
|
44 |
#'
|
|
45 |
#' @export
|
|
46 |
#'
|
|
47 |
FilterPanelAPI <- R6::R6Class( # nolint |
|
48 |
"FilterPanelAPI",
|
|
49 |
# public methods ----
|
|
50 |
public = list( |
|
51 |
#' @description
|
|
52 |
#' Initialize a `FilterPanelAPI` object.
|
|
53 |
#' @param datasets (`FilteredData`)
|
|
54 |
#'
|
|
55 |
initialize = function(datasets) { |
|
56 | 8x |
checkmate::assert_class(datasets, "FilteredData") |
57 | 6x |
private$filtered_data <- datasets |
58 |
},
|
|
59 | ||
60 |
#' @description
|
|
61 |
#' Gets the reactive values from the active `FilterState` objects of the `FilteredData` object.
|
|
62 |
#'
|
|
63 |
#' Gets all active filters in the form of a nested list.
|
|
64 |
#' The output list is a compatible input to `set_filter_state`.
|
|
65 |
#'
|
|
66 |
#' @return `list` with named elements corresponding to `FilteredDataset` objects with active filters.
|
|
67 |
#'
|
|
68 |
get_filter_state = function() { |
|
69 | 8x |
private$filtered_data$get_filter_state() |
70 |
},
|
|
71 | ||
72 |
#' @description
|
|
73 |
#' Sets active filter states.
|
|
74 |
#' @param filter (`teal_slices`)
|
|
75 |
#'
|
|
76 |
#' @return `NULL`, invisibly.
|
|
77 |
#'
|
|
78 |
set_filter_state = function(filter) { |
|
79 | 5x |
private$filtered_data$set_filter_state(filter) |
80 | 5x |
invisible(NULL) |
81 |
},
|
|
82 | ||
83 |
#' @description
|
|
84 |
#' Remove one or more `FilterState` of a `FilteredDataset` in the `FilteredData` object.
|
|
85 |
#'
|
|
86 |
#' @param filter (`teal_slices`)
|
|
87 |
#' specifying `FilterState` objects to remove;
|
|
88 |
#' `teal_slice`s may contain only `dataname` and `varname`, other elements are ignored
|
|
89 |
#'
|
|
90 |
#' @return `NULL`, invisibly.
|
|
91 |
#'
|
|
92 |
remove_filter_state = function(filter) { |
|
93 | 1x |
private$filtered_data$remove_filter_state(filter) |
94 | 1x |
invisible(NULL) |
95 |
},
|
|
96 | ||
97 |
#' @description
|
|
98 |
#' Remove all `FilterStates` of the `FilteredData` object.
|
|
99 |
#'
|
|
100 |
#' @param datanames (`character`)
|
|
101 |
#' `datanames` to remove their `FilterStates`;
|
|
102 |
#' omit to remove all `FilterStates` in the `FilteredData` object
|
|
103 |
#'
|
|
104 |
#' @return `NULL`, invisibly.
|
|
105 |
#'
|
|
106 |
clear_filter_states = function(datanames) { |
|
107 | 2x |
datanames_to_remove <- if (missing(datanames)) private$filtered_data$datanames() else datanames |
108 | 2x |
private$filtered_data$clear_filter_states(datanames = datanames_to_remove) |
109 | 2x |
invisible(NULL) |
110 |
}
|
|
111 |
),
|
|
112 |
# private methods ----
|
|
113 |
private = list( |
|
114 |
filtered_data = NULL |
|
115 |
)
|
|
116 |
)
|
1 |
#' Initialize `FilterStates` object
|
|
2 |
#'
|
|
3 |
#' @param data (`data.frame` or `MultiAssayExperiment` or `SummarizedExperiment` or `matrix`)
|
|
4 |
#' object to subset.
|
|
5 |
#' @param data_reactive (`function(sid)`)
|
|
6 |
#' should return an object of the same type as `data` or `NULL`.
|
|
7 |
#' This function is needed for the `FilterState` `shiny` module to update counts if filtered data changes.
|
|
8 |
#' If function returns `NULL` then filtered counts are not shown.
|
|
9 |
#' Function has to have `sid` argument being a character which is related to `sid` argument in the `get_call` method.
|
|
10 |
#' @param dataname (`character(1)`)
|
|
11 |
#' name of the data used in the subset expression,
|
|
12 |
#' passed to the function argument attached to this `FilterStates`.
|
|
13 |
#' @param datalabel (`character(1)`) optional
|
|
14 |
#' text label.
|
|
15 |
#' @param ... optional,
|
|
16 |
#' additional arguments for specific classes: keys.
|
|
17 |
#'
|
|
18 |
#' @return Object of class `FilterStates`.
|
|
19 |
#'
|
|
20 |
#' @keywords internal
|
|
21 |
#' @examples
|
|
22 |
#' # use non-exported function from teal.slice
|
|
23 |
#' init_filter_states <- getFromNamespace("init_filter_states", "teal.slice")
|
|
24 |
#'
|
|
25 |
#' df <- data.frame(
|
|
26 |
#' character = letters,
|
|
27 |
#' numeric = seq_along(letters),
|
|
28 |
#' date = seq(Sys.Date(), length.out = length(letters), by = "1 day"),
|
|
29 |
#' datetime = seq(Sys.time(), length.out = length(letters), by = "33.33 hours")
|
|
30 |
#' )
|
|
31 |
#' rf <- init_filter_states(
|
|
32 |
#' data = df,
|
|
33 |
#' dataname = "DF"
|
|
34 |
#' )
|
|
35 |
#'
|
|
36 |
#' library(shiny)
|
|
37 |
#' ui <- fluidPage(
|
|
38 |
#' actionButton("clear", tags$span(icon("xmark"), "Remove all filters")),
|
|
39 |
#' rf$ui_add(id = "add"),
|
|
40 |
#' rf$ui_active("states"),
|
|
41 |
#' verbatimTextOutput("expr"),
|
|
42 |
#' )
|
|
43 |
#'
|
|
44 |
#' server <- function(input, output, session) {
|
|
45 |
#' rf$srv_add(id = "add")
|
|
46 |
#' rf$srv_active(id = "states")
|
|
47 |
#' output$expr <- renderText({
|
|
48 |
#' deparse1(rf$get_call(), collapse = "\n")
|
|
49 |
#' })
|
|
50 |
#' observeEvent(input$clear, rf$clear_filter_states())
|
|
51 |
#' }
|
|
52 |
#'
|
|
53 |
#' if (interactive()) {
|
|
54 |
#' shinyApp(ui, server)
|
|
55 |
#' }
|
|
56 |
#'
|
|
57 |
#' @export
|
|
58 |
#'
|
|
59 |
init_filter_states <- function(data, |
|
60 |
data_reactive = reactive(NULL), |
|
61 |
dataname,
|
|
62 |
datalabel = NULL, |
|
63 |
...) { |
|
64 | 213x |
UseMethod("init_filter_states") |
65 |
}
|
|
66 | ||
67 |
#' @keywords internal
|
|
68 |
#' @export
|
|
69 |
init_filter_states.data.frame <- function(data, # nolint |
|
70 |
data_reactive = function(sid = "") NULL, |
|
71 |
dataname,
|
|
72 |
datalabel = NULL, |
|
73 |
keys = character(0), |
|
74 |
...) { |
|
75 | 96x |
DFFilterStates$new( |
76 | 96x |
data = data, |
77 | 96x |
data_reactive = data_reactive, |
78 | 96x |
dataname = dataname, |
79 | 96x |
datalabel = datalabel, |
80 | 96x |
keys = keys |
81 |
)
|
|
82 |
}
|
|
83 | ||
84 |
#' @keywords internal
|
|
85 |
#' @export
|
|
86 |
init_filter_states.matrix <- function(data, # nolint |
|
87 |
data_reactive = function(sid = "") NULL, |
|
88 |
dataname,
|
|
89 |
datalabel = NULL, |
|
90 |
...) { |
|
91 | 20x |
MatrixFilterStates$new( |
92 | 20x |
data = data, |
93 | 20x |
data_reactive = data_reactive, |
94 | 20x |
dataname = dataname, |
95 | 20x |
datalabel = datalabel |
96 |
)
|
|
97 |
}
|
|
98 | ||
99 |
#' @keywords internal
|
|
100 |
#' @export
|
|
101 |
init_filter_states.MultiAssayExperiment <- function(data, # nolint |
|
102 |
data_reactive = function(sid = "") NULL, |
|
103 |
dataname,
|
|
104 |
datalabel = "subjects", |
|
105 |
keys = character(0), |
|
106 |
...) { |
|
107 | 20x |
if (!requireNamespace("MultiAssayExperiment", quietly = TRUE)) { |
108 | ! |
stop("Cannot load MultiAssayExperiment - please install the package or restart your session.") |
109 |
}
|
|
110 | 20x |
MAEFilterStates$new( |
111 | 20x |
data = data, |
112 | 20x |
data_reactive = data_reactive, |
113 | 20x |
dataname = dataname, |
114 | 20x |
datalabel = datalabel, |
115 | 20x |
keys = keys |
116 |
)
|
|
117 |
}
|
|
118 | ||
119 |
#' @keywords internal
|
|
120 |
#' @export
|
|
121 |
init_filter_states.SummarizedExperiment <- function(data, # nolint |
|
122 |
data_reactive = function(sid = "") NULL, |
|
123 |
dataname,
|
|
124 |
datalabel = NULL, |
|
125 |
...) { |
|
126 | 77x |
if (!requireNamespace("SummarizedExperiment", quietly = TRUE)) { |
127 | ! |
stop("Cannot load SummarizedExperiment - please install the package or restart your session.") |
128 |
}
|
|
129 | 77x |
SEFilterStates$new( |
130 | 77x |
data = data, |
131 | 77x |
data_reactive = data_reactive, |
132 | 77x |
dataname = dataname, |
133 | 77x |
datalabel = datalabel |
134 |
)
|
|
135 |
}
|
|
136 | ||
137 |
#' Gets supported filterable variable names
|
|
138 |
#'
|
|
139 |
#' Gets filterable variable names from a given object. The names match variables
|
|
140 |
#' of classes in an vector `teal.slice:::.filterable_class`.
|
|
141 |
#' @param data
|
|
142 |
#' the `R` object containing elements which class can be checked through `vapply` or `apply`.
|
|
143 |
#' @return `character` vector of variable names.
|
|
144 |
#' @examples
|
|
145 |
#' # use non-exported function from teal.slice
|
|
146 |
#' get_supported_filter_varnames <- getFromNamespace("get_supported_filter_varnames", "teal.slice")
|
|
147 |
#'
|
|
148 |
#' df <- data.frame(
|
|
149 |
#' a = letters[1:3],
|
|
150 |
#' b = 1:3,
|
|
151 |
#' c = Sys.Date() + 1:3,
|
|
152 |
#' d = Sys.time() + 1:3,
|
|
153 |
#' z = complex(3)
|
|
154 |
#' )
|
|
155 |
#' get_supported_filter_varnames(df)
|
|
156 |
#' @keywords internal
|
|
157 |
#' @export
|
|
158 |
get_supported_filter_varnames <- function(data) { |
|
159 | 227x |
UseMethod("get_supported_filter_varnames") |
160 |
}
|
|
161 | ||
162 |
#' @keywords internal
|
|
163 |
#' @export
|
|
164 |
get_supported_filter_varnames.default <- function(data) { # nolint |
|
165 | 200x |
is_expected_class <- vapply( |
166 | 200x |
X = data, |
167 | 200x |
FUN = function(x) any(class(x) %in% .filterable_class), |
168 | 200x |
FUN.VALUE = logical(1) |
169 |
)
|
|
170 | 200x |
names(is_expected_class[is_expected_class]) |
171 |
}
|
|
172 | ||
173 |
#' @keywords internal
|
|
174 |
#' @export
|
|
175 |
get_supported_filter_varnames.matrix <- function(data) { # nolint |
|
176 |
# all columns are the same type in matrix
|
|
177 | 27x |
is_expected_class <- class(data[, 1]) %in% .filterable_class |
178 | 27x |
if (is_expected_class && !is.null(colnames(data))) { |
179 | 24x |
colnames(data) |
180 |
} else { |
|
181 | 3x |
character(0) |
182 |
}
|
|
183 |
}
|
|
184 | ||
185 |
#' @keywords internal
|
|
186 |
#' @export
|
|
187 |
get_supported_filter_varnames.MultiAssayExperiment <- function(data) { # nolint |
|
188 | ! |
data <- SummarizedExperiment::colData(data) |
189 |
# all columns are the same type in matrix
|
|
190 | ! |
is_expected_class <- class(data[, 1]) %in% .filterable_class |
191 | ! |
if (is_expected_class && !is.null(names(data))) { |
192 | ! |
names(data) |
193 |
} else { |
|
194 | ! |
character(0) |
195 |
}
|
|
196 |
}
|
|
197 | ||
198 |
#' Returns a `choices_labeled` object
|
|
199 |
#'
|
|
200 |
#' @param data (`data.frame` or `DFrame` or `list`)
|
|
201 |
#' where labels can be taken from in case when `varlabels` is not specified.
|
|
202 |
#' `data` must be specified if `varlabels` is not specified.
|
|
203 |
#' @param choices (`character`)
|
|
204 |
#' the vector of chosen variables
|
|
205 |
#' @param varlabels (`character`)
|
|
206 |
#' the labels of variables in data
|
|
207 |
#' @param keys (`character`)
|
|
208 |
#' the names of the key columns in data
|
|
209 |
#' @return `character(0)` if choices are empty; a `choices_labeled` object otherwise
|
|
210 |
#' @keywords internal
|
|
211 |
data_choices_labeled <- function(data, |
|
212 |
choices,
|
|
213 |
varlabels = teal.data::col_labels(data, fill = TRUE), |
|
214 |
keys = character(0)) { |
|
215 | 25x |
if (length(choices) == 0) { |
216 | 6x |
return(character(0)) |
217 |
}
|
|
218 | 19x |
choice_types <- variable_types(data = data, columns = choices) |
219 | 19x |
choice_types[keys] <- "primary_key" |
220 | ||
221 | 19x |
choices_labeled( |
222 | 19x |
choices = choices, |
223 | 19x |
labels = unname(varlabels[choices]), |
224 | 19x |
types = choice_types[choices] |
225 |
)
|
|
226 |
}
|
|
227 | ||
228 |
#' @noRd
|
|
229 |
#' @keywords internal
|
|
230 |
get_varlabels <- function(data) { |
|
231 | 17x |
if (!is.array(data)) { |
232 | 16x |
vapply( |
233 | 16x |
colnames(data), |
234 | 16x |
FUN = function(x) { |
235 | 120x |
label <- attr(data[[x]], "label") |
236 | 120x |
if (is.null(label)) { |
237 | 118x |
x
|
238 |
} else { |
|
239 | 2x |
label
|
240 |
}
|
|
241 |
},
|
|
242 | 16x |
FUN.VALUE = character(1) |
243 |
)
|
|
244 |
} else { |
|
245 | 1x |
character(0) |
246 |
}
|
|
247 |
}
|
1 |
# MAEFilterStates ------
|
|
2 | ||
3 |
#' @name MAEFilterStates
|
|
4 |
#' @docType class
|
|
5 |
#' @title `FilterStates` subclass for `MultiAssayExperiment`s
|
|
6 |
#' @description Handles filter states in a `MultiAssayExperiment`.
|
|
7 |
#' @keywords internal
|
|
8 |
#'
|
|
9 |
MAEFilterStates <- R6::R6Class( # nolint |
|
10 |
classname = "MAEFilterStates", |
|
11 |
inherit = FilterStates, |
|
12 |
# public methods ----
|
|
13 |
public = list( |
|
14 |
#' @description
|
|
15 |
#' Initialize `MAEFilterStates` object.
|
|
16 |
#'
|
|
17 |
#' @param data (`MultiAssayExperiment`)
|
|
18 |
#' the `R` object which `MultiAssayExperiment::subsetByColData` function is applied on.
|
|
19 |
#' @param data_reactive (`function(sid)`)
|
|
20 |
#' should return a `MultiAssayExperiment` object or `NULL`.
|
|
21 |
#' This object is needed for the `FilterState` counts being updated
|
|
22 |
#' on a change in filters. If function returns `NULL` then filtered counts are not shown.
|
|
23 |
#' Function has to have `sid` argument being a character.
|
|
24 |
#' @param dataname (`character(1)`)
|
|
25 |
#' name of the data used in the subset expression.
|
|
26 |
#' Passed to the function argument attached to this `FilterStates`.
|
|
27 |
#' @param datalabel (`character(1)`) optional
|
|
28 |
#' text label.
|
|
29 |
#' @param varlabels (`character`)
|
|
30 |
#' labels of the variables used in this object.
|
|
31 |
#' @param keys (`character`)
|
|
32 |
#' key column names.
|
|
33 |
#'
|
|
34 |
initialize = function(data, |
|
35 |
data_reactive = function(sid = "") NULL, |
|
36 |
dataname,
|
|
37 |
datalabel = "subjects", |
|
38 |
keys = character(0)) { |
|
39 | 24x |
if (!requireNamespace("MultiAssayExperiment", quietly = TRUE)) { |
40 | ! |
stop("Cannot load MultiAssayExperiment - please install the package or restart your session.") |
41 |
}
|
|
42 | 24x |
checkmate::assert_function(data_reactive, args = "sid") |
43 | 24x |
checkmate::assert_class(data, "MultiAssayExperiment") |
44 | ||
45 | 23x |
data <- SummarizedExperiment::colData(data) |
46 | 23x |
data_reactive <- function(sid = "") SummarizedExperiment::colData(data_reactive(sid = sid)) |
47 | 23x |
super$initialize(data, data_reactive, dataname, datalabel) |
48 | 23x |
private$keys <- keys |
49 | 23x |
private$set_filterable_varnames(include_varnames = colnames(data)) |
50 | ||
51 | 23x |
invisible(self) |
52 |
}
|
|
53 |
),
|
|
54 | ||
55 |
# private fields ----
|
|
56 | ||
57 |
private = list( |
|
58 |
extract_type = "list", |
|
59 |
fun = quote(MultiAssayExperiment::subsetByColData) |
|
60 |
)
|
|
61 |
)
|
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 | ! |
singleton(tags$head(lapply(css_files, includeCSS))) |
17 |
}
|
1 |
# This file contains helper functions used in unit tests.
|
|
2 | ||
3 |
# compares specified fields between two `teal_slice` objects
|
|
4 |
#' @noRd
|
|
5 |
#' @keywords internal
|
|
6 |
compare_slices <- function(ts1, ts2, fields) { |
|
7 | 9x |
isolate( |
8 | 9x |
all(vapply(fields, function(x) identical(ts1[[x]], ts2[[x]]), logical(1L))) |
9 |
)
|
|
10 |
}
|
|
11 | ||
12 | ||
13 |
# compare two teal_slice
|
|
14 |
#' @noRd
|
|
15 |
#' @keywords internal
|
|
16 |
expect_identical_slice <- function(x, y) { |
|
17 | 34x |
isolate({ |
18 | 34x |
testthat::expect_true( |
19 | 34x |
setequal( |
20 | 34x |
reactiveValuesToList(x), |
21 | 34x |
reactiveValuesToList(y) |
22 |
)
|
|
23 |
)
|
|
24 |
}) |
|
25 |
}
|
|
26 | ||
27 |
# compare two teal_slices
|
|
28 |
#' @noRd
|
|
29 |
#' @keywords internal
|
|
30 |
expect_identical_slices <- function(x, y) { |
|
31 | 12x |
isolate({ |
32 | 12x |
mapply( |
33 | 12x |
function(x, y) { |
34 | 27x |
expect_identical_slice(x, y) |
35 |
},
|
|
36 | 12x |
x = x, |
37 | 12x |
y = y |
38 |
)
|
|
39 | 12x |
testthat::expect_identical(attributes(x), attributes(y)) |
40 |
}) |
|
41 |
}
|
1 |
#' Managing `FilteredData` states
|
|
2 |
#'
|
|
3 |
#' @description `r lifecycle::badge("experimental")`
|
|
4 |
#'
|
|
5 |
#' Set, get and remove filter states of `FilteredData` object.
|
|
6 |
#'
|
|
7 |
#' @name filter_state_api
|
|
8 |
#'
|
|
9 |
#' @param datasets (`FilteredData`)
|
|
10 |
#' object to store filter state and filtered datasets, shared across modules
|
|
11 |
#'
|
|
12 |
#' see [`FilteredData`] for details
|
|
13 |
#'
|
|
14 |
#' @param filter (`teal_slices`)
|
|
15 |
#' specify filters in place on app start-up
|
|
16 |
#'
|
|
17 |
#' @param force (`logical(1)`)
|
|
18 |
#' flag specifying whether to include anchored filter states.
|
|
19 |
#'
|
|
20 |
#' @return
|
|
21 |
#' - `set_*`, `remove_*` and `clear_filter_state` return `NULL` invisibly
|
|
22 |
#' - `get_filter_state` returns a named `teal_slices` object
|
|
23 |
#' containing a `teal_slice` for every existing `FilterState`
|
|
24 |
#'
|
|
25 |
#' @seealso [`teal_slice`]
|
|
26 |
#'
|
|
27 |
#' @examples
|
|
28 |
#' datasets <- init_filtered_data(list(iris = iris, mtcars = mtcars))
|
|
29 |
#' fs <- teal_slices(
|
|
30 |
#' teal_slice(dataname = "iris", varname = "Species", selected = c("setosa", "versicolor")),
|
|
31 |
#' teal_slice(dataname = "iris", varname = "Sepal.Length", selected = c(5.1, 6.4)),
|
|
32 |
#' teal_slice(dataname = "mtcars", varname = "gear", selected = c(4, 5)),
|
|
33 |
#' teal_slice(dataname = "mtcars", varname = "carb", selected = c(4, 10))
|
|
34 |
#' )
|
|
35 |
#'
|
|
36 |
#' # set initial filter state
|
|
37 |
#' set_filter_state(datasets, filter = fs)
|
|
38 |
#'
|
|
39 |
#' # get filter state
|
|
40 |
#' get_filter_state(datasets)
|
|
41 |
#'
|
|
42 |
#' # modify filter state
|
|
43 |
#' set_filter_state(
|
|
44 |
#' datasets,
|
|
45 |
#' teal_slices(
|
|
46 |
#' teal_slice(dataname = "iris", varname = "Species", selected = "setosa", keep_na = TRUE)
|
|
47 |
#' )
|
|
48 |
#' )
|
|
49 |
#'
|
|
50 |
#' # remove specific filters
|
|
51 |
#' remove_filter_state(
|
|
52 |
#' datasets,
|
|
53 |
#' teal_slices(
|
|
54 |
#' teal_slice(dataname = "iris", varname = "Species"),
|
|
55 |
#' teal_slice(dataname = "mtcars", varname = "gear"),
|
|
56 |
#' teal_slice(dataname = "mtcars", varname = "carb")
|
|
57 |
#' )
|
|
58 |
#' )
|
|
59 |
#'
|
|
60 |
#' # remove all states
|
|
61 |
#' clear_filter_states(datasets)
|
|
62 |
#'
|
|
63 |
#' @examples
|
|
64 |
#' \donttest{
|
|
65 |
#' if (requireNamespace("MultiAssayExperiment", quietly = TRUE)) {
|
|
66 |
#' # Requires MultiAssayExperiment from Bioconductor
|
|
67 |
#' data(miniACC, package = "MultiAssayExperiment")
|
|
68 |
#'
|
|
69 |
#' datasets <- init_filtered_data(list(mae = miniACC))
|
|
70 |
#' fs <- teal_slices(
|
|
71 |
#' teal_slice(
|
|
72 |
#' dataname = "mae", varname = "years_to_birth", selected = c(30, 50),
|
|
73 |
#' keep_na = TRUE, keep_inf = FALSE
|
|
74 |
#' ),
|
|
75 |
#' teal_slice(
|
|
76 |
#' dataname = "mae", varname = "vital_status", selected = "1",
|
|
77 |
#' keep_na = FALSE
|
|
78 |
#' ),
|
|
79 |
#' teal_slice(
|
|
80 |
#' dataname = "mae", varname = "gender", selected = "female",
|
|
81 |
#' keep_na = TRUE
|
|
82 |
#' ),
|
|
83 |
#' teal_slice(
|
|
84 |
#' dataname = "mae", varname = "ARRAY_TYPE", selected = "",
|
|
85 |
#' keep_na = TRUE, experiment = "RPPAArray", arg = "subset"
|
|
86 |
#' )
|
|
87 |
#' )
|
|
88 |
#'
|
|
89 |
#' # set initial filter state
|
|
90 |
#' set_filter_state(datasets, filter = fs)
|
|
91 |
#'
|
|
92 |
#' # get filter state
|
|
93 |
#' get_filter_state(datasets)
|
|
94 |
#'
|
|
95 |
#' # modify filter state
|
|
96 |
#' set_filter_state(
|
|
97 |
#' datasets,
|
|
98 |
#' teal_slices(
|
|
99 |
#' teal_slice(dataname = "mae", varname = "years_to_birth", selected = c(40, 60))
|
|
100 |
#' )
|
|
101 |
#' )
|
|
102 |
#'
|
|
103 |
#' # remove specific filters
|
|
104 |
#' remove_filter_state(
|
|
105 |
#' datasets,
|
|
106 |
#' teal_slices(
|
|
107 |
#' teal_slice(dataname = "mae", varname = "years_to_birth"),
|
|
108 |
#' teal_slice(dataname = "mae", varname = "vital_status")
|
|
109 |
#' )
|
|
110 |
#' )
|
|
111 |
#'
|
|
112 |
#' # remove all states
|
|
113 |
#' clear_filter_states(datasets)
|
|
114 |
#' }
|
|
115 |
#' }
|
|
116 |
NULL
|
|
117 | ||
118 |
#' @rdname filter_state_api
|
|
119 |
#' @export
|
|
120 |
set_filter_state <- function(datasets, filter) { |
|
121 | 3x |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
122 | 3x |
checkmate::assert_class(filter, "teal_slices") |
123 | 3x |
datasets$set_filter_state(filter) |
124 | 3x |
invisible(NULL) |
125 |
}
|
|
126 | ||
127 |
#' @rdname filter_state_api
|
|
128 |
#' @export
|
|
129 |
get_filter_state <- function(datasets) { |
|
130 | 4x |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
131 | 4x |
if (isRunning()) { |
132 | ! |
datasets$get_filter_state() |
133 |
} else { |
|
134 | 4x |
isolate(datasets$get_filter_state()) |
135 |
}
|
|
136 |
}
|
|
137 | ||
138 |
#' @rdname filter_state_api
|
|
139 |
#' @export
|
|
140 |
remove_filter_state <- function(datasets, filter) { |
|
141 | 1x |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
142 | 1x |
checkmate::assert_class(filter, "teal_slices") |
143 | ||
144 | 1x |
datasets$remove_filter_state(filter) |
145 | 1x |
invisible(NULL) |
146 |
}
|
|
147 | ||
148 |
#' @rdname filter_state_api
|
|
149 |
#' @export
|
|
150 |
clear_filter_states <- function(datasets, force = FALSE) { |
|
151 | 1x |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
152 | 1x |
datasets$clear_filter_states(force = force) |
153 | 1x |
invisible(NULL) |
154 |
}
|
|
155 | ||
156 |
#' Gets filter expression for multiple `datanames` taking into account its order.
|
|
157 |
#'
|
|
158 |
#' @description `r lifecycle::badge("stable")`
|
|
159 |
#'
|
|
160 |
#' To be used in `Show R Code` button.
|
|
161 |
#'
|
|
162 |
#' @param datasets (`FilteredData`)
|
|
163 |
#' @param datanames (`character`) vector of dataset names
|
|
164 |
#'
|
|
165 |
#' @return A character string containing all subset expressions.
|
|
166 |
#'
|
|
167 |
#' @export
|
|
168 |
#'
|
|
169 |
get_filter_expr <- function(datasets, datanames = datasets$datanames()) { |
|
170 | 2x |
checkmate::check_class(datasets, "FilteredData") |
171 | 2x |
checkmate::assert_character(datanames, any.missing = FALSE) |
172 | 2x |
checkmate::assert_subset(datanames, datasets$datanames()) |
173 | 2x |
paste( |
174 | 2x |
unlist(lapply( |
175 | 2x |
datanames,
|
176 | 2x |
function(dataname) { |
177 | 4x |
datasets$get_call(dataname) |
178 |
}
|
|
179 |
)), |
|
180 | 2x |
collapse = "\n" |
181 |
)
|
|
182 |
}
|
1 |
# DFFilterStates ------
|
|
2 | ||
3 |
#' @name DFFilterStates
|
|
4 |
#' @docType class
|
|
5 |
#'
|
|
6 |
#' @title `FilterStates` subclass for data frames
|
|
7 |
#'
|
|
8 |
#' @description Handles filter states in a `data.frame`.
|
|
9 |
#'
|
|
10 |
#' @examples
|
|
11 |
#' # use non-exported function from teal.slice
|
|
12 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice")
|
|
13 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice")
|
|
14 |
#' init_filter_states <- getFromNamespace("init_filter_states", "teal.slice")
|
|
15 |
#'
|
|
16 |
#' library(shiny)
|
|
17 |
#' library(shinyjs)
|
|
18 |
#'
|
|
19 |
#' # create data frame to filter
|
|
20 |
#' data_df <- data.frame(
|
|
21 |
#' NUM1 = 1:100,
|
|
22 |
#' NUM2 = round(runif(100, min = 20, max = 23)),
|
|
23 |
#' CHAR1 = sample(LETTERS[1:6], size = 100, replace = TRUE),
|
|
24 |
#' CHAR2 = sample(c("M", "F"), size = 100, replace = TRUE),
|
|
25 |
#' DATE = seq(as.Date("2020-01-01"), by = 1, length.out = 100),
|
|
26 |
#' DATETIME = as.POSIXct(seq(as.Date("2020-01-01"), by = 1, length.out = 100))
|
|
27 |
#' )
|
|
28 |
#' data_na <- data.frame(
|
|
29 |
#' NUM1 = NA,
|
|
30 |
#' NUM2 = NA,
|
|
31 |
#' CHAR1 = NA,
|
|
32 |
#' CHAR2 = NA,
|
|
33 |
#' DATE = NA,
|
|
34 |
#' DATETIME = NA
|
|
35 |
#' )
|
|
36 |
#' data_df <- rbind(data_df, data_na)
|
|
37 |
#'
|
|
38 |
#' # initiate `FilterStates` object
|
|
39 |
#' filter_states_df <- init_filter_states(
|
|
40 |
#' data = data_df,
|
|
41 |
#' dataname = "dataset",
|
|
42 |
#' datalabel = ("label")
|
|
43 |
#' )
|
|
44 |
#'
|
|
45 |
#' ui <- fluidPage(
|
|
46 |
#' useShinyjs(),
|
|
47 |
#' include_css_files(pattern = "filter-panel"),
|
|
48 |
#' include_js_files(pattern = "count-bar-labels"),
|
|
49 |
#' column(4, tags$div(
|
|
50 |
#' tags$h4("Active filters"),
|
|
51 |
#' filter_states_df$ui_active("fsdf")
|
|
52 |
#' )),
|
|
53 |
#' column(4, tags$div(
|
|
54 |
#' tags$h4("Manual filter control"),
|
|
55 |
#' filter_states_df$ui_add("add_filters"), tags$br(),
|
|
56 |
#' tags$h4("Condition (i.e. call)"), # display the subset expression generated by this FilterStates
|
|
57 |
#' textOutput("call_df"), tags$br(),
|
|
58 |
#' tags$h4("Formatted state"), # display human readable filter state
|
|
59 |
#' textOutput("formatted_df"), tags$br()
|
|
60 |
#' )),
|
|
61 |
#' column(4, tags$div(
|
|
62 |
#' tags$h4("Programmatic filter control"),
|
|
63 |
#' actionButton("button1_df", "set NUM1 < 30", width = "100%"), tags$br(),
|
|
64 |
#' actionButton("button2_df", "set NUM2 %in% c(20, 21)", width = "100%"), tags$br(),
|
|
65 |
#' actionButton("button3_df", "set CHAR1 %in% c(\"B\", \"C\", \"D\")", width = "100%"), tags$br(),
|
|
66 |
#' actionButton("button4_df", "set CHAR2 == \"F\"", width = "100%"), tags$br(),
|
|
67 |
#' actionButton("button5_df", "set DATE <= 2020-02-02", width = "100%"), tags$br(),
|
|
68 |
#' actionButton("button6_df", "set DATETIME <= 2020-02-02", width = "100%"), tags$br(),
|
|
69 |
#' tags$hr(),
|
|
70 |
#' actionButton("button7_df", "remove NUM1", width = "100%"), tags$br(),
|
|
71 |
#' actionButton("button8_df", "remove NUM2", width = "100%"), tags$br(),
|
|
72 |
#' actionButton("button9_df", "remove CHAR1", width = "100%"), tags$br(),
|
|
73 |
#' actionButton("button10_df", "remove CHAR2", width = "100%"), tags$br(),
|
|
74 |
#' actionButton("button11_df", "remove DATE", width = "100%"), tags$br(),
|
|
75 |
#' actionButton("button12_df", "remove DATETIME", width = "100%"), tags$br(),
|
|
76 |
#' tags$hr(),
|
|
77 |
#' actionButton("button0_df", "clear all filters", width = "100%"), tags$br()
|
|
78 |
#' ))
|
|
79 |
#' )
|
|
80 |
#'
|
|
81 |
#' server <- function(input, output, session) {
|
|
82 |
#' filter_states_df$srv_add("add_filters")
|
|
83 |
#' filter_states_df$srv_active("fsdf")
|
|
84 |
#'
|
|
85 |
#' output$call_df <- renderPrint(filter_states_df$get_call())
|
|
86 |
#' output$formatted_df <- renderText(filter_states_df$format())
|
|
87 |
#'
|
|
88 |
#' observeEvent(input$button1_df, {
|
|
89 |
#' filter_state <- teal_slices(teal_slice("dataset", "NUM1", selected = c(0, 30)))
|
|
90 |
#' filter_states_df$set_filter_state(state = filter_state)
|
|
91 |
#' })
|
|
92 |
#' observeEvent(input$button2_df, {
|
|
93 |
#' filter_state <- teal_slices(teal_slice("dataset", "NUM2", selected = c(20, 21)))
|
|
94 |
#' filter_states_df$set_filter_state(state = filter_state)
|
|
95 |
#' })
|
|
96 |
#' observeEvent(input$button3_df, {
|
|
97 |
#' filter_state <- teal_slices(teal_slice("dataset", "CHAR1", selected = c("B", "C", "D")))
|
|
98 |
#' filter_states_df$set_filter_state(state = filter_state)
|
|
99 |
#' })
|
|
100 |
#' observeEvent(input$button4_df, {
|
|
101 |
#' filter_state <- teal_slices(teal_slice("dataset", "CHAR2", selected = c("F")))
|
|
102 |
#' filter_states_df$set_filter_state(state = filter_state)
|
|
103 |
#' })
|
|
104 |
#' observeEvent(input$button5_df, {
|
|
105 |
#' filter_state <- teal_slices(
|
|
106 |
#' teal_slice("dataset", "DATE", selected = c("2020-01-01", "2020-02-02"))
|
|
107 |
#' )
|
|
108 |
#' filter_states_df$set_filter_state(state = filter_state)
|
|
109 |
#' })
|
|
110 |
#' observeEvent(input$button6_df, {
|
|
111 |
#' filter_state <- teal_slices(
|
|
112 |
#' teal_slice("dataset", "DATETIME", selected = as.POSIXct(c("2020-01-01", "2020-02-02")))
|
|
113 |
#' )
|
|
114 |
#' filter_states_df$set_filter_state(state = filter_state)
|
|
115 |
#' })
|
|
116 |
#'
|
|
117 |
#' observeEvent(input$button7_df, {
|
|
118 |
#' filter_state <- teal_slices(teal_slice("dataset", "NUM1"))
|
|
119 |
#' filter_states_df$remove_filter_state(filter_state)
|
|
120 |
#' })
|
|
121 |
#' observeEvent(input$button8_df, {
|
|
122 |
#' filter_state <- teal_slices(teal_slice("dataset", "NUM2"))
|
|
123 |
#' filter_states_df$remove_filter_state(filter_state)
|
|
124 |
#' })
|
|
125 |
#' observeEvent(input$button9_df, {
|
|
126 |
#' filter_state <- teal_slices(teal_slice("dataset", "CHAR1"))
|
|
127 |
#' filter_states_df$remove_filter_state(filter_state)
|
|
128 |
#' })
|
|
129 |
#' observeEvent(input$button10_df, {
|
|
130 |
#' filter_state <- teal_slices(teal_slice("dataset", "CHAR2"))
|
|
131 |
#' filter_states_df$remove_filter_state(filter_state)
|
|
132 |
#' })
|
|
133 |
#' observeEvent(input$button11_df, {
|
|
134 |
#' filter_state <- teal_slices(
|
|
135 |
#' teal_slice("dataset", "DATE")
|
|
136 |
#' )
|
|
137 |
#' filter_states_df$remove_filter_state(filter_state)
|
|
138 |
#' })
|
|
139 |
#' observeEvent(input$button12_df, {
|
|
140 |
#' filter_state <- teal_slices(
|
|
141 |
#' teal_slice("dataset", "DATETIME", selected = as.POSIXct(c("2020-01-01", "2020-02-02")))
|
|
142 |
#' )
|
|
143 |
#' filter_states_df$remove_filter_state(filter_state)
|
|
144 |
#' })
|
|
145 |
#'
|
|
146 |
#' observeEvent(input$button0_df, filter_states_df$clear_filter_states())
|
|
147 |
#' }
|
|
148 |
#'
|
|
149 |
#' if (interactive()) {
|
|
150 |
#' shinyApp(ui, server)
|
|
151 |
#' }
|
|
152 |
#' @keywords internal
|
|
153 |
#'
|
|
154 |
DFFilterStates <- R6::R6Class( # nolint |
|
155 |
classname = "DFFilterStates", |
|
156 |
inherit = FilterStates, |
|
157 | ||
158 |
# public methods ----
|
|
159 |
public = list( |
|
160 |
#' @description
|
|
161 |
#' Initializes `DFFilterStates` object by setting `dataname`
|
|
162 |
#' and initializing `state_list` (`shiny::reactiveVal`).
|
|
163 |
#' This class contains a single `state_list` with no specified name,
|
|
164 |
#' which means that when calling the subset function associated with this class
|
|
165 |
#' (`dplyr::filter`), a list of conditions is passed to unnamed arguments (`...`).
|
|
166 |
#'
|
|
167 |
#' @param data (`data.frame`)
|
|
168 |
#' the `R` object which `dplyr::filter` function will be applied on.
|
|
169 |
#' @param data_reactive (`function(sid)`)
|
|
170 |
#' should return a `data.frame` object or `NULL`.
|
|
171 |
#' This object is needed for the `FilterState` counts being updated on a change in filters.
|
|
172 |
#' If function returns `NULL` then filtered counts are not shown.
|
|
173 |
#' Function has to have `sid` argument being a character.
|
|
174 |
#' @param dataname (`character`)
|
|
175 |
#' name of the data used in the *subset expression*.
|
|
176 |
#' Passed to the function argument attached to this `FilterStates`.
|
|
177 |
#' @param datalabel (`character(1)`) optional
|
|
178 |
#' text label.
|
|
179 |
#' @param keys (`character`)
|
|
180 |
#' key column names.
|
|
181 |
#'
|
|
182 |
initialize = function(data, |
|
183 |
data_reactive = function(sid = "") NULL, |
|
184 |
dataname,
|
|
185 |
datalabel = NULL, |
|
186 |
keys = character(0)) { |
|
187 | 99x |
checkmate::assert_function(data_reactive, args = "sid") |
188 | 99x |
checkmate::assert_data_frame(data) |
189 | 99x |
super$initialize(data, data_reactive, dataname, datalabel) |
190 | 99x |
private$keys <- keys |
191 | 99x |
private$set_filterable_varnames(include_varnames = colnames(private$data)) |
192 |
}
|
|
193 |
),
|
|
194 | ||
195 |
# private members ----
|
|
196 |
private = list( |
|
197 |
fun = quote(dplyr::filter) |
|
198 |
)
|
|
199 |
)
|
1 |
#' Initialize `FilteredDataset`
|
|
2 |
#'
|
|
3 |
#' Initializes a `FilteredDataset` object corresponding to the class of the filtered dataset.
|
|
4 |
#'
|
|
5 |
#' @param dataset any object
|
|
6 |
#' @param dataname (`character(1)`)
|
|
7 |
#' syntactically valid name given to the dataset.
|
|
8 |
#' @param keys (`character`) optional
|
|
9 |
#' vector of primary key column names.
|
|
10 |
#' @param parent_name (`character(1)`)
|
|
11 |
#' name of the parent dataset.
|
|
12 |
#' @param parent (`reactive`)
|
|
13 |
#' that returns a filtered `data.frame` from other `FilteredDataset` named `parent_name`.
|
|
14 |
#' Passing `parent` results in a `reactive` link that causes re-filtering of this `dataset`
|
|
15 |
#' based on the changes in `parent`.
|
|
16 |
#' @param join_keys (`character`)
|
|
17 |
#' vector of names of columns in this dataset to join with `parent` dataset.
|
|
18 |
#' If column names in the parent do not match these, they should be given as the names of this vector.
|
|
19 |
#' @param label (`character(1)`)
|
|
20 |
#' label to describe the dataset.
|
|
21 |
#'
|
|
22 |
#' @return Object of class `FilteredDataset`.
|
|
23 |
#'
|
|
24 |
#' @section Warning:
|
|
25 |
#' This function is exported to allow other packages to extend `teal.slice` but it is treated as internal.
|
|
26 |
#' Breaking changes may occur without warning.
|
|
27 |
#' We recommend consulting the package maintainer before using it.
|
|
28 |
#'
|
|
29 |
#' @examples
|
|
30 |
#' # DataframeFilteredDataset example
|
|
31 |
#' library(shiny)
|
|
32 |
#'
|
|
33 |
#' iris_fd <- init_filtered_dataset(iris, dataname = "iris")
|
|
34 |
#' ui <- fluidPage(
|
|
35 |
#' iris_fd$ui_add(id = "add"),
|
|
36 |
#' iris_fd$ui_active("dataset"),
|
|
37 |
#' verbatimTextOutput("call")
|
|
38 |
#' )
|
|
39 |
#' server <- function(input, output, session) {
|
|
40 |
#' iris_fd$srv_add(id = "add")
|
|
41 |
#' iris_fd$srv_active(id = "dataset")
|
|
42 |
#'
|
|
43 |
#' output$call <- renderText({
|
|
44 |
#' paste(
|
|
45 |
#' vapply(iris_fd$get_call(), deparse1, character(1), collapse = "\n"),
|
|
46 |
#' collapse = "\n"
|
|
47 |
#' )
|
|
48 |
#' })
|
|
49 |
#' }
|
|
50 |
#' if (interactive()) {
|
|
51 |
#' shinyApp(ui, server)
|
|
52 |
#' }
|
|
53 |
#'
|
|
54 |
#' @examples
|
|
55 |
#' \donttest{
|
|
56 |
#' if (requireNamespace("MultiAssayExperiment", quietly = TRUE)) {
|
|
57 |
#' # MAEFilteredDataset example
|
|
58 |
#' library(shiny)
|
|
59 |
#'
|
|
60 |
#' data(miniACC, package = "MultiAssayExperiment")
|
|
61 |
#'
|
|
62 |
#' MAE_fd <- init_filtered_dataset(miniACC, "MAE")
|
|
63 |
#' ui <- fluidPage(
|
|
64 |
#' MAE_fd$ui_add(id = "add"),
|
|
65 |
#' MAE_fd$ui_active("dataset"),
|
|
66 |
#' verbatimTextOutput("call")
|
|
67 |
#' )
|
|
68 |
#' server <- function(input, output, session) {
|
|
69 |
#' MAE_fd$srv_add(id = "add")
|
|
70 |
#' MAE_fd$srv_active(id = "dataset")
|
|
71 |
#' output$call <- renderText({
|
|
72 |
#' paste(
|
|
73 |
#' vapply(MAE_fd$get_call(), deparse1, character(1), collapse = "\n"),
|
|
74 |
#' collapse = "\n"
|
|
75 |
#' )
|
|
76 |
#' })
|
|
77 |
#' }
|
|
78 |
#' if (interactive()) {
|
|
79 |
#' shinyApp(ui, server)
|
|
80 |
#' }
|
|
81 |
#' }
|
|
82 |
#' }
|
|
83 |
#' @keywords internal
|
|
84 |
#' @export
|
|
85 |
init_filtered_dataset <- function(dataset, |
|
86 |
dataname,
|
|
87 |
keys = character(0), |
|
88 |
parent_name = character(0), |
|
89 |
parent = reactive(dataset), |
|
90 |
join_keys = character(0), |
|
91 |
label = attr(dataset, "label", exact = TRUE)) { |
|
92 | 101x |
UseMethod("init_filtered_dataset") |
93 |
}
|
|
94 | ||
95 |
#' @keywords internal
|
|
96 |
#' @export
|
|
97 |
init_filtered_dataset.data.frame <- function(dataset, |
|
98 |
dataname,
|
|
99 |
keys = character(0), |
|
100 |
parent_name = character(0), |
|
101 |
parent = NULL, |
|
102 |
join_keys = character(0), |
|
103 |
label = attr(dataset, "label", exact = TRUE)) { |
|
104 | 79x |
DataframeFilteredDataset$new( |
105 | 79x |
dataset = dataset, |
106 | 79x |
dataname = dataname, |
107 | 79x |
keys = keys, |
108 | 79x |
parent_name = parent_name, |
109 | 79x |
parent = parent, |
110 | 79x |
join_keys = join_keys, |
111 | 79x |
label = label |
112 |
)
|
|
113 |
}
|
|
114 | ||
115 |
#' @keywords internal
|
|
116 |
#' @export
|
|
117 |
init_filtered_dataset.MultiAssayExperiment <- function(dataset, |
|
118 |
dataname,
|
|
119 |
keys = character(0), |
|
120 |
parent_name, # ignored |
|
121 |
parent, # ignored |
|
122 |
join_keys, # ignored |
|
123 |
label = attr(dataset, "label", exact = TRUE)) { |
|
124 | 5x |
if (!requireNamespace("MultiAssayExperiment", quietly = TRUE)) { |
125 | ! |
stop("Cannot load MultiAssayExperiment - please install the package or restart your session.") |
126 |
}
|
|
127 | 5x |
MAEFilteredDataset$new( |
128 | 5x |
dataset = dataset, |
129 | 5x |
dataname = dataname, |
130 | 5x |
keys = keys, |
131 | 5x |
label = label |
132 |
)
|
|
133 |
}
|
|
134 | ||
135 |
#' @keywords internal
|
|
136 |
#' @export
|
|
137 |
init_filtered_dataset.default <- function(dataset, |
|
138 |
dataname,
|
|
139 |
keys, # ignored |
|
140 |
parent_name, # ignored |
|
141 |
parent, # ignored |
|
142 |
join_keys, # ignored |
|
143 |
label = attr(dataset, "label", exact = TRUE)) { |
|
144 | 17x |
DefaultFilteredDataset$new( |
145 | 17x |
dataset = dataset, |
146 | 17x |
dataname = dataname, |
147 | 17x |
label = label |
148 |
)
|
|
149 |
}
|
1 |
# MatrixFilterStates ------
|
|
2 | ||
3 |
#' @name MatrixFilterStates
|
|
4 |
#' @docType class
|
|
5 |
#' @title `FilterStates` subclass for matrices
|
|
6 |
#' @description Handles filter states in a `matrix`.
|
|
7 |
#' @keywords internal
|
|
8 |
#'
|
|
9 |
MatrixFilterStates <- R6::R6Class( # nolint |
|
10 |
classname = "MatrixFilterStates", |
|
11 |
inherit = FilterStates, |
|
12 | ||
13 |
# public methods ----
|
|
14 |
public = list( |
|
15 |
#' @description
|
|
16 |
#' Initialize `MatrixFilterStates` object.
|
|
17 |
#'
|
|
18 |
#' @param data (`matrix`)
|
|
19 |
#' the `R` object which `subset` function is applied on.
|
|
20 |
#' @param data_reactive (`function(sid)`)
|
|
21 |
#' should return a `matrix` object or `NULL`.
|
|
22 |
#' This object is needed for the `FilterState` counts being updated on a change in filters.
|
|
23 |
#' If function returns `NULL` then filtered counts are not shown.
|
|
24 |
#' Function has to have `sid` argument being a character.
|
|
25 |
#' @param dataname (`character(1)`)
|
|
26 |
#' name of the data used in the subset expression.
|
|
27 |
#' Passed to the function argument attached to this `FilterStates`.
|
|
28 |
#' @param datalabel (`character(1)`) optional
|
|
29 |
#' text label. Should be a name of experiment.
|
|
30 |
#'
|
|
31 |
initialize = function(data, |
|
32 |
data_reactive = function(sid = "") NULL, |
|
33 |
dataname,
|
|
34 |
datalabel = NULL) { |
|
35 | 24x |
checkmate::assert_matrix(data) |
36 | 23x |
super$initialize(data, data_reactive, dataname, datalabel) |
37 | 23x |
private$set_filterable_varnames(include_varnames = colnames(private$data)) |
38 | 23x |
if (!is.null(datalabel)) { |
39 | 19x |
private$dataname_prefixed <- sprintf( |
40 | 19x |
"%s[['%s']]", private$dataname_prefixed, datalabel |
41 |
)
|
|
42 |
}
|
|
43 |
}
|
|
44 |
),
|
|
45 |
private = list( |
|
46 |
extract_type = "matrix" |
|
47 |
)
|
|
48 |
)
|