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 | 1205x |
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 | 24x |
logger::log_debug("Destroying FilterState inputs and observers; id: { private$get_id() }") |
281 | 24x |
if (!session$isEnded()) { |
282 | 8x |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
283 |
} |
|
284 |
} |
|
285 |
) |
|
286 | ||
287 | 12x |
private$state_history <- reactiveVal(list()) |
288 | ||
289 | 12x |
NULL |
290 |
} |
|
291 |
) |
|
292 |
}, |
|
293 | ||
294 |
#' @description |
|
295 |
#' `shiny` UI module. |
|
296 |
#' The UI for this class contains simple message stating that it is not supported. |
|
297 |
#' @param id (`character(1)`) |
|
298 |
#' `shiny` module instance id. |
|
299 |
#' @param parent_id (`character(1)`) id of the `FilterStates` card container |
|
300 |
ui = function(id, parent_id = "cards") { |
|
301 | 12x |
ns <- NS(id) |
302 | ||
303 |
# Filter card consists of header and body, arranged in a single column. |
|
304 |
# Body is hidden and is toggled by clicking on header. |
|
305 |
## Header consists of title and summary, arranged in a column. |
|
306 |
### Title consists of conditional icon, varname, conditional varlabel, and controls, arranged in a row. |
|
307 |
### Summary consists of value and controls, arranged in a row. |
|
308 | ||
309 | 12x |
tags$div( |
310 | 12x |
id = id, |
311 | 12x |
class = "panel filter-card", |
312 | 12x |
include_js_files("count-bar-labels.js"), |
313 | 12x |
tags$div( |
314 | 12x |
class = "filter-card-header", |
315 | 12x |
`data-toggle` = "collapse", |
316 | 12x |
`data-bs-toggle` = "collapse", |
317 | 12x |
href = paste0("#", ns("body")), |
318 | 12x |
tags$div( |
319 | 12x |
class = "filter-card-title", |
320 | 12x |
if (private$is_anchored() && private$is_fixed()) { |
321 | ! |
icon("anchor-lock", class = "filter-card-icon") |
322 | 12x |
} else if (private$is_anchored() && !private$is_fixed()) { |
323 | ! |
icon("anchor", class = "filter-card-icon") |
324 | 12x |
} else if (!private$is_anchored() && private$is_fixed()) { |
325 | ! |
icon("lock", class = "filter-card-icon") |
326 |
}, |
|
327 | 12x |
tags$div(class = "filter-card-varname", tags$strong(private$get_varname())), |
328 | 12x |
tags$div(class = "filter-card-varlabel", private$get_varlabel()), |
329 | 12x |
tags$div( |
330 | 12x |
class = "filter-card-controls", |
331 |
# Suppress toggling body when clicking on this div. |
|
332 |
# This is for bootstrap 3 and 4. Causes page to scroll to top, prevented by setting href on buttons. |
|
333 | 12x |
onclick = "event.stopPropagation();event.preventDefault();", |
334 |
# This is for bootstrap 5. |
|
335 | 12x |
`data-bs-toggle` = "collapse", |
336 | 12x |
`data-bs-target` = NULL, |
337 | 12x |
if (isFALSE(private$is_fixed())) { |
338 | 12x |
actionLink( |
339 | 12x |
inputId = ns("back"), |
340 | 12x |
label = NULL, |
341 | 12x |
icon = icon("circle-arrow-left", lib = "font-awesome"), |
342 | 12x |
title = "Rewind state", |
343 | 12x |
class = "filter-card-back", |
344 | 12x |
style = "display: none" |
345 |
) |
|
346 |
}, |
|
347 | 12x |
if (isFALSE(private$is_fixed())) { |
348 | 12x |
actionLink( |
349 | 12x |
inputId = ns("reset"), |
350 | 12x |
label = NULL, |
351 | 12x |
icon = icon("circle-arrow-up", lib = "font-awesome"), |
352 | 12x |
title = "Restore original state", |
353 | 12x |
class = "filter-card-back", |
354 | 12x |
style = "display: none" |
355 |
) |
|
356 |
}, |
|
357 | 12x |
if (isFALSE(private$is_anchored())) { |
358 | 12x |
actionLink( |
359 | 12x |
inputId = ns("remove"), |
360 | 12x |
label = icon("circle-xmark", lib = "font-awesome"), |
361 | 12x |
title = "Remove filter", |
362 | 12x |
class = "filter-card-remove" |
363 |
) |
|
364 |
} |
|
365 |
) |
|
366 |
), |
|
367 | 12x |
tags$div(class = "filter-card-summary", private$ui_summary(ns("summary"))) |
368 |
), |
|
369 | 12x |
tags$div( |
370 | 12x |
id = ns("body"), |
371 | 12x |
class = "collapse out", |
372 | 12x |
`data-parent` = paste0("#", parent_id), |
373 | 12x |
`data-bs-parent` = paste0("#", parent_id), |
374 | 12x |
tags$div( |
375 | 12x |
class = "filter-card-body", |
376 | 12x |
if (private$is_fixed()) { |
377 | ! |
private$ui_inputs_fixed(ns("inputs")) |
378 |
} else { |
|
379 | 12x |
private$ui_inputs(ns("inputs")) |
380 |
} |
|
381 |
) |
|
382 |
) |
|
383 |
) |
|
384 |
}, |
|
385 | ||
386 |
#' @description |
|
387 |
#' Destroy inputs and observers stored in `private$session_bindings`. |
|
388 |
#' |
|
389 |
#' |
|
390 |
#' @return `NULL`, invisibly. |
|
391 |
#' |
|
392 |
finalize = function() { |
|
393 | 521x |
.finalize_session_bindings(self, private) |
394 | 521x |
invisible(NULL) |
395 |
} |
|
396 |
), |
|
397 | ||
398 |
# private members ---- |
|
399 |
private = list( |
|
400 |
# set by constructor |
|
401 |
x = NULL, # the filtered variable |
|
402 |
x_reactive = NULL, # reactive containing the filtered variable, used for updating counts and histograms |
|
403 |
teal_slice = NULL, # stores all transferable properties of this filter state |
|
404 |
extract_type = character(0), # used by private$get_varname_prefixed |
|
405 |
na_count = integer(0), |
|
406 |
filtered_na_count = NULL, # reactive containing the count of NA in the filtered dataset |
|
407 |
varlabel = character(0), # taken from variable labels in data; displayed in filter cards |
|
408 |
# other |
|
409 |
is_choice_limited = FALSE, # flag whether number of possible choices was limited when specifying filter |
|
410 |
session_bindings = list(), # stores observers and inputs to destroy afterwards |
|
411 |
state_history = NULL, # reactiveVal holding a list storing states this FilterState has had since instantiation |
|
412 | ||
413 |
# private methods ---- |
|
414 | ||
415 |
# setters for state features ---- |
|
416 | ||
417 |
# @description |
|
418 |
# Set values that can be selected from. |
|
419 |
set_choices = function(choices) { |
|
420 | ! |
stop("this is a virtual method") |
421 |
}, |
|
422 | ||
423 |
# @description |
|
424 |
# Set selection. |
|
425 |
# |
|
426 |
# @param value (`vector`) |
|
427 |
# value(s) that come from filter selection; values are set in the |
|
428 |
# module server after a selection is made in the app interface; |
|
429 |
# values are stored in `teal_slice$selected` which is reactive; |
|
430 |
# value types have to be the same as `private$get_choices()` |
|
431 |
# |
|
432 |
# @return `NULL`, invisibly. |
|
433 |
set_selected = function(value) { |
|
434 | 416x |
logger::log_debug( |
435 | 416x |
sprintf( |
436 | 416x |
"%s$set_selected setting selection of id: %s", |
437 | 416x |
class(self)[1], |
438 | 416x |
private$get_id() |
439 |
) |
|
440 |
) |
|
441 | 416x |
isolate({ |
442 | 416x |
value <- private$cast_and_validate(value) |
443 | 405x |
value <- private$check_length(value) |
444 | 399x |
value <- private$remove_out_of_bounds_values(value) |
445 | 399x |
private$teal_slice$selected <- value |
446 |
}) |
|
447 | ||
448 | 399x |
invisible(NULL) |
449 |
}, |
|
450 | ||
451 |
# @description |
|
452 |
# Sets `value` in `private$teal_slice$keep_na`. |
|
453 |
# |
|
454 |
# @param value (`logical(1)`) |
|
455 |
# corresponding to the state of a checkbox input in the `shiny` interface. |
|
456 |
# |
|
457 |
# @return `NULL`, invisibly. |
|
458 |
# |
|
459 |
set_keep_na = function(value) { |
|
460 | 16x |
checkmate::assert_flag(value) |
461 | 16x |
private$teal_slice$keep_na <- value |
462 | 16x |
logger::log_debug( |
463 | 16x |
sprintf( |
464 | 16x |
"%s$set_keep_na set for filter %s to %s.", |
465 | 16x |
class(self)[1], |
466 | 16x |
private$get_id(), |
467 | 16x |
value |
468 |
) |
|
469 |
) |
|
470 | 16x |
invisible(NULL) |
471 |
}, |
|
472 | ||
473 |
# @description |
|
474 |
# Sets `value` in `private$teal_slice$keep_inf`. |
|
475 |
# |
|
476 |
# @param value (`logical(1)`) |
|
477 |
# corresponding to the state of a checkbox input in the `shiny` interface. |
|
478 |
# |
|
479 |
# @return `NULL`, invisibly. |
|
480 |
# |
|
481 |
set_keep_inf = function(value) { |
|
482 | 9x |
checkmate::assert_flag(value) |
483 | 9x |
private$teal_slice$keep_inf <- value |
484 | 9x |
logger::log_debug( |
485 | 9x |
sprintf( |
486 | 9x |
"%s$set_keep_inf of filter %s set to %s", |
487 | 9x |
class(self)[1], |
488 | 9x |
private$get_id(), |
489 | 9x |
value |
490 |
) |
|
491 |
) |
|
492 | ||
493 | 9x |
invisible(NULL) |
494 |
}, |
|
495 | ||
496 |
# getters for state features ---- |
|
497 | ||
498 |
# @description |
|
499 |
# Returns dataname. |
|
500 |
# @return `character(1)` |
|
501 |
get_dataname = function() { |
|
502 | 88x |
isolate(private$teal_slice$dataname) |
503 |
}, |
|
504 | ||
505 |
# @description |
|
506 |
# Get variable name. |
|
507 |
# @return `character(1)` |
|
508 |
get_varname = function() { |
|
509 | 166x |
isolate(private$teal_slice$varname) |
510 |
}, |
|
511 | ||
512 |
# @description |
|
513 |
# Get id of the teal_slice. |
|
514 |
# @return `character(1)` |
|
515 |
get_id = function() { |
|
516 | 4x |
isolate(private$teal_slice$id) |
517 |
}, |
|
518 | ||
519 |
# @description |
|
520 |
# Get allowed values from `FilterState`. |
|
521 |
# @return |
|
522 |
# Vector describing the available choices. Return type depends on the `FilterState` subclass. |
|
523 |
get_choices = function() { |
|
524 | 776x |
isolate(private$teal_slice$choices) |
525 |
}, |
|
526 | ||
527 |
# @description |
|
528 |
# Get selected values from `FilterState`. |
|
529 |
# @return |
|
530 |
# Vector describing the current selection. Return type depends on the `FilterState` subclass. |
|
531 |
get_selected = function() { |
|
532 | 367x |
private$teal_slice$selected |
533 |
}, |
|
534 | ||
535 |
# @description |
|
536 |
# Returns current `keep_na` selection. |
|
537 |
# @return `logical(1)` |
|
538 |
get_keep_na = function() { |
|
539 | 129x |
private$teal_slice$keep_na |
540 |
}, |
|
541 | ||
542 |
# @description |
|
543 |
# Returns current `keep_inf` selection. |
|
544 |
# @return (`logical(1)`) |
|
545 |
get_keep_inf = function() { |
|
546 | 117x |
private$teal_slice$keep_inf |
547 |
}, |
|
548 | ||
549 |
# Check whether this filter is fixed (cannot be changed). |
|
550 |
# @return `logical(1)` |
|
551 |
is_fixed = function() { |
|
552 | 148x |
isolate(isTRUE(private$teal_slice$fixed)) |
553 |
}, |
|
554 | ||
555 |
# Check whether this filter is anchored (cannot be removed). |
|
556 |
# @return `logical(1)` |
|
557 |
is_anchored = function() { |
|
558 | 48x |
isolate(isTRUE(private$teal_slice$anchored)) |
559 |
}, |
|
560 | ||
561 |
# Check whether this filter is capable of selecting multiple values. |
|
562 |
# @return `logical(1)` |
|
563 |
is_multiple = function() { |
|
564 | 218x |
isolate(isTRUE(private$teal_slice$multiple)) |
565 |
}, |
|
566 | ||
567 |
# other ---- |
|
568 | ||
569 |
# @description |
|
570 |
# Returns variable label. |
|
571 |
# @return `character(1)` |
|
572 |
get_varlabel = function() { |
|
573 | 12x |
private$varlabel |
574 |
}, |
|
575 | ||
576 |
# @description |
|
577 |
# Return variable name prefixed by `dataname` to be evaluated as extracted object, for example `data$var` |
|
578 |
# @return Call that extracts the variable from the dataset. |
|
579 |
get_varname_prefixed = function(dataname) { |
|
580 | 109x |
varname <- private$get_varname() |
581 | 109x |
varname_backticked <- sprintf("`%s`", varname) |
582 | 109x |
ans <- |
583 | 109x |
if (isTRUE(private$extract_type == "list")) { |
584 | 16x |
sprintf("%s$%s", dataname, varname_backticked) |
585 | 109x |
} else if (isTRUE(private$extract_type == "matrix")) { |
586 | 7x |
sprintf("%s[, \"%s\"]", dataname, varname) |
587 |
} else { |
|
588 | 86x |
varname_backticked |
589 |
} |
|
590 | 109x |
str2lang(ans) |
591 |
}, |
|
592 | ||
593 |
# @description |
|
594 |
# Adds `is.na(varname)` moiety to the existing condition call, according to `keep_na` status. |
|
595 |
# @param filter_call `call` raw filter call, as defined by selection |
|
596 |
# @param varname `character(1)` name of a variable |
|
597 |
# @return `call` |
|
598 |
add_keep_na_call = function(filter_call, varname) { |
|
599 |
# No need to deal with NAs. |
|
600 | 108x |
if (private$na_count == 0L) { |
601 | 87x |
return(filter_call) |
602 |
} |
|
603 | ||
604 | 21x |
if (is.null(filter_call) && isFALSE(private$get_keep_na())) { |
605 | 2x |
call("!", call("is.na", varname)) |
606 | 19x |
} else if (!is.null(filter_call) && isTRUE(private$get_keep_na())) { |
607 | 12x |
call("|", call("is.na", varname), filter_call) |
608 | 7x |
} else if (!is.null(filter_call) && isFALSE(private$get_keep_na())) { |
609 | 7x |
call("&", call("!", call("is.na", varname)), filter_call) |
610 |
} |
|
611 |
}, |
|
612 | ||
613 |
# Converts values to the type fitting this `FilterState` and validates the conversion. |
|
614 |
# Raises error if casting does not execute successfully. |
|
615 |
# |
|
616 |
# @param values vector of values |
|
617 |
# |
|
618 |
# @return vector converted to appropriate class |
|
619 |
cast_and_validate = function(values) { |
|
620 | 11x |
values |
621 |
}, |
|
622 | ||
623 |
# Checks length of selection. |
|
624 |
check_length = function(values) { |
|
625 | 11x |
values |
626 |
}, |
|
627 | ||
628 |
# Filters out erroneous values from vector. |
|
629 |
# |
|
630 |
# @param values vector of values |
|
631 |
# |
|
632 |
# @return vector in which values that cannot be set in this FilterState have been dropped |
|
633 |
remove_out_of_bounds_values = function(values) { |
|
634 | 31x |
values |
635 |
}, |
|
636 | ||
637 |
# Checks if the selection is valid in terms of class and length. |
|
638 |
# It should not return anything but raise an error if selection |
|
639 |
# has a wrong class or is outside of possible choices |
|
640 |
validate_selection = function(value) { |
|
641 | ! |
invisible(NULL) |
642 |
}, |
|
643 | ||
644 |
# @description |
|
645 |
# Checks whether the current settings actually cause any values to be omitted. |
|
646 |
# @return logical scalar |
|
647 |
is_any_filtered = function() { |
|
648 | 75x |
if (private$is_choice_limited) { |
649 | 3x |
TRUE |
650 | 72x |
} else if (!setequal(private$get_selected(), private$get_choices())) { |
651 | 59x |
TRUE |
652 | 13x |
} else if (!isTRUE(private$get_keep_na()) && private$na_count > 0) { |
653 | 4x |
TRUE |
654 |
} else { |
|
655 | 9x |
FALSE |
656 |
} |
|
657 |
}, |
|
658 | ||
659 |
# shiny modules ----- |
|
660 | ||
661 |
# @description |
|
662 |
# Server module to display filter summary |
|
663 |
# @param id (`character(1)`) `shiny` module instance id. |
|
664 |
ui_summary = function(id) { |
|
665 | 12x |
ns <- NS(id) |
666 | 12x |
uiOutput(ns("summary"), class = "filter-card-summary") |
667 |
}, |
|
668 | ||
669 |
# @description |
|
670 |
# UI module to display filter summary |
|
671 |
# @param id (`character(1)`) `shiny` module instance id. |
|
672 |
# @return Nothing. Renders the UI. |
|
673 |
server_summary = function(id) { |
|
674 | 12x |
moduleServer( |
675 | 12x |
id = id, |
676 | 12x |
function(input, output, session) { |
677 | 12x |
output$summary <- renderUI(private$content_summary()) |
678 |
} |
|
679 |
) |
|
680 |
}, |
|
681 | ||
682 |
# module with inputs |
|
683 |
ui_inputs = function(id) { |
|
684 | ! |
stop("abstract class") |
685 |
}, |
|
686 |
# module with inputs |
|
687 |
server_inputs = function(id) { |
|
688 | ! |
stop("abstract class") |
689 |
}, |
|
690 | ||
691 |
# @description |
|
692 |
# Module displaying inputs in a fixed filter state. |
|
693 |
# There are no input widgets, only selection visualizations. |
|
694 |
# @param id (`character(1)`) `shiny` module instance id. |
|
695 |
ui_inputs_fixed = function(id) { |
|
696 | ! |
ns <- NS(id) |
697 | ! |
tags$div( |
698 | ! |
class = "choices_state", |
699 | ! |
uiOutput(ns("selection")) |
700 |
) |
|
701 |
}, |
|
702 | ||
703 |
# @description |
|
704 |
# Module creating the display of a fixed filter state. |
|
705 |
# @param id (`character(1)`) `shiny` module instance id. |
|
706 |
server_inputs_fixed = function(id) { |
|
707 | ! |
stop("abstract class") |
708 |
}, |
|
709 | ||
710 |
# @description |
|
711 |
# Module UI function displaying input to keep or remove NA in the `FilterState` call. |
|
712 |
# Renders a checkbox input only when variable with which `FilterState` has been created contains NAs. |
|
713 |
# @param id (`character(1)`) `shiny` module instance id. |
|
714 |
keep_na_ui = function(id) { |
|
715 | 14x |
ns <- NS(id) |
716 | 14x |
if (private$na_count > 0) { |
717 | ! |
isolate({ |
718 | ! |
countmax <- private$na_count |
719 | ! |
countnow <- private$filtered_na_count() |
720 | ! |
ui_input <- checkboxInput( |
721 | ! |
inputId = ns("value"), |
722 | ! |
label = tags$span( |
723 | ! |
id = ns("count_label"), |
724 | ! |
make_count_text( |
725 | ! |
label = "Keep NA", |
726 | ! |
countmax = countmax, |
727 | ! |
countnow = countnow |
728 |
) |
|
729 |
), |
|
730 | ! |
value = private$get_keep_na() |
731 |
) |
|
732 | ! |
tags$div( |
733 | ! |
uiOutput(ns("trigger_visible"), inline = TRUE), |
734 | ! |
ui_input |
735 |
) |
|
736 |
}) |
|
737 |
} else { |
|
738 | 14x |
NULL |
739 |
} |
|
740 |
}, |
|
741 | ||
742 |
# @description |
|
743 |
# Module server function to handle NA values in the `FilterState`. |
|
744 |
# Sets `private$slice$keep_na` according to the selection |
|
745 |
# and updates the relevant UI element if `private$slice$keep_na` has been changed by the api. |
|
746 |
# @param id (`character(1)`) `shiny` module instance id. |
|
747 |
# @return `NULL`, invisibly. |
|
748 |
keep_na_srv = function(id) { |
|
749 | 12x |
moduleServer(id, function(input, output, session) { |
750 |
# 1. renderUI is used here as an observer which triggers only if output is visible |
|
751 |
# and if the reactive changes - reactive triggers only if the output is visible. |
|
752 |
# 2. We want to trigger change of the labels only if reactive count changes (not underlying data) |
|
753 | 12x |
output$trigger_visible <- renderUI({ |
754 | 12x |
updateCountText( |
755 | 12x |
inputId = "count_label", |
756 | 12x |
label = "Keep NA", |
757 | 12x |
countmax = private$na_count, |
758 | 12x |
countnow = private$filtered_na_count() |
759 |
) |
|
760 | 12x |
NULL |
761 |
}) |
|
762 | ||
763 |
# this observer is needed in the situation when private$keep_inf has been |
|
764 |
# changed directly by the api - then it's needed to rerender UI element |
|
765 |
# to show relevant values |
|
766 | 12x |
private$session_bindings[[session$ns("keep_na_api")]] <- observeEvent( |
767 | 12x |
ignoreNULL = FALSE, # nothing selected is possible for NA |
768 | 12x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
769 | 12x |
eventExpr = private$get_keep_na(), |
770 | 12x |
handlerExpr = { |
771 | ! |
if (!setequal(private$get_keep_na(), input$value)) { |
772 | ! |
logger::log_debug("FilterState$keep_na_srv@1 changed reactive value, id: { private$get_id() }") |
773 | ! |
updateCheckboxInput( |
774 | ! |
inputId = "value", |
775 | ! |
label = sprintf("Keep NA (%s/%s)", private$filtered_na_count(), private$na_count), |
776 | ! |
value = private$get_keep_na() |
777 |
) |
|
778 |
} |
|
779 |
} |
|
780 |
) |
|
781 | 12x |
private$session_bindings[[session$ns("keep_na")]] <- observeEvent( |
782 | 12x |
ignoreNULL = FALSE, # ignoreNULL: we don't want to ignore NULL when nothing is selected in the `selectInput` |
783 | 12x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
784 | 12x |
eventExpr = input$value, |
785 | 12x |
handlerExpr = { |
786 | ! |
logger::log_debug("FilterState$keep_na_srv@2 changed input, id: { private$get_id() }") |
787 | ! |
keep_na <- if (is.null(input$value)) { |
788 | ! |
FALSE |
789 |
} else { |
|
790 | ! |
input$value |
791 |
} |
|
792 | ! |
private$set_keep_na(keep_na) |
793 |
} |
|
794 |
) |
|
795 | 12x |
invisible(NULL) |
796 |
}) |
|
797 |
} |
|
798 |
) |
|
799 |
) |
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 |
# FilterStates ------ |
|
2 | ||
3 |
#' @name FilterStates |
|
4 |
#' @docType class |
|
5 |
#' @title `FilterStates` `R6` class |
|
6 |
#' |
|
7 |
#' @description |
|
8 |
#' Abstract class that manages adding and removing `FilterState` objects |
|
9 |
#' and builds a *subset expression*. |
|
10 |
#' |
|
11 |
#' A `FilterStates` object tracks all condition calls |
|
12 |
#' (logical predicates that limit observations) associated with a given dataset |
|
13 |
#' and composes them into a single reproducible `R` expression |
|
14 |
#' that will assign a subset of the original data to a new variable. |
|
15 |
#' This expression is hereafter referred to as *subset expression*. |
|
16 |
#' |
|
17 |
#' The *subset expression* is constructed differently for different |
|
18 |
#' classes of the underlying data object and `FilterStates` sub-classes. |
|
19 |
#' Currently implemented for `data.frame`, `matrix`, |
|
20 |
#' `SummarizedExperiment`, and `MultiAssayExperiment`. |
|
21 |
#' |
|
22 |
#' @keywords internal |
|
23 |
#' |
|
24 |
FilterStates <- R6::R6Class( # nolint |
|
25 |
classname = "FilterStates", |
|
26 | ||
27 |
# public members ---- |
|
28 |
public = list( |
|
29 |
#' @description |
|
30 |
#' Initializes `FilterStates` object by setting |
|
31 |
#' `dataname`, and `datalabel`. |
|
32 |
#' |
|
33 |
#' @param data (`data.frame` or `MultiAssayExperiment` or `SummarizedExperiment` or `matrix`) |
|
34 |
#' the `R` object which `subset` function is applied on. |
|
35 |
#' @param data_reactive (`function(sid)`) |
|
36 |
#' should return an object of the same type as `data` object or `NULL`. |
|
37 |
#' This object is needed for the `FilterState` counts being updated |
|
38 |
#' on a change in filters. If function returns `NULL` then filtered counts are not shown. |
|
39 |
#' Function has to have `sid` argument being a character. |
|
40 |
#' @param dataname (`character(1)`) |
|
41 |
#' name of the dataset, used in the subset expression. |
|
42 |
#' Passed to the function argument attached to this `FilterStates`. |
|
43 |
#' @param datalabel (`character(1)`) optional |
|
44 |
#' text label. |
|
45 |
#' |
|
46 |
#' @return |
|
47 |
#' Object of class `FilterStates`, invisibly. |
|
48 |
#' |
|
49 |
initialize = function(data, |
|
50 |
data_reactive = function(sid = "") NULL, |
|
51 |
dataname, |
|
52 |
datalabel = NULL) { |
|
53 | 258x |
checkmate::assert_string(dataname) |
54 | 256x |
logger::log_debug("Instantiating { class(self)[1] }, dataname: { dataname }") |
55 | 256x |
checkmate::assert_function(data_reactive, args = "sid") |
56 | 256x |
checkmate::assert_string(datalabel, null.ok = TRUE) |
57 | ||
58 | 256x |
private$dataname <- dataname |
59 | 256x |
private$datalabel <- datalabel |
60 | 256x |
private$dataname_prefixed <- dataname |
61 | 256x |
private$data <- data |
62 | 256x |
private$data_reactive <- data_reactive |
63 | 256x |
private$state_list <- reactiveVal() |
64 | 256x |
invisible(self) |
65 |
}, |
|
66 | ||
67 |
#' @description |
|
68 |
#' Returns a formatted string representing this `FilterStates` object. |
|
69 |
#' |
|
70 |
#' @param show_all (`logical(1)`) passed to `format.teal_slices` |
|
71 |
#' @param trim_lines (`logical(1)`) passed to `format.teal_slices` |
|
72 |
#' |
|
73 |
#' @return `character(1)` the formatted string |
|
74 |
#' |
|
75 |
format = function(show_all = FALSE, trim_lines = TRUE) { |
|
76 | ! |
sprintf( |
77 | ! |
"%s:\n%s", |
78 | ! |
class(self)[1], |
79 | ! |
format(self$get_filter_state(), show_all = show_all, trim_lines = trim_lines) |
80 |
) |
|
81 |
}, |
|
82 | ||
83 |
#' @description |
|
84 |
#' Filter call |
|
85 |
#' |
|
86 |
#' Builds *subset expression* from condition calls generated by `FilterState`. |
|
87 |
#' The `lhs` of the expression is a `dataname_prefixed`, where word prefixed refers to |
|
88 |
#' situation when call is evaluated on elements of the original data, for example `dataname[[x]]`. |
|
89 |
#' By default `dataname_prefixed = dataname` and it's not alterable through class methods. |
|
90 |
#' Customization of `private$dataname_prefixed` is done through inheriting classes. |
|
91 |
#' |
|
92 |
#' The `rhs` is a call to `private$fun` with following arguments: |
|
93 |
#' - `dataname_prefixed` |
|
94 |
#' - list of logical expressions generated by `FilterState` objects |
|
95 |
#' stored in `private$state_list`. Each logical predicate is combined with `&` operator. |
|
96 |
#' Variables in these logical expressions by default are not prefixed but this can be changed |
|
97 |
#' by setting `private$extract_type` (change in the similar way as `dataname_prefixed`) |
|
98 |
#' Possible call outputs depending on a custom fields/options: |
|
99 |
#' ``` |
|
100 |
#' # default |
|
101 |
#' dataname <- subset(dataname, col == "x") |
|
102 |
#' |
|
103 |
#' # fun = dplyr::filter |
|
104 |
#' dataname <- dplyr::filter(dataname, col == "x") |
|
105 |
#' |
|
106 |
#' # fun = MultiAssayExperiment::subsetByColData; extract_type = "list" |
|
107 |
#' dataname <- MultiAssayExperiment::subsetByColData(dataname, dataname$col == "x") |
|
108 |
#' |
|
109 |
#' # teal_slice objects having `arg = "subset"` and `arg = "select"` |
|
110 |
#' dataname <- subset(dataname, subset = row_col == "x", select = col_col == "x") |
|
111 |
#' |
|
112 |
#' # dataname = dataname[[element]] |
|
113 |
#' dataname[[element]] <- subset(dataname[[element]], subset = col == "x") |
|
114 |
#' ``` |
|
115 |
#' |
|
116 |
#' If no filters are applied, `NULL` is returned to avoid no-op calls such as `dataname <- dataname`. |
|
117 |
#' |
|
118 |
#' @param sid (`character`) |
|
119 |
#' when specified then method returns code containing condition calls (logical predicates) of |
|
120 |
#' `FilterState` objects which `"sid"` attribute is different than this `sid` argument. |
|
121 |
#' |
|
122 |
#' @return `call` or `NULL` |
|
123 |
#' |
|
124 |
get_call = function(sid = "") { |
|
125 |
# `arg` must be the same as argument of the function where |
|
126 |
# predicate is passed to. |
|
127 |
# For unnamed arguments state_list should have `arg = NULL` |
|
128 | 88x |
states_list <- private$state_list_get() |
129 | 88x |
if (length(states_list) == 0) { |
130 | 52x |
return(NULL) |
131 |
} |
|
132 | 36x |
args <- vapply( |
133 | 36x |
states_list, |
134 | 36x |
function(x) { |
135 | 57x |
arg <- x$get_state()$arg |
136 | 7x |
`if`(is.null(arg), "", arg) # converting NULL -> "" to enable tapply. |
137 |
}, |
|
138 | 36x |
character(1) |
139 |
) |
|
140 | ||
141 | 36x |
filter_items <- tapply( |
142 | 36x |
X = states_list, |
143 | 36x |
INDEX = args, |
144 | 36x |
simplify = FALSE, |
145 | 36x |
function(items) { |
146 |
# removing filters identified by sid |
|
147 | 38x |
other_filter_idx <- !names(items) %in% sid |
148 | 38x |
filtered_items <- items[other_filter_idx] |
149 | ||
150 | 38x |
calls <- Filter( |
151 | 38x |
Negate(is.null), |
152 | 38x |
lapply( |
153 | 38x |
filtered_items, |
154 | 38x |
function(state) { |
155 | 51x |
state$get_call(dataname = private$dataname_prefixed) |
156 |
} |
|
157 |
) |
|
158 |
) |
|
159 | 38x |
calls_combine_by(calls, operator = "&") |
160 |
} |
|
161 |
) |
|
162 | 36x |
filter_items <- Filter( |
163 | 36x |
x = filter_items, |
164 | 36x |
f = Negate(is.null) |
165 |
) |
|
166 | 36x |
if (length(filter_items) > 0L) { |
167 | 35x |
filter_function <- private$fun |
168 | 35x |
data_name <- str2lang(private$dataname_prefixed) |
169 | 35x |
substitute( |
170 | 35x |
env = list( |
171 | 35x |
lhs = data_name, |
172 | 35x |
rhs = as.call(c(filter_function, c(list(data_name), filter_items))) |
173 |
), |
|
174 | 35x |
expr = lhs <- rhs |
175 |
) |
|
176 |
} else { |
|
177 |
# return NULL to avoid no-op call |
|
178 | 1x |
NULL |
179 |
} |
|
180 |
}, |
|
181 | ||
182 |
#' @description |
|
183 |
#' Prints this `FilterStates` object. |
|
184 |
#' |
|
185 |
#' @param ... additional arguments passed to `format`. |
|
186 |
print = function(...) { |
|
187 | ! |
cat(isolate(self$format(...)), "\n") |
188 |
}, |
|
189 | ||
190 |
#' @description |
|
191 |
#' Remove one or more `FilterState`s from the `state_list` along with their UI elements. |
|
192 |
#' |
|
193 |
#' @param state (`teal_slices`) |
|
194 |
#' specifying `FilterState` objects to remove; |
|
195 |
#' `teal_slice`s may contain only `dataname` and `varname`, other elements are ignored |
|
196 |
#' |
|
197 |
#' @return `NULL`, invisibly. |
|
198 |
#' |
|
199 |
remove_filter_state = function(state) { |
|
200 | 17x |
checkmate::assert_class(state, "teal_slices") |
201 | 17x |
isolate({ |
202 | 17x |
state_ids <- vapply(state, `[[`, character(1), "id") |
203 | 17x |
logger::log_debug("{ class(self)[1] }$remove_filter_state removing filters, state_id: { toString(state_ids) }") |
204 | 17x |
private$state_list_remove(state_ids) |
205 |
}) |
|
206 | 17x |
invisible(NULL) |
207 |
}, |
|
208 | ||
209 |
#' @description |
|
210 |
#' Gets reactive values from active `FilterState` objects. |
|
211 |
#' |
|
212 |
#' Get active filter state from `FilterState` objects stored in `state_list`(s). |
|
213 |
#' The output is a list compatible with input to `self$set_filter_state`. |
|
214 |
#' |
|
215 |
#' @return Object of class `teal_slices`. |
|
216 |
#' |
|
217 |
get_filter_state = function() { |
|
218 | 306x |
slices <- unname(lapply(private$state_list(), function(x) x$get_state())) |
219 | 306x |
fs <- do.call(teal_slices, c(slices, list(count_type = private$count_type))) |
220 | ||
221 | 306x |
include_varnames <- private$include_varnames |
222 | 306x |
if (length(include_varnames)) { |
223 | 180x |
attr(fs, "include_varnames") <- structure( |
224 | 180x |
list(include_varnames), |
225 | 180x |
names = private$dataname |
226 |
) |
|
227 |
} |
|
228 | ||
229 | 306x |
exclude_varnames <- private$exclude_varnames |
230 | 306x |
if (length(exclude_varnames)) { |
231 | 9x |
attr(fs, "exclude_varnames") <- structure( |
232 | 9x |
list(exclude_varnames), |
233 | 9x |
names = private$dataname |
234 |
) |
|
235 |
} |
|
236 | ||
237 | 306x |
fs |
238 |
}, |
|
239 | ||
240 |
#' @description |
|
241 |
#' Sets active `FilterState` objects. |
|
242 |
#' @param state (`teal_slices`) |
|
243 |
#' @return Function that raises an error. |
|
244 |
set_filter_state = function(state) { |
|
245 | 127x |
isolate({ |
246 | 127x |
logger::log_debug("{ class(self)[1] }$set_filter_state initializing, dataname: { private$dataname }") |
247 | 127x |
checkmate::assert_class(state, "teal_slices") |
248 | 127x |
lapply(state, function(x) { |
249 | 170x |
checkmate::assert_true( |
250 | 170x |
x$dataname == private$dataname, |
251 | 170x |
.var.name = "dataname matches private$dataname" |
252 |
) |
|
253 |
}) |
|
254 | ||
255 | 127x |
private$set_filterable_varnames( |
256 | 127x |
include_varnames = attr(state, "include_varnames")[[private$dataname]], |
257 | 127x |
exclude_varnames = attr(state, "exclude_varnames")[[private$dataname]] |
258 |
) |
|
259 | 127x |
count_type <- attr(state, "count_type") |
260 | 127x |
if (length(count_type)) { |
261 | 21x |
private$count_type <- count_type |
262 |
} |
|
263 | ||
264 |
# Drop teal_slices that refer to excluded variables. |
|
265 | 127x |
varnames <- unique(unlist(lapply(state, "[[", "varname"))) |
266 | 127x |
excluded_varnames <- setdiff(varnames, private$get_filterable_varnames()) |
267 | 127x |
if (length(excluded_varnames)) { |
268 | 3x |
state <- Filter(function(x) !isTRUE(x$varname %in% excluded_varnames), state) |
269 | 3x |
warning(sprintf("filters for columns: %s excluded from %s", toString(excluded_varnames), private$dataname)) |
270 |
} |
|
271 | ||
272 | 127x |
if (length(state) > 0) { |
273 | 90x |
private$set_filter_state_impl( |
274 | 90x |
state = state, |
275 | 90x |
data = private$data, |
276 | 90x |
data_reactive = private$data_reactive |
277 |
) |
|
278 |
} |
|
279 |
}) |
|
280 | ||
281 | 127x |
invisible(NULL) |
282 |
}, |
|
283 | ||
284 |
#' @description |
|
285 |
#' Remove all `FilterState` objects from this `FilterStates` object. |
|
286 |
#' |
|
287 |
#' @param force (`logical(1)`) |
|
288 |
#' flag specifying whether to include anchored filter states. |
|
289 |
#' |
|
290 |
#' @return `NULL`, invisibly. |
|
291 |
#' |
|
292 |
clear_filter_states = function(force = FALSE) { |
|
293 | 25x |
private$state_list_empty(force) |
294 | 25x |
invisible(NULL) |
295 |
}, |
|
296 | ||
297 |
# shiny modules ---- |
|
298 | ||
299 |
#' @description |
|
300 |
#' `shiny` UI definition that stores `FilterState` UI elements. |
|
301 |
#' Populated with elements created with `renderUI` in the module server. |
|
302 |
#' |
|
303 |
#' @param id (`character(1)`) |
|
304 |
#' `shiny` module instance id. |
|
305 |
#' |
|
306 |
#' @return `shiny.tag` |
|
307 |
#' |
|
308 |
ui_active = function(id) { |
|
309 | ! |
ns <- NS(id) |
310 | ! |
tagList( |
311 | ! |
include_css_files(pattern = "filter-panel"), |
312 | ! |
uiOutput(ns("trigger_visible_state_change"), inline = TRUE), |
313 | ! |
uiOutput( |
314 | ! |
ns("cards"), |
315 | ! |
class = "accordion", |
316 | ! |
`data-label` = ifelse(length(private$datalabel), paste0("> ", private$datalabel), ""), |
317 |
) |
|
318 |
) |
|
319 |
}, |
|
320 | ||
321 |
#' @description |
|
322 |
#' `shiny` server module. |
|
323 |
#' |
|
324 |
#' @param id (`character(1)`) |
|
325 |
#' `shiny` module instance id. |
|
326 |
#' |
|
327 |
#' @return `NULL`. |
|
328 |
#' |
|
329 |
srv_active = function(id) { |
|
330 | 12x |
moduleServer( |
331 | 12x |
id = id, |
332 | 12x |
function(input, output, session) { |
333 | 12x |
logger::log_debug("FilterState$srv_active initializing, dataname: { private$dataname }") |
334 | 12x |
current_state <- reactive(private$state_list_get()) |
335 | 12x |
previous_state <- reactiveVal(NULL) # FilterState list |
336 | 12x |
added_states <- reactiveVal(NULL) # FilterState list |
337 | ||
338 |
# gives a valid shiny ns based on a default slice id |
|
339 | 12x |
fs_to_shiny_ns <- function(x) { |
340 | 24x |
checkmate::assert_multi_class(x, c("FilterState", "FilterStateExpr")) |
341 | 24x |
gsub("[^[:alnum:]]+", "_", get_default_slice_id(x$get_state())) |
342 |
} |
|
343 | ||
344 | 12x |
output$trigger_visible_state_change <- renderUI({ |
345 | 14x |
current_state() |
346 | 14x |
isolate({ |
347 | 14x |
logger::log_debug("FilterStates$srv_active@1 determining added and removed filter states") |
348 |
# Be aware this returns a list because `current_state` is a list and not `teal_slices`. |
|
349 | 14x |
added_states(setdiff_teal_slices(current_state(), previous_state())) |
350 | 14x |
previous_state(current_state()) |
351 | 14x |
NULL |
352 |
}) |
|
353 |
}) |
|
354 | ||
355 | 12x |
output[["cards"]] <- renderUI({ |
356 | 14x |
lapply( |
357 | 14x |
current_state(), # observes only if added/removed |
358 | 14x |
function(state) { |
359 | 12x |
isolate( # isolates when existing state changes |
360 | 12x |
state$ui(id = session$ns(fs_to_shiny_ns(state)), parent_id = session$ns("cards")) |
361 |
) |
|
362 |
} |
|
363 |
) |
|
364 |
}) |
|
365 | ||
366 | 12x |
private$session_bindings[[session$ns("added_states")]] <- observeEvent( |
367 | 12x |
added_states(), # we want to call FilterState module only once when it's added |
368 | 12x |
ignoreNULL = TRUE, |
369 |
{ |
|
370 | 10x |
added_state_names <- vapply(added_states(), function(x) x$get_state()$id, character(1L)) |
371 | 10x |
logger::log_debug("FilterStates$srv_active@2 triggered by added states: { toString(added_state_names) }") |
372 | 10x |
lapply(added_states(), function(state) { |
373 | 12x |
state$server( |
374 | 12x |
id = fs_to_shiny_ns(state), |
375 | 12x |
remove_callback = function() private$state_list_remove(state$get_state()$id) |
376 |
) |
|
377 |
}) |
|
378 | 10x |
added_states(NULL) |
379 |
} |
|
380 |
) |
|
381 | ||
382 | 12x |
NULL |
383 |
} |
|
384 |
) |
|
385 |
}, |
|
386 | ||
387 |
#' @description |
|
388 |
#' `shiny` UI module to add filter variable. |
|
389 |
#' |
|
390 |
#' @param id (`character(1)`) |
|
391 |
#' `shiny` module instance id. |
|
392 |
#' |
|
393 |
#' @return `shiny.tag` |
|
394 |
#' |
|
395 |
ui_add = function(id) { |
|
396 | 1x |
checkmate::assert_string(id) |
397 | 1x |
data <- private$data |
398 | ||
399 | 1x |
ns <- NS(id) |
400 | ||
401 | 1x |
if (ncol(data) == 0) { |
402 | 1x |
tags$div("no sample variables available") |
403 | ! |
} else if (nrow(data) == 0) { |
404 | ! |
tags$div("no samples available") |
405 |
} else { |
|
406 | ! |
uiOutput(ns("add_filter")) |
407 |
} |
|
408 |
}, |
|
409 | ||
410 |
#' @description |
|
411 |
#' `shiny` server module to add filter variable. |
|
412 |
#' |
|
413 |
#' This module controls available choices to select as a filter variable. |
|
414 |
#' Once selected, a variable is removed from available choices. |
|
415 |
#' Removing a filter variable adds it back to available choices. |
|
416 |
#' |
|
417 |
#' @param id (`character(1)`) |
|
418 |
#' `shiny` module instance id. |
|
419 |
#' |
|
420 |
#' @return `NULL`. |
|
421 |
srv_add = function(id) { |
|
422 | 14x |
moduleServer( |
423 | 14x |
id = id, |
424 | 14x |
function(input, output, session) { |
425 | 14x |
logger::log_debug("FilterStates$srv_add initializing, dataname: { private$dataname }") |
426 | ||
427 |
# available choices to display |
|
428 | 14x |
avail_column_choices <- reactive({ |
429 | 17x |
data <- private$data |
430 | 17x |
vars_include <- private$get_filterable_varnames() |
431 | 17x |
active_filter_vars <- unique(unlist(lapply(self$get_filter_state(), "[[", "varname"))) |
432 | 17x |
choices <- setdiff(vars_include, active_filter_vars) |
433 | 17x |
varlabels <- get_varlabels(data) |
434 | ||
435 | 17x |
data_choices_labeled( |
436 | 17x |
data = data, |
437 | 17x |
choices = choices, |
438 | 17x |
varlabels = varlabels, |
439 | 17x |
keys = private$keys |
440 |
) |
|
441 |
}) |
|
442 | ||
443 | 14x |
output$add_filter <- renderUI({ |
444 | 14x |
logger::log_debug( |
445 | 14x |
"FilterStates$srv_add@1 updating available column choices, dataname: { private$dataname }" |
446 |
) |
|
447 | 14x |
if (length(avail_column_choices()) == 0) { |
448 |
# because input UI is not rendered on this condition but shiny still holds latest selected value |
|
449 | ! |
tags$span("No available columns to add.") |
450 |
} else { |
|
451 | 14x |
tags$div( |
452 | 14x |
teal.widgets::optionalSelectInput( |
453 | 14x |
session$ns("var_to_add"), |
454 | 14x |
choices = avail_column_choices(), |
455 | 14x |
selected = NULL, |
456 | 14x |
options = shinyWidgets::pickerOptions( |
457 | 14x |
liveSearch = TRUE, |
458 | 14x |
noneSelectedText = "Select variable to filter" |
459 |
) |
|
460 |
) |
|
461 |
) |
|
462 |
} |
|
463 |
}) |
|
464 | ||
465 | 14x |
private$session_bindings[[session$ns("var_to_add")]] <- observeEvent( |
466 | 14x |
eventExpr = input$var_to_add, |
467 | 14x |
handlerExpr = { |
468 | 3x |
logger::log_debug( |
469 | 3x |
sprintf( |
470 | 3x |
"FilterStates$srv_add@2 adding FilterState of variable %s, dataname: %s", |
471 | 3x |
input$var_to_add, |
472 | 3x |
private$dataname |
473 |
) |
|
474 |
) |
|
475 | 3x |
self$set_filter_state( |
476 | 3x |
teal_slices( |
477 | 3x |
teal_slice(dataname = private$dataname, varname = input$var_to_add) |
478 |
) |
|
479 |
) |
|
480 |
} |
|
481 |
) |
|
482 | ||
483 |
# Extra observer that clears all input values in session |
|
484 | 14x |
private$session_bindings[[session$ns("inputs")]] <- list( |
485 | 14x |
destroy = function() { |
486 | 27x |
if (!session$isEnded()) { |
487 | ! |
lapply(session$ns(names(input)), .subset2(input, "impl")$.values$remove) |
488 |
} |
|
489 |
} |
|
490 |
) |
|
491 | ||
492 | 14x |
NULL |
493 |
} |
|
494 |
) |
|
495 |
}, |
|
496 | ||
497 |
#' @description |
|
498 |
#' Object cleanup. |
|
499 |
#' |
|
500 |
#' - Destroy inputs and observers stored in `private$session_bindings` |
|
501 |
#' - Clean `state_list` |
|
502 |
#' |
|
503 |
#' @return `NULL`, invisibly. |
|
504 |
#' |
|
505 |
finalize = function() { |
|
506 | 567x |
.finalize_session_bindings(self, private) # Remove all inputs and observers |
507 | 567x |
private$state_list_empty(force = TRUE) |
508 | 567x |
isolate(private$state_list(NULL)) |
509 | 567x |
invisible(NULL) |
510 |
} |
|
511 |
), |
|
512 |
private = list( |
|
513 |
# private fields ---- |
|
514 |
count_type = "none", # specifies how observation numbers are displayed in filter cards, |
|
515 |
data = NULL, # data.frame, MAE, SE or matrix |
|
516 |
data_reactive = NULL, # reactive |
|
517 |
datalabel = NULL, # to follow default `experiment = NULL` in `teal_slice` |
|
518 |
dataname = NULL, # because it holds object of class name |
|
519 |
dataname_prefixed = character(0), # name used in call returned from get_call |
|
520 |
exclude_varnames = character(0), # holds column names |
|
521 |
include_varnames = character(0), # holds column names |
|
522 |
extract_type = character(0), # type of the prefix in a subset call (eg. "list": x$var; "matrix": x[["var"]]) |
|
523 |
fun = quote(subset), # function used to generate subset call |
|
524 |
keys = character(0), |
|
525 |
ns = NULL, # shiny ns() |
|
526 |
session_bindings = list(), # inputs and observers |
|
527 |
state_list = NULL, # list of `reactiveVal`s initialized by init methods of child classes, |
|
528 | ||
529 |
# private methods ---- |
|
530 | ||
531 |
# @description |
|
532 |
# Set the allowed filterable variables |
|
533 |
# @param include_varnames (`character`) Names of variables included in filtering. |
|
534 |
# @param exclude_varnames (`character`) Names of variables excluded from filtering. |
|
535 |
# |
|
536 |
# @details When retrieving the filtered variables only |
|
537 |
# those which have filtering supported (i.e. are of the permitted types). |
|
538 |
# Only one from `include_varnames` and `exclude_varnames` can be used in one call. When `exclude_varnames` |
|
539 |
# is called `include_varnames` is cleared - same otherwise. |
|
540 |
# are included. |
|
541 |
# |
|
542 |
# @return `NULL`, invisibly. |
|
543 |
set_filterable_varnames = function(include_varnames = character(0), exclude_varnames = character(0)) { |
|
544 | 272x |
if ((length(include_varnames) + length(exclude_varnames)) == 0L) { |
545 | 105x |
return(invisible(NULL)) |
546 |
} |
|
547 | 167x |
checkmate::assert_character(include_varnames, any.missing = FALSE, min.len = 0L, null.ok = TRUE) |
548 | 167x |
checkmate::assert_character(exclude_varnames, any.missing = FALSE, min.len = 0L, null.ok = TRUE) |
549 | 167x |
if (length(include_varnames) && length(exclude_varnames)) { |
550 | ! |
stop( |
551 | ! |
"`include_varnames` and `exclude_varnames` has been both specified for", |
552 | ! |
private$dataname, |
553 | ! |
". Only one per dataset is allowed.", |
554 |
) |
|
555 |
} |
|
556 | 167x |
supported_vars <- get_supported_filter_varnames(private$data) |
557 | 167x |
if (length(include_varnames)) { |
558 | 153x |
private$include_varnames <- intersect(include_varnames, supported_vars) |
559 | 153x |
private$exclude_varnames <- character(0) |
560 |
} else { |
|
561 | 14x |
private$exclude_varnames <- exclude_varnames |
562 | 14x |
private$include_varnames <- character(0) |
563 |
} |
|
564 | 167x |
invisible(NULL) |
565 |
}, |
|
566 | ||
567 |
# @description |
|
568 |
# Get vector of filterable varnames |
|
569 |
# |
|
570 |
# @details |
|
571 |
# These are the only columns which can be used in the filter panel |
|
572 |
# |
|
573 |
# @return character vector with names of the columns |
|
574 |
get_filterable_varnames = function() { |
|
575 | 144x |
if (length(private$include_varnames)) { |
576 | 97x |
private$include_varnames |
577 |
} else { |
|
578 | 47x |
supported_varnames <- get_supported_filter_varnames(private$data) |
579 | 47x |
setdiff(supported_varnames, private$exclude_varnames) |
580 |
} |
|
581 |
}, |
|
582 | ||
583 |
# state_list methods ---- |
|
584 | ||
585 |
# @description |
|
586 |
# Returns a list of `FilterState` objects stored in this `FilterStates`. |
|
587 |
# |
|
588 |
# @param state_id (`character(1)`) |
|
589 |
# name of element in a filter state (which is a `reactiveVal` containing a list) |
|
590 |
# |
|
591 |
# @return `list` of `FilterState` objects |
|
592 |
# |
|
593 |
state_list_get = function(state_id = NULL) { |
|
594 | 208x |
checkmate::assert_string(state_id, null.ok = TRUE) |
595 | ||
596 | 208x |
if (is.null(state_id)) { |
597 | 208x |
private$state_list() |
598 |
} else { |
|
599 | ! |
private$state_list()[[state_id]] |
600 |
} |
|
601 |
}, |
|
602 | ||
603 |
# @description |
|
604 |
# Adds a new `FilterState` object to this `FilterStates`. |
|
605 |
# Raises error if the length of `x` does not match the length of `state_id`. |
|
606 |
# |
|
607 |
# @param x (`FilterState`) |
|
608 |
# object to be added to filter state list |
|
609 |
# @param state_id (`character(1)`) |
|
610 |
# name of element in a filter state (which is a `reactiveVal` containing a list) |
|
611 |
# |
|
612 |
# @return `NULL`. |
|
613 |
# |
|
614 |
state_list_push = function(x, state_id) { |
|
615 | 175x |
logger::log_debug("{ class(self)[1] } pushing into state_list, dataname: { private$dataname }") |
616 | 175x |
checkmate::assert_string(state_id) |
617 | 175x |
checkmate::assert_multi_class(x, c("FilterState", "FilterStateExpr")) |
618 | 175x |
state <- stats::setNames(list(x), state_id) |
619 | 175x |
new_state_list <- c( |
620 | 175x |
isolate(private$state_list()), |
621 | 175x |
state |
622 |
) |
|
623 | 175x |
isolate(private$state_list(new_state_list)) |
624 | ||
625 | 175x |
logger::log_debug("{ class(self)[1] } pushed into queue, dataname: { private$dataname }") |
626 | 175x |
invisible(NULL) |
627 |
}, |
|
628 | ||
629 |
# @description |
|
630 |
# Removes a single filter state with all associated shiny elements: |
|
631 |
# * specified `FilterState` from `private$state_list` |
|
632 |
# * UI card created for this filter |
|
633 |
# * observers tracking the selection and remove button |
|
634 |
# |
|
635 |
# @param state_id (`character`) |
|
636 |
# identifiers of elements in a filter state (which is a `reactiveVal` containing a list). |
|
637 |
# @param force (`logical(1)`) |
|
638 |
# flag specifying whether to include anchored filter states. |
|
639 |
# |
|
640 |
# @return `NULL`, invisibly. |
|
641 |
# |
|
642 |
state_list_remove = function(state_id, force = FALSE) { |
|
643 | 109x |
checkmate::assert_character(state_id) |
644 | 109x |
logger::log_debug("{ class(self)[1] } removing a filter, state_id: { toString(state_id) }") |
645 | 109x |
isolate({ |
646 | 109x |
current_state_ids <- vapply(private$state_list(), function(x) x$get_state()$id, character(1)) |
647 | 109x |
to_remove <- state_id %in% current_state_ids |
648 | 109x |
if (any(to_remove)) { |
649 | 108x |
new_state_list <- Filter( |
650 | 108x |
function(state) { |
651 | 196x |
if (state$get_state()$id %in% state_id) { |
652 | 182x |
if (state$get_state()$anchored && !force) { |
653 | 7x |
return(TRUE) |
654 |
} else { |
|
655 | 175x |
state$finalize() |
656 | 175x |
FALSE |
657 |
} |
|
658 |
} else { |
|
659 | 14x |
TRUE |
660 |
} |
|
661 |
}, |
|
662 | 108x |
private$state_list() |
663 |
) |
|
664 | 108x |
private$state_list(new_state_list) |
665 |
} else { |
|
666 | 1x |
warning(sprintf("\"%s\" not found in state list", state_id)) |
667 |
} |
|
668 |
}) |
|
669 | ||
670 | 109x |
invisible(NULL) |
671 |
}, |
|
672 | ||
673 |
# @description |
|
674 |
# Remove all `FilterState` objects from this `FilterStates` object. |
|
675 |
# @param force (`logical(1)`) |
|
676 |
# flag specifying whether to include anchored filter states. |
|
677 |
# @return `NULL`, invisibly. |
|
678 |
# |
|
679 |
state_list_empty = function(force = FALSE) { |
|
680 | 592x |
isolate({ |
681 | 592x |
logger::log_debug( |
682 | 592x |
"{ class(self)[1] }$state_list_empty removing all non-anchored filters for dataname: { private$dataname }" |
683 |
) |
|
684 | ||
685 | 592x |
state_list <- private$state_list() |
686 | 592x |
if (length(state_list)) { |
687 | 92x |
state_ids <- vapply(state_list, function(x) x$get_state()$id, character(1)) |
688 | 92x |
private$state_list_remove(state_ids, force) |
689 |
} |
|
690 |
}) |
|
691 | ||
692 | 592x |
invisible(NULL) |
693 |
}, |
|
694 | ||
695 |
# @description |
|
696 |
# Set filter state |
|
697 |
# |
|
698 |
# Utility method for `set_filter_state` to create or modify `FilterState` using a single |
|
699 |
# `teal_slice`. |
|
700 |
# @param state (`teal_slices`) |
|
701 |
# @param data (`data.frame`, `matrix` or `DataFrame`) |
|
702 |
# @param data_reactive (`function`) |
|
703 |
# function having `sid` as argument. |
|
704 |
# |
|
705 |
# @return `NULL`, invisibly. |
|
706 |
# |
|
707 |
set_filter_state_impl = function(state, |
|
708 |
data, |
|
709 |
data_reactive) { |
|
710 | 192x |
checkmate::assert_class(state, "teal_slices") |
711 | 192x |
checkmate::assert_multi_class(data, c("data.frame", "matrix", "DataFrame", "HermesData")) |
712 | 192x |
checkmate::assert_function(data_reactive, args = "sid") |
713 | 192x |
if (length(state) == 0L) { |
714 | 86x |
return(invisible(NULL)) |
715 |
} |
|
716 | ||
717 | 106x |
slices_hashed <- vapply(state, `[[`, character(1L), "id") |
718 | 106x |
if (any(duplicated(slices_hashed))) { |
719 | ! |
stop( |
720 | ! |
"Some of the teal_slice objects refer to the same filter. ", |
721 | ! |
"Please specify different 'id' when calling teal_slice" |
722 |
) |
|
723 |
} |
|
724 | ||
725 | 106x |
state_list <- isolate(private$state_list_get()) |
726 | 106x |
lapply(state, function(slice) { |
727 | 183x |
state_id <- slice$id |
728 | 183x |
if (state_id %in% names(state_list)) { |
729 |
# Modify existing filter states. |
|
730 | 8x |
state_list[[state_id]]$set_state(slice) |
731 |
} else { |
|
732 | 175x |
if (inherits(slice, "teal_slice_expr")) { |
733 |
# create a new FilterStateExpr |
|
734 | 6x |
fstate <- init_filter_state_expr(slice) |
735 |
} else { |
|
736 |
# create a new FilterState |
|
737 | 169x |
fstate <- init_filter_state( |
738 | 169x |
x = data[, slice$varname, drop = TRUE], |
739 |
# data_reactive is a function which eventually calls get_call(sid). |
|
740 |
# This chain of calls returns column from the data filtered by everything |
|
741 |
# but filter identified by the sid argument. FilterState then get x_reactive |
|
742 |
# and this no longer needs to be a function to pass sid. reactive in the FilterState |
|
743 |
# is also beneficial as it can be cached and retriger filter counts only if |
|
744 |
# returned vector is different. |
|
745 | 169x |
x_reactive = if (private$count_type == "none") { |
746 | 163x |
reactive(NULL) |
747 |
} else { |
|
748 | 6x |
reactive(data_reactive(state_id)[, slice$varname, drop = TRUE]) |
749 |
}, |
|
750 | 169x |
slice = slice, |
751 | 169x |
extract_type = private$extract_type |
752 |
) |
|
753 |
} |
|
754 | 175x |
private$state_list_push(x = fstate, state_id = state_id) |
755 |
} |
|
756 |
}) |
|
757 | ||
758 | 106x |
invisible(NULL) |
759 |
} |
|
760 |
) |
|
761 |
) |
1 |
#' Complete filter specification |
|
2 |
#' |
|
3 |
#' Create `teal_slices` object to package multiple filters and additional settings. |
|
4 |
#' Check out [`teal_slices-utilities`] functions for working with `teal_slices` object. |
|
5 |
#' |
|
6 |
#' `teal_slices()` collates multiple `teal_slice` objects into a `teal_slices` object, |
|
7 |
#' a complete filter specification. This is used by all classes above `FilterState` |
|
8 |
#' as well as `filter_panel_api` wrapper functions. |
|
9 |
#' `teal_slices` has attributes that modify the behavior of the filter panel, which are resolved by different classes. |
|
10 |
#' |
|
11 |
#' `include_varnames` and `exclude_varnames` determine which variables can have filters assigned. |
|
12 |
#' The former enumerates allowed variables, the latter enumerates forbidden values. |
|
13 |
#' Since these could be mutually exclusive, it is impossible to set both allowed and forbidden |
|
14 |
#' variables for one data set in one `teal_slices`. |
|
15 |
#' |
|
16 |
#' @param ... any number of `teal_slice` objects. |
|
17 |
#' @param include_varnames,exclude_varnames (`named list`s of `character`) where list names |
|
18 |
#' match names of data sets and vector elements match variable names in respective data sets; |
|
19 |
#' specify which variables are allowed to be filtered; see `Details`. |
|
20 |
#' @param count_type `r lifecycle::badge("experimental")` |
|
21 |
#' _This is a new feature. Do kindly share your opinions on |
|
22 |
#' [`teal.slice`'s GitHub repository](https://github.com/insightsengineering/teal.slice/)._ |
|
23 |
#' |
|
24 |
#' (`character(1)`) string specifying how observations are tallied by these filter states. |
|
25 |
#' Possible options: |
|
26 |
#' - `"none"` (default) to have counts of single `FilterState` to show unfiltered number only. |
|
27 |
#' - `"all"` to have counts of single `FilterState` to show number of observation in filtered |
|
28 |
#' and unfiltered dataset. Note, that issues were reported when using this option with `MultiAssayExperiment`. |
|
29 |
#' Please make sure that adding new filters doesn't fail on target platform before deploying for production. |
|
30 |
#' @param allow_add (`logical(1)`) logical flag specifying whether the user will be able to add new filters |
|
31 |
#' |
|
32 |
#' @return |
|
33 |
#' `teal_slices`, which is an unnamed list of `teal_slice` objects. |
|
34 |
#' |
|
35 |
#' @examples |
|
36 |
#' filter_1 <- teal_slice( |
|
37 |
#' dataname = "dataname1", |
|
38 |
#' varname = "varname1", |
|
39 |
#' choices = letters, |
|
40 |
#' selected = "b", |
|
41 |
#' keep_na = TRUE, |
|
42 |
#' fixed = FALSE, |
|
43 |
#' extra1 = "extraone" |
|
44 |
#' ) |
|
45 |
#' filter_2 <- teal_slice( |
|
46 |
#' dataname = "dataname1", |
|
47 |
#' varname = "varname2", |
|
48 |
#' choices = 1:10, |
|
49 |
#' keep_na = TRUE, |
|
50 |
#' selected = 2, |
|
51 |
#' fixed = TRUE, |
|
52 |
#' anchored = FALSE, |
|
53 |
#' extra2 = "extratwo" |
|
54 |
#' ) |
|
55 |
#' filter_3 <- teal_slice( |
|
56 |
#' dataname = "dataname2", |
|
57 |
#' varname = "varname3", |
|
58 |
#' choices = 1:10 / 10, |
|
59 |
#' keep_na = TRUE, |
|
60 |
#' selected = 0.2, |
|
61 |
#' fixed = TRUE, |
|
62 |
#' anchored = FALSE, |
|
63 |
#' extra1 = "extraone", |
|
64 |
#' extra2 = "extratwo" |
|
65 |
#' ) |
|
66 |
#' |
|
67 |
#' all_filters <- teal_slices( |
|
68 |
#' filter_1, |
|
69 |
#' filter_2, |
|
70 |
#' filter_3, |
|
71 |
#' exclude_varnames = list( |
|
72 |
#' "dataname1" = "varname2" |
|
73 |
#' ) |
|
74 |
#' ) |
|
75 |
#' |
|
76 |
#' is.teal_slices(all_filters) |
|
77 |
#' all_filters[1:2] |
|
78 |
#' c(all_filters[1], all_filters[2]) |
|
79 |
#' print(all_filters) |
|
80 |
#' print(all_filters, trim_lines = FALSE) |
|
81 |
#' |
|
82 |
#' @seealso |
|
83 |
#' - [`teal_slice`] for creating constituent elements of `teal_slices` |
|
84 |
#' - `teal::slices_store` for robust utilities for saving and loading `teal_slices` in `JSON` format |
|
85 |
#' - [`is.teal_slices`], [`as.teal_slices`], [`as.list.teal_slices`], [`[.teal_slices`], [`c.teal_slices`] |
|
86 |
#' [`print.teal_slices`], [`format.teal_slices`] |
|
87 |
#' |
|
88 |
#' @export |
|
89 |
#' |
|
90 |
teal_slices <- function(..., |
|
91 |
exclude_varnames = NULL, |
|
92 |
include_varnames = NULL, |
|
93 |
count_type = NULL, |
|
94 |
allow_add = TRUE) { |
|
95 | 663x |
slices <- list(...) |
96 | 663x |
checkmate::assert_list(slices, types = "teal_slice", any.missing = FALSE) |
97 | 662x |
slices_id <- isolate(vapply(slices, `[[`, character(1L), "id")) |
98 | 662x |
if (any(duplicated(slices_id))) { |
99 | 1x |
stop( |
100 | 1x |
"Some teal_slice objects have the same id:\n", |
101 | 1x |
toString(unique(slices_id[duplicated(slices_id)])) |
102 |
) |
|
103 |
} |
|
104 | 661x |
checkmate::assert_list(exclude_varnames, names = "named", types = "character", null.ok = TRUE, min.len = 1) |
105 | 660x |
checkmate::assert_list(include_varnames, names = "named", types = "character", null.ok = TRUE, min.len = 1) |
106 | 659x |
checkmate::assert_character(count_type, len = 1, null.ok = TRUE) |
107 | 657x |
checkmate::assert_subset(count_type, choices = c("all", "none"), empty.ok = TRUE) |
108 | 656x |
checkmate::assert_logical(allow_add) |
109 | ||
110 | 655x |
duplicated_datasets <- intersect(names(include_varnames), names(exclude_varnames)) |
111 | 655x |
if (length(duplicated_datasets)) { |
112 | 1x |
stop( |
113 | 1x |
"Some datasets are specified in both, include_varnames and exclude_varnames:\n", |
114 | 1x |
toString(duplicated_datasets) |
115 |
) |
|
116 |
} |
|
117 | ||
118 | 654x |
structure( |
119 | 654x |
slices, |
120 | 654x |
exclude_varnames = exclude_varnames, |
121 | 654x |
include_varnames = include_varnames, |
122 | 654x |
count_type = count_type, |
123 | 654x |
allow_add = allow_add, |
124 | 654x |
class = c("teal_slices", class(slices)) |
125 |
) |
|
126 |
} |
|
127 | ||
128 |
#' `teal_slices` utility functions |
|
129 |
#' |
|
130 |
#' Helper functions for working with [`teal_slices`] object. |
|
131 |
#' @param x object to test for `teal_slices`, object to convert to `teal_slices` or a `teal_slices` object |
|
132 |
#' @param i (`character` or `numeric` or `logical`) indicating which elements to extract |
|
133 |
#' @param recursive (`logical(1)`) flag specifying whether to also convert to list the elements of this `teal_slices` |
|
134 |
#' @param ... additional arguments passed to other functions. |
|
135 |
#' @name teal_slices-utilities |
|
136 |
#' @inherit teal_slices examples |
|
137 |
#' @keywords internal |
|
138 | ||
139 |
#' @rdname teal_slices-utilities |
|
140 |
#' @export |
|
141 |
#' |
|
142 |
is.teal_slices <- function(x) { # nolint |
|
143 | 373x |
inherits(x, "teal_slices") |
144 |
} |
|
145 | ||
146 |
#' @rdname teal_slices-utilities |
|
147 |
#' @export |
|
148 |
#' |
|
149 |
as.teal_slices <- function(x) { # nolint |
|
150 | ! |
checkmate::assert_list(x) |
151 | ! |
lapply(x, checkmate::assert_list, names = "named", .var.name = "list element") |
152 | ||
153 | ! |
attrs <- attributes(unclass(x)) |
154 | ! |
ans <- lapply(x, function(x) if (is.teal_slice(x)) x else as.teal_slice(x)) |
155 | ! |
do.call(teal_slices, c(ans, attrs)) |
156 |
} |
|
157 | ||
158 | ||
159 |
#' @rdname teal_slices-utilities |
|
160 |
#' @export |
|
161 |
#' |
|
162 |
as.list.teal_slices <- function(x, recursive = FALSE, ...) { # nolint |
|
163 | 1038x |
ans <- unclass(x) |
164 | 45x |
if (recursive) ans[] <- lapply(ans, as.list) |
165 | 1038x |
ans |
166 |
} |
|
167 | ||
168 | ||
169 |
#' @rdname teal_slices-utilities |
|
170 |
#' @export |
|
171 |
#' |
|
172 |
`[.teal_slices` <- function(x, i) { |
|
173 | 3x |
if (missing(i)) i <- seq_along(x) |
174 | 475x |
if (length(i) == 0L) { |
175 | 162x |
return(x[0]) |
176 |
} |
|
177 | 1x |
if (is.logical(i) && length(i) > length(x)) stop("subscript out of bounds") |
178 | 1x |
if (is.numeric(i) && max(i) > length(x)) stop("subscript out of bounds") |
179 | 311x |
if (is.character(i)) { |
180 | 1x |
if (!all(is.element(i, names(x)))) stop("subscript out of bounds") |
181 | 2x |
i <- which(is.element(i, names(x))) |
182 |
} |
|
183 | ||
184 | 310x |
y <- NextMethod("[") |
185 | 310x |
attrs <- attributes(x) |
186 | 310x |
attrs$names <- attrs$names[i] |
187 | 310x |
attributes(y) <- attrs |
188 | 310x |
y |
189 |
} |
|
190 | ||
191 | ||
192 |
#' @rdname teal_slices-utilities |
|
193 |
#' @export |
|
194 |
#' |
|
195 |
c.teal_slices <- function(...) { |
|
196 | 211x |
x <- list(...) |
197 | 211x |
checkmate::assert_true(all(vapply(x, is.teal_slices, logical(1L))), .var.name = "all arguments are teal_slices") |
198 | ||
199 | 210x |
all_attributes <- lapply(x, attributes) |
200 | 210x |
all_attributes <- coalesce_r(all_attributes) |
201 | 210x |
all_attributes <- all_attributes[names(all_attributes) != "class"] |
202 | ||
203 | 210x |
do.call( |
204 | 210x |
teal_slices, |
205 | 210x |
c( |
206 | 210x |
unique(unlist(x, recursive = FALSE)), |
207 | 210x |
all_attributes |
208 |
) |
|
209 |
) |
|
210 |
} |
|
211 | ||
212 | ||
213 |
#' @rdname teal_slices-utilities |
|
214 |
#' @param show_all (`logical(1)`) whether to display non-null elements of constituent `teal_slice` objects |
|
215 |
#' @param trim_lines (`logical(1)`) whether to trim lines |
|
216 |
#' @export |
|
217 |
#' |
|
218 |
format.teal_slices <- function(x, show_all = FALSE, trim_lines = TRUE, ...) { |
|
219 | 45x |
checkmate::assert_flag(show_all) |
220 | 45x |
checkmate::assert_flag(trim_lines) |
221 | ||
222 | 45x |
x <- as.list(x, recursive = TRUE) |
223 | 45x |
attrs <- attributes(x) |
224 | 45x |
attributes(x) <- NULL |
225 | 45x |
slices_list <- list(slices = x, attributes = attrs) |
226 | 45x |
slices_list <- Filter(Negate(is.null), slices_list) # drop attributes if empty |
227 | ||
228 | 20x |
if (!show_all) slices_list$slices <- lapply(slices_list$slices, function(slice) Filter(Negate(is.null), slice)) |
229 | ||
230 | 45x |
jsonify(slices_list, trim_lines) |
231 |
} |
|
232 | ||
233 |
#' @rdname teal_slices-utilities |
|
234 |
#' @export |
|
235 |
#' |
|
236 |
print.teal_slices <- function(x, ...) { |
|
237 | 2x |
cat(format(x, ...), "\n") |
238 |
} |
|
239 | ||
240 | ||
241 |
#' `setdiff` method for `teal_slices` |
|
242 |
#' |
|
243 |
#' Compare two teal slices objects and return `teal_slices` containing slices present in `x` but not in `y`. |
|
244 |
#' @param x,y (`teal_slices`) |
|
245 |
#' @return `teal_slices` |
|
246 |
#' @keywords internal |
|
247 |
#' |
|
248 |
setdiff_teal_slices <- function(x, y) { |
|
249 | 14x |
Filter( |
250 | 14x |
function(xx) { |
251 | 12x |
!any(vapply(y, function(yy) identical(yy, xx), logical(1))) |
252 |
}, |
|
253 | 14x |
x |
254 |
) |
|
255 |
} |
|
256 | ||
257 |
#' Recursively coalesce list elements. |
|
258 |
#' |
|
259 |
#' Returns first element of list that it not `NULL`, recursively. |
|
260 |
#' |
|
261 |
#' Given a list of atomic vectors, the first non-null element is returned. |
|
262 |
#' Given a list of lists, for all `names` found in all elements of the list |
|
263 |
#' the first non-null element of a given name is returned. |
|
264 |
#' |
|
265 |
#' This function is used internally in `c.teal_slices` to manage `teal_slices` attributes. |
|
266 |
#' |
|
267 |
#' @param x (`list`), either of atomic vectors or of named lists |
|
268 |
#' @return |
|
269 |
#' Either an atomic vector of length 1 or a (potentially nested) list. |
|
270 |
#' |
|
271 |
#' @keywords internal |
|
272 |
#' |
|
273 |
coalesce_r <- function(x) { |
|
274 | 1312x |
checkmate::assert_list(x) |
275 | 1311x |
xnn <- Filter(Negate(is.null), x) |
276 | 1311x |
if (all(vapply(xnn, is.atomic, logical(1L)))) { |
277 | 884x |
return(xnn[[1L]]) |
278 |
} |
|
279 | 427x |
lapply(x, checkmate::assert_list, names = "named", null.ok = TRUE, .var.name = "list element") |
280 | 426x |
all_names <- unique(unlist(lapply(x, names))) |
281 | 426x |
sapply(all_names, function(nm) coalesce_r(lapply(x, `[[`, nm)), simplify = FALSE) |
282 |
} |
1 |
# RangeFilterState ------ |
|
2 | ||
3 |
#' @name RangeFilterState |
|
4 |
#' @docType class |
|
5 |
#' |
|
6 |
#' @title `FilterState` object for numeric data |
|
7 |
#' |
|
8 |
#' @description Manages choosing a numeric range. |
|
9 |
#' |
|
10 |
#' @examples |
|
11 |
#' # use non-exported function from teal.slice |
|
12 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice") |
|
13 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice") |
|
14 |
#' RangeFilterState <- getFromNamespace("RangeFilterState", "teal.slice") |
|
15 |
#' |
|
16 |
#' library(shiny) |
|
17 |
#' |
|
18 |
#' filter_state <- RangeFilterState$new( |
|
19 |
#' x = c(NA, Inf, seq(1:10)), |
|
20 |
#' slice = teal_slice(varname = "x", dataname = "data") |
|
21 |
#' ) |
|
22 |
#' isolate(filter_state$get_call()) |
|
23 |
#' filter_state$set_state( |
|
24 |
#' teal_slice( |
|
25 |
#' dataname = "data", |
|
26 |
#' varname = "x", |
|
27 |
#' selected = c(3L, 8L), |
|
28 |
#' keep_na = TRUE, |
|
29 |
#' keep_inf = TRUE |
|
30 |
#' ) |
|
31 |
#' ) |
|
32 |
#' isolate(filter_state$get_call()) |
|
33 |
#' |
|
34 |
#' # working filter in an app |
|
35 |
#' library(shinyjs) |
|
36 |
#' |
|
37 |
#' data_range <- c(runif(100, 0, 1), NA, Inf) |
|
38 |
#' fs <- RangeFilterState$new( |
|
39 |
#' x = data_range, |
|
40 |
#' slice = teal_slice( |
|
41 |
#' dataname = "data", |
|
42 |
#' varname = "x", |
|
43 |
#' selected = c(0.15, 0.93), |
|
44 |
#' keep_na = TRUE, |
|
45 |
#' keep_inf = TRUE |
|
46 |
#' ) |
|
47 |
#' ) |
|
48 |
#' |
|
49 |
#' ui <- fluidPage( |
|
50 |
#' useShinyjs(), |
|
51 |
#' include_css_files(pattern = "filter-panel"), |
|
52 |
#' include_js_files(pattern = "count-bar-labels"), |
|
53 |
#' column(4, tags$div( |
|
54 |
#' tags$h4("RangeFilterState"), |
|
55 |
#' fs$ui("fs") |
|
56 |
#' )), |
|
57 |
#' column(4, tags$div( |
|
58 |
#' id = "outputs", # div id is needed for toggling the element |
|
59 |
#' tags$h4("Condition (i.e. call)"), # display the condition call generated by this FilterState |
|
60 |
#' textOutput("condition_range"), tags$br(), |
|
61 |
#' tags$h4("Unformatted state"), # display raw filter state |
|
62 |
#' textOutput("unformatted_range"), tags$br(), |
|
63 |
#' tags$h4("Formatted state"), # display human readable filter state |
|
64 |
#' textOutput("formatted_range"), tags$br() |
|
65 |
#' )), |
|
66 |
#' column(4, tags$div( |
|
67 |
#' tags$h4("Programmatic filter control"), |
|
68 |
#' actionButton("button1_range", "set drop NA", width = "100%"), tags$br(), |
|
69 |
#' actionButton("button2_range", "set keep NA", width = "100%"), tags$br(), |
|
70 |
#' actionButton("button3_range", "set drop Inf", width = "100%"), tags$br(), |
|
71 |
#' actionButton("button4_range", "set keep Inf", width = "100%"), tags$br(), |
|
72 |
#' actionButton("button5_range", "set a range", width = "100%"), tags$br(), |
|
73 |
#' actionButton("button6_range", "set full range", width = "100%"), tags$br(), |
|
74 |
#' actionButton("button0_range", "set initial state", width = "100%"), tags$br() |
|
75 |
#' )) |
|
76 |
#' ) |
|
77 |
#' |
|
78 |
#' server <- function(input, output, session) { |
|
79 |
#' fs$server("fs") |
|
80 |
#' output$condition_range <- renderPrint(fs$get_call()) |
|
81 |
#' output$formatted_range <- renderText(fs$format()) |
|
82 |
#' output$unformatted_range <- renderPrint(fs$get_state()) |
|
83 |
#' # modify filter state programmatically |
|
84 |
#' observeEvent( |
|
85 |
#' input$button1_range, |
|
86 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = FALSE)) |
|
87 |
#' ) |
|
88 |
#' observeEvent( |
|
89 |
#' input$button2_range, |
|
90 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = TRUE)) |
|
91 |
#' ) |
|
92 |
#' observeEvent( |
|
93 |
#' input$button3_range, |
|
94 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_inf = FALSE)) |
|
95 |
#' ) |
|
96 |
#' observeEvent( |
|
97 |
#' input$button4_range, |
|
98 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_inf = TRUE)) |
|
99 |
#' ) |
|
100 |
#' observeEvent( |
|
101 |
#' input$button5_range, |
|
102 |
#' fs$set_state( |
|
103 |
#' teal_slice(dataname = "data", varname = "x", selected = c(0.2, 0.74)) |
|
104 |
#' ) |
|
105 |
#' ) |
|
106 |
#' observeEvent( |
|
107 |
#' input$button6_range, |
|
108 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", selected = c(0, 1))) |
|
109 |
#' ) |
|
110 |
#' observeEvent( |
|
111 |
#' input$button0_range, |
|
112 |
#' fs$set_state( |
|
113 |
#' teal_slice("data", "variable", selected = c(0.15, 0.93), keep_na = TRUE, keep_inf = TRUE) |
|
114 |
#' ) |
|
115 |
#' ) |
|
116 |
#' } |
|
117 |
#' |
|
118 |
#' if (interactive()) { |
|
119 |
#' shinyApp(ui, server) |
|
120 |
#' } |
|
121 |
#' @keywords internal |
|
122 |
#' |
|
123 |
RangeFilterState <- R6::R6Class( # nolint |
|
124 |
"RangeFilterState", |
|
125 |
inherit = FilterState, |
|
126 | ||
127 |
# public methods ---- |
|
128 |
public = list( |
|
129 | ||
130 |
#' @description |
|
131 |
#' Initialize a `FilterState` object for range selection. |
|
132 |
#' @param x (`numeric`) |
|
133 |
#' variable to be filtered. |
|
134 |
#' @param x_reactive (`reactive`) |
|
135 |
#' returning vector of the same type as `x`. Is used to update |
|
136 |
#' counts following the change in values of the filtered dataset. |
|
137 |
#' If it is set to `reactive(NULL)` then counts based on filtered |
|
138 |
#' dataset are not shown. |
|
139 |
#' @param slice (`teal_slice`) |
|
140 |
#' specification of this filter state. |
|
141 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`. |
|
142 |
#' `get_state` returns `teal_slice` object which can be reused in other places. |
|
143 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e. |
|
144 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`. |
|
145 |
#' @param extract_type (`character`) |
|
146 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values: |
|
147 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed |
|
148 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>` |
|
149 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]` |
|
150 |
#' |
|
151 |
#' @return Object of class `RangeFilterState`, invisibly. |
|
152 |
#' |
|
153 |
initialize = function(x, |
|
154 |
x_reactive = reactive(NULL), |
|
155 |
extract_type = character(0), |
|
156 |
slice) { |
|
157 | 118x |
isolate({ |
158 | 118x |
checkmate::assert_numeric(x, all.missing = FALSE) |
159 | 2x |
if (!any(is.finite(x))) stop("\"x\" contains no finite values") |
160 | 115x |
super$initialize(x = x, x_reactive = x_reactive, slice = slice, extract_type = extract_type) |
161 | 115x |
private$is_integer <- checkmate::test_integerish(x) |
162 | 115x |
private$inf_count <- sum(is.infinite(x)) |
163 | 115x |
private$inf_filtered_count <- reactive( |
164 | 115x |
if (!is.null(private$x_reactive())) sum(is.infinite(private$x_reactive())) |
165 |
) |
|
166 | ||
167 | 115x |
checkmate::assert_numeric(slice$choices, null.ok = TRUE) |
168 | 3x |
if (is.null(slice$keep_inf) && any(is.infinite(x))) slice$keep_inf <- TRUE |
169 | ||
170 | 114x |
private$set_choices(slice$choices) |
171 | 41x |
if (is.null(slice$selected)) slice$selected <- slice$choices |
172 | 114x |
private$set_selected(slice$selected) |
173 | ||
174 | 111x |
private$is_integer <- checkmate::test_integerish(x) |
175 | 111x |
private$inf_filtered_count <- reactive( |
176 | 111x |
if (!is.null(private$x_reactive())) sum(is.infinite(private$x_reactive())) |
177 |
) |
|
178 | 111x |
private$inf_count <- sum(is.infinite(x)) |
179 | ||
180 | 111x |
private$plot_data <- list( |
181 | 111x |
type = "histogram", |
182 | 111x |
nbinsx = 50, |
183 | 111x |
x = Filter(Negate(is.na), Filter(is.finite, private$x)), |
184 | 111x |
color = I(fetch_bs_color("secondary")), |
185 | 111x |
alpha = 0.2, |
186 | 111x |
bingroup = 1, |
187 | 111x |
showlegend = FALSE, |
188 | 111x |
hoverinfo = "none" |
189 |
) |
|
190 | 111x |
private$plot_mask <- list(list( |
191 | 111x |
type = "rect", fillcolor = rgb(1, 1, 1, .65), line = list(width = 0), |
192 | 111x |
x0 = -0.5, x1 = 1.5, y0 = -0.5, y1 = 1.5, xref = "paper", yref = "paper" |
193 |
)) |
|
194 | 111x |
private$plot_layout <- reactive({ |
195 | 5x |
shapes <- private$get_shape_properties(private$get_selected()) |
196 | 5x |
list( |
197 | 5x |
barmode = "overlay", |
198 | 5x |
xaxis = list( |
199 | 5x |
range = private$get_choices() * c(0.995, 1.005), |
200 | 5x |
rangeslider = list(thickness = 0), |
201 | 5x |
showticklabels = TRUE, |
202 | 5x |
ticks = "outside", |
203 | 5x |
ticklen = 1.5, |
204 | 5x |
tickmode = "auto", |
205 | 5x |
nticks = 10 |
206 |
), |
|
207 | 5x |
yaxis = list(showgrid = FALSE, showticklabels = FALSE), |
208 | 5x |
margin = list(b = 17, l = 0, r = 0, t = 0, autoexpand = FALSE), |
209 | 5x |
plot_bgcolor = "#FFFFFF00", |
210 | 5x |
paper_bgcolor = "#FFFFFF00", |
211 | 5x |
shapes = shapes |
212 |
) |
|
213 |
}) |
|
214 | 111x |
private$plot_config <- reactive({ |
215 | 5x |
list( |
216 | 5x |
doubleClick = "reset", |
217 | 5x |
displayModeBar = FALSE, |
218 | 5x |
edits = list(shapePosition = TRUE) |
219 |
) |
|
220 |
}) |
|
221 | 111x |
private$plot_filtered <- reactive({ |
222 | 5x |
finite_values <- Filter(is.finite, private$x_reactive()) |
223 | 5x |
if (!identical(finite_values, numeric(0))) { |
224 | 5x |
list( |
225 | 5x |
x = finite_values, |
226 | 5x |
bingroup = 1, |
227 | 5x |
color = I(fetch_bs_color("primary")) |
228 |
) |
|
229 |
} |
|
230 |
}) |
|
231 | 111x |
invisible(self) |
232 |
}) |
|
233 |
}, |
|
234 | ||
235 |
#' @description |
|
236 |
#' Returns reproducible condition call for current selection. |
|
237 |
#' For this class returned call looks like |
|
238 |
#' `<varname> >= <min value> & <varname> <= <max value>` with |
|
239 |
#' optional `is.na(<varname>)` and `is.finite(<varname>)`. |
|
240 |
#' @param dataname name of data set; defaults to `private$get_dataname()` |
|
241 |
#' @return `call` |
|
242 |
#' |
|
243 |
get_call = function(dataname) { |
|
244 | 35x |
if (isFALSE(private$is_any_filtered())) { |
245 | 1x |
return(NULL) |
246 |
} |
|
247 | 4x |
if (missing(dataname)) dataname <- private$get_dataname() |
248 | 34x |
varname <- private$get_varname_prefixed(dataname) |
249 | 34x |
filter_call <- |
250 | 34x |
call( |
251 |
"&", |
|
252 | 34x |
call(">=", varname, private$get_selected()[1L]), |
253 | 34x |
call("<=", varname, private$get_selected()[2L]) |
254 |
) |
|
255 | 34x |
private$add_keep_na_call(private$add_keep_inf_call(filter_call, varname), varname) |
256 |
}, |
|
257 | ||
258 |
#' @description |
|
259 |
#' Returns current `keep_inf` selection. |
|
260 |
#' @return `logical(1)` |
|
261 |
get_keep_inf = function() { |
|
262 | ! |
private$teal_slice$keep_inf |
263 |
} |
|
264 |
), |
|
265 | ||
266 |
# private fields---- |
|
267 |
private = list( |
|
268 |
inf_count = integer(0), |
|
269 |
inf_filtered_count = NULL, |
|
270 |
is_integer = logical(0), |
|
271 |
numeric_step = numeric(0), # step for the slider input widget, calculated from input data (x) |
|
272 |
plot_data = NULL, |
|
273 |
plot_mask = list(), |
|
274 |
plot_layout = NULL, |
|
275 |
plot_config = NULL, |
|
276 |
plot_filtered = NULL, |
|
277 | ||
278 |
# private methods ---- |
|
279 | ||
280 |
set_choices = function(choices) { |
|
281 | 114x |
x <- private$x[is.finite(private$x)] |
282 | 114x |
if (is.null(choices)) { |
283 | 102x |
choices <- range(x) |
284 |
} else { |
|
285 | 12x |
choices_adjusted <- c(max(choices[1L], min(x)), min(choices[2L], max(x))) |
286 | 12x |
if (any(choices != choices_adjusted)) { |
287 | 1x |
warning(sprintf( |
288 | 1x |
"Choices adjusted (some values outside of variable range). Varname: %s, dataname: %s.", |
289 | 1x |
private$get_varname(), private$get_dataname() |
290 |
)) |
|
291 | 1x |
choices <- choices_adjusted |
292 |
} |
|
293 | 12x |
if (choices[1L] > choices[2L]) { |
294 | 1x |
warning(sprintf( |
295 | 1x |
"Invalid choices: lower is higher / equal to upper, or not in range of variable values. |
296 | 1x |
Setting defaults. Varname: %s, dataname: %s.", |
297 | 1x |
private$get_varname(), private$get_dataname() |
298 |
)) |
|
299 | 1x |
choices <- range(x) |
300 |
} |
|
301 |
} |
|
302 | ||
303 | 114x |
private$set_is_choice_limited(private$x, choices) |
304 | 114x |
private$x <- private$x[ |
305 | 114x |
(private$x >= choices[1L] & private$x <= choices[2L]) | is.na(private$x) | !is.finite(private$x) |
306 |
] |
|
307 | ||
308 | 114x |
x_range <- range(private$x, finite = TRUE) |
309 | ||
310 |
# Required for displaying ticks on the slider, can modify choices! |
|
311 | 114x |
if (identical(diff(x_range), 0)) { |
312 | 2x |
choices <- x_range |
313 |
} else { |
|
314 | 112x |
x_pretty <- pretty(x_range, 100L) |
315 | 112x |
choices <- range(x_pretty) |
316 | 112x |
private$numeric_step <- signif(private$get_pretty_range_step(x_pretty), digits = 10) |
317 |
} |
|
318 | 114x |
private$teal_slice$choices <- choices |
319 | 114x |
invisible(NULL) |
320 |
}, |
|
321 | ||
322 |
# @description |
|
323 |
# Check whether the initial choices filter out some values of x and set the flag in case. |
|
324 |
set_is_choice_limited = function(xl, choices) { |
|
325 | 114x |
xl <- xl[!is.na(xl)] |
326 | 114x |
xl <- xl[is.finite(xl)] |
327 | 114x |
private$is_choice_limited <- (any(xl < choices[1L]) | any(xl > choices[2L])) |
328 | 114x |
invisible(NULL) |
329 |
}, |
|
330 | ||
331 |
# Adds is.infinite(varname) before existing condition calls if keep_inf is selected |
|
332 |
# returns a call |
|
333 |
add_keep_inf_call = function(filter_call, varname) { |
|
334 | 34x |
if (isTRUE(private$get_keep_inf())) { |
335 | 2x |
call("|", call("is.infinite", varname), filter_call) |
336 |
} else { |
|
337 | 32x |
filter_call |
338 |
} |
|
339 |
}, |
|
340 | ||
341 |
# @description gets pretty step size for range slider |
|
342 |
# adaptation of shiny's method (see shiny/R/input-slider.R function findStepSize) |
|
343 |
# @param pretty_range (numeric(n)) vector of pretty values |
|
344 |
# @return numeric(1) pretty step size for the sliderInput |
|
345 |
get_pretty_range_step = function(pretty_range) { |
|
346 | 114x |
if (private$is_integer && diff(range(pretty_range) > 2)) { |
347 | 46x |
return(1L) |
348 |
} else { |
|
349 | 68x |
n_steps <- length(pretty_range) - 1 |
350 | 68x |
return(signif(digits = 10, (max(pretty_range) - min(pretty_range)) / n_steps)) |
351 |
} |
|
352 |
}, |
|
353 |
cast_and_validate = function(values) { |
|
354 | 130x |
tryCatch( |
355 | 130x |
expr = { |
356 | 130x |
values <- as.numeric(values) |
357 | 4x |
if (anyNA(values)) stop() |
358 | 126x |
values |
359 |
}, |
|
360 | 130x |
error = function(e) stop("Vector of set values must contain values coercible to numeric") |
361 |
) |
|
362 |
}, |
|
363 |
# Also validates that selection is sorted. |
|
364 |
check_length = function(values) { |
|
365 | 2x |
if (length(values) != 2L) stop("Vector of set values must have length two.") |
366 | 2x |
if (values[1L] > values[2L]) stop("Vector of set values must be sorted.") |
367 | 122x |
values |
368 |
}, |
|
369 |
# Trim selection to limits imposed by private$get_choices() |
|
370 |
remove_out_of_bounds_values = function(values) { |
|
371 | 2x |
if (values[1L] < private$get_choices()[1L]) values[1L] <- private$get_choices()[1L] |
372 | 2x |
if (values[2L] > private$get_choices()[2L]) values[2L] <- private$get_choices()[2L] |
373 | 122x |
values |
374 |
}, |
|
375 | ||
376 |
# Answers the question of whether the current settings and values selected actually filters out any values. |
|
377 |
# @return logical scalar |
|
378 |
is_any_filtered = function() { |
|
379 | 35x |
if (private$is_choice_limited) { |
380 | 1x |
TRUE |
381 | 34x |
} else if (!isTRUE(all.equal(private$get_selected(), private$get_choices()))) { |
382 | 32x |
TRUE |
383 | 2x |
} else if (!isTRUE(private$get_keep_inf()) && private$inf_count > 0) { |
384 | ! |
TRUE |
385 | 2x |
} else if (!isTRUE(private$get_keep_na()) && private$na_count > 0) { |
386 | 1x |
TRUE |
387 |
} else { |
|
388 | 1x |
FALSE |
389 |
} |
|
390 |
}, |
|
391 | ||
392 |
# obtain shape determination for histogram |
|
393 |
# returns a list that is passed to plotly's layout.shapes property |
|
394 |
get_shape_properties = function(values) { |
|
395 | 5x |
list( |
396 | 5x |
list(type = "line", x0 = values[1], x1 = values[1], y0 = -100, y1 = 100, yref = "paper"), |
397 | 5x |
list(type = "line", x0 = values[2], x1 = values[2], y0 = -100, y1 = 100, yref = "paper") |
398 |
) |
|
399 |
}, |
|
400 | ||
401 |
# shiny modules ---- |
|
402 | ||
403 |
# UI Module for `RangeFilterState`. |
|
404 |
# This UI element contains two values for `min` and `max` |
|
405 |
# of the range and two checkboxes whether to keep the `NA` or `Inf` values. |
|
406 |
# @param id (`character(1)`) `shiny` module instance id. |
|
407 |
ui_inputs = function(id) { |
|
408 | 5x |
ns <- NS(id) |
409 | 5x |
isolate({ |
410 | 5x |
ui_input <- shinyWidgets::numericRangeInput( |
411 | 5x |
inputId = ns("selection_manual"), |
412 | 5x |
label = NULL, |
413 | 5x |
min = private$get_choices()[1L], |
414 | 5x |
max = private$get_choices()[2L], |
415 | 5x |
value = private$get_selected(), |
416 | 5x |
step = private$numeric_step, |
417 | 5x |
width = "100%" |
418 |
) |
|
419 | 5x |
tagList( |
420 | 5x |
tags$div( |
421 | 5x |
class = "choices_state", |
422 | 5x |
tags$head(tags$script( |
423 |
# Inline JS code for popover functionality. |
|
424 |
# Adding the script inline because when added from a file with include_js_files(), |
|
425 |
# it only works in the first info_button instance and not others. |
|
426 | 5x |
HTML( |
427 | 5x |
'$(document).ready(function() { |
428 | 5x |
$("[data-toggle=\'popover\']").popover(); |
429 | ||
430 | 5x |
$(document).on("click", function (e) { |
431 | 5x |
if (!$("[data-toggle=\'popover\']").is(e.target) && |
432 | 5x |
$("[data-toggle=\'popover\']").has(e.target).length === 0 && |
433 | 5x |
$(".popover").has(e.target).length === 0) { |
434 | 5x |
$("[data-toggle=\'popover\']").popover("hide"); |
435 |
} |
|
436 |
}); |
|
437 |
});' |
|
438 |
) |
|
439 |
)), |
|
440 | 5x |
tags$div( |
441 | 5x |
actionLink( |
442 | 5x |
ns("plotly_info"), |
443 | 5x |
label = NULL, |
444 | 5x |
icon = icon("question-circle"), |
445 | 5x |
"data-toggle" = "popover", |
446 | 5x |
"data-html" = "true", |
447 | 5x |
"data-placement" = "left", |
448 | 5x |
"data-trigger" = "click", |
449 | 5x |
"data-title" = "Plot actions", |
450 | 5x |
"data-content" = "<p> |
451 | 5x |
Drag vertical lines to set selection.<br> |
452 | 5x |
Drag across plot to zoom in.<br> |
453 | 5x |
Drag axis to pan.<br> |
454 | 5x |
Double click to zoom out." |
455 |
), |
|
456 | 5x |
style = "text-align: right; font-size: 0.7em; margin-bottom: -1em; position: relative; z-index: 9;" |
457 |
), |
|
458 | 5x |
shinycssloaders::withSpinner( |
459 | 5x |
plotly::plotlyOutput(ns("plot"), height = "50px"), |
460 | 5x |
type = 4, |
461 | 5x |
size = 0.25, |
462 | 5x |
hide.ui = FALSE |
463 |
), |
|
464 | 5x |
ui_input |
465 |
), |
|
466 | 5x |
tags$div( |
467 | 5x |
class = "filter-card-body-keep-na-inf", |
468 | 5x |
private$keep_inf_ui(ns("keep_inf")), |
469 | 5x |
private$keep_na_ui(ns("keep_na")) |
470 |
) |
|
471 |
) |
|
472 |
}) |
|
473 |
}, |
|
474 | ||
475 |
# @description |
|
476 |
# Server module |
|
477 |
# @param id (`character(1)`) `shiny` module instance id. |
|
478 |
# return `NULL`. |
|
479 |
server_inputs = function(id) { |
|
480 | 5x |
moduleServer( |
481 | 5x |
id = id, |
482 | 5x |
function(input, output, session) { |
483 | 5x |
logger::log_debug("RangeFilterState$server initializing, id: { private$get_id() }") |
484 | ||
485 |
# Capture manual input with debounce. |
|
486 | 5x |
selection_manual <- debounce(reactive(input$selection_manual), 200) |
487 | ||
488 |
# Prepare for histogram construction. |
|
489 | 5x |
plot_data <- c(private$plot_data, source = session$ns("histogram_plot")) |
490 | ||
491 | 5x |
trigger_event_data <- reactiveVal(NULL) |
492 | ||
493 |
# Display histogram, adding a second trace that contains filtered data. |
|
494 | 5x |
output$plot <- plotly::renderPlotly({ |
495 | 5x |
histogram <- do.call(plotly::plot_ly, plot_data) |
496 | 5x |
histogram <- do.call(plotly::layout, c(list(p = histogram), private$plot_layout())) |
497 | 5x |
histogram <- do.call(plotly::config, c(list(p = histogram), private$plot_config())) |
498 | 5x |
histogram <- do.call(plotly::add_histogram, c(list(p = histogram), private$plot_filtered())) |
499 | 5x |
trigger_event_data(TRUE) |
500 | 5x |
histogram |
501 |
}) |
|
502 | ||
503 | 5x |
relayout_data <- reactive({ |
504 | 3x |
req(trigger_event_data()) |
505 | 3x |
plotly::event_data("plotly_relayout", source = session$ns("histogram_plot")) |
506 |
}) |
|
507 | ||
508 |
# Dragging shapes (lines) on plot updates selection. |
|
509 | 5x |
private$session_bindings[[session$ns("relayout")]] <- observeEvent( |
510 | 5x |
ignoreNULL = FALSE, |
511 | 5x |
ignoreInit = TRUE, |
512 | 5x |
eventExpr = relayout_data(), |
513 | 5x |
handlerExpr = { |
514 | 1x |
logger::log_debug("RangeFilterState$server@1 selection changed, id: { private$get_id() }") |
515 | 1x |
event <- relayout_data() |
516 | 1x |
if (any(grepl("shapes", names(event)))) { |
517 | ! |
line_positions <- private$get_selected() |
518 | ! |
if (any(grepl("shapes[0]", names(event), fixed = TRUE))) { |
519 | ! |
line_positions[1] <- event[["shapes[0].x0"]] |
520 | ! |
} else if (any(grepl("shapes[1]", names(event), fixed = TRUE))) { |
521 | ! |
line_positions[2] <- event[["shapes[1].x0"]] |
522 |
} |
|
523 |
# If one line was dragged past the other, abort action and reset lines. |
|
524 | ! |
if (line_positions[1] > line_positions[2]) { |
525 | ! |
showNotification( |
526 | ! |
"Numeric range start value must be less than end value.", |
527 | ! |
type = "warning" |
528 |
) |
|
529 | ! |
plotly::plotlyProxyInvoke( |
530 | ! |
plotly::plotlyProxy("plot"), |
531 | ! |
"relayout", |
532 | ! |
shapes = private$get_shape_properties(private$get_selected()) |
533 |
) |
|
534 | ! |
return(NULL) |
535 |
} |
|
536 | ||
537 | ! |
private$set_selected(signif(line_positions, digits = 4L)) |
538 |
} |
|
539 |
} |
|
540 |
) |
|
541 | ||
542 |
# Change in selection updates shapes (lines) on plot and numeric input. |
|
543 | 5x |
private$session_bindings[[session$ns("selection_api")]] <- observeEvent( |
544 | 5x |
ignoreNULL = FALSE, |
545 | 5x |
ignoreInit = TRUE, |
546 | 5x |
eventExpr = private$get_selected(), |
547 | 5x |
handlerExpr = { |
548 | ! |
if (!isTRUE(all.equal(private$get_selected(), selection_manual()))) { |
549 | ! |
logger::log_debug("RangeFilterState$server@2 state changed, id: {private$get_id() }") |
550 | ! |
shinyWidgets::updateNumericRangeInput( |
551 | ! |
session = session, |
552 | ! |
inputId = "selection_manual", |
553 | ! |
value = private$get_selected() |
554 |
) |
|
555 |
} |
|
556 |
} |
|
557 |
) |
|
558 | ||
559 |
# Manual input updates selection. |
|
560 | 5x |
private$session_bindings[[session$ns("selection_manual")]] <- observeEvent( |
561 | 5x |
ignoreNULL = FALSE, |
562 | 5x |
ignoreInit = TRUE, |
563 | 5x |
eventExpr = selection_manual(), |
564 | 5x |
handlerExpr = { |
565 | ! |
selection <- selection_manual() |
566 |
# Abort and reset if non-numeric values is entered. |
|
567 | ! |
if (any(is.na(selection))) { |
568 | ! |
showNotification( |
569 | ! |
"Numeric range values must be numbers.", |
570 | ! |
type = "warning" |
571 |
) |
|
572 | ! |
shinyWidgets::updateNumericRangeInput( |
573 | ! |
session = session, |
574 | ! |
inputId = "selection_manual", |
575 | ! |
value = private$get_selected() |
576 |
) |
|
577 | ! |
return(NULL) |
578 |
} |
|
579 | ||
580 |
# Abort and reset if reversed choices are specified. |
|
581 | ! |
if (selection[1] > selection[2]) { |
582 | ! |
showNotification( |
583 | ! |
"Numeric range start value must be less than end value.", |
584 | ! |
type = "warning" |
585 |
) |
|
586 | ! |
shinyWidgets::updateNumericRangeInput( |
587 | ! |
session = session, |
588 | ! |
inputId = "selection_manual", |
589 | ! |
value = private$get_selected() |
590 |
) |
|
591 | ! |
return(NULL) |
592 |
} |
|
593 | ||
594 | ||
595 | ! |
if (!isTRUE(all.equal(selection, private$get_selected()))) { |
596 | ! |
logger::log_debug("RangeFilterState$server@3 manual selection changed, id: { private$get_id() }") |
597 | ! |
private$set_selected(selection) |
598 |
} |
|
599 |
} |
|
600 |
) |
|
601 | ||
602 | 5x |
private$keep_inf_srv("keep_inf") |
603 | 5x |
private$keep_na_srv("keep_na") |
604 | ||
605 | 5x |
NULL |
606 |
} |
|
607 |
) |
|
608 |
}, |
|
609 |
server_inputs_fixed = function(id) { |
|
610 | ! |
moduleServer( |
611 | ! |
id = id, |
612 | ! |
function(input, output, session) { |
613 | ! |
logger::log_debug("RangeFilterState$server initializing, id: { private$get_id() }") |
614 | ||
615 | ! |
plot_config <- private$plot_config() |
616 | ! |
plot_config$staticPlot <- TRUE |
617 | ||
618 | ! |
output$plot <- plotly::renderPlotly({ |
619 | ! |
histogram <- do.call(plotly::plot_ly, private$plot_data) |
620 | ! |
histogram <- do.call(plotly::layout, c(list(p = histogram), private$plot_layout())) |
621 | ! |
histogram <- do.call(plotly::config, c(list(p = histogram), plot_config)) |
622 | ! |
histogram <- do.call(plotly::add_histogram, c(list(p = histogram), private$plot_filtered())) |
623 | ! |
histogram |
624 |
}) |
|
625 | ||
626 | ! |
output$selection <- renderUI({ |
627 | ! |
shinycssloaders::withSpinner( |
628 | ! |
plotly::plotlyOutput(session$ns("plot"), height = "50px"), |
629 | ! |
type = 4, |
630 | ! |
size = 0.25 |
631 |
) |
|
632 |
}) |
|
633 | ||
634 | ! |
NULL |
635 |
} |
|
636 |
) |
|
637 |
}, |
|
638 | ||
639 |
# @description |
|
640 |
# Server module to display filter summary |
|
641 |
# renders text describing selected range and |
|
642 |
# if NA or Inf are included also |
|
643 |
# @return `shiny.tag` to include in the `ui_summary` |
|
644 |
content_summary = function() { |
|
645 | 5x |
selection <- private$get_selected() |
646 | 5x |
tagList( |
647 | 5x |
tags$span(HTML(selection[1], "–", selection[2]), class = "filter-card-summary-value"), |
648 | 5x |
tags$span( |
649 | 5x |
class = "filter-card-summary-controls", |
650 | 5x |
if (private$na_count > 0) { |
651 | ! |
tags$span("NA", if (isTRUE(private$get_keep_na())) icon("check") else icon("xmark")) |
652 |
}, |
|
653 | 5x |
if (private$inf_count > 0) { |
654 | ! |
tags$span("Inf", if (isTRUE(private$get_keep_inf())) icon("check") else icon("xmark")) |
655 |
} |
|
656 |
) |
|
657 |
) |
|
658 |
}, |
|
659 | ||
660 |
# @description |
|
661 |
# Module displaying input to keep or remove NA in the `FilterState` call. |
|
662 |
# Renders a checkbox input only when variable with which the `FilterState` has been created contains Infs. |
|
663 |
# @param id (`character(1)`) `shiny` module instance id. |
|
664 |
keep_inf_ui = function(id) { |
|
665 | 5x |
ns <- NS(id) |
666 | ||
667 | 5x |
if (private$inf_count > 0) { |
668 | ! |
countmax <- private$na_count |
669 | ! |
countnow <- isolate(private$filtered_na_count()) |
670 | ! |
ui_input <- checkboxInput( |
671 | ! |
inputId = ns("value"), |
672 | ! |
label = tags$span( |
673 | ! |
id = ns("count_label"), |
674 | ! |
make_count_text( |
675 | ! |
label = "Keep Inf", |
676 | ! |
countmax = countmax, |
677 | ! |
countnow = countnow |
678 |
) |
|
679 |
), |
|
680 | ! |
value = isolate(private$get_keep_inf()) |
681 |
) |
|
682 | ! |
tags$div( |
683 | ! |
uiOutput(ns("trigger_visible"), inline = TRUE), |
684 | ! |
ui_input |
685 |
) |
|
686 |
} else { |
|
687 | 5x |
NULL |
688 |
} |
|
689 |
}, |
|
690 | ||
691 |
# @description |
|
692 |
# Module to handle Inf values in the FilterState |
|
693 |
# Sets `private$slice$keep_inf` according to the selection |
|
694 |
# and updates the relevant UI element if `private$slice$keep_inf` has been changed by the api. |
|
695 |
# @param id (`character(1)`) `shiny` module instance id. |
|
696 |
# @return `NULL`. |
|
697 |
keep_inf_srv = function(id) { |
|
698 | 5x |
moduleServer(id, function(input, output, session) { |
699 |
# 1. renderUI is used here as an observer which triggers only if output is visible |
|
700 |
# and if the reactive changes - reactive triggers only if the output is visible. |
|
701 |
# 2. We want to trigger change of the labels only if reactive count changes (not underlying data) |
|
702 | 5x |
output$trigger_visible <- renderUI({ |
703 | 5x |
updateCountText( |
704 | 5x |
inputId = "count_label", |
705 | 5x |
label = "Keep Inf", |
706 | 5x |
countmax = private$inf_count, |
707 | 5x |
countnow = private$inf_filtered_count() |
708 |
) |
|
709 | 5x |
NULL |
710 |
}) |
|
711 | ||
712 |
# this observer is needed in the situation when private$teal_slice$keep_inf has been |
|
713 |
# changed directly by the api - then it's needed to rerender UI element |
|
714 |
# to show relevant values |
|
715 | 5x |
private$session_bindings[[session$ns("keep_inf_api")]] <- observeEvent( |
716 | 5x |
ignoreNULL = TRUE, # its not possible for range that NULL is selected |
717 | 5x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
718 | 5x |
eventExpr = private$get_keep_inf(), |
719 | 5x |
handlerExpr = { |
720 | ! |
if (!setequal(private$get_keep_inf(), input$value)) { |
721 | ! |
logger::log_debug("RangeFilterState$keep_inf_srv@1 changed reactive value, id: { private$get_id() }") |
722 | ! |
updateCheckboxInput( |
723 | ! |
inputId = "value", |
724 | ! |
value = private$get_keep_inf() |
725 |
) |
|
726 |
} |
|
727 |
} |
|
728 |
) |
|
729 | ||
730 | 5x |
private$session_bindings[[session$ns("keep_inf")]] <- observeEvent( |
731 | 5x |
ignoreNULL = TRUE, # it's not possible for range that NULL is selected |
732 | 5x |
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state |
733 | 5x |
eventExpr = input$value, |
734 | 5x |
handlerExpr = { |
735 | ! |
logger::log_debug("FilterState$keep_na_srv@2 changed input, id: { private$get_id() }") |
736 | ! |
keep_inf <- input$value |
737 | ! |
private$set_keep_inf(keep_inf) |
738 |
} |
|
739 |
) |
|
740 | ||
741 | 5x |
invisible(NULL) |
742 |
}) |
|
743 |
} |
|
744 |
) |
|
745 |
) |
1 |
# LogicalFilterState ------ |
|
2 | ||
3 |
#' @name LogicalFilterState |
|
4 |
#' @docType class |
|
5 |
#' |
|
6 |
#' @title `FilterState` object for logical data |
|
7 |
#' |
|
8 |
#' @description Manages choosing a logical state. |
|
9 |
#' |
|
10 |
#' @examples |
|
11 |
#' # use non-exported function from teal.slice |
|
12 |
#' include_css_files <- getFromNamespace("include_css_files", "teal.slice") |
|
13 |
#' include_js_files <- getFromNamespace("include_js_files", "teal.slice") |
|
14 |
#' LogicalFilterState <- getFromNamespace("LogicalFilterState", "teal.slice") |
|
15 |
#' |
|
16 |
#' library(shiny) |
|
17 |
#' |
|
18 |
#' filter_state <- LogicalFilterState$new( |
|
19 |
#' x = sample(c(TRUE, FALSE, NA), 10, replace = TRUE), |
|
20 |
#' slice = teal_slice(varname = "x", dataname = "data") |
|
21 |
#' ) |
|
22 |
#' isolate(filter_state$get_call()) |
|
23 |
#' filter_state$set_state( |
|
24 |
#' teal_slice(dataname = "data", varname = "x", selected = TRUE, keep_na = TRUE) |
|
25 |
#' ) |
|
26 |
#' isolate(filter_state$get_call()) |
|
27 |
#' |
|
28 |
#' # working filter in an app |
|
29 |
#' library(shinyjs) |
|
30 |
#' |
|
31 |
#' data_logical <- c(sample(c(TRUE, FALSE), 10, replace = TRUE), NA) |
|
32 |
#' fs <- LogicalFilterState$new( |
|
33 |
#' x = data_logical, |
|
34 |
#' slice = teal_slice(dataname = "data", varname = "x", selected = FALSE, keep_na = TRUE) |
|
35 |
#' ) |
|
36 |
#' |
|
37 |
#' ui <- fluidPage( |
|
38 |
#' useShinyjs(), |
|
39 |
#' include_css_files(pattern = "filter-panel"), |
|
40 |
#' include_js_files(pattern = "count-bar-labels"), |
|
41 |
#' column(4, tags$div( |
|
42 |
#' tags$h4("LogicalFilterState"), |
|
43 |
#' fs$ui("fs") |
|
44 |
#' )), |
|
45 |
#' column(4, tags$div( |
|
46 |
#' id = "outputs", # div id is needed for toggling the element |
|
47 |
#' tags$h4("Condition (i.e. call)"), # display the condition call generated by this FilterState |
|
48 |
#' textOutput("condition_logical"), tags$br(), |
|
49 |
#' tags$h4("Unformatted state"), # display raw filter state |
|
50 |
#' textOutput("unformatted_logical"), tags$br(), |
|
51 |
#' tags$h4("Formatted state"), # display human readable filter state |
|
52 |
#' textOutput("formatted_logical"), tags$br() |
|
53 |
#' )), |
|
54 |
#' column(4, tags$div( |
|
55 |
#' tags$h4("Programmatic filter control"), |
|
56 |
#' actionButton("button1_logical", "set drop NA", width = "100%"), tags$br(), |
|
57 |
#' actionButton("button2_logical", "set keep NA", width = "100%"), tags$br(), |
|
58 |
#' actionButton("button3_logical", "set a selection", width = "100%"), tags$br(), |
|
59 |
#' actionButton("button0_logical", "set initial state", width = "100%"), tags$br() |
|
60 |
#' )) |
|
61 |
#' ) |
|
62 |
#' |
|
63 |
#' server <- function(input, output, session) { |
|
64 |
#' fs$server("fs") |
|
65 |
#' output$condition_logical <- renderPrint(fs$get_call()) |
|
66 |
#' output$formatted_logical <- renderText(fs$format()) |
|
67 |
#' output$unformatted_logical <- renderPrint(fs$get_state()) |
|
68 |
#' # modify filter state programmatically |
|
69 |
#' observeEvent( |
|
70 |
#' input$button1_logical, |
|
71 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = FALSE)) |
|
72 |
#' ) |
|
73 |
#' observeEvent( |
|
74 |
#' input$button2_logical, |
|
75 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", keep_na = TRUE)) |
|
76 |
#' ) |
|
77 |
#' observeEvent( |
|
78 |
#' input$button3_logical, |
|
79 |
#' fs$set_state(teal_slice(dataname = "data", varname = "x", selected = TRUE)) |
|
80 |
#' ) |
|
81 |
#' observeEvent( |
|
82 |
#' input$button0_logical, |
|
83 |
#' fs$set_state( |
|
84 |
#' teal_slice(dataname = "data", varname = "x", selected = FALSE, keep_na = TRUE) |
|
85 |
#' ) |
|
86 |
#' ) |
|
87 |
#' } |
|
88 |
#' |
|
89 |
#' if (interactive()) { |
|
90 |
#' shinyApp(ui, server) |
|
91 |
#' } |
|
92 |
#' |
|
93 |
#' @keywords internal |
|
94 |
#' |
|
95 |
LogicalFilterState <- R6::R6Class( # nolint |
|
96 |
"LogicalFilterState", |
|
97 |
inherit = FilterState, |
|
98 | ||
99 |
# public methods ---- |
|
100 |
public = list( |
|
101 | ||
102 |
#' @description |
|
103 |
#' Initialize a `FilterState` object. |
|
104 |
#' |
|
105 |
#' @param x (`logical`) |
|
106 |
#' variable to be filtered. |
|
107 |
#' @param x_reactive (`reactive`) |
|
108 |
#' returning vector of the same type as `x`. Is used to update |
|
109 |
#' counts following the change in values of the filtered dataset. |
|
110 |
#' If it is set to `reactive(NULL)` then counts based on filtered |
|
111 |
#' dataset are not shown. |
|
112 |
#' @param slice (`teal_slice`) |
|
113 |
#' specification of this filter state. |
|
114 |
#' `teal_slice` is stored in the object and `set_state` directly manipulates values within `teal_slice`. |
|
115 |
#' `get_state` returns `teal_slice` object which can be reused in other places. |
|
116 |
#' Note that `teal_slice` is a `reactiveValues`, which means it has reference semantics, i.e. |
|
117 |
#' changes made to an object are automatically reflected in all places that refer to the same `teal_slice`. |
|
118 |
#' @param extract_type (`character`) |
|
119 |
#' specifying whether condition calls should be prefixed by `dataname`. Possible values: |
|
120 |
#' - `character(0)` (default) `varname` in the condition call will not be prefixed |
|
121 |
#' - `"list"` `varname` in the condition call will be returned as `<dataname>$<varname>` |
|
122 |
#' - `"matrix"` `varname` in the condition call will be returned as `<dataname>[, <varname>]` |
|
123 |
#' |
|
124 |
#' @return Object of class `LogicalFilterState`, invisibly. |
|
125 |
#' |
|
126 |
initialize = function(x, |
|
127 |
x_reactive = reactive(NULL), |
|
128 |
extract_type = character(0), |
|
129 |
slice) { |
|
130 | 16x |
isolate({ |
131 | 16x |
checkmate::assert_logical(x) |
132 | 15x |
checkmate::assert_logical(slice$selected, null.ok = TRUE) |
133 | 14x |
super$initialize(x = x, x_reactive = x_reactive, slice = slice, extract_type = extract_type) |
134 | ||
135 | 14x |
private$set_choices(slice$choices) |
136 | ! |
if (is.null(slice$multiple)) slice$multiple <- FALSE |
137 | 14x |
if (is.null(slice$selected) && slice$multiple) { |
138 | 7x |
slice$selected <- private$get_choices() |
139 | 7x |
} else if (length(slice$selected) != 1 && !slice$multiple) { |
140 | 3x |
slice$selected <- TRUE |
141 |
} |
|
142 | 14x |
private$set_selected(slice$selected) |
143 | 14x |
df <- factor(x, levels = c(TRUE, FALSE)) |
144 | 14x |
tbl <- table(df) |
145 | 14x |
private$set_choices_counts(tbl) |
146 |
}) |
|
147 | 14x |
invisible(self) |
148 |
}, |
|
149 | ||
150 |
#' @description |
|
151 |
#' Returns reproducible condition call for current selection. |
|
152 |
#' For `LogicalFilterState` it's a `!<varname>` or `<varname>` and optionally `is.na(<varname>)` |
|
153 |
#' @param dataname name of data set; defaults to `private$get_dataname()` |
|
154 |
#' @return `call` |
|
155 |
#' |
|
156 |
get_call = function(dataname) { |
|
157 | 6x |
if (isFALSE(private$is_any_filtered())) { |
158 | ! |
return(NULL) |
159 |
} |
|
160 | 4x |
if (missing(dataname)) dataname <- private$get_dataname() |
161 | 6x |
varname <- private$get_varname_prefixed(dataname) |
162 | 6x |
choices <- private$get_selected() |
163 | 6x |
n_choices <- length(choices) |
164 | ||
165 | 6x |
filter_call <- |
166 | 6x |
if (n_choices == 1 && choices) { |
167 | 1x |
varname |
168 | 6x |
} else if (n_choices == 1 && !choices) { |
169 | 4x |
call("!", varname) |
170 |
} else { |
|
171 | 1x |
call("%in%", varname, make_c_call(choices)) |
172 |
} |
|
173 | 6x |
private$add_keep_na_call(filter_call, varname) |
174 |
} |
|
175 |
), |
|
176 | ||
177 |
# private members ---- |
|
178 |
private = list( |
|
179 |
choices_counts = integer(0), |
|
180 | ||
181 |
# private methods ---- |
|
182 |
set_choices = function(choices) { |
|
183 | 14x |
private$teal_slice$choices <- c(TRUE, FALSE) |
184 | 14x |
invisible(NULL) |
185 |
}, |
|
186 |
# @description |
|
187 |
# Sets choices_counts private field |
|
188 |
set_choices_counts = function(choices_counts) { |
|
189 | 14x |
private$choices_counts <- choices_counts |
190 | 14x |
invisible(NULL) |
191 |
}, |
|
192 |
cast_and_validate = function(values) { |
|
193 | 21x |
tryCatch( |
194 | 21x |
expr = { |
195 | 21x |
values <- as.logical(values) |
196 | 1x |
if (anyNA(values)) stop() |
197 | 20x |
values |
198 |
}, |
|
199 | 21x |
error = function(e) stop("Vector of set values must contain values coercible to logical.") |
200 |
) |
|
201 |
}, |
|
202 |
# If multiple forbidden but selected, restores previous selection with warning. |
|
203 |
check_length = function(values) { |
|
204 | 20x |
if (!private$is_multiple() && length(values) > 1) { |
205 | 1x |
warning( |
206 | 1x |
sprintf("Selection: %s is not a vector of length one. ", toString(values, width = 360)), |
207 | 1x |
"Maintaining previous selection." |
208 |
) |
|
209 | 1x |
values <- isolate(private$get_selected()) |
210 |
} |
|
211 | 20x |
values |
212 |
}, |
|
213 | ||
214 |
# Answers the question of whether the current settings and values selected actually filters out any values. |
|
215 |
# @return logical scalar |
|
216 |
is_any_filtered = function() { |
|
217 | 6x |
if (private$is_choice_limited) { |
218 | ! |
TRUE |
219 | 6x |
} else if (all(private$choices_counts > 0)) { |
220 | 6x |
TRUE |
221 |
} else if ( |
|
222 | ! |
setequal(private$get_selected(), private$get_choices()) && |
223 | ! |
!anyNA(private$get_selected(), private$get_choices()) |
224 |
) { |
|
225 | ! |
TRUE |
226 | ! |
} else if (!isTRUE(private$get_keep_na()) && private$na_count > 0) { |
227 | ! |
TRUE |
228 |
} else { |
|
229 | ! |
FALSE |
230 |
} |
|
231 |
}, |
|
232 | ||
233 |
# shiny modules ---- |
|
234 | ||
235 |
# @description |
|
236 |
# UI Module for `EmptyFilterState`. |
|
237 |
# This UI element contains available choices selection and |
|
238 |
# checkbox whether to keep or not keep the `NA` values. |
|
239 |
# @param id (`character(1)`) `shiny` module instance id. |
|
240 |
ui_inputs = function(id) { |
|
241 | ! |
ns <- NS(id) |
242 | ! |
isolate({ |
243 | ! |
countsmax <- private$choices_counts |
244 | ! |
countsnow <- if (!is.null(private$x_reactive())) { |
245 | ! |
unname(table(factor(private$x_reactive(), levels = private$get_choices()))) |
246 |
} else { |
|
247 | ! |
NULL |
248 |
} |
|
249 | ||
250 | ! |
labels <- countBars( |
251 | ! |
inputId = ns("labels"), |
252 | ! |
choices = as.character(private$get_choices()), |
253 | ! |
countsnow = countsnow, |
254 | ! |
countsmax = countsmax |
255 |
) |
|
256 | ! |
ui_input <- if (private$is_multiple()) { |
257 | ! |
checkboxGroupInput( |
258 | ! |
inputId = ns("selection"), |
259 | ! |
label = NULL, |
260 | ! |
selected = isolate(as.character(private$get_selected())), |
261 | ! |
choiceNames = labels, |
262 | ! |
choiceValues = factor(as.character(private$get_choices()), levels = c("TRUE", "FALSE")), |
263 | ! |
width = "100%" |
264 |
) |
|
265 |
} else { |
|
266 | ! |
radioButtons( |
267 | ! |
inputId = ns("selection"), |
268 | ! |
label = NULL, |
269 | ! |
selected = isolate(as.character(private$get_selected())), |
270 | ! |
choiceNames = labels, |
271 | ! |
choiceValues = factor(as.character(private$get_choices()), levels = c("TRUE", "FALSE")), |
272 | ! |
width = "100%" |
273 |
) |
|
274 |
} |
|
275 | ! |
tags$div( |
276 | ! |
tags$div( |
277 | ! |
class = "choices_state", |
278 | ! |
uiOutput(ns("trigger_visible"), inline = TRUE), |
279 | ! |
ui_input |
280 |
), |
|
281 | ! |
private$keep_na_ui(ns("keep_na")) |
282 |
) |
|
283 |
}) |
|
284 |
}, |
|
285 | ||
286 |
# @description |
|
287 |
# Server module |
|
288 |
# @param id (`character(1)`) `shiny` module instance id. |
|
289 |
# @return `NULL`. |
|
290 |
server_inputs = function(id) { |
|
291 | ! |
moduleServer( |
292 | ! |
id = id, |
293 | ! |
function(input, output, session) { |
294 |
# this observer is needed in the situation when teal_slice$selected has been |
|
295 |
# changed directly by the api - then it's needed to rerender UI element |
|
296 |
# to show relevant values |
|
297 | ! |
non_missing_values <- reactive(Filter(Negate(is.na), private$x_reactive())) |
298 | ! |
output$trigger_visible <- renderUI({ |
299 | ! |
logger::log_debug("LogicalFilterState$server@1 updating count labels, id: { private$get_id() }") |
300 | ||
301 | ! |
countsnow <- if (!is.null(private$x_reactive())) { |
302 | ! |
unname(table(factor(non_missing_values(), levels = private$get_choices()))) |
303 |
} else { |
|
304 | ! |
NULL |
305 |
} |
|
306 | ||
307 | ! |
updateCountBars( |
308 | ! |
inputId = "labels", |
309 | ! |
choices = as.character(private$get_choices()), |
310 | ! |
countsmax = private$choices_counts, |
311 | ! |
countsnow = countsnow |
312 |
) |
|
313 | ! |
NULL |
314 |
}) |
|
315 | ||
316 | ! |
private$session_bindings[[session$ns("selected_api")]] <- observeEvent( |
317 | ! |
ignoreNULL = !private$is_multiple(), |
318 | ! |
ignoreInit = TRUE, |
319 | ! |
eventExpr = private$get_selected(), |
320 | ! |
handlerExpr = { |
321 | ! |
if (!setequal(private$get_selected(), input$selection)) { |
322 | ! |
logger::log_debug("LogicalFilterState$server@1 state changed, id: { private$get_id() }") |
323 | ! |
if (private$is_multiple()) { |
324 | ! |
updateCheckboxGroupInput( |
325 | ! |
inputId = "selection", |
326 | ! |
selected = private$get_selected() |
327 |
) |
|
328 |
} else { |
|
329 | ! |
updateRadioButtons( |
330 | ! |
inputId = "selection", |
331 | ! |
selected = private$get_selected() |
332 |
) |
|
333 |
} |
|
334 |
} |
|
335 |
} |
|
336 |
) |
|
337 | ||
338 | ! |
private$session_bindings[[session$ns("selection")]] <- observeEvent( |
339 | ! |
ignoreNULL = FALSE, |
340 | ! |
ignoreInit = TRUE, |
341 | ! |
eventExpr = input$selection, |
342 | ! |
handlerExpr = { |
343 | ! |
logger::log_debug("LogicalFilterState$server@2 selection changed, id: { private$get_id() }") |
344 |
# for private$is_multiple() == TRUE input$selection will always have value |
|
345 | ! |
if (is.null(input$selection) && isFALSE(private$is_multiple())) { |
346 | ! |
selection_state <- private$get_selected() |
347 |
} else { |
|
348 | ! |
selection_state <- as.logical(input$selection) |
349 |
} |
|
350 | ||
351 | ! |
if (is.null(selection_state)) { |
352 | ! |
selection_state <- logical(0) |
353 |
} |
|
354 | ! |
private$set_selected(selection_state) |
355 |
} |
|
356 |
) |
|
357 | ||
358 | ! |
private$keep_na_srv("keep_na") |
359 | ||
360 | ! |
NULL |
361 |
} |
|
362 |
) |
|
363 |
}, |
|
364 |
server_inputs_fixed = function(id) { |
|
365 | ! |
moduleServer( |
366 | ! |
id = id, |
367 | ! |
function(input, output, session) { |
368 | ! |
logger::log_debug("LogicalFilterState$server initializing, id: { private$get_id() }") |
369 | ||
370 | ! |
output$selection <- renderUI({ |
371 | ! |
countsnow <- unname(table(factor(private$x_reactive(), levels = private$get_choices()))) |
372 | ! |
countsmax <- private$choices_counts |
373 | ||
374 | ! |
ind <- private$get_choices() %in% private$get_selected() |
375 | ! |
countBars( |
376 | ! |
inputId = session$ns("labels"), |
377 | ! |
choices = private$get_selected(), |
378 | ! |
countsnow = countsnow[ind], |
379 | ! |
countsmax = countsmax[ind] |
380 |
) |
|
381 |
}) |
|
382 | ||
383 | ! |
NULL |
384 |
} |
|
385 |
) |
|
386 |
}, |
|
387 | ||
388 |
# @description |
|
389 |
# Server module to display filter summary |
|
390 |
# renders text describing whether TRUE or FALSE is selected |
|
391 |
# and if NA are included also |
|
392 |
content_summary = function(id) { |
|
393 | ! |
tagList( |
394 | ! |
tags$span( |
395 | ! |
class = "filter-card-summary-value", |
396 | ! |
toString(private$get_selected()) |
397 |
), |
|
398 | ! |
tags$span( |
399 | ! |
class = "filter-card-summary-controls", |
400 | ! |
if (private$na_count > 0) { |
401 | ! |
tags$span("NA", if (isTRUE(private$get_keep_na())) icon("check") else icon("xmark")) |
402 |
} |
|
403 |
) |
|
404 |
) |
|
405 |
} |
|
406 |
) |
|
407 |
) |
1 |
#' 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 |