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 |
#' isolate(datasets$get_call("iris")) |
|
54 |
#' |
|
55 |
#' datasets$set_filter_state( |
|
56 |
#' teal_slices(teal_slice(dataname = "mtcars", varname = "mpg", selected = c(15, 20))) |
|
57 |
#' ) |
|
58 |
#' |
|
59 |
#' isolate(datasets$get_filter_state()) |
|
60 |
#' isolate(datasets$get_call("iris")) |
|
61 |
#' isolate(datasets$get_call("mtcars")) |
|
62 |
#' |
|
63 |
#' @examplesIf requireNamespace("MultiAssayExperiment") |
|
64 |
#' ### set_filter_state |
|
65 |
#' library(shiny) |
|
66 |
#' |
|
67 |
#' data(miniACC, package = "MultiAssayExperiment") |
|
68 |
#' datasets <- FilteredData$new(list(iris = iris, mae = miniACC)) |
|
69 |
#' fs <- teal_slices( |
|
70 |
#' teal_slice( |
|
71 |
#' dataname = "iris", varname = "Sepal.Length", selected = c(5.1, 6.4), |
|
72 |
#' keep_na = TRUE, keep_inf = FALSE |
|
73 |
#' ), |
|
74 |
#' teal_slice( |
|
75 |
#' dataname = "iris", varname = "Species", selected = c("setosa", "versicolor"), |
|
76 |
#' keep_na = FALSE |
|
77 |
#' ), |
|
78 |
#' teal_slice( |
|
79 |
#' dataname = "mae", varname = "years_to_birth", selected = c(30, 50), |
|
80 |
#' keep_na = TRUE, keep_inf = FALSE |
|
81 |
#' ), |
|
82 |
#' teal_slice(dataname = "mae", varname = "vital_status", selected = "1", keep_na = FALSE), |
|
83 |
#' teal_slice(dataname = "mae", varname = "gender", selected = "female", keep_na = TRUE), |
|
84 |
#' teal_slice( |
|
85 |
#' dataname = "mae", varname = "ARRAY_TYPE", |
|
86 |
#' selected = "", keep_na = TRUE, experiment = "RPPAArray", arg = "subset" |
|
87 |
#' ) |
|
88 |
#' ) |
|
89 |
#' datasets$set_filter_state(state = fs) |
|
90 |
#' isolate(datasets$get_filter_state()) |
|
91 |
#' |
|
92 |
#' @keywords internal |
|
93 |
#' |
|
94 |
FilteredData <- R6::R6Class( # nolint |
|
95 |
"FilteredData", |
|
96 |
# public methods ---- |
|
97 |
public = list( |
|
98 |
#' @description |
|
99 |
#' Initialize a `FilteredData` object. |
|
100 |
#' @param data_objects (`named list`) |
|
101 |
#' List of data objects. |
|
102 |
#' Names of the list will be used as `dataname` for respective datasets. |
|
103 |
#' @param join_keys (`join_keys`) optional joining keys, see [`teal.data::join_keys()`]. |
|
104 |
#' |
|
105 |
initialize = function(data_objects, join_keys = teal.data::join_keys()) { |
|
106 | 67x |
checkmate::assert_list(data_objects, any.missing = FALSE, min.len = 0, names = "unique") |
107 |
# unpack data.object from the nested list |
|
108 | 67x |
data_objects <- lapply(data_objects, function(dataset) { |
109 | 101x |
if (is.list(dataset) && "dataset" %in% names(dataset)) { |
110 | ! |
dataset$dataset |
111 |
} else { |
|
112 | 101x |
dataset |
113 |
} |
|
114 |
}) |
|
115 | ||
116 |
# Note the internals of data_objects are checked in set_dataset |
|
117 | 67x |
checkmate::assert_class(join_keys, "join_keys") |
118 | 66x |
self$set_join_keys(join_keys) |
119 | 66x |
child_parent <- sapply( |
120 | 66x |
names(data_objects), |
121 | 66x |
function(i) teal.data::parent(join_keys, i), |
122 | 66x |
USE.NAMES = TRUE, |
123 | 66x |
simplify = FALSE |
124 |
) |
|
125 | 66x |
ordered_datanames <- topological_sort(child_parent) |
126 | 66x |
ordered_datanames <- intersect(ordered_datanames, names(data_objects)) |
127 | ||
128 | 66x |
for (dataname in ordered_datanames) { |
129 | 100x |
ds_object <- data_objects[[dataname]] |
130 | 100x |
self$set_dataset(data = ds_object, dataname = dataname) |
131 |
} |
|
132 | ||
133 | 66x |
self$set_available_teal_slices(x = reactive(NULL)) |
134 | ||
135 | 66x |
invisible(self) |
136 |
}, |
|
137 | ||
138 |
#' @description |
|
139 |
#' Gets `datanames`. |
|
140 |
#' @details |
|
141 |
#' The `datanames` are returned in the order in which they must be evaluated (in case of dependencies). |
|
142 |
#' @return Character vector. |
|
143 |
datanames = function() { |
|
144 | 118x |
names(private$filtered_datasets) |
145 |
}, |
|
146 | ||
147 |
#' @description |
|
148 |
#' Gets data label for the dataset. |
|
149 |
#' Useful to display in `Show R Code`. |
|
150 |
#' |
|
151 |
#' @param dataname (`character(1)`) name of the dataset |
|
152 |
#' @return Character string. |
|
153 |
get_datalabel = function(dataname) { |
|
154 | 1x |
private$get_filtered_dataset(dataname)$get_dataset_label() |
155 |
}, |
|
156 | ||
157 |
#' @description |
|
158 |
#' Set list of external filter states available for activation. |
|
159 |
#' @details |
|
160 |
#' Unlike adding new filter from the column, these filters can come with some prespecified settings. |
|
161 |
#' `teal_slices` are wrapped in a `reactive` so they can be updated from elsewhere in the app. |
|
162 |
#' Filters passed in `x` are limited to those that can be set for this `FilteredData` object, |
|
163 |
#' i.e. they have the correct `dataname` and `varname` (waived `teal_slice_fixed` as they do not have `varname`). |
|
164 |
#' List is accessible in `ui/srv_active` through `ui/srv_available_filters`. |
|
165 |
#' @param x (`reactive`) |
|
166 |
#' should return `teal_slices` |
|
167 |
#' @return `NULL`, invisibly. |
|
168 |
set_available_teal_slices = function(x) { |
|
169 | 67x |
checkmate::assert_class(x, "reactive") |
170 | 67x |
private$available_teal_slices <- reactive({ |
171 |
# Available filters should be limited to the ones relevant for this FilteredData. |
|
172 | 4x |
current_state <- isolate(self$get_filter_state()) |
173 | 4x |
allowed <- attr(current_state, "include_varnames") |
174 | 4x |
forbidden <- attr(current_state, "exclude_varnames") |
175 | 4x |
foo <- function(slice) { |
176 | 13x |
if (slice$dataname %in% self$datanames()) { |
177 | 13x |
if (slice$fixed) { |
178 | 4x |
TRUE |
179 |
} else { |
|
180 | 9x |
isTRUE(slice$varname %in% allowed[[slice$dataname]]) || |
181 | 9x |
isFALSE(slice$varname %in% forbidden[[slice$dataname]]) |
182 |
} |
|
183 |
} else { |
|
184 | ! |
FALSE |
185 |
} |
|
186 |
} |
|
187 | 4x |
Filter(foo, x()) |
188 |
}) |
|
189 | 67x |
invisible(NULL) |
190 |
}, |
|
191 | ||
192 |
#' @description |
|
193 |
#' Get list of filter states available for this object. |
|
194 |
#' @details |
|
195 |
#' All `teal_slice` objects that have been created since the beginning of the app session |
|
196 |
#' are stored in one `teal_slices` object. This returns a subset of that `teal_slices`, |
|
197 |
#' describing filter states that can be set for this object. |
|
198 |
#' @return `reactive` that returns `teal_slices`. |
|
199 |
get_available_teal_slices = function() { |
|
200 | 4x |
private$available_teal_slices |
201 |
}, |
|
202 | ||
203 |
# datasets methods ---- |
|
204 | ||
205 |
#' @description |
|
206 |
#' Gets a `call` to filter the dataset according to the filter state. |
|
207 |
#' @details |
|
208 |
#' It returns a `call` to filter the dataset only, assuming the |
|
209 |
#' other (filtered) datasets it depends on are available. |
|
210 |
#' |
|
211 |
#' Together with `self$datanames()` which returns the datasets in the correct |
|
212 |
#' evaluation order, this generates the whole filter code, see the function |
|
213 |
#' `FilteredData$get_filter_code`. |
|
214 |
#' |
|
215 |
#' For the return type, note that `rlang::is_expression` returns `TRUE` on the |
|
216 |
#' return type, both for base `R` expressions and calls (single expression, |
|
217 |
#' capturing a function call). |
|
218 |
#' |
|
219 |
#' The filtered dataset has the name given by `self$filtered_dataname(dataname)` |
|
220 |
#' |
|
221 |
#' This can be used for the `Show R Code` generation. |
|
222 |
#' |
|
223 |
#' @param dataname (`character(1)`) name of the dataset |
|
224 |
#' |
|
225 |
#' @return A list of `call`s. |
|
226 |
#' |
|
227 |
get_call = function(dataname) { |
|
228 | 10x |
checkmate::assert_subset(dataname, self$datanames()) |
229 | 9x |
private$get_filtered_dataset(dataname)$get_call() |
230 |
}, |
|
231 | ||
232 |
#' @description |
|
233 |
#' Gets filtered or unfiltered dataset. |
|
234 |
#' |
|
235 |
#' For `filtered = FALSE`, the original data set with `set_data` is returned including all attributes. |
|
236 |
#' |
|
237 |
#' @param dataname (`character(1)`) name of the dataset. |
|
238 |
#' @param filtered (`logical(1)`) whether to return a filtered or unfiltered dataset. |
|
239 |
#' |
|
240 |
#' @return A data object, a `data.frame` or a `MultiAssayExperiment`. |
|
241 |
#' |
|
242 |
get_data = function(dataname, filtered = TRUE) { |
|
243 | 24x |
checkmate::assert_subset(dataname, self$datanames()) |
244 | 23x |
checkmate::assert_flag(filtered) |
245 | 22x |
data <- private$get_filtered_dataset(dataname)$get_dataset(filtered) |
246 | 3x |
if (filtered) data() else data |
247 |
}, |
|
248 | ||
249 |
#' @description |
|
250 |
#' Get join keys between two datasets. |
|
251 |
#' |
|
252 |
#' @return `join_keys` |
|
253 |
#' |
|
254 |
get_join_keys = function() { |
|
255 | 2x |
private$join_keys |
256 |
}, |
|
257 | ||
258 |
#' @description |
|
259 |
#' Creates filter overview table to be displayed in the application. |
|
260 |
#' One row is created per dataset, according to the `get_filter_overview` methods |
|
261 |
#' of the contained `FilteredDataset` objects. |
|
262 |
#' |
|
263 |
#' @param datanames (`character`) vector of dataset names. |
|
264 |
#' |
|
265 |
#' @return A `data.frame` listing the numbers of observations in all datasets. |
|
266 |
#' |
|
267 |
get_filter_overview = function(datanames) { |
|
268 | 9x |
rows <- lapply( |
269 | 9x |
datanames, |
270 | 9x |
function(dataname) { |
271 | 11x |
private$get_filtered_dataset(dataname)$get_filter_overview() |
272 |
} |
|
273 |
) |
|
274 | 5x |
unssuported_idx <- vapply(rows, function(x) all(is.na(x[-1])), logical(1)) |
275 | 5x |
dplyr::bind_rows(c(rows[!unssuported_idx], rows[unssuported_idx])) |
276 |
}, |
|
277 | ||
278 |
#' @description |
|
279 |
#' Get keys for the dataset. |
|
280 |
#' |
|
281 |
#' @param dataname (`character(1)`) name of the dataset. |
|
282 |
#' |
|
283 |
#' @return Character vector of key column names. |
|
284 |
#' |
|
285 |
get_keys = function(dataname) { |
|
286 | 1x |
private$get_filtered_dataset(dataname)$get_keys() |
287 |
}, |
|
288 | ||
289 |
#' @description |
|
290 |
#' Adds a dataset to this `FilteredData`. |
|
291 |
#' |
|
292 |
#' @details |
|
293 |
#' `set_dataset` creates a `FilteredDataset` object which keeps `dataset` for the filtering purpose. |
|
294 |
#' If this data has a parent specified in the `join_keys` object stored in `private$join_keys` |
|
295 |
#' then created `FilteredDataset` (child) gets linked with other `FilteredDataset` (parent). |
|
296 |
#' "Child" dataset return filtered data then dependent on the reactive filtered data of the |
|
297 |
#' "parent". See more in documentation of `parent` argument in `DataframeFilteredDataset` constructor. |
|
298 |
#' |
|
299 |
#' @param data (`data.frame` or `MultiAssayExperiment`) |
|
300 |
#' data to be filtered. |
|
301 |
#' |
|
302 |
#' @param dataname (`character(1)`) |
|
303 |
#' the name of the `dataset` to be added to this object. |
|
304 |
#' |
|
305 |
#' @return `self`, invisibly. |
|
306 |
#' |
|
307 |
set_dataset = function(data, dataname) { |
|
308 | 105x |
checkmate::assert_string(dataname) |
309 | 105x |
logger::log_trace("FilteredData$set_dataset setting dataset, name: { dataname }") |
310 |
# to include it nicely in the Show R Code; |
|
311 |
# the UI also uses `datanames` in ids, so no whitespaces allowed |
|
312 | 105x |
check_simple_name(dataname) |
313 | ||
314 | 105x |
parent_dataname <- teal.data::parent(private$join_keys, dataname) |
315 | 105x |
keys <- private$join_keys[dataname, dataname] |
316 | 104x |
if (is.null(keys)) keys <- character(0) |
317 | ||
318 | 105x |
if (length(parent_dataname) == 0) { |
319 | 95x |
private$filtered_datasets[[dataname]] <- init_filtered_dataset( |
320 | 95x |
dataset = data, |
321 | 95x |
dataname = dataname, |
322 | 95x |
keys = keys |
323 |
) |
|
324 |
} else { |
|
325 | 10x |
join_keys <- private$join_keys[dataname, parent_dataname] |
326 | ! |
if (is.null(join_keys)) join_keys <- character(0) |
327 | 10x |
private$filtered_datasets[[dataname]] <- init_filtered_dataset( |
328 | 10x |
dataset = data, |
329 | 10x |
dataname = dataname, |
330 | 10x |
keys = keys, |
331 | 10x |
parent_name = parent_dataname, |
332 | 10x |
parent = reactive(self$get_data(parent_dataname, filtered = TRUE)), |
333 | 10x |
join_keys = join_keys |
334 |
) |
|
335 |
} |
|
336 | ||
337 | 105x |
invisible(self) |
338 |
}, |
|
339 | ||
340 |
#' @description |
|
341 |
#' Set the `join_keys`. |
|
342 |
#' |
|
343 |
#' @param join_keys (`join_keys`), see [`teal.data::join_keys()`]. |
|
344 |
#' |
|
345 |
#' @return `self`, invisibly. |
|
346 |
#' |
|
347 |
set_join_keys = function(join_keys) { |
|
348 | 66x |
checkmate::assert_class(join_keys, "join_keys") |
349 | 66x |
private$join_keys <- join_keys |
350 | 66x |
invisible(self) |
351 |
}, |
|
352 | ||
353 |
# Functions useful for restoring from another dataset ---- |
|
354 | ||
355 |
#' @description |
|
356 |
#' Gets states of all contained `FilterState` objects. |
|
357 |
#' |
|
358 |
#' @return A `teal_slices` object. |
|
359 |
#' |
|
360 |
get_filter_state = function() { |
|
361 | 53x |
states <- unname(lapply(private$filtered_datasets, function(x) x$get_filter_state())) |
362 | 53x |
slices <- Filter(Negate(is.null), states) |
363 | 53x |
slices <- do.call(c, slices) |
364 | 53x |
if (!is.null(slices)) { |
365 | 53x |
attr(slices, "allow_add") <- private$allow_add |
366 |
} |
|
367 | 53x |
slices |
368 |
}, |
|
369 | ||
370 |
#' @description |
|
371 |
#' Returns a formatted string representing this `FilteredData` object. |
|
372 |
#' |
|
373 |
#' @param show_all (`logical(1)`) passed to `format.teal_slice`. |
|
374 |
#' @param trim_lines (`logical(1)`) passed to `format.teal_slice`. |
|
375 |
#' |
|
376 |
#' @return `character(1)` the formatted string. |
|
377 |
#' |
|
378 |
format = function(show_all = FALSE, trim_lines = TRUE) { |
|
379 | 5x |
datasets <- lapply(self$datanames(), private$get_filtered_dataset) |
380 | 5x |
ind <- vapply(datasets, inherits, logical(1L), what = "DefaultFilteredDataset") |
381 | 5x |
states <- do.call(c, lapply(datasets[!ind], function(ds) ds$get_filter_state())) |
382 | 5x |
states_fmt <- format(states, show_all = show_all, trim_lines = trim_lines) |
383 | 5x |
holders_fmt <- vapply(datasets[ind], format, character(1L), show_all = show_all, trim_lines = trim_lines) |
384 | ||
385 | 5x |
sprintf( |
386 | 5x |
"%s:\n%s", |
387 | 5x |
class(self)[1], |
388 | 5x |
paste(c(states_fmt, holders_fmt), collapse = "\n") |
389 |
) |
|
390 |
}, |
|
391 | ||
392 |
#' @description |
|
393 |
#' Prints this `FilteredData` object. |
|
394 |
#' |
|
395 |
#' @param ... additional arguments passed to `format`. |
|
396 |
#' |
|
397 |
print = function(...) { |
|
398 | ! |
cat(isolate(self$format(...)), "\n") |
399 |
}, |
|
400 | ||
401 |
#' @description |
|
402 |
#' Sets active filter states. |
|
403 |
#' |
|
404 |
#' @param state (`teal_slices`) |
|
405 |
#' |
|
406 |
#' @return `NULL`, invisibly. |
|
407 |
set_filter_state = function(state) { |
|
408 | 31x |
isolate({ |
409 | 31x |
logger::log_trace("{ class(self)[1] }$set_filter_state initializing") |
410 | 31x |
checkmate::assert_class(state, "teal_slices") |
411 | 31x |
allow_add <- attr(state, "allow_add") |
412 | 31x |
if (!is.null(allow_add)) { |
413 | 31x |
private$allow_add <- allow_add |
414 |
} |
|
415 | ||
416 | 31x |
lapply(self$datanames(), function(dataname) { |
417 | 63x |
states <- Filter(function(x) identical(x$dataname, dataname), state) |
418 | 63x |
private$get_filtered_dataset(dataname)$set_filter_state(states) |
419 |
}) |
|
420 | ||
421 | 31x |
logger::log_trace("{ class(self)[1] }$set_filter_state initialized") |
422 |
}) |
|
423 | ||
424 | 31x |
invisible(NULL) |
425 |
}, |
|
426 | ||
427 |
#' @description |
|
428 |
#' Removes one or more `FilterState` from a `FilteredData` object. |
|
429 |
#' |
|
430 |
#' @param state (`teal_slices`) |
|
431 |
#' specifying `FilterState` objects to remove; |
|
432 |
#' `teal_slice`s may contain only `dataname` and `varname`, other elements are ignored. |
|
433 |
#' |
|
434 |
#' @return `NULL`, invisibly. |
|
435 |
#' |
|
436 |
remove_filter_state = function(state) { |
|
437 | 8x |
isolate({ |
438 | 8x |
checkmate::assert_class(state, "teal_slices") |
439 | 8x |
datanames <- unique(vapply(state, "[[", character(1L), "dataname")) |
440 | 8x |
checkmate::assert_subset(datanames, self$datanames()) |
441 | ||
442 | 8x |
logger::log_trace( |
443 | 8x |
"{ class(self)[1] }$remove_filter_state removing filter(s), dataname: { private$dataname }" |
444 |
) |
|
445 | ||
446 | 8x |
lapply(datanames, function(dataname) { |
447 | 9x |
slices <- Filter(function(x) identical(x$dataname, dataname), state) |
448 | 9x |
private$get_filtered_dataset(dataname)$remove_filter_state(slices) |
449 |
}) |
|
450 | ||
451 | 8x |
logger::log_trace( |
452 | 8x |
"{ class(self)[1] }$remove_filter_state removed filter(s), dataname: { private$dataname }" |
453 |
) |
|
454 |
}) |
|
455 | ||
456 | 8x |
invisible(NULL) |
457 |
}, |
|
458 | ||
459 |
#' @description |
|
460 |
#' Remove all `FilterStates` of a `FilteredDataset` or all `FilterStates` of a `FilteredData` object. |
|
461 |
#' |
|
462 |
#' @param datanames (`character`) |
|
463 |
#' names of datasets for which to remove all filter states. |
|
464 |
#' Defaults to all datasets in this `FilteredData` object. |
|
465 |
#' @param force (`logical(1)`) |
|
466 |
#' flag specifying whether to include anchored filter states. |
|
467 |
#' |
|
468 |
#' @return `NULL`, invisibly. |
|
469 |
#' |
|
470 |
clear_filter_states = function(datanames = self$datanames(), force = FALSE) { |
|
471 | 7x |
logger::log_trace( |
472 | 7x |
"FilteredData$clear_filter_states called, datanames: { toString(datanames) }" |
473 |
) |
|
474 | ||
475 | 7x |
for (dataname in datanames) { |
476 | 12x |
fdataset <- private$get_filtered_dataset(dataname = dataname) |
477 | 12x |
fdataset$clear_filter_states(force) |
478 |
} |
|
479 | ||
480 | 7x |
logger::log_trace( |
481 | 7x |
paste( |
482 | 7x |
"FilteredData$clear_filter_states removed all non-anchored FilterStates,", |
483 | 7x |
"datanames: { toString(datanames) }" |
484 |
) |
|
485 |
) |
|
486 | ||
487 | 7x |
invisible(NULL) |
488 |
}, |
|
489 | ||
490 | ||
491 |
# shiny modules ----- |
|
492 | ||
493 |
#' @description |
|
494 |
#' top-level `shiny` module for the filter panel in the `teal` app. |
|
495 |
#' Contains 1) filter overview panel, 2) filter active panel, and 3) add filters panel. |
|
496 |
#' |
|
497 |
#' @param id (`character(1)`) |
|
498 |
#' `shiny` module instance id. |
|
499 |
#' @return `shiny.tag` |
|
500 |
ui_filter_panel = function(id) { |
|
501 | ! |
ns <- NS(id) |
502 | ! |
tags$div( |
503 | ! |
id = ns(NULL), # used for hiding / showing |
504 | ! |
include_css_files(pattern = "filter-panel"), |
505 | ! |
self$ui_overview(ns("overview")), |
506 | ! |
self$ui_active(ns("active")), |
507 | ! |
if (private$allow_add) { |
508 | ! |
self$ui_add(ns("add")) |
509 |
} |
|
510 |
) |
|
511 |
}, |
|
512 | ||
513 |
#' @description |
|
514 |
#' Server function for filter panel. |
|
515 |
#' |
|
516 |
#' @param id (`character(1)`) |
|
517 |
#' `shiny` module instance id. |
|
518 |
#' @param active_datanames (`function` or `reactive`) |
|
519 |
#' returning `datanames` that should be shown on the filter panel. |
|
520 |
#' Must be a subset of the `datanames` in this `FilteredData`. |
|
521 |
#' If the function returns `NULL` (as opposed to `character(0)`), |
|
522 |
#' the filter panel will be hidden. |
|
523 |
#' @return `NULL`. |
|
524 |
srv_filter_panel = function(id, active_datanames = self$datanames) { |
|
525 | 1x |
checkmate::assert_function(active_datanames) |
526 | 1x |
moduleServer( |
527 | 1x |
id = id, |
528 | 1x |
function(input, output, session) { |
529 | 1x |
logger::log_trace("FilteredData$srv_filter_panel initializing") |
530 | ||
531 | 1x |
active_datanames_resolved <- reactive({ |
532 | 1x |
checkmate::assert_subset(active_datanames(), self$datanames()) |
533 | ! |
active_datanames() |
534 |
}) |
|
535 | ||
536 | 1x |
self$srv_overview("overview", active_datanames_resolved) |
537 | 1x |
self$srv_active("active", active_datanames_resolved) |
538 | 1x |
if (private$allow_add) { |
539 | 1x |
self$srv_add("add", active_datanames_resolved) |
540 |
} |
|
541 | ||
542 | 1x |
logger::log_trace("FilteredData$srv_filter_panel initialized") |
543 | 1x |
NULL |
544 |
} |
|
545 |
) |
|
546 |
}, |
|
547 | ||
548 |
#' @description |
|
549 |
#' Server module responsible for displaying active filters. |
|
550 |
#' @param id (`character(1)`) |
|
551 |
#' `shiny` module instance id. |
|
552 |
#' @return `shiny.tag` |
|
553 |
ui_active = function(id) { |
|
554 | ! |
ns <- NS(id) |
555 | ! |
tags$div( |
556 | ! |
id = id, # not used, can be used to customize CSS behavior |
557 | ! |
class = "well", |
558 | ! |
tags$div( |
559 | ! |
class = "filter-panel-active-header", |
560 | ! |
tags$span("Active Filter Variables", class = "text-primary mb-4"), |
561 | ! |
private$ui_available_filters(ns("available_filters")), |
562 | ! |
actionLink( |
563 | ! |
inputId = ns("minimise_filter_active"), |
564 | ! |
label = NULL, |
565 | ! |
icon = icon("angle-down", lib = "font-awesome"), |
566 | ! |
title = "Minimise panel", |
567 | ! |
class = "remove_all pull-right" |
568 |
), |
|
569 | ! |
actionLink( |
570 | ! |
inputId = ns("remove_all_filters"), |
571 | ! |
label = "", |
572 | ! |
icon("circle-xmark", lib = "font-awesome"), |
573 | ! |
title = "Remove active filters", |
574 | ! |
class = "remove_all pull-right" |
575 |
) |
|
576 |
), |
|
577 | ! |
tags$div( |
578 | ! |
id = ns("filter_active_vars_contents"), |
579 | ! |
tagList( |
580 | ! |
lapply( |
581 | ! |
self$datanames(), |
582 | ! |
function(dataname) { |
583 | ! |
fdataset <- private$get_filtered_dataset(dataname) |
584 | ! |
fdataset$ui_active(id = ns(dataname)) |
585 |
} |
|
586 |
) |
|
587 |
) |
|
588 |
), |
|
589 | ! |
shinyjs::hidden( |
590 | ! |
tags$div( |
591 | ! |
id = ns("filters_active_count"), |
592 | ! |
textOutput(ns("teal_filters_count")) |
593 |
) |
|
594 |
) |
|
595 |
) |
|
596 |
}, |
|
597 | ||
598 |
#' @description |
|
599 |
#' Server module responsible for displaying active filters. |
|
600 |
#' @param id (`character(1)`) |
|
601 |
#' `shiny` module instance id. |
|
602 |
#' @param active_datanames (`reactive`) |
|
603 |
#' defining subset of `self$datanames()` to be displayed. |
|
604 |
#' @return `NULL`. |
|
605 |
srv_active = function(id, active_datanames = self$datanames) { |
|
606 | 3x |
checkmate::assert_function(active_datanames) |
607 | 3x |
moduleServer(id, function(input, output, session) { |
608 | 3x |
logger::log_trace("FilteredData$srv_active initializing") |
609 | ||
610 | 3x |
private$srv_available_filters("available_filters") |
611 | ||
612 | 3x |
observeEvent(input$minimise_filter_active, { |
613 | ! |
shinyjs::toggle("filter_active_vars_contents") |
614 | ! |
shinyjs::toggle("filters_active_count") |
615 | ! |
toggle_icon(session$ns("minimise_filter_active"), c("fa-angle-right", "fa-angle-down")) |
616 | ! |
toggle_title(session$ns("minimise_filter_active"), c("Restore panel", "Minimise Panel")) |
617 |
}) |
|
618 | ||
619 | 3x |
observeEvent(private$get_filter_count(), { |
620 | 3x |
shinyjs::toggle("remove_all_filters", condition = private$get_filter_count() != 0) |
621 | 3x |
shinyjs::show("filter_active_vars_contents") |
622 | 3x |
shinyjs::hide("filters_active_count") |
623 | 3x |
toggle_icon(session$ns("minimise_filter_active"), c("fa-angle-right", "fa-angle-down"), TRUE) |
624 | 3x |
toggle_title(session$ns("minimise_filter_active"), c("Restore panel", "Minimise Panel"), TRUE) |
625 |
}) |
|
626 | ||
627 | 3x |
observeEvent(active_datanames(), { |
628 | 2x |
lapply(self$datanames(), function(dataname) { |
629 | 4x |
if (dataname %in% active_datanames()) { |
630 | 4x |
shinyjs::show(dataname) |
631 |
} else { |
|
632 | ! |
shinyjs::hide(dataname) |
633 |
} |
|
634 |
}) |
|
635 |
}) |
|
636 | ||
637 |
# should not use for-loop as variables are otherwise only bound by reference |
|
638 |
# and last dataname would be used |
|
639 | 3x |
lapply( |
640 | 3x |
self$datanames(), |
641 | 3x |
function(dataname) { |
642 | 6x |
fdataset <- private$get_filtered_dataset(dataname) |
643 | 6x |
fdataset$srv_active(id = dataname) |
644 |
} |
|
645 |
) |
|
646 | ||
647 | 3x |
output$teal_filters_count <- renderText({ |
648 | 3x |
n_filters_active <- private$get_filter_count() |
649 | 3x |
req(n_filters_active > 0L) |
650 | 2x |
sprintf( |
651 | 2x |
"%s filter%s applied across datasets", |
652 | 2x |
n_filters_active, |
653 | 2x |
ifelse(n_filters_active == 1, "", "s") |
654 |
) |
|
655 |
}) |
|
656 | ||
657 | 3x |
observeEvent(input$remove_all_filters, { |
658 | 1x |
logger::log_trace("FilteredData$srv_filter_panel@1 removing all non-anchored filters") |
659 | 1x |
self$clear_filter_states() |
660 | 1x |
logger::log_trace("FilteredData$srv_filter_panel@1 removed all non-anchored filters") |
661 |
}) |
|
662 | 3x |
logger::log_trace("FilteredData$srv_active initialized") |
663 | 3x |
NULL |
664 |
}) |
|
665 |
}, |
|
666 | ||
667 |
#' @description |
|
668 |
#' Server module responsible for displaying drop-downs with variables to add a filter. |
|
669 |
#' @param id (`character(1)`) |
|
670 |
#' `shiny` module instance id. |
|
671 |
#' @return `shiny.tag` |
|
672 |
ui_add = function(id) { |
|
673 | ! |
ns <- NS(id) |
674 | ! |
tags$div( |
675 | ! |
id = id, # not used, can be used to customize CSS behavior |
676 | ! |
class = "well", |
677 | ! |
tags$div( |
678 | ! |
class = "row", |
679 | ! |
tags$div( |
680 | ! |
class = "col-sm-9", |
681 | ! |
tags$label("Add Filter Variables", class = "text-primary mb-4") |
682 |
), |
|
683 | ! |
tags$div( |
684 | ! |
class = "col-sm-3", |
685 | ! |
actionLink( |
686 | ! |
ns("minimise_filter_add_vars"), |
687 | ! |
label = NULL, |
688 | ! |
icon = icon("angle-down", lib = "font-awesome"), |
689 | ! |
title = "Minimise panel", |
690 | ! |
class = "remove pull-right" |
691 |
) |
|
692 |
) |
|
693 |
), |
|
694 | ! |
tags$div( |
695 | ! |
id = ns("filter_add_vars_contents"), |
696 | ! |
tagList( |
697 | ! |
lapply( |
698 | ! |
self$datanames(), |
699 | ! |
function(dataname) { |
700 | ! |
fdataset <- private$get_filtered_dataset(dataname) |
701 | ! |
tags$span(id = ns(dataname), fdataset$ui_add(ns(dataname))) |
702 |
} |
|
703 |
) |
|
704 |
) |
|
705 |
) |
|
706 |
) |
|
707 |
}, |
|
708 | ||
709 |
#' @description |
|
710 |
#' Server module responsible for displaying drop-downs with variables to add a filter. |
|
711 |
#' @param id (`character(1)`) |
|
712 |
#' `shiny` module instance id. |
|
713 |
#' @param active_datanames (`reactive`) |
|
714 |
#' defining subset of `self$datanames()` to be displayed. |
|
715 |
#' @return `NULL`. |
|
716 |
srv_add = function(id, active_datanames = reactive(self$datanames())) { |
|
717 | 1x |
checkmate::assert_class(active_datanames, "reactive") |
718 | 1x |
moduleServer(id, function(input, output, session) { |
719 | 1x |
logger::log_trace("FilteredData$srv_add initializing") |
720 | 1x |
observeEvent(input$minimise_filter_add_vars, { |
721 | ! |
shinyjs::toggle("filter_add_vars_contents") |
722 | ! |
toggle_icon(session$ns("minimise_filter_add_vars"), c("fa-angle-right", "fa-angle-down")) |
723 | ! |
toggle_title(session$ns("minimise_filter_add_vars"), c("Restore panel", "Minimise Panel")) |
724 |
}) |
|
725 | ||
726 | 1x |
observeEvent(active_datanames(), { |
727 | ! |
lapply(self$datanames(), function(dataname) { |
728 | ! |
if (dataname %in% active_datanames()) { |
729 | ! |
shinyjs::show(dataname) |
730 |
} else { |
|
731 | ! |
shinyjs::hide(dataname) |
732 |
} |
|
733 |
}) |
|
734 |
}) |
|
735 | ||
736 |
# should not use for-loop as variables are otherwise only bound by reference |
|
737 |
# and last dataname would be used |
|
738 | 1x |
lapply( |
739 | 1x |
self$datanames(), |
740 | 1x |
function(dataname) { |
741 | 2x |
fdataset <- private$get_filtered_dataset(dataname) |
742 | 2x |
fdataset$srv_add(id = dataname) |
743 |
} |
|
744 |
) |
|
745 | 1x |
logger::log_trace("FilteredData$srv_filter_panel initialized") |
746 | 1x |
NULL |
747 |
}) |
|
748 |
}, |
|
749 | ||
750 |
#' @description |
|
751 |
#' Creates the UI definition for the module showing counts for each dataset |
|
752 |
#' contrasting the filtered to the full unfiltered dataset. |
|
753 |
#' |
|
754 |
#' Per dataset, it displays |
|
755 |
#' the number of rows/observations in each dataset, |
|
756 |
#' the number of unique subjects. |
|
757 |
#' |
|
758 |
#' @param id (`character(1)`) |
|
759 |
#' `shiny` module instance id. |
|
760 |
#' |
|
761 |
ui_overview = function(id) { |
|
762 | ! |
ns <- NS(id) |
763 | ! |
tags$div( |
764 | ! |
id = id, # not used, can be used to customize CSS behavior |
765 | ! |
class = "well", |
766 | ! |
tags$div( |
767 | ! |
class = "row", |
768 | ! |
tags$div( |
769 | ! |
class = "col-sm-9", |
770 | ! |
tags$label("Active Filter Summary", class = "text-primary mb-4") |
771 |
), |
|
772 | ! |
tags$div( |
773 | ! |
class = "col-sm-3", |
774 | ! |
actionLink( |
775 | ! |
ns("minimise_filter_overview"), |
776 | ! |
label = NULL, |
777 | ! |
icon = icon("angle-down", lib = "font-awesome"), |
778 | ! |
title = "Minimise panel", |
779 | ! |
class = "remove pull-right" |
780 |
) |
|
781 |
) |
|
782 |
), |
|
783 | ! |
tags$div( |
784 | ! |
id = ns("filters_overview_contents"), |
785 | ! |
tags$div( |
786 | ! |
class = "teal_active_summary_filter_panel", |
787 | ! |
tableOutput(ns("table")) |
788 |
) |
|
789 |
) |
|
790 |
) |
|
791 |
}, |
|
792 | ||
793 |
#' @description |
|
794 |
#' Server function to display the number of records in the filtered and unfiltered |
|
795 |
#' data. |
|
796 |
#' |
|
797 |
#' @param id (`character(1)`) |
|
798 |
#' `shiny` module instance id. |
|
799 |
#' @param active_datanames (`reactive`) |
|
800 |
#' returning `datanames` that should be shown on the filter panel, |
|
801 |
#' must be a subset of the `datanames` argument provided to `ui_filter_panel`; |
|
802 |
#' if the function returns `NULL` (as opposed to `character(0)`), the filter |
|
803 |
#' panel will be hidden. |
|
804 |
#' @return `NULL`. |
|
805 |
srv_overview = function(id, active_datanames = self$datanames) { |
|
806 | 1x |
checkmate::assert_class(active_datanames, "reactive") |
807 | 1x |
moduleServer( |
808 | 1x |
id = id, |
809 | 1x |
function(input, output, session) { |
810 | 1x |
logger::log_trace("FilteredData$srv_filter_overview initializing") |
811 | ||
812 | 1x |
observeEvent(input$minimise_filter_overview, { |
813 | ! |
shinyjs::toggle("filters_overview_contents") |
814 | ! |
toggle_icon(session$ns("minimise_filter_overview"), c("fa-angle-right", "fa-angle-down")) |
815 | ! |
toggle_title(session$ns("minimise_filter_overview"), c("Restore panel", "Minimise Panel")) |
816 |
}) |
|
817 | ||
818 | 1x |
output$table <- renderUI({ |
819 | ! |
logger::log_trace("FilteredData$srv_filter_overview@1 updating counts") |
820 | ! |
if (length(active_datanames()) == 0) { |
821 | ! |
return(NULL) |
822 |
} |
|
823 | ||
824 | ! |
datasets_df <- self$get_filter_overview(datanames = active_datanames()) |
825 | ||
826 | ! |
attr(datasets_df$dataname, "label") <- "Data Name" |
827 | ||
828 | ! |
if (!is.null(datasets_df$obs)) { |
829 |
# some datasets (MAE colData) doesn't return obs column |
|
830 | ! |
datasets_df <- transform( |
831 | ! |
datasets_df, |
832 | ! |
obs_str_summary = ifelse( |
833 | ! |
!is.na(obs), |
834 | ! |
sprintf("%s/%s", obs_filtered, obs), |
835 |
"" |
|
836 |
) |
|
837 |
) |
|
838 | ! |
attr(datasets_df$obs_str_summary, "label") <- "Obs" |
839 |
} |
|
840 | ||
841 | ||
842 | ! |
if (!is.null(datasets_df$subjects)) { |
843 |
# some datasets (without keys) doesn't return subjects |
|
844 | ! |
datasets_df <- transform( |
845 | ! |
datasets_df, |
846 | ! |
subjects_summary = ifelse( |
847 | ! |
!is.na(subjects), |
848 | ! |
sprintf("%s/%s", subjects_filtered, subjects), |
849 |
"" |
|
850 |
) |
|
851 |
) |
|
852 | ! |
attr(datasets_df$subjects_summary, "label") <- "Subjects" |
853 |
} |
|
854 | ||
855 | ! |
all_names <- c("dataname", "obs_str_summary", "subjects_summary") |
856 | ! |
datasets_df <- datasets_df[, colnames(datasets_df) %in% all_names] |
857 | ||
858 | ! |
body_html <- apply( |
859 | ! |
datasets_df, |
860 | ! |
1, |
861 | ! |
function(x) { |
862 | ! |
tags$tr( |
863 | ! |
tagList( |
864 | ! |
tags$td( |
865 | ! |
if (all(x[-1] == "")) { |
866 | ! |
icon( |
867 | ! |
name = "exclamation-triangle", |
868 | ! |
title = "Unsupported dataset", |
869 | ! |
`data-container` = "body", |
870 | ! |
`data-toggle` = "popover", |
871 | ! |
`data-content` = "object not supported by the filter panel" |
872 |
) |
|
873 |
}, |
|
874 | ! |
x[1] |
875 |
), |
|
876 | ! |
lapply(x[-1], tags$td) |
877 |
) |
|
878 |
) |
|
879 |
} |
|
880 |
) |
|
881 | ||
882 | ! |
header_labels <- vapply( |
883 | ! |
seq_along(datasets_df), |
884 | ! |
function(i) { |
885 | ! |
label <- attr(datasets_df[[i]], "label") |
886 | ! |
ifelse(!is.null(label), label, names(datasets_df)[[i]]) |
887 |
}, |
|
888 | ! |
character(1) |
889 |
) |
|
890 | ! |
header_html <- tags$tr(tagList(lapply(header_labels, tags$td))) |
891 | ||
892 | ! |
table_html <- tags$table( |
893 | ! |
class = "table custom-table", |
894 | ! |
tags$thead(header_html), |
895 | ! |
tags$tbody(body_html) |
896 |
) |
|
897 | ! |
logger::log_trace("FilteredData$srv_filter_overview@1 updated counts") |
898 | ! |
table_html |
899 |
}) |
|
900 | 1x |
logger::log_trace("FilteredData$srv_filter_overview initialized") |
901 | 1x |
NULL |
902 |
} |
|
903 |
) |
|
904 |
} |
|
905 |
), |
|
906 | ||
907 |
# private members ---- |
|
908 |
private = list( |
|
909 |
# selectively hide / show to only show `active_datanames` out of all datanames |
|
910 | ||
911 |
# private attributes ---- |
|
912 |
filtered_datasets = list(), |
|
913 | ||
914 |
# activate/deactivate filter panel |
|
915 |
filter_panel_active = TRUE, |
|
916 | ||
917 |
# `reactive` containing teal_slices that can be selected; only active in module-specific mode |
|
918 |
available_teal_slices = NULL, |
|
919 | ||
920 |
# keys used for joining/filtering data a join_keys object (see teal.data) |
|
921 |
join_keys = NULL, |
|
922 | ||
923 |
# flag specifying whether the user may add filters |
|
924 |
allow_add = TRUE, |
|
925 | ||
926 |
# private methods ---- |
|
927 | ||
928 |
# @description |
|
929 |
# Gets `FilteredDataset` object which contains all information |
|
930 |
# pertaining to the specified dataset. |
|
931 |
# |
|
932 |
# @param dataname (`character(1)`) |
|
933 |
# name of the dataset |
|
934 |
# |
|
935 |
# @return `FilteredDataset` object or list of `FilteredDataset`s |
|
936 |
# |
|
937 |
get_filtered_dataset = function(dataname = character(0)) { |
|
938 | 147x |
if (length(dataname) == 0) { |
939 | ! |
private$filtered_datasets |
940 |
} else { |
|
941 | 147x |
private$filtered_datasets[[dataname]] |
942 |
} |
|
943 |
}, |
|
944 | ||
945 |
# we implement these functions as checks rather than returning logicals so they can |
|
946 |
# give informative error messages immediately |
|
947 | ||
948 |
# @description |
|
949 |
# Gets the number of active `FilterState` objects in all `FilterStates` |
|
950 |
# in all `FilteredDataset`s in this `FilteredData` object. |
|
951 |
# @return `integer(1)` |
|
952 |
get_filter_count = function() { |
|
953 | 11x |
length(self$get_filter_state()) |
954 |
}, |
|
955 | ||
956 |
# @description |
|
957 |
# Activate available filters. |
|
958 |
# Module is composed from plus button and dropdown menu. Menu is shown when |
|
959 |
# the button is clicked. Menu contains available/active filters list |
|
960 |
# passed via `set_available_teal_slice`. |
|
961 |
ui_available_filters = function(id) { |
|
962 | ! |
ns <- NS(id) |
963 | ||
964 | ! |
active_slices_id <- isolate(vapply(self$get_filter_state(), `[[`, character(1), "id")) |
965 | ! |
tags$div( |
966 | ! |
id = ns("available_menu"), |
967 | ! |
shinyWidgets::dropMenu( |
968 | ! |
actionLink( |
969 | ! |
ns("show"), |
970 | ! |
label = NULL, |
971 | ! |
icon = icon("plus", lib = "font-awesome"), |
972 | ! |
title = "Available filters", |
973 | ! |
class = "remove pull-right" |
974 |
), |
|
975 | ! |
tags$div( |
976 | ! |
class = "menu-content", |
977 | ! |
shinycssloaders::withSpinner( |
978 | ! |
uiOutput(ns("checkbox")), |
979 | ! |
type = 4, |
980 | ! |
size = 0.25 |
981 |
) |
|
982 |
) |
|
983 |
) |
|
984 |
) |
|
985 |
}, |
|
986 |
# @description |
|
987 |
# Activate available filters. When a filter is selected or removed, |
|
988 |
# `set_filter_state` or `remove_filter_state` is executed for |
|
989 |
# the appropriate filter state id. |
|
990 |
srv_available_filters = function(id) { |
|
991 | 4x |
moduleServer(id, function(input, output, session) { |
992 | 4x |
slices_available <- self$get_available_teal_slices() |
993 | 4x |
slices_interactive <- reactive( |
994 | 4x |
Filter(function(slice) isFALSE(slice$fixed), slices_available()) |
995 |
) |
|
996 | 4x |
slices_fixed <- reactive( |
997 | 4x |
Filter(function(slice) isTRUE(slice$fixed), slices_available()) |
998 |
) |
|
999 | 4x |
available_slices_id <- reactive(vapply(slices_available(), `[[`, character(1), "id")) |
1000 | 4x |
active_slices_id <- reactive(vapply(self$get_filter_state(), `[[`, character(1), "id")) |
1001 | 4x |
duplicated_slice_references <- reactive({ |
1002 |
# slice refers to a particular column |
|
1003 | 8x |
slice_reference <- vapply(slices_available(), get_default_slice_id, character(1)) |
1004 | 8x |
is_duplicated_reference <- duplicated(slice_reference) | duplicated(slice_reference, fromLast = TRUE) |
1005 | 8x |
is_active <- available_slices_id() %in% active_slices_id() |
1006 | 8x |
is_not_expr <- !vapply(slices_available(), inherits, logical(1), "teal_slice_expr") |
1007 | 8x |
slice_reference[is_duplicated_reference & is_active & is_not_expr] |
1008 |
}) |
|
1009 | ||
1010 | 4x |
checkbox_group_element <- function(name, value, label, checked, disabled = FALSE) { |
1011 | 35x |
tags$div( |
1012 | 35x |
class = "checkbox available-filters", |
1013 | 35x |
tags$label( |
1014 | 35x |
tags$input( |
1015 | 35x |
type = "checkbox", |
1016 | 35x |
name = name, |
1017 | 35x |
value = value, |
1018 | 35x |
checked = checked, |
1019 | 35x |
disabled = if (disabled) "disabled" |
1020 |
), |
|
1021 | 35x |
tags$span(label, disabled = if (disabled) disabled) |
1022 |
) |
|
1023 |
) |
|
1024 |
} |
|
1025 | ||
1026 | 4x |
output$checkbox <- renderUI({ |
1027 | 8x |
checkbox <- checkboxGroupInput( |
1028 | 8x |
session$ns("available_slices_id"), |
1029 | 8x |
label = NULL, |
1030 | 8x |
choices = NULL, |
1031 | 8x |
selected = NULL |
1032 |
) |
|
1033 | 8x |
active_slices_ids <- active_slices_id() |
1034 | 8x |
duplicated_slice_refs <- duplicated_slice_references() |
1035 | ||
1036 | 8x |
checkbox_group_slice <- function(slice) { |
1037 |
# we need to isolate changes in the fields of the slice (teal_slice) |
|
1038 | 35x |
isolate({ |
1039 | 35x |
checkbox_group_element( |
1040 | 35x |
name = session$ns("available_slices_id"), |
1041 | 35x |
value = slice$id, |
1042 | 35x |
label = slice$id, |
1043 | 35x |
checked = if (slice$id %in% active_slices_ids) "checked", |
1044 | 35x |
disabled = slice$anchored || |
1045 | 35x |
get_default_slice_id(slice) %in% duplicated_slice_refs && |
1046 | 35x |
!slice$id %in% active_slices_ids |
1047 |
) |
|
1048 |
}) |
|
1049 |
} |
|
1050 | ||
1051 | 8x |
interactive_choice_mock <- lapply(slices_interactive(), checkbox_group_slice) |
1052 | 8x |
non_interactive_choice_mock <- lapply(slices_fixed(), checkbox_group_slice) |
1053 | ||
1054 | 8x |
htmltools::tagInsertChildren( |
1055 | 8x |
checkbox, |
1056 | 8x |
tags$br(), |
1057 | 8x |
if (length(non_interactive_choice_mock)) tags$strong("Fixed filters"), |
1058 | 8x |
non_interactive_choice_mock, |
1059 | 8x |
if (length(interactive_choice_mock)) tags$strong("Interactive filters"), |
1060 | 8x |
interactive_choice_mock, |
1061 | 8x |
.cssSelector = "div.shiny-options-group", |
1062 | 8x |
after = 0 |
1063 |
) |
|
1064 |
}) |
|
1065 | ||
1066 | 4x |
observeEvent(input$available_slices_id, ignoreNULL = FALSE, ignoreInit = TRUE, { |
1067 | 5x |
new_slices_id <- setdiff(input$available_slices_id, active_slices_id()) |
1068 | 5x |
removed_slices_id <- setdiff(active_slices_id(), input$available_slices_id) |
1069 | 5x |
if (length(new_slices_id)) { |
1070 | 3x |
new_teal_slices <- Filter( |
1071 | 3x |
function(slice) slice$id %in% new_slices_id, |
1072 | 3x |
private$available_teal_slices() |
1073 |
) |
|
1074 | 3x |
self$set_filter_state(new_teal_slices) |
1075 |
} |
|
1076 | ||
1077 | 5x |
if (length(removed_slices_id)) { |
1078 | 4x |
removed_teal_slices <- Filter( |
1079 | 4x |
function(slice) slice$id %in% removed_slices_id, |
1080 | 4x |
self$get_filter_state() |
1081 |
) |
|
1082 | 4x |
self$remove_filter_state(removed_teal_slices) |
1083 |
} |
|
1084 |
}) |
|
1085 | ||
1086 | 4x |
observeEvent(private$available_teal_slices(), ignoreNULL = FALSE, { |
1087 | 3x |
if (length(private$available_teal_slices())) { |
1088 | 1x |
shinyjs::show("available_menu") |
1089 |
} else { |
|
1090 | 2x |
shinyjs::hide("available_menu") |
1091 |
} |
|
1092 |
}) |
|
1093 |
}) |
|
1094 |
} |
|
1095 |
) |
|
1096 |
) |
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 | 121x |
isolate({ |
158 | 121x |
checkmate::assert_numeric(x, all.missing = FALSE) |
159 | 2x |
if (!any(is.finite(x))) stop("\"x\" contains no finite values") |
160 | 118x |
super$initialize(x = x, x_reactive = x_reactive, slice = slice, extract_type = extract_type) |
161 | 118x |
private$is_integer <- checkmate::test_integerish(x) |
162 | 118x |
private$inf_count <- sum(is.infinite(x)) |
163 | 118x |
private$inf_filtered_count <- reactive( |
164 | 118x |
if (!is.null(private$x_reactive())) sum(is.infinite(private$x_reactive())) |
165 |
) |
|
166 | ||
167 | 118x |
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 | 117x |
private$set_choices(slice$choices) |
171 | 42x |
if (is.null(slice$selected)) slice$selected <- slice$choices |
172 | 117x |
private$set_selected(slice$selected) |
173 | ||
174 | 114x |
private$is_integer <- checkmate::test_integerish(x) |
175 | 114x |
private$inf_filtered_count <- reactive( |
176 | 114x |
if (!is.null(private$x_reactive())) sum(is.infinite(private$x_reactive())) |
177 |
) |
|
178 | 114x |
private$inf_count <- sum(is.infinite(x)) |
179 | ||
180 | 114x |
private$plot_data <- list( |
181 | 114x |
type = "histogram", |
182 | 114x |
nbinsx = 50, |
183 | 114x |
x = Filter(Negate(is.na), Filter(is.finite, private$x)), |
184 | 114x |
color = I(fetch_bs_color("secondary")), |
185 | 114x |
alpha = 0.2, |
186 | 114x |
bingroup = 1, |
187 | 114x |
showlegend = FALSE, |
188 | 114x |
hoverinfo = "none" |
189 |
) |
|
190 | 114x |
private$plot_mask <- list(list( |
191 | 114x |
type = "rect", fillcolor = rgb(1, 1, 1, .65), line = list(width = 0), |
192 | 114x |
x0 = -0.5, x1 = 1.5, y0 = -0.5, y1 = 1.5, xref = "paper", yref = "paper" |
193 |
)) |
|
194 | 114x |
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 | 114x |
private$plot_config <- reactive({ |
215 | 5x |
list( |
216 | 5x |
doubleClick = "reset", |
217 | 5x |
displayModeBar = FALSE, |
218 | 5x |
edits = list(shapePosition = TRUE) |
219 |
) |
|
220 |
}) |
|
221 | 114x |
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 | 114x |
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 | 117x |
x <- private$x[is.finite(private$x)] |
282 | 117x |
if (is.null(choices)) { |
283 | 105x |
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 | 117x |
private$set_is_choice_limited(private$x, choices) |
304 | 117x |
private$x <- private$x[ |
305 | 117x |
(private$x >= choices[1L] & private$x <= choices[2L]) | is.na(private$x) | !is.finite(private$x) |
306 |
] |
|
307 | ||
308 | 117x |
x_range <- range(private$x, finite = TRUE) |
309 | ||
310 |
# Required for displaying ticks on the slider, can modify choices! |
|
311 | 117x |
if (identical(diff(x_range), 0)) { |
312 | 2x |
choices <- x_range |
313 |
} else { |
|
314 | 115x |
x_pretty <- pretty(x_range, 100L) |
315 | 115x |
choices <- range(x_pretty) |
316 | 115x |
private$numeric_step <- signif(private$get_pretty_range_step(x_pretty), digits = 10) |
317 |
} |
|
318 | 117x |
private$teal_slice$choices <- choices |
319 | 117x |
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 | 117x |
xl <- xl[!is.na(xl)] |
326 | 117x |
xl <- xl[is.finite(xl)] |
327 | 117x |
private$is_choice_limited <- (any(xl < choices[1L]) | any(xl > choices[2L])) |
328 | 117x |
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 | 117x |
if (private$is_integer && diff(range(pretty_range) > 2)) { |
347 | 46x |
return(1L) |
348 |
} else { |
|
349 | 71x |
n_steps <- length(pretty_range) - 1 |
350 | 71x |
return(signif(digits = 10, (max(pretty_range) - min(pretty_range)) / n_steps)) |
351 |
} |
|
352 |
}, |
|
353 |
cast_and_validate = function(values) { |
|
354 | 133x |
tryCatch( |
355 | 133x |
expr = { |
356 | 133x |
values <- as.numeric(values) |
357 | 4x |
if (anyNA(values)) stop() |
358 | 129x |
values |
359 |
}, |
|
360 | 133x |
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 | 125x |
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 | 125x |
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_trace("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 |
# Display histogram, adding a second trace that contains filtered data. |
|
492 | 5x |
output$plot <- plotly::renderPlotly({ |
493 | 5x |
histogram <- do.call(plotly::plot_ly, plot_data) |
494 | 5x |
histogram <- do.call(plotly::layout, c(list(p = histogram), private$plot_layout())) |
495 | 5x |
histogram <- do.call(plotly::config, c(list(p = histogram), private$plot_config())) |
496 | 5x |
histogram <- do.call(plotly::add_histogram, c(list(p = histogram), private$plot_filtered())) |
497 | 5x |
histogram |
498 |
}) |
|
499 | ||
500 |
# Dragging shapes (lines) on plot updates selection. |
|
501 | 5x |
private$observers$relayout <- |
502 | 5x |
observeEvent( |
503 | 5x |
ignoreNULL = FALSE, |
504 | 5x |
ignoreInit = TRUE, |
505 | 5x |
eventExpr = plotly::event_data("plotly_relayout", source = session$ns("histogram_plot")), |
506 | 5x |
handlerExpr = { |
507 | 1x |
logger::log_trace("RangeFilterState$server@1 selection changed, id: { private$get_id() }") |
508 | 1x |
event <- plotly::event_data("plotly_relayout", source = session$ns("histogram_plot")) |
509 | 1x |
if (any(grepl("shapes", names(event)))) { |
510 | ! |
line_positions <- private$get_selected() |
511 | ! |
if (any(grepl("shapes[0]", names(event), fixed = TRUE))) { |
512 | ! |
line_positions[1] <- event[["shapes[0].x0"]] |
513 | ! |
} else if (any(grepl("shapes[1]", names(event), fixed = TRUE))) { |
514 | ! |
line_positions[2] <- event[["shapes[1].x0"]] |
515 |
} |
|
516 |
# If one line was dragged past the other, abort action and reset lines. |
|
517 | ! |
if (line_positions[1] > line_positions[2]) { |
518 | ! |
showNotification( |
519 | ! |
"Numeric range start value must be less than end value.", |
520 | ! |
type = "warning" |
521 |
) |
|
522 | ! |
plotly::plotlyProxyInvoke( |
523 | ! |
plotly::plotlyProxy("plot"), |
524 | ! |
"relayout", |
525 | ! |
shapes = private$get_shape_properties(private$get_selected()) |
526 |
) |
|
527 | ! |
return(NULL) |
528 |
} |
|
529 | ||
530 | ! |
private$set_selected(signif(line_positions, digits = 4L)) |
531 |
} |
|
532 |
} |
|
533 |
) |
|
534 | ||
535 |
# Change in selection updates shapes (lines) on plot and numeric input. |
|
536 | 5x |
private$observers$selection_api <- |
537 | 5x |
observeEvent( |
538 | 5x |
ignoreNULL = FALSE, |
539 | 5x |
ignoreInit = TRUE, |
540 | 5x |
eventExpr = private$get_selected(), |
541 | 5x |
handlerExpr = { |
542 | ! |
logger::log_trace("RangeFilterState$server@2 state changed, id: {private$get_id() }") |
543 | ! |
if (!isTRUE(all.equal(private$get_selected(), selection_manual()))) { |
544 | ! |
shinyWidgets::updateNumericRangeInput( |
545 | ! |
session = session, |
546 | ! |
inputId = "selection_manual", |
547 | ! |
value = private$get_selected() |
548 |
) |
|
549 |
} |
|
550 |
} |
|
551 |
) |
|
552 | ||
553 |
# Manual input updates selection. |
|
554 | 5x |
private$observers$selection_manual <- observeEvent( |
555 | 5x |
ignoreNULL = FALSE, |
556 | 5x |
ignoreInit = TRUE, |
557 | 5x |
eventExpr = selection_manual(), |
558 | 5x |
handlerExpr = { |
559 | ! |
selection <- selection_manual() |
560 |
# Abort and reset if non-numeric values is entered. |
|
561 | ! |
if (any(is.na(selection))) { |
562 | ! |
showNotification( |
563 | ! |
"Numeric range values must be numbers.", |
564 | ! |
type = "warning" |
565 |
) |
|
566 | ! |
shinyWidgets::updateNumericRangeInput( |
567 | ! |
session = session, |
568 | ! |
inputId = "selection_manual", |
569 | ! |
value = private$get_selected() |
570 |
) |
|
571 | ! |
return(NULL) |
572 |
} |
|
573 | ||
574 |
# Abort and reset if reversed choices are specified. |
|
575 | ! |
if (selection[1] > selection[2]) { |
576 | ! |
showNotification( |
577 | ! |
"Numeric range start value must be less than end value.", |
578 | ! |
type = "warning" |
579 |
) |
|
580 | ! |
shinyWidgets::updateNumericRangeInput( |
581 | ! |
session = session, |
582 | ! |
inputId = "selection_manual", |
583 | ! |
value = private$get_selected() |
584 |
) |
|
585 | ! |
return(NULL) |
586 |
} |
|
587 | ||
588 | ||
589 | ! |
if (!isTRUE(all.equal(selection, private$get_selected()))) { |
590 | ! |
logger::log_trace("RangeFilterState$server@3 manual selection changed, id: { private$get_id() }") |
591 | ! |
private$set_selected(selection) |
592 |
} |
|
593 |
} |
|
594 |
) |
|
595 | ||
596 | 5x |
private$keep_inf_srv("keep_inf") |
597 | 5x |
private$keep_na_srv("keep_na") |
598 | ||
599 | 5x |
logger::log_trace("RangeFilterState$server initialized, id: { private$get_id() }") |
600 | 5x |
NULL |
601 |
} |
|
602 |
) |
|
603 |
}, |
|
604 |
server_inputs_fixed = function(id) { |
|
605 | ! |
moduleServer( |
606 | ! |
id = id, |
607 | ! |
function(input, output, session) { |
608 | ! |
logger::log_trace("RangeFilterState$server initializing, id: { private$get_id() }") |
609 | ||
610 | ! |
plot_config <- private$plot_config() |
611 | ! |
plot_config$staticPlot <- TRUE |
612 | ||
613 | ! |
output$plot <- plotly::renderPlotly({ |
614 | ! |
histogram <- do.call(plotly::plot_ly, private$plot_data) |
615 | ! |
histogram <- do.call(plotly::layout, c(list(p = histogram), private$plot_layout())) |
616 | ! |
histogram <- do.call(plotly::config, c(list(p = histogram), plot_config)) |
617 | ! |
histogram <- do.call(plotly::add_histogram, c(list(p = histogram), private$plot_filtered())) |
618 | ! |
histogram |
619 |
}) |
|
620 | ||
621 | ! |
output$selection <- renderUI({ |
622 | ! |
shinycssloaders::withSpinner( |
623 | ! |
plotly::plotlyOutput(session$ns("plot"), height = "50px"), |
624 | ! |
type = 4, |
625 | ! |
size = 0.25 |
626 |
) |
|
627 |
}) |
|
628 | ||
629 | ! |
logger::log_trace("RangeFilterState$server initialized, id: { private$get_id() }") |
630 | ! |
NULL |
631 |
} |
|
632 |
) |
|
633 |
}, |
|
634 | ||
635 |
# @description |
|
636 |
# Server module to display filter summary |
|
637 |
# renders text describing selected range and |
|
638 |
# if NA or Inf are included also |
|
639 |
# @return `shiny.tag` to include in the `ui_summary` |
|
640 |
content_summary = function() { |
|
641 | 5x |
selection <- private$get_selected() |
642 | 5x |
tagList( |
643 | 5x |
tags$span(HTML(selection[1], "–", selection[2]), class = "filter-card-summary-value"), |
644 | 5x |
tags$span( |
645 | 5x |
class = "filter-card-summary-controls", |
646 | 5x |
if (private$na_count > 0) { |
647 | ! |
tags$span("NA", if (isTRUE(private$get_keep_na())) icon("check") else icon("xmark")) |
648 |
}, |
|
649 | 5x |
if (private$inf_count > 0) { |
650 | ! |
tags$span("Inf", if (isTRUE(private$get_keep_inf())) icon("check") else icon("xmark")) |
651 |
} |
|
652 |
) |
|
653 |
) |
|
654 |
}, |
|
655 | ||
656 |
# @description |
|
657 |
# Module displaying input to keep or remove NA in the `FilterState` call. |
|
658 |
# Renders a checkbox input only when variable with which the `FilterState` has been created contains Infs. |
|
659 |
# @param id (`character(1)`) `shiny` module instance id. |
|
660 |
keep_inf_ui = function(id) { |
|
661 | 5x |
ns <- NS(id) |
662 | ||
663 | 5x |
if (private$inf_count > 0) { |
664 | ! |
countmax <- private$na_count |
665 | ! |
countnow <- isolate(private$filtered_na_count()) |
666 | ! |
ui_input <- checkboxInput( |
667 | ! |
inputId = ns("value"), |
668 | ! |
label = tags$span( |
669 | ! |
id = ns("count_label"), |
670 | ! |
make_count_text( |
671 | ! |
label = "Keep Inf", |
672 | ! |
countmax = countmax, |
673 | ! |
countnow = countnow |
674 |
) |
|
675 |
), |
|
676 | ! |
value = isolate(private$get_keep_inf()) |
677 |
) |
|
678 | ! |
tags$div( |
679 | ! |
uiOutput(ns("trigger_visible"), inline = TRUE), |
680 | ! |
ui_input |
681 |
) |
|
682 |
} else { |
|
683 | 5x |
NULL |
684 |
} |
|
685 |
}, |
|
686 | ||
687 |
# @description |
|
688 |
# Module to handle Inf values in the FilterState |
|
689 |
# Sets `private$slice$keep_inf` according to the selection |
|
690 |
# and updates the relevant UI element if `private$slice$keep_inf` has been changed by the api. |
|
691 |
# @param id (`character(1)`) `shiny` module instance id. |
|
692 |
# @return `NULL`. |
|
693 |
keep_inf_srv = function(id) { |
|
694 | 5x |
moduleServer(id, function(input, output, session) { |
695 |
# 1. renderUI is used here as an observer which triggers only if output is visible |
|
696 |
# and if the reactive changes - reactive triggers only if the output is visible. |
|
697 |
# 2. We want to trigger change of the labels only if reactive count changes (not underlying data) |
|
698 | 5x |
output$trigger_visible <- renderUI({ |
699 | 5x |
updateCountText( |
700 | 5x |
inputId = "count_label", |
701 | 5x |
label = "Keep Inf", |
702 | 5x |
countmax = private$inf_count, |
703 | 5x |
countnow = private$inf_filtered_count() |
704 |
) |
|
705 | 5x |
NULL |
706 |
}) |
|
707 | ||
708 |
# this observer is needed in the situation when private$teal_slice$keep_inf has been |
|
709 |
# changed directly by the api - then it's needed to rerender UI element |
|
710 |
# to show relevant values |
|
711 | 5x |
private$observers$keep_inf_api <- observeEvent( |
712 | 5x |
ignoreNULL = TRUE, # its not possible for range that NULL is selected |
713 | 5x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
714 | 5x |
eventExpr = private$get_keep_inf(), |
715 | 5x |
handlerExpr = { |
716 | ! |
if (!setequal(private$get_keep_inf(), input$value)) { |
717 | ! |
logger::log_trace("RangeFilterState$keep_inf_srv@1 changed reactive value, id: { private$get_id() }") |
718 | ! |
updateCheckboxInput( |
719 | ! |
inputId = "value", |
720 | ! |
value = private$get_keep_inf() |
721 |
) |
|
722 |
} |
|
723 |
} |
|
724 |
) |
|
725 | ||
726 | 5x |
private$observers$keep_inf <- observeEvent( |
727 | 5x |
ignoreNULL = TRUE, # it's not possible for range that NULL is selected |
728 | 5x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
729 | 5x |
eventExpr = input$value, |
730 | 5x |
handlerExpr = { |
731 | ! |
logger::log_trace("FilterState$keep_na_srv@2 changed input, id: { private$get_id() }") |
732 | ! |
keep_inf <- input$value |
733 | ! |
private$set_keep_inf(keep_inf) |
734 |
} |
|
735 |
) |
|
736 | ||
737 | 5x |
invisible(NULL) |
738 |
}) |
|
739 |
} |
|
740 |
) |
|
741 |
) |
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 | 23x |
if (!requireNamespace("MultiAssayExperiment", quietly = TRUE)) { |
56 | ! |
stop("Cannot load MultiAssayExperiment - please install the package or restart your session.") |
57 |
} |
|
58 | 23x |
checkmate::assert_class(dataset, "MultiAssayExperiment") |
59 | 21x |
super$initialize(dataset, dataname, keys, label) |
60 | 21x |
experiment_names <- names(dataset) |
61 | ||
62 |
# subsetting by subjects means subsetting by colData(MAE) |
|
63 | 21x |
private$add_filter_states( |
64 | 21x |
filter_states = init_filter_states( |
65 | 21x |
data = dataset, |
66 | 21x |
data_reactive = private$data_filtered_fun, |
67 | 21x |
dataname = dataname, |
68 | 21x |
datalabel = "subjects", |
69 | 21x |
keys = self$get_keys() |
70 |
), |
|
71 | 21x |
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 | 21x |
lapply( |
77 | 21x |
experiment_names, |
78 | 21x |
function(experiment_name) { |
79 | 105x |
data_reactive <- function(sid = "") private$data_filtered_fun(sid)[[experiment_name]] |
80 | 105x |
private$add_filter_states( |
81 | 105x |
filter_states = init_filter_states( |
82 | 105x |
data = dataset[[experiment_name]], |
83 | 105x |
data_reactive = data_reactive, |
84 | 105x |
dataname = dataname, |
85 | 105x |
datalabel = experiment_name |
86 |
), |
|
87 | 105x |
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 | 15x |
isolate({ |
101 | 15x |
logger::log_trace("{ class(self)[1] }$set_filter_state initializing, dataname: { private$dataname }") |
102 | 15x |
checkmate::assert_class(state, "teal_slices") |
103 | 14x |
lapply(state, function(x) { |
104 | 52x |
checkmate::assert_true(x$dataname == private$dataname, .var.name = "dataname matches private$dataname") |
105 |
}) |
|
106 | ||
107 |
# set state on subjects |
|
108 | 14x |
subject_state <- Filter(function(x) is.null(x$experiment), state) |
109 | 14x |
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 | 14x |
experiments <- unique(unlist(lapply(state, "[[", "experiment"))) |
114 | 14x |
available_experiments <- setdiff(names(private$get_filter_states()), "subjects") |
115 | 14x |
excluded_filters <- setdiff(experiments, available_experiments) |
116 | 14x |
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 | 14x |
lapply(available_experiments, function(experiment) { |
127 | 70x |
slices <- Filter(function(x) identical(x$experiment, experiment), state) |
128 | 70x |
private$get_filter_states()[[experiment]]$set_filter_state(slices) |
129 |
}) |
|
130 | ||
131 | 14x |
logger::log_trace("{ class(self)[1] }$set_filter_state initialized, dataname: { private$dataname }") |
132 | ||
133 | 14x |
invisible(NULL) |
134 |
}) |
|
135 |
}, |
|
136 | ||
137 |
#' @description |
|
138 |
#' Remove one or more `FilterState` of a `MAEFilteredDataset`. |
|
139 |
#' |
|
140 |
#' @param state (`teal_slices`) |
|
141 |
#' specifying `FilterState` objects to remove; |
|
142 |
#' `teal_slice`s may contain only `dataname` and `varname`, other elements are ignored. |
|
143 |
#' |
|
144 |
#' @return `NULL`, invisibly. |
|
145 |
#' |
|
146 |
remove_filter_state = function(state) { |
|
147 | 1x |
checkmate::assert_class(state, "teal_slices") |
148 | ||
149 | 1x |
isolate({ |
150 | 1x |
logger::log_trace("{ class(self)[1] }$remove_filter_state removing filter(s), dataname: { private$dataname }") |
151 |
# remove state on subjects |
|
152 | 1x |
subject_state <- Filter(function(x) is.null(x$experiment), state) |
153 | 1x |
private$get_filter_states()[["subjects"]]$remove_filter_state(subject_state) |
154 | ||
155 |
# remove state on experiments |
|
156 |
# determine target experiments (defined in teal_slices) |
|
157 | 1x |
experiments <- unique(unlist(lapply(state, "[[", "experiment"))) |
158 | 1x |
available_experiments <- setdiff(names(private$get_filter_states()), "subjects") |
159 | 1x |
excluded_filters <- setdiff(experiments, available_experiments) |
160 | 1x |
if (length(excluded_filters)) { |
161 | ! |
stop(sprintf( |
162 | ! |
"%s doesn't contain elements specified in 'experiment': %s\n'experiment' should be a subset of: %s", |
163 | ! |
private$dataname, |
164 | ! |
toString(excluded_filters), |
165 | ! |
toString(available_experiments) |
166 |
)) |
|
167 |
} |
|
168 |
# remove states on state_lists with corresponding experiments |
|
169 | 1x |
lapply(experiments, function(experiment) { |
170 | ! |
slices <- Filter(function(x) identical(x$experiment, experiment), state) |
171 | ! |
private$get_filter_states()[[experiment]]$remove_filter_state(slices) |
172 |
}) |
|
173 | ||
174 | ||
175 | 1x |
logger::log_trace("{ class(self)[1] }$remove_filter_state removed filter(s), dataname: { private$dataname }") |
176 |
}) |
|
177 | ||
178 | 1x |
invisible(NULL) |
179 |
}, |
|
180 | ||
181 |
#' @description |
|
182 |
#' UI module to add filter variable for this dataset. |
|
183 |
#' @param id (`character(1)`) |
|
184 |
#' `shiny` module instance id. |
|
185 |
#' |
|
186 |
#' @return `shiny.tag` |
|
187 |
#' |
|
188 |
ui_add = function(id) { |
|
189 | ! |
ns <- NS(id) |
190 | ! |
data <- self$get_dataset() |
191 | ! |
experiment_names <- names(data) |
192 | ||
193 | ! |
tags$div( |
194 | ! |
tags$label("Add", tags$code(self$get_dataname()), "filter"), |
195 | ! |
tags$br(), |
196 | ! |
HTML("►"), |
197 | ! |
tags$label("Add subjects filter"), |
198 | ! |
private$get_filter_states()[["subjects"]]$ui_add(id = ns("subjects")), |
199 | ! |
tagList( |
200 | ! |
lapply( |
201 | ! |
experiment_names, |
202 | ! |
function(experiment_name) { |
203 | ! |
tagList( |
204 | ! |
HTML("►"), |
205 | ! |
tags$label("Add", tags$code(experiment_name), "filter"), |
206 | ! |
private$get_filter_states()[[experiment_name]]$ui_add(id = ns(experiment_name)) |
207 |
) |
|
208 |
} |
|
209 |
) |
|
210 |
) |
|
211 |
) |
|
212 |
}, |
|
213 | ||
214 |
#' @description |
|
215 |
#' Creates row for filter overview in the form of \cr |
|
216 |
#' `dataname -- observations (remaining/total) -- subjects (remaining/total)` - MAE |
|
217 |
#' @return A `data.frame`. |
|
218 |
get_filter_overview = function() { |
|
219 | 2x |
data <- self$get_dataset() |
220 | 2x |
data_filtered <- self$get_dataset(TRUE) |
221 | 2x |
experiment_names <- names(data) |
222 | ||
223 | 2x |
mae_info <- data.frame( |
224 | 2x |
dataname = private$dataname, |
225 | 2x |
subjects = nrow(SummarizedExperiment::colData(data)), |
226 | 2x |
subjects_filtered = nrow(SummarizedExperiment::colData(data_filtered())) |
227 |
) |
|
228 | ||
229 | 2x |
experiment_obs_info <- do.call("rbind", lapply( |
230 | 2x |
experiment_names, |
231 | 2x |
function(experiment_name) { |
232 | 10x |
data.frame( |
233 | 10x |
dataname = sprintf("- %s", experiment_name), |
234 | 10x |
obs = nrow(data[[experiment_name]]), |
235 | 10x |
obs_filtered = nrow(data_filtered()[[experiment_name]]) |
236 |
) |
|
237 |
} |
|
238 |
)) |
|
239 | ||
240 | 2x |
get_experiment_keys <- function(mae, experiment) { |
241 | 20x |
sample_subset <- subset(MultiAssayExperiment::sampleMap(mae), colname %in% colnames(experiment)) |
242 | 20x |
length(unique(sample_subset$primary)) |
243 |
} |
|
244 | ||
245 | 2x |
experiment_subjects_info <- do.call("rbind", lapply( |
246 | 2x |
experiment_names, |
247 | 2x |
function(experiment_name) { |
248 | 10x |
data.frame( |
249 | 10x |
subjects = get_experiment_keys(data, data[[experiment_name]]), |
250 | 10x |
subjects_filtered = get_experiment_keys(data_filtered(), data_filtered()[[experiment_name]]) |
251 |
) |
|
252 |
} |
|
253 |
)) |
|
254 | ||
255 | 2x |
experiment_info <- cbind(experiment_obs_info, experiment_subjects_info) |
256 | 2x |
dplyr::bind_rows(mae_info, experiment_info) |
257 |
} |
|
258 |
) |
|
259 |
) |
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 | 580x |
checkmate::assert_string(dataname) |
125 | 573x |
checkmate::assert_flag(fixed) |
126 | 571x |
checkmate::assert_flag(anchored) |
127 | ||
128 | 569x |
formal_args <- as.list(environment()) |
129 | ||
130 | 569x |
if (!missing(expr) && !missing(varname)) { |
131 | ! |
stop("Must provide either `expr` or `varname`.") |
132 | 569x |
} 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 | 539x |
} else if (!missing(varname)) { |
143 | 538x |
checkmate::assert_string(varname) |
144 | 535x |
checkmate::assert_multi_class(choices, .filterable_class, null.ok = TRUE) |
145 | 534x |
checkmate::assert_multi_class(selected, .filterable_class, null.ok = TRUE) |
146 | 532x |
checkmate::assert_flag(keep_na, null.ok = TRUE) |
147 | 531x |
checkmate::assert_flag(keep_inf, null.ok = TRUE) |
148 | 530x |
checkmate::assert_flag(multiple) |
149 | ||
150 | 530x |
ts_var_args <- c( |
151 | 530x |
"dataname", "varname", "id", "choices", "selected", "keep_na", "keep_inf", |
152 | 530x |
"fixed", "anchored", "multiple" |
153 |
) |
|
154 | 530x |
formal_args <- formal_args[ts_var_args] |
155 | 530x |
args <- c(formal_args, list(...)) |
156 | 530x |
args[c("choices", "selected")] <- |
157 | 530x |
lapply(args[c("choices", "selected")], function(x) if (is.factor(x)) as.character(x) else x) |
158 | 530x |
if (missing(id)) { |
159 | 521x |
args$id <- get_default_slice_id(args) |
160 |
} else { |
|
161 | 9x |
checkmate::assert_string(id) |
162 |
} |
|
163 | 527x |
ans <- do.call(reactiveValues, args) |
164 | 527x |
class(ans) <- c("teal_slice_var", "teal_slice", class(ans)) |
165 |
} else { |
|
166 | 1x |
stop("Must provide either `expr` or `varname`.") |
167 |
} |
|
168 | ||
169 | 550x |
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 | 283x |
formal_args <- setdiff(names(formals(teal_slice)), "...") |
204 | ||
205 | 283x |
x <- if (isRunning()) { |
206 | ! |
reactiveValuesToList(x) |
207 |
} else { |
|
208 | 283x |
isolate(reactiveValuesToList(x)) |
209 |
} |
|
210 | ||
211 | 283x |
formal_args <- intersect(formal_args, names(x)) |
212 | 283x |
extra_args <- rev(setdiff(names(x), formal_args)) |
213 | ||
214 | 283x |
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 | 116x |
checkmate::assert_flag(show_all) |
223 | 92x |
checkmate::assert_flag(trim_lines) |
224 | ||
225 | 86x |
x_list <- as.list(x) |
226 | 47x |
if (!show_all) x_list <- Filter(Negate(is.null), x_list) |
227 | ||
228 | 86x |
jsonify(x_list, trim_lines) |
229 |
} |
|
230 | ||
231 |
#' @rdname teal_slice-utilities |
|
232 |
#' @export |
|
233 |
#' |
|
234 |
print.teal_slice <- function(x, ...) { |
|
235 | 15x |
cat(format(x, ...)) |
236 |
} |
|
237 | ||
238 | ||
239 |
# format utils ----- |
|
240 | ||
241 |
#' Convert a list to a justified `JSON` string |
|
242 |
#' |
|
243 |
#' This function takes a list and converts it to a `JSON` string. |
|
244 |
#' The resulting `JSON` string is then optionally justified to improve readability |
|
245 |
#' and trimmed to easier fit in the console when printing. |
|
246 |
#' |
|
247 |
#' @param x (`list`), possibly recursive, obtained from `teal_slice` or `teal_slices`. |
|
248 |
#' @param trim_lines (`logical(1)`) flag specifying whether to trim lines of the `JSON` string. |
|
249 |
#' @return A `JSON` string representation of the input list. |
|
250 |
#' @keywords internal |
|
251 |
#' |
|
252 |
jsonify <- function(x, trim_lines) { |
|
253 | 131x |
checkmate::assert_list(x) |
254 | ||
255 | 131x |
x_json <- to_json(x) |
256 | 131x |
x_json_justified <- justify_json(x_json) |
257 | 121x |
if (trim_lines) x_json_justified <- trim_lines_json(x_json_justified) |
258 | 131x |
paste(x_json_justified, collapse = "\n") |
259 |
} |
|
260 | ||
261 |
#' Converts a list to a `JSON` string |
|
262 |
#' |
|
263 |
#' Converts a list representation of `teal_slice` or `teal_slices` into a `JSON` string. |
|
264 |
#' Ensures proper unboxing of list elements. |
|
265 |
#' This function is used by the `format` methods for `teal_slice` and `teal_slices`. |
|
266 |
#' @param x (`list`) possibly recursive, obtained from `teal_slice` or `teal_slices`. |
|
267 |
#' @return A `JSON` string. |
|
268 |
# |
|
269 |
#' @param x (`list`) representation of `teal_slices` object. |
|
270 |
#' @keywords internal |
|
271 |
#' |
|
272 |
to_json <- function(x) { |
|
273 | 131x |
no_unbox <- function(x) { |
274 | 2390x |
vars <- c("selected", "choices") |
275 | 2390x |
if (is.list(x)) { |
276 | 385x |
for (var in vars) { |
277 | 307x |
if (!is.null(x[[var]])) x[[var]] <- I(format_time(x[[var]])) |
278 |
} |
|
279 | 385x |
lapply(x, no_unbox) |
280 |
} else { |
|
281 | 2005x |
x |
282 |
} |
|
283 |
} |
|
284 | ||
285 | 131x |
jsonlite::toJSON(no_unbox(x), pretty = TRUE, auto_unbox = TRUE, digits = 16, null = "null") |
286 |
} |
|
287 | ||
288 |
#' Format `POSIXt` for storage |
|
289 |
#' |
|
290 |
#' Convert `POSIXt` date time object to character representation in UTC time zone. |
|
291 |
#' |
|
292 |
#' Date times are stored as string representations expressed in the UTC time zone. |
|
293 |
#' The storage format is `YYYY-MM-DD HH:MM:SS`. |
|
294 |
#' |
|
295 |
#' @param x (`POSIXt`) vector of date time values or anything else |
|
296 |
#' |
|
297 |
#' @return If `x` is of class `POSIXt`, a character vector, otherwise `x` itself. |
|
298 |
#' |
|
299 |
#' @keywords internal |
|
300 |
format_time <- function(x) { |
|
301 | 307x |
if ("POSIXt" %in% class(x)) { |
302 | 20x |
format(x, format = "%Y-%m-%d %H:%M:%S", usetz = TRUE, tz = "UTC") |
303 |
} else { |
|
304 | 287x |
x |
305 |
} |
|
306 |
} |
|
307 | ||
308 |
#' Justify colons in `JSON` string |
|
309 |
#' |
|
310 |
#' This function takes a `JSON` string as input, splits it into lines, and pads element names |
|
311 |
#' with spaces so that colons are justified between lines. |
|
312 |
#' |
|
313 |
#' @param json (`character(1)`) a `JSON` string. |
|
314 |
#' |
|
315 |
#' @return A list of character strings, which can be collapsed into a `JSON` string. |
|
316 |
#' |
|
317 |
#' @keywords internal |
|
318 |
justify_json <- function(json) { |
|
319 | 131x |
format_name <- function(name, name_width) { |
320 | 2775x |
if (nchar(name) == 1 || nchar(gsub("\\s", "", name)) <= 2) { |
321 | 630x |
return(name) |
322 | 2145x |
} else if (grepl("slices|attributes", name)) { |
323 | 90x |
paste0(name, ":") |
324 |
} else { |
|
325 | 2055x |
paste(format(name, width = name_width), ":") |
326 |
} |
|
327 |
} |
|
328 | 131x |
json_lines <- strsplit(json, "\n")[[1]] |
329 | 131x |
json_lines_split <- regmatches(json_lines, regexpr(":", json_lines), invert = TRUE) |
330 | 131x |
name_width <- max(unlist(regexpr(":", json_lines))) - 1 |
331 | 131x |
vapply(json_lines_split, function(x) paste0(format_name(x[1], name_width), stats::na.omit(x[2])), character(1)) |
332 |
} |
|
333 | ||
334 |
#' Trim lines in `JSON` string |
|
335 |
#' |
|
336 |
#' This function takes a `JSON` string as input and returns a modified version of the |
|
337 |
#' input where the values portion of each line is trimmed for a less messy console output. |
|
338 |
#' |
|
339 |
#' @param x (`character`) |
|
340 |
#' |
|
341 |
#' @return A character string trimmed after a certain hard-coded number of characters in the value portion. |
|
342 |
#' |
|
343 |
#' @keywords internal |
|
344 |
#' |
|
345 |
trim_lines_json <- function(x) { |
|
346 | 121x |
name_width <- max(unlist(gregexpr(":", x))) - 1 |
347 | 121x |
trim_position <- name_width + 37L |
348 | 121x |
x_trim <- substr(x, 1, trim_position) |
349 | 121x |
substr(x_trim, trim_position - 2, trim_position) <- "..." |
350 | 121x |
x_trim |
351 |
} |
|
352 | ||
353 |
#' Default `teal_slice` id |
|
354 |
#' |
|
355 |
#' Create a slice id if none provided. |
|
356 |
#' |
|
357 |
#' Function returns a default `id` for a `teal_slice` object which needs |
|
358 |
#' to be distinct from other `teal_slice` objects created for any |
|
359 |
#' `FilterStates` object. Returned `id` can be treated as a location of |
|
360 |
#' a vector on which `FilterState` is built: |
|
361 |
#' - for a `data.frame` `id` concatenates `dataname` and `varname`. |
|
362 |
#' - for a `MultiAssayExperiment` `id` concatenates `dataname`, `varname`, |
|
363 |
#' `experiment` and `arg`, so that one can add `teal_slice` for a `varname` |
|
364 |
#' which exists in multiple `SummarizedExperiment`s or exists in both `colData` |
|
365 |
#' and `rowData` of given experiment. |
|
366 |
#' For such a vector `teal.slice` doesn't allow to activate more than one filters. |
|
367 |
#' In case of `teal_slice_expr` `id` is mandatory and must be unique. |
|
368 |
#' |
|
369 |
#' @param x (`teal_slice` or `list`) |
|
370 |
#' @return (`character(1)`) `id` for a `teal_slice` object. |
|
371 |
#' |
|
372 |
#' @keywords internal |
|
373 |
get_default_slice_id <- function(x) { |
|
374 | 610x |
checkmate::assert_multi_class(x, c("teal_slice", "list")) |
375 | 610x |
isolate({ |
376 | 610x |
if (inherits(x, "teal_slice_expr") || is.null(x$varname)) { |
377 | 10x |
x$id |
378 |
} else { |
|
379 | 600x |
paste( |
380 | 600x |
Filter( |
381 | 600x |
length, |
382 | 600x |
as.list(x)[c("dataname", "varname", "experiment", "arg")] |
383 |
), |
|
384 | 600x |
collapse = " " |
385 |
) |
|
386 |
} |
|
387 |
}) |
|
388 |
} |
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 | 103x |
checkmate::assert_data_frame(dataset) |
72 | 101x |
super$initialize(dataset, dataname, keys, label) |
73 | ||
74 |
# overwrite filtered_data if there is relationship with parent dataset |
|
75 | 99x |
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_trace("filtering data dataname: { dataname }, sid: { sid }") |
86 |
} else { |
|
87 | ! |
logger::log_trace("filtering data dataname: { private$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 | 99x |
private$add_filter_states( |
99 | 99x |
filter_states = init_filter_states( |
100 | 99x |
data = dataset, |
101 | 99x |
data_reactive = private$data_filtered_fun, |
102 | 99x |
dataname = dataname, |
103 | 99x |
keys = self$get_keys() |
104 |
), |
|
105 | 99x |
id = "filter" |
106 |
) |
|
107 | ||
108 |
# todo: Should we make these defaults? It could be handled by the app developer |
|
109 | 99x |
if (!is.null(parent)) { |
110 | 10x |
fs <- teal_slices( |
111 | 10x |
exclude_varnames = structure( |
112 | 10x |
list(intersect(colnames(dataset), colnames(isolate(parent())))), |
113 | 10x |
names = private$dataname |
114 |
) |
|
115 |
) |
|
116 | 10x |
self$set_filter_state(fs) |
117 |
} |
|
118 | ||
119 | 99x |
invisible(self) |
120 |
}, |
|
121 | ||
122 |
#' @description |
|
123 |
#' Gets the subset expression. |
|
124 |
#' |
|
125 |
#' This function returns subset expressions equivalent to selected items |
|
126 |
#' within each of `filter_states`. Configuration of the expressions is constant and |
|
127 |
#' depends on `filter_states` type and order which are set during initialization. |
|
128 |
#' This class contains single `FilterStates` which contains single `state_list` |
|
129 |
#' and all `FilterState` objects apply to one argument (`...`) in a `dplyr::filter` call. |
|
130 |
#' |
|
131 |
#' @param sid (`character`) |
|
132 |
#' when specified, the method returns code containing conditions calls of |
|
133 |
#' `FilterState` objects with `sid` different to that of this `sid` argument. |
|
134 |
#' |
|
135 |
#' @return Either a `list` of length 1 containing a filter `call`, or `NULL`. |
|
136 |
get_call = function(sid = "") { |
|
137 | 42x |
logger::log_trace("DataframeFilteredDataset$get_call initializing for dataname: { private$dataname }") |
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 |
logger::log_trace("DataframeFilteredDataset$get_call initializing for dataname: { private$dataname }") |
181 | 42x |
filter_call |
182 |
}, |
|
183 | ||
184 |
#' @description |
|
185 |
#' Set filter state. |
|
186 |
#' |
|
187 |
#' @param state (`teal_slices`) |
|
188 |
#' @return `NULL`, invisibly. |
|
189 |
#' |
|
190 |
set_filter_state = function(state) { |
|
191 | 81x |
isolate({ |
192 | 81x |
logger::log_trace("{ class(self)[1] }$set_filter_state initializing, dataname: { private$dataname }") |
193 | 81x |
checkmate::assert_class(state, "teal_slices") |
194 | 80x |
lapply(state, function(slice) { |
195 | 97x |
checkmate::assert_true(slice$dataname == private$dataname) |
196 |
}) |
|
197 | 80x |
private$get_filter_states()[[1L]]$set_filter_state(state = state) |
198 | 80x |
invisible(NULL) |
199 |
}) |
|
200 |
}, |
|
201 | ||
202 |
#' @description |
|
203 |
#' Remove one or more `FilterState` form a `FilteredDataset`. |
|
204 |
#' |
|
205 |
#' @param state (`teal_slices`) |
|
206 |
#' specifying `FilterState` objects to remove; |
|
207 |
#' `teal_slice`s may contain only `dataname` and `varname`, other elements are ignored |
|
208 |
#' |
|
209 |
#' @return `NULL`, invisibly. |
|
210 |
#' |
|
211 |
remove_filter_state = function(state) { |
|
212 | 11x |
checkmate::assert_class(state, "teal_slices") |
213 | ||
214 | 11x |
isolate({ |
215 | 11x |
logger::log_trace("{ class(self)[1] }$remove_filter_state removing filter(s), dataname: { private$dataname }") |
216 | ||
217 | 11x |
varnames <- unique(unlist(lapply(state, "[[", "varname"))) |
218 | 11x |
private$get_filter_states()[[1]]$remove_filter_state(state) |
219 | ||
220 | 11x |
logger::log_trace("{ class(self)[1] }$remove_filter_state removed filter(s), dataname: { private$dataname }") |
221 |
}) |
|
222 | ||
223 | 11x |
invisible(NULL) |
224 |
}, |
|
225 | ||
226 |
#' @description |
|
227 |
#' UI module to add filter variable for this dataset. |
|
228 |
#' |
|
229 |
#' @param id (`character(1)`) |
|
230 |
#' `shiny` module instance id. |
|
231 |
#' |
|
232 |
#' @return `shiny.tag` |
|
233 |
ui_add = function(id) { |
|
234 | ! |
ns <- NS(id) |
235 | ! |
tagList( |
236 | ! |
tags$label("Add", tags$code(self$get_dataname()), "filter"), |
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_trace("FilteredDataset$srv_filter_overview initialized") |
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 |
# 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_trace("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$observers$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_trace("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$observers$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_trace("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$observers$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_trace("DatetimeFilterState$server@2 selection changed, id: { private$get_id() }") |
473 |
} |
|
474 |
) |
|
475 | ||
476 | ! |
private$keep_na_srv("keep_na") |
477 | ||
478 | ! |
private$observers$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_trace("DatetimeFilterState$server@2 reset start date, id: { private$get_id() }") |
489 |
} |
|
490 |
) |
|
491 | ! |
private$observers$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_trace("DatetimeFilterState$server@3 reset end date, id: { private$get_id() }") |
502 |
} |
|
503 |
) |
|
504 | ||
505 | ! |
logger::log_trace("DatetimeFilterState$server initialized, id: { private$get_id() }") |
506 | ! |
NULL |
507 |
} |
|
508 |
) |
|
509 |
}, |
|
510 |
server_inputs_fixed = function(id) { |
|
511 | ! |
moduleServer( |
512 | ! |
id = id, |
513 | ! |
function(input, output, session) { |
514 | ! |
logger::log_trace("DatetimeFilterState$server initializing, id: { private$get_id() }") |
515 | ||
516 | ! |
output$selection <- renderUI({ |
517 | ! |
vals <- format(private$get_selected(), usetz = TRUE, nsmall = 3) |
518 | ! |
tags$div( |
519 | ! |
tags$div(icon("clock"), vals[1]), |
520 | ! |
tags$div(span(" - "), icon("clock"), vals[2]) |
521 |
) |
|
522 |
}) |
|
523 | ||
524 | ! |
logger::log_trace("DatetimeFilterState$server initialized, id: { private$get_id() }") |
525 | ! |
NULL |
526 |
} |
|
527 |
) |
|
528 |
}, |
|
529 | ||
530 |
# @description |
|
531 |
# UI module to display filter summary |
|
532 |
# renders text describing selected date range and |
|
533 |
# if NA are included also |
|
534 |
content_summary = function(id) { |
|
535 | ! |
selected <- format(private$get_selected(), "%Y-%m-%d %H:%M:%S") |
536 | ! |
min <- selected[1] |
537 | ! |
max <- selected[2] |
538 | ! |