1 |
# This file adds a splash screen for delayed data loading on top of teal |
|
2 | ||
3 |
#' UI to show a splash screen in the beginning, then delegate to [srv_teal()] |
|
4 |
#' |
|
5 |
#' @description `r lifecycle::badge("stable")` |
|
6 |
#' The splash screen could be used to query for a password to fetch the data. |
|
7 |
#' [init()] is a very thin wrapper around this module useful for end-users which |
|
8 |
#' assumes that it is a top-level module and cannot be embedded. |
|
9 |
#' This function instead adheres to the Shiny module conventions. |
|
10 |
#' |
|
11 |
#' If data is obtained through delayed loading, its splash screen is used. Otherwise, |
|
12 |
#' a default splash screen is shown. |
|
13 |
#' |
|
14 |
#' Please also refer to the doc of [init()]. |
|
15 |
#' |
|
16 |
#' @param id (`character(1)`)\cr |
|
17 |
#' module id |
|
18 |
#' @inheritParams init |
|
19 |
#' @export |
|
20 |
ui_teal_with_splash <- function(id, |
|
21 |
data, |
|
22 |
title, |
|
23 |
header = tags$p("Add Title Here"), |
|
24 |
footer = tags$p("Add Footer Here")) { |
|
25 | 32x |
checkmate::assert_class(data, "TealDataAbstract") |
26 | 32x |
is_pulled_data <- teal.data::is_pulled(data) |
27 | 32x |
ns <- NS(id) |
28 | ||
29 |
# Startup splash screen for delayed loading |
|
30 |
# We use delayed loading in all cases, even when the data does not need to be fetched. |
|
31 |
# This has the benefit that when filtering the data takes a lot of time initially, the |
|
32 |
# Shiny app does not time out. |
|
33 | 32x |
splash_ui <- if (is_pulled_data) { |
34 |
# blank ui if data is already pulled |
|
35 | 28x |
div() |
36 |
} else { |
|
37 | 4x |
message("App was initialized with delayed data loading.") |
38 | 4x |
data$get_ui(ns("startapp_module")) |
39 |
} |
|
40 | ||
41 | 32x |
ui_teal(id = ns("teal"), splash_ui = splash_ui, title = title, header = header, footer = footer) |
42 |
} |
|
43 | ||
44 |
#' Server function that loads the data through reactive loading and then delegates |
|
45 |
#' to [srv_teal()]. |
|
46 |
#' |
|
47 |
#' @description `r lifecycle::badge("stable")` |
|
48 |
#' Please also refer to the doc of [init()]. |
|
49 |
#' |
|
50 |
#' @inheritParams init |
|
51 |
#' @param modules `teal_modules` object containing the output modules which |
|
52 |
#' will be displayed in the teal application. See [modules()] and [module()] for |
|
53 |
#' more details. |
|
54 |
#' @inheritParams shiny::moduleServer |
|
55 |
#' @return `reactive`, return value of [srv_teal()] |
|
56 |
#' @export |
|
57 |
srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { |
|
58 | 4x |
checkmate::assert_class(data, "TealDataAbstract") |
59 | 4x |
moduleServer(id, function(input, output, session) { |
60 | 4x |
logger::log_trace( |
61 | 4x |
"srv_teal_with_splash initializing module with data { paste(data$get_datanames(), collapse = ' ')}." |
62 |
) |
|
63 | ||
64 | 4x |
if (getOption("teal.show_js_log", default = FALSE)) { |
65 | ! |
shinyjs::showLog() |
66 |
} |
|
67 | ||
68 | 4x |
is_pulled_data <- teal.data::is_pulled(data) |
69 |
# raw_data contains TealDataAbstract, i.e. R6 object and container for data |
|
70 |
# reactive to get data through delayed loading |
|
71 |
# we must leave it inside the server because of callModule which needs to pick up the right session |
|
72 | 4x |
if (is_pulled_data) { |
73 | 2x |
raw_data <- reactiveVal(data) # will trigger by setting it |
74 |
} else { |
|
75 | 2x |
raw_data <- data$get_server()(id = "startapp_module") |
76 | 2x |
if (!is.reactive(raw_data)) { |
77 | ! |
stop("The delayed loading module has to return a reactive object.") |
78 |
} |
|
79 |
} |
|
80 | ||
81 | 4x |
res <- srv_teal(id = "teal", modules = modules, raw_data = raw_data, filter = filter) |
82 | 4x |
logger::log_trace( |
83 | 4x |
"srv_teal_with_splash initialized the module with data { paste(data$get_datanames(), collapse = ' ') }." |
84 |
) |
|
85 | 4x |
return(res) |
86 |
}) |
|
87 |
} |
1 |
#' Creates a `teal_modules` object. |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' This function collects a list of `teal_modules` and `teal_module` objects and returns a `teal_modules` object |
|
5 |
#' containing the passed objects. |
|
6 |
#' |
|
7 |
#' This function dictates what modules are included in a `teal` application. The internal structure of `teal_modules` |
|
8 |
#' shapes the navigation panel of a `teal` application. |
|
9 |
#' |
|
10 |
#' @param ... (`teal_module` or `teal_modules`) see [module()] and [modules()] for more details |
|
11 |
#' @param label (`character(1)`) label of modules collection (default `"root"`). |
|
12 |
#' If using the `label` argument then it must be explicitly named. |
|
13 |
#' For example `modules("lab", ...)` should be converted to `modules(label = "lab", ...)` |
|
14 |
#' |
|
15 |
#' @export |
|
16 |
#' |
|
17 |
#' @return object of class \code{teal_modules}. Object contains following fields |
|
18 |
#' - `label`: taken from the `label` argument |
|
19 |
#' - `children`: a list containing objects passed in `...`. List elements are named after |
|
20 |
#' their `label` attribute converted to a valid `shiny` id. |
|
21 |
#' @examples |
|
22 |
#' library(shiny) |
|
23 |
#' |
|
24 |
#' app <- init( |
|
25 |
#' data = teal_data(dataset("iris", iris)), |
|
26 |
#' modules = modules( |
|
27 |
#' label = "Modules", |
|
28 |
#' modules( |
|
29 |
#' label = "Module", |
|
30 |
#' module( |
|
31 |
#' label = "Inner module", |
|
32 |
#' server = function(id, data) { |
|
33 |
#' moduleServer( |
|
34 |
#' id, |
|
35 |
#' module = function(input, output, session) { |
|
36 |
#' output$data <- renderDataTable(data[["iris"]]()) |
|
37 |
#' } |
|
38 |
#' ) |
|
39 |
#' }, |
|
40 |
#' ui = function(id) { |
|
41 |
#' ns <- NS(id) |
|
42 |
#' tagList(dataTableOutput(ns("data"))) |
|
43 |
#' }, |
|
44 |
#' datanames = "all" |
|
45 |
#' ) |
|
46 |
#' ), |
|
47 |
#' module( |
|
48 |
#' label = "Another module", |
|
49 |
#' server = function(id) { |
|
50 |
#' moduleServer( |
|
51 |
#' id, |
|
52 |
#' module = function(input, output, session) { |
|
53 |
#' output$text <- renderText("Another module") |
|
54 |
#' } |
|
55 |
#' ) |
|
56 |
#' }, |
|
57 |
#' ui = function(id) { |
|
58 |
#' ns <- NS(id) |
|
59 |
#' tagList(textOutput(ns("text"))) |
|
60 |
#' }, |
|
61 |
#' datanames = NULL |
|
62 |
#' ) |
|
63 |
#' ) |
|
64 |
#' ) |
|
65 |
#' if (interactive()) { |
|
66 |
#' runApp(app) |
|
67 |
#' } |
|
68 |
modules <- function(..., label = "root") { |
|
69 | 79x |
checkmate::assert_string(label) |
70 | 77x |
submodules <- list(...) |
71 | 77x |
if (any(vapply(submodules, is.character, FUN.VALUE = logical(1)))) { |
72 | 2x |
stop( |
73 | 2x |
"The only character argument to modules() must be 'label' and it must be named, ", |
74 | 2x |
"change modules('lab', ...) to modules(label = 'lab', ...)" |
75 |
) |
|
76 |
} |
|
77 | ||
78 | 75x |
checkmate::assert_list(submodules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules")) |
79 |
# name them so we can more easily access the children |
|
80 |
# beware however that the label of the submodules should not be changed as it must be kept synced |
|
81 | 72x |
labels <- vapply(submodules, function(submodule) submodule$label, character(1)) |
82 | 72x |
names(submodules) <- make.unique(gsub("[^[:alnum:]]+", "_", labels), sep = "_") |
83 | 72x |
structure( |
84 | 72x |
list( |
85 | 72x |
label = label, |
86 | 72x |
children = submodules |
87 |
), |
|
88 | 72x |
class = "teal_modules" |
89 |
) |
|
90 |
} |
|
91 | ||
92 |
#' Function which appends a teal_module onto the children of a teal_modules object |
|
93 |
#' @keywords internal |
|
94 |
#' @param modules `teal_modules` |
|
95 |
#' @param module `teal_module` object to be appended onto the children of `modules` |
|
96 |
#' @return `teal_modules` object with `module` appended |
|
97 |
append_module <- function(modules, module) { |
|
98 | 7x |
checkmate::assert_class(modules, "teal_modules") |
99 | 5x |
checkmate::assert_class(module, "teal_module") |
100 | 3x |
modules$children <- c(modules$children, list(module)) |
101 | 3x |
labels <- vapply(modules$children, function(submodule) submodule$label, character(1)) |
102 | 3x |
names(modules$children) <- make.unique(gsub("[^[:alnum:]]", "_", tolower(labels)), sep = "_") |
103 | 3x |
modules |
104 |
} |
|
105 | ||
106 |
#' Does the object make use of the `arg` |
|
107 |
#' |
|
108 |
#' @param modules (`teal_module` or `teal_modules`) object |
|
109 |
#' @param arg (`character(1)`) names of the arguments to be checked against formals of `teal` modules. |
|
110 |
#' @return `logical` whether the object makes use of `arg` |
|
111 |
#' @rdname is_arg_used |
|
112 |
#' @keywords internal |
|
113 |
is_arg_used <- function(modules, arg) { |
|
114 | 285x |
checkmate::assert_string(arg) |
115 | 282x |
if (inherits(modules, "teal_modules")) { |
116 | 19x |
any(unlist(lapply(modules$children, is_arg_used, arg))) |
117 | 263x |
} else if (inherits(modules, "teal_module")) { |
118 | 32x |
is_arg_used(modules$server, arg) || is_arg_used(modules$ui, arg) |
119 | 231x |
} else if (is.function(modules)) { |
120 | 229x |
isTRUE(arg %in% names(formals(modules))) |
121 |
} else { |
|
122 | 2x |
stop("is_arg_used function not implemented for this object") |
123 |
} |
|
124 |
} |
|
125 | ||
126 | ||
127 |
#' Creates a `teal_module` object. |
|
128 |
#' |
|
129 |
#' @description `r lifecycle::badge("stable")` |
|
130 |
#' This function embeds a `shiny` module inside a `teal` application. One `teal_module` maps to one `shiny` module. |
|
131 |
#' |
|
132 |
#' @param label (`character(1)`) Label shown in the navigation item for the module. Any label possible except |
|
133 |
#' `"global_filters"` - read more in `mapping` argument of [teal::teal_slices]. |
|
134 |
#' @param server (`function`) `shiny` module with following arguments: |
|
135 |
#' - `id` - teal will set proper shiny namespace for this module (see [shiny::moduleServer()]). |
|
136 |
#' - `input`, `output`, `session` - (not recommended) then [shiny::callModule()] will be used to call a module. |
|
137 |
#' - `data` (optional) module will receive a `tdata` object, a list of reactive (filtered) data specified in |
|
138 |
#' the `filters` argument. |
|
139 |
#' - `datasets` (optional) module will receive `FilteredData`. (See `[teal.slice::FilteredData]`). |
|
140 |
#' - `reporter` (optional) module will receive `Reporter`. (See [teal.reporter::Reporter]). |
|
141 |
# - `filter_panel_api` (optional) module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]). |
|
142 |
#' - `...` (optional) `server_args` elements will be passed to the module named argument or to the `...`. |
|
143 |
#' @param ui (`function`) Shiny `ui` module function with following arguments: |
|
144 |
#' - `id` - teal will set proper shiny namespace for this module. |
|
145 |
#' - `data` (optional) module will receive list of reactive (filtered) data specified in the `filters` argument. |
|
146 |
#' - `datasets` (optional) module will receive `FilteredData`. (See `[teal.slice::FilteredData]`). |
|
147 |
#' - `...` (optional) `ui_args` elements will be passed to the module named argument or to the `...`. |
|
148 |
#' @param filters (`character`) Deprecated. Use `datanames` instead. |
|
149 |
#' @param datanames (`character`) A vector with `datanames` that are relevant for the item. The |
|
150 |
#' filter panel will automatically update the shown filters to include only |
|
151 |
#' filters in the listed datasets. `NULL` will hide the filter panel, |
|
152 |
#' and the keyword `'all'` will show filters of all datasets. `datanames` also determines |
|
153 |
#' a subset of datasets which are appended to the `data` argument in `server` function. |
|
154 |
#' @param server_args (named `list`) with additional arguments passed on to the |
|
155 |
#' `server` function. |
|
156 |
#' @param ui_args (named `list`) with additional arguments passed on to the |
|
157 |
#' `ui` function. |
|
158 |
#' |
|
159 |
#' @return object of class `teal_module`. |
|
160 |
#' @export |
|
161 |
#' @examples |
|
162 |
#' library(shiny) |
|
163 |
#' |
|
164 |
#' app <- init( |
|
165 |
#' data = teal_data(dataset("iris", iris)), |
|
166 |
#' modules = list( |
|
167 |
#' module( |
|
168 |
#' label = "Module", |
|
169 |
#' server = function(id, data) { |
|
170 |
#' moduleServer( |
|
171 |
#' id, |
|
172 |
#' module = function(input, output, session) { |
|
173 |
#' output$data <- renderDataTable(data[["iris"]]()) |
|
174 |
#' } |
|
175 |
#' ) |
|
176 |
#' }, |
|
177 |
#' ui = function(id) { |
|
178 |
#' ns <- NS(id) |
|
179 |
#' tagList(dataTableOutput(ns("data"))) |
|
180 |
#' } |
|
181 |
#' ) |
|
182 |
#' ) |
|
183 |
#' ) |
|
184 |
#' if (interactive()) { |
|
185 |
#' runApp(app) |
|
186 |
#' } |
|
187 |
module <- function(label = "module", |
|
188 |
server = function(id, ...) { |
|
189 | 1x |
moduleServer(id, function(input, output, session) {}) # nolint |
190 |
}, |
|
191 |
ui = function(id, ...) { |
|
192 | 1x |
tags$p(paste0("This module has no UI (id: ", id, " )")) |
193 |
}, |
|
194 |
filters, |
|
195 |
datanames = "all", |
|
196 |
server_args = NULL, |
|
197 |
ui_args = NULL) { |
|
198 | 109x |
checkmate::assert_string(label) |
199 | 106x |
checkmate::assert_function(server) |
200 | 106x |
checkmate::assert_function(ui) |
201 | 106x |
checkmate::assert_character(datanames, min.len = 1, null.ok = TRUE, any.missing = FALSE) |
202 | 105x |
checkmate::assert_list(server_args, null.ok = TRUE, names = "named") |
203 | 103x |
checkmate::assert_list(ui_args, null.ok = TRUE, names = "named") |
204 | ||
205 | 101x |
if (!missing(filters)) { |
206 | ! |
checkmate::assert_character(filters, min.len = 1, null.ok = TRUE, any.missing = FALSE) |
207 | ! |
datanames <- filters |
208 | ! |
msg <- |
209 | ! |
"The `filters` argument is deprecated and will be removed in the next release. Please use `datanames` instead." |
210 | ! |
logger::log_warn(msg) |
211 | ! |
warning(msg) |
212 |
} |
|
213 | ||
214 | 101x |
if (label == "global_filters") { |
215 | 1x |
stop("Label 'global_filters' is reserved in teal. Please change to something else.") |
216 |
} |
|
217 | 100x |
server_formals <- names(formals(server)) |
218 | 100x |
if (!( |
219 | 100x |
"id" %in% server_formals || |
220 | 100x |
all(c("input", "output", "session") %in% server_formals) |
221 |
)) { |
|
222 | 2x |
stop( |
223 | 2x |
"\nmodule() `server` argument requires a function with following arguments:", |
224 | 2x |
"\n - id - teal will set proper shiny namespace for this module.", |
225 | 2x |
"\n - input, output, session (not recommended) - then shiny::callModule will be used to call a module.", |
226 | 2x |
"\n\nFollowing arguments can be used optionaly:", |
227 | 2x |
"\n - `data` - module will receive list of reactive (filtered) data specified in the `filters` argument", |
228 | 2x |
"\n - `datasets` - module will receive `FilteredData`. See `help(teal.slice::FilteredData)`", |
229 | 2x |
"\n - `reporter` - module will receive `Reporter`. See `help(teal.reporter::Reporter)`", |
230 | 2x |
"\n - `filter_panel_api` - module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]).", |
231 | 2x |
"\n - `...` server_args elements will be passed to the module named argument or to the `...`" |
232 |
) |
|
233 |
} |
|
234 | ||
235 | 98x |
if (!is.element("data", server_formals)) { |
236 | 71x |
message(sprintf("module \"%s\" server function takes no data so \"datanames\" will be ignored", label)) |
237 | 71x |
datanames <- NULL |
238 |
} |
|
239 | ||
240 | 98x |
srv_extra_args <- setdiff(names(server_args), server_formals) |
241 | 98x |
if (length(srv_extra_args) > 0 && !"..." %in% server_formals) { |
242 | 1x |
stop( |
243 | 1x |
"\nFollowing `server_args` elements have no equivalent in the formals of the `server`:\n", |
244 | 1x |
paste(paste(" -", srv_extra_args), collapse = "\n"), |
245 | 1x |
"\n\nUpdate the `server` arguments by including above or add `...`" |
246 |
) |
|
247 |
} |
|
248 | ||
249 | 97x |
ui_formals <- names(formals(ui)) |
250 | 97x |
if (!"id" %in% ui_formals) { |
251 | 1x |
stop( |
252 | 1x |
"\nmodule() `ui` argument requires a function with following arguments:", |
253 | 1x |
"\n - id - teal will set proper shiny namespace for this module.", |
254 | 1x |
"\n\nFollowing arguments can be used optionaly:", |
255 | 1x |
"\n - `data` - module will receive list of reactive (filtered) data specied in the `filters` argument", |
256 | 1x |
"\n - `datasets` - module will receive `FilteredData`. See `help(teal.slice::FilteredData)`", |
257 | 1x |
"\n - `...` ui_args elements will be passed to the module argument of the same name or to the `...`" |
258 |
) |
|
259 |
} |
|
260 | ||
261 | 96x |
ui_extra_args <- setdiff(names(ui_args), ui_formals) |
262 | 96x |
if (length(ui_extra_args) > 0 && !"..." %in% ui_formals) { |
263 | 1x |
stop( |
264 | 1x |
"\nFollowing `ui_args` elements have no equivalent in the formals of `ui`:\n", |
265 | 1x |
paste(paste(" -", ui_extra_args), collapse = "\n"), |
266 | 1x |
"\n\nUpdate the `ui` arguments by including above or add `...`" |
267 |
) |
|
268 |
} |
|
269 | ||
270 | 95x |
structure( |
271 | 95x |
list( |
272 | 95x |
label = label, |
273 | 95x |
server = server, ui = ui, datanames = datanames, |
274 | 95x |
server_args = server_args, ui_args = ui_args |
275 |
), |
|
276 | 95x |
class = "teal_module" |
277 |
) |
|
278 |
} |
|
279 | ||
280 | ||
281 |
#' Get module depth |
|
282 |
#' |
|
283 |
#' Depth starts at 0, so a single `teal.module` has depth 0. |
|
284 |
#' Nesting it increases overall depth by 1. |
|
285 |
#' |
|
286 |
#' @inheritParams init |
|
287 |
#' @param depth optional, integer determining current depth level |
|
288 |
#' |
|
289 |
#' @return depth level for given module |
|
290 |
#' @keywords internal |
|
291 |
#' |
|
292 |
#' @examples |
|
293 |
#' mods <- modules( |
|
294 |
#' label = "d1", |
|
295 |
#' modules( |
|
296 |
#' label = "d2", |
|
297 |
#' modules( |
|
298 |
#' label = "d3", |
|
299 |
#' module(label = "aaa1"), module(label = "aaa2"), module(label = "aaa3") |
|
300 |
#' ), |
|
301 |
#' module(label = "bbb") |
|
302 |
#' ), |
|
303 |
#' module(label = "ccc") |
|
304 |
#' ) |
|
305 |
#' stopifnot(teal:::modules_depth(mods) == 3L) |
|
306 |
#' |
|
307 |
#' mods <- modules( |
|
308 |
#' label = "a", |
|
309 |
#' modules( |
|
310 |
#' label = "b1", module(label = "c") |
|
311 |
#' ), |
|
312 |
#' module(label = "b2") |
|
313 |
#' ) |
|
314 |
#' stopifnot(teal:::modules_depth(mods) == 2L) |
|
315 |
modules_depth <- function(modules, depth = 0L) { |
|
316 | 12x |
checkmate::assert( |
317 | 12x |
checkmate::check_class(modules, "teal_module"), |
318 | 12x |
checkmate::check_class(modules, "teal_modules") |
319 |
) |
|
320 | 12x |
checkmate::assert_int(depth, lower = 0) |
321 | 11x |
if (inherits(modules, "teal_modules")) { |
322 | 4x |
max(vapply(modules$children, modules_depth, integer(1), depth = depth + 1L)) |
323 |
} else { |
|
324 | 7x |
depth |
325 |
} |
|
326 |
} |
|
327 | ||
328 | ||
329 |
module_labels <- function(modules) { |
|
330 | ! |
if (inherits(modules, "teal_modules")) { |
331 | ! |
lapply(modules$children, module_labels) |
332 |
} else { |
|
333 | ! |
modules$label |
334 |
} |
|
335 |
} |
|
336 | ||
337 |
#' Converts `teal_modules` to a string |
|
338 |
#' |
|
339 |
#' @param x (`teal_modules`) to print |
|
340 |
#' @param indent (`integer`) indent level; |
|
341 |
#' each `submodule` is indented one level more |
|
342 |
#' @param ... (optional) additional parameters to pass to recursive calls of `toString` |
|
343 |
#' @return (`character`) |
|
344 |
#' @export |
|
345 |
#' @rdname modules |
|
346 |
toString.teal_modules <- function(x, indent = 0, ...) { # nolint |
|
347 |
# argument must be `x` to be consistent with base method |
|
348 | ! |
paste(c( |
349 | ! |
paste0(rep(" ", indent), "+ ", x$label), |
350 | ! |
unlist(lapply(x$children, toString, indent = indent + 1, ...)) |
351 | ! |
), collapse = "\n") |
352 |
} |
|
353 | ||
354 |
#' Converts `teal_module` to a string |
|
355 |
#' |
|
356 |
#' @inheritParams toString.teal_modules |
|
357 |
#' @param x `teal_module` |
|
358 |
#' @param ... ignored |
|
359 |
#' @export |
|
360 |
#' @rdname module |
|
361 |
toString.teal_module <- function(x, indent = 0, ...) { # nolint |
|
362 | ! |
paste0(paste(rep(" ", indent), collapse = ""), "+ ", x$label, collapse = "") |
363 |
} |
|
364 | ||
365 |
#' Prints `teal_modules` |
|
366 |
#' @param x `teal_modules` |
|
367 |
#' @param ... parameters passed to `toString` |
|
368 |
#' @export |
|
369 |
#' @rdname modules |
|
370 |
print.teal_modules <- function(x, ...) { |
|
371 | ! |
s <- toString(x, ...) |
372 | ! |
cat(s) |
373 | ! |
return(invisible(s)) |
374 |
} |
|
375 | ||
376 |
#' Prints `teal_module` |
|
377 |
#' @param x `teal_module` |
|
378 |
#' @param ... parameters passed to `toString` |
|
379 |
#' @export |
|
380 |
#' @rdname module |
|
381 |
print.teal_module <- print.teal_modules |
1 |
#' Filter state snapshot management. |
|
2 |
#' |
|
3 |
#' Capture and restore snapshots of the global (app) filter state. |
|
4 |
#' |
|
5 |
#' This module introduces snapshots: stored descriptions of the filter state of the entire application. |
|
6 |
#' Snapshots allow the user to save the current filter state of the application for later use in the session, |
|
7 |
#' as well as to save it to file in order to share it with an app developer or other users. |
|
8 |
#' |
|
9 |
#' The snapshot manager is accessed through the filter manager, with the cog icon in the top right corner. |
|
10 |
#' At the beginning of a session it presents two icons: a camera and an circular arrow. |
|
11 |
#' Clicking the camera captures a snapshot and clicking the arrow resets initial application state. |
|
12 |
#' As snapshots are added, they will show up as rows in a table and each will have a select button and a save button. |
|
13 |
#' |
|
14 |
#' @section Server logic: |
|
15 |
#' Snapshots are basically `teal_slices` objects, however, since each module is served by a separate instance |
|
16 |
#' of `FilteredData` and these objects require shared state, `teal_slice` is a `reactiveVal` so `teal_slices` |
|
17 |
#' cannot be stored as is. Therefore, `teal_slices` are reversibly converted to a list of lists representation |
|
18 |
#' (attributes are maintained). |
|
19 |
#' |
|
20 |
#' Snapshots are stored in a `reactiveVal` as a named list. |
|
21 |
#' The first snapshot is the initial state of the application and the user can add a snapshot whenever they see fit. |
|
22 |
#' |
|
23 |
#' For every snapshot except the initial one, a piece of UI is generated that contains |
|
24 |
#' the snapshot name, a select button to restore that snapshot, and a save button to save it to a file. |
|
25 |
#' The initial snapshot is restored by a separate "reset" button. |
|
26 |
#' It cannot be saved directly but a user is welcome to capture the initial state as a snapshot and save that. |
|
27 |
#' |
|
28 |
#' @section Snapshot mechanics: |
|
29 |
#' When a snapshot is captured, the user is prompted to name it. |
|
30 |
#' Names are displayed as is but since they are used to create button ids, |
|
31 |
#' under the hood they are converted to syntactically valid strings. |
|
32 |
#' New snapshot names are validated so that their valid versions are unique. |
|
33 |
#' Leading and trailing white space is trimmed. |
|
34 |
#' |
|
35 |
#' The module can read the global state of the application from `slices_global` and `mapping_matrix`. |
|
36 |
#' The former provides a list of all existing `teal_slice`s and the latter says which slice is active in which module. |
|
37 |
#' Once a name has been accepted, `slices_global` is converted to a list of lists - a snapshot. |
|
38 |
#' The snapshot contains the `mapping` attribute of the initial application state |
|
39 |
#' (or one that has been restored), which may not reflect the current one, |
|
40 |
#' so `mapping_matrix` is transformed to obtain the current mapping, i.e. a list that, |
|
41 |
#' when passed to the `mapping` argument of [`teal::teal_slices`], would result in the current mapping. |
|
42 |
#' This is substituted as the snapshot's `mapping` attribute and the snapshot is added to the snapshot list. |
|
43 |
#' |
|
44 |
#' To restore app state, a snapshot is retrieved from storage and rebuilt into a `teal_slices` object. |
|
45 |
#' Then state of all `FilteredData` objects (provided in `filtered_data_list`) is cleared |
|
46 |
#' and set anew according to the `mapping` attribute of the snapshot. |
|
47 |
#' The snapshot is then set as the current content of `slices_global`. |
|
48 |
#' |
|
49 |
#' To save a snapshot, the snapshot is retrieved and reassembled just like for restoring, |
|
50 |
#' and then saved to file with [`teal.slice::slices_store`]. |
|
51 |
#' |
|
52 |
#' @param id (`character(1)`) `shiny` module id |
|
53 |
#' @param slices_global (`reactiveVal`) that contains a `teal_slices` object |
|
54 |
#' containing all `teal_slice`s existing in the app, both active and inactive |
|
55 |
#' @param mapping_matrix (`reactive`) that contains a `data.frame` representation |
|
56 |
#' of the mapping of filter state ids (rows) to modules labels (columns); |
|
57 |
#' all columns are `logical` vectors |
|
58 |
#' @param filtered_data_list non-nested (`named list`) that contains `FilteredData` objects |
|
59 |
#' |
|
60 |
#' @return Nothing is returned. |
|
61 |
#' |
|
62 |
#' @name snapshot_manager_module |
|
63 |
#' @aliases snapshot snapshot_manager |
|
64 |
#' |
|
65 |
#' @author Aleksander Chlebowski |
|
66 |
#' |
|
67 |
#' @rdname snapshot_manager_module |
|
68 |
#' @keywords internal |
|
69 |
#' |
|
70 |
snapshot_manager_ui <- function(id) { |
|
71 | ! |
ns <- NS(id) |
72 | ! |
div( |
73 | ! |
class = "snapshot_manager_content", |
74 | ! |
div( |
75 | ! |
class = "snapshot_table_row", |
76 | ! |
span(tags$b("Snapshot manager")), |
77 | ! |
actionLink(ns("snapshot_add"), label = NULL, icon = icon("camera"), title = "add snapshot"), |
78 | ! |
actionLink(ns("snapshot_reset"), label = NULL, icon = icon("undo"), title = "reset initial state"), |
79 | ! |
NULL |
80 |
), |
|
81 | ! |
uiOutput(ns("snapshot_list")) |
82 |
) |
|
83 |
} |
|
84 | ||
85 |
#' @rdname snapshot_manager_module |
|
86 |
#' @keywords internal |
|
87 |
#' |
|
88 |
snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_data_list) { |
|
89 | 7x |
checkmate::assert_character(id) |
90 | 7x |
checkmate::assert_true(is.reactive(slices_global)) |
91 | 7x |
checkmate::assert_class(isolate(slices_global()), "teal_slices") |
92 | 7x |
checkmate::assert_true(is.reactive(mapping_matrix)) |
93 | 7x |
checkmate::assert_data_frame(isolate(mapping_matrix()), null.ok = TRUE) |
94 | 7x |
checkmate::assert_list(filtered_data_list, types = "FilteredData", any.missing = FALSE, names = "named") |
95 | ||
96 | 7x |
moduleServer(id, function(input, output, session) { |
97 | 7x |
ns <- session$ns |
98 | ||
99 |
# Store global filter states. |
|
100 | 7x |
filter <- isolate(slices_global()) |
101 | 7x |
snapshot_history <- reactiveVal({ |
102 | 7x |
list( |
103 | 7x |
"Initial application state" = as.list(filter, recursive = TRUE) |
104 |
) |
|
105 |
}) |
|
106 | ||
107 |
# Snapshot current application state - name snaphsot. |
|
108 | 7x |
observeEvent(input$snapshot_add, { |
109 | ! |
showModal( |
110 | ! |
modalDialog( |
111 | ! |
textInput(ns("snapshot_name"), "Name the snapshot", width = "100%", placeholder = "Meaningful, unique name"), |
112 | ! |
footer = tagList( |
113 | ! |
actionButton(ns("snapshot_name_accept"), "Accept", icon = icon("thumbs-up")), |
114 | ! |
modalButton(label = "Cancel", icon = icon("thumbs-down")) |
115 |
), |
|
116 | ! |
size = "s" |
117 |
) |
|
118 |
) |
|
119 |
}) |
|
120 |
# Snapshot current application state - store snaphsot. |
|
121 | 7x |
observeEvent(input$snapshot_name_accept, { |
122 | ! |
snapshot_name <- trimws(input$snapshot_name) |
123 | ! |
if (identical(snapshot_name, "")) { |
124 | ! |
showNotification( |
125 | ! |
"Please name the snapshot.", |
126 | ! |
type = "message" |
127 |
) |
|
128 | ! |
updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
129 | ! |
} else if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) { |
130 | ! |
showNotification( |
131 | ! |
"This name is in conflict with other snapshot names. Please choose a different one.", |
132 | ! |
type = "message" |
133 |
) |
|
134 | ! |
updateTextInput(inputId = "snapshot_name", value = , placeholder = "Meaningful, unique name") |
135 |
} else { |
|
136 | ! |
snapshot <- as.list(slices_global(), recursive = TRUE) |
137 | ! |
attr(snapshot, "mapping") <- matrix_to_mapping(mapping_matrix()) |
138 | ! |
snapshot_update <- c(snapshot_history(), list(snapshot)) |
139 | ! |
names(snapshot_update)[length(snapshot_update)] <- snapshot_name |
140 | ! |
snapshot_history(snapshot_update) |
141 | ! |
removeModal() |
142 |
# Reopen filter manager modal by clicking button in the main application. |
|
143 | ! |
shinyjs::click(id = "teal-main_ui-filter_manager-show", asis = TRUE) |
144 |
} |
|
145 |
}) |
|
146 | ||
147 |
# Restore initial state. |
|
148 | 7x |
observeEvent(input$snapshot_reset, { |
149 | ! |
s <- "Initial application state" |
150 |
### Begin restore procedure. ### |
|
151 | ! |
snapshot <- snapshot_history()[[s]] |
152 | ! |
snapshot_state <- as.teal_slices(snapshot) |
153 | ! |
mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list)) |
154 | ! |
mapply( |
155 | ! |
function(filtered_data, filter_ids) { |
156 | ! |
filtered_data$clear_filter_states(force = TRUE) |
157 | ! |
slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state) |
158 | ! |
filtered_data$set_filter_state(slices) |
159 |
}, |
|
160 | ! |
filtered_data = filtered_data_list, |
161 | ! |
filter_ids = mapping_unfolded |
162 |
) |
|
163 | ! |
slices_global(snapshot_state) |
164 | ! |
removeModal() |
165 |
### End restore procedure. ### |
|
166 |
}) |
|
167 | ||
168 |
# Create UI elements and server logic for the snapshot table. |
|
169 |
# Observers must be tracked to avoid duplication and excess reactivity. |
|
170 |
# Remaining elements are tracked likewise for consistency and a slight speed margin. |
|
171 | 7x |
observers <- reactiveValues() |
172 | 7x |
handlers <- reactiveValues() |
173 | 7x |
divs <- reactiveValues() |
174 | ||
175 | 7x |
observeEvent(snapshot_history(), { |
176 | 3x |
lapply(names(snapshot_history())[-1L], function(s) { |
177 | ! |
id_pickme <- sprintf("pickme_%s", make.names(s)) |
178 | ! |
id_saveme <- sprintf("saveme_%s", make.names(s)) |
179 | ! |
id_rowme <- sprintf("rowme_%s", make.names(s)) |
180 | ||
181 |
# Observer for restoring snapshot. |
|
182 | ! |
if (!is.element(id_pickme, names(observers))) { |
183 | ! |
observers[[id_pickme]] <- observeEvent(input[[id_pickme]], { |
184 |
### Begin restore procedure. ### |
|
185 | ! |
snapshot <- snapshot_history()[[s]] |
186 | ! |
snapshot_state <- as.teal_slices(snapshot) |
187 | ! |
mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list)) |
188 | ! |
mapply( |
189 | ! |
function(filtered_data, filter_ids) { |
190 | ! |
filtered_data$clear_filter_states(force = TRUE) |
191 | ! |
slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state) |
192 | ! |
filtered_data$set_filter_state(slices) |
193 |
}, |
|
194 | ! |
filtered_data = filtered_data_list, |
195 | ! |
filter_ids = mapping_unfolded |
196 |
) |
|
197 | ! |
slices_global(snapshot_state) |
198 | ! |
removeModal() |
199 |
### End restore procedure. ### |
|
200 |
}) |
|
201 |
} |
|
202 |
# Create handler for downloading snapshot. |
|
203 | ! |
if (!is.element(id_saveme, names(handlers))) { |
204 | ! |
output[[id_saveme]] <- downloadHandler( |
205 | ! |
filename = function() { |
206 | ! |
sprintf("teal_snapshot_%s_%s.json", s, Sys.Date()) |
207 |
}, |
|
208 | ! |
content = function(file) { |
209 | ! |
snapshot <- snapshot_history()[[s]] |
210 | ! |
snapshot_state <- as.teal_slices(snapshot) |
211 | ! |
teal.slice::slices_store(tss = snapshot_state, file = file) |
212 |
} |
|
213 |
) |
|
214 | ! |
handlers[[id_saveme]] <- id_saveme |
215 |
} |
|
216 |
# Create a row for the snapshot table. |
|
217 | ! |
if (!is.element(id_rowme, names(divs))) { |
218 | ! |
divs[[id_rowme]] <- div( |
219 | ! |
class = "snapshot_table_row", |
220 | ! |
span(h5(s)), |
221 | ! |
actionLink(inputId = ns(id_pickme), label = icon("circle-check"), title = "select"), |
222 | ! |
downloadLink(outputId = ns(id_saveme), label = icon("save"), title = "save to file") |
223 |
) |
|
224 |
} |
|
225 |
}) |
|
226 |
}) |
|
227 | ||
228 |
# Create table to display list of snapshots and their actions. |
|
229 | 7x |
output$snapshot_list <- renderUI({ |
230 | 3x |
rows <- lapply(rev(reactiveValuesToList(divs)), function(d) d) |
231 | 3x |
if (length(rows) == 0L) { |
232 | 3x |
div( |
233 | 3x |
class = "snapshot_manager_placeholder", |
234 | 3x |
"Snapshots will appear here." |
235 |
) |
|
236 |
} else { |
|
237 | ! |
rows |
238 |
} |
|
239 |
}) |
|
240 |
}) |
|
241 |
} |
|
242 | ||
243 | ||
244 | ||
245 | ||
246 |
### utility functions ---- |
|
247 | ||
248 |
#' Explicitly enumerate global filters. |
|
249 |
#' |
|
250 |
#' Transform module mapping such that global filters are explicitly specified for every module. |
|
251 |
#' |
|
252 |
#' @param mapping (`named list`) as stored in mapping parameter of `teal_slices` |
|
253 |
#' @param module_names (`character`) vector containing names of all modules in the app |
|
254 |
#' @return A `named_list` with one element per module, each element containing all filters applied to that module. |
|
255 |
#' @keywords internal |
|
256 |
#' |
|
257 |
unfold_mapping <- function(mapping, module_names) { |
|
258 | ! |
module_names <- structure(module_names, names = module_names) |
259 | ! |
lapply(module_names, function(x) c(mapping[[x]], mapping[["global_filters"]])) |
260 |
} |
|
261 | ||
262 |
#' Convert mapping matrix to filter mapping specification. |
|
263 |
#' |
|
264 |
#' Transform a mapping matrix, i.e. a data frame that maps each filter state to each module, |
|
265 |
#' to a list specification like the one used in the `mapping` attribute of `teal_slices`. |
|
266 |
#' Global filters are gathered in one list element. |
|
267 |
#' If a module has no active filters but the global ones, it will not be mentioned in the output. |
|
268 |
#' |
|
269 |
#' @param mapping_matrix (`data.frame`) of logical vectors where |
|
270 |
#' columns represent modules and row represent `teal_slice`s |
|
271 |
#' @return `named list` like that in the `mapping` attribute of a `teal_slices` object. |
|
272 |
#' @keywords internal |
|
273 |
#' |
|
274 |
matrix_to_mapping <- function(mapping_matrix) { |
|
275 | ! |
mapping_matrix[] <- lapply(mapping_matrix, function(x) x | is.na(x)) |
276 | ! |
global <- vapply(as.data.frame(t(mapping_matrix)), all, logical(1L)) |
277 | ! |
global_filters <- names(global[global]) |
278 | ! |
local_filters <- mapping_matrix[!rownames(mapping_matrix) %in% global_filters, ] |
279 | ||
280 | ! |
mapping <- c(lapply(local_filters, function(x) rownames(local_filters)[x]), list(global_filters = global_filters)) |
281 | ! |
Filter(function(x) length(x) != 0L, mapping) |
282 |
} |
1 |
#' Filter manager modal |
|
2 |
#' |
|
3 |
#' Opens modal containing the filter manager UI. |
|
4 |
#' |
|
5 |
#' @name module_filter_manager_modal |
|
6 |
#' @inheritParams filter_manager_srv |
|
7 |
#' @examples |
|
8 |
#' fd1 <- teal.slice::init_filtered_data(list(iris = list(dataset = iris))) |
|
9 |
#' fd2 <- teal.slice::init_filtered_data( |
|
10 |
#' list(iris = list(dataset = iris), mtcars = list(dataset = mtcars)) |
|
11 |
#' ) |
|
12 |
#' fd3 <- teal.slice::init_filtered_data( |
|
13 |
#' list(iris = list(dataset = iris), women = list(dataset = women)) |
|
14 |
#' ) |
|
15 |
#' filter <- teal_slices( |
|
16 |
#' teal.slice::teal_slice(dataname = "iris", varname = "Sepal.Length"), |
|
17 |
#' teal.slice::teal_slice(dataname = "iris", varname = "Species"), |
|
18 |
#' teal.slice::teal_slice(dataname = "mtcars", varname = "mpg"), |
|
19 |
#' teal.slice::teal_slice(dataname = "women", varname = "height"), |
|
20 |
#' mapping = list( |
|
21 |
#' module2 = c("mtcars mpg"), |
|
22 |
#' module3 = c("women height"), |
|
23 |
#' global_filters = "iris Species" |
|
24 |
#' ) |
|
25 |
#' ) |
|
26 |
#' |
|
27 |
#' app <- shinyApp( |
|
28 |
#' ui = fluidPage( |
|
29 |
#' teal:::filter_manager_modal_ui("manager") |
|
30 |
#' ), |
|
31 |
#' server = function(input, output, session) { |
|
32 |
#' teal:::filter_manager_modal_srv( |
|
33 |
#' "manager", |
|
34 |
#' filtered_data_list = list(module1 = fd1, module2 = fd2, module3 = fd3), |
|
35 |
#' filter = filter |
|
36 |
#' ) |
|
37 |
#' } |
|
38 |
#' ) |
|
39 |
#' if (interactive()) { |
|
40 |
#' runApp(app) |
|
41 |
#' } |
|
42 |
#' |
|
43 |
#' @keywords internal |
|
44 |
#' |
|
45 |
NULL |
|
46 | ||
47 |
#' @rdname module_filter_manager_modal |
|
48 |
filter_manager_modal_ui <- function(id) { |
|
49 | 1x |
ns <- NS(id) |
50 | 1x |
tags$button( |
51 | 1x |
id = ns("show"), |
52 | 1x |
class = "btn action-button filter_manager_button", |
53 | 1x |
title = "Show filters manager modal", |
54 | 1x |
icon("gear") |
55 |
) |
|
56 |
} |
|
57 | ||
58 |
#' @rdname module_filter_manager_modal |
|
59 |
filter_manager_modal_srv <- function(id, filtered_data_list, filter) { |
|
60 | 4x |
moduleServer(id, function(input, output, session) { |
61 | 4x |
observeEvent(input$show, { |
62 | ! |
logger::log_trace("filter_manager_modal_srv@1 show button has been clicked.") |
63 | ! |
showModal( |
64 | ! |
modalDialog( |
65 | ! |
filter_manager_ui(session$ns("filter_manager")), |
66 | ! |
size = "l", |
67 | ! |
footer = NULL, |
68 | ! |
easyClose = TRUE |
69 |
) |
|
70 |
) |
|
71 |
}) |
|
72 | ||
73 | 4x |
filter_manager_srv("filter_manager", filtered_data_list, filter) |
74 |
}) |
|
75 |
} |
|
76 | ||
77 |
#' @rdname module_filter_manager |
|
78 |
filter_manager_ui <- function(id) { |
|
79 | ! |
ns <- NS(id) |
80 | ! |
div( |
81 | ! |
class = "filter_manager_content", |
82 | ! |
tableOutput(ns("slices_table")), |
83 | ! |
snapshot_manager_ui(ns("snapshot_manager")) |
84 |
) |
|
85 |
} |
|
86 | ||
87 |
#' Manage multiple `FilteredData` objects |
|
88 |
#' |
|
89 |
#' Oversee filter states in the whole application. |
|
90 |
#' |
|
91 |
#' @rdname module_filter_manager |
|
92 |
#' @details |
|
93 |
#' This module observes the changes of the filters in each `FilteredData` object |
|
94 |
#' and keeps track of all filters used. A mapping of filters to modules |
|
95 |
#' is kept in the `mapping_matrix` object (which is actually a `data.frame`) |
|
96 |
#' that tracks which filters (rows) are active in which modules (columns). |
|
97 |
#' |
|
98 |
#' @param id (`character(1)`)\cr |
|
99 |
#' `shiny` module id. |
|
100 |
#' @param filtered_data_list (`named list`)\cr |
|
101 |
#' A list, possibly nested, of `FilteredData` objects. |
|
102 |
#' Each `FilteredData` will be served to one module in the `teal` application. |
|
103 |
#' The structure of the list must reflect the nesting of modules in tabs |
|
104 |
#' and names of the list must be the same as labels of their respective modules. |
|
105 |
#' @inheritParams init |
|
106 |
#' @return A list of `reactive`s, each holding a `teal_slices`, as returned by `filter_manager_module_srv`. |
|
107 |
#' @keywords internal |
|
108 |
#' |
|
109 |
filter_manager_srv <- function(id, filtered_data_list, filter) { |
|
110 | 6x |
moduleServer(id, function(input, output, session) { |
111 | 6x |
logger::log_trace("filter_manager_srv initializing for: { paste(names(filtered_data_list), collapse = ', ')}.") |
112 | ||
113 | 6x |
is_module_specific <- isTRUE(attr(filter, "module_specific")) |
114 | ||
115 |
# Create global list of slices. |
|
116 |
# Contains all available teal_slice objects available to all modules. |
|
117 |
# Passed whole to instances of FilteredData used for individual modules. |
|
118 |
# Down there a subset that pertains to the data sets used in that module is applied and displayed. |
|
119 | 6x |
slices_global <- reactiveVal(filter) |
120 | ||
121 | 6x |
filtered_data_list <- |
122 | 6x |
if (!is_module_specific) { |
123 |
# Retrieve the first FilteredData from potentially nested list. |
|
124 |
# List of length one is named "global_filters" because that name is forbidden for a module label. |
|
125 | 5x |
list(global_filters = unlist(filtered_data_list)[[1]]) |
126 |
} else { |
|
127 |
# Flatten potentially nested list of FilteredData objects while maintaining useful names. |
|
128 |
# Simply using `unlist` would result in concatenated names. |
|
129 | 1x |
flatten_nested <- function(x, name = NULL) { |
130 | 5x |
if (inherits(x, "FilteredData")) { |
131 | 3x |
setNames(list(x), name) |
132 |
} else { |
|
133 | 2x |
unlist(lapply(names(x), function(name) flatten_nested(x[[name]], name))) |
134 |
} |
|
135 |
} |
|
136 | 1x |
flatten_nested(filtered_data_list) |
137 |
} |
|
138 | ||
139 |
# Create mapping fo filters to modules in matrix form (presented as data.frame). |
|
140 |
# Modules get NAs for filteres that cannot be set for them. |
|
141 | 6x |
mapping_matrix <- reactive({ |
142 | 6x |
state_ids_global <- vapply(slices_global(), `[[`, character(1L), "id") |
143 | 6x |
mapping_smooth <- lapply(filtered_data_list, function(x) { |
144 | 8x |
state_ids_local <- vapply(x$get_filter_state(), `[[`, character(1L), "id") |
145 | 8x |
state_ids_allowed <- vapply(x$get_available_teal_slices()(), `[[`, character(1L), "id") |
146 | 8x |
states_active <- state_ids_global %in% state_ids_local |
147 | 8x |
ifelse(state_ids_global %in% state_ids_allowed, states_active, NA) |
148 |
}) |
|
149 | ||
150 | 6x |
as.data.frame(mapping_smooth, row.names = state_ids_global, check.names = FALSE) |
151 |
}) |
|
152 | ||
153 | 6x |
output$slices_table <- renderTable( |
154 | 6x |
expr = { |
155 |
# Display logical values as UTF characters. |
|
156 | 3x |
mm <- mapping_matrix() |
157 | 3x |
mm[] <- lapply(mm, ifelse, yes = intToUtf8(9989), no = intToUtf8(10060)) |
158 | 3x |
mm[] <- lapply(mm, function(x) ifelse(is.na(x), intToUtf8(128306), x)) |
159 | 3x |
if (!is_module_specific) colnames(mm) <- "Global Filters" |
160 | ||
161 |
# Display placeholder if no filters defined. |
|
162 | 3x |
if (nrow(mm) == 0L) { |
163 | 3x |
mm <- data.frame(`Filter manager` = "No filters specified.", check.names = FALSE) |
164 | 3x |
rownames(mm) <- "" |
165 |
} |
|
166 | ||
167 | 3x |
mm |
168 |
}, |
|
169 | 6x |
align = paste(c("l", rep("c", length(filtered_data_list))), collapse = ""), |
170 | 6x |
rownames = TRUE |
171 |
) |
|
172 | ||
173 |
# Create list of module calls. |
|
174 | 6x |
modules_out <- lapply(names(filtered_data_list), function(module_name) { |
175 | 8x |
filter_manager_module_srv( |
176 | 8x |
id = module_name, |
177 | 8x |
module_fd = filtered_data_list[[module_name]], |
178 | 8x |
slices_global = slices_global |
179 |
) |
|
180 |
}) |
|
181 | ||
182 |
# Call snapshot manager. |
|
183 | 6x |
snapshot_manager_srv("snapshot_manager", slices_global, mapping_matrix, filtered_data_list) |
184 | ||
185 | 6x |
modules_out # returned for testing purpose |
186 |
}) |
|
187 |
} |
|
188 | ||
189 |
#' Module specific filter manager |
|
190 |
#' |
|
191 |
#' Track filter states in single module. |
|
192 |
#' |
|
193 |
#' This module tracks the state of a single `FilteredData` object and global `teal_slices` |
|
194 |
#' and updates both objects as necessary. Filter states added in different modules |
|
195 |
#' Filter states added any individual module are added to global `teal_slices` |
|
196 |
#' and from there become available in other modules |
|
197 |
#' by setting `private$available_teal_slices` in each `FilteredData`. |
|
198 |
#' |
|
199 |
#' @param id (`character(1)`)\cr |
|
200 |
#' `shiny` module id. |
|
201 |
#' @param module_fd (`FilteredData`)\cr |
|
202 |
#' object to filter data in the teal-module |
|
203 |
#' @param slices_global (`reactiveVal`)\cr |
|
204 |
#' stores `teal_slices` with all available filters; allows the following actions: |
|
205 |
#' - to disable/enable a specific filter in a module |
|
206 |
#' - to restore saved filter settings |
|
207 |
#' - to save current filter panel settings |
|
208 |
#' @return A `reactive` expression containing the slices active in this module. |
|
209 |
#' @keywords internal |
|
210 |
#' |
|
211 |
filter_manager_module_srv <- function(id, module_fd, slices_global) { |
|
212 | 8x |
moduleServer(id, function(input, output, session) { |
213 |
# Only operate on slices that refer to data sets present in this module. |
|
214 | 8x |
module_fd$set_available_teal_slices(reactive(slices_global())) |
215 | ||
216 |
# Track filter state of this module. |
|
217 | 8x |
slices_module <- reactive(module_fd$get_filter_state()) |
218 | ||
219 |
# Reactive values for comparing states. |
|
220 | 8x |
previous_slices <- reactiveVal(isolate(slices_module())) |
221 | 8x |
slices_added <- reactiveVal(NULL) |
222 | ||
223 |
# Observe changes in module filter state and trigger appropriate actions. |
|
224 | 8x |
observeEvent(slices_module(), ignoreNULL = FALSE, { |
225 | 3x |
logger::log_trace("filter_manager_srv@1 detecting states deltas in module: { id }.") |
226 | 3x |
added <- setdiff_teal_slices(slices_module(), slices_global()) |
227 | ! |
if (length(added)) slices_added(added) |
228 | 3x |
previous_slices(slices_module()) |
229 |
}) |
|
230 | ||
231 | 8x |
observeEvent(slices_added(), ignoreNULL = TRUE, { |
232 | ! |
logger::log_trace("filter_manager_srv@2 added filter in module: { id }.") |
233 |
# In case the new state has the same id as an existing state, add a suffix to it. |
|
234 | ! |
global_ids <- vapply(slices_global(), `[[`, character(1L), "id") |
235 | ! |
lapply( |
236 | ! |
slices_added(), |
237 | ! |
function(slice) { |
238 | ! |
if (slice$id %in% global_ids) { |
239 | ! |
slice$id <- utils::tail(make.unique(c(global_ids, slice$id), sep = "_"), 1) |
240 |
} |
|
241 |
} |
|
242 |
) |
|
243 | ! |
slices_global_new <- c(slices_global(), slices_added()) |
244 | ! |
slices_global(slices_global_new) |
245 | ! |
slices_added(NULL) |
246 |
}) |
|
247 | ||
248 | 8x |
slices_module # returned for testing purpose |
249 |
}) |
|
250 |
} |
1 |
#' Send input validation messages to output. |
|
2 |
#' |
|
3 |
#' Captures messages from `InputValidator` objects and collates them |
|
4 |
#' into one message passed to `validate`. |
|
5 |
#' |
|
6 |
#' `shiny::validate` is used to withhold rendering of an output element until |
|
7 |
#' certain conditions are met and to print a validation message in place |
|
8 |
#' of the output element. |
|
9 |
#' `shinyvalidate::InputValidator` allows to validate input elements |
|
10 |
#' and to display specific messages in their respective input widgets. |
|
11 |
#' `validate_inputs` provides a hybrid solution. |
|
12 |
#' Given an `InputValidator` object, messages corresponding to inputs that fail validation |
|
13 |
#' are extracted and placed in one validation message that is passed to a `validate`/`need` call. |
|
14 |
#' This way the input `validator` messages are repeated in the output. |
|
15 |
#' |
|
16 |
#' The `...` argument accepts any number of `InputValidator` objects |
|
17 |
#' or a nested list of such objects. |
|
18 |
#' If `validators` are passed directly, all their messages are printed together |
|
19 |
#' under one (optional) header message specified by `header`. If a list is passed, |
|
20 |
#' messages are grouped by `validator`. The list's names are used as headers |
|
21 |
#' for their respective message groups. |
|
22 |
#' If neither of the nested list elements is named, a header message is taken from `header`. |
|
23 |
#' |
|
24 |
#' @param ... either any number of `InputValidator` objects |
|
25 |
#' or an optionally named, possibly nested `list` of `InputValidator` |
|
26 |
#' objects, see `Details` |
|
27 |
#' @param header `character(1)` generic validation message; set to NULL to omit |
|
28 |
#' |
|
29 |
#' @return |
|
30 |
#' Returns NULL if the final validation call passes and a `shiny.silent.error` if it fails. |
|
31 |
#' |
|
32 |
#' @seealso [`shinyvalidate::InputValidator`], [`shiny::validate`] |
|
33 |
#' |
|
34 |
#' @examples |
|
35 |
#' library(shiny) |
|
36 |
#' library(shinyvalidate) |
|
37 |
#' |
|
38 |
#' ui <- fluidPage( |
|
39 |
#' selectInput("method", "validation method", c("sequential", "combined", "grouped")), |
|
40 |
#' sidebarLayout( |
|
41 |
#' sidebarPanel( |
|
42 |
#' selectInput("letter", "select a letter:", c(letters[1:3], LETTERS[4:6])), |
|
43 |
#' selectInput("number", "select a number:", 1:6), |
|
44 |
#' br(), |
|
45 |
#' selectInput("color", "select a color:", |
|
46 |
#' c("black", "indianred2", "springgreen2", "cornflowerblue"), |
|
47 |
#' multiple = TRUE |
|
48 |
#' ), |
|
49 |
#' sliderInput("size", "select point size:", |
|
50 |
#' min = 0.1, max = 4, value = 0.25 |
|
51 |
#' ) |
|
52 |
#' ), |
|
53 |
#' mainPanel(plotOutput("plot")) |
|
54 |
#' ) |
|
55 |
#' ) |
|
56 |
#' |
|
57 |
#' server <- function(input, output) { |
|
58 |
#' # set up input validation |
|
59 |
#' iv <- InputValidator$new() |
|
60 |
#' iv$add_rule("letter", sv_in_set(LETTERS, "choose a capital letter")) |
|
61 |
#' iv$add_rule("number", ~ if (as.integer(.) %% 2L == 1L) "choose an even number") |
|
62 |
#' iv$enable() |
|
63 |
#' # more input validation |
|
64 |
#' iv_par <- InputValidator$new() |
|
65 |
#' iv_par$add_rule("color", sv_required(message = "choose a color")) |
|
66 |
#' iv_par$add_rule("color", ~ if (length(.) > 1L) "choose only one color") |
|
67 |
#' iv_par$add_rule( |
|
68 |
#' "size", |
|
69 |
#' sv_between( |
|
70 |
#' left = 0.5, right = 3, |
|
71 |
#' message_fmt = "choose a value between {left} and {right}" |
|
72 |
#' ) |
|
73 |
#' ) |
|
74 |
#' iv_par$enable() |
|
75 |
#' |
|
76 |
#' output$plot <- renderPlot({ |
|
77 |
#' # validate output |
|
78 |
#' switch(input[["method"]], |
|
79 |
#' "sequential" = { |
|
80 |
#' validate_inputs(iv) |
|
81 |
#' validate_inputs(iv_par, header = "Set proper graphical parameters") |
|
82 |
#' }, |
|
83 |
#' "combined" = validate_inputs(iv, iv_par), |
|
84 |
#' "grouped" = validate_inputs(list( |
|
85 |
#' "Some inputs require attention" = iv, |
|
86 |
#' "Set proper graphical parameters" = iv_par |
|
87 |
#' )) |
|
88 |
#' ) |
|
89 |
#' |
|
90 |
#' plot(eruptions ~ waiting, faithful, |
|
91 |
#' las = 1, pch = 16, |
|
92 |
#' col = input[["color"]], cex = input[["size"]] |
|
93 |
#' ) |
|
94 |
#' }) |
|
95 |
#' } |
|
96 |
#' |
|
97 |
#' if (interactive()) { |
|
98 |
#' shinyApp(ui, server) |
|
99 |
#' } |
|
100 |
#' |
|
101 |
#' @export |
|
102 |
#' |
|
103 |
validate_inputs <- function(..., header = "Some inputs require attention") { |
|
104 | 36x |
dots <- list(...) |
105 | 2x |
if (!is_validators(dots)) stop("validate_inputs accepts validators or a list thereof") |
106 | ||
107 | 34x |
messages <- extract_validator(dots, header) |
108 | 34x |
failings <- if (!any_names(dots)) { |
109 | 29x |
add_header(messages, header) |
110 |
} else { |
|
111 | 5x |
unlist(messages) |
112 |
} |
|
113 | ||
114 | 34x |
shiny::validate(shiny::need(is.null(failings), failings)) |
115 |
} |
|
116 | ||
117 |
### internal functions |
|
118 | ||
119 |
#' @keywords internal |
|
120 |
# recursive object type test |
|
121 |
# returns logical of length 1 |
|
122 |
is_validators <- function(x) { |
|
123 | 118x |
all(if (is.list(x)) unlist(lapply(x, is_validators)) else inherits(x, "InputValidator")) |
124 |
} |
|
125 | ||
126 |
#' @keywords internal |
|
127 |
# test if an InputValidator object is enabled |
|
128 |
# returns logical of length 1 |
|
129 |
# official method requested at https://github.com/rstudio/shinyvalidate/issues/64 |
|
130 |
validator_enabled <- function(x) { |
|
131 | 49x |
x$.__enclos_env__$private$enabled |
132 |
} |
|
133 | ||
134 |
#' @keywords internal |
|
135 |
# recursively extract messages from validator list |
|
136 |
# returns character vector or a list of character vectors, possibly nested and named |
|
137 |
extract_validator <- function(iv, header) { |
|
138 | 113x |
if (inherits(iv, "InputValidator")) { |
139 | 49x |
add_header(gather_messages(iv), header) |
140 |
} else { |
|
141 | 58x |
if (is.null(names(iv))) names(iv) <- rep("", length(iv)) |
142 | 64x |
mapply(extract_validator, iv = iv, header = names(iv), SIMPLIFY = FALSE) |
143 |
} |
|
144 |
} |
|
145 | ||
146 |
#' @keywords internal |
|
147 |
# collate failing messages from validator |
|
148 |
# returns list |
|
149 |
gather_messages <- function(iv) { |
|
150 | 49x |
if (validator_enabled(iv)) { |
151 | 46x |
status <- iv$validate() |
152 | 46x |
failing_inputs <- Filter(Negate(is.null), status) |
153 | 46x |
unique(lapply(failing_inputs, function(x) x[["message"]])) |
154 |
} else { |
|
155 | 3x |
logger::log_warn("Validator is disabled and will be omitted.") |
156 | 3x |
list() |
157 |
} |
|
158 |
} |
|
159 | ||
160 |
#' @keywords internal |
|
161 |
# add optional header to failing messages |
|
162 |
add_header <- function(messages, header = "") { |
|
163 | 78x |
ans <- unlist(messages) |
164 | 78x |
if (length(ans) != 0L && isTRUE(nchar(header) > 0L)) { |
165 | 31x |
ans <- c(paste0(header, "\n"), ans, "\n") |
166 |
} |
|
167 | 78x |
ans |
168 |
} |
|
169 | ||
170 |
#' @keywords internal |
|
171 |
# recursively check if the object contains a named list |
|
172 |
any_names <- function(x) { |
|
173 | 103x |
any( |
174 | 103x |
if (is.list(x)) { |
175 | 58x |
if (!is.null(names(x)) && any(names(x) != "")) TRUE else unlist(lapply(x, any_names)) |
176 |
} else { |
|
177 | 40x |
FALSE |
178 |
} |
|
179 |
) |
|
180 |
} |
1 |
#' Include `CSS` files from `/inst/css/` package directory to application header |
|
2 |
#' |
|
3 |
#' `system.file` should not be used to access files in other packages, it does |
|
4 |
#' not work with `devtools`. Therefore, we redefine this method in each package |
|
5 |
#' as needed. Thus, we do not export this method |
|
6 |
#' |
|
7 |
#' @param pattern (`character`) pattern of files to be included |
|
8 |
#' |
|
9 |
#' @return HTML code that includes `CSS` files |
|
10 |
#' @keywords internal |
|
11 |
include_css_files <- function(pattern = "*") { |
|
12 | 32x |
css_files <- list.files( |
13 | 32x |
system.file("css", package = "teal", mustWork = TRUE), |
14 | 32x |
pattern = pattern, full.names = TRUE |
15 |
) |
|
16 | 32x |
return( |
17 | 32x |
shiny::singleton( |
18 | 32x |
shiny::tags$head(lapply(css_files, shiny::includeCSS)) |
19 |
) |
|
20 |
) |
|
21 |
} |
|
22 | ||
23 |
#' Include `JS` files from `/inst/js/` package directory to application header |
|
24 |
#' |
|
25 |
#' `system.file` should not be used to access files in other packages, it does |
|
26 |
#' not work with `devtools`. Therefore, we redefine this method in each package |
|
27 |
#' as needed. Thus, we do not export this method |
|
28 |
#' |
|
29 |
#' @param pattern (`character`) pattern of files to be included, passed to `system.file` |
|
30 |
#' @param except (`character`) vector of basename filenames to be excluded |
|
31 |
#' |
|
32 |
#' @return HTML code that includes `JS` files |
|
33 |
#' @keywords internal |
|
34 |
include_js_files <- function(pattern = NULL, except = NULL) { |
|
35 | 32x |
checkmate::assert_character(except, min.len = 1, any.missing = FALSE, null.ok = TRUE) |
36 | 32x |
js_files <- list.files(system.file("js", package = "teal", mustWork = TRUE), pattern = pattern, full.names = TRUE) |
37 | 32x |
js_files <- js_files[!(basename(js_files) %in% except)] # no-op if except is NULL |
38 | ||
39 | 32x |
return(singleton(lapply(js_files, includeScript))) |
40 |
} |
|
41 | ||
42 |
#' Run `JS` file from `/inst/js/` package directory |
|
43 |
#' |
|
44 |
#' This is triggered from the server to execute on the client |
|
45 |
#' rather than triggered directly on the client. |
|
46 |
#' Unlike `include_js_files` which includes `JavaScript` functions, |
|
47 |
#' the `run_js` actually executes `JavaScript` functions. |
|
48 |
#' |
|
49 |
#' `system.file` should not be used to access files in other packages, it does |
|
50 |
#' not work with `devtools`. Therefore, we redefine this method in each package |
|
51 |
#' as needed. Thus, we do not export this method |
|
52 |
#' |
|
53 |
#' @param files (`character`) vector of filenames |
|
54 |
#' @keywords internal |
|
55 |
run_js_files <- function(files) { |
|
56 | 8x |
checkmate::assert_character(files, min.len = 1, any.missing = FALSE) |
57 | 8x |
lapply(files, function(file) { |
58 | 8x |
shinyjs::runjs(paste0(readLines(system.file("js", file, package = "teal", mustWork = TRUE)), collapse = "\n")) |
59 |
}) |
|
60 | 8x |
return(invisible(NULL)) |
61 |
} |
|
62 | ||
63 |
#' Code to include teal `CSS` and `JavaScript` files |
|
64 |
#' |
|
65 |
#' This is useful when you want to use the same `JavaScript` and `CSS` files that are |
|
66 |
#' used with the teal application. |
|
67 |
#' This is also useful for running standalone modules in teal with the correct |
|
68 |
#' styles. |
|
69 |
#' Also initializes `shinyjs` so you can use it. |
|
70 |
#' |
|
71 |
#' @return HTML code to include |
|
72 |
#' @examples |
|
73 |
#' shiny_ui <- tagList( |
|
74 |
#' teal:::include_teal_css_js(), |
|
75 |
#' p("Hello") |
|
76 |
#' ) |
|
77 |
#' @keywords internal |
|
78 |
include_teal_css_js <- function() { |
|
79 | 32x |
tagList( |
80 | 32x |
shinyjs::useShinyjs(), |
81 | 32x |
include_css_files(), |
82 |
# init.js is executed from the server |
|
83 | 32x |
include_js_files(except = "init.js"), |
84 | 32x |
shinyjs::hidden(icon("gear")), # add hidden icon to load font-awesome css for icons |
85 |
) |
|
86 |
} |
1 |
# This module is the main teal module that puts everything together. |
|
2 | ||
3 |
#' teal main app module |
|
4 |
#' |
|
5 |
#' This is the main teal app that puts everything together. |
|
6 |
#' |
|
7 |
#' It displays the splash UI which is used to fetch the data, possibly |
|
8 |
#' prompting for a password input to fetch the data. Once the data is ready, |
|
9 |
#' the splash screen is replaced by the actual teal UI that is tabsetted and |
|
10 |
#' has a filter panel with `datanames` that are relevant for the current tab. |
|
11 |
#' Nested tabs are possible, but we limit it to two nesting levels for reasons |
|
12 |
#' of clarity of the UI. |
|
13 |
#' |
|
14 |
#' The splash screen functionality can also be used |
|
15 |
#' for non-delayed data which takes time to load into memory, avoiding |
|
16 |
#' Shiny session timeouts. |
|
17 |
#' |
|
18 |
#' Server evaluates the `raw_data` (delayed data mechanism) and creates the |
|
19 |
#' `datasets` object that is shared across modules. |
|
20 |
#' Once it is ready and non-`NULL`, the splash screen is replaced by the |
|
21 |
#' main teal UI that depends on the data. |
|
22 |
#' The currently active tab is tracked and the right filter panel |
|
23 |
#' updates the displayed datasets to filter for according to the active `datanames` |
|
24 |
#' of the tab. |
|
25 |
#' |
|
26 |
#' It is written as a Shiny module so it can be added into other apps as well. |
|
27 |
#' |
|
28 |
#' @name module_teal |
|
29 |
#' |
|
30 |
#' @inheritParams ui_teal_with_splash |
|
31 |
#' |
|
32 |
#' @param splash_ui (`shiny.tag`)\cr UI to display initially, |
|
33 |
#' can be a splash screen or a Shiny module UI. For the latter, see |
|
34 |
#' [init()] about how to call the corresponding server function. |
|
35 |
#' |
|
36 |
#' @param raw_data (`reactive`)\cr |
|
37 |
#' returns the `TealData`, only evaluated once, `NULL` value is ignored |
|
38 |
#' |
|
39 |
#' @return |
|
40 |
#' `ui_teal` returns `HTML` for Shiny module UI. |
|
41 |
#' `srv_teal` returns `reactive` which returns the currently active module. |
|
42 |
#' |
|
43 |
#' @keywords internal |
|
44 |
#' |
|
45 |
#' @examples |
|
46 |
#' mods <- teal:::example_modules() |
|
47 |
#' raw_data <- reactive(teal:::example_cdisc_data()) |
|
48 |
#' app <- shinyApp( |
|
49 |
#' ui = function() { |
|
50 |
#' teal:::ui_teal("dummy") |
|
51 |
#' }, |
|
52 |
#' server = function(input, output, session) { |
|
53 |
#' active_module <- teal:::srv_teal(id = "dummy", modules = mods, raw_data = raw_data) |
|
54 |
#' } |
|
55 |
#' ) |
|
56 |
#' if (interactive()) { |
|
57 |
#' runApp(app) |
|
58 |
#' } |
|
59 |
NULL |
|
60 | ||
61 |
#' @rdname module_teal |
|
62 |
ui_teal <- function(id, |
|
63 |
splash_ui = tags$h2("Starting the Teal App"), |
|
64 |
title = NULL, |
|
65 |
header = tags$p(""), |
|
66 |
footer = tags$p("")) { |
|
67 | 32x |
if (checkmate::test_string(header)) { |
68 | ! |
header <- tags$h1(header) |
69 |
} |
|
70 | 32x |
if (checkmate::test_string(footer)) { |
71 | ! |
footer <- tags$p(footer) |
72 |
} |
|
73 | 32x |
checkmate::assert( |
74 | 32x |
checkmate::check_class(splash_ui, "shiny.tag"), |
75 | 32x |
checkmate::check_class(splash_ui, "shiny.tag.list"), |
76 | 32x |
checkmate::check_class(splash_ui, "html") |
77 |
) |
|
78 | 32x |
checkmate::assert( |
79 | 32x |
checkmate::check_class(header, "shiny.tag"), |
80 | 32x |
checkmate::check_class(header, "shiny.tag.list"), |
81 | 32x |
checkmate::check_class(header, "html") |
82 |
) |
|
83 | 32x |
checkmate::assert( |
84 | 32x |
checkmate::check_class(footer, "shiny.tag"), |
85 | 32x |
checkmate::check_class(footer, "shiny.tag.list"), |
86 | 32x |
checkmate::check_class(footer, "html") |
87 |
) |
|
88 | ||
89 | 32x |
ns <- NS(id) |
90 |
# Once the data is loaded, we will remove this element and add the real teal UI instead |
|
91 | 32x |
splash_ui <- div( |
92 |
# id so we can remove the splash screen once ready, which is the first child of this container |
|
93 | 32x |
id = ns("main_ui_container"), |
94 |
# we put it into a div, so it can easily be removed as a whole, also when it is a tagList (and not |
|
95 |
# just the first item of the tagList) |
|
96 | 32x |
div(splash_ui) |
97 |
) |
|
98 | ||
99 |
# show busy icon when shiny session is busy computing stuff |
|
100 |
# based on https://stackoverflow.com/questions/17325521/r-shiny-display-loading-message-while-function-is-running/22475216#22475216 #nolint |
|
101 | 32x |
shiny_busy_message_panel <- conditionalPanel( |
102 | 32x |
condition = "(($('html').hasClass('shiny-busy')) && (document.getElementById('shiny-notification-panel') == null))", # nolint |
103 | 32x |
div( |
104 | 32x |
icon("arrows-rotate", "spin fa-spin"), |
105 | 32x |
"Computing ...", |
106 |
# CSS defined in `custom.css` |
|
107 | 32x |
class = "shinybusymessage" |
108 |
) |
|
109 |
) |
|
110 | ||
111 | 32x |
res <- fluidPage( |
112 | 32x |
title = title, |
113 | 32x |
theme = get_teal_bs_theme(), |
114 | 32x |
include_teal_css_js(), |
115 | 32x |
tags$header(header), |
116 | 32x |
tags$hr(class = "my-2"), |
117 | 32x |
shiny_busy_message_panel, |
118 | 32x |
splash_ui, |
119 | 32x |
tags$hr(), |
120 | 32x |
tags$footer( |
121 | 32x |
div( |
122 | 32x |
footer, |
123 | 32x |
teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"), |
124 | 32x |
textOutput(ns("identifier")) |
125 |
) |
|
126 |
) |
|
127 |
) |
|
128 | 32x |
return(res) |
129 |
} |
|
130 | ||
131 | ||
132 |
#' @rdname module_teal |
|
133 |
srv_teal <- function(id, modules, raw_data, filter = teal_slices()) { |
|
134 | 9x |
stopifnot(is.reactive(raw_data)) |
135 | 8x |
moduleServer(id, function(input, output, session) { |
136 | 8x |
logger::log_trace("srv_teal initializing the module.") |
137 | ||
138 | 8x |
output$identifier <- renderText( |
139 | 8x |
paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32)) |
140 |
) |
|
141 | ||
142 | 8x |
teal.widgets::verbatim_popup_srv( |
143 | 8x |
"sessionInfo", |
144 | 8x |
verbatim_content = utils::capture.output(utils::sessionInfo()), |
145 | 8x |
title = "SessionInfo" |
146 |
) |
|
147 | ||
148 |
# `JavaScript` code |
|
149 | 8x |
run_js_files(files = "init.js") # `JavaScript` code to make the clipboard accessible |
150 |
# set timezone in shiny app |
|
151 |
# timezone is set in the early beginning so it will be available also |
|
152 |
# for `DDL` and all shiny modules |
|
153 | 8x |
get_client_timezone(session$ns) |
154 | 8x |
observeEvent( |
155 | 8x |
eventExpr = input$timezone, |
156 | 8x |
once = TRUE, |
157 | 8x |
handlerExpr = { |
158 | ! |
session$userData$timezone <- input$timezone |
159 | ! |
logger::log_trace("srv_teal@1 Timezone set to client's timezone: { input$timezone }.") |
160 |
} |
|
161 |
) |
|
162 | ||
163 |
# loading the data ----- |
|
164 | 8x |
env <- environment() |
165 | 8x |
datasets_reactive <- reactive({ |
166 | 6x |
if (is.null(raw_data())) { |
167 | 1x |
return(NULL) |
168 |
} |
|
169 | 5x |
env$progress <- shiny::Progress$new(session) |
170 | 5x |
env$progress$set(0.25, message = "Setting data") |
171 | ||
172 |
# create a list of data following structure of the nested modules list structure. |
|
173 |
# Because it's easier to unpack modules and datasets when they follow the same nested structure. |
|
174 | 5x |
datasets_singleton <- teal.slice::init_filtered_data(raw_data()) |
175 |
# Singleton starts with only global filters active. |
|
176 | 5x |
filter_global <- Filter(function(x) x$id %in% attr(filter, "mapping")$global_filters, filter) |
177 | 5x |
datasets_singleton$set_filter_state(filter_global) |
178 | 5x |
module_datasets <- function(modules) { |
179 | 19x |
if (inherits(modules, "teal_modules")) { |
180 | 8x |
datasets <- lapply(modules$children, module_datasets) |
181 | 8x |
labels <- vapply(modules$children, `[[`, character(1), "label") |
182 | 8x |
names(datasets) <- labels |
183 | 8x |
datasets |
184 | 11x |
} else if (isTRUE(attr(filter, "module_specific"))) { |
185 |
# we should create FilteredData even if modules$datanames is null |
|
186 |
# null controls a display of filter panel but data should be still passed |
|
187 | 3x |
datanames <- if (is.null(modules$datanames)) raw_data()$get_datanames() else modules$datanames |
188 | 3x |
data_objects <- sapply( |
189 | 3x |
datanames, |
190 | 3x |
function(dataname) { |
191 | 6x |
dataset <- raw_data()$get_dataset(dataname) |
192 | 6x |
list( |
193 | 6x |
dataset = dataset$get_raw_data(), |
194 | 6x |
metadata = dataset$get_metadata(), |
195 | 6x |
label = dataset$get_dataset_label() |
196 |
) |
|
197 |
}, |
|
198 | 3x |
simplify = FALSE |
199 |
) |
|
200 | 3x |
datasets_module <- teal.slice::init_filtered_data( |
201 | 3x |
data_objects, |
202 | 3x |
join_keys = raw_data()$get_join_keys(), |
203 | 3x |
code = raw_data()$get_code_class(), |
204 | 3x |
check = raw_data()$get_check() |
205 |
) |
|
206 | ||
207 |
# set initial filters |
|
208 | 3x |
slices <- Filter(x = filter, f = function(x) { |
209 | ! |
x$id %in% unique(unlist(attr(filter, "mapping")[c(modules$label, "global_filters")])) && |
210 | ! |
x$dataname %in% datanames |
211 |
}) |
|
212 | 3x |
include_varnames <- attr(slices, "include_varnames")[names(attr(slices, "include_varnames")) %in% datanames] |
213 | 3x |
exclude_varnames <- attr(slices, "exclude_varnames")[names(attr(slices, "exclude_varnames")) %in% datanames] |
214 | 3x |
slices$include_varnames <- include_varnames |
215 | 3x |
slices$exclude_varnames <- exclude_varnames |
216 | 3x |
datasets_module$set_filter_state(slices) |
217 | 3x |
datasets_module |
218 |
} else { |
|
219 | 8x |
datasets_singleton |
220 |
} |
|
221 |
} |
|
222 | 5x |
datasets <- module_datasets(modules) |
223 | ||
224 | 5x |
logger::log_trace("srv_teal@4 Raw Data transferred to FilteredData.") |
225 | 5x |
datasets |
226 |
}) |
|
227 | ||
228 | 8x |
reporter <- teal.reporter::Reporter$new() |
229 | 8x |
is_any_previewer <- function(modules) { |
230 | ! |
if (inherits(modules, "teal_modules")) { |
231 | ! |
any(unlist(lapply(modules$children, is_any_previewer), use.names = FALSE)) |
232 | ! |
} else if (inherits(modules, "teal_module_previewer")) { |
233 | ! |
TRUE |
234 |
} else { |
|
235 | ! |
FALSE |
236 |
} |
|
237 |
} |
|
238 | 8x |
if (is_arg_used(modules, "reporter") && !is_any_previewer(modules)) { |
239 | ! |
modules <- append_module(modules, reporter_previewer_module()) |
240 |
} |
|
241 | ||
242 |
# Replace splash / welcome screen once data is loaded ---- |
|
243 |
# ignoreNULL to not trigger at the beginning when data is NULL |
|
244 |
# just handle it once because data obtained through delayed loading should |
|
245 |
# usually not change afterwards |
|
246 |
# if restored from bookmarked state, `filter` is ignored |
|
247 | 8x |
observeEvent(datasets_reactive(), ignoreNULL = TRUE, once = TRUE, { |
248 | 1x |
logger::log_trace("srv_teal@5 setting main ui after data was pulled") |
249 | 1x |
env$progress$set(0.5, message = "Setting up main UI") |
250 | 1x |
on.exit(env$progress$close()) |
251 |
# main_ui_container contains splash screen first and we remove it and replace it by the real UI |
|
252 | ||
253 | 1x |
removeUI(sprintf("#%s:first-child", session$ns("main_ui_container"))) |
254 | 1x |
insertUI( |
255 | 1x |
selector = paste0("#", session$ns("main_ui_container")), |
256 | 1x |
where = "beforeEnd", |
257 |
# we put it into a div, so it can easily be removed as a whole, also when it is a tagList (and not |
|
258 |
# just the first item of the tagList) |
|
259 | 1x |
ui = div(ui_tabs_with_filters( |
260 | 1x |
session$ns("main_ui"), |
261 | 1x |
modules = modules, |
262 | 1x |
datasets = datasets_reactive(), |
263 | 1x |
filter = filter |
264 |
)), |
|
265 |
# needed so that the UI inputs are available and can be immediately updated, otherwise, updating may not |
|
266 |
# have any effect as they are ignored when not present |
|
267 | 1x |
immediate = TRUE |
268 |
) |
|
269 | ||
270 |
# must make sure that this is only executed once as modules assume their observers are only |
|
271 |
# registered once (calling server functions twice would trigger observers twice each time) |
|
272 | 1x |
active_module <- srv_tabs_with_filters( |
273 | 1x |
id = "main_ui", |
274 | 1x |
datasets = datasets_reactive(), |
275 | 1x |
modules = modules, |
276 | 1x |
reporter = reporter, |
277 | 1x |
filter = filter |
278 |
) |
|
279 | 1x |
return(active_module) |
280 |
}) |
|
281 |
}) |
|
282 |
} |
1 |
#' Get dummy `CDISC` data |
|
2 |
#' |
|
3 |
#' Get dummy `CDISC` data including `ADSL`, `ADAE` and `ADLB`. |
|
4 |
#' Some NAs are also introduced to stress test. |
|
5 |
#' |
|
6 |
#' @return `cdisc_data` |
|
7 |
#' @keywords internal |
|
8 |
example_cdisc_data <- function() { # nolint |
|
9 | ! |
ADSL <- data.frame( # nolint |
10 | ! |
STUDYID = "study", |
11 | ! |
USUBJID = 1:10, |
12 | ! |
SEX = sample(c("F", "M"), 10, replace = TRUE), |
13 | ! |
AGE = stats::rpois(10, 40) |
14 |
) |
|
15 | ! |
ADTTE <- rbind(ADSL, ADSL, ADSL) # nolint |
16 | ! |
ADTTE$PARAMCD <- rep(c("OS", "EFS", "PFS"), each = 10) # nolint |
17 | ! |
ADTTE$AVAL <- c( # nolint |
18 | ! |
stats::rnorm(10, mean = 700, sd = 200), # dummy OS level |
19 | ! |
stats::rnorm(10, mean = 400, sd = 100), # dummy EFS level |
20 | ! |
stats::rnorm(10, mean = 450, sd = 200) # dummy PFS level |
21 |
) |
|
22 | ||
23 | ! |
ADSL$logical_test <- sample(c(TRUE, FALSE, NA), size = nrow(ADSL), replace = TRUE) # nolint |
24 | ! |
ADSL$SEX[c(2, 5)] <- NA # nolint |
25 | ||
26 | ! |
cdisc_data_obj <- teal.data::cdisc_data( |
27 | ! |
cdisc_dataset(dataname = "ADSL", x = ADSL), |
28 | ! |
cdisc_dataset(dataname = "ADTTE", x = ADTTE) |
29 |
) |
|
30 | ||
31 | ! |
res <- teal.data::cdisc_data( |
32 | ! |
teal.data::cdisc_dataset(dataname = "ADSL", x = ADSL), |
33 | ! |
teal.data::cdisc_dataset(dataname = "ADTTE", x = ADTTE), |
34 | ! |
code = ' |
35 | ! |
ADSL <- data.frame( |
36 | ! |
STUDYID = "study", |
37 | ! |
USUBJID = 1:10, |
38 | ! |
SEX = sample(c("F", "M"), 10, replace = TRUE), |
39 | ! |
AGE = rpois(10, 40) |
40 |
) |
|
41 | ! |
ADTTE <- rbind(ADSL, ADSL, ADSL) |
42 | ! |
ADTTE$PARAMCD <- rep(c("OS", "EFS", "PFS"), each = 10) |
43 | ! |
ADTTE$AVAL <- c( |
44 | ! |
rnorm(10, mean = 700, sd = 200), |
45 | ! |
rnorm(10, mean = 400, sd = 100), |
46 | ! |
rnorm(10, mean = 450, sd = 200) |
47 |
) |
|
48 | ||
49 | ! |
ADSL$logical_test <- sample(c(TRUE, FALSE, NA), size = nrow(ADSL), replace = TRUE) |
50 | ! |
ADSL$SEX[c(2, 5)] <- NA |
51 |
' |
|
52 |
) |
|
53 | ! |
return(res) |
54 |
} |
|
55 | ||
56 |
#' Get datasets to go with example modules. |
|
57 |
#' |
|
58 |
#' Creates a nested list, the structure of which matches the module hierarchy created by `example_modules`. |
|
59 |
#' Each list leaf is the same `FilteredData` object. |
|
60 |
#' |
|
61 |
#' @return named list of `FilteredData` objects, each with `ADSL` set. |
|
62 |
#' @keywords internal |
|
63 |
example_datasets <- function() { # nolint |
|
64 | ! |
dummy_cdisc_data <- example_cdisc_data() |
65 | ! |
datasets <- teal.slice::init_filtered_data(dummy_cdisc_data) |
66 | ! |
list( |
67 | ! |
"d2" = list( |
68 | ! |
"d3" = list( |
69 | ! |
"aaa1" = datasets, |
70 | ! |
"aaa2" = datasets, |
71 | ! |
"aaa3" = datasets |
72 |
), |
|
73 | ! |
"bbb" = datasets |
74 |
), |
|
75 | ! |
"ccc" = datasets |
76 |
) |
|
77 |
} |
|
78 | ||
79 |
#' An example `teal` module |
|
80 |
#' |
|
81 |
#' @description `r lifecycle::badge("experimental")` |
|
82 |
#' @inheritParams module |
|
83 |
#' @return A `teal` module which can be included in the `modules` argument to [teal::init()]. |
|
84 |
#' @examples |
|
85 |
#' app <- init( |
|
86 |
#' data = teal_data( |
|
87 |
#' dataset("IRIS", iris), |
|
88 |
#' dataset("MTCARS", mtcars) |
|
89 |
#' ), |
|
90 |
#' modules = example_module() |
|
91 |
#' ) |
|
92 |
#' if (interactive()) { |
|
93 |
#' shinyApp(app$ui, app$server) |
|
94 |
#' } |
|
95 |
#' @export |
|
96 |
example_module <- function(label = "example teal module", datanames = "all") { |
|
97 | 15x |
checkmate::assert_string(label) |
98 | 15x |
module( |
99 | 15x |
label, |
100 | 15x |
server = function(id, data) { |
101 | ! |
checkmate::assert_class(data, "tdata") |
102 | ! |
moduleServer(id, function(input, output, session) { |
103 | ! |
output$text <- renderPrint(data[[input$dataname]]()) |
104 |
}) |
|
105 |
}, |
|
106 | 15x |
ui = function(id, data) { |
107 | ! |
ns <- NS(id) |
108 | ! |
teal.widgets::standard_layout( |
109 | ! |
output = verbatimTextOutput(ns("text")), |
110 | ! |
encoding = selectInput(ns("dataname"), "Choose a dataset", choices = names(data)) |
111 |
) |
|
112 |
}, |
|
113 | 15x |
datanames = datanames |
114 |
) |
|
115 |
} |
|
116 | ||
117 | ||
118 |
#' Get example modules. |
|
119 |
#' |
|
120 |
#' Creates an example hierarchy of `teal_modules` from which a `teal` app can be created. |
|
121 |
#' @param datanames (`character`)\cr |
|
122 |
#' names of the datasets to be used in the example modules. Possible choices are `ADSL`, `ADTTE`. |
|
123 |
#' @return `teal_modules` |
|
124 |
#' @keywords internal |
|
125 |
example_modules <- function(datanames = c("ADSL", "ADTTE")) { |
|
126 | 2x |
checkmate::assert_subset(datanames, c("ADSL", "ADTTE")) |
127 | 2x |
mods <- modules( |
128 | 2x |
label = "d1", |
129 | 2x |
modules( |
130 | 2x |
label = "d2", |
131 | 2x |
modules( |
132 | 2x |
label = "d3", |
133 | 2x |
example_module(label = "aaa1", datanames = datanames), |
134 | 2x |
example_module(label = "aaa2", datanames = datanames), |
135 | 2x |
example_module(label = "aaa3", datanames = datanames) |
136 |
), |
|
137 | 2x |
example_module(label = "bbb", datanames = datanames) |
138 |
), |
|
139 | 2x |
example_module(label = "ccc", datanames = datanames) |
140 |
) |
|
141 | 2x |
return(mods) |
142 |
} |
1 |
#' Get Client Timezone |
|
2 |
#' |
|
3 |
#' Local timezone in the browser may differ from the system timezone from the server. |
|
4 |
#' This script can be run to register a shiny input which contains information about |
|
5 |
#' the timezone in the browser. |
|
6 |
#' |
|
7 |
#' @param ns (`function`) namespace function passed from the `session` object in the |
|
8 |
#' Shiny server. For Shiny modules this will allow for proper name spacing of the |
|
9 |
#' registered input. |
|
10 |
#' |
|
11 |
#' @return (`Shiny`) input variable accessible with `input$tz` which is a (`character`) |
|
12 |
#' string containing the timezone of the browser/client. |
|
13 |
#' @keywords internal |
|
14 |
get_client_timezone <- function(ns) { |
|
15 | 8x |
script <- sprintf( |
16 | 8x |
"Shiny.setInputValue(`%s`, Intl.DateTimeFormat().resolvedOptions().timeZone)", |
17 | 8x |
ns("timezone") |
18 |
) |
|
19 | 8x |
shinyjs::runjs(script) # function does not return anything |
20 | 8x |
return(invisible(NULL)) |
21 |
} |
|
22 | ||
23 |
#' Resolve the expected bootstrap theme |
|
24 |
#' @keywords internal |
|
25 |
get_teal_bs_theme <- function() { |
|
26 | 36x |
bs_theme <- getOption("teal.bs_theme") |
27 | 36x |
if (is.null(bs_theme)) { |
28 | 33x |
NULL |
29 | 3x |
} else if (!inherits(bs_theme, "bs_theme")) { |
30 | 2x |
warning("teal.bs_theme has to be of a bslib::bs_theme class, the default shiny bootstrap is used.") |
31 | 2x |
NULL |
32 |
} else { |
|
33 | 1x |
bs_theme |
34 |
} |
|
35 |
} |
|
36 | ||
37 |
include_parent_datanames <- function(dataname, join_keys) { |
|
38 | 145x |
parents <- character(0) |
39 | 145x |
for (i in dataname) { |
40 | 25x |
while (length(i) > 0) { |
41 | 25x |
parent_i <- join_keys$get_parent(i) |
42 | 25x |
parents <- c(parent_i, parents) |
43 | 25x |
i <- parent_i |
44 |
} |
|
45 |
} |
|
46 | ||
47 | 145x |
return(unique(c(parents, dataname))) |
48 |
} |
1 |
#' Add right filter panel into each of the top-level `teal_modules` UIs. |
|
2 |
#' |
|
3 |
#' The [ui_nested_tabs] function returns a nested tabbed UI corresponding |
|
4 |
#' to the nested modules. |
|
5 |
#' This function adds the right filter panel to each main tab. |
|
6 |
#' |
|
7 |
#' The right filter panel's filter choices affect the `datasets` object. Therefore, |
|
8 |
#' all modules using the same `datasets` share the same filters. |
|
9 |
#' |
|
10 |
#' This works with nested modules of depth greater than 2, though the filter |
|
11 |
#' panel is inserted at the right of the modules at depth 1 and not at the leaves. |
|
12 |
#' |
|
13 |
#' @name module_tabs_with_filters |
|
14 |
#' |
|
15 |
#' @inheritParams module_teal |
|
16 |
#' |
|
17 |
#' @param datasets (`named list` of `FilteredData`)\cr |
|
18 |
#' object to store filter state and filtered datasets, shared across modules. For more |
|
19 |
#' details see [`teal.slice::FilteredData`]. Structure of the list must be the same as structure |
|
20 |
#' of the `modules` argument and list names must correspond to the labels in `modules`. |
|
21 |
#' When filter is not module-specific then list contains the same object in all elements. |
|
22 |
#' @param reporter (`Reporter`) object from `teal.reporter` |
|
23 |
#' |
|
24 |
#' @return A `tagList` of The main menu, place holders for filters and |
|
25 |
#' place holders for the teal modules |
|
26 |
#' |
|
27 |
#' |
|
28 |
#' @keywords internal |
|
29 |
#' |
|
30 |
#' @examples |
|
31 |
#' |
|
32 |
#' mods <- teal:::example_modules() |
|
33 |
#' datasets <- teal:::example_datasets() |
|
34 |
#' |
|
35 |
#' app <- shinyApp( |
|
36 |
#' ui = function() { |
|
37 |
#' tagList( |
|
38 |
#' teal:::include_teal_css_js(), |
|
39 |
#' textOutput("info"), |
|
40 |
#' fluidPage( # needed for nice tabs |
|
41 |
#' ui_tabs_with_filters("dummy", modules = mods, datasets = datasets) |
|
42 |
#' ) |
|
43 |
#' ) |
|
44 |
#' }, |
|
45 |
#' server = function(input, output, session) { |
|
46 |
#' output$info <- renderText({ |
|
47 |
#' paste0("The currently active tab name is ", active_module()$label) |
|
48 |
#' }) |
|
49 |
#' active_module <- srv_tabs_with_filters(id = "dummy", datasets = datasets, modules = mods) |
|
50 |
#' } |
|
51 |
#' ) |
|
52 |
#' if (interactive()) { |
|
53 |
#' runApp(app) |
|
54 |
#' } |
|
55 |
#' |
|
56 |
NULL |
|
57 | ||
58 |
#' @rdname module_tabs_with_filters |
|
59 |
ui_tabs_with_filters <- function(id, modules, datasets, filter = teal_slices()) { |
|
60 | 1x |
checkmate::assert_class(modules, "teal_modules") |
61 | 1x |
checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
62 | 1x |
checkmate::assert_class(filter, "teal_slices") |
63 | ||
64 | 1x |
ns <- NS(id) |
65 | 1x |
is_module_specific <- isTRUE(attr(filter, "module_specific")) |
66 | ||
67 | 1x |
teal_ui <- ui_nested_tabs(ns("root"), modules = modules, datasets, is_module_specific = is_module_specific) |
68 | 1x |
filter_panel_btns <- tags$li( |
69 | 1x |
class = "flex-grow", |
70 | 1x |
tags$button( |
71 | 1x |
class = "btn action-button filter_hamburger", # see sidebar.css for style filter_hamburger |
72 | 1x |
href = "javascript:void(0)", |
73 | 1x |
onclick = "toggleFilterPanel();", # see sidebar.js |
74 | 1x |
title = "Toggle filter panels", |
75 | 1x |
icon("fas fa-bars") |
76 |
), |
|
77 | 1x |
filter_manager_modal_ui(ns("filter_manager")) |
78 |
) |
|
79 | 1x |
teal_ui$children[[1]] <- tagAppendChild(teal_ui$children[[1]], filter_panel_btns) |
80 | ||
81 | 1x |
if (!is_module_specific) { |
82 |
# need to rearrange html so that filter panel is within tabset |
|
83 | 1x |
tabset_bar <- teal_ui$children[[1]] |
84 | 1x |
teal_modules <- teal_ui$children[[2]] |
85 | 1x |
filter_ui <- unlist(datasets)[[1]]$ui_filter_panel(ns("filter_panel")) |
86 | 1x |
list( |
87 | 1x |
tabset_bar, |
88 | 1x |
tags$hr(class = "my-2"), |
89 | 1x |
fluidRow( |
90 | 1x |
column(width = 9, teal_modules, class = "teal_primary_col"), |
91 | 1x |
column(width = 3, filter_ui, class = "teal_secondary_col") |
92 |
) |
|
93 |
) |
|
94 |
} else { |
|
95 | ! |
teal_ui |
96 |
} |
|
97 |
} |
|
98 | ||
99 |
#' @rdname module_tabs_with_filters |
|
100 |
srv_tabs_with_filters <- function(id, |
|
101 |
datasets, |
|
102 |
modules, |
|
103 |
reporter = teal.reporter::Reporter$new(), |
|
104 |
filter = teal_slices()) { |
|
105 | 6x |
checkmate::assert_class(modules, "teal_modules") |
106 | 6x |
checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
107 | 6x |
checkmate::assert_class(reporter, "Reporter") |
108 | 4x |
checkmate::assert_class(filter, "teal_slices") |
109 | ||
110 | 4x |
moduleServer(id, function(input, output, session) { |
111 | 4x |
logger::log_trace("srv_tabs_with_filters initializing the module.") |
112 | ||
113 | 4x |
is_module_specific <- isTRUE(attr(filter, "module_specific")) |
114 | 4x |
manager_out <- filter_manager_modal_srv("filter_manager", filtered_data_list = datasets, filter = filter) |
115 | ||
116 | 4x |
active_module <- srv_nested_tabs( |
117 | 4x |
id = "root", |
118 | 4x |
datasets = datasets, |
119 | 4x |
modules = modules, |
120 | 4x |
reporter = reporter, |
121 | 4x |
is_module_specific = is_module_specific |
122 |
) |
|
123 | ||
124 | 4x |
if (!is_module_specific) { |
125 | 4x |
active_datanames <- reactive(active_module()$datanames) |
126 | 4x |
singleton <- unlist(datasets)[[1]] |
127 | 4x |
singleton$srv_filter_panel("filter_panel", active_datanames = active_datanames) |
128 | ||
129 | 4x |
observeEvent( |
130 | 4x |
eventExpr = active_datanames(), |
131 | 4x |
handlerExpr = { |
132 | 5x |
script <- if (length(active_datanames()) == 0 || is.null(active_datanames())) { |
133 |
# hide the filter panel and disable the burger button |
|
134 | 1x |
"handleNoActiveDatasets();" |
135 |
} else { |
|
136 |
# show the filter panel and enable the burger button |
|
137 | 4x |
"handleActiveDatasetsPresent();" |
138 |
} |
|
139 | 5x |
shinyjs::runjs(script) |
140 |
}, |
|
141 | 4x |
ignoreNULL = FALSE |
142 |
) |
|
143 |
} |
|
144 | ||
145 | 4x |
showNotification("Data loaded - App fully started up") |
146 | 4x |
logger::log_trace("srv_tabs_with_filters initialized the module") |
147 | 4x |
return(active_module) |
148 |
}) |
|
149 |
} |
1 |
# This is the main function from teal to be used by the end-users. Although it delegates |
|
2 |
# directly to `module_teal_with_splash.R`, we keep it in a separate file because its doc is quite large |
|
3 |
# and it is very end-user oriented. It may also perform more argument checking with more informative |
|
4 |
# error messages. |
|
5 | ||
6 | ||
7 |
#' Create the Server and UI Function For the Shiny App |
|
8 |
#' |
|
9 |
#' @description `r lifecycle::badge("stable")` |
|
10 |
#' End-users: This is the most important function for you to start a |
|
11 |
#' teal app that is composed out of teal modules. |
|
12 |
#' |
|
13 |
#' **Notes for developers**: |
|
14 |
#' This is a wrapper function around the `module_teal.R` functions. Unless you are |
|
15 |
#' an end-user, don't use this function, but instead this module. |
|
16 |
#' |
|
17 |
#' @param data (`TealData` or `TealDataset` or `TealDatasetConnector` or `list` or `data.frame` |
|
18 |
#' or `MultiAssayExperiment`)\cr |
|
19 |
#' `R6` object as returned by [teal.data::cdisc_data()], [teal.data::teal_data()], |
|
20 |
#' [teal.data::cdisc_dataset()], [teal.data::dataset()], [teal.data::dataset_connector()] or |
|
21 |
#' [teal.data::cdisc_dataset_connector()] or a single `data.frame` or a `MultiAssayExperiment` |
|
22 |
#' or a list of the previous objects or function returning a named list. |
|
23 |
#' NOTE: teal does not guarantee reproducibility of the code when names of the list elements |
|
24 |
#' do not match the original object names. To ensure reproducibility please use [teal.data::teal_data()] |
|
25 |
#' or [teal.data::cdisc_data()] with `check = TRUE` enabled. |
|
26 |
#' @param modules (`list`, `teal_modules` or `teal_module`)\cr |
|
27 |
#' nested list of `teal_modules` or `teal_module` objects or a single |
|
28 |
#' `teal_modules` or `teal_module` object. These are the specific output modules which |
|
29 |
#' will be displayed in the teal application. See [modules()] and [module()] for |
|
30 |
#' more details. |
|
31 |
#' @param title (`NULL` or `character`)\cr |
|
32 |
#' The browser window title (defaults to the host URL of the page). |
|
33 |
#' @param filter (`teal_slices`)\cr |
|
34 |
#' Specification of initial filter. Filters can be specified using [teal::teal_slices()]. |
|
35 |
#' Old way of specifying filters through a list is deprecated and will be removed in the |
|
36 |
#' next release. Please fix your applications to use [teal::teal_slices()]. |
|
37 |
#' @param header (`shiny.tag` or `character`) \cr |
|
38 |
#' the header of the app. Note shiny code placed here (and in the footer |
|
39 |
#' argument) will be placed in the app's `ui` function so code which needs to be placed in the `ui` function |
|
40 |
#' (such as loading `CSS` via [htmltools::htmlDependency()]) should be included here. |
|
41 |
#' @param footer (`shiny.tag` or `character`)\cr |
|
42 |
#' the footer of the app |
|
43 |
#' @param id (`character`)\cr |
|
44 |
#' module id to embed it, if provided, |
|
45 |
#' the server function must be called with [shiny::moduleServer()]; |
|
46 |
#' See the vignette for an example. However, [ui_teal_with_splash()] |
|
47 |
#' is then preferred to this function. |
|
48 |
#' |
|
49 |
#' @return named list with `server` and `ui` function |
|
50 |
#' |
|
51 |
#' @export |
|
52 |
#' |
|
53 |
#' @include modules.R |
|
54 |
#' |
|
55 |
#' @examples |
|
56 |
#' new_iris <- transform(iris, id = seq_len(nrow(iris))) |
|
57 |
#' new_mtcars <- transform(mtcars, id = seq_len(nrow(mtcars))) |
|
58 |
#' |
|
59 |
#' app <- init( |
|
60 |
#' data = teal_data( |
|
61 |
#' dataset("new_iris", new_iris), |
|
62 |
#' dataset("new_mtcars", new_mtcars), |
|
63 |
#' code = " |
|
64 |
#' new_iris <- transform(iris, id = seq_len(nrow(iris))) |
|
65 |
#' new_mtcars <- transform(mtcars, id = seq_len(nrow(mtcars))) |
|
66 |
#' " |
|
67 |
#' ), |
|
68 |
#' modules = modules( |
|
69 |
#' module( |
|
70 |
#' label = "data source", |
|
71 |
#' server = function(input, output, session, data) {}, |
|
72 |
#' ui = function(id, ...) div(p("information about data source")), |
|
73 |
#' datanames = "all" |
|
74 |
#' ), |
|
75 |
#' example_module(label = "example teal module"), |
|
76 |
#' module( |
|
77 |
#' "Iris Sepal.Length histogram", |
|
78 |
#' server = function(input, output, session, data) { |
|
79 |
#' output$hist <- renderPlot( |
|
80 |
#' hist(data[["new_iris"]]()$Sepal.Length) |
|
81 |
#' ) |
|
82 |
#' }, |
|
83 |
#' ui = function(id, ...) { |
|
84 |
#' ns <- NS(id) |
|
85 |
#' plotOutput(ns("hist")) |
|
86 |
#' }, |
|
87 |
#' datanames = "new_iris" |
|
88 |
#' ) |
|
89 |
#' ), |
|
90 |
#' title = "App title", |
|
91 |
#' filter = teal_slices( |
|
92 |
#' teal_slice(dataname = "new_iris", varname = "Species"), |
|
93 |
#' teal_slice(dataname = "new_iris", varname = "Sepal.Length"), |
|
94 |
#' teal_slice(dataname = "new_mtcars", varname = "cyl"), |
|
95 |
#' exclude_varnames = list(new_iris = c("Sepal.Width", "Petal.Width")), |
|
96 |
#' mapping = list( |
|
97 |
#' `example teal module` = "new_iris Species", |
|
98 |
#' `Iris Sepal.Length histogram` = "new_iris Species", |
|
99 |
#' global_filters = "new_mtcars cyl" |
|
100 |
#' ) |
|
101 |
#' ), |
|
102 |
#' header = tags$h1("Sample App"), |
|
103 |
#' footer = tags$p("Copyright 2017 - 2023") |
|
104 |
#' ) |
|
105 |
#' if (interactive()) { |
|
106 |
#' shinyApp(app$ui, app$server) |
|
107 |
#' } |
|
108 |
#' |
|
109 |
init <- function(data, |
|
110 |
modules, |
|
111 |
title = NULL, |
|
112 |
filter = teal_slices(), |
|
113 |
header = tags$p(), |
|
114 |
footer = tags$p(), |
|
115 |
id = character(0)) { |
|
116 | 38x |
logger::log_trace("init initializing teal app with: data ({ class(data)[1] }).") |
117 | 38x |
data <- teal.data::to_relational_data(data = data) |
118 | ||
119 | 33x |
checkmate::assert_class(data, "TealData") |
120 | 33x |
checkmate::assert_multi_class(modules, c("teal_module", "list", "teal_modules")) |
121 | 33x |
checkmate::assert_string(title, null.ok = TRUE) |
122 | 33x |
checkmate::assert( |
123 | 33x |
checkmate::check_class(filter, "teal_slices"), |
124 | 33x |
checkmate::check_list(filter, names = "named") |
125 |
) |
|
126 | 32x |
checkmate::assert_multi_class(header, c("shiny.tag", "character")) |
127 | 32x |
checkmate::assert_multi_class(footer, c("shiny.tag", "character")) |
128 | 32x |
checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
129 | ||
130 | 32x |
teal.logger::log_system_info() |
131 | ||
132 | 32x |
if (inherits(modules, "teal_module")) { |
133 | 1x |
modules <- list(modules) |
134 |
} |
|
135 | 32x |
if (inherits(modules, "list")) { |
136 | 2x |
modules <- do.call(teal::modules, modules) |
137 |
} |
|
138 | ||
139 |
# resolve modules datanames |
|
140 | 32x |
datanames <- teal.data::get_dataname(data) |
141 | 32x |
join_keys <- data$get_join_keys() |
142 | 32x |
resolve_modules_datanames <- function(modules) { |
143 | 240x |
if (inherits(modules, "teal_modules")) { |
144 | 90x |
modules$children <- sapply(modules$children, resolve_modules_datanames, simplify = FALSE) |
145 | 90x |
modules |
146 |
} else { |
|
147 | 150x |
modules$datanames <- if (identical(modules$datanames, "all")) { |
148 | 5x |
datanames |
149 | 150x |
} else if (is.character(modules$datanames)) { |
150 | 145x |
datanames_adjusted <- intersect(modules$datanames, datanames) |
151 | 145x |
include_parent_datanames(dataname = datanames_adjusted, join_keys = join_keys) |
152 |
} |
|
153 | 150x |
modules |
154 |
} |
|
155 |
} |
|
156 | 32x |
modules <- resolve_modules_datanames(modules = modules) |
157 | ||
158 | 32x |
if (!inherits(filter, "teal_slices")) { |
159 | 1x |
checkmate::assert_subset(names(filter), choices = datanames) |
160 |
# list_to_teal_slices is lifted from teal.slice package, see zzz.R |
|
161 |
# This is a temporary measure and will be removed two release cycles from now (now meaning 0.13.0). |
|
162 | 1x |
filter <- list_to_teal_slices(filter) |
163 |
} |
|
164 |
# convert teal.slice::teal_slices to teal::teal_slices |
|
165 | 32x |
filter <- as.teal_slices(as.list(filter)) |
166 | ||
167 |
# check teal_slices |
|
168 | 32x |
for (i in seq_along(filter)) { |
169 | 2x |
dataname_i <- shiny::isolate(filter[[i]]$dataname) |
170 | 2x |
if (!dataname_i %in% datanames) { |
171 | ! |
stop( |
172 | ! |
sprintf( |
173 | ! |
"filter[[%s]] has a different dataname than available in a 'data':\n %s not in %s", |
174 | ! |
i, |
175 | ! |
dataname_i, |
176 | ! |
toString(datanames) |
177 |
) |
|
178 |
) |
|
179 |
} |
|
180 |
} |
|
181 | ||
182 | 32x |
if (isTRUE(attr(filter, "module_specific"))) { |
183 | ! |
module_names <- unlist(c(module_labels(modules), "global_filters")) |
184 | ! |
failed_mod_names <- setdiff(names(attr(filter, "mapping")), module_names) |
185 | ! |
if (length(failed_mod_names)) { |
186 | ! |
stop( |
187 | ! |
sprintf( |
188 | ! |
"Some module names in the mapping arguments don't match module labels.\n %s not in %s", |
189 | ! |
toString(failed_mod_names), |
190 | ! |
toString(unique(module_names)) |
191 |
) |
|
192 |
) |
|
193 |
} |
|
194 | ||
195 | ! |
if (anyDuplicated(module_names)) { |
196 |
# In teal we are able to set nested modules with duplicated label. |
|
197 |
# Because mapping argument bases on the relationship between module-label and filter-id, |
|
198 |
# it is possible that module-label in mapping might refer to multiple teal_module (identified by the same label) |
|
199 | ! |
stop( |
200 | ! |
sprintf( |
201 | ! |
"Module labels should be unique when teal_slices(mapping = TRUE). Duplicated labels:\n%s ", |
202 | ! |
toString(module_names[duplicated(module_names)]) |
203 |
) |
|
204 |
) |
|
205 |
} |
|
206 |
} |
|
207 | ||
208 |
# Note regarding case `id = character(0)`: |
|
209 |
# rather than using `callModule` and creating a submodule of this module, we directly modify |
|
210 |
# the `ui` and `server` with `id = character(0)` and calling the server function directly |
|
211 |
# rather than through `callModule` |
|
212 | 32x |
res <- list( |
213 | 32x |
ui = ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer), |
214 | 32x |
server = function(input, output, session) { |
215 |
# copy object so that load won't be shared between the session |
|
216 | ! |
data <- data$copy(deep = TRUE) |
217 | ! |
filter <- deep_copy_filter(filter) |
218 | ! |
srv_teal_with_splash(id = id, data = data, modules = modules, filter = filter) |
219 |
} |
|
220 |
) |
|
221 | 32x |
logger::log_trace("init teal app has been initialized.") |
222 | 32x |
return(res) |
223 |
} |
1 |
#' Show R Code Modal |
|
2 |
#' |
|
3 |
#' @export |
|
4 |
#' @description `r lifecycle::badge("stable")` |
|
5 |
#' Use the [shiny::showModal()] function to show the R code inside. |
|
6 |
#' |
|
7 |
#' @param title (`character(1)`)\cr |
|
8 |
#' Title of the modal, displayed in the first comment of the R-code. |
|
9 |
#' @param rcode (`character`)\cr |
|
10 |
#' vector with R code to show inside the modal. |
|
11 |
#' @param session (`ShinySession` optional)\cr |
|
12 |
#' `shiny` Session object, if missing then [shiny::getDefaultReactiveDomain()] is used. |
|
13 |
#' |
|
14 |
#' @references [shiny::showModal()] |
|
15 |
show_rcode_modal <- function(title = NULL, rcode, session = getDefaultReactiveDomain()) { |
|
16 | ! |
rcode <- paste(rcode, collapse = "\n") |
17 | ||
18 | ! |
ns <- session$ns |
19 | ! |
showModal(modalDialog( |
20 | ! |
tagList( |
21 | ! |
tags$div( |
22 | ! |
actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))), |
23 | ! |
modalButton("Dismiss"), |
24 | ! |
style = "mb-4" |
25 |
), |
|
26 | ! |
tags$div(tags$pre(id = ns("r_code"), rcode)), |
27 |
), |
|
28 | ! |
title = title, |
29 | ! |
footer = tagList( |
30 | ! |
actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))), |
31 | ! |
modalButton("Dismiss") |
32 |
), |
|
33 | ! |
size = "l", |
34 | ! |
easyClose = TRUE |
35 |
)) |
|
36 | ||
37 | ! |
return(NULL) |
38 |
} |
1 |
#' Create a `tdata` Object |
|
2 |
#' |
|
3 |
#' Create a new object called `tdata` which contains `data`, a `reactive` list of data.frames |
|
4 |
#' (or `MultiAssayExperiment`), with attributes: |
|
5 |
#' \itemize{ |
|
6 |
#' \item{`code` (`reactive`) containing code used to generate the data} |
|
7 |
#' \item{join_keys (`JoinKeys`) containing the relationships between the data} |
|
8 |
#' \item{metadata (`named list`) containing any metadata associated with the data frames} |
|
9 |
#' } |
|
10 |
#' @name tdata |
|
11 |
#' @param data A `named list` of `data.frames` (or `MultiAssayExperiment`) |
|
12 |
#' which optionally can be `reactive`. |
|
13 |
#' Inside this object all of these items will be made `reactive`. |
|
14 |
#' @param code A `character` (or `reactive` which evaluates to a `character`) containing |
|
15 |
#' the code used to generate the data. This should be `reactive` if the code is changing |
|
16 |
#' during a reactive context (e.g. if filtering changes the code). Inside this |
|
17 |
#' object `code` will be made reactive |
|
18 |
#' @param join_keys A `teal.data::JoinKeys` object containing relationships between the |
|
19 |
#' datasets. |
|
20 |
#' @param metadata A `named list` each element contains a list of metadata about the named data.frame |
|
21 |
#' Each element of these list should be atomic and length one. |
|
22 |
#' @return A `tdata` object |
|
23 |
#' @examples |
|
24 |
#' |
|
25 |
#' data <- new_tdata( |
|
26 |
#' data = list(iris = iris, mtcars = reactive(mtcars), dd = data.frame(x = 1:10)), |
|
27 |
#' code = "iris <- iris |
|
28 |
#' mtcars <- mtcars |
|
29 |
#' dd <- data.frame(x = 1:10)", |
|
30 |
#' metadata = list(dd = list(author = "NEST"), iris = list(version = 1)) |
|
31 |
#' ) |
|
32 |
#' |
|
33 |
#' # Extract a data.frame |
|
34 |
#' isolate(data[["iris"]]()) |
|
35 |
#' |
|
36 |
#' # Get code |
|
37 |
#' isolate(get_code(data)) |
|
38 |
#' |
|
39 |
#' # Get metadata |
|
40 |
#' get_metadata(data, "iris") |
|
41 |
#' |
|
42 |
#' @export |
|
43 |
new_tdata <- function(data, code = "", join_keys = NULL, metadata = NULL) { |
|
44 | 40x |
checkmate::assert_list( |
45 | 40x |
data, |
46 | 40x |
any.missing = FALSE, names = "unique", |
47 | 40x |
types = c("data.frame", "reactive", "MultiAssayExperiment") |
48 |
) |
|
49 | 36x |
checkmate::assert_class(join_keys, "JoinKeys", null.ok = TRUE) |
50 | 35x |
checkmate::assert_multi_class(code, c("character", "reactive")) |
51 | ||
52 | 34x |
checkmate::assert_list(metadata, names = "unique", null.ok = TRUE) |
53 | 32x |
checkmate::assert_subset(names(metadata), names(data)) |
54 | 20x |
for (m in metadata) teal.data::validate_metadata(m) |
55 | ||
56 | 31x |
if (is.reactive(code)) { |
57 | 15x |
isolate(checkmate::assert_class(code(), "character", .var.name = "code")) |
58 |
} |
|
59 | ||
60 |
# create reactive data.frames |
|
61 | 30x |
for (x in names(data)) { |
62 | 49x |
if (!is.reactive(data[[x]])) { |
63 | 29x |
data[[x]] <- do.call(reactive, list(as.name(x)), envir = list2env(data[x])) |
64 |
} else { |
|
65 | 20x |
isolate( |
66 | 20x |
checkmate::assert_multi_class( |
67 | 20x |
data[[x]](), c("data.frame", "MultiAssayExperiment"), |
68 | 20x |
.var.name = "data" |
69 |
) |
|
70 |
) |
|
71 |
} |
|
72 |
} |
|
73 | ||
74 |
# set attributes |
|
75 | 29x |
attr(data, "code") <- if (is.reactive(code)) code else reactive(code) |
76 | 29x |
attr(data, "join_keys") <- join_keys |
77 | 29x |
attr(data, "metadata") <- metadata |
78 | ||
79 |
# set class |
|
80 | 29x |
class(data) <- c("tdata", class(data)) |
81 | 29x |
data |
82 |
} |
|
83 | ||
84 |
#' Function to convert a `tdata` object to an `environment` |
|
85 |
#' Any `reactives` inside `tdata` are first evaluated |
|
86 |
#' @param data a `tdata` object |
|
87 |
#' @return an `environment` |
|
88 |
#' @examples |
|
89 |
#' |
|
90 |
#' data <- new_tdata( |
|
91 |
#' data = list(iris = iris, mtcars = reactive(mtcars)), |
|
92 |
#' code = "iris <- iris |
|
93 |
#' mtcars = mtcars" |
|
94 |
#' ) |
|
95 |
#' |
|
96 |
#' my_env <- isolate(tdata2env(data)) |
|
97 |
#' |
|
98 |
#' @export |
|
99 |
tdata2env <- function(data) { # nolint |
|
100 | 2x |
checkmate::assert_class(data, "tdata") |
101 | 1x |
list2env(lapply(data, function(x) if (is.reactive(x)) x() else x)) |
102 |
} |
|
103 | ||
104 |
#' @rdname tdata |
|
105 |
#' @param x a `tdata` object |
|
106 |
#' @param ... additional arguments for the generic |
|
107 |
#' @export |
|
108 |
get_code.tdata <- function(x, ...) { # nolint |
|
109 |
# note teal.data which teal depends on defines the get_code method |
|
110 | 6x |
attr(x, "code")() |
111 |
} |
|
112 | ||
113 | ||
114 |
#' Wrapper for `get_code.tdata` |
|
115 |
#' This wrapper is to be used by downstream packages to extract the code of a `tdata` object |
|
116 |
#' |
|
117 |
#' @param data (`tdata`) object |
|
118 |
#' |
|
119 |
#' @return (`character`) code used in the `tdata` object. |
|
120 |
#' @export |
|
121 |
get_code_tdata <- function(data) { |
|
122 | 4x |
checkmate::assert_class(data, "tdata") |
123 | 2x |
get_code(data) |
124 |
} |
|
125 | ||
126 | ||
127 |
#' Function to get join keys from a `tdata` object |
|
128 |
#' @param data `tdata` - object to extract the join keys |
|
129 |
#' @return Either `JoinKeys` object or `NULL` if no join keys |
|
130 |
#' @export |
|
131 |
get_join_keys <- function(data) { |
|
132 | 3x |
UseMethod("get_join_keys", data) |
133 |
} |
|
134 | ||
135 | ||
136 |
#' @rdname get_join_keys |
|
137 |
#' @export |
|
138 |
get_join_keys.tdata <- function(data) { |
|
139 | 3x |
attr(data, "join_keys") |
140 |
} |
|
141 | ||
142 | ||
143 |
#' @rdname get_join_keys |
|
144 |
#' @export |
|
145 |
get_join_keys.default <- function(data) { |
|
146 | ! |
stop("get_join_keys function not implemented for this object") |
147 |
} |
|
148 | ||
149 |
#' Function to get metadata from a `tdata` object |
|
150 |
#' @param data `tdata` - object to extract the data from |
|
151 |
#' @param dataname `character(1)` the dataset name whose metadata is requested |
|
152 |
#' @return Either list of metadata or NULL if no metadata |
|
153 |
#' @export |
|
154 |
get_metadata <- function(data, dataname) { |
|
155 | 6x |
checkmate::assert_string(dataname) |
156 | 6x |
UseMethod("get_metadata", data) |
157 |
} |
|
158 | ||
159 |
#' @rdname get_metadata |
|
160 |
#' @export |
|
161 |
get_metadata.tdata <- function(data, dataname) { |
|
162 | 6x |
metadata <- attr(data, "metadata") |
163 | 6x |
if (is.null(metadata)) { |
164 | 1x |
return(NULL) |
165 |
} |
|
166 | 5x |
metadata[[dataname]] |
167 |
} |
|
168 | ||
169 |
#' @rdname get_metadata |
|
170 |
#' @export |
|
171 |
get_metadata.default <- function(data, dataname) { |
|
172 | ! |
stop("get_metadata function not implemented for this object") |
173 |
} |
1 |
#' Create a UI of nested tabs of `teal_modules` |
|
2 |
#' |
|
3 |
#' @section `ui_nested_tabs`: |
|
4 |
#' Each `teal_modules` is translated to a `tabsetPanel` and each |
|
5 |
#' of its children is another tab-module called recursively. The UI of a |
|
6 |
#' `teal_module` is obtained by calling the `ui` function on it. |
|
7 |
#' |
|
8 |
#' The `datasets` argument is required to resolve the teal arguments in an |
|
9 |
#' isolated context (with respect to reactivity) |
|
10 |
#' |
|
11 |
#' @section `srv_nested_tabs`: |
|
12 |
#' This module calls recursively all elements of the `modules` returns one which |
|
13 |
#' is currently active. |
|
14 |
#' - `teal_module` returns self as a active module. |
|
15 |
#' - `teal_modules` also returns module active within self which is determined by the `input$active_tab`. |
|
16 |
#' |
|
17 |
#' @name module_nested_tabs |
|
18 |
#' |
|
19 |
#' @inheritParams module_tabs_with_filters |
|
20 |
#' |
|
21 |
#' @param depth (`integer(1)`)\cr |
|
22 |
#' number which helps to determine depth of the modules nesting. |
|
23 |
#' @param is_module_specific (`logical(1)`)\cr |
|
24 |
#' flag determining if the filter panel is global or module-specific. |
|
25 |
#' When set to `TRUE`, a filter panel is called inside of each module tab. |
|
26 |
#' @return depending on class of `modules`, `ui_nested_tabs` returns: |
|
27 |
#' - `teal_module`: instantiated UI of the module |
|
28 |
#' - `teal_modules`: `tabsetPanel` with each tab corresponding to recursively |
|
29 |
#' calling this function on it.\cr |
|
30 |
#' `srv_nested_tabs` returns a reactive which returns the active module that corresponds to the selected tab. |
|
31 |
#' |
|
32 |
#' @examples |
|
33 |
#' mods <- teal:::example_modules() |
|
34 |
#' datasets <- teal:::example_datasets() |
|
35 |
#' app <- shinyApp( |
|
36 |
#' ui = function() { |
|
37 |
#' tagList( |
|
38 |
#' teal:::include_teal_css_js(), |
|
39 |
#' textOutput("info"), |
|
40 |
#' fluidPage( # needed for nice tabs |
|
41 |
#' teal:::ui_nested_tabs("dummy", modules = mods, datasets = datasets) |
|
42 |
#' ) |
|
43 |
#' ) |
|
44 |
#' }, |
|
45 |
#' server = function(input, output, session) { |
|
46 |
#' active_module <- teal:::srv_nested_tabs( |
|
47 |
#' "dummy", |
|
48 |
#' datasets = datasets, |
|
49 |
#' modules = mods |
|
50 |
#' ) |
|
51 |
#' output$info <- renderText({ |
|
52 |
#' paste0("The currently active tab name is ", active_module()$label) |
|
53 |
#' }) |
|
54 |
#' } |
|
55 |
#' ) |
|
56 |
#' if (interactive()) { |
|
57 |
#' runApp(app) |
|
58 |
#' } |
|
59 |
#' @keywords internal |
|
60 |
NULL |
|
61 | ||
62 |
#' @rdname module_nested_tabs |
|
63 |
ui_nested_tabs <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
|
64 | 2x |
checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
65 | 2x |
checkmate::assert_count(depth) |
66 | 2x |
UseMethod("ui_nested_tabs", modules) |
67 |
} |
|
68 | ||
69 |
#' @rdname module_nested_tabs |
|
70 |
#' @export |
|
71 |
ui_nested_tabs.default <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
|
72 | ! |
stop("Modules class not supported: ", paste(class(modules), collapse = " ")) |
73 |
} |
|
74 | ||
75 |
#' @rdname module_nested_tabs |
|
76 |
#' @export |
|
77 |
ui_nested_tabs.teal_modules <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
|
78 | 1x |
checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
79 | 1x |
ns <- NS(id) |
80 | 1x |
do.call( |
81 | 1x |
tabsetPanel, |
82 | 1x |
c( |
83 |
# by giving an id, we can reactively respond to tab changes |
|
84 | 1x |
list( |
85 | 1x |
id = ns("active_tab"), |
86 | 1x |
type = if (modules$label == "root") "pills" else "tabs" |
87 |
), |
|
88 | 1x |
lapply( |
89 | 1x |
names(modules$children), |
90 | 1x |
function(module_id) { |
91 | 1x |
module_label <- modules$children[[module_id]]$label |
92 | 1x |
tabPanel( |
93 | 1x |
title = module_label, |
94 | 1x |
value = module_id, # when clicked this tab value changes input$<tabset panel id> |
95 | 1x |
ui_nested_tabs( |
96 | 1x |
id = ns(module_id), |
97 | 1x |
modules = modules$children[[module_id]], |
98 | 1x |
datasets = datasets[[module_label]], |
99 | 1x |
depth = depth + 1L, |
100 | 1x |
is_module_specific = is_module_specific |
101 |
) |
|
102 |
) |
|
103 |
} |
|
104 |
) |
|
105 |
) |
|
106 |
) |
|
107 |
} |
|
108 | ||
109 |
#' @rdname module_nested_tabs |
|
110 |
#' @export |
|
111 |
ui_nested_tabs.teal_module <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
|
112 | 1x |
checkmate::assert_class(datasets, class = "FilteredData") |
113 | 1x |
ns <- NS(id) |
114 | ||
115 | 1x |
args <- isolate(teal.transform::resolve_delayed(modules$ui_args, datasets)) |
116 | 1x |
args <- c(list(id = ns("module")), args) |
117 | ||
118 | 1x |
if (is_arg_used(modules$ui, "datasets")) { |
119 | ! |
args <- c(args, datasets = datasets) |
120 |
} |
|
121 | ||
122 | 1x |
if (is_arg_used(modules$ui, "data")) { |
123 | ! |
data <- .datasets_to_data(modules, datasets) |
124 | ! |
args <- c(args, data = list(data)) |
125 |
} |
|
126 | ||
127 | 1x |
teal_ui <- tags$div( |
128 | 1x |
id = id, |
129 | 1x |
class = "teal_module", |
130 | 1x |
uiOutput(ns("data_reactive"), inline = TRUE), |
131 | 1x |
tagList( |
132 | 1x |
if (depth >= 2L) div(style = "mt-6"), |
133 | 1x |
do.call(modules$ui, args) |
134 |
) |
|
135 |
) |
|
136 | ||
137 | 1x |
if (!is.null(modules$datanames) && is_module_specific) { |
138 | ! |
fluidRow( |
139 | ! |
column(width = 9, teal_ui, class = "teal_primary_col"), |
140 | ! |
column( |
141 | ! |
width = 3, |
142 | ! |
datasets$ui_filter_panel(ns("module_filter_panel")), |
143 | ! |
class = "teal_secondary_col" |
144 |
) |
|
145 |
) |
|
146 |
} else { |
|
147 | 1x |
teal_ui |
148 |
} |
|
149 |
} |
|
150 | ||
151 |
#' @rdname module_nested_tabs |
|
152 |
srv_nested_tabs <- function(id, datasets, modules, is_module_specific = FALSE, |
|
153 |
reporter = teal.reporter::Reporter$new()) { |
|
154 | 54x |
checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
155 | 54x |
checkmate::assert_class(reporter, "Reporter") |
156 | 53x |
UseMethod("srv_nested_tabs", modules) |
157 |
} |
|
158 | ||
159 |
#' @rdname module_nested_tabs |
|
160 |
#' @export |
|
161 |
srv_nested_tabs.default <- function(id, datasets, modules, is_module_specific = FALSE, |
|
162 |
reporter = teal.reporter::Reporter$new()) { |
|
163 | ! |
stop("Modules class not supported: ", paste(class(modules), collapse = " ")) |
164 |
} |
|
165 | ||
166 |
#' @rdname module_nested_tabs |
|
167 |
#' @export |
|
168 |
srv_nested_tabs.teal_modules <- function(id, datasets, modules, is_module_specific = FALSE, |
|
169 |
reporter = teal.reporter::Reporter$new()) { |
|
170 | 24x |
checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
171 | ||
172 | 24x |
moduleServer(id = id, module = function(input, output, session) { |
173 | 24x |
logger::log_trace("srv_nested_tabs.teal_modules initializing the module { deparse1(modules$label) }.") |
174 | ||
175 | 24x |
labels <- vapply(modules$children, `[[`, character(1), "label") |
176 | 24x |
modules_reactive <- sapply( |
177 | 24x |
names(modules$children), |
178 | 24x |
function(module_id) { |
179 | 35x |
srv_nested_tabs( |
180 | 35x |
id = module_id, |
181 | 35x |
datasets = datasets[[labels[module_id]]], |
182 | 35x |
modules = modules$children[[module_id]], |
183 | 35x |
is_module_specific = is_module_specific, |
184 | 35x |
reporter = reporter |
185 |
) |
|
186 |
}, |
|
187 | 24x |
simplify = FALSE |
188 |
) |
|
189 | ||
190 |
# when not ready input$active_tab would return NULL - this would fail next reactive |
|
191 | 24x |
input_validated <- eventReactive(input$active_tab, input$active_tab, ignoreNULL = TRUE) |
192 | 24x |
get_active_module <- reactive({ |
193 | 13x |
if (length(modules$children) == 1L) { |
194 |
# single tab is active by default |
|
195 | 2x |
modules_reactive[[1]]() |
196 |
} else { |
|
197 |
# switch to active tab |
|
198 | 11x |
modules_reactive[[input_validated()]]() |
199 |
} |
|
200 |
}) |
|
201 | ||
202 | 24x |
get_active_module |
203 |
}) |
|
204 |
} |
|
205 | ||
206 |
#' @rdname module_nested_tabs |
|
207 |
#' @export |
|
208 |
srv_nested_tabs.teal_module <- function(id, datasets, modules, is_module_specific = TRUE, |
|
209 |
reporter = teal.reporter::Reporter$new()) { |
|
210 | 29x |
checkmate::assert_class(datasets, "FilteredData") |
211 | 29x |
logger::log_trace("srv_nested_tabs.teal_module initializing the module: { deparse1(modules$label) }.") |
212 | ||
213 | 29x |
moduleServer(id = id, module = function(input, output, session) { |
214 | 29x |
modules$server_args <- teal.transform::resolve_delayed(modules$server_args, datasets) |
215 | 29x |
if (!is.null(modules$datanames) && is_module_specific) { |
216 | ! |
datasets$srv_filter_panel("module_filter_panel", active_datanames = reactive(modules$datanames)) |
217 |
} |
|
218 | ||
219 |
# Create two triggers to limit reactivity between filter-panel and modules. |
|
220 |
# We want to recalculate only visible modules |
|
221 |
# - trigger the data when the tab is selected |
|
222 |
# - trigger module to be called when the tab is selected for the first time |
|
223 | 29x |
trigger_data <- reactiveVal(1L) |
224 | 29x |
trigger_module <- reactiveVal(NULL) |
225 | 29x |
output$data_reactive <- renderUI({ |
226 | 18x |
lapply(datasets$datanames(), function(x) { |
227 | 22x |
datasets$get_data(x, filtered = TRUE) |
228 |
}) |
|
229 | 18x |
isolate(trigger_data(trigger_data() + 1)) |
230 | 18x |
isolate(trigger_module(TRUE)) |
231 | ||
232 | 18x |
NULL |
233 |
}) |
|
234 | ||
235 |
# collect arguments to run teal_module |
|
236 | 29x |
args <- c(list(id = "module"), modules$server_args) |
237 | 29x |
if (is_arg_used(modules$server, "reporter")) { |
238 | ! |
args <- c(args, list(reporter = reporter)) |
239 |
} |
|
240 | ||
241 | 29x |
if (is_arg_used(modules$server, "datasets")) { |
242 | 2x |
args <- c(args, datasets = datasets) |
243 |
} |
|
244 | ||
245 | 29x |
if (is_arg_used(modules$server, "data")) { |
246 | 8x |
data <- .datasets_to_data(modules, datasets, trigger_data) |
247 | 8x |
args <- c(args, data = list(data)) |
248 |
} |
|
249 | ||
250 | 29x |
if (is_arg_used(modules$server, "filter_panel_api")) { |
251 | 2x |
filter_panel_api <- teal.slice::FilterPanelAPI$new(datasets) |
252 | 2x |
args <- c(args, filter_panel_api = filter_panel_api) |
253 |
} |
|
254 | ||
255 | 29x |
if (is_arg_used(modules$server, "datasets") && is_arg_used(modules$server, "data")) { |
256 | 1x |
warning( |
257 | 1x |
"Module '", modules$label, "' has `data` and `datasets` arguments in the formals.", |
258 | 1x |
"\nIt's recommended to use `data` to work with filtered objects." |
259 |
) |
|
260 |
} |
|
261 | ||
262 |
# observe the trigger_module above to induce the module once the renderUI is triggered |
|
263 | 29x |
observeEvent( |
264 | 29x |
ignoreNULL = TRUE, |
265 | 29x |
once = TRUE, |
266 | 29x |
eventExpr = trigger_module(), |
267 | 29x |
handlerExpr = { |
268 | 18x |
module_output <- if (is_arg_used(modules$server, "id")) { |
269 | 18x |
do.call(modules$server, args) |
270 |
} else { |
|
271 | ! |
do.call(callModule, c(args, list(module = modules$server))) |
272 |
} |
|
273 |
} |
|
274 |
) |
|
275 | ||
276 | 29x |
reactive(modules) |
277 |
}) |
|
278 |
} |
|
279 | ||
280 |
#' Convert `FilteredData` to reactive list of datasets of the `tdata` type. |
|
281 |
#' |
|
282 |
#' Converts `FilteredData` object to `tdata` object containing datasets needed for a specific module. |
|
283 |
#' Please note that if module needs dataset which has a parent, then parent will be also returned. |
|
284 |
#' A hash per `dataset` is calculated internally and returned in the code. |
|
285 |
#' |
|
286 |
#' @param module (`teal_module`) module where needed filters are taken from |
|
287 |
#' @param datasets (`FilteredData`) object where needed data are taken from |
|
288 |
#' @param trigger_data (`reactiveVal`) to trigger getting the filtered data |
|
289 |
#' @return list of reactive datasets with following attributes: |
|
290 |
#' - `code` (`character`) containing datasets reproducible code. |
|
291 |
#' - `join_keys` (`JoinKeys`) containing relationships between datasets. |
|
292 |
#' - `metadata` (`list`) containing metadata of datasets. |
|
293 |
#' |
|
294 |
#' @keywords internal |
|
295 |
.datasets_to_data <- function(module, datasets, trigger_data = reactiveVal(1L)) { |
|
296 | 13x |
checkmate::assert_class(module, "teal_module") |
297 | 13x |
checkmate::assert_class(datasets, "FilteredData") |
298 | 13x |
checkmate::assert_class(trigger_data, "reactiveVal") |
299 | ||
300 | 12x |
datanames <- if (is.null(module$datanames)) datasets$datanames() else module$datanames |
301 | ||
302 |
# list of reactive filtered data |
|
303 | 12x |
data <- sapply( |
304 | 12x |
datanames, |
305 | 12x |
function(x) eventReactive(trigger_data(), datasets$get_data(x, filtered = TRUE)), |
306 | 12x |
simplify = FALSE |
307 |
) |
|
308 | ||
309 | 12x |
hashes <- calculate_hashes(datanames, datasets) |
310 | 12x |
metadata <- lapply(datanames, datasets$get_metadata) |
311 | 12x |
names(metadata) <- datanames |
312 | ||
313 | 12x |
new_tdata( |
314 | 12x |
data, |
315 | 12x |
eventReactive( |
316 | 12x |
trigger_data(), |
317 | 12x |
c( |
318 | 12x |
get_rcode_str_install(), |
319 | 12x |
get_rcode_libraries(), |
320 | 12x |
get_datasets_code(datanames, datasets, hashes), |
321 | 12x |
teal.slice::get_filter_expr(datasets, datanames) |
322 |
) |
|
323 |
), |
|
324 | 12x |
datasets$get_join_keys(), |
325 | 12x |
metadata |
326 |
) |
|
327 |
} |
|
328 | ||
329 |
#' Get the hash of a dataset |
|
330 |
#' |
|
331 |
#' @param datanames (`character`) names of datasets |
|
332 |
#' @param datasets (`FilteredData`) object holding the data |
|
333 |
#' |
|
334 |
#' @return A list of hashes per dataset |
|
335 |
#' @keywords internal |
|
336 |
#' |
|
337 |
calculate_hashes <- function(datanames, datasets) { |
|
338 | 16x |
sapply(datanames, function(x) rlang::hash(datasets$get_data(x, filtered = FALSE)), simplify = FALSE) |
339 |
} |
1 |
#' @title `TealReportCard` |
|
2 |
#' @description `r lifecycle::badge("experimental")` |
|
3 |
#' A child of [`ReportCard`] that is used for teal specific applications. |
|
4 |
#' In addition to the parent methods, it supports rendering teal specific elements such as |
|
5 |
#' the source code, the encodings panel content and the filter panel content as part of the |
|
6 |
#' meta data. |
|
7 |
#' @export |
|
8 |
#' |
|
9 |
TealReportCard <- R6::R6Class( # nolint: object_name_linter. |
|
10 |
classname = "TealReportCard", |
|
11 |
inherit = teal.reporter::ReportCard, |
|
12 |
public = list( |
|
13 |
#' @description Appends the source code to the `content` meta data of this `TealReportCard`. |
|
14 |
#' |
|
15 |
#' @param src (`character(1)`) code as text. |
|
16 |
#' @param ... any `rmarkdown` R chunk parameter and its value. |
|
17 |
#' But `eval` parameter is always set to `FALSE`. |
|
18 |
#' @return invisibly self |
|
19 |
#' @examples |
|
20 |
#' card <- TealReportCard$new()$append_src( |
|
21 |
#' "plot(iris)" |
|
22 |
#' ) |
|
23 |
#' card$get_content()[[1]]$get_content() |
|
24 |
append_src = function(src, ...) { |
|
25 | 4x |
checkmate::assert_character(src, min.len = 0, max.len = 1) |
26 | 4x |
params <- list(...) |
27 | 4x |
params$eval <- FALSE |
28 | 4x |
rblock <- RcodeBlock$new(src) |
29 | 4x |
rblock$set_params(params) |
30 | 4x |
self$append_content(rblock) |
31 | 4x |
self$append_metadata("SRC", src) |
32 | 4x |
invisible(self) |
33 |
}, |
|
34 |
#' @description Appends the filter state list to the `content` and `metadata` of this `TealReportCard`. |
|
35 |
#' If the filter state list has an attribute named `formatted`, it appends it to the card otherwise it uses |
|
36 |
#' the default `yaml::as.yaml` to format the list. |
|
37 |
#' If the filter state list is empty, nothing is appended to the `content`. |
|
38 |
#' |
|
39 |
#' @param fs (`teal_slices`) object returned from [teal_slices()] function. |
|
40 |
#' @return invisibly self |
|
41 |
append_fs = function(fs) { |
|
42 | 4x |
checkmate::assert_class(fs, "teal_slices") |
43 | 3x |
self$append_text("Filter State", "header3") |
44 | 3x |
self$append_content(TealSlicesBlock$new(fs)) |
45 | 3x |
invisible(self) |
46 |
}, |
|
47 |
#' @description Appends the encodings list to the `content` and `metadata` of this `TealReportCard`. |
|
48 |
#' |
|
49 |
#' @param encodings (`list`) list of encodings selections of the teal app. |
|
50 |
#' @return invisibly self |
|
51 |
#' @examples |
|
52 |
#' card <- TealReportCard$new()$append_encodings(list(variable1 = "X")) |
|
53 |
#' card$get_content()[[1]]$get_content() |
|
54 |
#' |
|
55 |
append_encodings = function(encodings) { |
|
56 | 4x |
checkmate::assert_list(encodings) |
57 | 4x |
self$append_text("Selected Options", "header3") |
58 | 4x |
if (requireNamespace("yaml", quietly = TRUE)) { |
59 | 4x |
self$append_text(yaml::as.yaml(encodings, handlers = list( |
60 | 4x |
POSIXct = function(x) format(x, "%Y-%m-%d"), |
61 | 4x |
POSIXlt = function(x) format(x, "%Y-%m-%d"), |
62 | 4x |
Date = function(x) format(x, "%Y-%m-%d") |
63 | 4x |
)), "verbatim") |
64 |
} else { |
|
65 | ! |
stop("yaml package is required to format the encodings list") |
66 |
} |
|
67 | 4x |
self$append_metadata("Encodings", encodings) |
68 | 4x |
invisible(self) |
69 |
} |
|
70 |
), |
|
71 |
private = list() |
|
72 |
) |
|
73 | ||
74 |
#' @title `RcodeBlock` |
|
75 |
#' @keywords internal |
|
76 |
TealSlicesBlock <- R6::R6Class( # nolint: object_name_linter. |
|
77 |
classname = "TealSlicesBlock", |
|
78 |
inherit = teal.reporter:::TextBlock, |
|
79 |
public = list( |
|
80 |
#' @description Returns a `TealSlicesBlock` object. |
|
81 |
#' |
|
82 |
#' @details Returns a `TealSlicesBlock` object with no content and no parameters. |
|
83 |
#' |
|
84 |
#' @param content (`teal_slices`) object returned from [teal_slices()] function. |
|
85 |
#' @param style (`character(1)`) string specifying style to apply. |
|
86 |
#' |
|
87 |
#' @return `TealSlicesBlock` |
|
88 |
#' @examples |
|
89 |
#' block <- teal:::TealSlicesBlock$new() |
|
90 |
#' |
|
91 |
initialize = function(content = teal_slices(), style = "verbatim") { |
|
92 | 9x |
self$set_content(content) |
93 | 8x |
self$set_style(style) |
94 | 8x |
invisible(self) |
95 |
}, |
|
96 | ||
97 |
#' @description Sets content of this `TealSlicesBlock`. |
|
98 |
#' Sets content as `YAML` text which represents a list generated from `teal_slices`. |
|
99 |
#' The list displays limited number of fields from `teal_slice` objects, but this list is |
|
100 |
#' sufficient to conclude which filters were applied. |
|
101 |
#' When `selected` field in `teal_slice` object is a range, then it is displayed as a "min" |
|
102 |
#' |
|
103 |
#' |
|
104 |
#' @param content (`teal_slices`) object returned from [teal_slices()] function. |
|
105 |
#' @return invisibly self |
|
106 |
set_content = function(content) { |
|
107 | 10x |
checkmate::assert_class(content, "teal_slices") |
108 | 9x |
if (length(content) != 0) { |
109 | 7x |
states_list <- lapply(content, function(x) { |
110 | 7x |
x_list <- shiny::isolate(as.list(x)) |
111 | 7x |
if ( |
112 | 7x |
inherits(x_list$choices, c("integer", "numeric", "Date", "POSIXct", "POSIXlt")) && |
113 | 7x |
length(x_list$choices) == 2 && |
114 | 7x |
length(x_list$selected) == 2 |
115 |
) { |
|
116 | ! |
x_list$range <- paste(x_list$selected, collapse = " - ") |
117 | ! |
x_list["selected"] <- NULL |
118 |
} |
|
119 | 7x |
if (!is.null(x_list$arg)) { |
120 | ! |
x_list$arg <- if (x_list$arg == "subset") "Genes" else "Samples" |
121 |
} |
|
122 | ||
123 | 7x |
x_list <- x_list[ |
124 | 7x |
c("dataname", "varname", "experiment", "arg", "expr", "selected", "range", "keep_na", "keep_inf") |
125 |
] |
|
126 | 7x |
names(x_list) <- c( |
127 | 7x |
"Dataset name", "Variable name", "Experiment", "Filtering by", "Applied expression", |
128 | 7x |
"Selected Values", "Selected range", "Include NA values", "Include Inf values" |
129 |
) |
|
130 | ||
131 | 7x |
Filter(Negate(is.null), x_list) |
132 |
}) |
|
133 | ||
134 | 7x |
if (requireNamespace("yaml", quietly = TRUE)) { |
135 | 7x |
super$set_content(yaml::as.yaml(states_list)) |
136 |
} else { |
|
137 | ! |
stop("yaml package is required to format the filter state list") |
138 |
} |
|
139 |
} |
|
140 | 9x |
private$teal_slices <- content |
141 | 9x |
invisible(self) |
142 |
}, |
|
143 |
#' @description Create the `RcodeBlock` from a list. |
|
144 |
#' @param x `named list` with two fields `c("text", "params")`. |
|
145 |
#' Use the `get_available_params` method to get all possible parameters. |
|
146 |
#' @return invisibly self |
|
147 |
from_list = function(x) { |
|
148 | 1x |
checkmate::assert_list(x) |
149 | 1x |
checkmate::assert_names(names(x), must.include = c("teal_slices")) |
150 | 1x |
self$set_content(x$teal_slices) |
151 | 1x |
invisible(self) |
152 |
}, |
|
153 |
#' @description Convert the `RcodeBlock` to a list. |
|
154 |
#' @return `named list` with a text and `params`. |
|
155 | ||
156 |
to_list = function() { |
|
157 | 2x |
list(teal_slices = private$teal_slices) |
158 |
} |
|
159 |
), |
|
160 |
private = list( |
|
161 |
style = "verbatim", |
|
162 |
teal_slices = NULL # teal_slices |
|
163 |
) |
|
164 |
) |
1 |
# This file contains Shiny modules useful for debugging and developing teal. |
|
2 |
# We do not export the functions in this file. They are for |
|
3 |
# developers only and can be accessed via `:::`. |
|
4 | ||
5 |
#' Dummy module to show the filter calls generated by the right encoding panel |
|
6 |
#' |
|
7 |
#' |
|
8 |
#' Please do not remove, this is useful for debugging teal without |
|
9 |
#' dependencies and simplifies `\link[devtools]{load_all}` which otherwise fails |
|
10 |
#' and avoids session restarts! |
|
11 |
#' |
|
12 |
#' @param label `character` label of module |
|
13 |
#' @keywords internal |
|
14 |
#' |
|
15 |
#' @examples |
|
16 |
#' app <- init( |
|
17 |
#' data = list(iris = iris, mtcars = mtcars), |
|
18 |
#' modules = teal:::filter_calls_module(), |
|
19 |
#' header = "Simple teal app" |
|
20 |
#' ) |
|
21 |
#' if (interactive()) { |
|
22 |
#' runApp(app) |
|
23 |
#' } |
|
24 |
filter_calls_module <- function(label = "Filter Calls Module") { # nolint |
|
25 | ! |
checkmate::assert_string(label) |
26 | ||
27 | ! |
module( |
28 | ! |
label = label, |
29 | ! |
server = function(input, output, session, data) { |
30 | ! |
checkmate::assert_class(data, "tdata") |
31 | ||
32 | ! |
output$filter_calls <- renderText({ |
33 | ! |
get_code_tdata(data) |
34 |
}) |
|
35 |
}, |
|
36 | ! |
ui = function(id, ...) { |
37 | ! |
ns <- NS(id) |
38 | ! |
div( |
39 | ! |
h2("The following filter calls are generated:"), |
40 | ! |
verbatimTextOutput(ns("filter_calls")) |
41 |
) |
|
42 |
}, |
|
43 | ! |
datanames = "all" |
44 |
) |
|
45 |
} |
1 |
#' Validate that dataset has a minimum number of observations |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("stable")` |
|
4 |
#' @param x a data.frame |
|
5 |
#' @param min_nrow minimum number of rows in \code{x} |
|
6 |
#' @param complete \code{logical} default \code{FALSE} when set to \code{TRUE} then complete cases are checked. |
|
7 |
#' @param allow_inf \code{logical} default \code{TRUE} when set to \code{FALSE} then error thrown if any values are |
|
8 |
#' infinite. |
|
9 |
#' @param msg (`character(1)`) additional message to display alongside the default message. |
|
10 |
#' |
|
11 |
#' @details This function is a wrapper for `shiny::validate`. |
|
12 |
#' |
|
13 |
#' @export |
|
14 |
#' |
|
15 |
#' @examples |
|
16 |
#' library(teal) |
|
17 |
#' ui <- fluidPage( |
|
18 |
#' sliderInput("len", "Max Length of Sepal", |
|
19 |
#' min = 4.3, max = 7.9, value = 5 |
|
20 |
#' ), |
|
21 |
#' plotOutput("plot") |
|
22 |
#' ) |
|
23 |
#' |
|
24 |
#' server <- function(input, output) { |
|
25 |
#' output$plot <- renderPlot({ |
|
26 |
#' df <- iris[iris$Sepal.Length <= input$len, ] |
|
27 |
#' validate_has_data( |
|
28 |
#' iris_f, |
|
29 |
#' min_nrow = 10, |
|
30 |
#' complete = FALSE, |
|
31 |
#' msg = "Please adjust Max Length of Sepal" |
|
32 |
#' ) |
|
33 |
#' |
|
34 |
#' hist(iris_f$Sepal.Length, breaks = 5) |
|
35 |
#' }) |
|
36 |
#' } |
|
37 |
#' if (interactive()) { |
|
38 |
#' shinyApp(ui, server) |
|
39 |
#' } |
|
40 |
#' |
|
41 |
validate_has_data <- function(x, |
|
42 |
min_nrow = NULL, |
|
43 |
complete = FALSE, |
|
44 |
allow_inf = TRUE, |
|
45 |
msg = NULL) { |
|
46 | 17x |
stopifnot( |
47 | 17x |
"Please provide a character vector in msg argument of validate_has_data." = is.character(msg) || is.null(msg) |
48 |
) |
|
49 | 15x |
validate(need(!is.null(x) && is.data.frame(x), "No data left.")) |
50 | 15x |
if (!is.null(min_nrow)) { |
51 | 15x |
if (complete) { |
52 | 5x |
complete_index <- stats::complete.cases(x) |
53 | 5x |
validate(need( |
54 | 5x |
sum(complete_index) > 0 && nrow(x[complete_index, , drop = FALSE]) >= min_nrow, |
55 | 5x |
paste(c(paste("Number of complete cases is less than:", min_nrow), msg), collapse = "\n") |
56 |
)) |
|
57 |
} else { |
|
58 | 10x |
validate(need( |
59 | 10x |
nrow(x) >= min_nrow, |
60 | 10x |
paste( |
61 | 10x |
c(paste("Minimum number of records not met: >=", min_nrow, "records required."), msg), |
62 | 10x |
collapse = "\n" |
63 |
) |
|
64 |
)) |
|
65 |
} |
|
66 | ||
67 | 10x |
if (!allow_inf) { |
68 | 6x |
validate(need( |
69 | 6x |
all(vapply(x, function(col) !is.numeric(col) || !any(is.infinite(col)), logical(1))), |
70 | 6x |
"Dataframe contains Inf values which is not allowed." |
71 |
)) |
|
72 |
} |
|
73 |
} |
|
74 |
} |
|
75 | ||
76 |
#' Validate that dataset has unique rows for key variables |
|
77 |
#' |
|
78 |
#' @description `r lifecycle::badge("stable")` |
|
79 |
#' @param x a data.frame |
|
80 |
#' @param key a vector of ID variables from \code{x} that identify unique records |
|
81 |
#' |
|
82 |
#' @details This function is a wrapper for `shiny::validate`. |
|
83 |
#' |
|
84 |
#' @export |
|
85 |
#' |
|
86 |
#' @examples |
|
87 |
#' iris$id <- rep(1:50, times = 3) |
|
88 |
#' ui <- fluidPage( |
|
89 |
#' selectInput( |
|
90 |
#' inputId = "species", |
|
91 |
#' label = "Select species", |
|
92 |
#' choices = c("setosa", "versicolor", "virginica"), |
|
93 |
#' selected = "setosa", |
|
94 |
#' multiple = TRUE |
|
95 |
#' ), |
|
96 |
#' plotOutput("plot") |
|
97 |
#' ) |
|
98 |
#' server <- function(input, output) { |
|
99 |
#' output$plot <- renderPlot({ |
|
100 |
#' iris_f <- iris[iris$Species %in% input$species, ] |
|
101 |
#' validate_one_row_per_id(iris_f, key = c("id")) |
|
102 |
#' |
|
103 |
#' hist(iris_f$Sepal.Length, breaks = 5) |
|
104 |
#' }) |
|
105 |
#' } |
|
106 |
#' if (interactive()) { |
|
107 |
#' shinyApp(ui, server) |
|
108 |
#' } |
|
109 |
#' |
|
110 |
validate_one_row_per_id <- function(x, key = c("USUBJID", "STUDYID")) { |
|
111 | ! |
validate(need(!any(duplicated(x[key])), paste("Found more than one row per id."))) |
112 |
} |
|
113 | ||
114 |
#' Validates that vector includes all expected values |
|
115 |
#' |
|
116 |
#' @description `r lifecycle::badge("stable")` |
|
117 |
#' @param x values to test. All must be in \code{choices} |
|
118 |
#' @param choices a vector to test for values of \code{x} |
|
119 |
#' @param msg warning message to display |
|
120 |
#' |
|
121 |
#' @details This function is a wrapper for `shiny::validate`. |
|
122 |
#' |
|
123 |
#' @export |
|
124 |
#' |
|
125 |
#' @examples |
|
126 |
#' ui <- fluidPage( |
|
127 |
#' selectInput( |
|
128 |
#' "species", |
|
129 |
#' "Select species", |
|
130 |
#' choices = c("setosa", "versicolor", "virginica", "unknown species"), |
|
131 |
#' selected = "setosa", |
|
132 |
#' multiple = FALSE |
|
133 |
#' ), |
|
134 |
#' verbatimTextOutput("summary") |
|
135 |
#' ) |
|
136 |
#' |
|
137 |
#' server <- function(input, output) { |
|
138 |
#' output$summary <- renderPrint({ |
|
139 |
#' validate_in(input$species, iris$Species, "Species does not exist.") |
|
140 |
#' nrow(iris[iris$Species == input$species, ]) |
|
141 |
#' }) |
|
142 |
#' } |
|
143 |
#' if (interactive()) { |
|
144 |
#' shinyApp(ui, server) |
|
145 |
#' } |
|
146 |
#' |
|
147 |
validate_in <- function(x, choices, msg) { |
|
148 | ! |
validate(need(length(x) > 0 && length(choices) > 0 && all(x %in% choices), msg)) |
149 |
} |
|
150 | ||
151 |
#' Validates that vector has length greater than 0 |
|
152 |
#' |
|
153 |
#' @description `r lifecycle::badge("stable")` |
|
154 |
#' @param x vector |
|
155 |
#' @param msg message to display |
|
156 |
#' |
|
157 |
#' @details This function is a wrapper for `shiny::validate`. |
|
158 |
#' |
|
159 |
#' @export |
|
160 |
#' |
|
161 |
#' @examples |
|
162 |
#' data <- data.frame( |
|
163 |
#' id = c(1:10, 11:20, 1:10), |
|
164 |
#' strata = rep(c("A", "B"), each = 15) |
|
165 |
#' ) |
|
166 |
#' ui <- fluidPage( |
|
167 |
#' selectInput("ref1", "Select strata1 to compare", |
|
168 |
#' choices = c("A", "B", "C"), selected = "A" |
|
169 |
#' ), |
|
170 |
#' selectInput("ref2", "Select strata2 to compare", |
|
171 |
#' choices = c("A", "B", "C"), selected = "B" |
|
172 |
#' ), |
|
173 |
#' verbatimTextOutput("arm_summary") |
|
174 |
#' ) |
|
175 |
#' |
|
176 |
#' server <- function(input, output) { |
|
177 |
#' output$arm_summary <- renderText({ |
|
178 |
#' sample_1 <- data$id[data$strata == input$ref1] |
|
179 |
#' sample_2 <- data$id[data$strata == input$ref2] |
|
180 |
#' |
|
181 |
#' validate_has_elements(sample_1, "No subjects in strata1.") |
|
182 |
#' validate_has_elements(sample_2, "No subjects in strata2.") |
|
183 |
#' |
|
184 |
#' paste0( |
|
185 |
#' "Number of samples in: strata1=", length(sample_1), |
|
186 |
#' " comparions strata2=", length(sample_2) |
|
187 |
#' ) |
|
188 |
#' }) |
|
189 |
#' } |
|
190 |
#' if (interactive()) { |
|
191 |
#' shinyApp(ui, server) |
|
192 |
#' } |
|
193 |
validate_has_elements <- function(x, msg) { |
|
194 | ! |
validate(need(length(x) > 0, msg)) |
195 |
} |
|
196 | ||
197 |
#' Validates no intersection between two vectors |
|
198 |
#' |
|
199 |
#' @description `r lifecycle::badge("stable")` |
|
200 |
#' @param x vector |
|
201 |
#' @param y vector |
|
202 |
#' @param msg message to display if \code{x} and \code{y} intersect |
|
203 |
#' |
|
204 |
#' @details This function is a wrapper for `shiny::validate`. |
|
205 |
#' |
|
206 |
#' @export |
|
207 |
#' |
|
208 |
#' @examples |
|
209 |
#' data <- data.frame( |
|
210 |
#' id = c(1:10, 11:20, 1:10), |
|
211 |
#' strata = rep(c("A", "B", "C"), each = 10) |
|
212 |
#' ) |
|
213 |
#' |
|
214 |
#' ui <- fluidPage( |
|
215 |
#' selectInput("ref1", "Select strata1 to compare", |
|
216 |
#' choices = c("A", "B", "C"), |
|
217 |
#' selected = "A" |
|
218 |
#' ), |
|
219 |
#' selectInput("ref2", "Select strata2 to compare", |
|
220 |
#' choices = c("A", "B", "C"), |
|
221 |
#' selected = "B" |
|
222 |
#' ), |
|
223 |
#' verbatimTextOutput("summary") |
|
224 |
#' ) |
|
225 |
#' |
|
226 |
#' server <- function(input, output) { |
|
227 |
#' output$summary <- renderText({ |
|
228 |
#' sample_1 <- data$id[data$strata == input$ref1] |
|
229 |
#' sample_2 <- data$id[data$strata == input$ref2] |
|
230 |
#' |
|
231 |
#' validate_no_intersection( |
|
232 |
#' sample_1, sample_2, |
|
233 |
#' "subjects within strata1 and strata2 cannot overlap" |
|
234 |
#' ) |
|
235 |
#' paste0( |
|
236 |
#' "Number of subject in: reference treatment=", length(sample_1), |
|
237 |
#' " comparions treatment=", length(sample_2) |
|
238 |
#' ) |
|
239 |
#' }) |
|
240 |
#' } |
|
241 |
#' if (interactive()) { |
|
242 |
#' shinyApp(ui, server) |
|
243 |
#' } |
|
244 |
#' |
|
245 |
validate_no_intersection <- function(x, y, msg) { |
|
246 | ! |
validate(need(length(intersect(x, y)) == 0, msg)) |
247 |
} |
|
248 | ||
249 | ||
250 |
#' Validates that dataset contains specific variable |
|
251 |
#' |
|
252 |
#' @description `r lifecycle::badge("stable")` |
|
253 |
#' @param data a data.frame |
|
254 |
#' @param varname name of variable in \code{data} |
|
255 |
#' @param msg message to display if \code{data} does not include \code{varname} |
|
256 |
#' |
|
257 |
#' @details This function is a wrapper for `shiny::validate`. |
|
258 |
#' |
|
259 |
#' @export |
|
260 |
#' |
|
261 |
#' @examples |
|
262 |
#' data <- data.frame( |
|
263 |
#' one = rep("a", length.out = 20), |
|
264 |
#' two = rep(c("a", "b"), length.out = 20) |
|
265 |
#' ) |
|
266 |
#' ui <- fluidPage( |
|
267 |
#' selectInput( |
|
268 |
#' "var", |
|
269 |
#' "Select variable", |
|
270 |
#' choices = c("one", "two", "three", "four"), |
|
271 |
#' selected = "one" |
|
272 |
#' ), |
|
273 |
#' verbatimTextOutput("summary") |
|
274 |
#' ) |
|
275 |
#' |
|
276 |
#' server <- function(input, output) { |
|
277 |
#' output$summary <- renderText({ |
|
278 |
#' validate_has_variable(data, input$var) |
|
279 |
#' paste0("Selected treatment variables: ", paste(input$var, collapse = ", ")) |
|
280 |
#' }) |
|
281 |
#' } |
|
282 |
#' if (interactive()) { |
|
283 |
#' shinyApp(ui, server) |
|
284 |
#' } |
|
285 |
validate_has_variable <- function(data, varname, msg) { |
|
286 | ! |
if (length(varname) != 0) { |
287 | ! |
has_vars <- varname %in% names(data) |
288 | ||
289 | ! |
if (!all(has_vars)) { |
290 | ! |
if (missing(msg)) { |
291 | ! |
msg <- sprintf( |
292 | ! |
"%s does not have the required variables: %s.", |
293 | ! |
deparse(substitute(data)), |
294 | ! |
toString(varname[!has_vars]) |
295 |
) |
|
296 |
} |
|
297 | ! |
validate(need(FALSE, msg)) |
298 |
} |
|
299 |
} |
|
300 |
} |
|
301 | ||
302 |
#' Validate that variables has expected number of levels |
|
303 |
#' |
|
304 |
#' @description `r lifecycle::badge("stable")` |
|
305 |
#' @param x variable name. If \code{x} is not a factor, the unique values |
|
306 |
#' are treated as levels. |
|
307 |
#' @param min_levels cutoff for minimum number of levels of \code{x} |
|
308 |
#' @param max_levels cutoff for maximum number of levels of \code{x} |
|
309 |
#' @param var_name name of variable being validated for use in |
|
310 |
#' validation message |
|
311 |
#' |
|
312 |
#' @details If the number of levels of \code{x} is less than \code{min_levels} |
|
313 |
#' or greater than \code{max_levels} the validation will fail. |
|
314 |
#' This function is a wrapper for `shiny::validate`. |
|
315 |
#' |
|
316 |
#' @export |
|
317 |
#' @examples |
|
318 |
#' data <- data.frame( |
|
319 |
#' one = rep("a", length.out = 20), |
|
320 |
#' two = rep(c("a", "b"), length.out = 20), |
|
321 |
#' three = rep(c("a", "b", "c"), length.out = 20), |
|
322 |
#' four = rep(c("a", "b", "c", "d"), length.out = 20), |
|
323 |
#' stringsAsFactors = TRUE |
|
324 |
#' ) |
|
325 |
#' ui <- fluidPage( |
|
326 |
#' selectInput( |
|
327 |
#' "var", |
|
328 |
#' "Select variable", |
|
329 |
#' choices = c("one", "two", "three", "four"), |
|
330 |
#' selected = "one" |
|
331 |
#' ), |
|
332 |
#' verbatimTextOutput("summary") |
|
333 |
#' ) |
|
334 |
#' |
|
335 |
#' server <- function(input, output) { |
|
336 |
#' output$summary <- renderText({ |
|
337 |
#' validate_n_levels(data[[input$var]], min_levels = 2, max_levels = 15, var_name = input$var) |
|
338 |
#' paste0( |
|
339 |
#' "Levels of selected treatment variable: ", |
|
340 |
#' paste(levels(data[[input$var]]), |
|
341 |
#' collapse = ", " |
|
342 |
#' ) |
|
343 |
#' ) |
|
344 |
#' }) |
|
345 |
#' } |
|
346 |
#' if (interactive()) { |
|
347 |
#' shinyApp(ui, server) |
|
348 |
#' } |
|
349 |
validate_n_levels <- function(x, min_levels = 1, max_levels = 12, var_name) { |
|
350 | ! |
x_levels <- if (is.factor(x)) { |
351 | ! |
levels(x) |
352 |
} else { |
|
353 | ! |
unique(x) |
354 |
} |
|
355 | ||
356 | ! |
if (!is.null(min_levels) && !(is.null(max_levels))) { |
357 | ! |
validate(need( |
358 | ! |
length(x_levels) >= min_levels && length(x_levels) <= max_levels, |
359 | ! |
sprintf( |
360 | ! |
"%s variable needs minimum %s level(s) and maximum %s level(s).", |
361 | ! |
var_name, min_levels, max_levels |
362 |
) |
|
363 |
)) |
|
364 | ! |
} else if (!is.null(min_levels)) { |
365 | ! |
validate(need( |
366 | ! |
length(x_levels) >= min_levels, |
367 | ! |
sprintf("%s variable needs minimum %s levels(s)", var_name, min_levels) |
368 |
)) |
|
369 | ! |
} else if (!is.null(max_levels)) { |
370 | ! |
validate(need( |
371 | ! |
length(x_levels) <= max_levels, |
372 | ! |
sprintf("%s variable needs maximum %s level(s)", var_name, max_levels) |
373 |
)) |
|
374 |
} |
|
375 |
} |
1 |
.onLoad <- function(libname, pkgname) { # nolint |
|
2 |
# adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R |
|
3 | ! |
teal_default_options <- list(teal.show_js_log = FALSE) |
4 | ||
5 | ! |
op <- options() |
6 | ! |
toset <- !(names(teal_default_options) %in% names(op)) |
7 | ! |
if (any(toset)) options(teal_default_options[toset]) |
8 | ||
9 | ! |
options("shiny.sanitize.errors" = FALSE) |
10 | ||
11 |
# Set up the teal logger instance |
|
12 | ! |
teal.logger::register_logger("teal") |
13 | ||
14 | ! |
invisible() |
15 |
} |
|
16 | ||
17 |
.onAttach <- function(libname, pkgname) { # nolint |
|
18 | 2x |
packageStartupMessage( |
19 | 2x |
"\nYou are using teal version ", |
20 |
# `system.file` uses the `shim` of `system.file` by `teal` |
|
21 |
# we avoid `desc` dependency here to get the version |
|
22 | 2x |
read.dcf(system.file("DESCRIPTION", package = "teal"))[, "Version"] |
23 |
) |
|
24 |
} |
|
25 | ||
26 |
# Use non-exported function(s) from teal.slice. |
|
27 |
# This is a temporary measure and will be removed two release cycles from now (now meaning 0.13.0). |
|
28 |
list_to_teal_slices <- getFromNamespace("list_to_teal_slices", "teal.slice") # nolint |
|
29 |
# This one is here because setdiff_teal_slice should not be exported from teal.slice. |
|
30 |
setdiff_teal_slices <- getFromNamespace("setdiff_teal_slices", "teal.slice") |
|
31 |
# all *Block objects are private in teal.reporter |
|
32 |
RcodeBlock <- getFromNamespace("RcodeBlock", "teal.reporter") # nolint |
1 |
#' Generates library calls from current session info |
|
2 |
#' |
|
3 |
#' Function to create multiple library calls out of current session info to make reproducible code works. |
|
4 |
#' |
|
5 |
#' @return Character object contain code |
|
6 |
#' @keywords internal |
|
7 |
get_rcode_libraries <- function() { |
|
8 | 14x |
vapply( |
9 | 14x |
utils::sessionInfo()$otherPkgs, |
10 | 14x |
function(x) { |
11 | 238x |
paste0("library(", x$Package, ")") |
12 |
}, |
|
13 | 14x |
character(1) |
14 |
) %>% |
|
15 |
# put it into reverse order to correctly simulate executed code |
|
16 | 14x |
rev() %>% |
17 | 14x |
paste0(sep = "\n") %>% |
18 | 14x |
paste0(collapse = "") |
19 |
} |
|
20 | ||
21 | ||
22 | ||
23 |
get_rcode_str_install <- function() { |
|
24 | 18x |
code_string <- getOption("teal.load_nest_code") |
25 | ||
26 | 18x |
if (!is.null(code_string) && is.character(code_string)) { |
27 | 2x |
return(code_string) |
28 |
} |
|
29 | ||
30 | 16x |
return("# Add any code to install/load your NEST environment here\n") |
31 |
} |
|
32 | ||
33 |
#' Get datasets code |
|
34 |
#' |
|
35 |
#' Get combined code from `FilteredData` and from `CodeClass` object. |
|
36 |
#' |
|
37 |
#' @param datanames (`character`) names of datasets to extract code from |
|
38 |
#' @param datasets (`FilteredData`) object |
|
39 |
#' @param hashes named (`list`) of hashes per dataset |
|
40 |
#' |
|
41 |
#' @return `character(3)` containing following elements: |
|
42 |
#' - code from `CodeClass` (data loading code) |
|
43 |
#' - hash check of loaded objects |
|
44 |