1 |
# FilterState ------ |
|
2 | ||
3 |
#' @name FilterState |
|
4 |
#' @docType class |
|
5 |
#' |
|
6 |
#' @title `FilterState` abstract class |
|
7 |
#' |
|
8 |
#' @description Abstract class to encapsulate single filter state. |
|
9 |
#' |
|
10 |
#' @details |
|
11 |
#' This class is responsible for managing a single filter item within a `FilteredData` object |
|
12 |
#' and outputs a condition call (logical predicate) for subsetting one variable. |
|
13 |
#' Filter states depend on the variable type: |
|
14 |
#' (`logical`, `integer`, `numeric`, `character`, `factor`, `Date`, `POSIXct`, `POSIXlt`) |
|
15 |
#' and `FilterState` subclasses exist that correspond to those types. |
|
16 |
#' - `logical`: `class = LogicalFilterState` |
|
17 |
#' - `integer`: `class = RangeFilterState` |
|
18 |
#' - `numeric`: `class = RangeFilterState` |
|
19 |
#' - `character`: `class = ChoicesFilterState` |
|
20 |
#' - `factor`: `class = ChoicesFilterState` |
|
21 |
#' - `Date`: `class = DateFilterState` |
|
22 |
#' - `POSIXct`, `POSIXlt`: `class = DatetimeFilterState` |
|
23 |
#' - all `NA` entries: `class: FilterState`, cannot be filtered |
|
24 |
#' - default: `FilterState`, cannot be filtered |
|
25 |
#' |
|
26 |
#' Each variable's filter state is an `R6` object keeps the variable that is filtered, |
|
27 |
#' a `teal_slice` object that describes the filter state, as well as a `shiny` module (UI and server) |
|
28 |
#' that allows the user to alter the filter state. |
|
29 |
#' Changes to the filter state that cause some observations to be omitted |
|
30 |
#' trigger the `get_call` method and every `R` function call up in the reactive chain. |
|
31 |
#' |
|
32 |
#' @section Modifying state: |
|
33 |
#' Modifying a `FilterState` object is possible in three scenarios: |
|
34 |
#' - In an interactive session, by passing an appropriate `teal_slice` to the `set_state` method. |
|
35 |
#' - In a running application, by changing appropriate inputs. |
|
36 |
#' - In a running application, by using [filter_state_api] which directly uses |
|
37 |
#' `set_state` method of the `FilterState` object. |
|
38 |
#' |
|
39 |
#' @keywords internal |
|
40 |
#' |
|
41 |
FilterState <- R6::R6Class( # nolint |
|
42 |
"FilterState", |
|
43 | ||
44 |
# public methods ---- |
|
45 |
public = list( |
|
46 | ||
47 |
#' @description |
|
48 |
#' Initialize a `FilterState` object. |
|
49 |
#' |
|
50 |
#' @param x (`vector`) |
|
51 |
#' variable to be filtered. |
|
52 |
#' @param x_reactive (`reactive`) |
|
53 |
#' returning vector of the same type as `x`. Is used to update |
|
54 |
#' counts following the change in values of the filtered dataset. |
|
55 |
#' If it is set to `reactive(NULL)` then counts based on filtered |
|
56 |
#' dataset are not shown. |
|
57 |
#' @param slice (`teal_slice`) |
|
58 |
#' specification of this filter state. |
|
59 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`. |
|
60 |
#' `get_state` returns `teal_slice` object which can be reused in other places. |
|
61 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e. |
|
62 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`. |
|
63 |
#' @param extract_type (`character`) |
|
64 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values: |
|
65 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed |
|
66 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>` |
|
67 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]` |
|
68 |
#' |
|
69 |
#' @return Object of class `FilterState`, invisibly. |
|
70 |
#' |
|
71 |
initialize = function(x, |
|
72 |
x_reactive = reactive(NULL), |
|
73 |
slice, |
|
74 |
extract_type = character(0)) { |
|
75 | 364x |
checkmate::assert_class(x_reactive, "reactive") |
76 | 363x |
checkmate::assert_class(slice, "teal_slice") |
77 | 361x |
checkmate::assert_character(extract_type, max.len = 1, any.missing = FALSE) |
78 | 361x |
if (length(extract_type) == 1) { |
79 | 50x |
checkmate::assert_choice(extract_type, choices = c("list", "matrix")) |
80 |
} |
|
81 | ||
82 |
# Set data properties. |
|
83 | 360x |
private$x <- x |
84 | 360x |
private$x_reactive <- x_reactive |
85 |
# Set derived data properties. |
|
86 | 360x |
private$na_count <- sum(is.na(x)) |
87 | 360x |
private$filtered_na_count <- reactive( |
88 | 360x |
if (!is.null(private$x_reactive())) { |
89 | ! |
sum(is.na(private$x_reactive())) |
90 |
} |
|
91 |
) |
|
92 |
# Set extract type. |
|
93 | 360x |
private$extract_type <- extract_type |
94 | ||
95 |
# Set state properties. |
|
96 | 19x |
if (is.null(isolate(slice$keep_na)) && anyNA(x)) slice$keep_na <- TRUE |
97 | 360x |
private$teal_slice <- slice |
98 |
# Obtain variable label. |
|
99 | 360x |
varlabel <- attr(x, "label", exact = TRUE) |
100 |
# Display only when different from varname. |
|
101 | 360x |
private$varlabel <- |
102 | 360x |
if (is.null(varlabel) || identical(varlabel, private$get_varname())) { |
103 | 359x |
character(0) |
104 |
} else { |
|
105 | 1x |
varlabel |
106 |
} |
|
107 | ||
108 | 360x |
private$state_history <- reactiveVal(list()) |
109 | ||
110 | 360x |
invisible(self) |
111 |
}, |
|
112 | ||
113 |
#' @description |
|
114 |
#' Returns a formatted string representing this `FilterState` object. |
|
115 |
#' |
|
116 |
#' @param show_all (`logical(1)`) passed to `format.teal_slice` |
|
117 |
#' @param trim_lines (`logical(1)`) passed to `format.teal_slice` |
|
118 |
#' |
|
119 |
#' @return `character(1)` the formatted string |
|
120 |
#' |
|
121 |
format = function(show_all = FALSE, trim_lines = TRUE) { |
|
122 | 68x |
sprintf( |
123 | 68x |
"%s:\n%s", |
124 | 68x |
class(self)[1], |
125 | 68x |
format(self$get_state(), show_all = show_all, trim_lines = trim_lines) |
126 |
) |
|
127 |
}, |
|
128 | ||
129 |
#' @description |
|
130 |
#' Prints this `FilterState` object. |
|
131 |
#' |
|
132 |
#' @param ... additional arguments |
|
133 |
#' |
|
134 |
print = function(...) { |
|
135 | 14x |
cat(isolate(self$format(...))) |
136 |
}, |
|
137 | ||
138 |
#' @description |
|
139 |
#' Sets mutable parameters of the filter state. |
|
140 |
#' - `fixed` state is prevented from changing state |
|
141 |
#' - `anchored` state is prevented from removing state |
|
142 |
#' |
|
143 |
#' @param state (`teal_slice`) |
|
144 |
#' |
|
145 |
#' @return `self` invisibly |
|
146 |
#' |
|
147 |
set_state = function(state) { |
|
148 | 89x |
checkmate::assert_class(state, "teal_slice") |
149 | 88x |
if (private$is_fixed()) { |
150 | 1x |
warning("attempt to set state on fixed filter aborted id: ", private$get_id()) |
151 |
} else { |
|
152 | 87x |
logger::log_debug("{ class(self)[1] }$set_state setting state of filter id: { private$get_id() }") |
153 | 87x |
isolate({ |
154 | 87x |
if (!is.null(state$selected)) { |
155 | 78x |
private$set_selected(state$selected) |
156 |
} |
|
157 | 75x |
if (!is.null(state$keep_na)) { |
158 | 16x |
private$set_keep_na(state$keep_na) |
159 |
} |
|
160 | 75x |
if (!is.null(state$keep_inf)) { |
161 | 9x |
private$set_keep_inf(state$keep_inf) |
162 |
} |
|
163 | 75x |
current_state <- sprintf( |
164 | 75x |
"selected: %s; keep_na: %s; keep_inf: %s", |
165 | 75x |
toString(private$get_selected()), |
166 | 75x |
private$get_keep_na(), |
167 | 75x |
private$get_keep_inf() |
168 |
) |
|
169 |
}) |
|
170 |
} |
|
171 | ||
172 | 76x |
invisible(self) |
173 |
}, |
|
174 | ||
175 | ||
176 |
#' @description |
|
177 |
#' Returns a complete description of the filter state. |
|
178 |
#' |
|
179 |
#' @return A `teal_slice` object. |
|
180 |
#' |
|
181 |
get_state = function() { |
|
182 | 845x |
private$teal_slice |
183 |
}, |
|
184 | ||
185 |
#' @description |
|
186 |
#' Returns reproducible condition call for current selection relevant |
|
187 |
#' for selected variable type. |
|
188 |
#' Method is using internal reactive values which makes it reactive |
|
189 |
#' and must be executed in reactive or isolated context. |
|
190 |
#' |
|
191 |
get_call = function() { |
|
192 | 1x |
stop("this is a virtual method") |
193 |
}, |
|
194 | ||
195 |
#' @description |
|
196 |
#' `shiny` module server. |
|
197 |
#' |
|
198 |
#' @param id (`character(1)`) |
|
199 |
#' `shiny` module instance id. |
|
200 |
#' |
|
201 |
#' @param remove_callback (`function`) |
|
202 |
#' callback to handle removal of this `FilterState` object from `state_list` |
|
203 |
#' |
|
204 |
#' @return Reactive expression signaling that remove button has been clicked. |
|
205 |
#' |
|
206 |
server = function(id, remove_callback) { |
|
207 | 12x |
moduleServer( |
208 | 12x |
id = id, |
209 | 12x |
function(input, output, session) { |
210 | 12x |
logger::log_debug("FilterState$server initializing module for slice: { private$get_id() } ") |
211 | 12x |
private$server_summary("summary") |
212 | 12x |
if (private$is_fixed()) { |
213 | ! |
private$server_inputs_fixed("inputs") |
214 |
} else { |
|
215 | 12x |
private$server_inputs("inputs") |
216 |
} |
|
217 | ||
218 | 12x |
private$session_bindings[[session$ns("state")]] <- observeEvent( |
219 | 12x |
eventExpr = list(private$get_selected(), private$get_keep_na(), private$get_keep_inf()), |
220 | 12x |
handlerExpr = { |
221 | 4x |
current_state <- as.list(self$get_state()) |
222 | 4x |
history <- private$state_history() |
223 | 4x |
history_update <- c(history, list(current_state)) |
224 | 4x |
private$state_history(history_update) |
225 |
} |
|
226 |
) |
|
227 | ||
228 | 12x |
private$session_bindings[[session$ns("back")]] <- observeEvent( |
229 | 12x |
eventExpr = input$back, |
230 | 12x |
handlerExpr = { |
231 | ! |
history <- rev(private$state_history()) |
232 | ! |
slice <- history[[2L]] |
233 | ! |
history_update <- rev(history[-(1:2)]) |
234 | ! |
private$state_history(history_update) |
235 | ! |
self$set_state(as.teal_slice(slice)) |
236 |
} |
|
237 |
) |
|
238 | ||
239 | 12x |
private$session_bindings[[session$ns("reset")]] <- observeEvent( |
240 | 12x |
eventExpr = input$reset, |
241 | 12x |
handlerExpr = { |
242 | ! |
slice <- private$state_history()[[1L]] |
243 | ! |
self$set_state(as.teal_slice(slice)) |
244 |
} |
|
245 |
) |
|
246 | ||
247 |
# Buttons for rewind/reset are disabled upon change in history to prevent double-clicking. |
|
248 |
# Re-enabling occurs after 100 ms, after they are potentially hidden when no history is present. |
|
249 | 12x |
private$session_bindings[[session$ns("state_history")]] <- observeEvent( |
250 | 12x |
eventExpr = private$state_history(), |
251 | 12x |
handlerExpr = { |
252 | 4x |
shinyjs::disable(id = "back") |
253 | 4x |
shinyjs::disable(id = "reset") |
254 | 4x |
shinyjs::delay( |
255 | 4x |
ms = 100, |
256 | 4x |
expr = { |
257 | ! |
shinyjs::toggleElement(id = "back", condition = length(private$state_history()) > 1L) |
258 | ! |
shinyjs::enable(id = "back") |
259 |
} |
|
260 |
) |
|
261 | 4x |
shinyjs::delay( |
262 | 4x |
ms = 100, |
263 | 4x |
expr = { |
264 | ! |
shinyjs::toggleElement(id = "reset", condition = length(private$state_history()) > 1L) |
265 | ! |
shinyjs::enable(id = "reset") |
266 |
} |
|
267 |
) |
|
268 |
} |
|
269 |
) |
|
270 | ||
271 | 12x |
private$session_bindings[[session$ns("remove")]] <- observeEvent( |
272 | 12x |
once = TRUE, # remove button can be called once, should be destroyed afterwards |
273 | 12x |
ignoreInit = TRUE, # ignoreInit: should not matter because we destroy the previous input set of the UI |
274 | 12x |
eventExpr = input$remove, # when remove button is clicked in the FilterState ui |
275 | 12x |
handlerExpr = remove_callback() |
276 |
) |
|
277 | ||
278 | 12x |
private$session_bindings[[session$ns("inputs")]] <- list( |
279 | 12x |
destroy = function() { |
280 | 12x |
logger::log_debug("Destroying FilterState inputs and observers; id: { private$get_id() }") |
281 | 12x |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
282 |
} |
|
283 |
) |
|
284 | ||
285 | 12x |
private$state_history <- reactiveVal(list()) |
286 | ||
287 | 12x |
NULL |
288 |
} |
|
289 |
) |
|
290 |
}, |
|
291 | ||
292 |
#' @description |
|
293 |
#' `shiny` UI module. |
|
294 |
#' The UI for this class contains simple message stating that it is not supported. |
|
295 |
#' @param id (`character(1)`) |
|
296 |
#' `shiny` module instance id. |
|
297 |
#' @param parent_id (`character(1)`) id of the `FilterStates` card container |
|
298 |
ui = function(id, parent_id = "cards") { |
|
299 | 12x |
ns <- NS(id) |
300 | ||
301 |
# Filter card consists of header and body, arranged in a single column. |
|
302 |
# Body is hidden and is toggled by clicking on header. |
|
303 |
## Header consists of title and summary, arranged in a column. |
|
304 |
### Title consists of conditional icon, varname, conditional varlabel, and controls, arranged in a row. |
|
305 |
### Summary consists of value and controls, arranged in a row. |
|
306 | ||
307 | 12x |
tags$div( |
308 | 12x |
id = id, |
309 | 12x |
class = "panel filter-card", |
310 | 12x |
include_js_files("count-bar-labels.js"), |
311 | 12x |
tags$div( |
312 | 12x |
class = "filter-card-header", |
313 | 12x |
`data-toggle` = "collapse", |
314 | 12x |
`data-bs-toggle` = "collapse", |
315 | 12x |
href = paste0("#", ns("body")), |
316 | 12x |
tags$div( |
317 | 12x |
class = "filter-card-title", |
318 | 12x |
if (private$is_anchored() && private$is_fixed()) { |
319 | ! |
icon("anchor-lock", class = "filter-card-icon") |
320 | 12x |
} else if (private$is_anchored() && !private$is_fixed()) { |
321 | ! |
icon("anchor", class = "filter-card-icon") |
322 | 12x |
} else if (!private$is_anchored() && private$is_fixed()) { |
323 | ! |
icon("lock", class = "filter-card-icon") |
324 |
}, |
|
325 | 12x |
tags$div(class = "filter-card-varname", tags$strong(private$get_varname())), |
326 | 12x |
tags$div(class = "filter-card-varlabel", private$get_varlabel()), |
327 | 12x |
tags$div( |
328 | 12x |
class = "filter-card-controls", |
329 |
# Suppress toggling body when clicking on this div. |
|
330 |
# This is for bootstrap 3 and 4. Causes page to scroll to top, prevented by setting href on buttons. |
|
331 | 12x |
onclick = "event.stopPropagation();event.preventDefault();", |
332 |
# This is for bootstrap 5. |
|
333 | 12x |
`data-bs-toggle` = "collapse", |
334 | 12x |
`data-bs-target` = NULL, |
335 | 12x |
if (isFALSE(private$is_fixed())) { |
336 | 12x |
actionLink( |
337 | 12x |
inputId = ns("back"), |
338 | 12x |
label = NULL, |
339 | 12x |
icon = icon("circle-arrow-left", lib = "font-awesome"), |
340 | 12x |
title = "Rewind state", |
341 | 12x |
class = "filter-card-back", |
342 | 12x |
style = "display: none" |
343 |
) |
|
344 |
}, |
|
345 | 12x |
if (isFALSE(private$is_fixed())) { |
346 | 12x |
actionLink( |
347 | 12x |
inputId = ns("reset"), |
348 | 12x |
label = NULL, |
349 | 12x |
icon = icon("circle-arrow-up", lib = "font-awesome"), |
350 | 12x |
title = "Restore original state", |
351 | 12x |
class = "filter-card-back", |
352 | 12x |
style = "display: none" |
353 |
) |
|
354 |
}, |
|
355 | 12x |
if (isFALSE(private$is_anchored())) { |
356 | 12x |
actionLink( |
357 | 12x |
inputId = ns("remove"), |
358 | 12x |
label = icon("circle-xmark", lib = "font-awesome"), |
359 | 12x |
title = "Remove filter", |
360 | 12x |
class = "filter-card-remove" |
361 |
) |
|
362 |
} |
|
363 |
) |
|
364 |
), |
|
365 | 12x |
tags$div(class = "filter-card-summary", private$ui_summary(ns("summary"))) |
366 |
), |
|
367 | 12x |
tags$div( |
368 | 12x |
id = ns("body"), |
369 | 12x |
class = "collapse out", |
370 | 12x |
`data-parent` = paste0("#", parent_id), |
371 | 12x |
`data-bs-parent` = paste0("#", parent_id), |
372 | 12x |
tags$div( |
373 | 12x |
class = "filter-card-body", |
374 | 12x |
if (private$is_fixed()) { |
375 | ! |
private$ui_inputs_fixed(ns("inputs")) |
376 |
} else { |
|
377 | 12x |
private$ui_inputs(ns("inputs")) |
378 |
} |
|
379 |
) |
|
380 |
) |
|
381 |
) |
|
382 |
}, |
|
383 | ||
384 |
#' @description |
|
385 |
#' Destroy inputs and observers stored in `private$session_bindings`. |
|
386 |
#' |
|
387 |
#' |
|
388 |
#' @return `NULL`, invisibly. |
|
389 |
#' |
|
390 |
finalize = function() { |
|
391 | 431x |
.finalize_session_bindings(self, private) |
392 | 431x |
invisible(NULL) |
393 |
} |
|
394 |
), |
|
395 | ||
396 |
# private members ---- |
|
397 |
private = list( |
|
398 |
# set by constructor |
|
399 |
x = NULL, # the filtered variable |
|
400 |
x_reactive = NULL, # reactive containing the filtered variable, used for updating counts and histograms |
|
401 |
teal_slice = NULL, # stores all transferable properties of this filter state |
|
402 |
extract_type = character(0), # used by private$get_varname_prefixed |
|
403 |
na_count = integer(0), |
|
404 |
filtered_na_count = NULL, # reactive containing the count of NA in the filtered dataset |
|
405 |
varlabel = character(0), # taken from variable labels in data; displayed in filter cards |
|
406 |
# other |
|
407 |
is_choice_limited = FALSE, # flag whether number of possible choices was limited when specifying filter |
|
408 |
session_bindings = list(), # stores observers and inputs to destroy afterwards |
|
409 |
state_history = NULL, # reactiveVal holding a list storing states this FilterState has had since instantiation |
|
410 | ||
411 |
# private methods ---- |
|
412 | ||
413 |
# setters for state features ---- |
|
414 | ||
415 |
# @description |
|
416 |
# Set values that can be selected from. |
|
417 |
set_choices = function(choices) { |
|
418 | ! |
stop("this is a virtual method") |
419 |
}, |
|
420 | ||
421 |
# @description |
|
422 |
# Set selection. |
|
423 |
# |
|
424 |
# @param value (`vector`) |
|
425 |
# value(s) that come from filter selection; values are set in the |
|
426 |
# module server after a selection is made in the app interface; |
|
427 |
# values are stored in `teal_slice$selected` which is reactive; |
|
428 |
# value types have to be the same as `private$get_choices()` |
|
429 |
# |
|
430 |
# @return `NULL`, invisibly. |
|
431 |
set_selected = function(value) { |
|
432 | 416x |
logger::log_debug( |
433 | 416x |
sprintf( |
434 | 416x |
"%s$set_selected setting selection of id: %s", |
435 | 416x |
class(self)[1], |
436 | 416x |
private$get_id() |
437 |
) |
|
438 |
) |
|
439 | 416x |
isolate({ |
440 | 416x |
value <- private$cast_and_validate(value) |
441 | 405x |
value <- private$check_length(value) |
442 | 399x |
value <- private$remove_out_of_bounds_values(value) |
443 | 399x |
private$teal_slice$selected <- value |
444 |
}) |
|
445 | ||
446 | 399x |
invisible(NULL) |
447 |
}, |
|
448 | ||
449 |
# @description |
|
450 |
# Sets `value` in `private$teal_slice$keep_na`. |
|
451 |
# |
|
452 |
# @param value (`logical(1)`) |
|
453 |
# corresponding to the state of a checkbox input in the `shiny` interface. |
|
454 |
# |
|
455 |
# @return `NULL`, invisibly. |
|
456 |
# |
|
457 |
set_keep_na = function(value) { |
|
458 | 16x |
checkmate::assert_flag(value) |
459 | 16x |
private$teal_slice$keep_na <- value |
460 | 16x |
logger::log_debug( |
461 | 16x |
sprintf( |
462 | 16x |
"%s$set_keep_na set for filter %s to %s.", |
463 | 16x |
class(self)[1], |
464 | 16x |
private$get_id(), |
465 | 16x |
value |
466 |
) |
|
467 |
) |
|
468 | 16x |
invisible(NULL) |
469 |
}, |
|
470 | ||
471 |
# @description |
|
472 |
# Sets `value` in `private$teal_slice$keep_inf`. |
|
473 |
# |
|
474 |
# @param value (`logical(1)`) |
|
475 |
# corresponding to the state of a checkbox input in the `shiny` interface. |
|
476 |
# |
|
477 |
# @return `NULL`, invisibly. |
|
478 |
# |
|
479 |
set_keep_inf = function(value) { |
|
480 | 9x |
checkmate::assert_flag(value) |
481 | 9x |
private$teal_slice$keep_inf <- value |
482 | 9x |
logger::log_debug( |
483 | 9x |
sprintf( |
484 | 9x |
"%s$set_keep_inf of filter %s set to %s", |
485 | 9x |
class(self)[1], |
486 | 9x |
private$get_id(), |
487 | 9x |
value |
488 |
) |
|
489 |
) |
|
490 | ||
491 | 9x |
invisible(NULL) |
492 |
}, |
|
493 | ||
494 |
# getters for state features ---- |
|
495 | ||
496 |
# @description |
|
497 |
# Returns dataname. |
|
498 |
# @return `character(1)` |
|
499 |
get_dataname = function() { |
|
500 | 88x |
isolate(private$teal_slice$dataname) |
501 |
}, |
|
502 | ||
503 |
# @description |
|
504 |
# Get variable name. |
|
505 |
# @return `character(1)` |
|
506 |
get_varname = function() { |
|
507 | 166x |
isolate(private$teal_slice$varname) |
508 |
}, |
|
509 | ||
510 |
# @description |
|
511 |
# Get id of the teal_slice. |
|
512 |
# @return `character(1)` |
|
513 |
get_id = function() { |
|
514 | 4x |
isolate(private$teal_slice$id) |
515 |
}, |
|
516 | ||
517 |
# @description |
|
518 |
# Get allowed values from `FilterState`. |
|
519 |
# @return |
|
520 |
# Vector describing the available choices. Return type depends on the `FilterState` subclass. |
|
521 |
get_choices = function() { |
|
522 | 776x |
isolate(private$teal_slice$choices) |
523 |
}, |
|
524 | ||
525 |
# @description |
|
526 |
# Get selected values from `FilterState`. |
|
527 |
# @return |
|
528 |
# Vector describing the current selection. Return type depends on the `FilterState` subclass. |
|
529 |
get_selected = function() { |
|
530 | 367x |
private$teal_slice$selected |
531 |
}, |
|
532 | ||
533 |
# @description |
|
534 |
# Returns current `keep_na` selection. |
|
535 |
# @return `logical(1)` |
|
536 |
get_keep_na = function() { |
|
537 | 129x |
private$teal_slice$keep_na |
538 |
}, |
|
539 | ||
540 |
# @description |
|
541 |
# Returns current `keep_inf` selection. |
|
542 |
# @return (`logical(1)`) |
|
543 |
get_keep_inf = function() { |
|
544 | 117x |
private$teal_slice$keep_inf |
545 |
}, |
|
546 | ||
547 |
# Check whether this filter is fixed (cannot be changed). |
|
548 |
# @return `logical(1)` |
|
549 |
is_fixed = function() { |
|
550 | 148x |
isolate(isTRUE(private$teal_slice$fixed)) |
551 |
}, |
|
552 | ||
553 |
# Check whether this filter is anchored (cannot be removed). |
|
554 |
# @return `logical(1)` |
|
555 |
is_anchored = function() { |
|
556 | 48x |
isolate(isTRUE(private$teal_slice$anchored)) |
557 |
}, |
|
558 | ||
559 |
# Check whether this filter is capable of selecting multiple values. |
|
560 |
# @return `logical(1)` |
|
561 |
is_multiple = function() { |
|
562 | 218x |
isolate(isTRUE(private$teal_slice$multiple)) |
563 |
}, |
|
564 | ||
565 |
# other ---- |
|
566 | ||
567 |
# @description |
|
568 |
# Returns variable label. |
|
569 |
# @return `character(1)` |
|
570 |
get_varlabel = function() { |
|
571 | 12x |
private$varlabel |
572 |
}, |
|
573 | ||
574 |
# @description |
|
575 |
# Return variable name prefixed by `dataname` to be evaluated as extracted object, for example `data$var` |
|
576 |
# @return Call that extracts the variable from the dataset. |
|
577 |
get_varname_prefixed = function(dataname) { |
|
578 | 109x |
varname <- private$get_varname() |
579 | 109x |
varname_backticked <- sprintf("`%s`", varname) |
580 | 109x |
ans <- |
581 | 109x |
if (isTRUE(private$extract_type == "list")) { |
582 | 16x |
sprintf("%s$%s", dataname, varname_backticked) |
583 | 109x |
} else if (isTRUE(private$extract_type == "matrix")) { |
584 | 7x |
sprintf("%s[, \"%s\"]", dataname, varname) |
585 |
} else { |
|
586 | 86x |
varname_backticked |
587 |
} |
|
588 | 109x |
str2lang(ans) |
589 |
}, |
|
590 | ||
591 |
# @description |
|
592 |
# Adds `is.na(varname)` moiety to the existing condition call, according to `keep_na` status. |
|
593 |
# @param filter_call `call` raw filter call, as defined by selection |
|
594 |
# @param varname `character(1)` name of a variable |
|
595 |
# @return `call` |
|
596 |
add_keep_na_call = function(filter_call, varname) { |
|
597 |
# No need to deal with NAs. |
|
598 | 108x |
if (private$na_count == 0L) { |
599 | 87x |
return(filter_call) |
600 |
} |
|
601 | ||
602 | 21x |
if (is.null(filter_call) && isFALSE(private$get_keep_na())) { |
603 | 2x |
call("!", call("is.na", varname)) |
604 | 19x |
} else if (!is.null(filter_call) && isTRUE(private$get_keep_na())) { |
605 | 12x |
call("|", call("is.na", varname), filter_call) |
606 | 7x |
} else if (!is.null(filter_call) && isFALSE(private$get_keep_na())) { |
607 | 7x |
call("&", call("!", call("is.na", varname)), filter_call) |
608 |
} |
|
609 |
}, |
|
610 | ||
611 |
# Converts values to the type fitting this `FilterState` and validates the conversion. |
|
612 |
# Raises error if casting does not execute successfully. |
|
613 |
# |
|
614 |
# @param values vector of values |
|
615 |
# |
|
616 |
# @return vector converted to appropriate class |
|
617 |
cast_and_validate = function(values) { |
|
618 | 11x |
values |
619 |
}, |
|
620 | ||
621 |
# Checks length of selection. |
|
622 |
check_length = function(values) { |
|
623 | 11x |
values |
624 |
}, |
|
625 | ||
626 |
# Filters out erroneous values from vector. |
|
627 |
# |
|
628 |
# @param values vector of values |
|
629 |
# |
|
630 |
# @return vector in which values that cannot be set in this FilterState have been dropped |
|
631 |
remove_out_of_bounds_values = function(values) { |
|
632 | 31x |
values |
633 |
}, |
|
634 | ||
635 |
# Checks if the selection is valid in terms of class and length. |
|
636 |
# It should not return anything but raise an error if selection |
|
637 |
# has a wrong class or is outside of possible choices |
|
638 |
validate_selection = function(value) { |
|
639 | ! |
invisible(NULL) |
640 |
}, |
|
641 | ||
642 |
# @description |
|
643 |
# Checks whether the current settings actually cause any values to be omitted. |
|
644 |
# @return logical scalar |
|
645 |
is_any_filtered = function() { |
|
646 | 75x |
if (private$is_choice_limited) { |
647 | 3x |
TRUE |
648 | 72x |
} else if (!setequal(private$get_selected(), private$get_choices())) { |
649 | 59x |
TRUE |
650 | 13x |
} else if (!isTRUE(private$get_keep_na()) && private$na_count > 0) { |
651 | 4x |
TRUE |
652 |
} else { |
|
653 | 9x |
FALSE |
654 |
} |
|
655 |
}, |
|
656 | ||
657 |
# shiny modules ----- |
|
658 | ||
659 |
# @description |
|
660 |
# Server module to display filter summary |
|
661 |
# @param id (`character(1)`) `shiny` module instance id. |
|
662 |
ui_summary = function(id) { |
|
663 | 12x |
ns <- NS(id) |
664 | 12x |
uiOutput(ns("summary"), class = "filter-card-summary") |
665 |
}, |
|
666 | ||
667 |
# @description |
|
668 |
# UI module to display filter summary |
|
669 |
# @param id (`character(1)`) `shiny` module instance id. |
|
670 |
# @return Nothing. Renders the UI. |
|
671 |
server_summary = function(id) { |
|
672 | 12x |
moduleServer( |
673 | 12x |
id = id, |
674 | 12x |
function(input, output, session) { |
675 | 12x |
output$summary <- renderUI(private$content_summary()) |
676 |
} |
|
677 |
) |
|
678 |
}, |
|
679 | ||
680 |
# module with inputs |
|
681 |
ui_inputs = function(id) { |
|
682 | ! |
stop("abstract class") |
683 |
}, |
|
684 |
# module with inputs |
|
685 |
server_inputs = function(id) { |
|
686 | ! |
stop("abstract class") |
687 |
}, |
|
688 | ||
689 |
# @description |
|
690 |
# Module displaying inputs in a fixed filter state. |
|
691 |
# There are no input widgets, only selection visualizations. |
|
692 |
# @param id (`character(1)`) `shiny` module instance id. |
|
693 |
ui_inputs_fixed = function(id) { |
|
694 | ! |
ns <- NS(id) |
695 | ! |
tags$div( |
696 | ! |
class = "choices_state", |
697 | ! |
uiOutput(ns("selection")) |
698 |
) |
|
699 |
}, |
|
700 | ||
701 |
# @description |
|
702 |
# Module creating the display of a fixed filter state. |
|
703 |
# @param id (`character(1)`) `shiny` module instance id. |
|
704 |
server_inputs_fixed = function(id) { |
|
705 | ! |
stop("abstract class") |
706 |
}, |
|
707 | ||
708 |
# @description |
|
709 |
# Module UI function displaying input to keep or remove NA in the `FilterState` call. |
|
710 |
# Renders a checkbox input only when variable with which `FilterState` has been created contains NAs. |
|
711 |
# @param id (`character(1)`) `shiny` module instance id. |
|
712 |
keep_na_ui = function(id) { |
|
713 | 14x |
ns <- NS(id) |
714 | 14x |
if (private$na_count > 0) { |
715 | ! |
isolate({ |
716 | ! |
countmax <- private$na_count |
717 | ! |
countnow <- private$filtered_na_count() |
718 | ! |
ui_input <- checkboxInput( |
719 | ! |
inputId = ns("value"), |
720 | ! |
label = tags$span( |
721 | ! |
id = ns("count_label"), |
722 | ! |
make_count_text( |
723 | ! |
label = "Keep NA", |
724 | ! |
countmax = countmax, |
725 | ! |
countnow = countnow |
726 |
) |
|
727 |
), |
|
728 | ! |
value = private$get_keep_na() |
729 |
) |
|
730 | ! |
tags$div( |
731 | ! |
uiOutput(ns("trigger_visible"), inline = TRUE), |
732 | ! |
ui_input |
733 |
) |
|
734 |
}) |
|
735 |
} else { |
|
736 | 14x |
NULL |
737 |
} |
|
738 |
}, |
|
739 | ||
740 |
# @description |
|
741 |
# Module server function to handle NA values in the `FilterState`. |
|
742 |
# Sets `private$slice$keep_na` according to the selection |
|
743 |
# and updates the relevant UI element if `private$slice$keep_na` has been changed by the api. |
|
744 |
# @param id (`character(1)`) `shiny` module instance id. |
|
745 |
# @return `NULL`, invisibly. |
|
746 |
keep_na_srv = function(id) { |
|
747 | 12x |
moduleServer(id, function(input, output, session) { |
748 |
# 1. renderUI is used here as an observer which triggers only if output is visible |
|
749 |
# and if the reactive changes - reactive triggers only if the output is visible. |
|
750 |
# 2. We want to trigger change of the labels only if reactive count changes (not underlying data) |
|
751 | 12x |
output$trigger_visible <- renderUI({ |
752 | 12x |
updateCountText( |
753 | 12x |
inputId = "count_label", |
754 | 12x |
label = "Keep NA", |
755 | 12x |
countmax = private$na_count, |
756 | 12x |
countnow = private$filtered_na_count() |
757 |
) |
|
758 | 12x |
NULL |
759 |
}) |
|
760 | ||
761 |
# this observer is needed in the situation when private$keep_inf has been |
|
762 |
# changed directly by the api - then it's needed to rerender UI element |
|
763 |
# to show relevant values |
|
764 | 12x |
private$session_bindings[[session$ns("keep_na_api")]] <- observeEvent( |
765 | 12x |
ignoreNULL = FALSE, # nothing selected is possible for NA |
766 | 12x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
767 | 12x |
eventExpr = private$get_keep_na(), |
768 | 12x |
handlerExpr = { |
769 | ! |
if (!setequal(private$get_keep_na(), input$value)) { |
770 | ! |
logger::log_debug("FilterState$keep_na_srv@1 changed reactive value, id: { private$get_id() }") |
771 | ! |
updateCheckboxInput( |
772 | ! |
inputId = "value", |
773 | ! |
label = sprintf("Keep NA (%s/%s)", private$filtered_na_count(), private$na_count), |
774 | ! |
value = private$get_keep_na() |
775 |
) |
|
776 |
} |
|
777 |
} |
|
778 |
) |
|
779 | 12x |
private$session_bindings[[session$ns("keep_na")]] <- observeEvent( |
780 | 12x |
ignoreNULL = FALSE, # ignoreNULL: we don't want to ignore NULL when nothing is selected in the `selectInput` |
781 | 12x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
782 | 12x |
eventExpr = input$value, |
783 | 12x |
handlerExpr = { |
784 | ! |
logger::log_debug("FilterState$keep_na_srv@2 changed input, id: { private$get_id() }") |
785 | ! |
keep_na <- if (is.null(input$value)) { |
786 | ! |
FALSE |
787 |
} else { |
|
788 | ! |
input$value |
789 |
} |
|
790 | ! |
private$set_keep_na(keep_na) |
791 |
} |
|
792 |
) |
|
793 | 12x |
invisible(NULL) |
794 |
}) |
|
795 |
} |
|
796 |
) |
|
797 |
) |
1 |
# DateFilterState ------ |
|
2 | ||
3 |
#' @name DateFilterState |
|
4 |
#' @docType class |
|
5 |
#' |
|
6 |
#' @title `FilterState` object for `Date` data |
|
7 |
#' |
|
8 |
#' @description Manages choosing a range of `Date`s. |
|
9 |
#' |
|
10 |
#' @examples |
|
11 |
#' # use non-exported function from teal.slice |
|
12 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice") |
|
13 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice") |
|
14 |
#' DateFilterState <- getFromNamespace("DateFilterState", "teal.slice") |
|
15 |
#' |
|
16 |
#' library(shiny) |
|
17 |
#' |
|
18 |
#' filter_state <- DateFilterState$new( |
|
19 |
#' x = c(Sys.Date() + seq(1:10), NA), |
|
20 |
#' slice = teal_slice(varname = "x", dataname = "data"), |
|
21 |
#' extract_type = character(0) |
|
22 |
#' ) |
|
23 |
#' isolate(filter_state$get_call()) |
|
24 |
#' filter_state$set_state( |
|
25 |
#' teal_slice( |
|
26 |
#' dataname = "data", |
|
27 |
#' varname = "x", |
|
28 |
#' selected = c(Sys.Date() + 3L, Sys.Date() + 8L), |
|
29 |
#' keep_na = TRUE |
|
30 |
#' ) |
|
31 |
#' ) |
|
32 |
#' isolate(filter_state$get_call()) |
|
33 |
#' |
|
34 |
#' # working filter in an app |
|
35 |
#' library(shinyjs) |
|
36 |
#' |
|
37 |
#' dates <- c(Sys.Date() - 100, Sys.Date()) |
|
38 |
#' data_date <- c(seq(from = dates[1], to = dates[2], length.out = 100), NA) |
|
39 |
#' fs <- DateFilterState$new( |
|
40 |
#' x = data_date, |
|
41 |
#' slice = teal_slice( |
|
42 |
#' dataname = "data", varname = "x", selected = data_date[c(47, 98)], keep_na = TRUE |
|
43 |
#' ) |
|
44 |
#' ) |
|
45 |
#' |
|
46 |
#' ui <- fluidPage( |
|
47 |
#' useShinyjs(), |
|
48 |
#' include_css_files(pattern = "filter-panel"), |
|
49 |
#' include_js_files(pattern = "count-bar-labels"), |
|
50 |
#' column(4, tags$div( |
|
51 |
#' tags$h4("DateFilterState"), |
|
52 |
#' fs$ui("fs") |
|
53 |
#' )), |
|
54 |
#' column(4, tags$div( |
|
55 |
#' id = "outputs", # div id is needed for toggling the element |
|
56 |
#' tags$h4("Condition (i.e. call)"), # display the condition call generated by this FilterState |
|
57 |
#' textOutput("condition_date"), tags$br(), |
|
58 |
#' tags$h4("Unformatted state"), # display raw filter state |
|
59 |
#' textOutput("unformatted_date"), tags$br(), |
|
60 |
#' tags$h4("Formatted state"), # display human readable filter state |
|
61 |
#' textOutput("formatted_date"), tags$br() |
|
62 |
#' )), |
|
63 |
#' column(4, tags$div( |
|
64 |
#' tags$h4("Programmatic filter control"), |
|
65 |
#' actionButton("button1_date", "set drop NA", width = "100%"), tags$br(), |
|
66 |
#' actionButton("button2_date", "set keep NA", width = "100%"), tags$br(), |
|
67 |
#' actionButton("button3_date", "set a range", width = "100%"), tags$br(), |
|
68 |
#' actionButton("button4_date", "set full range", width = "100%"), tags$br(), |
|
69 |
#' actionButton("button0_date", "set initial state", width = "100%"), tags$br() |
|
70 |
#' )) |
|
71 |
#' ) |
|
72 |
#' |
|
73 |
#' server <- function(input, output, session) { |
|
74 |
#' fs$server("fs") |
|
75 |
#' output$condition_date <- renderPrint(fs$get_call()) |
|
76 |
#' output$formatted_date <- renderText(fs$format()) |
|
77 |
#' output$unformatted_date <- renderPrint(fs$get_state()) |
|
78 |
#' # modify filter state programmatically |
|
79 |
#' observeEvent( |
|
80 |
#' input$button1_date, |
|
81 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = FALSE)) |
|
82 |
#' ) |
|
83 |
#' observeEvent( |
|
84 |
#' input$button2_date, |
|
85 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = TRUE)) |
|
86 |
#' ) |
|
87 |
#' observeEvent( |
|
88 |
#' input$button3_date, |
|
89 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", selected = data_date[c(34, 56)])) |
|
90 |
#' ) |
|
91 |
#' observeEvent( |
|
92 |
#' input$button4_date, |
|
93 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", selected = dates)) |
|
94 |
#' ) |
|
95 |
#' observeEvent( |
|
96 |
#' input$button0_date, |
|
97 |
#' fs$set_state( |
|
98 |
#' teal_slice("data", "variable", selected = data_date[c(47, 98)], keep_na = TRUE) |
|
99 |
#' ) |
|
100 |
#' ) |
|
101 |
#' } |
|
102 |
#' |
|
103 |
#' if (interactive()) { |
|
104 |
#' shinyApp(ui, server) |
|
105 |
#' } |
|
106 |
#' |
|
107 |
#' @keywords internal |
|
108 |
#' |
|
109 |
DateFilterState <- R6::R6Class( # nolint |
|
110 |
"DateFilterState", |
|
111 |
inherit = FilterState, |
|
112 | ||
113 |
# public methods ---- |
|
114 | ||
115 |
public = list( |
|
116 | ||
117 |
#' @description |
|
118 |
#' Initialize a `FilterState` object. |
|
119 |
#' |
|
120 |
#' @param x (`Date`) |
|
121 |
#' variable to be filtered. |
|
122 |
#' @param x_reactive (`reactive`) |
|
123 |
#' returning vector of the same type as `x`. Is used to update |
|
124 |
#' counts following the change in values of the filtered dataset. |
|
125 |
#' If it is set to `reactive(NULL)` then counts based on filtered |
|
126 |
#' dataset are not shown. |
|
127 |
#' @param slice (`teal_slice`) |
|
128 |
#' specification of this filter state. |
|
129 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`. |
|
130 |
#' `get_state` returns `teal_slice` object which can be reused in other places. |
|
131 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e. |
|
132 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`. |
|
133 |
#' @param extract_type (`character`) |
|
134 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values: |
|
135 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed |
|
136 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>` |
|
137 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]` |
|
138 |
#' |
|
139 |
#' @return Object of class `DateFilterState`, invisibly. |
|
140 |
#' |
|
141 |
initialize = function(x, |
|
142 |
x_reactive = reactive(NULL), |
|
143 |
slice, |
|
144 |
extract_type = character(0)) { |
|
145 | 24x |
isolate({ |
146 | 24x |
checkmate::assert_date(x) |
147 | 23x |
checkmate::assert_class(x_reactive, "reactive") |
148 | ||
149 | 23x |
super$initialize( |
150 | 23x |
x = x, |
151 | 23x |
x_reactive = x_reactive, |
152 | 23x |
slice = slice, |
153 | 23x |
extract_type = extract_type |
154 |
) |
|
155 | 23x |
checkmate::assert_date(slice$choices, null.ok = TRUE) |
156 | 22x |
private$set_choices(slice$choices) |
157 | 14x |
if (is.null(slice$selected)) slice$selected <- slice$choices |
158 | 22x |
private$set_selected(slice$selected) |
159 |
}) |
|
160 | ||
161 | 21x |
invisible(self) |
162 |
}, |
|
163 | ||
164 |
#' @description |
|
165 |
#' Returns reproducible condition call for current selection. |
|
166 |
#' For this class returned call looks like |
|
167 |
#' `<varname> >= <min value> & <varname> <= <max value>` with optional `is.na(<varname>)`. |
|
168 |
#' @param dataname (`character(1)`) containing possibly prefixed name of data set |
|
169 |
#' @return `call` or `NULL` |
|
170 |
#' |
|
171 |
get_call = function(dataname) { |
|
172 | 7x |
if (isFALSE(private$is_any_filtered())) { |
173 | 1x |
return(NULL) |
174 |
} |
|
175 | 6x |
choices <- as.character(private$get_selected()) |
176 | 6x |
varname <- private$get_varname_prefixed(dataname) |
177 | 6x |
filter_call <- |
178 | 6x |
call( |
179 |
"&", |
|
180 | 6x |
call(">=", varname, call("as.Date", choices[1L])), |
181 | 6x |
call("<=", varname, call("as.Date", choices[2L])) |
182 |
) |
|
183 | 6x |
private$add_keep_na_call(filter_call, varname) |
184 |
} |
|
185 |
), |
|
186 | ||
187 |
# private methods ---- |
|
188 | ||
189 |
private = list( |
|
190 |
set_choices = function(choices) { |
|
191 | 22x |
if (is.null(choices)) { |
192 | 19x |
choices <- range(private$x, na.rm = TRUE) |
193 |
} else { |
|
194 | 3x |
choices_adjusted <- c( |
195 | 3x |
max(choices[1L], min(private$x, na.rm = TRUE)), |
196 | 3x |
min(choices[2L], max(private$x, na.rm = TRUE)) |
197 |
) |
|
198 | 3x |
if (any(choices != choices_adjusted)) { |
199 | 1x |
warning(sprintf( |
200 | 1x |
"Choices adjusted (some values outside of variable range). Varname: %s, dataname: %s.", |
201 | 1x |
private$get_varname(), private$get_dataname() |
202 |
)) |
|
203 | 1x |
choices <- choices_adjusted |
204 |
} |
|
205 | 3x |
if (choices[1L] >= choices[2L]) { |
206 | 1x |
warning(sprintf( |
207 | 1x |
"Invalid choices: lower is higher / equal to upper, or not in range of variable values. |
208 | 1x |
Setting defaults. Varname: %s, dataname: %s.", |
209 | 1x |
private$get_varname(), private$get_dataname() |
210 |
)) |
|
211 | 1x |
choices <- range(private$x, na.rm = TRUE) |
212 |
} |
|
213 |
} |
|
214 | 22x |
private$set_is_choice_limited(private$x, choices) |
215 | 22x |
private$x <- private$x[(private$x >= choices[1L] & private$x <= choices[2L]) | is.na(private$x)] |
216 | 22x |
private$teal_slice$choices <- choices |
217 | 22x |
invisible(NULL) |
218 |
}, |
|
219 | ||
220 |
# @description |
|
221 |
# Check whether the initial choices filter out some values of x and set the flag in case. |
|
222 |
set_is_choice_limited = function(xl, choices) { |
|
223 | 22x |
private$is_choice_limited <- (any(xl < choices[1L], na.rm = TRUE) | any(xl > choices[2L], na.rm = TRUE)) |
224 | 22x |
invisible(NULL) |
225 |
}, |
|
226 |
cast_and_validate = function(values) { |
|
227 | 33x |
tryCatch( |
228 | 33x |
expr = { |
229 | 33x |
values <- as.Date(values, origin = "1970-01-01") |
230 | ! |
if (anyNA(values)) stop() |
231 | 30x |
values |
232 |
}, |
|
233 | 33x |
error = function(e) stop("Vector of set values must contain values coercible to Date.") |
234 |
) |
|
235 |
}, |
|
236 |
check_length = function(values) { |
|
237 | 1x |
if (length(values) != 2) stop("Vector of set values must have length two.") |
238 | 29x |
if (values[1] > values[2]) { |
239 | 1x |
warning( |
240 | 1x |
sprintf( |
241 | 1x |
"Start date %s is set after the end date %s, the values will be replaced with a default date range.", |
242 | 1x |
values[1], values[2] |
243 |
) |
|
244 |
) |
|
245 | 1x |
values <- isolate(private$get_choices()) |
246 |
} |
|
247 | 29x |
values |
248 |
}, |
|
249 |
remove_out_of_bounds_values = function(values) { |
|
250 | 29x |
choices <- private$get_choices() |
251 | 29x |
if (values[1] < choices[1L] | values[1] > choices[2L]) { |
252 | 5x |
warning( |
253 | 5x |
sprintf( |
254 | 5x |
"Value: %s is outside of the possible range for column %s of dataset %s, setting minimum possible value.", |
255 | 5x |
values[1], private$get_varname(), private$get_dataname() |
256 |
) |
|
257 |
) |
|
258 | 5x |
values[1] <- choices[1L] |
259 |
} |
|
260 | ||
261 | 29x |
if (values[2] > choices[2L] | values[2] < choices[1L]) { |
262 | 5x |
warning( |
263 | 5x |
sprintf( |
264 | 5x |
"Value: %s is outside of the possible range for column %s of dataset %s, setting maximum possible value.", |
265 | 5x |
values[2], private$get_varname(), private$get_dataname() |
266 |
) |
|
267 |
) |
|
268 | 5x |
values[2] <- choices[2L] |
269 |
} |
|
270 | ||
271 | 29x |
values |
272 |
}, |
|
273 | ||
274 |
# shiny modules ---- |
|
275 | ||
276 |
# @description |
|
277 |
# UI Module for `DateFilterState`. |
|
278 |
# This UI element contains two date selections for `min` and `max` |
|
279 |
# of the range and a checkbox whether to keep the `NA` values. |
|
280 |
# @param id (`character(1)`) `shiny` module instance id. |
|
281 |
ui_inputs = function(id) { |
|
282 | ! |
ns <- NS(id) |
283 | ! |
isolate({ |
284 | ! |
tags$div( |
285 | ! |
tags$div( |
286 | ! |
class = "flex", |
287 | ! |
actionButton( |
288 | ! |
class = "date_reset_button", |
289 | ! |
inputId = ns("start_date_reset"), |
290 | ! |
label = NULL, |
291 | ! |
icon = icon("fas fa-undo") |
292 |
), |
|
293 | ! |
tags$div( |
294 | ! |
class = "w-80 filter_datelike_input", |
295 | ! |
dateRangeInput( |
296 | ! |
inputId = ns("selection"), |
297 | ! |
label = NULL, |
298 | ! |
start = private$get_selected()[1], |
299 | ! |
end = private$get_selected()[2], |
300 | ! |
min = private$get_choices()[1L], |
301 | ! |
max = private$get_choices()[2L], |
302 | ! |
width = "100%" |
303 |
) |
|
304 |
), |
|
305 | ! |
actionButton( |
306 | ! |
class = "date_reset_button", |
307 | ! |
inputId = ns("end_date_reset"), |
308 | ! |
label = NULL, |
309 | ! |
icon = icon("fas fa-undo") |
310 |
) |
|
311 |
), |
|
312 | ! |
private$keep_na_ui(ns("keep_na")) |
313 |
) |
|
314 |
}) |
|
315 |
}, |
|
316 | ||
317 |
# @description |
|
318 |
# Server module |
|
319 |
# @param id (`character(1)`) `shiny` module instance id. |
|
320 |
# @return `NULL`. |
|
321 |
server_inputs = function(id) { |
|
322 | ! |
moduleServer( |
323 | ! |
id = id, |
324 | ! |
function(input, output, session) { |
325 | ! |
logger::log_debug("DateFilterState$server initializing, id: { private$get_id() }") |
326 | ||
327 |
# this observer is needed in the situation when teal_slice$selected has been |
|
328 |
# changed directly by the api - then it's needed to rerender UI element |
|
329 |
# to show relevant values |
|
330 | ! |
private$session_bindings[[session$ns("selection_api")]] <- observeEvent( |
331 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
332 | ! |
ignoreInit = TRUE, |
333 | ! |
eventExpr = private$get_selected(), |
334 | ! |
handlerExpr = { |
335 | ! |
if (!setequal(private$get_selected(), input$selection)) { |
336 | ! |
logger::log_debug("DateFilterState$server@1 state changed, id: { private$get_id() }") |
337 | ! |
updateDateRangeInput( |
338 | ! |
session = session, |
339 | ! |
inputId = "selection", |
340 | ! |
start = private$get_selected()[1], |
341 | ! |
end = private$get_selected()[2] |
342 |
) |
|
343 |
} |
|
344 |
} |
|
345 |
) |
|
346 | ||
347 | ! |
private$session_bindings[[session$ns("selection")]] <- observeEvent( |
348 | ! |
ignoreNULL = TRUE, # dates needs to be selected |
349 | ! |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
350 | ! |
eventExpr = input$selection, |
351 | ! |
handlerExpr = { |
352 | ! |
logger::log_debug("DateFilterState$server@2 selection changed, id: { private$get_id() }") |
353 | ! |
start_date <- input$selection[1] |
354 | ! |
end_date <- input$selection[2] |
355 | ||
356 | ! |
if (is.na(start_date) || is.na(end_date) || start_date > end_date) { |
357 | ! |
updateDateRangeInput( |
358 | ! |
session = session, |
359 | ! |
inputId = "selection", |
360 | ! |
start = private$get_selected()[1], |
361 | ! |
end = private$get_selected()[2] |
362 |
) |
|
363 | ! |
showNotification( |
364 | ! |
"Start date must not be greater than the end date. Setting back to previous value.", |
365 | ! |
type = "warning" |
366 |
) |
|
367 | ! |
return(NULL) |
368 |
} |
|
369 | ||
370 | ! |
private$set_selected(c(start_date, end_date)) |
371 |
} |
|
372 |
) |
|
373 | ||
374 | ||
375 | ! |
private$keep_na_srv("keep_na") |
376 | ||
377 | ! |
private$session_bindings[[session$ns("reset1")]] <- observeEvent(input$start_date_reset, { |
378 | ! |
logger::log_debug("DateFilterState$server@3 reset start date, id: { private$get_id() }") |
379 | ! |
updateDateRangeInput( |
380 | ! |
session = session, |
381 | ! |
inputId = "selection", |
382 | ! |
start = private$get_choices()[1L] |
383 |
) |
|
384 |
}) |
|
385 | ||
386 | ! |
private$session_bindings[[session$ns("reset2")]] <- observeEvent(input$end_date_reset, { |
387 | ! |
logger::log_debug("DateFilterState$server@4 reset end date, id: { private$get_id() }") |
388 | ! |
updateDateRangeInput( |
389 | ! |
session = session, |
390 | ! |
inputId = "selection", |
391 | ! |
end = private$get_choices()[2L] |
392 |
) |
|
393 |
}) |
|
394 | ||
395 | ! |
NULL |
396 |
} |
|
397 |
) |
|
398 |
}, |
|
399 |
server_inputs_fixed = function(id) { |
|
400 | ! |
moduleServer( |
401 | ! |
id = id, |
402 | ! |
function(input, output, session) { |
403 | ! |
logger::log_debug("DateFilterState$server initializing, id: { private$get_id() }") |
404 | ||
405 | ! |
output$selection <- renderUI({ |
406 | ! |
vals <- format(private$get_selected(), nsmall = 3) |
407 | ! |
tags$div( |
408 | ! |
tags$div(icon("calendar-days"), vals[1]), |
409 | ! |
tags$div(span(" - "), icon("calendar-days"), vals[2]) |
410 |
) |
|
411 |
}) |
|
412 | ||
413 | ! |
NULL |
414 |
} |
|
415 |
) |
|
416 |
}, |
|
417 | ||
418 |
# @description |
|
419 |
# Server module to display filter summary |
|
420 |
# renders text describing selected date range and |
|
421 |
# if NA are included also |
|
422 |
content_summary = function(id) { |
|
423 | ! |
selected <- as.character(private$get_selected()) |
424 | ! |
min <- selected[1] |
425 | ! |
max <- selected[2] |
426 | ! |
tagList( |
427 | ! |
tags$span( |
428 | ! |
class = "filter-card-summary-value", |
429 | ! |
HTML(min, "–", max) |
430 |
), |
|
431 | ! |
tags$span( |
432 | ! |
class = "filter-card-summary-controls", |
433 | ! |
if (private$na_count > 0) { |
434 | ! |
tags$span("NA", if (isTRUE(private$get_keep_na())) icon("check") else icon("xmark")) |
435 |
} |
|
436 |
) |
|
437 |
) |
|
438 |
} |
|
439 |
) |
|
440 |
) |
1 |
#' Managing `FilteredData` states |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("experimental")` |
|
4 |
#' |
|
5 |
#' Set, get and remove filter states of `FilteredData` object. |
|
6 |
#' |
|
7 |
#' @name filter_state_api |
|
8 |
#' |
|
9 |
#' @param datasets (`FilteredData`) |
|
10 |
#' object to store filter state and filtered datasets, shared across modules |
|
11 |
#' |
|
12 |
#' see [`FilteredData`] for details |
|
13 |
#' |
|
14 |
#' @param filter (`teal_slices`) |
|
15 |
#' specify filters in place on app start-up |
|
16 |
#' |
|
17 |
#' @param force (`logical(1)`) |
|
18 |
#' flag specifying whether to include anchored filter states. |
|
19 |
#' |
|
20 |
#' @return |
|
21 |
#' - `set_*`, `remove_*` and `clear_filter_state` return `NULL` invisibly |
|
22 |
#' - `get_filter_state` returns a named `teal_slices` object |
|
23 |
#' containing a `teal_slice` for every existing `FilterState` |
|
24 |
#' |
|
25 |
#' @seealso [`teal_slice`] |
|
26 |
#' |
|
27 |
#' @examples |
|
28 |
#' datasets <- init_filtered_data(list(iris = iris, mtcars = mtcars)) |
|
29 |
#' fs <- teal_slices( |
|
30 |
#' teal_slice(dataname = "iris", varname = "Species", selected = c("setosa", "versicolor")), |
|
31 |
#' teal_slice(dataname = "iris", varname = "Sepal.Length", selected = c(5.1, 6.4)), |
|
32 |
#' teal_slice(dataname = "mtcars", varname = "gear", selected = c(4, 5)), |
|
33 |
#' teal_slice(dataname = "mtcars", varname = "carb", selected = c(4, 10)) |
|
34 |
#' ) |
|
35 |
#' |
|
36 |
#' # set initial filter state |
|
37 |
#' set_filter_state(datasets, filter = fs) |
|
38 |
#' |
|
39 |
#' # get filter state |
|
40 |
#' get_filter_state(datasets) |
|
41 |
#' |
|
42 |
#' # modify filter state |
|
43 |
#' set_filter_state( |
|
44 |
#' datasets, |
|
45 |
#' teal_slices( |
|
46 |
#' teal_slice(dataname = "iris", varname = "Species", selected = "setosa", keep_na = TRUE) |
|
47 |
#' ) |
|
48 |
#' ) |
|
49 |
#' |
|
50 |
#' # remove specific filters |
|
51 |
#' remove_filter_state( |
|
52 |
#' datasets, |
|
53 |
#' teal_slices( |
|
54 |
#' teal_slice(dataname = "iris", varname = "Species"), |
|
55 |
#' teal_slice(dataname = "mtcars", varname = "gear"), |
|
56 |
#' teal_slice(dataname = "mtcars", varname = "carb") |
|
57 |
#' ) |
|
58 |
#' ) |
|
59 |
#' |
|
60 |
#' # remove all states |
|
61 |
#' clear_filter_states(datasets) |
|
62 |
#' |
|
63 |
#' @examples |
|
64 |
#' \donttest{ |
|
65 |
#' if (requireNamespace("MultiAssayExperiment", quietly = TRUE)) { |
|
66 |
#' # Requires MultiAssayExperiment from Bioconductor |
|
67 |
#' data(miniACC, package = "MultiAssayExperiment") |
|
68 |
#' |
|
69 |
#' datasets <- init_filtered_data(list(mae = miniACC)) |
|
70 |
#' fs <- teal_slices( |
|
71 |
#' teal_slice( |
|
72 |
#' dataname = "mae", varname = "years_to_birth", selected = c(30, 50), |
|
73 |
#' keep_na = TRUE, keep_inf = FALSE |
|
74 |
#' ), |
|
75 |
#' teal_slice( |
|
76 |
#' dataname = "mae", varname = "vital_status", selected = "1", |
|
77 |
#' keep_na = FALSE |
|
78 |
#' ), |
|
79 |
#' teal_slice( |
|
80 |
#' dataname = "mae", varname = "gender", selected = "female", |
|
81 |
#' keep_na = TRUE |
|
82 |
#' ), |
|
83 |
#' teal_slice( |
|
84 |
#' dataname = "mae", varname = "ARRAY_TYPE", selected = "", |
|
85 |
#' keep_na = TRUE, experiment = "RPPAArray", arg = "subset" |
|
86 |
#' ) |
|
87 |
#' ) |
|
88 |
#' |
|
89 |
#' # set initial filter state |
|
90 |
#' set_filter_state(datasets, filter = fs) |
|
91 |
#' |
|
92 |
#' # get filter state |
|
93 |
#' get_filter_state(datasets) |
|
94 |
#' |
|
95 |
#' # modify filter state |
|
96 |
#' set_filter_state( |
|
97 |
#' datasets, |
|
98 |
#' teal_slices( |
|
99 |
#' teal_slice(dataname = "mae", varname = "years_to_birth", selected = c(40, 60)) |
|
100 |
#' ) |
|
101 |
#' ) |
|
102 |
#' |
|
103 |
#' # remove specific filters |
|
104 |
#' remove_filter_state( |
|
105 |
#' datasets, |
|
106 |
#' teal_slices( |
|
107 |
#' teal_slice(dataname = "mae", varname = "years_to_birth"), |
|
108 |
#' teal_slice(dataname = "mae", varname = "vital_status") |
|
109 |
#' ) |
|
110 |
#' ) |
|
111 |
#' |
|
112 |
#' # remove all states |
|
113 |
#' clear_filter_states(datasets) |
|
114 |
#' } |
|
115 |
#' } |
|
116 |
NULL |
|
117 | ||
118 |
#' @rdname filter_state_api |
|
119 |
#' @export |
|
120 |
set_filter_state <- function(datasets, filter) { |
|
121 | 3x |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
122 | 3x |
checkmate::assert_class(filter, "teal_slices") |
123 | 3x |
datasets$set_filter_state(filter) |
124 | 3x |
invisible(NULL) |
125 |
} |
|
126 | ||
127 |
#' @rdname filter_state_api |
|
128 |
#' @export |
|
129 |
get_filter_state <- function(datasets) { |
|
130 | 4x |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
131 | 4x |
if (isRunning()) { |
132 | ! |
datasets$get_filter_state() |
133 |
} else { |
|
134 | 4x |
isolate(datasets$get_filter_state()) |
135 |
} |
|
136 |
} |
|
137 | ||
138 |
#' @rdname filter_state_api |
|
139 |
#' @export |
|
140 |
remove_filter_state <- function(datasets, filter) { |
|
141 | 1x |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
142 | 1x |
checkmate::assert_class(filter, "teal_slices") |
143 | ||
144 | 1x |
datasets$remove_filter_state(filter) |
145 | 1x |
invisible(NULL) |
146 |
} |
|
147 | ||
148 |
#' @rdname filter_state_api |
|
149 |
#' @export |
|
150 |
clear_filter_states <- function(datasets, force = FALSE) { |
|
151 | 1x |
checkmate::assert_multi_class(datasets, c("FilteredData", "FilterPanelAPI")) |
152 | 1x |
datasets$clear_filter_states(force = force) |
153 | 1x |
invisible(NULL) |
154 |
} |
|
155 | ||
156 |
#' Gets filter expression for multiple `datanames` taking into account its order. |
|
157 |
#' |
|
158 |
#' @description `r lifecycle::badge("stable")` |
|
159 |
#' |
|
160 |
#' To be used in `Show R Code` button. |
|
161 |
#' |
|
162 |
#' @param datasets (`FilteredData`) |
|
163 |
#' @param datanames (`character`) vector of dataset names |
|
164 |
#' |
|
165 |
#' @return A character string containing all subset expressions. |
|
166 |
#' |
|
167 |
#' @export |
|
168 |
#' |
|
169 |
get_filter_expr <- function(datasets, datanames = datasets$datanames()) { |
|
170 | 2x |
checkmate::check_class(datasets, "FilteredData") |
171 | 2x |
checkmate::assert_character(datanames, any.missing = FALSE) |
172 | 2x |
checkmate::assert_subset(datanames, datasets$datanames()) |
173 | 2x |
paste( |
174 | 2x |
unlist(lapply( |
175 | 2x |
datanames, |
176 | 2x |
function(dataname) { |
177 | 4x |
datasets$get_call(dataname) |
178 |
} |
|
179 |
)), |
|
180 | 2x |
collapse = "\n" |
181 |
) |
|
182 |
} |
1 |
#' Include `JS` files from `/inst/js/` package directory to application header |
|
2 |
#' |
|
3 |
#' `system.file` should not be used to access files in other packages, it does |
|
4 |
#' not work with `devtools`. Therefore, we redefine this method in each package |
|
5 |
#' as needed. Thus, we do not export this method. |
|
6 |
#' |
|
7 |
#' @param pattern (`character`) pattern of files to be included, passed to `system.file` |
|
8 |
#' |
|
9 |
#' @return HTML code that includes `JS` files |
|
10 |
#' @keywords internal |
|
11 |
include_js_files <- function(pattern) { |
|
12 | 12x |
checkmate::assert_character(pattern, min.len = 1, null.ok = TRUE) |
13 | 12x |
js_files <- list.files( |
14 | 12x |
system.file("js", package = "teal.slice", mustWork = TRUE), |
15 | 12x |
pattern = pattern, |
16 | 12x |
full.names = TRUE |
17 |
) |
|
18 | 12x |
singleton(lapply(js_files, includeScript)) |
19 |
} |
|
20 | ||
21 |
#' Build concatenating call |
|
22 |
#' |
|
23 |
#' This function takes a vector of values and returns a `c` call. If the vector |
|
24 |
#' has only one element, the element is returned directly. |
|
25 |
#' |
|
26 |
#' @param choices A vector of values. |
|
27 |
#' |
|
28 |
#' @return A `c` call. |
|
29 |
#' |
|
30 |
#' @examples |
|
31 |
#' # use non-exported function from teal.slice |
|
32 |
#' make_c_call <- getFromNamespace("make_c_call", "teal.slice") |
|
33 |
#' make_c_call(1:3) |
|
34 |
#' make_c_call(1) |
|
35 |
#' |
|
36 |
#' @keywords internal |
|
37 |
make_c_call <- function(choices) { |
|
38 | 55x |
if (length(choices) > 1) { |
39 | 27x |
do.call("call", append(list("c"), choices)) |
40 |
} else { |
|
41 | 28x |
choices |
42 |
} |
|
43 |
} |
|
44 | ||
45 |
#' Destroys inputs and observers stored in `private$session_bindings` |
|
46 |
#' |
|
47 |
#' @description |
|
48 |
#' Call a `destroy` method to remove `observer` and `input` from obsolete session which happens |
|
49 |
#' when `filter_panel_srv` is called again in new `FilteredData` object. |
|
50 |
#' Inputs are not stored directly in a field as they don't have `destroy` method. Instead, we |
|
51 |
#' store callback `destroy` function for inputs which removes bindings from a `session`. |
|
52 |
#' @param self,private slots of a `R6` class |
|
53 |
#' @return `NULL` invisibly |
|
54 |
#' @keywords internal |
|
55 |
.finalize_session_bindings <- function(self, private) { |
|
56 |
# Only finalize shiny session binding when there is an active session |
|
57 |
if ( |
|
58 | 1320x |
!is.null(getDefaultReactiveDomain()) && |
59 | 1320x |
!getDefaultReactiveDomain()$isEnded() |
60 |
) { |
|
61 | 286x |
lapply(private$session_bindings, function(x) x$destroy()) |
62 |
} |
|
63 | 1320x |
invisible(NULL) |
64 |
} |
|
65 | ||
66 | ||
67 | ||
68 |
#' Encodes ids to be used in JavaScript and Shiny |
|
69 |
#' |
|
70 |
#' @description |
|
71 |
#' Replaces non-ASCII characters into a format that can be used in HTML, |
|
72 |
#' JavaScript and Shiny. |
|
73 |
#' |
|
74 |
#' When the id has a character that is not allowed, it is replaced with `"_"` |
|
75 |
#' and a 4 character hash of the original id is added to the start of the |
|
76 |
#' resulting id. |
|
77 |
#' |
|
78 |
#' |
|
79 |
#' @param id (`character(1)`) The id string. |
|
80 |
#' |
|
81 |
#' @return Sanitized string that removes special characters and spaces. |
|
82 |
#' |
|
83 |
#' @keywords internal |
|
84 |
sanitize_id <- function(id) { |
|
85 | 392x |
pattern_escape <- "[^0-9A-Za-z_]" |
86 | ||
87 | 392x |
id_new <- gsub(pattern_escape, "_", id, perl = TRUE) |
88 | 392x |
hashes <- vapply(id[id != id_new], rlang::hash, character(1), USE.NAMES = FALSE) |
89 | ||
90 | 392x |
id[id != id_new] <- paste0("h", substr(hashes, 1, 4), "_", id_new[id != id_new]) |
91 | 392x |
id |
92 |
} |
|
93 | ||
94 |
#' `NS` wrapper to sanitize ids for shiny |
|
95 |
#' |
|
96 |
#' Special characters and spaces are not allowed in shiny ids (in JS) |
|
97 |
#' |
|
98 |
#' @noRd |
|
99 |
NS <- function(namespace, id = NULL) { # nolint: object_name. |
|
100 | 148x |
if (!missing(id)) { |
101 | 3x |
return(shiny::NS(namespace, sanitize_id(id))) |
102 |
} |
|
103 | ||
104 | 145x |
function(id) { |
105 | 282x |
shiny::NS(namespace, sanitize_id(id)) |
106 |
} |
|
107 |
} |
|
108 | ||
109 |
#' `moduleServer` wrapper to sanitize ids for shiny |
|
110 |
#' |
|
111 |
#' Special characters and spaces are not allowed in shiny ids (in JS) |
|
112 |
#' |
|
113 |
#' @noRd |
|
114 |
moduleServer <- function(id, module, session = getDefaultReactiveDomain()) { # nolint: object_name. |
|
115 | 107x |
id <- sanitize_id(id) |
116 | 107x |
shiny::moduleServer(id, module, session) |
117 |
} |
1 |
#' Initialize `FilterState` |
|
2 |
#' |
|
3 |
#' Initializes a `FilterState` object corresponding to the class of the filtered variable. |
|
4 |
#' |
|
5 |
#' @param x (`vector`) |
|
6 |
#' variable to be filtered. |
|
7 |
#' @param x_reactive (`reactive`) |
|
8 |
#' returning vector of the same type as `x`. Is used to update |
|
9 |
#' counts following the change in values of the filtered dataset. |
|
10 |
#' If it is set to `reactive(NULL)` then counts based on filtered |
|
11 |
#' dataset are not shown. |
|
12 |
#' @param slice (`teal_slice`) |
|
13 |
#' specification of this filter state. |
|
14 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`. |
|
15 |
#' `get_state` returns `teal_slice` object which can be reused in other places. |
|
16 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e. |
|
17 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`. |
|
18 |
#' @param extract_type (`character`) |
|
19 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values: |
|
20 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed |
|
21 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>` |
|
22 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]` |
|
23 |
#' |
|
24 |
#' @examples |
|
25 |
#' # use non-exported function from teal.slice |
|
26 |
#' init_filter_state <- getFromNamespace("init_filter_state", "teal.slice") |
|
27 |
#' |
|
28 |
#' library(shiny) |
|
29 |
#' |
|
30 |
#' filter_state <- init_filter_state( |
|
31 |
#' x = c(1:10, NA, Inf), |
|
32 |
#' x_reactive = reactive(c(1:10, NA, Inf)), |
|
33 |
#' slice = teal_slice( |
|
34 |
#' varname = "varname", |
|
35 |
#' dataname = "dataname" |
|
36 |
#' ), |
|
37 |
#' extract_type = "matrix" |
|
38 |
#' ) |
|
39 |
#' |
|
40 |
#' isolate(filter_state$get_call()) |
|
41 |
#' |
|
42 |
#' # working filter in an app |
|
43 |
#' |
|
44 |
#' ui <- fluidPage( |
|
45 |
#' filter_state$ui(id = "app"), |
|
46 |
#' verbatimTextOutput("call") |
|
47 |
#' ) |
|
48 |
#' server <- function(input, output, session) { |
|
49 |
#' filter_state$server("app") |
|
50 |
#' |
|
51 |
#' output$call <- renderText( |
|
52 |
#' deparse1(filter_state$get_call(), collapse = "\n") |
|
53 |
#' ) |
|
54 |
#' } |
|
55 |
#' |
|
56 |
#' if (interactive()) { |
|
57 |
#' shinyApp(ui, server) |
|
58 |
#' } |
|
59 |
#' |
|
60 |
#' @return `FilterState` object |
|
61 |
#' @keywords internal |
|
62 |
init_filter_state <- function(x, |
|
63 |
x_reactive = reactive(NULL), |
|
64 |
slice, |
|
65 |
extract_type = character(0)) { |
|
66 | 187x |
checkmate::assert_class(x_reactive, "reactive") |
67 | 186x |
checkmate::assert_character(extract_type, max.len = 1, any.missing = FALSE) |
68 | 186x |
checkmate::assert_class(slice, "teal_slice") |
69 | 185x |
if (length(extract_type) == 1) { |
70 | 40x |
checkmate::assert_choice(extract_type, choices = c("list", "matrix")) |
71 |
} |
|
72 | ||
73 | 184x |
if (all(is.na(x))) { |
74 | 1x |
EmptyFilterState$new( |
75 | 1x |
x = x, |
76 | 1x |
x_reactive = x_reactive, |
77 | 1x |
slice = slice, |
78 | 1x |
extract_type = extract_type |
79 |
) |
|
80 |
} else { |
|
81 | 183x |
UseMethod("init_filter_state") |
82 |
} |
|
83 |
} |
|
84 | ||
85 |
#' @keywords internal |
|
86 |
#' @export |
|
87 |
init_filter_state.default <- function(x, |
|
88 |
x_reactive = reactive(NULL), |
|
89 |
slice, |
|
90 |
extract_type = character(0)) { |
|
91 | 1x |
args <- list( |
92 | 1x |
x = x, |
93 | 1x |
x_reactive = x_reactive, |
94 | 1x |
extract_type = extract_type, |
95 | 1x |
slice |
96 |
) |
|
97 | ||
98 | 1x |
do.call(FilterState$new, args) |
99 |
} |
|
100 | ||
101 |
#' @keywords internal |
|
102 |
#' @export |
|
103 |
init_filter_state.logical <- function(x, |
|
104 |
x_reactive = reactive(NULL), |
|
105 |
slice, |
|
106 |
extract_type = character(0)) { |
|
107 | 1x |
LogicalFilterState$new( |
108 | 1x |
x = x, |
109 | 1x |
x_reactive = x_reactive, |
110 | 1x |
slice = slice, |
111 | 1x |
extract_type = extract_type |
112 |
) |
|
113 |
} |
|
114 | ||
115 |
#' @keywords internal |
|
116 |
#' @export |
|
117 |
init_filter_state.numeric <- function(x, |
|
118 |
x_reactive = reactive(NULL), |
|
119 |
slice, |
|
120 |
extract_type = character(0)) { |
|
121 | 115x |
args <- list( |
122 | 115x |
x = x, |
123 | 115x |
x_reactive = x_reactive, |
124 | 115x |
slice = slice, |
125 | 115x |
extract_type = extract_type |
126 |
) |
|
127 | ||
128 | 115x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
129 | 26x |
do.call(ChoicesFilterState$new, args) |
130 |
} else { |
|
131 | 89x |
do.call(RangeFilterState$new, args) |
132 |
} |
|
133 |
} |
|
134 | ||
135 |
#' @keywords internal |
|
136 |
#' @export |
|
137 |
init_filter_state.factor <- function(x, |
|
138 |
x_reactive = reactive(NULL), |
|
139 |
slice, |
|
140 |
extract_type = character(0)) { |
|
141 | 30x |
ChoicesFilterState$new( |
142 | 30x |
x = x, |
143 | 30x |
x_reactive = x_reactive, |
144 | 30x |
slice = slice, |
145 | 30x |
extract_type = extract_type |
146 |
) |
|
147 |
} |
|
148 | ||
149 |
#' @keywords internal |
|
150 |
#' @export |
|
151 |
init_filter_state.character <- function(x, |
|
152 |
x_reactive = reactive(NULL), |
|
153 |
slice, |
|
154 |
extract_type = character(0)) { |
|
155 | 30x |
ChoicesFilterState$new( |
156 | 30x |
x = x, |
157 | 30x |
x_reactive = x_reactive, |
158 | 30x |
slice = slice, |
159 | 30x |
extract_type = extract_type |
160 |
) |
|
161 |
} |
|
162 | ||
163 |
#' @keywords internal |
|
164 |
#' @export |
|
165 |
init_filter_state.Date <- function(x, |
|
166 |
x_reactive = reactive(NULL), |
|
167 |
slice, |
|
168 |
extract_type = character(0)) { |
|
169 | 2x |
args <- list( |
170 | 2x |
x = x, |
171 | 2x |
x_reactive = x_reactive, |
172 | 2x |
slice = slice, |
173 | 2x |
extract_type = extract_type |
174 |
) |
|
175 | ||
176 | 2x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
177 | 1x |
do.call(ChoicesFilterState$new, args) |
178 |
} else { |
|
179 | 1x |
do.call(DateFilterState$new, args) |
180 |
} |
|
181 |
} |
|
182 | ||
183 |
#' @keywords internal |
|
184 |
#' @export |
|
185 |
init_filter_state.POSIXct <- function(x, |
|
186 |
x_reactive = reactive(NULL), |
|
187 |
slice, |
|
188 |
extract_type = character(0)) { |
|
189 | 2x |
args <- list( |
190 | 2x |
x = x, |
191 | 2x |
x_reactive = x_reactive, |
192 | 2x |
slice = slice, |
193 | 2x |
extract_type = extract_type |
194 |
) |
|
195 | ||
196 | 2x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
197 | 1x |
do.call(ChoicesFilterState$new, args) |
198 |
} else { |
|
199 | 1x |
do.call(DatetimeFilterState$new, args) |
200 |
} |
|
201 |
} |
|
202 | ||
203 |
#' @keywords internal |
|
204 |
#' @export |
|
205 |
init_filter_state.POSIXlt <- function(x, |
|
206 |
x_reactive = reactive(NULL), |
|
207 |
slice, |
|
208 |
extract_type = character(0)) { |
|
209 | 2x |
args <- list( |
210 | 2x |
x = x, |
211 | 2x |
x_reactive = x_reactive, |
212 | 2x |
slice = slice, |
213 | 2x |
extract_type = extract_type |
214 |
) |
|
215 | ||
216 | 2x |
if (length(unique(x[!is.na(x)])) < getOption("teal.threshold_slider_vs_checkboxgroup")) { |
217 | 1x |
do.call(ChoicesFilterState$new, args) |
218 |
} else { |
|
219 | 1x |
do.call(DatetimeFilterState$new, args) |
220 |
} |
|
221 |
} |
|
222 | ||
223 | ||
224 |
#' Initialize a `FilterStateExpr` object |
|
225 |
#' |
|
226 |
#' @param slice (`teal_slice_expr`) |
|
227 |
#' specifying this filter state. |
|
228 |
#' |
|
229 |
#' @return `FilterStateExpr` object |
|
230 |
#' @keywords internal |
|
231 |
init_filter_state_expr <- function(slice) { |
|
232 | 6x |
FilterStateExpr$new(slice) |
233 |
} |
|
234 | ||
235 | ||
236 |
#' Get hex code of the current Bootstrap theme color. |
|
237 |
#' |
|
238 |
#' Determines the color specification for the currently active Bootstrap color theme and returns one queried color. |
|
239 |
#' |
|
240 |
#' @param color (`character(1)`) naming one of the available theme colors |
|
241 |
#' @param alpha either a `numeric(1)` or `character(1)` specifying transparency |
|
242 |
#' in the range of `0-1` or a hexadecimal value `00-ff`, respectively; |
|
243 |
#' set to NULL to omit adding the alpha channel |
|
244 |
#' |
|
245 |
#' @return Named `character(1)` containing a hexadecimal color representation. |
|
246 |
#' |
|
247 |
#' @examples |
|
248 |
#' fetch_bs_color <- getFromNamespace("fetch_bs_color", "teal.slice") |
|
249 |
#' fetch_bs_color("primary") |
|
250 |
#' fetch_bs_color("danger", 0.35) |
|
251 |
#' fetch_bs_color("danger", "80") |
|
252 |
#' |
|
253 |
#' @keywords internal |
|
254 |
#' |
|
255 |
fetch_bs_color <- function(color, alpha = NULL) { |
|
256 | 116x |
checkmate::assert_string(color) |
257 | 116x |
checkmate::assert( |
258 | 116x |
checkmate::check_number(alpha, lower = 0, upper = 1, null.ok = TRUE), |
259 | 116x |
checkmate::check_string(alpha, pattern = "[0-9a-f]{2}", null.ok = TRUE) |
260 |
) |
|
261 | ||
262 |
# locate file that describes the current theme |
|
263 | 116x |
sass_file <- if (utils::packageVersion("bslib") < as.package_version("0.5.1.9000")) { |
264 | ! |
bslib::bs_theme()[["layers"]][[2]][["defaults"]][[1]] |
265 |
} else { |
|
266 | 116x |
bslib::bs_theme()[["layers"]][[2]][["defaults"]][[1]][[1]] |
267 |
} |
|
268 | 116x |
sass_file <- attr(sass_file, "sass_file_path") |
269 | ||
270 |
# load scss file that encodes variables |
|
271 | 116x |
variables_file <- readLines(sass_file) |
272 |
# locate theme color variables |
|
273 | 116x |
ind <- grep("// scss-docs-(start|end) theme-color-variables", variables_file) |
274 | 116x |
color_definitions <- variables_file[(ind[1] + 1L):(ind[2] - 1L)] |
275 | ||
276 |
# extract colors names |
|
277 | 116x |
color_names <- sub("(\\$)(\\w.+)(:.+)", "\\2", color_definitions) |
278 | ||
279 |
# verify that an available color was requested |
|
280 | 116x |
checkmate::assert_choice(color, color_names) |
281 | ||
282 |
# extract color references |
|
283 | 116x |
color_references <- sub("(\\$)(\\w.+)(:\\s.+\\$)(\\w.+)(\\s.+)", "\\4", color_definitions) |
284 | ||
285 |
# translate references to color codes |
|
286 | 116x |
color_specification <- structure(color_references, names = color_names) |
287 | 116x |
color_specification <- vapply(color_specification, function(x) { |
288 | 928x |
line <- grep(sprintf("^\\$%s:\\s+#\\w{6}\\s+!default", x), variables_file, value = TRUE) |
289 | 928x |
code <- sub("(.+)(#\\w{6})(\\s+.+)", "\\2", line) |
290 | 928x |
code |
291 | 116x |
}, character(1L)) |
292 | ||
293 | 116x |
if (!is.null(alpha)) { |
294 | ! |
if (is.numeric(alpha)) alpha <- as.hexmode(ceiling(255 * alpha)) |
295 |
} |
|
296 | ||
297 | 116x |
paste0(color_specification[color], alpha) |
298 |
} |
1 |
#' Get classes of selected columns from dataset |
|
2 |
#' |
|
3 |
#' @param data (`data.frame` or `DataFrame` or `matrix`) Object in which to determine variable types. |
|
4 |
#' @param columns (`character`) Vector of columns in `data` for which to get types. |
|
5 |
#' Set to `NULL` to get types of all columns. |
|
6 |
#' |
|
7 |
#' @return Character vector of classes of `columns` from provided `data`. |
|
8 |
#' |
|
9 |
#' @examples |
|
10 |
#' # use non-exported function from teal.slice |
|
11 |
#' variable_types <- getFromNamespace("variable_types", "teal.slice") |
|
12 |
#' |
|
13 |
#' variable_types( |
|
14 |
#' data.frame( |
|
15 |
#' x = 1:3, y = factor(c("a", "b", "a")), z = c("h1", "h2", "h3"), |
|
16 |
#' stringsAsFactors = FALSE |
|
17 |
#' ), |
|
18 |
#' "x" |
|
19 |
#' ) |
|
20 |
#' |
|
21 |
#' variable_types( |
|
22 |
#' data.frame( |
|
23 |
#' x = 1:3, y = factor(c("a", "b", "a")), z = c("h1", "h2", "h3"), |
|
24 |
#' stringsAsFactors = FALSE |
|
25 |
#' ), |
|
26 |
#' c("x", "z") |
|
27 |
#' ) |
|
28 |
#' |
|
29 |
#' variable_types( |
|
30 |
#' data.frame( |
|
31 |
#' x = 1:3, y = factor(c("a", "b", "a")), z = c("h1", "h2", "h3"), |
|
32 |
#' stringsAsFactors = FALSE |
|
33 |
#' ) |
|
34 |
#' ) |
|
35 |
#' |
|
36 |
#' @keywords internal |
|
37 |
#' |
|
38 |
variable_types <- function(data, columns = NULL) { |
|
39 | 19x |
checkmate::assert_multi_class(data, c("data.frame", "DataFrame", "matrix")) |
40 | 19x |
checkmate::assert_character(columns, any.missing = FALSE, null.ok = TRUE) |
41 | 19x |
checkmate::assert_subset(columns, colnames(data)) |
42 | ||
43 | 19x |
if (is.matrix(data)) { |
44 | 1x |
type <- typeof(data) |
45 | 1x |
if (type == "double") type <- "numeric" |
46 | 1x |
types <- |
47 | 1x |
if (is.null(columns)) { |
48 | ! |
stats::setNames(rep_len(type, ncol(data)), nm = colnames(data)) |
49 |
} else { |
|
50 | 1x |
stats::setNames(rep_len(type, length(columns)), nm = columns) |
51 |
} |
|
52 |
} else { |
|
53 | 18x |
types <- vapply(data, function(x) class(x)[1L], character(1L)) |
54 | 18x |
if (!is.null(columns)) types <- types[columns] |
55 |
# alternative after R 4.4.0: `types <- types[columns %||% TRUE]` |
|
56 |
} |
|
57 | 19x |
types |
58 |
} |
1 |
# FilteredData ------ |
|
2 | ||
3 |
#' @name FilteredData |
|
4 |
#' @docType class |
|
5 |
#' |
|
6 |
#' @title Class to encapsulate filtered datasets |
|
7 |
#' |
|
8 |
#' @description |
|
9 |
#' Manages filtering of all datasets in the application or module. |
|
10 |
#' |
|
11 |
#' @details |
|
12 |
#' The main purpose of this class is to provide a collection of reactive datasets, |
|
13 |
#' each dataset having a filter state that determines how it is filtered. |
|
14 |
#' |
|
15 |
#' For each dataset, `get_filter_expr` returns the call to filter the dataset according |
|
16 |
#' to the filter state. The data itself can be obtained through `get_data`. |
|
17 |
#' |
|
18 |
#' The datasets are filtered lazily, i.e. only when requested / needed in a `shiny` app. |
|
19 |
#' |
|
20 |
#' By design, any `dataname` set through `set_dataset` cannot be removed because |
|
21 |
#' other code may already depend on it. As a workaround, the underlying |
|
22 |
#' data can be set to `NULL`. |
|
23 |
#' |
|
24 |
#' The class currently supports variables of the following types within datasets: |
|
25 |
#' - `choices`: variable of type `factor`, e.g. `ADSL$COUNTRY`, `iris$Species` |
|
26 |
#' zero or more options can be selected, when the variable is a factor |
|
27 |
#' - `logical`: variable of type `logical`, e.g. `ADSL$TRT_FLAG` |
|
28 |
#' exactly one option must be selected, `TRUE` or `FALSE` |
|
29 |
#' - `ranges`: variable of type `numeric`, e.g. `ADSL$AGE`, `iris$Sepal.Length` |
|
30 |
#' numerical range, a range within this range can be selected |
|
31 |
#' - `dates`: variable of type `Date`, `POSIXlt` |
|
32 |
#' Other variables cannot be used for filtering the data in this class. |
|
33 |
#' |
|
34 |
#' Common arguments are: |
|
35 |
#' 1. `filtered`: whether to return a filtered result or not |
|
36 |
#' 2. `dataname`: the name of one of the datasets in this `FilteredData` object |
|
37 |
#' 3. `varname`: one of the columns in a dataset |
|
38 |
#' |
|
39 |
#' @examples |
|
40 |
#' # use non-exported function from teal.slice |
|
41 |
#' FilteredData <- getFromNamespace("FilteredData", "teal.slice") |
|
42 |
#' |
|
43 |
#' library(shiny) |
|
44 |
#' |
|
45 |
#' datasets <- FilteredData$new(list(iris = iris, mtcars = mtcars)) |
|
46 |
#' |
|
47 |
#' # get datanames |
|
48 |
#' datasets$datanames() |
|
49 |
#' |
|
50 |
#' datasets$set_filter_state( |
|
51 |
#' teal_slices(teal_slice(dataname = "iris", varname = "Species", selected = "virginica")) |
|
52 |
#' ) |
|
53 |
#' |
|
54 |
#' datasets$set_filter_state( |
|
55 |
#' teal_slices(teal_slice(dataname = "mtcars", varname = "mpg", selected = c(15, 20))) |
|
56 |
#' ) |
|
57 |
#' |
|
58 |
#' isolate(datasets$get_filter_state()) |
|
59 |
#' isolate(datasets$get_call("iris")) |
|
60 |
#' isolate(datasets$get_call("mtcars")) |
|
61 |
#' |
|
62 |
#' @examplesIf requireNamespace("MultiAssayExperiment") |
|
63 |
#' ### set_filter_state |
|
64 |
#' library(shiny) |
|
65 |
#' |
|
66 |
#' data(miniACC, package = "MultiAssayExperiment") |
|
67 |
#' datasets <- FilteredData$new(list(iris = iris, mae = miniACC)) |
|
68 |
#' fs <- teal_slices( |
|
69 |
#' teal_slice( |
|
70 |
#' dataname = "iris", varname = "Sepal.Length", selected = c(5.1, 6.4), |
|
71 |
#' keep_na = TRUE, keep_inf = FALSE |
|
72 |
#' ), |
|
73 |
#' teal_slice( |
|
74 |
#' dataname = "iris", varname = "Species", selected = c("setosa", "versicolor"), |
|
75 |
#' keep_na = FALSE |
|
76 |
#' ), |
|
77 |
#' teal_slice( |
|
78 |
#' dataname = "mae", varname = "years_to_birth", selected = c(30, 50), |
|
79 |
#' keep_na = TRUE, keep_inf = FALSE |
|
80 |
#' ), |
|
81 |
#' teal_slice(dataname = "mae", varname = "vital_status", selected = "1", keep_na = FALSE), |
|
82 |
#' teal_slice(dataname = "mae", varname = "gender", selected = "female", keep_na = TRUE), |
|
83 |
#' teal_slice( |
|
84 |
#' dataname = "mae", varname = "ARRAY_TYPE", |
|
85 |
#' selected = "", keep_na = TRUE, experiment = "RPPAArray", arg = "subset" |
|
86 |
#' ) |
|
87 |
#' ) |
|
88 |
#' datasets$set_filter_state(state = fs) |
|
89 |
#' isolate(datasets$get_filter_state()) |
|
90 |
#' |
|
91 |
#' @keywords internal |
|
92 |
#' |
|
93 |
FilteredData <- R6::R6Class( # nolint |
|
94 |
"FilteredData", |
|
95 |
# public methods ---- |
|
96 |
public = list( |
|
97 |
#' @description |
|
98 |
#' Initialize a `FilteredData` object. |
|
99 |
#' @param data_objects (`named list`) |
|
100 |
#' List of data objects. |
|
101 |
#' Names of the list will be used as `dataname` for respective datasets. |
|
102 |
#' @param join_keys (`join_keys`) optional joining keys, see [`teal.data::join_keys()`]. |
|
103 |
#' |
|
104 |
initialize = function(data_objects, join_keys = teal.data::join_keys()) { |
|
105 | 65x |
checkmate::assert_list(data_objects, any.missing = FALSE, min.len = 0, names = "unique") |
106 |
# unpack data.object from the nested list |
|
107 | 65x |
data_objects <- lapply(data_objects, function(dataset) { |
108 | 95x |
if (is.list(dataset) && "dataset" %in% names(dataset)) { |
109 | ! |
dataset$dataset |
110 |
} else { |
|
111 | 95x |
dataset |
112 |
} |
|
113 |
}) |
|
114 | ||
115 |
# Note the internals of data_objects are checked in set_dataset |
|
116 | 65x |
checkmate::assert_class(join_keys, "join_keys") |
117 | 64x |
self$set_join_keys(join_keys) |
118 | 64x |
child_parent <- sapply( |
119 | 64x |
names(data_objects), |
120 | 64x |
function(i) teal.data::parent(join_keys, i), |
121 | 64x |
USE.NAMES = TRUE, |
122 | 64x |
simplify = FALSE |
123 |
) |
|
124 | 64x |
ordered_datanames <- topological_sort(child_parent) |
125 | 64x |
ordered_datanames <- intersect(ordered_datanames, names(data_objects)) |
126 | ||
127 | 64x |
for (dataname in ordered_datanames) { |
128 | 94x |
ds_object <- data_objects[[dataname]] |
129 | 94x |
self$set_dataset(data = ds_object, dataname = dataname) |
130 |
} |
|
131 | ||
132 | 64x |
self$set_available_teal_slices(x = reactive(NULL)) |
133 | ||
134 | 64x |
invisible(self) |
135 |
}, |
|
136 | ||
137 |
#' @description |
|
138 |
#' Gets `datanames`. |
|
139 |
#' @details |
|
140 |
#' The `datanames` are returned in the order in which they must be evaluated (in case of dependencies). |
|
141 |
#' @return Character vector. |
|
142 |
datanames = function() { |
|
143 | 115x |
names(private$filtered_datasets) |
144 |
}, |
|
145 | ||
146 |
#' @description |
|
147 |
#' Gets data label for the dataset. |
|
148 |
#' Useful to display in `Show R Code`. |
|
149 |
#' |
|
150 |
#' @param dataname (`character(1)`) name of the dataset |
|
151 |
#' @return Character string. |
|
152 |
get_datalabel = function(dataname) { |
|
153 | 1x |
private$get_filtered_dataset(dataname)$get_dataset_label() |
154 |
}, |
|
155 | ||
156 |
#' @description |
|
157 |
#' Set list of external filter states available for activation. |
|
158 |
#' @details |
|
159 |
#' Unlike adding new filter from the column, these filters can come with some prespecified settings. |
|
160 |
#' `teal_slices` are wrapped in a `reactive` so they can be updated from elsewhere in the app. |
|
161 |
#' Filters passed in `x` are limited to those that can be set for this `FilteredData` object, |
|
162 |
#' i.e. they have the correct `dataname` and `varname` (waived `teal_slice_fixed` as they do not have `varname`). |
|
163 |
#' List is accessible in `ui/srv_active` through `ui/srv_available_filters`. |
|
164 |
#' @param x (`reactive`) |
|
165 |
#' should return `teal_slices` |
|
166 |
#' @return `NULL`, invisibly. |
|
167 |
set_available_teal_slices = function(x) { |
|
168 | 65x |
checkmate::assert_class(x, "reactive") |
169 | 65x |
private$available_teal_slices <- reactive({ |
170 |
# Available filters should be limited to the ones relevant for this FilteredData. |
|
171 | 4x |
current_state <- isolate(self$get_filter_state()) |
172 | 4x |
allowed <- attr(current_state, "include_varnames") |
173 | 4x |
forbidden <- attr(current_state, "exclude_varnames") |
174 | 4x |
foo <- function(slice) { |
175 | 13x |
if (slice$dataname %in% self$datanames()) { |
176 | 13x |
if (slice$fixed) { |
177 | 4x |
TRUE |
178 |
} else { |
|
179 | 9x |
isTRUE(slice$varname %in% allowed[[slice$dataname]]) || |
180 | 9x |
isFALSE(slice$varname %in% forbidden[[slice$dataname]]) |
181 |
} |
|
182 |
} else { |
|
183 | ! |
FALSE |
184 |
} |
|
185 |
} |
|
186 | 4x |
Filter(foo, x()) |
187 |
}) |
|
188 | 65x |
invisible(NULL) |
189 |
}, |
|
190 | ||
191 |
#' @description |
|
192 |
#' Get list of filter states available for this object. |
|
193 |
#' @details |
|
194 |
#' All `teal_slice` objects that have been created since the beginning of the app session |
|
195 |
#' are stored in one `teal_slices` object. This returns a subset of that `teal_slices`, |
|
196 |
#' describing filter states that can be set for this object. |
|
197 |
#' @return `reactive` that returns `teal_slices`. |
|
198 |
get_available_teal_slices = function() { |
|
199 | 4x |
private$available_teal_slices |
200 |
}, |
|
201 | ||
202 |
# datasets methods ---- |
|
203 | ||
204 |
#' @description |
|
205 |
#' Gets a `call` to filter the dataset according to the filter state. |
|
206 |
#' @details |
|
207 |
#' It returns a `call` to filter the dataset only, assuming the |
|
208 |
#' other (filtered) datasets it depends on are available. |
|
209 |
#' |
|
210 |
#' Together with `self$datanames()` which returns the datasets in the correct |
|
211 |
#' evaluation order, this generates the whole filter code, see the function |
|
212 |
#' `FilteredData$get_filter_code`. |
|
213 |
#' |
|
214 |
#' For the return type, note that `rlang::is_expression` returns `TRUE` on the |
|
215 |
#' return type, both for base `R` expressions and calls (single expression, |
|
216 |
#' capturing a function call). |
|
217 |
#' |
|
218 |
#' The filtered dataset has the name given by `self$filtered_dataname(dataname)` |
|
219 |
#' |
|
220 |
#' This can be used for the `Show R Code` generation. |
|
221 |
#' |
|
222 |
#' @param dataname (`character(1)`) name of the dataset |
|
223 |
#' |
|
224 |
#' @return A list of `call`s. |
|
225 |
#' |
|
226 |
get_call = function(dataname) { |
|
227 | 10x |
checkmate::assert_subset(dataname, self$datanames()) |
228 | 9x |
private$get_filtered_dataset(dataname)$get_call() |
229 |
}, |
|
230 | ||
231 |
#' @description |
|
232 |
#' Gets filtered or unfiltered dataset. |
|
233 |
#' |
|
234 |
#' For `filtered = FALSE`, the original data set with `set_data` is returned including all attributes. |
|
235 |
#' |
|
236 |
#' @param dataname (`character(1)`) name of the dataset. |
|
237 |
#' @param filtered (`logical(1)`) whether to return a filtered or unfiltered dataset. |
|
238 |
#' |
|
239 |
#' @return A data object, a `data.frame` or a `MultiAssayExperiment`. |
|
240 |
#' |
|
241 |
get_data = function(dataname, filtered = TRUE) { |
|
242 | 24x |
checkmate::assert_subset(dataname, self$datanames()) |
243 | 23x |
checkmate::assert_flag(filtered) |
244 | 22x |
data <- private$get_filtered_dataset(dataname)$get_dataset(filtered) |
245 | 3x |
if (filtered) data() else data |
246 |
}, |
|
247 | ||
248 |
#' @description |
|
249 |
#' Get join keys between two datasets. |
|
250 |
#' |
|
251 |
#' @return `join_keys` |
|
252 |
#' |
|
253 |
get_join_keys = function() { |
|
254 | 2x |
private$join_keys |
255 |
}, |
|
256 | ||
257 |
#' @description |
|
258 |
#' Creates filter overview table to be displayed in the application. |
|
259 |
#' One row is created per dataset, according to the `get_filter_overview` methods |
|
260 |
#' of the contained `FilteredDataset` objects. |
|
261 |
#' |
|
262 |
#' @param datanames (`character`) vector of dataset names. |
|
263 |
#' |
|
264 |
#' @return A `data.frame` listing the numbers of observations in all datasets. |
|
265 |
#' |
|
266 |
get_filter_overview = function(datanames) { |
|
267 | 9x |
rows <- lapply( |
268 | 9x |
datanames, |
269 | 9x |
function(dataname) { |
270 | 11x |
private$get_filtered_dataset(dataname)$get_filter_overview() |
271 |
} |
|
272 |
) |
|
273 | 5x |
unssuported_idx <- vapply(rows, function(x) all(is.na(x[-1])), logical(1)) |
274 | 5x |
dplyr::bind_rows(c(rows[!unssuported_idx], rows[unssuported_idx])) |
275 |
}, |
|
276 | ||
277 |
#' @description |
|
278 |
#' Get keys for the dataset. |
|
279 |
#' |
|
280 |
#' @param dataname (`character(1)`) name of the dataset. |
|
281 |
#' |
|
282 |
#' @return Character vector of key column names. |
|
283 |
#' |
|
284 |
get_keys = function(dataname) { |
|
285 | 1x |
private$get_filtered_dataset(dataname)$get_keys() |
286 |
}, |
|
287 | ||
288 |
#' @description |
|
289 |
#' Adds a dataset to this `FilteredData`. |
|
290 |
#' |
|
291 |
#' @details |
|
292 |
#' `set_dataset` creates a `FilteredDataset` object which keeps `dataset` for the filtering purpose. |
|
293 |
#' If this data has a parent specified in the `join_keys` object stored in `private$join_keys` |
|
294 |
#' then created `FilteredDataset` (child) gets linked with other `FilteredDataset` (parent). |
|
295 |
#' "Child" dataset return filtered data then dependent on the reactive filtered data of the |
|
296 |
#' "parent". See more in documentation of `parent` argument in `DataframeFilteredDataset` constructor. |
|
297 |
#' |
|
298 |
#' @param data (`data.frame` or `MultiAssayExperiment`) |
|
299 |
#' data to be filtered. |
|
300 |
#' |
|
301 |
#' @param dataname (`character(1)`) |
|
302 |
#' the name of the `dataset` to be added to this object. |
|
303 |
#' |
|
304 |
#' @return `self`, invisibly. |
|
305 |
#' |
|
306 |
set_dataset = function(data, dataname) { |
|
307 | 99x |
checkmate::assert_string(dataname) |
308 | 99x |
logger::log_debug("FilteredData$set_dataset setting dataset, name: { dataname }") |
309 | ||
310 | 99x |
parent_dataname <- teal.data::parent(private$join_keys, dataname) |
311 | 99x |
keys <- private$join_keys[dataname, dataname] |
312 | 98x |
if (is.null(keys)) keys <- character(0) |
313 | ||
314 | 99x |
if (length(parent_dataname) == 0) { |
315 | 89x |
private$filtered_datasets[[dataname]] <- init_filtered_dataset( |
316 | 89x |
dataset = data, |
317 | 89x |
dataname = dataname, |
318 | 89x |
keys = keys |
319 |
) |
|
320 |
} else { |
|
321 | 10x |
join_keys <- private$join_keys[dataname, parent_dataname] |
322 | ! |
if (is.null(join_keys)) join_keys <- character(0) |
323 | 10x |
private$filtered_datasets[[dataname]] <- init_filtered_dataset( |
324 | 10x |
dataset = data, |
325 | 10x |
dataname = dataname, |
326 | 10x |
keys = keys, |
327 | 10x |
parent_name = parent_dataname, |
328 | 10x |
parent = reactive(self$get_data(parent_dataname, filtered = TRUE)), |
329 | 10x |
join_keys = join_keys |
330 |
) |
|
331 |
} |
|
332 | ||
333 | 99x |
invisible(self) |
334 |
}, |
|
335 | ||
336 |
#' @description |
|
337 |
#' Set the `join_keys`. |
|
338 |
#' |
|
339 |
#' @param join_keys (`join_keys`), see [`teal.data::join_keys()`]. |
|
340 |
#' |
|
341 |
#' @return `self`, invisibly. |
|
342 |
#' |
|
343 |
set_join_keys = function(join_keys) { |
|
344 | 64x |
checkmate::assert_class(join_keys, "join_keys") |
345 | 64x |
private$join_keys <- join_keys |
346 | 64x |
invisible(self) |
347 |
}, |
|
348 | ||
349 |
# Functions useful for restoring from another dataset ---- |
|
350 | ||
351 |
#' @description |
|
352 |
#' Gets states of all contained `FilterState` objects. |
|
353 |
#' |
|
354 |
#' @return A `teal_slices` object. |
|
355 |
#' |
|
356 |
get_filter_state = function() { |
|
357 | 46x |
states <- unname(lapply(private$filtered_datasets, function(x) x$get_filter_state())) |
358 | 46x |
slices <- Filter(Negate(is.null), states) |
359 | 46x |
slices <- do.call(c, slices) |
360 | 46x |
if (!is.null(slices)) { |
361 | 46x |
attr(slices, "allow_add") <- private$allow_add |
362 |
} |
|
363 | 46x |
slices |
364 |
}, |
|
365 | ||
366 |
#' @description |
|
367 |
#' Returns a formatted string representing this `FilteredData` object. |
|
368 |
#' |
|
369 |
#' @param show_all (`logical(1)`) passed to `format.teal_slice`. |
|
370 |
#' @param trim_lines (`logical(1)`) passed to `format.teal_slice`. |
|
371 |
#' |
|
372 |
#' @return `character(1)` the formatted string. |
|
373 |
#' |
|
374 |
format = function(show_all = FALSE, trim_lines = TRUE) { |
|
375 | 5x |
datasets <- lapply(self$datanames(), private$get_filtered_dataset) |
376 | 5x |
ind <- vapply(datasets, inherits, logical(1L), what = "DefaultFilteredDataset") |
377 | 5x |
states <- do.call(c, lapply(datasets[!ind], function(ds) ds$get_filter_state())) |
378 | 5x |
states_fmt <- format(states, show_all = show_all, trim_lines = trim_lines) |
379 | 5x |
holders_fmt <- vapply(datasets[ind], format, character(1L), show_all = show_all, trim_lines = trim_lines) |
380 | ||
381 | 5x |
sprintf( |
382 | 5x |
"%s:\n%s", |
383 | 5x |
class(self)[1], |
384 | 5x |
paste(c(states_fmt, holders_fmt), collapse = "\n") |
385 |
) |
|
386 |
}, |
|
387 | ||
388 |
#' @description |
|
389 |
#' Prints this `FilteredData` object. |
|
390 |
#' |
|
391 |
#' @param ... additional arguments passed to `format`. |
|
392 |
#' |
|
393 |
print = function(...) { |
|
394 | ! |
cat(isolate(self$format(...)), "\n") |
395 |
}, |
|
396 | ||
397 |
#' @description |
|
398 |
#' Sets active filter states. |
|
399 |
#' |
|
400 |
#' @param state (`teal_slices`) |
|
401 |
#' |
|
402 |
#' @return `NULL`, invisibly. |
|
403 |
set_filter_state = function(state) { |
|
404 | 29x |
isolate({ |
405 | 29x |
logger::log_debug("{ class(self)[1] }$set_filter_state initializing") |
406 | 29x |
checkmate::assert_class(state, "teal_slices") |
407 | 29x |
allow_add <- attr(state, "allow_add") |
408 | 29x |
if (!is.null(allow_add)) { |
409 | 29x |
private$allow_add <- allow_add |
410 |
} |
|
411 | ||
412 | 29x |
lapply(self$datanames(), function(dataname) { |
413 | 57x |
states <- Filter(function(x) identical(x$dataname, dataname), state) |
414 | 57x |
private$get_filtered_dataset(dataname)$set_filter_state(states) |
415 |
}) |
|
416 | ||
417 | 29x |
invisible(NULL) |
418 |
}) |
|
419 | ||
420 | 29x |
invisible(NULL) |
421 |
}, |
|
422 | ||
423 |
#' @description |
|
424 |
#' Removes one or more `FilterState` from a `FilteredData` object. |
|
425 |
#' |
|
426 |
#' @param state (`teal_slices`) |
|
427 |
#' specifying `FilterState` objects to remove; |
|
428 |
#' `teal_slice`s may contain only `dataname` and `varname`, other elements are ignored. |
|
429 |
#' |
|
430 |
#' @return `NULL`, invisibly. |
|
431 |
#' |
|
432 |
remove_filter_state = function(state) { |
|
433 | 8x |
isolate({ |
434 | 8x |
checkmate::assert_class(state, "teal_slices") |
435 | 8x |
datanames <- unique(vapply(state, "[[", character(1L), "dataname")) |
436 | 8x |
checkmate::assert_subset(datanames, self$datanames()) |
437 | ||
438 | 8x |
logger::log_debug( |
439 | 8x |
"{ class(self)[1] }$remove_filter_state removing filter(s), dataname: { private$dataname }" |
440 |
) |
|
441 | ||
442 | 8x |
lapply(datanames, function(dataname) { |
443 | 9x |
slices <- Filter(function(x) identical(x$dataname, dataname), state) |
444 | 9x |
private$get_filtered_dataset(dataname)$remove_filter_state(slices) |
445 |
}) |
|
446 | ||
447 | 8x |
logger::log_debug( |
448 | 8x |
"{ class(self)[1] }$remove_filter_state removed filter(s), dataname: { private$dataname }" |
449 |
) |
|
450 |
}) |
|
451 | ||
452 | 8x |
invisible(NULL) |
453 |
}, |
|
454 | ||
455 |
#' @description |
|
456 |
#' Remove all `FilterStates` of a `FilteredDataset` or all `FilterStates` of a `FilteredData` object. |
|
457 |
#' |
|
458 |
#' @param datanames (`character`) |
|
459 |
#' names of datasets for which to remove all filter states. |
|
460 |
#' Defaults to all datasets in this `FilteredData` object. |
|
461 |
#' @param force (`logical(1)`) |
|
462 |
#' flag specifying whether to include anchored filter states. |
|
463 |
#' |
|
464 |
#' @return `NULL`, invisibly. |
|
465 |
#' |
|
466 |
clear_filter_states = function(datanames = self$datanames(), force = FALSE) { |
|
467 | 7x |
logger::log_debug( |
468 | 7x |
"FilteredData$clear_filter_states called, datanames: { toString(datanames) }" |
469 |
) |
|
470 | ||
471 | 7x |
for (dataname in datanames) { |
472 | 12x |
fdataset <- private$get_filtered_dataset(dataname = dataname) |
473 | 12x |
fdataset$clear_filter_states(force) |
474 |
} |
|
475 | ||
476 | 7x |
logger::log_debug( |
477 | 7x |
paste( |
478 | 7x |
"FilteredData$clear_filter_states removed all non-anchored FilterStates,", |
479 | 7x |
"datanames: { toString(datanames) }" |
480 |
) |
|
481 |
) |
|
482 | ||
483 | 7x |
invisible(NULL) |
484 |
}, |
|
485 | ||
486 | ||
487 |
# shiny modules ----- |
|
488 | ||
489 |
#' @description |
|
490 |
#' top-level `shiny` module for the filter panel in the `teal` app. |
|
491 |
#' Contains 1) filter overview panel, 2) filter active panel, and 3) add filters panel. |
|
492 |
#' |
|
493 |
#' @param id (`character(1)`) |
|
494 |
#' `shiny` module instance id. |
|
495 |
#' @param active_datanames (`reactive`) |
|
496 |
#' defining subset of `self$datanames()` to be displayed. |
|
497 |
#' @return `shiny.tag` |
|
498 |
ui_filter_panel = function(id, active_datanames = self$datanames) { |
|
499 | ! |
ns <- NS(id) |
500 | ! |
tags$div( |
501 | ! |
id = ns(NULL), # used for hiding / showing |
502 | ! |
include_css_files(pattern = "filter-panel"), |
503 | ! |
include_js_files(pattern = "togglePanelItems"), |
504 | ! |
shinyjs::useShinyjs(), |
505 | ! |
self$ui_overview(ns("overview")), |
506 | ! |
self$ui_active(ns("active"), active_datanames = active_datanames) |
507 |
) |
|
508 |
}, |
|
509 | ||
510 |
#' @description |
|
511 |
#' Server function for filter panel. |
|
512 |
#' |
|
513 |
#' @param id (`character(1)`) |
|
514 |
#' `shiny` module instance id. |
|
515 |
#' @param active_datanames (`function` or `reactive`) |
|
516 |
#' returning `datanames` that should be shown on the filter panel. |
|
517 |
#' Must be a subset of the `datanames` in this `FilteredData`. |
|
518 |
#' If the function returns `NULL` (as opposed to `character(0)`), |
|
519 |
#' the filter panel will be hidden. |
|
520 |
#' @return `NULL`. |
|
521 |
srv_filter_panel = function(id, active_datanames = self$datanames) { |
|
522 | 1x |
checkmate::assert_function(active_datanames) |
523 | 1x |
moduleServer( |
524 | 1x |
id = id, |
525 | 1x |
function(input, output, session) { |
526 | 1x |
logger::log_debug("FilteredData$srv_filter_panel initializing") |
527 | ||
528 | 1x |
active_datanames_resolved <- reactive({ |
529 | 1x |
checkmate::assert_subset(active_datanames(), self$datanames()) |
530 | ! |
active_datanames() |
531 |
}) |
|
532 | ||
533 | 1x |
self$srv_overview("overview", active_datanames_resolved) |
534 | 1x |
self$srv_active("active", active_datanames_resolved) |
535 | ||
536 | 1x |
NULL |
537 |
} |
|
538 |
) |
|
539 |
}, |
|
540 | ||
541 |
#' @description |
|
542 |
#' Server module responsible for displaying active filters. |
|
543 |
#' @param id (`character(1)`) |
|
544 |
#' `shiny` module instance id. |
|
545 |
#' @param active_datanames (`reactive`) |
|
546 |
#' defining subset of `self$datanames()` to be displayed. |
|
547 |
#' @return `shiny.tag` |
|
548 |
ui_active = function(id, active_datanames = self$datanames) { |
|
549 | ! |
ns <- NS(id) |
550 | ! |
tags$div( |
551 | ! |
id = id, # not used, can be used to customize CSS behavior |
552 | ! |
class = "well", |
553 | ! |
include_js_files(pattern = "togglePanelItems"), |
554 | ! |
tags$div( |
555 | ! |
style = "display: flex; justify-content: space-between;", |
556 | ! |
tags$span("Active Filter Variables", class = "text-primary", style = "font-weight: 700;"), |
557 | ! |
tags$div( |
558 | ! |
style = "min-width: 60px;", |
559 | ! |
uiOutput(ns("remove_all_filters_ui")), |
560 | ! |
tags$a( |
561 | ! |
class = "remove_all", |
562 | ! |
tags$i( |
563 | ! |
class = "fa fa-angle-down", |
564 | ! |
title = "fold/expand ...", |
565 | ! |
onclick = sprintf( |
566 | ! |
"togglePanelItems(this, ['%s', '%s'], 'fa-angle-down', 'fa-angle-right');", |
567 | ! |
ns("filter_active_vars_contents"), |
568 | ! |
ns("filters_active_count") |
569 |
) |
|
570 |
) |
|
571 |
), |
|
572 | ! |
private$ui_available_filters(ns("available_filters")) |
573 |
) |
|
574 |
), |
|
575 | ! |
tags$div( |
576 | ! |
id = ns("filter_active_vars_contents"), |
577 | ! |
tagList( |
578 | ! |
lapply( |
579 | ! |
isolate(active_datanames()), |
580 | ! |
function(dataname) { |
581 | ! |
fdataset <- private$get_filtered_dataset(dataname) |
582 | ! |
fdataset$ui_active(id = ns(dataname), allow_add = private$allow_add) |
583 |
} |
|
584 |
) |
|
585 |
) |
|
586 |
), |
|
587 | ! |
tags$div( |
588 | ! |
id = ns("filters_active_count"), |
589 | ! |
style = "display: none;", |
590 | ! |
textOutput(ns("teal_filters_count")) |
591 |
) |
|
592 |
) |
|
593 |
}, |
|
594 | ||
595 |
#' @description |
|
596 |
#' Server module responsible for displaying active filters. |
|
597 |
#' @param id (`character(1)`) |
|
598 |
#' `shiny` module instance id. |
|
599 |
#' @param active_datanames (`reactive`) |
|
600 |
#' defining subset of `self$datanames()` to be displayed. |
|
601 |
#' @return `NULL`. |
|
602 |
srv_active = function(id, active_datanames = self$datanames) { |
|
603 | 3x |
checkmate::assert_function(active_datanames) |
604 | 3x |
moduleServer(id, function(input, output, session) { |
605 | 3x |
logger::log_debug("FilteredData$srv_active initializing") |
606 | ||
607 | 3x |
private$srv_available_filters("available_filters") |
608 | ||
609 | 3x |
private$session_bindings[[session$ns("minimise_filter_active")]] <- observeEvent( |
610 | 3x |
eventExpr = input$minimise_filter_active, |
611 | 3x |
handlerExpr = { |
612 | ! |
shinyjs::toggle("filter_active_vars_contents") |
613 | ! |
shinyjs::toggle("filters_active_count") |
614 | ! |
toggle_icon(session$ns("minimise_filter_active"), c("fa-angle-right", "fa-angle-down")) |
615 | ! |
toggle_title(session$ns("minimise_filter_active"), c("Restore panel", "Minimise Panel")) |
616 |
} |
|
617 |
) |
|
618 | ||
619 | 3x |
filter_count <- reactive({ |
620 | 3x |
length(self$get_filter_state()) |
621 |
}) |
|
622 | ||
623 | 3x |
is_filter_removable <- reactive({ |
624 | 3x |
non_anchored <- Filter(function(x) !x$anchored, self$get_filter_state()) |
625 | 3x |
isTRUE(length(non_anchored) > 0) |
626 |
}) |
|
627 | ||
628 | 3x |
output$remove_all_filters_ui <- renderUI({ |
629 | 3x |
req(is_filter_removable()) |
630 | 2x |
actionLink( |
631 | 2x |
inputId = session$ns("remove_all_filters"), |
632 | 2x |
label = "", |
633 | 2x |
icon("circle-xmark", lib = "font-awesome"), |
634 | 2x |
title = "Remove active filters", |
635 | 2x |
class = "remove_all" |
636 |
) |
|
637 |
}) |
|
638 | ||
639 | 3x |
private$session_bindings[[session$ns("is_filter_removable")]] <- observeEvent( |
640 | 3x |
eventExpr = is_filter_removable(), |
641 | 3x |
handlerExpr = { |
642 | 3x |
shinyjs::toggle("remove_all_filters", condition = is_filter_removable()) |
643 | 3x |
shinyjs::show("filter_active_vars_contents") |
644 | 3x |
shinyjs::hide("filters_active_count") |
645 | 3x |
toggle_icon(session$ns("minimise_filter_active"), c("fa-angle-right", "fa-angle-down"), TRUE) |
646 | 3x |
toggle_title(session$ns("minimise_filter_active"), c("Restore panel", "Minimise Panel"), TRUE) |
647 |
} |
|
648 |
) |
|
649 | ||
650 | 3x |
private$session_bindings[[session$ns("active_datanames")]] <- observeEvent( |
651 | 3x |
eventExpr = active_datanames(), |
652 | 3x |
handlerExpr = lapply(self$datanames(), function(dataname) { |
653 | 4x |
if (dataname %in% active_datanames()) { |
654 | 4x |
shinyjs::show(dataname) |
655 |
} else { |
|
656 | ! |
shinyjs::hide(dataname) |
657 |
} |
|
658 |
}) |
|
659 |
) |
|
660 | ||
661 |
# should not use for-loop as variables are otherwise only bound by reference |
|
662 |
# and last dataname would be used |
|
663 | 3x |
lapply( |
664 | 3x |
self$datanames(), |
665 | 3x |
function(dataname) { |
666 | 6x |
fdataset <- private$get_filtered_dataset(dataname) |
667 | 6x |
fdataset$srv_active(id = dataname) |
668 |
} |
|
669 |
) |
|
670 | ||
671 | 3x |
output$teal_filters_count <- renderText({ |
672 | 3x |
n_filters_active <- filter_count() |
673 | 3x |
req(n_filters_active > 0L) |
674 | 2x |
sprintf( |
675 | 2x |
"%s filter%s applied across datasets", |
676 | 2x |
n_filters_active, |
677 | 2x |
ifelse(n_filters_active == 1, "", "s") |
678 |
) |
|
679 |
}) |
|
680 | ||
681 | 3x |
private$session_bindings[[session$ns("remove_all_filters")]] <- observeEvent( |
682 | 3x |
eventExpr = input$remove_all_filters, |
683 | 3x |
handlerExpr = { |
684 | 1x |
logger::log_debug("FilteredData$srv_filter_panel@1 removing all non-anchored filters") |
685 | 1x |
self$clear_filter_states() |
686 | 1x |
logger::log_debug("FilteredData$srv_filter_panel@1 removed all non-anchored filters") |
687 |
} |
|
688 |
) |
|
689 | ||
690 | 3x |
private$session_bindings[[session$ns("inputs")]] <- list( |
691 | 3x |
destroy = function() { |
692 | 2x |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
693 |
} |
|
694 |
) |
|
695 | ||
696 | 3x |
NULL |
697 |
}) |
|
698 |
}, |
|
699 | ||
700 |
#' @description |
|
701 |
#' Creates the UI definition for the module showing counts for each dataset |
|
702 |
#' contrasting the filtered to the full unfiltered dataset. |
|
703 |
#' |
|
704 |
#' Per dataset, it displays |
|
705 |
#' the number of rows/observations in each dataset, |
|
706 |
#' the number of unique subjects. |
|
707 |
#' |
|
708 |
#' @param id (`character(1)`) |
|
709 |
#' `shiny` module instance id. |
|
710 |
#' |
|
711 |
ui_overview = function(id) { |
|
712 | ! |
ns <- NS(id) |
713 | ! |
tags$div( |
714 | ! |
id = id, # not used, can be used to customize CSS behavior |
715 | ! |
class = "well", |
716 | ! |
tags$div( |
717 | ! |
class = "row", |
718 | ! |
tags$div( |
719 | ! |
class = "col-sm-9", |
720 | ! |
tags$label("Active Filter Summary", class = "text-primary mb-4") |
721 |
), |
|
722 | ! |
tags$div( |
723 | ! |
class = "col-sm-3", |
724 | ! |
tags$a( |
725 | ! |
class = "filter-icon", |
726 | ! |
tags$i( |
727 | ! |
class = "fa fa-angle-down", |
728 | ! |
title = "fold/expand ...", |
729 | ! |
onclick = sprintf( |
730 | ! |
"togglePanelItems(this, '%s', 'fa-angle-down', 'fa-angle-right');", |
731 | ! |
ns("filters_overview_contents") |
732 |
) |
|
733 |
) |
|
734 |
) |
|
735 |
) |
|
736 |
), |
|
737 | ! |
tags$div( |
738 | ! |
id = ns("filters_overview_contents"), |
739 | ! |
tags$div( |
740 | ! |
class = "teal_active_summary_filter_panel", |
741 | ! |
tableOutput(ns("table")) |
742 |
) |
|
743 |
) |
|
744 |
) |
|
745 |
}, |
|
746 | ||
747 |
#' @description |
|
748 |
#' Server function to display the number of records in the filtered and unfiltered |
|
749 |
#' data. |
|
750 |
#' |
|
751 |
#' @param id (`character(1)`) |
|
752 |
#' `shiny` module instance id. |
|
753 |
#' @param active_datanames (`reactive`) |
|
754 |
#' returning `datanames` that should be shown on the filter panel, |
|
755 |
#' must be a subset of the `datanames` argument provided to `ui_filter_panel`; |
|
756 |
#' if the function returns `NULL` (as opposed to `character(0)`), the filter |
|
757 |
#' panel will be hidden. |
|
758 |
#' @return `NULL`. |
|
759 |
srv_overview = function(id, active_datanames = self$datanames) { |
|
760 | 1x |
checkmate::assert_class(active_datanames, "reactive") |
761 | 1x |
moduleServer( |
762 | 1x |
id = id, |
763 | 1x |
function(input, output, session) { |
764 | 1x |
logger::log_debug("FilteredData$srv_filter_overview initializing") |
765 | ||
766 | 1x |
output$table <- renderUI({ |
767 | ! |
logger::log_debug("FilteredData$srv_filter_overview@1 updating counts") |
768 | ! |
if (length(active_datanames()) == 0) { |
769 | ! |
return(NULL) |
770 |
} |
|
771 | ||
772 | ! |
datasets_df <- self$get_filter_overview(datanames = active_datanames()) |
773 | ||
774 | ! |
attr(datasets_df$dataname, "label") <- "Data Name" |
775 | ||
776 | ! |
if (!is.null(datasets_df$obs)) { |
777 |
# some datasets (MAE colData) doesn't return obs column |
|
778 | ! |
datasets_df <- transform( |
779 | ! |
datasets_df, |
780 | ! |
obs_str_summary = ifelse( |
781 | ! |
!is.na(obs), |
782 | ! |
sprintf("%s/%s", obs_filtered, obs), |
783 |
"" |
|
784 |
) |
|
785 |
) |
|
786 | ! |
attr(datasets_df$obs_str_summary, "label") <- "Obs" |
787 |
} |
|
788 | ||
789 | ||
790 | ! |
if (!is.null(datasets_df$subjects)) { |
791 |
# some datasets (without keys) doesn't return subjects |
|
792 | ! |
datasets_df <- transform( |
793 | ! |
datasets_df, |
794 | ! |
subjects_summary = ifelse( |
795 | ! |
!is.na(subjects), |
796 | ! |
sprintf("%s/%s", subjects_filtered, subjects), |
797 |
"" |
|
798 |
) |
|
799 |
) |
|
800 | ! |
attr(datasets_df$subjects_summary, "label") <- "Subjects" |
801 |
} |
|
802 | ||
803 | ! |
all_names <- c("dataname", "obs_str_summary", "subjects_summary") |
804 | ! |
datasets_df <- datasets_df[, colnames(datasets_df) %in% all_names] |
805 | ||
806 | ! |
body_html <- apply( |
807 | ! |
datasets_df, |
808 | ! |
1, |
809 | ! |
function(x) { |
810 | ! |
tags$tr( |
811 | ! |
tagList( |
812 | ! |
tags$td( |
813 | ! |
if (all(x[-1] == "")) { |
814 | ! |
icon( |
815 | ! |
name = "exclamation-triangle", |
816 | ! |
title = "Unsupported dataset", |
817 | ! |
`data-container` = "body", |
818 | ! |
`data-toggle` = "popover", |
819 | ! |
`data-content` = "object not supported by the filter panel" |
820 |
) |
|
821 |
}, |
|
822 | ! |
x[1] |
823 |
), |
|
824 | ! |
lapply(x[-1], tags$td) |
825 |
) |
|
826 |
) |
|
827 |
} |
|
828 |
) |
|
829 | ||
830 | ! |
header_labels <- vapply( |
831 | ! |
seq_along(datasets_df), |
832 | ! |
function(i) { |
833 | ! |
label <- attr(datasets_df[[i]], "label") |
834 | ! |
ifelse(!is.null(label), label, names(datasets_df)[[i]]) |
835 |
}, |
|
836 | ! |
character(1) |
837 |
) |
|
838 | ! |
header_html <- tags$tr(tagList(lapply(header_labels, tags$td))) |
839 | ||
840 | ! |
table_html <- tags$table( |
841 | ! |
class = "table custom-table", |
842 | ! |
tags$thead(header_html), |
843 | ! |
tags$tbody(body_html) |
844 |
) |
|
845 | ! |
logger::log_debug("FilteredData$srv_filter_overview@1 updated counts") |
846 | ! |
table_html |
847 |
}) |
|
848 | ||
849 | 1x |
NULL |
850 |
} |
|
851 |
) |
|
852 |
}, |
|
853 | ||
854 |
#' @description |
|
855 |
#' Object and dependencies cleanup. |
|
856 |
#' |
|
857 |
#' - Destroy inputs and observers stored in `private$session_bindings` |
|
858 |
#' - Finalize `FilteredData` stored in `private$filtered_datasets` |
|
859 |
#' |
|
860 |
#' @return `NULL`, invisibly. |
|
861 |
finalize = function() { |
|
862 | 64x |
.finalize_session_bindings(self, private) |
863 | 64x |
lapply(private$filtered_datasets, function(x) x$finalize()) |
864 | 64x |
invisible(NULL) |
865 |
} |
|
866 |
), |
|
867 | ||
868 |
# private members ---- |
|
869 |
private = list( |
|
870 |
# selectively hide / show to only show `active_datanames` out of all datanames |
|
871 | ||
872 |
# private attributes ---- |
|
873 |
filtered_datasets = list(), |
|
874 | ||
875 |
# activate/deactivate filter panel |
|
876 |
filter_panel_active = TRUE, |
|
877 | ||
878 |
# `reactive` containing teal_slices that can be selected; only active in module-specific mode |
|
879 |
available_teal_slices = NULL, |
|
880 | ||
881 |
# keys used for joining/filtering data a join_keys object (see teal.data) |
|
882 |
join_keys = NULL, |
|
883 | ||
884 |
# flag specifying whether the user may add filters |
|
885 |
allow_add = TRUE, |
|
886 | ||
887 |
# observers and inputs list |
|
888 |
session_bindings = list(), |
|
889 | ||
890 |
# private methods ---- |
|
891 | ||
892 |
# @description |
|
893 |
# Gets `FilteredDataset` object which contains all information |
|
894 |
# pertaining to the specified dataset. |
|
895 |
# |
|
896 |
# @param dataname (`character(1)`) |
|
897 |
# name of the dataset |
|
898 |
# |
|
899 |
# @return `FilteredDataset` object or list of `FilteredDataset`s |
|
900 |
# |
|
901 |
get_filtered_dataset = function(dataname = character(0)) { |
|
902 | 139x |
if (length(dataname) == 0) { |
903 | ! |
private$filtered_datasets |
904 |
} else { |
|
905 | 139x |
private$filtered_datasets[[dataname]] |
906 |
} |
|
907 |
}, |
|
908 | ||
909 |
# @description |
|
910 |
# Activate available filters. |
|
911 |
# Module is composed from plus button and dropdown menu. Menu is shown when |
|
912 |
# the button is clicked. Menu contains available/active filters list |
|
913 |
# passed via `set_available_teal_slice`. |
|
914 |
ui_available_filters = function(id) { |
|
915 | ! |
ns <- NS(id) |
916 | ||
917 | ! |
active_slices_id <- isolate(vapply(self$get_filter_state(), `[[`, character(1), "id")) |
918 | ! |
tags$div( |
919 | ! |
id = ns("available_menu"), |
920 | ! |
shinyWidgets::dropMenu( |
921 | ! |
actionLink( |
922 | ! |
ns("show"), |
923 | ! |
label = NULL, |
924 | ! |
icon = icon("plus", lib = "font-awesome"), |
925 | ! |
title = "Available filters", |
926 | ! |
class = "remove pull-right" |
927 |
), |
|
928 | ! |
tags$div( |
929 | ! |
class = "menu-content", |
930 | ! |
shinycssloaders::withSpinner( |
931 | ! |
uiOutput(ns("checkbox")), |
932 | ! |
type = 4, |
933 | ! |
size = 0.25 |
934 |
) |
|
935 |
) |
|
936 |
) |
|
937 |
) |
|
938 |
}, |
|
939 |
# @description |
|
940 |
# Activate available filters. When a filter is selected or removed, |
|
941 |
# `set_filter_state` or `remove_filter_state` is executed for |
|
942 |
# the appropriate filter state id. |
|
943 |
srv_available_filters = function(id) { |
|
944 | 4x |
moduleServer(id, function(input, output, session) { |
945 | 4x |
slices_available <- self$get_available_teal_slices() |
946 | 4x |
slices_interactive <- reactive( |
947 | 4x |
Filter(function(slice) isFALSE(slice$fixed), slices_available()) |
948 |
) |
|
949 | 4x |
slices_fixed <- reactive( |
950 | 4x |
Filter(function(slice) isTRUE(slice$fixed), slices_available()) |
951 |
) |
|
952 | 4x |
available_slices_id <- reactive(vapply(slices_available(), `[[`, character(1), "id")) |
953 | 4x |
active_slices_id <- reactive(vapply(self$get_filter_state(), `[[`, character(1), "id")) |
954 | 4x |
duplicated_slice_references <- reactive({ |
955 |
# slice refers to a particular column |
|
956 | 8x |
slice_reference <- vapply(slices_available(), get_default_slice_id, character(1)) |
957 | 8x |
is_duplicated_reference <- duplicated(slice_reference) | duplicated(slice_reference, fromLast = TRUE) |
958 | 8x |
is_active <- available_slices_id() %in% active_slices_id() |
959 | 8x |
is_not_expr <- !vapply(slices_available(), inherits, logical(1), "teal_slice_expr") |
960 | 8x |
slice_reference[is_duplicated_reference & is_active & is_not_expr] |
961 |
}) |
|
962 | ||
963 | 4x |
checkbox_group_element <- function(name, value, label, checked, disabled = FALSE) { |
964 | 35x |
tags$div( |
965 | 35x |
class = "checkbox available-filters", |
966 | 35x |
tags$label( |
967 | 35x |
tags$input( |
968 | 35x |
type = "checkbox", |
969 | 35x |
name = name, |
970 | 35x |
value = value, |
971 | 35x |
checked = checked, |
972 | 35x |
disabled = if (disabled) "disabled" |
973 |
), |
|
974 | 35x |
tags$span(label, disabled = if (disabled) disabled) |
975 |
) |
|
976 |
) |
|
977 |
} |
|
978 | ||
979 | 4x |
output$checkbox <- renderUI({ |
980 | 8x |
checkbox <- checkboxGroupInput( |
981 | 8x |
session$ns("available_slices_id"), |
982 | 8x |
label = NULL, |
983 | 8x |
choices = NULL, |
984 | 8x |
selected = NULL |
985 |
) |
|
986 | 8x |
active_slices_ids <- active_slices_id() |
987 | 8x |
duplicated_slice_refs <- duplicated_slice_references() |
988 | ||
989 | 8x |
checkbox_group_slice <- function(slice) { |
990 |
# we need to isolate changes in the fields of the slice (teal_slice) |
|
991 | 35x |
isolate({ |
992 | 35x |
checkbox_group_element( |
993 | 35x |
name = session$ns("available_slices_id"), |
994 | 35x |
value = slice$id, |
995 | 35x |
label = slice$id, |
996 | 35x |
checked = if (slice$id %in% active_slices_ids) "checked", |
997 | 35x |
disabled = slice$anchored || |
998 | 35x |
get_default_slice_id(slice) %in% duplicated_slice_refs && |
999 | 35x |
!slice$id %in% active_slices_ids |
1000 |
) |
|
1001 |
}) |
|
1002 |
} |
|
1003 | ||
1004 | 8x |
interactive_choice_mock <- lapply(slices_interactive(), checkbox_group_slice) |
1005 | 8x |
non_interactive_choice_mock <- lapply(slices_fixed(), checkbox_group_slice) |
1006 | ||
1007 | 8x |
htmltools::tagInsertChildren( |
1008 | 8x |
checkbox, |
1009 | 8x |
tags$br(), |
1010 | 8x |
if (length(non_interactive_choice_mock)) tags$strong("Fixed filters"), |
1011 | 8x |
non_interactive_choice_mock, |
1012 | 8x |
if (length(interactive_choice_mock)) tags$strong("Interactive filters"), |
1013 | 8x |
interactive_choice_mock, |
1014 | 8x |
.cssSelector = "div.shiny-options-group", |
1015 | 8x |
after = 0 |
1016 |
) |
|
1017 |
}) |
|
1018 | ||
1019 | 4x |
private$session_bindings[[session$ns("available_slices_id")]] <- observeEvent( |
1020 | 4x |
eventExpr = input$available_slices_id, |
1021 | 4x |
ignoreNULL = FALSE, |
1022 | 4x |
ignoreInit = TRUE, |
1023 | 4x |
handlerExpr = { |
1024 | 5x |
new_slices_id <- setdiff(input$available_slices_id, active_slices_id()) |
1025 | 5x |
removed_slices_id <- setdiff(active_slices_id(), input$available_slices_id) |
1026 | 5x |
if (length(new_slices_id)) { |
1027 | 3x |
new_teal_slices <- Filter( |
1028 | 3x |
function(slice) slice$id %in% new_slices_id, |
1029 | 3x |
private$available_teal_slices() |
1030 |
) |
|
1031 | 3x |
self$set_filter_state(new_teal_slices) |
1032 |
} |
|
1033 | ||
1034 | 5x |
if (length(removed_slices_id)) { |
1035 | 4x |
removed_teal_slices <- Filter( |
1036 | 4x |
function(slice) slice$id %in% removed_slices_id, |
1037 | 4x |
self$get_filter_state() |
1038 |
) |
|
1039 | 4x |
self$remove_filter_state(removed_teal_slices) |
1040 |
} |
|
1041 |
} |
|
1042 |
) |
|
1043 | ||
1044 | 4x |
private$session_bindings[[session$ns("available_teal_slices")]] <- observeEvent( |
1045 | 4x |
eventExpr = private$available_teal_slices(), |
1046 | 4x |
ignoreNULL = FALSE, |
1047 | 4x |
handlerExpr = { |
1048 | 3x |
if (length(private$available_teal_slices())) { |
1049 | 1x |
shinyjs::show("available_menu") |
1050 |
} else { |
|
1051 | 2x |
shinyjs::hide("available_menu") |
1052 |
} |
|
1053 |
} |
|
1054 |
) |
|
1055 |
}) |
|
1056 |
} |
|
1057 |
) |
|
1058 |
) |
1 |
# FilteredDataset abstract -------- |
|
2 | ||
3 |
#' @name FilteredDataset |
|
4 |
#' @docType class |
|
5 |
#' |
|
6 |
#' @title `FilteredDataset` `R6` class |
|
7 |
#' @description |
|
8 |
#' `FilteredDataset` is a class which renders/controls `FilterStates`(s) |
|
9 |
#' Each `FilteredDataset` contains `filter_states` field - a `list` which contains one |
|
10 |
#' (`data.frame`) or multiple (`MultiAssayExperiment`) `FilterStates` objects. |
|
11 |
#' Each `FilterStates` is responsible for one filter/subset expression applied for specific |
|
12 |
#' components of the dataset. |
|
13 |
#' |
|
14 |
#' @keywords internal |
|
15 |
FilteredDataset <- R6::R6Class( # nolint |
|
16 |
"FilteredDataset", |
|
17 |
# public methods ---- |
|
18 |
public = list( |
|
19 |
#' @description |
|
20 |
#' Initializes this `FilteredDataset` object. |
|
21 |
#' |
|
22 |
#' @param dataset any object |
|
23 |
#' @param dataname (`character(1)`) |
|
24 |
#' syntactically valid name given to the dataset. |
|
25 |
#' @param keys (`character`) optional |
|
26 |
#' vector of primary key column names. |
|
27 |
#' @param label (`character(1)`) |
|
28 |
#' label to describe the dataset. |
|
29 |
#' |
|
30 |
#' @return Object of class `FilteredDataset`, invisibly. |
|
31 |
#' |
|
32 |
initialize = function(dataset, dataname, keys = character(0), label = attr(dataset, "label", exact = TRUE)) { |
|
33 | 147x |
checkmate::assert_string(dataname) |
34 | 145x |
logger::log_debug("Instantiating { class(self)[1] }, dataname: { dataname }") |
35 | 145x |
checkmate::assert_character(keys, any.missing = FALSE) |
36 | 145x |
checkmate::assert_character(label, null.ok = TRUE) |
37 | 145x |
private$dataset <- dataset |
38 | 145x |
private$dataname <- dataname |
39 | 145x |
private$keys <- keys |
40 | 145x |
private$label <- if (is.null(label)) character(0) else label |
41 | ||
42 |
# function executing reactive call and returning data |
|
43 | 145x |
private$data_filtered_fun <- function(sid = "") { |
44 | 24x |
checkmate::assert_character(sid) |
45 | 24x |
if (length(sid)) { |
46 | 24x |
logger::log_debug("filtering data dataname: { dataname }, sid: { sid }") |
47 |
} else { |
|
48 | ! |
logger::log_debug("filtering data dataname: { private$dataname }") |
49 |
} |
|
50 | 24x |
env <- new.env(parent = parent.env(globalenv())) |
51 | 24x |
env[[dataname]] <- private$dataset |
52 | 24x |
filter_call <- self$get_call(sid) |
53 | 24x |
eval_expr_with_msg(filter_call, env) |
54 | 24x |
get(x = dataname, envir = env) |
55 |
} |
|
56 | ||
57 | 145x |
private$data_filtered <- reactive(private$data_filtered_fun()) |
58 | 145x |
invisible(self) |
59 |
}, |
|
60 | ||
61 |
#' @description |
|
62 |
#' Returns a formatted string representing this `FilteredDataset` object. |
|
63 |
#' |
|
64 |
#' @param show_all (`logical(1)`) passed to `format.teal_slice`. |
|
65 |
#' @param trim_lines (`logical(1)`) passed to `format.teal_slice`. |
|
66 |
#' |
|
67 |
#' @return The formatted character string. |
|
68 |
#' |
|
69 |
format = function(show_all = FALSE, trim_lines = TRUE) { |
|
70 | 24x |
sprintf( |
71 | 24x |
"%s:\n%s", |
72 | 24x |
class(self)[1], |
73 | 24x |
format(self$get_filter_state(), show_all = show_all, trim_lines = trim_lines) |
74 |
) |
|
75 |
}, |
|
76 | ||
77 |
#' @description |
|
78 |
#' Prints this `FilteredDataset` object. |
|
79 |
#' |
|
80 |
#' @param ... additional arguments passed to `format`. |
|
81 |
#' |
|
82 |
print = function(...) { |
|
83 | 10x |
cat(isolate(self$format(...)), "\n") |
84 |
}, |
|
85 | ||
86 |
#' @description |
|
87 |
#' Removes all filter items applied to this dataset. |
|
88 |
#' |
|
89 |
#' @param force (`logical(1)`) |
|
90 |
#' flag specifying whether to include anchored filter states. |
|
91 |
#' |
|
92 |
#' @return `NULL`. |
|
93 |
clear_filter_states = function(force = FALSE) { |
|
94 | 14x |
logger::log_debug("Removing filters from FilteredDataset: { deparse1(self$get_dataname()) }") |
95 | 14x |
lapply( |
96 | 14x |
private$get_filter_states(), |
97 | 14x |
function(filter_states) filter_states$clear_filter_states(force) |
98 |
) |
|
99 | 14x |
logger::log_debug("Removed filters from FilteredDataset: { deparse1(self$get_dataname()) }") |
100 | 14x |
NULL |
101 |
}, |
|
102 | ||
103 |
# managing filter states ----- |
|
104 | ||
105 |
# getters ---- |
|
106 |
#' @description |
|
107 |
#' Gets a filter expression. |
|
108 |
#' |
|
109 |
#' This function returns filter calls equivalent to selected items |
|
110 |
#' within each of `filter_states`. Configuration of the calls is constant and |
|
111 |
#' depends on `filter_states` type and order which are set during initialization. |
|
112 |
#' |
|
113 |
#' @param sid (`character`) |
|
114 |
#' when specified, the method returns code containing conditions calls of |
|
115 |
#' `FilterState` objects with `sid` different to this `sid` argument. |
|
116 |
#' |
|
117 |
#' @return Either a `list` of filter `call`s, or `NULL`. |
|
118 |
get_call = function(sid = "") { |
|
119 | 47x |
filter_call <- Filter( |
120 | 47x |
f = Negate(is.null), |
121 | 47x |
x = lapply(private$get_filter_states(), function(x) x$get_call(sid)) |
122 |
) |
|
123 | 47x |
if (length(filter_call) == 0) { |
124 | 29x |
return(NULL) |
125 |
} |
|
126 | 18x |
filter_call |
127 |
}, |
|
128 | ||
129 |
#' @description |
|
130 |
#' Gets states of all contained `FilterState` objects. |
|
131 |
#' |
|
132 |
#' @return A `teal_slices` object. |
|
133 |
#' |
|
134 |
get_filter_state = function() { |
|
135 | 150x |
states <- unname(lapply(private$get_filter_states(), function(x) x$get_filter_state())) |
136 | 150x |
do.call(c, states) |
137 |
}, |
|
138 | ||
139 |
#' @description |
|
140 |
#' Set filter state. |
|
141 |
#' |
|
142 |
#' @param state (`teal_slices`) |
|
143 |
#' |
|
144 |
#' @return Virtual method, returns nothing and raises error. |
|
145 |
#' |
|
146 |
set_filter_state = function(state) { |
|
147 | ! |
stop("set_filter_state is an abstract class method.") |
148 |
}, |
|
149 | ||
150 |
#' @description |
|
151 |
#' Gets the name of the dataset. |
|
152 |
#' |
|
153 |
#' @return A character string. |
|
154 |
get_dataname = function() { |
|
155 | 8x |
private$dataname |
156 |
}, |
|
157 | ||
158 |
#' @description |
|
159 |
#' Gets the dataset object in this `FilteredDataset`. |
|
160 |
#' |
|
161 |
#' @param filtered (`logical(1)`) |
|
162 |
#' |
|
163 |
#' @return |
|
164 |
#' The stored dataset. If `data.frame` or `MultiAssayExperiment`, |
|
165 |
#' either raw or as a reactive with current filters applied (depending on `filtered`). |
|
166 |
#' |
|
167 |
get_dataset = function(filtered = FALSE) { |
|
168 | 51x |
if (filtered) { |
169 | 33x |
private$data_filtered |
170 |
} else { |
|
171 | 18x |
private$dataset |
172 |
} |
|
173 |
}, |
|
174 | ||
175 |
#' @description |
|
176 |
#' Get filter overview of a dataset. |
|
177 |
#' @return Virtual method, returns nothing and raises an error. |
|
178 |
get_filter_overview = function() { |
|
179 | ! |
stop("get_filter_overview is an abstract class method") |
180 |
}, |
|
181 | ||
182 |
#' @description |
|
183 |
#' Gets the key columns for this dataset. |
|
184 |
#' @return Character vector of variable names |
|
185 |
get_keys = function() { |
|
186 | 127x |
private$keys |
187 |
}, |
|
188 | ||
189 |
#' @description |
|
190 |
#' Gets the dataset label. |
|
191 |
#' @return Character string. |
|
192 |
get_dataset_label = function() { |
|
193 | 2x |
private$label |
194 |
}, |
|
195 | ||
196 |
# modules ------ |
|
197 |
#' @description |
|
198 |
#' `shiny` module containing active filters for a dataset, along with a title and a remove button. |
|
199 |
#' @param id (`character(1)`) |
|
200 |
#' `shiny` module instance id. |
|
201 |
#' @param allow_add (`logical(1)`) |
|
202 |
#' logical flag specifying whether the user will be able to add new filters |
|
203 |
#' |
|
204 |
#' @return `shiny.tag` |
|
205 |
ui_active = function(id, allow_add = TRUE) { |
|
206 | ! |
dataname <- self$get_dataname() |
207 | ! |
checkmate::assert_string(dataname) |
208 | ||
209 | ! |
ns <- NS(id) |
210 | ! |
if_multiple_filter_states <- length(private$get_filter_states()) > 1 |
211 | ! |
tags$span( |
212 | ! |
id = id, |
213 | ! |
include_css_files("filter-panel"), |
214 | ! |
include_js_files(pattern = "icons"), |
215 | ! |
tags$div( |
216 | ! |
id = ns("whole_ui"), # to hide it entirely |
217 | ! |
fluidRow( |
218 | ! |
style = "padding: 0px 15px 0px 15px;", |
219 | ! |
tags$div( |
220 | ! |
style = "display: flex; align-items: center; justify-content: space-between;", |
221 | ! |
tags$div( |
222 | ! |
style = "display: flex;", |
223 | ! |
tags$span(dataname, class = "filter_panel_dataname"), |
224 | ! |
if (allow_add) { |
225 | ! |
tags$a( |
226 | ! |
class = "filter-icon add-filter", |
227 | ! |
tags$i( |
228 | ! |
id = ns("add_filter_icon"), |
229 | ! |
class = "fa fa-plus", |
230 | ! |
title = "fold/expand transform panel", |
231 | ! |
onclick = sprintf( |
232 | ! |
"togglePanelItems(this, '%s', 'fa-plus', 'fa-minus');", |
233 | ! |
ns("add_panel") |
234 |
) |
|
235 |
) |
|
236 |
) |
|
237 |
} |
|
238 |
), |
|
239 | ! |
tags$div( |
240 | ! |
style = "min-width: 40px; z-index: 1; display: flex; justify-content: flex-end;", |
241 | ! |
uiOutput(ns("collapse_ui")), |
242 | ! |
uiOutput(ns("remove_filters_ui")) |
243 |
) |
|
244 |
), |
|
245 | ! |
if (allow_add) { |
246 | ! |
tags$div( |
247 | ! |
id = ns("add_panel"), |
248 | ! |
class = "add-panel", |
249 | ! |
style = "display: none;", |
250 | ! |
self$ui_add(ns(private$dataname)) |
251 |
) |
|
252 |
} |
|
253 |
), |
|
254 | ! |
tags$div( |
255 | ! |
id = ns("filter_count_ui"), |
256 | ! |
style = "display: none;", |
257 | ! |
tagList( |
258 | ! |
textOutput(ns("filter_count")) |
259 |
) |
|
260 |
), |
|
261 | ! |
tags$div( |
262 |
# id needed to insert and remove UI to filter single variable as needed |
|
263 |
# it is currently also used by the above module to entirely hide this panel |
|
264 | ! |
id = ns("filters"), |
265 | ! |
class = "parent-hideable-list-group", |
266 | ! |
tagList( |
267 | ! |
lapply( |
268 | ! |
names(private$get_filter_states()), |
269 | ! |
function(x) { |
270 | ! |
tagList(private$get_filter_states()[[x]]$ui_active(id = ns(x))) |
271 |
} |
|
272 |
) |
|
273 |
) |
|
274 |
) |
|
275 |
) |
|
276 |
) |
|
277 |
}, |
|
278 | ||
279 |
#' @description |
|
280 |
#' Server module for a dataset active filters. |
|
281 |
#' |
|
282 |
#' @param id (`character(1)`) |
|
283 |
#' `shiny` module instance id. |
|
284 |
#' @return `NULL`. |
|
285 |
srv_active = function(id) { |
|
286 | 7x |
moduleServer( |
287 | 7x |
id = id, |
288 | 7x |
function(input, output, session) { |
289 | 7x |
dataname <- self$get_dataname() |
290 | 7x |
logger::log_debug("FilteredDataset$srv_active initializing, dataname: { dataname }") |
291 | 7x |
checkmate::assert_string(dataname) |
292 | ||
293 | 7x |
filter_count <- reactive({ |
294 | 8x |
length(self$get_filter_state()) |
295 |
}) |
|
296 | ||
297 | 7x |
output$filter_count <- renderText( |
298 | 7x |
sprintf( |
299 | 7x |
"%d filter%s applied", |
300 | 7x |
filter_count(), |
301 | 7x |
if (filter_count() != 1) "s" else "" |
302 |
) |
|
303 |
) |
|
304 | ||
305 | 7x |
lapply( |
306 | 7x |
names(private$get_filter_states()), |
307 | 7x |
function(x) { |
308 | 12x |
private$get_filter_states()[[x]]$srv_active(id = x) |
309 |
} |
|
310 |
) |
|
311 | ||
312 | 7x |
is_filter_collapsible <- reactive({ |
313 | 8x |
filter_count() != 0 |
314 |
}) |
|
315 | ||
316 | 7x |
output$collapse_ui <- renderUI({ |
317 | 8x |
req(is_filter_collapsible()) |
318 | 5x |
tags$a( |
319 | 5x |
id = session$ns("collapse"), |
320 | 5x |
class = "filter-icon", |
321 | 5x |
tags$i( |
322 | 5x |
id = session$ns("collapse_icon"), |
323 | 5x |
class = "fa fa-angle-down", |
324 | 5x |
title = "fold/expand dataset filters", |
325 |
# TODO: clickWhenClassPresent() is used to hide the add_ui pannel during a collapse of the UI. |
|
326 |
# In the future, it should be completely handled by collapsing the UI by positioning. |
|
327 | 5x |
onclick = sprintf( |
328 | 5x |
"togglePanelItems(this, ['%s', '%s'], 'fa-angle-down', 'fa-angle-right'); |
329 | 5x |
clickWhenClassPresent('%s', 'fa-minus', this.classList.contains('fa-angle-right'));", |
330 | 5x |
session$ns("filter_count_ui"), |
331 | 5x |
session$ns("filters"), |
332 | 5x |
session$ns("add_filter_icon") |
333 |
) |
|
334 |
) |
|
335 |
) |
|
336 |
}) |
|
337 | ||
338 | 7x |
is_filter_removable <- reactive({ |
339 | 8x |
non_anchored <- Filter(function(x) !x$anchored, self$get_filter_state()) |
340 | 8x |
isTRUE(length(non_anchored) > 0) |
341 |
}) |
|
342 | ||
343 | 7x |
private$session_bindings[[session$ns("get_filter_state")]] <- observeEvent( |
344 | 7x |
self$get_filter_state(), |
345 | 7x |
ignoreInit = TRUE, |
346 |
{ |
|
347 | 3x |
shinyjs::hide("filter_count_ui") |
348 | 3x |
shinyjs::show("filters") |
349 | 3x |
shinyjs::toggle("remove_filters_ui", condition = is_filter_removable()) |
350 | 3x |
shinyjs::toggle("collapse_ui", condition = is_filter_collapsible()) |
351 | 3x |
shinyjs::runjs( |
352 | 3x |
sprintf( |
353 | 3x |
"setAndRemoveClass('#%s', 'fa-angle-down', 'fa-angle-right')", |
354 | 3x |
session$ns("collapse_icon") |
355 |
) |
|
356 |
) |
|
357 |
} |
|
358 |
) |
|
359 | ||
360 | 7x |
output$remove_filters_ui <- renderUI({ |
361 | 8x |
req(is_filter_removable()) |
362 | 5x |
actionLink( |
363 | 5x |
session$ns("remove_filters"), |
364 | 5x |
label = "", |
365 | 5x |
icon = icon("circle-xmark", lib = "font-awesome"), |
366 | 5x |
class = "filter-icon" |
367 |
) |
|
368 |
}) |
|
369 | ||
370 | 7x |
private$session_bindings[[session$ns("remove_filters")]] <- observeEvent(input$remove_filters, { |
371 | 1x |
logger::log_debug("FilteredDataset$srv_active@1 removing all non-anchored filters, dataname: { dataname }") |
372 | 1x |
self$clear_filter_states() |
373 | 1x |
logger::log_debug("FilteredDataset$srv_active@1 removed all non-anchored filters, dataname: { dataname }") |
374 |
}) |
|
375 | ||
376 | 7x |
private$session_bindings[[session$ns("inputs")]] <- list( |
377 | 7x |
destroy = function() { |
378 | 8x |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
379 |
} |
|
380 |
) |
|
381 | ||
382 |