1 |
#' `teal` module: Principal component analysis
|
|
2 |
#'
|
|
3 |
#' Module conducts principal component analysis (PCA) on a given dataset and offers different
|
|
4 |
#' ways of visualizing the outcomes, including elbow plot, circle plot, biplot, and eigenvector plot.
|
|
5 |
#' Additionally, it enables dynamic customization of plot aesthetics, such as opacity, size, and
|
|
6 |
#' font size, through UI inputs.
|
|
7 |
#'
|
|
8 |
#' @inheritParams teal::module
|
|
9 |
#' @inheritParams shared_params
|
|
10 |
#' @param dat (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
11 |
#' specifying columns used to compute PCA.
|
|
12 |
#' @param font_size (`numeric`) optional, specifies font size.
|
|
13 |
#' It controls the font size for plot titles, axis labels, and legends.
|
|
14 |
#' - If vector of `length == 1` then the font sizes will have a fixed size.
|
|
15 |
#' - while vector of `value`, `min`, and `max` allows dynamic adjustment.
|
|
16 |
#' @param ggplot2_args `r roxygen_ggplot2_args_param("Elbow plot", "Circle plot", "Biplot", "Eigenvector plot")`
|
|
17 |
#'
|
|
18 |
#' @inherit shared_params return
|
|
19 |
#'
|
|
20 |
#' @section Decorating Module:
|
|
21 |
#'
|
|
22 |
#' This module generates the following objects, which can be modified in place using decorators:
|
|
23 |
#' - `elbow_plot` (`ggplot`)
|
|
24 |
#' - `circle_plot` (`ggplot`)
|
|
25 |
#' - `biplot` (`ggplot`)
|
|
26 |
#' - `eigenvector_plot` (`ggplot`)
|
|
27 |
#'
|
|
28 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
29 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
30 |
#' See code snippet below:
|
|
31 |
#'
|
|
32 |
#' ```
|
|
33 |
#' tm_a_pca(
|
|
34 |
#' ..., # arguments for module
|
|
35 |
#' decorators = list(
|
|
36 |
#' elbow_plot = teal_transform_module(...), # applied to the `elbow_plot` output
|
|
37 |
#' circle_plot = teal_transform_module(...), # applied to the `circle_plot` output
|
|
38 |
#' biplot = teal_transform_module(...), # applied to the `biplot` output
|
|
39 |
#' eigenvector_plot = teal_transform_module(...) # applied to the `eigenvector_plot` output
|
|
40 |
#' )
|
|
41 |
#' )
|
|
42 |
#' ```
|
|
43 |
#'
|
|
44 |
#' For additional details and examples of decorators, refer to the vignette
|
|
45 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
46 |
#'
|
|
47 |
#' To learn more please refer to the vignette
|
|
48 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
49 |
#'
|
|
50 |
#' @examplesShinylive
|
|
51 |
#' library(teal.modules.general)
|
|
52 |
#' interactive <- function() TRUE
|
|
53 |
#' {{ next_example }}
|
|
54 |
#' @examples
|
|
55 |
#'
|
|
56 |
#' # general data example
|
|
57 |
#' data <- teal_data()
|
|
58 |
#' data <- within(data, {
|
|
59 |
#' require(nestcolor)
|
|
60 |
#' USArrests <- USArrests
|
|
61 |
#' })
|
|
62 |
#'
|
|
63 |
#' app <- init(
|
|
64 |
#' data = data,
|
|
65 |
#' modules = modules(
|
|
66 |
#' tm_a_pca(
|
|
67 |
#' "PCA",
|
|
68 |
#' dat = data_extract_spec(
|
|
69 |
#' dataname = "USArrests",
|
|
70 |
#' select = select_spec(
|
|
71 |
#' choices = variable_choices(
|
|
72 |
#' data = data[["USArrests"]], c("Murder", "Assault", "UrbanPop", "Rape")
|
|
73 |
#' ),
|
|
74 |
#' selected = c("Murder", "Assault"),
|
|
75 |
#' multiple = TRUE
|
|
76 |
#' ),
|
|
77 |
#' filter = NULL
|
|
78 |
#' )
|
|
79 |
#' )
|
|
80 |
#' )
|
|
81 |
#' )
|
|
82 |
#' if (interactive()) {
|
|
83 |
#' shinyApp(app$ui, app$server)
|
|
84 |
#' }
|
|
85 |
#'
|
|
86 |
#' @examplesShinylive
|
|
87 |
#' library(teal.modules.general)
|
|
88 |
#' interactive <- function() TRUE
|
|
89 |
#' {{ next_example }}
|
|
90 |
#' @examples
|
|
91 |
#'
|
|
92 |
#' # CDISC data example
|
|
93 |
#' data <- teal_data()
|
|
94 |
#' data <- within(data, {
|
|
95 |
#' require(nestcolor)
|
|
96 |
#' ADSL <- teal.data::rADSL
|
|
97 |
#' })
|
|
98 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
99 |
#'
|
|
100 |
#' app <- init(
|
|
101 |
#' data = data,
|
|
102 |
#' modules = modules(
|
|
103 |
#' tm_a_pca(
|
|
104 |
#' "PCA",
|
|
105 |
#' dat = data_extract_spec(
|
|
106 |
#' dataname = "ADSL",
|
|
107 |
#' select = select_spec(
|
|
108 |
#' choices = variable_choices(
|
|
109 |
#' data = data[["ADSL"]], c("BMRKR1", "AGE", "EOSDY")
|
|
110 |
#' ),
|
|
111 |
#' selected = c("BMRKR1", "AGE"),
|
|
112 |
#' multiple = TRUE
|
|
113 |
#' ),
|
|
114 |
#' filter = NULL
|
|
115 |
#' )
|
|
116 |
#' )
|
|
117 |
#' )
|
|
118 |
#' )
|
|
119 |
#' if (interactive()) {
|
|
120 |
#' shinyApp(app$ui, app$server)
|
|
121 |
#' }
|
|
122 |
#'
|
|
123 |
#' @export
|
|
124 |
#'
|
|
125 |
tm_a_pca <- function(label = "Principal Component Analysis", |
|
126 |
dat,
|
|
127 |
plot_height = c(600, 200, 2000), |
|
128 |
plot_width = NULL, |
|
129 |
ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), |
|
130 |
ggplot2_args = teal.widgets::ggplot2_args(), |
|
131 |
rotate_xaxis_labels = FALSE, |
|
132 |
font_size = c(12, 8, 20), |
|
133 |
alpha = c(1, 0, 1), |
|
134 |
size = c(2, 1, 8), |
|
135 |
pre_output = NULL, |
|
136 |
post_output = NULL, |
|
137 |
transformators = list(), |
|
138 |
decorators = list()) { |
|
139 | ! |
message("Initializing tm_a_pca") |
140 | ||
141 |
# Normalize the parameters
|
|
142 | ! |
if (inherits(dat, "data_extract_spec")) dat <- list(dat) |
143 | ! |
if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args) |
144 | ||
145 |
# Start of assertions
|
|
146 | ! |
checkmate::assert_string(label) |
147 | ! |
checkmate::assert_list(dat, types = "data_extract_spec") |
148 | ||
149 | ! |
checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
150 | ! |
checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
151 | ! |
checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
152 | ! |
checkmate::assert_numeric( |
153 | ! |
plot_width[1], |
154 | ! |
lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width" |
155 |
)
|
|
156 | ||
157 | ! |
ggtheme <- match.arg(ggtheme) |
158 | ||
159 | ! |
plot_choices <- c("Elbow plot", "Circle plot", "Biplot", "Eigenvector plot") |
160 | ! |
checkmate::assert_list(ggplot2_args, types = "ggplot2_args") |
161 | ! |
checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices)) |
162 | ||
163 | ! |
checkmate::assert_flag(rotate_xaxis_labels) |
164 | ||
165 | ! |
if (length(font_size) == 1) { |
166 | ! |
checkmate::assert_numeric(font_size, any.missing = FALSE, finite = TRUE, lower = 8, upper = 20) |
167 |
} else { |
|
168 | ! |
checkmate::assert_numeric(font_size, len = 3, any.missing = FALSE, finite = TRUE, lower = 8, upper = 20) |
169 | ! |
checkmate::assert_numeric(font_size[1], lower = font_size[2], upper = font_size[3], .var.name = "font_size") |
170 |
}
|
|
171 | ||
172 | ! |
if (length(alpha) == 1) { |
173 | ! |
checkmate::assert_numeric(alpha, any.missing = FALSE, finite = TRUE, lower = 0, upper = 1) |
174 |
} else { |
|
175 | ! |
checkmate::assert_numeric(alpha, len = 3, any.missing = FALSE, finite = TRUE, lower = 0, upper = 1) |
176 | ! |
checkmate::assert_numeric(alpha[1], lower = alpha[2], upper = alpha[3], .var.name = "alpha") |
177 |
}
|
|
178 | ||
179 | ! |
if (length(size) == 1) { |
180 | ! |
checkmate::assert_numeric(size, any.missing = FALSE, finite = TRUE, lower = 1, upper = 8) |
181 |
} else { |
|
182 | ! |
checkmate::assert_numeric(size, len = 3, any.missing = FALSE, finite = TRUE, lower = 1, upper = 8) |
183 | ! |
checkmate::assert_numeric(size[1], lower = size[2], upper = size[3], .var.name = "size") |
184 |
}
|
|
185 | ||
186 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
187 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
188 | ||
189 | ! |
available_decorators <- c("elbow_plot", "circle_plot", "biplot", "eigenvector_plot") |
190 | ! |
assert_decorators(decorators, available_decorators) |
191 | ||
192 |
# Make UI args
|
|
193 | ! |
args <- as.list(environment()) |
194 | ||
195 | ! |
data_extract_list <- list(dat = dat) |
196 | ||
197 | ! |
ans <- module( |
198 | ! |
label = label, |
199 | ! |
server = srv_a_pca, |
200 | ! |
ui = ui_a_pca, |
201 | ! |
ui_args = args, |
202 | ! |
server_args = c( |
203 | ! |
data_extract_list,
|
204 | ! |
list( |
205 | ! |
plot_height = plot_height, |
206 | ! |
plot_width = plot_width, |
207 | ! |
ggplot2_args = ggplot2_args, |
208 | ! |
decorators = decorators |
209 |
)
|
|
210 |
),
|
|
211 | ! |
transformators = transformators, |
212 | ! |
datanames = teal.transform::get_extract_datanames(data_extract_list) |
213 |
)
|
|
214 | ! |
attr(ans, "teal_bookmarkable") <- FALSE |
215 | ! |
ans
|
216 |
}
|
|
217 | ||
218 |
# UI function for the PCA module
|
|
219 |
ui_a_pca <- function(id, ...) { |
|
220 | ! |
ns <- NS(id) |
221 | ! |
args <- list(...) |
222 | ! |
is_single_dataset_value <- teal.transform::is_single_dataset(args$dat) |
223 | ||
224 | ! |
color_selector <- args$dat |
225 | ! |
for (i in seq_along(color_selector)) { |
226 | ! |
color_selector[[i]]$select$multiple <- FALSE |
227 | ! |
color_selector[[i]]$select$always_selected <- NULL |
228 | ! |
color_selector[[i]]$select$selected <- NULL |
229 |
}
|
|
230 | ||
231 | ! |
tagList( |
232 | ! |
include_css_files("custom"), |
233 | ! |
teal.widgets::standard_layout( |
234 | ! |
output = teal.widgets::white_small_well( |
235 | ! |
uiOutput(ns("all_plots")) |
236 |
),
|
|
237 | ! |
encoding = tags$div( |
238 |
### Reporter
|
|
239 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
240 |
###
|
|
241 | ! |
tags$label("Encodings", class = "text-primary"), |
242 | ! |
teal.transform::datanames_input(args["dat"]), |
243 | ! |
teal.transform::data_extract_ui( |
244 | ! |
id = ns("dat"), |
245 | ! |
label = "Data selection", |
246 | ! |
data_extract_spec = args$dat, |
247 | ! |
is_single_dataset = is_single_dataset_value |
248 |
),
|
|
249 | ! |
teal.widgets::panel_group( |
250 | ! |
teal.widgets::panel_item( |
251 | ! |
title = "Display", |
252 | ! |
collapsed = FALSE, |
253 | ! |
checkboxGroupInput( |
254 | ! |
ns("tables_display"), |
255 | ! |
"Tables display",
|
256 | ! |
choices = c("PC importance" = "importance", "Eigenvectors" = "eigenvector"), |
257 | ! |
selected = c("importance", "eigenvector") |
258 |
),
|
|
259 | ! |
radioButtons( |
260 | ! |
ns("plot_type"), |
261 | ! |
label = "Plot type", |
262 | ! |
choices = args$plot_choices, |
263 | ! |
selected = args$plot_choices[1] |
264 |
),
|
|
265 | ! |
conditionalPanel( |
266 | ! |
condition = sprintf("input['%s'] == 'Elbow plot'", ns("plot_type")), |
267 | ! |
ui_decorate_teal_data( |
268 | ! |
ns("d_elbow_plot"), |
269 | ! |
decorators = select_decorators(args$decorators, "elbow_plot") |
270 |
)
|
|
271 |
),
|
|
272 | ! |
conditionalPanel( |
273 | ! |
condition = sprintf("input['%s'] == 'Circle plot'", ns("plot_type")), |
274 | ! |
ui_decorate_teal_data( |
275 | ! |
ns("d_circle_plot"), |
276 | ! |
decorators = select_decorators(args$decorators, "circle_plot") |
277 |
)
|
|
278 |
),
|
|
279 | ! |
conditionalPanel( |
280 | ! |
condition = sprintf("input['%s'] == 'Biplot'", ns("plot_type")), |
281 | ! |
ui_decorate_teal_data( |
282 | ! |
ns("d_biplot"), |
283 | ! |
decorators = select_decorators(args$decorators, "biplot") |
284 |
)
|
|
285 |
),
|
|
286 | ! |
conditionalPanel( |
287 | ! |
condition = sprintf("input['%s'] == 'Eigenvector plot'", ns("plot_type")), |
288 | ! |
ui_decorate_teal_data( |
289 | ! |
ns("d_eigenvector_plot"), |
290 | ! |
decorators = select_decorators(args$decorators, "eigenvector_plot") |
291 |
)
|
|
292 |
)
|
|
293 |
),
|
|
294 | ! |
teal.widgets::panel_item( |
295 | ! |
title = "Pre-processing", |
296 | ! |
radioButtons( |
297 | ! |
ns("standardization"), "Standardization", |
298 | ! |
choices = c("None" = "none", "Center" = "center", "Center & Scale" = "center_scale"), |
299 | ! |
selected = "center_scale" |
300 |
),
|
|
301 | ! |
radioButtons( |
302 | ! |
ns("na_action"), "NA action", |
303 | ! |
choices = c("None" = "none", "Drop" = "drop"), |
304 | ! |
selected = "none" |
305 |
)
|
|
306 |
),
|
|
307 | ! |
teal.widgets::panel_item( |
308 | ! |
title = "Selected plot specific settings", |
309 | ! |
collapsed = FALSE, |
310 | ! |
uiOutput(ns("plot_settings")), |
311 | ! |
conditionalPanel( |
312 | ! |
condition = sprintf("input['%s'] == 'Biplot'", ns("plot_type")), |
313 | ! |
list( |
314 | ! |
teal.transform::data_extract_ui( |
315 | ! |
id = ns("response"), |
316 | ! |
label = "Color by", |
317 | ! |
data_extract_spec = color_selector, |
318 | ! |
is_single_dataset = is_single_dataset_value |
319 |
),
|
|
320 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("alpha"), "Opacity:", args$alpha, ticks = FALSE), |
321 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("size"), "Points size:", args$size, ticks = FALSE) |
322 |
)
|
|
323 |
)
|
|
324 |
),
|
|
325 | ! |
teal.widgets::panel_item( |
326 | ! |
title = "Plot settings", |
327 | ! |
collapsed = TRUE, |
328 | ! |
conditionalPanel( |
329 | ! |
condition = sprintf( |
330 | ! |
"input['%s'] == 'Elbow plot' || input['%s'] == 'Eigenvector plot'",
|
331 | ! |
ns("plot_type"), |
332 | ! |
ns("plot_type") |
333 |
),
|
|
334 | ! |
list(checkboxInput(ns("rotate_xaxis_labels"), "Rotate X axis labels", value = args$rotate_xaxis_labels)) |
335 |
),
|
|
336 | ! |
selectInput( |
337 | ! |
inputId = ns("ggtheme"), |
338 | ! |
label = "Theme (by ggplot):", |
339 | ! |
choices = ggplot_themes, |
340 | ! |
selected = args$ggtheme, |
341 | ! |
multiple = FALSE |
342 |
),
|
|
343 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("font_size"), "Font Size", args$font_size, ticks = FALSE) |
344 |
)
|
|
345 |
)
|
|
346 |
),
|
|
347 | ! |
forms = tagList( |
348 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
349 |
),
|
|
350 | ! |
pre_output = args$pre_output, |
351 | ! |
post_output = args$post_output |
352 |
)
|
|
353 |
)
|
|
354 |
}
|
|
355 | ||
356 |
# Server function for the PCA module
|
|
357 |
srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, plot_width, ggplot2_args, decorators) { |
|
358 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
359 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
360 | ! |
checkmate::assert_class(data, "reactive") |
361 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
362 | ! |
moduleServer(id, function(input, output, session) { |
363 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
364 | ||
365 | ! |
response <- dat |
366 | ||
367 | ! |
for (i in seq_along(response)) { |
368 | ! |
response[[i]]$select$multiple <- FALSE |
369 | ! |
response[[i]]$select$always_selected <- NULL |
370 | ! |
response[[i]]$select$selected <- NULL |
371 | ! |
all_cols <- teal.data::col_labels(isolate(data())[[response[[i]]$dataname]]) |
372 | ! |
ignore_cols <- unlist(teal.data::join_keys(isolate(data()))[[response[[i]]$dataname]]) |
373 | ! |
color_cols <- all_cols[!names(all_cols) %in% ignore_cols] |
374 | ! |
response[[i]]$select$choices <- teal.transform::choices_labeled(names(color_cols), color_cols) |
375 |
}
|
|
376 | ||
377 | ! |
selector_list <- teal.transform::data_extract_multiple_srv( |
378 | ! |
data_extract = list(dat = dat, response = response), |
379 | ! |
datasets = data, |
380 | ! |
select_validation_rule = list( |
381 | ! |
dat = ~ if (length(.) < 2L) "Please select more than 1 variable to perform PCA.", |
382 | ! |
response = shinyvalidate::compose_rules( |
383 | ! |
shinyvalidate::sv_optional(), |
384 | ! |
~ if (isTRUE(is.element(., selector_list()$dat()$select))) { |
385 | ! |
"Response must not have been used for PCA."
|
386 |
}
|
|
387 |
)
|
|
388 |
)
|
|
389 |
)
|
|
390 | ||
391 | ! |
iv_r <- reactive({ |
392 | ! |
iv <- shinyvalidate::InputValidator$new() |
393 | ! |
teal.transform::compose_and_enable_validators(iv, selector_list) |
394 |
}) |
|
395 | ||
396 | ! |
iv_extra <- shinyvalidate::InputValidator$new() |
397 | ! |
iv_extra$add_rule("x_axis", function(value) { |
398 | ! |
if (isTRUE(input$plot_type %in% c("Circle plot", "Biplot"))) { |
399 | ! |
if (!shinyvalidate::input_provided(value)) { |
400 | ! |
"Need X axis"
|
401 |
}
|
|
402 |
}
|
|
403 |
}) |
|
404 | ! |
iv_extra$add_rule("y_axis", function(value) { |
405 | ! |
if (isTRUE(input$plot_type %in% c("Circle plot", "Biplot"))) { |
406 | ! |
if (!shinyvalidate::input_provided(value)) { |
407 | ! |
"Need Y axis"
|
408 |
}
|
|
409 |
}
|
|
410 |
}) |
|
411 | ! |
rule_dupl <- function(...) { |
412 | ! |
if (isTRUE(input$plot_type %in% c("Circle plot", "Biplot"))) { |
413 | ! |
if (isTRUE(input$x_axis == input$y_axis)) { |
414 | ! |
"Please choose different X and Y axes."
|
415 |
}
|
|
416 |
}
|
|
417 |
}
|
|
418 | ! |
iv_extra$add_rule("x_axis", rule_dupl) |
419 | ! |
iv_extra$add_rule("y_axis", rule_dupl) |
420 | ! |
iv_extra$add_rule("variables", function(value) { |
421 | ! |
if (identical(input$plot_type, "Circle plot")) { |
422 | ! |
if (!shinyvalidate::input_provided(value)) { |
423 | ! |
"Need Original Coordinates"
|
424 |
}
|
|
425 |
}
|
|
426 |
}) |
|
427 | ! |
iv_extra$add_rule("pc", function(value) { |
428 | ! |
if (identical(input$plot_type, "Eigenvector plot")) { |
429 | ! |
if (!shinyvalidate::input_provided(value)) { |
430 | ! |
"Need PC"
|
431 |
}
|
|
432 |
}
|
|
433 |
}) |
|
434 | ! |
iv_extra$enable() |
435 | ||
436 | ! |
anl_merged_input <- teal.transform::merge_expression_srv( |
437 | ! |
selector_list = selector_list, |
438 | ! |
datasets = data |
439 |
)
|
|
440 | ! |
qenv <- teal.code::eval_code(data(), 'library("ggplot2");library("dplyr");library("tidyr")') # nolint quotes |
441 | ! |
anl_merged_q <- reactive({ |
442 | ! |
req(anl_merged_input()) |
443 | ! |
qenv %>% |
444 | ! |
teal.code::eval_code(as.expression(anl_merged_input()$expr)) |
445 |
}) |
|
446 | ||
447 | ! |
merged <- list( |
448 | ! |
anl_input_r = anl_merged_input, |
449 | ! |
anl_q_r = anl_merged_q |
450 |
)
|
|
451 | ||
452 | ! |
validation <- reactive({ |
453 | ! |
req(merged$anl_q_r()) |
454 |
# inputs
|
|
455 | ! |
keep_cols <- as.character(merged$anl_input_r()$columns_source$dat) |
456 | ! |
na_action <- input$na_action |
457 | ! |
standardization <- input$standardization |
458 | ! |
center <- standardization %in% c("center", "center_scale") |
459 | ! |
scale <- standardization == "center_scale" |
460 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
461 | ||
462 | ! |
teal::validate_has_data(ANL, 10) |
463 | ! |
validate(need( |
464 | ! |
na_action != "none" | !anyNA(ANL[keep_cols]), |
465 | ! |
paste( |
466 | ! |
"There are NAs in the dataset. Please deal with them in preprocessing",
|
467 | ! |
"or select \"Drop\" in the NA actions inside the encodings panel (left)."
|
468 |
)
|
|
469 |
)) |
|
470 | ! |
if (scale) { |
471 | ! |
not_single <- vapply(ANL[keep_cols], function(column) length(unique(column)) != 1, FUN.VALUE = logical(1)) |
472 | ||
473 | ! |
msg <- paste0( |
474 | ! |
"You have selected `Center & Scale` under `Standardization` in the `Pre-processing` panel, ",
|
475 | ! |
"but one or more of your columns has/have a variance value of zero, indicating all values are identical"
|
476 |
)
|
|
477 | ! |
validate(need(all(not_single), msg)) |
478 |
}
|
|
479 |
}) |
|
480 | ||
481 |
# computation ----
|
|
482 | ! |
computation <- reactive({ |
483 | ! |
validation() |
484 | ||
485 |
# inputs
|
|
486 | ! |
keep_cols <- as.character(merged$anl_input_r()$columns_source$dat) |
487 | ! |
na_action <- input$na_action |
488 | ! |
standardization <- input$standardization |
489 | ! |
center <- standardization %in% c("center", "center_scale") |
490 | ! |
scale <- standardization == "center_scale" |
491 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
492 | ||
493 | ! |
qenv <- teal.code::eval_code( |
494 | ! |
merged$anl_q_r(), |
495 | ! |
substitute( |
496 | ! |
expr = keep_columns <- keep_cols, |
497 | ! |
env = list(keep_cols = keep_cols) |
498 |
)
|
|
499 |
)
|
|
500 | ||
501 | ! |
if (na_action == "drop") { |
502 | ! |
qenv <- teal.code::eval_code( |
503 | ! |
qenv,
|
504 | ! |
quote(ANL <- tidyr::drop_na(ANL, keep_columns)) |
505 |
)
|
|
506 |
}
|
|
507 | ||
508 | ! |
qenv <- teal.code::eval_code( |
509 | ! |
qenv,
|
510 | ! |
substitute( |
511 | ! |
expr = pca <- summary(stats::prcomp(ANL[keep_columns], center = center, scale. = scale, retx = TRUE)), |
512 | ! |
env = list(center = center, scale = scale) |
513 |
)
|
|
514 |
)
|
|
515 | ||
516 | ! |
qenv <- teal.code::eval_code( |
517 | ! |
qenv,
|
518 | ! |
quote({ |
519 | ! |
tbl_importance <- dplyr::as_tibble(pca$importance, rownames = "Metric") |
520 | ! |
tbl_importance
|
521 |
}) |
|
522 |
)
|
|
523 | ||
524 | ! |
teal.code::eval_code( |
525 | ! |
qenv,
|
526 | ! |
quote({ |
527 | ! |
tbl_eigenvector <- dplyr::as_tibble(pca$rotation, rownames = "Variable") |
528 | ! |
tbl_eigenvector
|
529 |
}) |
|
530 |
)
|
|
531 |
}) |
|
532 | ||
533 |
# plot args ----
|
|
534 | ! |
output$plot_settings <- renderUI({ |
535 |
# reactivity triggers
|
|
536 | ! |
req(iv_r()$is_valid()) |
537 | ! |
req(computation()) |
538 | ! |
qenv <- computation() |
539 | ||
540 | ! |
ns <- session$ns |
541 | ||
542 | ! |
pca <- qenv[["pca"]] |
543 | ! |
chcs_pcs <- colnames(pca$rotation) |
544 | ! |
chcs_vars <- qenv[["keep_columns"]] |
545 | ||
546 | ! |
tagList( |
547 | ! |
conditionalPanel( |
548 | ! |
condition = sprintf( |
549 | ! |
"input['%s'] == 'Biplot' || input['%s'] == 'Circle plot'",
|
550 | ! |
ns("plot_type"), ns("plot_type") |
551 |
),
|
|
552 | ! |
list( |
553 | ! |
teal.widgets::optionalSelectInput(ns("x_axis"), "X axis", choices = chcs_pcs, selected = chcs_pcs[1]), |
554 | ! |
teal.widgets::optionalSelectInput(ns("y_axis"), "Y axis", choices = chcs_pcs, selected = chcs_pcs[2]), |
555 | ! |
teal.widgets::optionalSelectInput( |
556 | ! |
ns("variables"), "Original coordinates", |
557 | ! |
choices = chcs_vars, selected = chcs_vars, |
558 | ! |
multiple = TRUE |
559 |
)
|
|
560 |
)
|
|
561 |
),
|
|
562 | ! |
conditionalPanel( |
563 | ! |
condition = sprintf("input['%s'] == 'Elbow plot'", ns("plot_type")), |
564 | ! |
helpText("No plot specific settings available.") |
565 |
),
|
|
566 | ! |
conditionalPanel( |
567 | ! |
condition = paste0("input['", ns("plot_type"), "'] == 'Eigenvector plot'"), |
568 | ! |
teal.widgets::optionalSelectInput(ns("pc"), "PC", choices = chcs_pcs, selected = chcs_pcs[1]) |
569 |
)
|
|
570 |
)
|
|
571 |
}) |
|
572 | ||
573 |
# plot elbow ----
|
|
574 | ! |
plot_elbow <- function(base_q) { |
575 | ! |
ggtheme <- input$ggtheme |
576 | ! |
rotate_xaxis_labels <- input$rotate_xaxis_labels |
577 | ! |
font_size <- input$font_size |
578 | ||
579 | ! |
angle_value <- ifelse(isTRUE(rotate_xaxis_labels), 45, 0) |
580 | ! |
hjust_value <- ifelse(isTRUE(rotate_xaxis_labels), 1, 0.5) |
581 | ||
582 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
583 | ! |
labs = list(x = "Principal component", y = "Proportion of variance explained", color = "", fill = "Legend"), |
584 | ! |
theme = list( |
585 | ! |
legend.position = "right", |
586 | ! |
legend.spacing.y = quote(grid::unit(-5, "pt")), |
587 | ! |
legend.title = quote(ggplot2::element_text(vjust = 25)), |
588 | ! |
axis.text.x = substitute( |
589 | ! |
ggplot2::element_text(angle = angle_value, hjust = hjust_value), |
590 | ! |
list(angle_value = angle_value, hjust_value = hjust_value) |
591 |
),
|
|
592 | ! |
text = substitute(ggplot2::element_text(size = font_size), list(font_size = font_size)) |
593 |
)
|
|
594 |
)
|
|
595 | ||
596 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
597 | ! |
teal.widgets::resolve_ggplot2_args( |
598 | ! |
user_plot = ggplot2_args[["Elbow plot"]], |
599 | ! |
user_default = ggplot2_args$default, |
600 | ! |
module_plot = dev_ggplot2_args |
601 |
),
|
|
602 | ! |
ggtheme = ggtheme |
603 |
)
|
|
604 | ||
605 | ! |
teal.code::eval_code( |
606 | ! |
base_q,
|
607 | ! |
substitute( |
608 | ! |
expr = { |
609 | ! |
elb_dat <- pca$importance[c("Proportion of Variance", "Cumulative Proportion"), ] %>% |
610 | ! |
dplyr::as_tibble(rownames = "metric") %>% |
611 | ! |
tidyr::gather("component", "value", -metric) %>% |
612 | ! |
dplyr::mutate( |
613 | ! |
component = factor(component, levels = unique(stringr::str_sort(component, numeric = TRUE))) |
614 |
)
|
|
615 | ||
616 | ! |
cols <- c(getOption("ggplot2.discrete.colour"), c("lightblue", "darkred", "black"))[1:3] |
617 | ! |
elbow_plot <- ggplot2::ggplot(mapping = ggplot2::aes_string(x = "component", y = "value")) + |
618 | ! |
ggplot2::geom_bar( |
619 | ! |
ggplot2::aes(fill = "Single variance"), |
620 | ! |
data = dplyr::filter(elb_dat, metric == "Proportion of Variance"), |
621 | ! |
color = "black", |
622 | ! |
stat = "identity" |
623 |
) + |
|
624 | ! |
ggplot2::geom_point( |
625 | ! |
ggplot2::aes(color = "Cumulative variance"), |
626 | ! |
data = dplyr::filter(elb_dat, metric == "Cumulative Proportion") |
627 |
) + |
|
628 | ! |
ggplot2::geom_line( |
629 | ! |
ggplot2::aes(group = 1, color = "Cumulative variance"), |
630 | ! |
data = dplyr::filter(elb_dat, metric == "Cumulative Proportion") |
631 |
) + |
|
632 | ! |
labs + |
633 | ! |
ggplot2::scale_color_manual(values = c("Cumulative variance" = cols[2], "Single variance" = cols[3])) + |
634 | ! |
ggplot2::scale_fill_manual(values = c("Cumulative variance" = cols[2], "Single variance" = cols[1])) + |
635 | ! |
ggthemes + |
636 | ! |
themes
|
637 |
},
|
|
638 | ! |
env = list( |
639 | ! |
ggthemes = parsed_ggplot2_args$ggtheme, |
640 | ! |
labs = parsed_ggplot2_args$labs, |
641 | ! |
themes = parsed_ggplot2_args$theme |
642 |
)
|
|
643 |
)
|
|
644 |
)
|
|
645 |
}
|
|
646 | ||
647 |
# plot circle ----
|
|
648 | ! |
plot_circle <- function(base_q) { |
649 | ! |
x_axis <- input$x_axis |
650 | ! |
y_axis <- input$y_axis |
651 | ! |
variables <- input$variables |
652 | ! |
ggtheme <- input$ggtheme |
653 | ||
654 | ! |
rotate_xaxis_labels <- input$rotate_xaxis_labels |
655 | ! |
font_size <- input$font_size |
656 | ||
657 | ! |
angle <- ifelse(isTRUE(rotate_xaxis_labels), 45, 0) |
658 | ! |
hjust <- ifelse(isTRUE(rotate_xaxis_labels), 1, 0.5) |
659 | ||
660 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
661 | ! |
theme = list( |
662 | ! |
text = substitute(ggplot2::element_text(size = font_size), list(font_size = font_size)), |
663 | ! |
axis.text.x = substitute( |
664 | ! |
ggplot2::element_text(angle = angle_val, hjust = hjust_val), |
665 | ! |
list(angle_val = angle, hjust_val = hjust) |
666 |
)
|
|
667 |
)
|
|
668 |
)
|
|
669 | ||
670 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
671 | ! |
user_plot = ggplot2_args[["Circle plot"]], |
672 | ! |
user_default = ggplot2_args$default, |
673 | ! |
module_plot = dev_ggplot2_args |
674 |
)
|
|
675 | ||
676 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
677 | ! |
all_ggplot2_args,
|
678 | ! |
ggtheme = ggtheme |
679 |
)
|
|
680 | ||
681 | ! |
teal.code::eval_code( |
682 | ! |
base_q,
|
683 | ! |
substitute( |
684 | ! |
expr = { |
685 | ! |
pca_rot <- pca$rotation[, c(x_axis, y_axis)] %>% |
686 | ! |
dplyr::as_tibble(rownames = "label") %>% |
687 | ! |
dplyr::filter(label %in% variables) |
688 | ||
689 | ! |
circle_data <- data.frame( |
690 | ! |
x = cos(seq(0, 2 * pi, length.out = 100)), |
691 | ! |
y = sin(seq(0, 2 * pi, length.out = 100)) |
692 |
)
|
|
693 | ||
694 | ! |
circle_plot <- ggplot2::ggplot(pca_rot) + |
695 | ! |
ggplot2::geom_point(ggplot2::aes_string(x = x_axis, y = y_axis)) + |
696 | ! |
ggplot2::geom_label( |
697 | ! |
ggplot2::aes_string(x = x_axis, y = y_axis, label = "label"), |
698 | ! |
nudge_x = 0.1, nudge_y = 0.05, |
699 | ! |
fontface = "bold" |
700 |
) + |
|
701 | ! |
ggplot2::geom_path(ggplot2::aes(x, y, group = 1), data = circle_data) + |
702 | ! |
ggplot2::geom_point(ggplot2::aes(x = x, y = y), data = data.frame(x = 0, y = 0), shape = "x", size = 5) + |
703 | ! |
labs + |
704 | ! |
ggthemes + |
705 | ! |
themes
|
706 |
},
|
|
707 | ! |
env = list( |
708 | ! |
x_axis = x_axis, |
709 | ! |
y_axis = y_axis, |
710 | ! |
variables = variables, |
711 | ! |
ggthemes = parsed_ggplot2_args$ggtheme, |
712 | ! |
labs = `if`(is.null(parsed_ggplot2_args$labs), quote(labs()), parsed_ggplot2_args$labs), |
713 | ! |
themes = parsed_ggplot2_args$theme |
714 |
)
|
|
715 |
)
|
|
716 |
)
|
|
717 |
}
|
|
718 | ||
719 |
# plot biplot ----
|
|
720 | ! |
plot_biplot <- function(base_q) { |
721 | ! |
qenv <- base_q |
722 | ||
723 | ! |
ANL <- qenv[["ANL"]] |
724 | ||
725 | ! |
resp_col <- as.character(merged$anl_input_r()$columns_source$response) |
726 | ! |
dat_cols <- as.character(merged$anl_input_r()$columns_source$dat) |
727 | ! |
x_axis <- input$x_axis |
728 | ! |
y_axis <- input$y_axis |
729 | ! |
variables <- input$variables |
730 | ! |
pca <- qenv[["pca"]] |
731 | ||
732 | ! |
ggtheme <- input$ggtheme |
733 | ||
734 | ! |
rotate_xaxis_labels <- input$rotate_xaxis_labels |
735 | ! |
alpha <- input$alpha |
736 | ! |
size <- input$size |
737 | ! |
font_size <- input$font_size |
738 | ||
739 | ! |
qenv <- teal.code::eval_code( |
740 | ! |
qenv,
|
741 | ! |
substitute( |
742 | ! |
expr = pca_rot <- dplyr::as_tibble(pca$x[, c(x_axis, y_axis)]), |
743 | ! |
env = list(x_axis = x_axis, y_axis = y_axis) |
744 |
)
|
|
745 |
)
|
|
746 | ||
747 |
# rot_vars = data frame that displays arrows in the plot, need to be scaled to data
|
|
748 | ! |
if (!is.null(input$variables)) { |
749 | ! |
qenv <- teal.code::eval_code( |
750 | ! |
qenv,
|
751 | ! |
substitute( |
752 | ! |
expr = { |
753 | ! |
r <- sqrt(qchisq(0.69, df = 2)) * prod(colMeans(pca_rot ^ 2)) ^ (1 / 4) # styler: off |
754 | ! |
v_scale <- rowSums(pca$rotation ^ 2) # styler: off |
755 | ||
756 | ! |
rot_vars <- pca$rotation[, c(x_axis, y_axis)] %>% |
757 | ! |
dplyr::as_tibble(rownames = "label") %>% |
758 | ! |
dplyr::mutate_at(vars(c(x_axis, y_axis)), function(x) r * x / sqrt(max(v_scale))) |
759 |
},
|
|
760 | ! |
env = list(x_axis = x_axis, y_axis = y_axis) |
761 |
)
|
|
762 |
) %>% |
|
763 | ! |
teal.code::eval_code( |
764 | ! |
if (is.logical(pca$center) && !pca$center) { |
765 | ! |
substitute( |
766 | ! |
expr = { |
767 | ! |
rot_vars <- rot_vars %>% |
768 | ! |
tibble::column_to_rownames("label") %>% |
769 | ! |
sweep(1, apply(ANL[keep_columns], 2, mean, na.rm = TRUE)) %>% |
770 | ! |
tibble::rownames_to_column("label") %>% |
771 | ! |
dplyr::mutate( |
772 | ! |
xstart = mean(pca$x[, x_axis], na.rm = TRUE), |
773 | ! |
ystart = mean(pca$x[, y_axis], na.rm = TRUE) |
774 |
)
|
|
775 |
},
|
|
776 | ! |
env = list(x_axis = x_axis, y_axis = y_axis) |
777 |
)
|
|
778 |
} else { |
|
779 | ! |
quote(rot_vars <- rot_vars %>% dplyr::mutate(xstart = 0, ystart = 0)) |
780 |
}
|
|
781 |
) %>% |
|
782 | ! |
teal.code::eval_code( |
783 | ! |
substitute( |
784 | ! |
expr = rot_vars <- rot_vars %>% dplyr::filter(label %in% variables), |
785 | ! |
env = list(variables = variables) |
786 |
)
|
|
787 |
)
|
|
788 |
}
|
|
789 | ||
790 | ! |
pca_plot_biplot_expr <- list(quote(ggplot())) |
791 | ||
792 | ! |
if (length(resp_col) == 0) { |
793 | ! |
pca_plot_biplot_expr <- c( |
794 | ! |
pca_plot_biplot_expr,
|
795 | ! |
substitute( |
796 | ! |
ggplot2::geom_point(ggplot2::aes_string(x = x_axis, y = y_axis), |
797 | ! |
data = pca_rot, alpha = alpha, size = size |
798 |
),
|
|
799 | ! |
list(x_axis = input$x_axis, y_axis = input$y_axis, alpha = input$alpha, size = input$size) |
800 |
)
|
|
801 |
)
|
|
802 | ! |
dev_labs <- list() |
803 |
} else { |
|
804 | ! |
rp_keys <- setdiff(colnames(ANL), as.character(unlist(merged$anl_input_r()$columns_source))) |
805 | ||
806 | ! |
response <- ANL[[resp_col]] |
807 | ||
808 | ! |
aes_biplot <- substitute( |
809 | ! |
ggplot2::aes_string(x = x_axis, y = y_axis, color = "response"), |
810 | ! |
env = list(x_axis = x_axis, y_axis = y_axis) |
811 |
)
|
|
812 | ||
813 | ! |
qenv <- teal.code::eval_code( |
814 | ! |
qenv,
|
815 | ! |
substitute(response <- ANL[[resp_col]], env = list(resp_col = resp_col)) |
816 |
)
|
|
817 | ||
818 | ! |
dev_labs <- list(color = varname_w_label(resp_col, ANL)) |
819 | ||
820 | ! |
scales_biplot <- |
821 | ! |
if ( |
822 | ! |
is.character(response) || |
823 | ! |
is.factor(response) || |
824 | ! |
(is.numeric(response) && length(unique(response)) <= 6) |
825 |
) { |
|
826 | ! |
qenv <- teal.code::eval_code( |
827 | ! |
qenv,
|
828 | ! |
quote(pca_rot$response <- as.factor(response)) |
829 |
)
|
|
830 | ! |
quote(ggplot2::scale_color_brewer(palette = "Dark2")) |
831 | ! |
} else if (inherits(response, "Date")) { |
832 | ! |
qenv <- teal.code::eval_code( |
833 | ! |
qenv,
|
834 | ! |
quote(pca_rot$response <- numeric(response)) |
835 |
)
|
|
836 | ||
837 | ! |
quote( |
838 | ! |
ggplot2::scale_color_gradient( |
839 | ! |
low = c(getOption("ggplot2.discrete.colour")[2], "darkred")[1], |
840 | ! |
high = c(getOption("ggplot2.discrete.colour"), "lightblue")[1], |
841 | ! |
labels = function(x) as.Date(x, origin = "1970-01-01") |
842 |
)
|
|
843 |
)
|
|
844 |
} else { |
|
845 | ! |
qenv <- teal.code::eval_code( |
846 | ! |
qenv,
|
847 | ! |
quote(pca_rot$response <- response) |
848 |
)
|
|
849 | ! |
quote(ggplot2::scale_color_gradient( |
850 | ! |
low = c(getOption("ggplot2.discrete.colour")[2], "darkred")[1], |
851 | ! |
high = c(getOption("ggplot2.discrete.colour"), "lightblue")[1] |
852 |
)) |
|
853 |
}
|
|
854 | ||
855 | ! |
pca_plot_biplot_expr <- c( |
856 | ! |
pca_plot_biplot_expr,
|
857 | ! |
substitute( |
858 | ! |
ggplot2::geom_point(aes_biplot, data = pca_rot, alpha = alpha, size = size), |
859 | ! |
env = list(aes_biplot = aes_biplot, alpha = alpha, size = size) |
860 |
),
|
|
861 | ! |
scales_biplot
|
862 |
)
|
|
863 |
}
|
|
864 | ||
865 | ! |
if (!is.null(input$variables)) { |
866 | ! |
pca_plot_biplot_expr <- c( |
867 | ! |
pca_plot_biplot_expr,
|
868 | ! |
substitute( |
869 | ! |
ggplot2::geom_segment( |
870 | ! |
ggplot2::aes_string(x = "xstart", y = "ystart", xend = x_axis, yend = y_axis), |
871 | ! |
data = rot_vars, |
872 | ! |
lineend = "round", linejoin = "round", |
873 | ! |
arrow = grid::arrow(length = grid::unit(0.5, "cm")) |
874 |
),
|
|
875 | ! |
env = list(x_axis = x_axis, y_axis = y_axis) |
876 |
),
|
|
877 | ! |
substitute( |
878 | ! |
ggplot2::geom_label( |
879 | ! |
ggplot2::aes_string( |
880 | ! |
x = x_axis, |
881 | ! |
y = y_axis, |
882 | ! |
label = "label" |
883 |
),
|
|
884 | ! |
data = rot_vars, |
885 | ! |
nudge_y = 0.1, |
886 | ! |
fontface = "bold" |
887 |
),
|
|
888 | ! |
env = list(x_axis = x_axis, y_axis = y_axis) |
889 |
),
|
|
890 | ! |
quote(ggplot2::geom_point(ggplot2::aes(x = xstart, y = ystart), data = rot_vars, shape = "x", size = 5)) |
891 |
)
|
|
892 |
}
|
|
893 | ||
894 | ! |
angle <- ifelse(isTRUE(rotate_xaxis_labels), 45, 0) |
895 | ! |
hjust <- ifelse(isTRUE(rotate_xaxis_labels), 1, 0.5) |
896 | ||
897 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
898 | ! |
labs = dev_labs, |
899 | ! |
theme = list( |
900 | ! |
text = substitute(ggplot2::element_text(size = font_size), list(font_size = font_size)), |
901 | ! |
axis.text.x = substitute( |
902 | ! |
ggplot2::element_text(angle = angle_val, hjust = hjust_val), |
903 | ! |
list(angle_val = angle, hjust_val = hjust) |
904 |
)
|
|
905 |
)
|
|
906 |
)
|
|
907 | ||
908 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
909 | ! |
user_plot = ggplot2_args[["Biplot"]], |
910 | ! |
user_default = ggplot2_args$default, |
911 | ! |
module_plot = dev_ggplot2_args |
912 |
)
|
|
913 | ||
914 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
915 | ! |
all_ggplot2_args,
|
916 | ! |
ggtheme = ggtheme |
917 |
)
|
|
918 | ||
919 | ! |
pca_plot_biplot_expr <- c( |
920 | ! |
pca_plot_biplot_expr,
|
921 | ! |
parsed_ggplot2_args
|
922 |
)
|
|
923 | ||
924 | ! |
teal.code::eval_code( |
925 | ! |
qenv,
|
926 | ! |
substitute( |
927 | ! |
expr = { |
928 | ! |
biplot <- plot_call |
929 |
},
|
|
930 | ! |
env = list( |
931 | ! |
plot_call = Reduce(function(x, y) call("+", x, y), pca_plot_biplot_expr) |
932 |
)
|
|
933 |
)
|
|
934 |
)
|
|
935 |
}
|
|
936 | ||
937 |
# plot eigenvector_plot ----
|
|
938 | ! |
plot_eigenvector <- function(base_q) { |
939 | ! |
req(input$pc) |
940 | ! |
pc <- input$pc |
941 | ! |
ggtheme <- input$ggtheme |
942 | ||
943 | ! |
rotate_xaxis_labels <- input$rotate_xaxis_labels |
944 | ! |
font_size <- input$font_size |
945 | ||
946 | ! |
angle <- ifelse(rotate_xaxis_labels, 45, 0) |
947 | ! |
hjust <- ifelse(rotate_xaxis_labels, 1, 0.5) |
948 | ||
949 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
950 | ! |
theme = list( |
951 | ! |
text = substitute(ggplot2::element_text(size = font_size), list(font_size = font_size)), |
952 | ! |
axis.text.x = substitute( |
953 | ! |
ggplot2::element_text(angle = angle_val, hjust = hjust_val), |
954 | ! |
list(angle_val = angle, hjust_val = hjust) |
955 |
)
|
|
956 |
)
|
|
957 |
)
|
|
958 | ||
959 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
960 | ! |
user_plot = ggplot2_args[["Eigenvector plot"]], |
961 | ! |
user_default = ggplot2_args$default, |
962 | ! |
module_plot = dev_ggplot2_args |
963 |
)
|
|
964 | ||
965 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
966 | ! |
all_ggplot2_args,
|
967 | ! |
ggtheme = ggtheme |
968 |
)
|
|
969 | ||
970 | ! |
ggplot_exprs <- c( |
971 | ! |
list( |
972 | ! |
quote(ggplot(pca_rot)), |
973 | ! |
substitute( |
974 | ! |
ggplot2::geom_bar( |
975 | ! |
ggplot2::aes_string(x = "Variable", y = pc), |
976 | ! |
stat = "identity", |
977 | ! |
color = "black", |
978 | ! |
fill = c(getOption("ggplot2.discrete.colour"), "lightblue")[1] |
979 |
),
|
|
980 | ! |
env = list(pc = pc) |
981 |
),
|
|
982 | ! |
substitute( |
983 | ! |
ggplot2::geom_text( |
984 | ! |
ggplot2::aes( |
985 | ! |
x = Variable, |
986 | ! |
y = pc_name, |
987 | ! |
label = round(pc_name, 3), |
988 | ! |
vjust = ifelse(pc_name > 0, -0.5, 1.3) |
989 |
)
|
|
990 |
),
|
|
991 | ! |
env = list(pc_name = as.name(pc)) |
992 |
)
|
|
993 |
),
|
|
994 | ! |
parsed_ggplot2_args$labs, |
995 | ! |
parsed_ggplot2_args$ggtheme, |
996 | ! |
parsed_ggplot2_args$theme |
997 |
)
|
|
998 | ||
999 | ! |
teal.code::eval_code( |
1000 | ! |
base_q,
|
1001 | ! |
substitute( |
1002 | ! |
expr = { |
1003 | ! |
pca_rot <- pca$rotation[, pc, drop = FALSE] %>% |
1004 | ! |
dplyr::as_tibble(rownames = "Variable") |
1005 | ! |
eigenvector_plot <- plot_call |
1006 |
},
|
|
1007 | ! |
env = list( |
1008 | ! |
pc = pc, |
1009 | ! |
plot_call = Reduce(function(x, y) call("+", x, y), ggplot_exprs) |
1010 |
)
|
|
1011 |
)
|
|
1012 |
)
|
|
1013 |
}
|
|
1014 | ||
1015 |
# qenvs ---
|
|
1016 | ! |
output_q <- lapply( |
1017 | ! |
list( |
1018 | ! |
elbow_plot = plot_elbow, |
1019 | ! |
circle_plot = plot_circle, |
1020 | ! |
biplot = plot_biplot, |
1021 | ! |
eigenvector_plot = plot_eigenvector |
1022 |
),
|
|
1023 | ! |
function(fun) { |
1024 | ! |
reactive({ |
1025 | ! |
req(computation()) |
1026 | ! |
teal::validate_inputs(iv_r()) |
1027 | ! |
teal::validate_inputs(iv_extra, header = "Plot settings are required") |
1028 | ! |
fun(computation()) |
1029 |
}) |
|
1030 |
}
|
|
1031 |
)
|
|
1032 | ||
1033 | ! |
decorated_q <- mapply( |
1034 | ! |
function(obj_name, q) { |
1035 | ! |
srv_decorate_teal_data( |
1036 | ! |
id = sprintf("d_%s", obj_name), |
1037 | ! |
data = q, |
1038 | ! |
decorators = select_decorators(decorators, obj_name), |
1039 | ! |
expr = reactive({ |
1040 | ! |
substitute(print(.plot), env = list(.plot = as.name(obj_name))) |
1041 |
}), |
|
1042 | ! |
expr_is_reactive = TRUE |
1043 |
)
|
|
1044 |
},
|
|
1045 | ! |
names(output_q), |
1046 | ! |
output_q
|
1047 |
)
|
|
1048 | ||
1049 |
# plot final ----
|
|
1050 | ! |
decorated_output_q <- reactive({ |
1051 | ! |
switch(req(input$plot_type), |
1052 | ! |
"Elbow plot" = decorated_q$elbow_plot(), |
1053 | ! |
"Circle plot" = decorated_q$circle_plot(), |
1054 | ! |
"Biplot" = decorated_q$biplot(), |
1055 | ! |
"Eigenvector plot" = decorated_q$eigenvector_plot(), |
1056 | ! |
stop("Unknown plot") |
1057 |
)
|
|
1058 |
}) |
|
1059 | ||
1060 | ! |
plot_r <- reactive({ |
1061 | ! |
plot_name <- gsub(" ", "_", tolower(req(input$plot_type))) |
1062 | ! |
req(decorated_output_q())[[plot_name]] |
1063 |
}) |
|
1064 | ||
1065 | ! |
pws <- teal.widgets::plot_with_settings_srv( |
1066 | ! |
id = "pca_plot", |
1067 | ! |
plot_r = plot_r, |
1068 | ! |
height = plot_height, |
1069 | ! |
width = plot_width, |
1070 | ! |
graph_align = "center" |
1071 |
)
|
|
1072 | ||
1073 |
# tables ----
|
|
1074 | ! |
output$tbl_importance <- renderTable( |
1075 | ! |
expr = { |
1076 | ! |
req("importance" %in% input$tables_display, computation()) |
1077 | ! |
computation()[["tbl_importance"]] |
1078 |
},
|
|
1079 | ! |
bordered = TRUE, |
1080 | ! |
align = "c", |
1081 | ! |
digits = 3 |
1082 |
)
|
|
1083 | ||
1084 | ! |
output$tbl_importance_ui <- renderUI({ |
1085 | ! |
req("importance" %in% input$tables_display) |
1086 | ! |
tags$div( |
1087 | ! |
align = "center", |
1088 | ! |
tags$h4("Principal components importance"), |
1089 | ! |
tableOutput(session$ns("tbl_importance")), |
1090 | ! |
tags$hr() |
1091 |
)
|
|
1092 |
}) |
|
1093 | ||
1094 | ! |
output$tbl_eigenvector <- renderTable( |
1095 | ! |
expr = { |
1096 | ! |
req("eigenvector" %in% input$tables_display, req(computation())) |
1097 | ! |
computation()[["tbl_eigenvector"]] |
1098 |
},
|
|
1099 | ! |
bordered = TRUE, |
1100 | ! |
align = "c", |
1101 | ! |
digits = 3 |
1102 |
)
|
|
1103 | ||
1104 | ! |
output$tbl_eigenvector_ui <- renderUI({ |
1105 | ! |
req("eigenvector" %in% input$tables_display) |
1106 | ! |
tags$div( |
1107 | ! |
align = "center", |
1108 | ! |
tags$h4("Eigenvectors"), |
1109 | ! |
tableOutput(session$ns("tbl_eigenvector")), |
1110 | ! |
tags$hr() |
1111 |
)
|
|
1112 |
}) |
|
1113 | ||
1114 | ! |
output$all_plots <- renderUI({ |
1115 | ! |
teal::validate_inputs(iv_r()) |
1116 | ! |
teal::validate_inputs(iv_extra, header = "Plot settings are required") |
1117 | ||
1118 | ! |
validation() |
1119 | ! |
tags$div( |
1120 | ! |
class = "overflow-scroll", |
1121 | ! |
uiOutput(session$ns("tbl_importance_ui")), |
1122 | ! |
uiOutput(session$ns("tbl_eigenvector_ui")), |
1123 | ! |
teal.widgets::plot_with_settings_ui(id = session$ns("pca_plot")) |
1124 |
)
|
|
1125 |
}) |
|
1126 | ||
1127 |
# Render R code.
|
|
1128 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_output_q()))) |
1129 | ||
1130 | ! |
teal.widgets::verbatim_popup_srv( |
1131 | ! |
id = "rcode", |
1132 | ! |
verbatim_content = source_code_r, |
1133 | ! |
title = "R Code for PCA" |
1134 |
)
|
|
1135 | ||
1136 |
### REPORTER
|
|
1137 | ! |
if (with_reporter) { |
1138 | ! |
card_fun <- function(comment, label) { |
1139 | ! |
card <- teal::report_card_template( |
1140 | ! |
title = "Principal Component Analysis Plot", |
1141 | ! |
label = label, |
1142 | ! |
with_filter = with_filter, |
1143 | ! |
filter_panel_api = filter_panel_api |
1144 |
)
|
|
1145 | ! |
card$append_text("Principal Components Table", "header3") |
1146 | ! |
card$append_table(computation()[["tbl_importance"]]) |
1147 | ! |
card$append_text("Eigenvectors Table", "header3") |
1148 | ! |
card$append_table(computation()[["tbl_eigenvector"]]) |
1149 | ! |
card$append_text("Plot", "header3") |
1150 | ! |
card$append_plot(plot_r(), dim = pws$dim()) |
1151 | ! |
if (!comment == "") { |
1152 | ! |
card$append_text("Comment", "header3") |
1153 | ! |
card$append_text(comment) |
1154 |
}
|
|
1155 | ! |
card$append_src(source_code_r()) |
1156 | ! |
card
|
1157 |
}
|
|
1158 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
1159 |
}
|
|
1160 |
###
|
|
1161 |
}) |
|
1162 |
}
|
1 |
#' `teal` module: Univariate and bivariate visualizations
|
|
2 |
#'
|
|
3 |
#' Module enables the creation of univariate and bivariate plots,
|
|
4 |
#' facilitating the exploration of data distributions and relationships between two variables.
|
|
5 |
#'
|
|
6 |
#' This is a general module to visualize 1 & 2 dimensional data.
|
|
7 |
#'
|
|
8 |
#' @note
|
|
9 |
#' For more examples, please see the vignette "Using bivariate plot" via
|
|
10 |
#' `vignette("using-bivariate-plot", package = "teal.modules.general")`.
|
|
11 |
#'
|
|
12 |
#' @inheritParams teal::module
|
|
13 |
#' @inheritParams shared_params
|
|
14 |
#' @param x (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
15 |
#' Variable names selected to plot along the x-axis by default.
|
|
16 |
#' Can be numeric, factor or character.
|
|
17 |
#' No empty selections are allowed.
|
|
18 |
#' @param y (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
19 |
#' Variable names selected to plot along the y-axis by default.
|
|
20 |
#' Can be numeric, factor or character.
|
|
21 |
#' @param use_density (`logical`) optional, indicates whether to plot density (`TRUE`) or frequency (`FALSE`).
|
|
22 |
#' Defaults to frequency (`FALSE`).
|
|
23 |
#' @param row_facet (`data_extract_spec` or `list` of multiple `data_extract_spec`) optional,
|
|
24 |
#' specification of the data variable(s) to use for faceting rows.
|
|
25 |
#' @param col_facet (`data_extract_spec` or `list` of multiple `data_extract_spec`) optional,
|
|
26 |
#' specification of the data variable(s) to use for faceting columns.
|
|
27 |
#' @param facet (`logical`) optional, specifies whether the facet encodings `ui` elements are toggled
|
|
28 |
#' on and shown to the user by default. Defaults to `TRUE` if either `row_facet` or `column_facet`
|
|
29 |
#' are supplied.
|
|
30 |
#' @param color_settings (`logical`) Whether coloring, filling and size should be applied
|
|
31 |
#' and `UI` tool offered to the user.
|
|
32 |
#' @param color (`data_extract_spec` or `list` of multiple `data_extract_spec`) optional,
|
|
33 |
#' specification of the data variable(s) selected for the outline color inside the coloring settings.
|
|
34 |
#' It will be applied when `color_settings` is set to `TRUE`.
|
|
35 |
#' @param fill (`data_extract_spec` or `list` of multiple `data_extract_spec`) optional,
|
|
36 |
#' specification of the data variable(s) selected for the fill color inside the coloring settings.
|
|
37 |
#' It will be applied when `color_settings` is set to `TRUE`.
|
|
38 |
#' @param size (`data_extract_spec` or `list` of multiple `data_extract_spec`) optional,
|
|
39 |
#' specification of the data variable(s) selected for the size of `geom_point` plots inside the coloring settings.
|
|
40 |
#' It will be applied when `color_settings` is set to `TRUE`.
|
|
41 |
#' @param free_x_scales (`logical`) optional, whether X scaling shall be changeable.
|
|
42 |
#' Does not allow scaling to be changed by default (`FALSE`).
|
|
43 |
#' @param free_y_scales (`logical`) optional, whether Y scaling shall be changeable.
|
|
44 |
#' Does not allow scaling to be changed by default (`FALSE`).
|
|
45 |
#' @param swap_axes (`logical`) optional, whether to swap X and Y axes. Defaults to `FALSE`.
|
|
46 |
#'
|
|
47 |
#' @inherit shared_params return
|
|
48 |
#'
|
|
49 |
#' @section Decorating Module:
|
|
50 |
#'
|
|
51 |
#' This module generates the following objects, which can be modified in place using decorators:
|
|
52 |
#' - `plot` (`ggplot`)
|
|
53 |
#'
|
|
54 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
55 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
56 |
#' See code snippet below:
|
|
57 |
#'
|
|
58 |
#' ```
|
|
59 |
#' tm_g_bivariate(
|
|
60 |
#' ..., # arguments for module
|
|
61 |
#' decorators = list(
|
|
62 |
#' plot = teal_transform_module(...) # applied to the `plot` output
|
|
63 |
#' )
|
|
64 |
#' )
|
|
65 |
#' ```
|
|
66 |
#'
|
|
67 |
#' For additional details and examples of decorators, refer to the vignette
|
|
68 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
69 |
#'
|
|
70 |
#' To learn more please refer to the vignette
|
|
71 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
72 |
#'
|
|
73 |
#'
|
|
74 |
#' @examplesShinylive
|
|
75 |
#' library(teal.modules.general)
|
|
76 |
#' interactive <- function() TRUE
|
|
77 |
#' {{ next_example }}
|
|
78 |
#' @examples
|
|
79 |
#' # general data example
|
|
80 |
#' data <- teal_data()
|
|
81 |
#' data <- within(data, {
|
|
82 |
#' require(nestcolor)
|
|
83 |
#' CO2 <- data.frame(CO2)
|
|
84 |
#' })
|
|
85 |
#'
|
|
86 |
#' app <- init(
|
|
87 |
#' data = data,
|
|
88 |
#' modules = tm_g_bivariate(
|
|
89 |
#' x = data_extract_spec(
|
|
90 |
#' dataname = "CO2",
|
|
91 |
#' select = select_spec(
|
|
92 |
#' label = "Select variable:",
|
|
93 |
#' choices = variable_choices(data[["CO2"]]),
|
|
94 |
#' selected = "conc",
|
|
95 |
#' fixed = FALSE
|
|
96 |
#' )
|
|
97 |
#' ),
|
|
98 |
#' y = data_extract_spec(
|
|
99 |
#' dataname = "CO2",
|
|
100 |
#' select = select_spec(
|
|
101 |
#' label = "Select variable:",
|
|
102 |
#' choices = variable_choices(data[["CO2"]]),
|
|
103 |
#' selected = "uptake",
|
|
104 |
#' multiple = FALSE,
|
|
105 |
#' fixed = FALSE
|
|
106 |
#' )
|
|
107 |
#' ),
|
|
108 |
#' row_facet = data_extract_spec(
|
|
109 |
#' dataname = "CO2",
|
|
110 |
#' select = select_spec(
|
|
111 |
#' label = "Select variable:",
|
|
112 |
#' choices = variable_choices(data[["CO2"]]),
|
|
113 |
#' selected = "Type",
|
|
114 |
#' fixed = FALSE
|
|
115 |
#' )
|
|
116 |
#' ),
|
|
117 |
#' col_facet = data_extract_spec(
|
|
118 |
#' dataname = "CO2",
|
|
119 |
#' select = select_spec(
|
|
120 |
#' label = "Select variable:",
|
|
121 |
#' choices = variable_choices(data[["CO2"]]),
|
|
122 |
#' selected = "Treatment",
|
|
123 |
#' fixed = FALSE
|
|
124 |
#' )
|
|
125 |
#' )
|
|
126 |
#' )
|
|
127 |
#' )
|
|
128 |
#' if (interactive()) {
|
|
129 |
#' shinyApp(app$ui, app$server)
|
|
130 |
#' }
|
|
131 |
#'
|
|
132 |
#' @examplesShinylive
|
|
133 |
#' library(teal.modules.general)
|
|
134 |
#' interactive <- function() TRUE
|
|
135 |
#' {{ next_example }}
|
|
136 |
#' @examples
|
|
137 |
#' # CDISC data example
|
|
138 |
#' data <- teal_data()
|
|
139 |
#' data <- within(data, {
|
|
140 |
#' require(nestcolor)
|
|
141 |
#' ADSL <- teal.data::rADSL
|
|
142 |
#' })
|
|
143 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
144 |
#'
|
|
145 |
#' app <- init(
|
|
146 |
#' data = data,
|
|
147 |
#' modules = tm_g_bivariate(
|
|
148 |
#' x = data_extract_spec(
|
|
149 |
#' dataname = "ADSL",
|
|
150 |
#' select = select_spec(
|
|
151 |
#' label = "Select variable:",
|
|
152 |
#' choices = variable_choices(data[["ADSL"]]),
|
|
153 |
#' selected = "AGE",
|
|
154 |
#' fixed = FALSE
|
|
155 |
#' )
|
|
156 |
#' ),
|
|
157 |
#' y = data_extract_spec(
|
|
158 |
#' dataname = "ADSL",
|
|
159 |
#' select = select_spec(
|
|
160 |
#' label = "Select variable:",
|
|
161 |
#' choices = variable_choices(data[["ADSL"]]),
|
|
162 |
#' selected = "SEX",
|
|
163 |
#' multiple = FALSE,
|
|
164 |
#' fixed = FALSE
|
|
165 |
#' )
|
|
166 |
#' ),
|
|
167 |
#' row_facet = data_extract_spec(
|
|
168 |
#' dataname = "ADSL",
|
|
169 |
#' select = select_spec(
|
|
170 |
#' label = "Select variable:",
|
|
171 |
#' choices = variable_choices(data[["ADSL"]]),
|
|
172 |
#' selected = "ARM",
|
|
173 |
#' fixed = FALSE
|
|
174 |
#' )
|
|
175 |
#' ),
|
|
176 |
#' col_facet = data_extract_spec(
|
|
177 |
#' dataname = "ADSL",
|
|
178 |
#' select = select_spec(
|
|
179 |
#' label = "Select variable:",
|
|
180 |
#' choices = variable_choices(data[["ADSL"]]),
|
|
181 |
#' selected = "COUNTRY",
|
|
182 |
#' fixed = FALSE
|
|
183 |
#' )
|
|
184 |
#' )
|
|
185 |
#' )
|
|
186 |
#' )
|
|
187 |
#' if (interactive()) {
|
|
188 |
#' shinyApp(app$ui, app$server)
|
|
189 |
#' }
|
|
190 |
#'
|
|
191 |
#' @export
|
|
192 |
#'
|
|
193 |
tm_g_bivariate <- function(label = "Bivariate Plots", |
|
194 |
x,
|
|
195 |
y,
|
|
196 |
row_facet = NULL, |
|
197 |
col_facet = NULL, |
|
198 |
facet = !is.null(row_facet) || !is.null(col_facet), |
|
199 |
color = NULL, |
|
200 |
fill = NULL, |
|
201 |
size = NULL, |
|
202 |
use_density = FALSE, |
|
203 |
color_settings = FALSE, |
|
204 |
free_x_scales = FALSE, |
|
205 |
free_y_scales = FALSE, |
|
206 |
plot_height = c(600, 200, 2000), |
|
207 |
plot_width = NULL, |
|
208 |
rotate_xaxis_labels = FALSE, |
|
209 |
swap_axes = FALSE, |
|
210 |
ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), |
|
211 |
ggplot2_args = teal.widgets::ggplot2_args(), |
|
212 |
pre_output = NULL, |
|
213 |
post_output = NULL, |
|
214 |
transformators = list(), |
|
215 |
decorators = list()) { |
|
216 | 18x |
message("Initializing tm_g_bivariate") |
217 | ||
218 |
# Normalize the parameters
|
|
219 | 14x |
if (inherits(x, "data_extract_spec")) x <- list(x) |
220 | 13x |
if (inherits(y, "data_extract_spec")) y <- list(y) |
221 | 1x |
if (inherits(row_facet, "data_extract_spec")) row_facet <- list(row_facet) |
222 | 1x |
if (inherits(col_facet, "data_extract_spec")) col_facet <- list(col_facet) |
223 | 1x |
if (inherits(color, "data_extract_spec")) color <- list(color) |
224 | 1x |
if (inherits(fill, "data_extract_spec")) fill <- list(fill) |
225 | 1x |
if (inherits(size, "data_extract_spec")) size <- list(size) |
226 | ||
227 |
# Start of assertions
|
|
228 | 18x |
checkmate::assert_string(label) |
229 | ||
230 | 18x |
checkmate::assert_list(x, types = "data_extract_spec") |
231 | 18x |
assert_single_selection(x) |
232 | ||
233 | 16x |
checkmate::assert_list(y, types = "data_extract_spec") |
234 | 16x |
assert_single_selection(y) |
235 | ||
236 | 14x |
checkmate::assert_list(row_facet, types = "data_extract_spec", null.ok = TRUE) |
237 | 14x |
assert_single_selection(row_facet) |
238 | ||
239 | 14x |
checkmate::assert_list(col_facet, types = "data_extract_spec", null.ok = TRUE) |
240 | 14x |
assert_single_selection(col_facet) |
241 | ||
242 | 14x |
checkmate::assert_flag(facet) |
243 | ||
244 | 14x |
checkmate::assert_list(color, types = "data_extract_spec", null.ok = TRUE) |
245 | 14x |
assert_single_selection(color) |
246 | ||
247 | 14x |
checkmate::assert_list(fill, types = "data_extract_spec", null.ok = TRUE) |
248 | 14x |
assert_single_selection(fill) |
249 | ||
250 | 14x |
checkmate::assert_list(size, types = "data_extract_spec", null.ok = TRUE) |
251 | 14x |
assert_single_selection(size) |
252 | ||
253 | 14x |
checkmate::assert_flag(use_density) |
254 | ||
255 |
# Determines color, fill & size if they are not explicitly set
|
|
256 | 14x |
checkmate::assert_flag(color_settings) |
257 | 14x |
if (color_settings) { |
258 | 2x |
if (is.null(color)) { |
259 | 2x |
color <- x |
260 | 2x |
color[[1]]$select <- teal.transform::select_spec(choices = color[[1]]$select$choices, selected = NULL) |
261 |
}
|
|
262 | 2x |
if (is.null(fill)) { |
263 | 2x |
fill <- x |
264 | 2x |
fill[[1]]$select <- teal.transform::select_spec(choices = fill[[1]]$select$choices, selected = NULL) |
265 |
}
|
|
266 | 2x |
if (is.null(size)) { |
267 | 2x |
size <- x |
268 | 2x |
size[[1]]$select <- teal.transform::select_spec(choices = size[[1]]$select$choices, selected = NULL) |
269 |
}
|
|
270 |
} else { |
|
271 | 12x |
if (!is.null(c(color, fill, size))) { |
272 | 3x |
stop("'color_settings' argument needs to be set to TRUE if 'color', 'fill', and/or 'size' is/are supplied.") |
273 |
}
|
|
274 |
}
|
|
275 | ||
276 | 11x |
checkmate::assert_flag(free_x_scales) |
277 | 11x |
checkmate::assert_flag(free_y_scales) |
278 | ||
279 | 11x |
checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
280 | 10x |
checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
281 | 8x |
checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
282 | 7x |
checkmate::assert_numeric( |
283 | 7x |
plot_width[1], |
284 | 7x |
lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width" |
285 |
)
|
|
286 | ||
287 | 5x |
checkmate::assert_flag(rotate_xaxis_labels) |
288 | 5x |
checkmate::assert_flag(swap_axes) |
289 | ||
290 | 5x |
ggtheme <- match.arg(ggtheme) |
291 | 5x |
checkmate::assert_class(ggplot2_args, "ggplot2_args") |
292 | ||
293 | 5x |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
294 | 5x |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
295 | ||
296 | 5x |
assert_decorators(decorators, "plot") |
297 |
# End of assertions
|
|
298 | ||
299 |
# Make UI args
|
|
300 | 5x |
args <- as.list(environment()) |
301 | ||
302 | 5x |
data_extract_list <- list( |
303 | 5x |
x = x, |
304 | 5x |
y = y, |
305 | 5x |
row_facet = row_facet, |
306 | 5x |
col_facet = col_facet, |
307 | 5x |
color_settings = color_settings, |
308 | 5x |
color = color, |
309 | 5x |
fill = fill, |
310 | 5x |
size = size |
311 |
)
|
|
312 | ||
313 | 5x |
ans <- module( |
314 | 5x |
label = label, |
315 | 5x |
server = srv_g_bivariate, |
316 | 5x |
ui = ui_g_bivariate, |
317 | 5x |
ui_args = args, |
318 | 5x |
server_args = c( |
319 | 5x |
data_extract_list,
|
320 | 5x |
list(plot_height = plot_height, plot_width = plot_width, ggplot2_args = ggplot2_args, decorators = decorators) |
321 |
),
|
|
322 | 5x |
transformators = transformators, |
323 | 5x |
datanames = teal.transform::get_extract_datanames(data_extract_list) |
324 |
)
|
|
325 | 5x |
attr(ans, "teal_bookmarkable") <- TRUE |
326 | 5x |
ans
|
327 |
}
|
|
328 | ||
329 |
# UI function for the bivariate module
|
|
330 |
ui_g_bivariate <- function(id, ...) { |
|
331 | ! |
args <- list(...) |
332 | ! |
is_single_dataset_value <- teal.transform::is_single_dataset( |
333 | ! |
args$x, args$y, args$row_facet, args$col_facet, args$color, args$fill, args$size |
334 |
)
|
|
335 | ||
336 | ! |
ns <- NS(id) |
337 | ! |
teal.widgets::standard_layout( |
338 | ! |
output = teal.widgets::white_small_well( |
339 | ! |
tags$div(teal.widgets::plot_with_settings_ui(id = ns("myplot"))) |
340 |
),
|
|
341 | ! |
encoding = tags$div( |
342 |
### Reporter
|
|
343 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
344 |
###
|
|
345 | ! |
tags$label("Encodings", class = "text-primary"), |
346 | ! |
teal.transform::datanames_input(args[c("x", "y", "row_facet", "col_facet", "color", "fill", "size")]), |
347 | ! |
teal.transform::data_extract_ui( |
348 | ! |
id = ns("x"), |
349 | ! |
label = "X variable", |
350 | ! |
data_extract_spec = args$x, |
351 | ! |
is_single_dataset = is_single_dataset_value |
352 |
),
|
|
353 | ! |
teal.transform::data_extract_ui( |
354 | ! |
id = ns("y"), |
355 | ! |
label = "Y variable", |
356 | ! |
data_extract_spec = args$y, |
357 | ! |
is_single_dataset = is_single_dataset_value |
358 |
),
|
|
359 | ! |
conditionalPanel( |
360 | ! |
condition = |
361 | ! |
"$(\"button[data-id*='-x-dataset'][data-id$='-select']\").text() == '- Nothing selected - ' || |
362 | ! |
$(\"button[data-id*='-y-dataset'][data-id$='-select']\").text() == '- Nothing selected - ' ", |
363 | ! |
shinyWidgets::radioGroupButtons( |
364 | ! |
inputId = ns("use_density"), |
365 | ! |
label = NULL, |
366 | ! |
choices = c("frequency", "density"), |
367 | ! |
selected = ifelse(args$use_density, "density", "frequency"), |
368 | ! |
justified = TRUE |
369 |
)
|
|
370 |
),
|
|
371 | ! |
ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")), |
372 | ! |
if (!is.null(args$row_facet) || !is.null(args$col_facet)) { |
373 | ! |
tags$div( |
374 | ! |
class = "data-extract-box", |
375 | ! |
tags$label("Facetting"), |
376 | ! |
shinyWidgets::switchInput(inputId = ns("facetting"), value = args$facet, size = "mini"), |
377 | ! |
conditionalPanel( |
378 | ! |
condition = paste0("input['", ns("facetting"), "']"), |
379 | ! |
tags$div( |
380 | ! |
if (!is.null(args$row_facet)) { |
381 | ! |
teal.transform::data_extract_ui( |
382 | ! |
id = ns("row_facet"), |
383 | ! |
label = "Row facetting variable", |
384 | ! |
data_extract_spec = args$row_facet, |
385 | ! |
is_single_dataset = is_single_dataset_value |
386 |
)
|
|
387 |
},
|
|
388 | ! |
if (!is.null(args$col_facet)) { |
389 | ! |
teal.transform::data_extract_ui( |
390 | ! |
id = ns("col_facet"), |
391 | ! |
label = "Column facetting variable", |
392 | ! |
data_extract_spec = args$col_facet, |
393 | ! |
is_single_dataset = is_single_dataset_value |
394 |
)
|
|
395 |
},
|
|
396 | ! |
checkboxInput(ns("free_x_scales"), "free x scales", value = args$free_x_scales), |
397 | ! |
checkboxInput(ns("free_y_scales"), "free y scales", value = args$free_y_scales) |
398 |
)
|
|
399 |
)
|
|
400 |
)
|
|
401 |
},
|
|
402 | ! |
if (args$color_settings) { |
403 |
# Put a grey border around the coloring settings
|
|
404 | ! |
tags$div( |
405 | ! |
class = "data-extract-box", |
406 | ! |
tags$label("Color settings"), |
407 | ! |
shinyWidgets::switchInput(inputId = ns("coloring"), value = TRUE, size = "mini"), |
408 | ! |
conditionalPanel( |
409 | ! |
condition = paste0("input['", ns("coloring"), "']"), |
410 | ! |
tags$div( |
411 | ! |
teal.transform::data_extract_ui( |
412 | ! |
id = ns("color"), |
413 | ! |
label = "Outline color by variable", |
414 | ! |
data_extract_spec = args$color, |
415 | ! |
is_single_dataset = is_single_dataset_value |
416 |
),
|
|
417 | ! |
teal.transform::data_extract_ui( |
418 | ! |
id = ns("fill"), |
419 | ! |
label = "Fill color by variable", |
420 | ! |
data_extract_spec = args$fill, |
421 | ! |
is_single_dataset = is_single_dataset_value |
422 |
),
|
|
423 | ! |
tags$div( |
424 | ! |
id = ns("size_settings"), |
425 | ! |
teal.transform::data_extract_ui( |
426 | ! |
id = ns("size"), |
427 | ! |
label = "Size of points by variable (only if x and y are numeric)", |
428 | ! |
data_extract_spec = args$size, |
429 | ! |
is_single_dataset = is_single_dataset_value |
430 |
)
|
|
431 |
)
|
|
432 |
)
|
|
433 |
)
|
|
434 |
)
|
|
435 |
},
|
|
436 | ! |
teal.widgets::panel_group( |
437 | ! |
teal.widgets::panel_item( |
438 | ! |
title = "Plot settings", |
439 | ! |
checkboxInput(ns("rotate_xaxis_labels"), "Rotate X axis labels", value = args$rotate_xaxis_labels), |
440 | ! |
checkboxInput(ns("swap_axes"), "Swap axes", value = args$swap_axes), |
441 | ! |
selectInput( |
442 | ! |
inputId = ns("ggtheme"), |
443 | ! |
label = "Theme (by ggplot):", |
444 | ! |
choices = ggplot_themes, |
445 | ! |
selected = args$ggtheme, |
446 | ! |
multiple = FALSE |
447 |
),
|
|
448 | ! |
sliderInput( |
449 | ! |
ns("alpha"), "Opacity Scatterplot:", |
450 | ! |
min = 0, max = 1, |
451 | ! |
step = .05, value = .5, ticks = FALSE |
452 |
),
|
|
453 | ! |
sliderInput( |
454 | ! |
ns("fixed_size"), "Scatterplot point size:", |
455 | ! |
min = 1, max = 8, |
456 | ! |
step = 1, value = 2, ticks = FALSE |
457 |
),
|
|
458 | ! |
checkboxInput(ns("add_lines"), "Add lines"), |
459 |
)
|
|
460 |
)
|
|
461 |
),
|
|
462 | ! |
forms = tagList( |
463 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
464 |
),
|
|
465 | ! |
pre_output = args$pre_output, |
466 | ! |
post_output = args$post_output |
467 |
)
|
|
468 |
}
|
|
469 | ||
470 |
# Server function for the bivariate module
|
|
471 |
srv_g_bivariate <- function(id, |
|
472 |
data,
|
|
473 |
reporter,
|
|
474 |
filter_panel_api,
|
|
475 |
x,
|
|
476 |
y,
|
|
477 |
row_facet,
|
|
478 |
col_facet,
|
|
479 |
color_settings = FALSE, |
|
480 |
color,
|
|
481 |
fill,
|
|
482 |
size,
|
|
483 |
plot_height,
|
|
484 |
plot_width,
|
|
485 |
ggplot2_args,
|
|
486 |
decorators) { |
|
487 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
488 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
489 | ! |
checkmate::assert_class(data, "reactive") |
490 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
491 | ! |
moduleServer(id, function(input, output, session) { |
492 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
493 | ||
494 | ! |
ns <- session$ns |
495 | ||
496 | ! |
data_extract <- list( |
497 | ! |
x = x, y = y, row_facet = row_facet, col_facet = col_facet, |
498 | ! |
color = color, fill = fill, size = size |
499 |
)
|
|
500 | ||
501 | ! |
rule_var <- function(other) { |
502 | ! |
function(value) { |
503 | ! |
othervalue <- selector_list()[[other]]()$select |
504 | ! |
if (length(value) == 0L && length(othervalue) == 0L) { |
505 | ! |
"Please select at least one of x-variable or y-variable"
|
506 |
}
|
|
507 |
}
|
|
508 |
}
|
|
509 | ! |
rule_diff <- function(other) { |
510 | ! |
function(value) { |
511 | ! |
othervalue <- selector_list()[[other]]()[["select"]] |
512 | ! |
if (!is.null(othervalue)) { |
513 | ! |
if (identical(value, othervalue)) { |
514 | ! |
"Row and column facetting variables must be different."
|
515 |
}
|
|
516 |
}
|
|
517 |
}
|
|
518 |
}
|
|
519 | ||
520 | ! |
selector_list <- teal.transform::data_extract_multiple_srv( |
521 | ! |
data_extract = data_extract, |
522 | ! |
datasets = data, |
523 | ! |
select_validation_rule = list( |
524 | ! |
x = rule_var("y"), |
525 | ! |
y = rule_var("x"), |
526 | ! |
row_facet = shinyvalidate::compose_rules( |
527 | ! |
shinyvalidate::sv_optional(), |
528 | ! |
rule_diff("col_facet") |
529 |
),
|
|
530 | ! |
col_facet = shinyvalidate::compose_rules( |
531 | ! |
shinyvalidate::sv_optional(), |
532 | ! |
rule_diff("row_facet") |
533 |
)
|
|
534 |
)
|
|
535 |
)
|
|
536 | ||
537 | ! |
iv_r <- reactive({ |
538 | ! |
iv_facet <- shinyvalidate::InputValidator$new() |
539 | ! |
iv_child <- teal.transform::compose_and_enable_validators(iv_facet, selector_list, |
540 | ! |
validator_names = c("row_facet", "col_facet") |
541 |
)
|
|
542 | ! |
iv_child$condition(~ isTRUE(input$facetting)) |
543 | ||
544 | ! |
iv <- shinyvalidate::InputValidator$new() |
545 | ! |
iv$add_validator(iv_child) |
546 | ! |
teal.transform::compose_and_enable_validators(iv, selector_list, validator_names = c("x", "y")) |
547 |
}) |
|
548 | ||
549 | ! |
anl_merged_input <- teal.transform::merge_expression_srv( |
550 | ! |
selector_list = selector_list, |
551 | ! |
datasets = data |
552 |
)
|
|
553 | ! |
qenv <- teal.code::eval_code( |
554 | ! |
data(), |
555 | ! |
'library("ggplot2");library("dplyr");library("teal.modules.general")' # nolint quotes |
556 |
)
|
|
557 | ||
558 | ! |
anl_merged_q <- reactive({ |
559 | ! |
req(anl_merged_input()) |
560 | ! |
qenv %>% |
561 | ! |
teal.code::eval_code(as.expression(anl_merged_input()$expr)) |
562 |
}) |
|
563 | ||
564 | ! |
merged <- list( |
565 | ! |
anl_input_r = anl_merged_input, |
566 | ! |
anl_q_r = anl_merged_q |
567 |
)
|
|
568 | ||
569 | ! |
output_q <- reactive({ |
570 | ! |
teal::validate_inputs(iv_r()) |
571 | ||
572 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
573 | ! |
teal::validate_has_data(ANL, 3) |
574 | ||
575 | ! |
x_col_vec <- as.vector(merged$anl_input_r()$columns_source$x) |
576 | ! |
x_name <- `if`(is.null(x_col_vec), character(0), x_col_vec) |
577 | ! |
y_col_vec <- as.vector(merged$anl_input_r()$columns_source$y) |
578 | ! |
y_name <- `if`(is.null(y_col_vec), character(0), y_col_vec) |
579 | ||
580 | ! |
row_facet_name <- as.vector(merged$anl_input_r()$columns_source$row_facet) |
581 | ! |
col_facet_name <- as.vector(merged$anl_input_r()$columns_source$col_facet) |
582 | ! |
color_name <- if ("color" %in% names(merged$anl_input_r()$columns_source)) { |
583 | ! |
as.vector(merged$anl_input_r()$columns_source$color) |
584 |
} else { |
|
585 | ! |
character(0) |
586 |
}
|
|
587 | ! |
fill_name <- if ("fill" %in% names(merged$anl_input_r()$columns_source)) { |
588 | ! |
as.vector(merged$anl_input_r()$columns_source$fill) |
589 |
} else { |
|
590 | ! |
character(0) |
591 |
}
|
|
592 | ! |
size_name <- if ("size" %in% names(merged$anl_input_r()$columns_source)) { |
593 | ! |
as.vector(merged$anl_input_r()$columns_source$size) |
594 |
} else { |
|
595 | ! |
character(0) |
596 |
}
|
|
597 | ||
598 | ! |
use_density <- input$use_density == "density" |
599 | ! |
free_x_scales <- input$free_x_scales |
600 | ! |
free_y_scales <- input$free_y_scales |
601 | ! |
ggtheme <- input$ggtheme |
602 | ! |
rotate_xaxis_labels <- input$rotate_xaxis_labels |
603 | ! |
swap_axes <- input$swap_axes |
604 | ||
605 | ! |
is_scatterplot <- all(vapply(ANL[c(x_name, y_name)], is.numeric, logical(1))) && |
606 | ! |
length(x_name) > 0 && length(y_name) > 0 |
607 | ||
608 | ! |
if (is_scatterplot) { |
609 | ! |
shinyjs::show("alpha") |
610 | ! |
alpha <- input$alpha |
611 | ! |
shinyjs::show("add_lines") |
612 | ||
613 | ! |
if (color_settings && input$coloring) { |
614 | ! |
shinyjs::hide("fixed_size") |
615 | ! |
shinyjs::show("size_settings") |
616 | ! |
size <- NULL |
617 |
} else { |
|
618 | ! |
shinyjs::show("fixed_size") |
619 | ! |
size <- input$fixed_size |
620 |
}
|
|
621 |
} else { |
|
622 | ! |
shinyjs::hide("add_lines") |
623 | ! |
updateCheckboxInput(session, "add_lines", value = restoreInput(ns("add_lines"), FALSE)) |
624 | ! |
shinyjs::hide("alpha") |
625 | ! |
shinyjs::hide("fixed_size") |
626 | ! |
shinyjs::hide("size_settings") |
627 | ! |
alpha <- 1 |
628 | ! |
size <- NULL |
629 |
}
|
|
630 | ||
631 | ! |
teal::validate_has_data(ANL[, c(x_name, y_name), drop = FALSE], 3, complete = TRUE, allow_inf = FALSE) |
632 | ||
633 | ! |
cl <- bivariate_plot_call( |
634 | ! |
data_name = "ANL", |
635 | ! |
x = x_name, |
636 | ! |
y = y_name, |
637 | ! |
x_class = ifelse(!identical(x_name, character(0)), class(ANL[[x_name]]), "NULL"), |
638 | ! |
y_class = ifelse(!identical(y_name, character(0)), class(ANL[[y_name]]), "NULL"), |
639 | ! |
x_label = varname_w_label(x_name, ANL), |
640 | ! |
y_label = varname_w_label(y_name, ANL), |
641 | ! |
freq = !use_density, |
642 | ! |
theme = ggtheme, |
643 | ! |
rotate_xaxis_labels = rotate_xaxis_labels, |
644 | ! |
swap_axes = swap_axes, |
645 | ! |
alpha = alpha, |
646 | ! |
size = size, |
647 | ! |
ggplot2_args = ggplot2_args |
648 |
)
|
|
649 | ||
650 | ! |
facetting <- (isTRUE(input$facetting) && (!is.null(row_facet_name) || !is.null(col_facet_name))) |
651 | ||
652 | ! |
if (facetting) { |
653 | ! |
facet_cl <- facet_ggplot_call(row_facet_name, col_facet_name, free_x_scales, free_y_scales) |
654 | ||
655 | ! |
if (!is.null(facet_cl)) { |
656 | ! |
cl <- call("+", cl, facet_cl) |
657 |
}
|
|
658 |
}
|
|
659 | ||
660 | ! |
if (input$add_lines) { |
661 | ! |
cl <- call("+", cl, quote(geom_line(size = 1))) |
662 |
}
|
|
663 | ||
664 | ! |
coloring_cl <- NULL |
665 | ! |
if (color_settings) { |
666 | ! |
if (input$coloring) { |
667 | ! |
coloring_cl <- coloring_ggplot_call( |
668 | ! |
colour = color_name, |
669 | ! |
fill = fill_name, |
670 | ! |
size = size_name, |
671 | ! |
is_point = any(grepl("geom_point", cl %>% deparse())) |
672 |
)
|
|
673 | ! |
legend_lbls <- substitute( |
674 | ! |
expr = labs(color = color_name, fill = fill_name, size = size_name), |
675 | ! |
env = list( |
676 | ! |
color_name = varname_w_label(color_name, ANL), |
677 | ! |
fill_name = varname_w_label(fill_name, ANL), |
678 | ! |
size_name = varname_w_label(size_name, ANL) |
679 |
)
|
|
680 |
)
|
|
681 |
}
|
|
682 | ! |
if (!is.null(coloring_cl)) { |
683 | ! |
cl <- call("+", call("+", cl, coloring_cl), legend_lbls) |
684 |
}
|
|
685 |
}
|
|
686 | ||
687 | ! |
teal.code::eval_code(merged$anl_q_r(), substitute(expr = plot <- cl, env = list(cl = cl))) |
688 |
}) |
|
689 | ||
690 | ! |
decorated_output_q_facets <- srv_decorate_teal_data( |
691 | ! |
"decorator",
|
692 | ! |
data = output_q, |
693 | ! |
decorators = select_decorators(decorators, "plot"), |
694 | ! |
expr = reactive({ |
695 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
696 | ! |
row_facet_name <- as.vector(merged$anl_input_r()$columns_source$row_facet) |
697 | ! |
col_facet_name <- as.vector(merged$anl_input_r()$columns_source$col_facet) |
698 | ||
699 |
# Add labels to facets
|
|
700 | ! |
nulled_row_facet_name <- varname_w_label(row_facet_name, ANL) |
701 | ! |
nulled_col_facet_name <- varname_w_label(col_facet_name, ANL) |
702 | ! |
facetting <- (isTRUE(input$facetting) && (!is.null(row_facet_name) || !is.null(col_facet_name))) |
703 | ! |
without_facet <- (is.null(nulled_row_facet_name) && is.null(nulled_col_facet_name)) || !facetting |
704 | ||
705 | ! |
print_call <- if (without_facet) { |
706 | ! |
quote(print(plot)) |
707 |
} else { |
|
708 | ! |
substitute( |
709 | ! |
expr = { |
710 |
# Add facetting labels
|
|
711 |
# optional: grid.newpage() # nolint: commented_code.
|
|
712 |
# Prefixed with teal.modules.general as its usage will appear in "Show R code"
|
|
713 | ! |
plot <- teal.modules.general::add_facet_labels( |
714 | ! |
plot,
|
715 | ! |
xfacet_label = nulled_col_facet_name, |
716 | ! |
yfacet_label = nulled_row_facet_name |
717 |
)
|
|
718 | ! |
grid::grid.newpage() |
719 | ! |
grid::grid.draw(plot) |
720 |
},
|
|
721 | ! |
env = list(nulled_col_facet_name = nulled_col_facet_name, nulled_row_facet_name = nulled_row_facet_name) |
722 |
)
|
|
723 |
}
|
|
724 | ! |
print_call
|
725 |
}), |
|
726 | ! |
expr_is_reactive = TRUE |
727 |
)
|
|
728 | ||
729 | ! |
plot_r <- reactive(req(decorated_output_q_facets())[["plot"]]) |
730 | ||
731 | ! |
pws <- teal.widgets::plot_with_settings_srv( |
732 | ! |
id = "myplot", |
733 | ! |
plot_r = plot_r, |
734 | ! |
height = plot_height, |
735 | ! |
width = plot_width |
736 |
)
|
|
737 | ||
738 |
# Render R code.
|
|
739 | ||
740 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_output_q_facets()))) |
741 | ||
742 | ! |
teal.widgets::verbatim_popup_srv( |
743 | ! |
id = "rcode", |
744 | ! |
verbatim_content = source_code_r, |
745 | ! |
title = "Bivariate Plot" |
746 |
)
|
|
747 | ||
748 |
### REPORTER
|
|
749 | ! |
if (with_reporter) { |
750 | ! |
card_fun <- function(comment, label) { |
751 | ! |
card <- teal::report_card_template( |
752 | ! |
title = "Bivariate Plot", |
753 | ! |
label = label, |
754 | ! |
with_filter = with_filter, |
755 | ! |
filter_panel_api = filter_panel_api |
756 |
)
|
|
757 | ! |
card$append_text("Plot", "header3") |
758 | ! |
card$append_plot(plot_r(), dim = pws$dim()) |
759 | ! |
if (!comment == "") { |
760 | ! |
card$append_text("Comment", "header3") |
761 | ! |
card$append_text(comment) |
762 |
}
|
|
763 | ! |
card$append_src(source_code_r()) |
764 | ! |
card
|
765 |
}
|
|
766 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
767 |
}
|
|
768 |
###
|
|
769 |
}) |
|
770 |
}
|
|
771 | ||
772 |
# Get Substituted ggplot call
|
|
773 |
bivariate_plot_call <- function(data_name, |
|
774 |
x = character(0), |
|
775 |
y = character(0), |
|
776 |
x_class = "NULL", |
|
777 |
y_class = "NULL", |
|
778 |
x_label = NULL, |
|
779 |
y_label = NULL, |
|
780 |
freq = TRUE, |
|
781 |
theme = "gray", |
|
782 |
rotate_xaxis_labels = FALSE, |
|
783 |
swap_axes = FALSE, |
|
784 |
alpha = double(0), |
|
785 |
size = 2, |
|
786 |
ggplot2_args = teal.widgets::ggplot2_args()) { |
|
787 | ! |
supported_types <- c("NULL", "numeric", "integer", "factor", "character", "logical", "ordered") |
788 | ! |
validate(need(x_class %in% supported_types, paste0("Data type '", x_class, "' is not supported."))) |
789 | ! |
validate(need(y_class %in% supported_types, paste0("Data type '", y_class, "' is not supported."))) |
790 | ||
791 | ||
792 | ! |
if (identical(x, character(0))) { |
793 | ! |
x <- x_label <- "-" |
794 |
} else { |
|
795 | ! |
x <- if (is.call(x)) x else as.name(x) |
796 |
}
|
|
797 | ! |
if (identical(y, character(0))) { |
798 | ! |
y <- y_label <- "-" |
799 |
} else { |
|
800 | ! |
y <- if (is.call(y)) y else as.name(y) |
801 |
}
|
|
802 | ||
803 | ! |
cl <- bivariate_ggplot_call( |
804 | ! |
x_class = x_class, |
805 | ! |
y_class = y_class, |
806 | ! |
freq = freq, |
807 | ! |
theme = theme, |
808 | ! |
rotate_xaxis_labels = rotate_xaxis_labels, |
809 | ! |
swap_axes = swap_axes, |
810 | ! |
alpha = alpha, |
811 | ! |
size = size, |
812 | ! |
ggplot2_args = ggplot2_args, |
813 | ! |
x = x, |
814 | ! |
y = y, |
815 | ! |
xlab = x_label, |
816 | ! |
ylab = y_label, |
817 | ! |
data_name = data_name |
818 |
)
|
|
819 |
}
|
|
820 | ||
821 |
# Create ggplot part of plot call
|
|
822 |
# Due to the type of the x and y variable the plot type is chosen
|
|
823 |
bivariate_ggplot_call <- function(x_class, |
|
824 |
y_class,
|
|
825 |
freq = TRUE, |
|
826 |
theme = "gray", |
|
827 |
rotate_xaxis_labels = FALSE, |
|
828 |
swap_axes = FALSE, |
|
829 |
size = double(0), |
|
830 |
alpha = double(0), |
|
831 |
x = NULL, |
|
832 |
y = NULL, |
|
833 |
xlab = "-", |
|
834 |
ylab = "-", |
|
835 |
data_name = "ANL", |
|
836 |
ggplot2_args = teal.widgets::ggplot2_args()) { |
|
837 | 42x |
x_class <- switch(x_class, |
838 | 42x |
"character" = , |
839 | 42x |
"ordered" = , |
840 | 42x |
"logical" = , |
841 | 42x |
"factor" = "factor", |
842 | 42x |
"integer" = , |
843 | 42x |
"numeric" = "numeric", |
844 | 42x |
"NULL" = "NULL", |
845 | 42x |
stop("unsupported x_class: ", x_class) |
846 |
)
|
|
847 | 42x |
y_class <- switch(y_class, |
848 | 42x |
"character" = , |
849 | 42x |
"ordered" = , |
850 | 42x |
"logical" = , |
851 | 42x |
"factor" = "factor", |
852 | 42x |
"integer" = , |
853 | 42x |
"numeric" = "numeric", |
854 | 42x |
"NULL" = "NULL", |
855 | 42x |
stop("unsupported y_class: ", y_class) |
856 |
)
|
|
857 | ||
858 | 42x |
if (all(c(x_class, y_class) == "NULL")) { |
859 | ! |
stop("either x or y is required") |
860 |
}
|
|
861 | ||
862 | 42x |
reduce_plot_call <- function(...) { |
863 | 104x |
args <- Filter(Negate(is.null), list(...)) |
864 | 104x |
Reduce(function(x, y) call("+", x, y), args) |
865 |
}
|
|
866 | ||
867 | 42x |
plot_call <- substitute(ggplot2::ggplot(data_name), env = list(data_name = as.name(data_name))) |
868 | ||
869 |
# Single data plots
|
|
870 | 42x |
if (x_class == "numeric" && y_class == "NULL") { |
871 | 6x |
plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = xval), env = list(xval = x))) |
872 | ||
873 | 6x |
if (freq) { |
874 | 4x |
plot_call <- reduce_plot_call( |
875 | 4x |
plot_call,
|
876 | 4x |
quote(ggplot2::geom_histogram(bins = 30)), |
877 | 4x |
quote(ggplot2::ylab("Frequency")) |
878 |
)
|
|
879 |
} else { |
|
880 | 2x |
plot_call <- reduce_plot_call( |
881 | 2x |
plot_call,
|
882 | 2x |
quote(ggplot2::geom_histogram(bins = 30, ggplot2::aes(y = ggplot2::after_stat(density)))), |
883 | 2x |
quote(ggplot2::geom_density(ggplot2::aes(y = ggplot2::after_stat(density)))), |
884 | 2x |
quote(ggplot2::ylab("Density")) |
885 |
)
|
|
886 |
}
|
|
887 | 36x |
} else if (x_class == "NULL" && y_class == "numeric") { |
888 | 6x |
plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = yval), env = list(yval = y))) |
889 | ||
890 | 6x |
if (freq) { |
891 | 4x |
plot_call <- reduce_plot_call( |
892 | 4x |
plot_call,
|
893 | 4x |
quote(ggplot2::geom_histogram(bins = 30)), |
894 | 4x |
quote(ggplot2::ylab("Frequency")) |
895 |
)
|
|
896 |
} else { |
|
897 | 2x |
plot_call <- reduce_plot_call( |
898 | 2x |
plot_call,
|
899 | 2x |
quote(ggplot2::geom_histogram(bins = 30, ggplot2::aes(y = ggplot2::after_stat(density)))), |
900 | 2x |
quote(ggplot2::geom_density(ggplot2::aes(y = ggplot2::after_stat(density)))), |
901 | 2x |
quote(ggplot2::ylab("Density")) |
902 |
)
|
|
903 |
}
|
|
904 | 30x |
} else if (x_class == "factor" && y_class == "NULL") { |
905 | 4x |
plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = xval), env = list(xval = x))) |
906 | ||
907 | 4x |
if (freq) { |
908 | 2x |
plot_call <- reduce_plot_call( |
909 | 2x |
plot_call,
|
910 | 2x |
quote(ggplot2::geom_bar()), |
911 | 2x |
quote(ggplot2::ylab("Frequency")) |
912 |
)
|
|
913 |
} else { |
|
914 | 2x |
plot_call <- reduce_plot_call( |
915 | 2x |
plot_call,
|
916 | 2x |
quote(ggplot2::geom_bar(ggplot2::aes(y = ggplot2::after_stat(prop), group = 1))), |
917 | 2x |
quote(ggplot2::ylab("Fraction")) |
918 |
)
|
|
919 |
}
|
|
920 | 26x |
} else if (x_class == "NULL" && y_class == "factor") { |
921 | 4x |
plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = yval), env = list(yval = y))) |
922 | ||
923 | 4x |
if (freq) { |
924 | 2x |
plot_call <- reduce_plot_call( |
925 | 2x |
plot_call,
|
926 | 2x |
quote(ggplot2::geom_bar()), |
927 | 2x |
quote(ggplot2::ylab("Frequency")) |
928 |
)
|
|
929 |
} else { |
|
930 | 2x |
plot_call <- reduce_plot_call( |
931 | 2x |
plot_call,
|
932 | 2x |
quote(ggplot2::geom_bar(ggplot2::aes(y = ggplot2::after_stat(prop), group = 1))), |
933 | 2x |
quote(ggplot2::ylab("Fraction")) |
934 |
)
|
|
935 |
}
|
|
936 |
# Numeric Plots
|
|
937 | 22x |
} else if (x_class == "numeric" && y_class == "numeric") { |
938 | 2x |
plot_call <- reduce_plot_call( |
939 | 2x |
plot_call,
|
940 | 2x |
substitute(ggplot2::aes(x = xval, y = yval), env = list(xval = x, yval = y)), |
941 |
# pch = 21 for consistent coloring behaviour b/w all geoms (outline and fill properties)
|
|
942 | 2x |
`if`( |
943 | 2x |
!is.null(size), |
944 | 2x |
substitute( |
945 | 2x |
ggplot2::geom_point(alpha = alphaval, size = sizeval, pch = 21), |
946 | 2x |
env = list(alphaval = alpha, sizeval = size) |
947 |
),
|
|
948 | 2x |
substitute( |
949 | 2x |
ggplot2::geom_point(alpha = alphaval, pch = 21), |
950 | 2x |
env = list(alphaval = alpha) |
951 |
)
|
|
952 |
)
|
|
953 |
)
|
|
954 | 20x |
} else if ((x_class == "numeric" && y_class == "factor") || (x_class == "factor" && y_class == "numeric")) { |
955 | 6x |
plot_call <- reduce_plot_call( |
956 | 6x |
plot_call,
|
957 | 6x |
substitute(ggplot2::aes(x = xval, y = yval), env = list(xval = x, yval = y)), |
958 | 6x |
quote(ggplot2::geom_boxplot()) |
959 |
)
|
|
960 |
# Factor and character plots
|
|
961 | 14x |
} else if (x_class == "factor" && y_class == "factor") { |
962 | 14x |
plot_call <- reduce_plot_call( |
963 | 14x |
plot_call,
|
964 | 14x |
substitute( |
965 | 14x |
ggmosaic::geom_mosaic(aes(x = ggmosaic::product(xval), fill = yval), na.rm = TRUE), |
966 | 14x |
env = list(xval = x, yval = y) |
967 |
)
|
|
968 |
)
|
|
969 |
} else { |
|
970 | ! |
stop("x y type combination not allowed") |
971 |
}
|
|
972 | ||
973 | 42x |
labs_base <- if (x_class == "NULL") { |
974 | 10x |
list(x = substitute(ylab, list(ylab = ylab))) |
975 | 42x |
} else if (y_class == "NULL") { |
976 | 10x |
list(x = substitute(xlab, list(xlab = xlab))) |
977 |
} else { |
|
978 | 22x |
list( |
979 | 22x |
x = substitute(xlab, list(xlab = xlab)), |
980 | 22x |
y = substitute(ylab, list(ylab = ylab)) |
981 |
)
|
|
982 |
}
|
|
983 | ||
984 | 42x |
dev_ggplot2_args <- teal.widgets::ggplot2_args(labs = labs_base) |
985 | ||
986 | 42x |
if (rotate_xaxis_labels) { |
987 | ! |
dev_ggplot2_args$theme <- list(axis.text.x = quote(ggplot2::element_text(angle = 45, hjust = 1))) |
988 |
}
|
|
989 | ||
990 | 42x |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
991 | 42x |
user_plot = ggplot2_args, |
992 | 42x |
module_plot = dev_ggplot2_args |
993 |
)
|
|
994 | ||
995 | 42x |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(all_ggplot2_args, ggtheme = theme) |
996 | ||
997 | 42x |
plot_call <- reduce_plot_call( |
998 | 42x |
plot_call,
|
999 | 42x |
parsed_ggplot2_args$labs, |
1000 | 42x |
parsed_ggplot2_args$ggtheme, |
1001 | 42x |
parsed_ggplot2_args$theme |
1002 |
)
|
|
1003 | ||
1004 | 42x |
if (swap_axes) { |
1005 | ! |
plot_call <- reduce_plot_call(plot_call, quote(coord_flip())) |
1006 |
}
|
|
1007 | ||
1008 | 42x |
plot_call
|
1009 |
}
|
|
1010 | ||
1011 |
# Create facet call
|
|
1012 |
facet_ggplot_call <- function(row_facet = character(0), |
|
1013 |
col_facet = character(0), |
|
1014 |
free_x_scales = FALSE, |
|
1015 |
free_y_scales = FALSE) { |
|
1016 | ! |
scales <- if (free_x_scales && free_y_scales) { |
1017 | ! |
"free"
|
1018 | ! |
} else if (free_x_scales) { |
1019 | ! |
"free_x"
|
1020 | ! |
} else if (free_y_scales) { |
1021 | ! |
"free_y"
|
1022 |
} else { |
|
1023 | ! |
"fixed"
|
1024 |
}
|
|
1025 | ||
1026 | ! |
if (identical(row_facet, character(0)) && identical(col_facet, character(0))) { |
1027 | ! |
NULL
|
1028 | ! |
} else if (!identical(row_facet, character(0)) && !identical(col_facet, character(0))) { |
1029 | ! |
call( |
1030 | ! |
"facet_grid",
|
1031 | ! |
rows = call_fun_dots("vars", row_facet), |
1032 | ! |
cols = call_fun_dots("vars", col_facet), |
1033 | ! |
scales = scales |
1034 |
)
|
|
1035 | ! |
} else if (identical(row_facet, character(0)) && !identical(col_facet, character(0))) { |
1036 | ! |
call("facet_grid", cols = call_fun_dots("vars", col_facet), scales = scales) |
1037 | ! |
} else if (!identical(row_facet, character(0)) && identical(col_facet, character(0))) { |
1038 | ! |
call("facet_grid", rows = call_fun_dots("vars", row_facet), scales = scales) |
1039 |
}
|
|
1040 |
}
|
|
1041 | ||
1042 |
coloring_ggplot_call <- function(colour, |
|
1043 |
fill,
|
|
1044 |
size,
|
|
1045 |
is_point = FALSE) { |
|
1046 |
if ( |
|
1047 | 15x |
!identical(colour, character(0)) && |
1048 | 15x |
!identical(fill, character(0)) && |
1049 | 15x |
is_point && |
1050 | 15x |
!identical(size, character(0)) |
1051 |
) { |
|
1052 | 1x |
substitute( |
1053 | 1x |
expr = ggplot2::aes(colour = colour_name, fill = fill_name, size = size_name), |
1054 | 1x |
env = list(colour_name = as.name(colour), fill_name = as.name(fill), size_name = as.name(size)) |
1055 |
)
|
|
1056 |
} else if ( |
|
1057 | 14x |
identical(colour, character(0)) && |
1058 | 14x |
!identical(fill, character(0)) && |
1059 | 14x |
is_point && |
1060 | 14x |
identical(size, character(0)) |
1061 |
) { |
|
1062 | 1x |
substitute(expr = ggplot2::aes(fill = fill_name), env = list(fill_name = as.name(fill))) |
1063 |
} else if ( |
|
1064 | 13x |
!identical(colour, character(0)) && |
1065 | 13x |
!identical(fill, character(0)) && |
1066 | 13x |
(!is_point || identical(size, character(0))) |
1067 |
) { |
|
1068 | 3x |
substitute( |
1069 | 3x |
expr = ggplot2::aes(colour = colour_name, fill = fill_name), |
1070 | 3x |
env = list(colour_name = as.name(colour), fill_name = as.name(fill)) |
1071 |
)
|
|
1072 |
} else if ( |
|
1073 | 10x |
!identical(colour, character(0)) && |
1074 | 10x |
identical(fill, character(0)) && |
1075 | 10x |
(!is_point || identical(size, character(0))) |
1076 |
) { |
|
1077 | 1x |
substitute(expr = ggplot2::aes(colour = colour_name), env = list(colour_name = as.name(colour))) |
1078 |
} else if ( |
|
1079 | 9x |
identical(colour, character(0)) && |
1080 | 9x |
!identical(fill, character(0)) && |
1081 | 9x |
(!is_point || identical(size, character(0))) |
1082 |
) { |
|
1083 | 2x |
substitute(expr = ggplot2::aes(fill = fill_name), env = list(fill_name = as.name(fill))) |
1084 |
} else if ( |
|
1085 | 7x |
identical(colour, character(0)) && |
1086 | 7x |
identical(fill, character(0)) && |
1087 | 7x |
is_point && |
1088 | 7x |
!identical(size, character(0)) |
1089 |
) { |
|
1090 | 1x |
substitute(expr = ggplot2::aes(size = size_name), env = list(size_name = as.name(size))) |
1091 |
} else if ( |
|
1092 | 6x |
!identical(colour, character(0)) && |
1093 | 6x |
identical(fill, character(0)) && |
1094 | 6x |
is_point && |
1095 | 6x |
!identical(size, character(0)) |
1096 |
) { |
|
1097 | 1x |
substitute( |
1098 | 1x |
expr = ggplot2::aes(colour = colour_name, size = size_name), |
1099 | 1x |
env = list(colour_name = as.name(colour), size_name = as.name(size)) |
1100 |
)
|
|
1101 |
} else if ( |
|
1102 | 5x |
identical(colour, character(0)) && |
1103 | 5x |
!identical(fill, character(0)) && |
1104 | 5x |
is_point && |
1105 | 5x |
!identical(size, character(0)) |
1106 |
) { |
|
1107 | 1x |
substitute( |
1108 | 1x |
expr = ggplot2::aes(colour = colour_name, fill = fill_name, size = size_name), |
1109 | 1x |
env = list(colour_name = as.name(fill), fill_name = as.name(fill), size_name = as.name(size)) |
1110 |
)
|
|
1111 |
} else { |
|
1112 | 4x |
NULL
|
1113 |
}
|
|
1114 |
}
|
1 |
#' `teal` module: Front page
|
|
2 |
#'
|
|
3 |
#' Creates a simple front page for `teal` applications, displaying
|
|
4 |
#' introductory text, tables, additional `html` or `shiny` tags, and footnotes.
|
|
5 |
#'
|
|
6 |
#' @inheritParams teal::module
|
|
7 |
#' @param header_text (`character` vector) text to be shown at the top of the module, for each
|
|
8 |
#' element, if named the name is shown first in bold as a header followed by the value. The first
|
|
9 |
#' element's header is displayed larger than the others.
|
|
10 |
#' @param tables (`named list` of `data.frame`s) tables to be shown in the module.
|
|
11 |
#' @param additional_tags (`shiny.tag.list` or `html`) additional `shiny` tags or `html` to be included after the table,
|
|
12 |
#' for example to include an image, `tagList(tags$img(src = "image.png"))` or to include further `html`,
|
|
13 |
#' `HTML("html text here")`.
|
|
14 |
#' @param footnotes (`character` vector) of text to be shown at the bottom of the module, for each
|
|
15 |
#' element, if named the name is shown first in bold, followed by the value.
|
|
16 |
#' @param show_metadata (`logical`) `r lifecycle::badge("deprecated")` indicating
|
|
17 |
#' whether the metadata of the datasets be available on the module.
|
|
18 |
#' Metadata shown automatically when `datanames` set.
|
|
19 |
#' @inheritParams tm_variable_browser
|
|
20 |
#'
|
|
21 |
#' @inherit shared_params return
|
|
22 |
#'
|
|
23 |
#' @examplesShinylive
|
|
24 |
#' library(teal.modules.general)
|
|
25 |
#' interactive <- function() TRUE
|
|
26 |
#' {{ next_example }}
|
|
27 |
#' @examples
|
|
28 |
#' data <- teal_data()
|
|
29 |
#' data <- within(data, {
|
|
30 |
#' require(nestcolor)
|
|
31 |
#' ADSL <- teal.data::rADSL
|
|
32 |
#' attr(ADSL, "metadata") <- list("Author" = "NEST team", "data_source" = "synthetic data")
|
|
33 |
#' })
|
|
34 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
35 |
#'
|
|
36 |
#' table_1 <- data.frame(Info = c("A", "B"), Text = c("A", "B"))
|
|
37 |
#' table_2 <- data.frame(`Column 1` = c("C", "D"), `Column 2` = c(5.5, 6.6), `Column 3` = c("A", "B"))
|
|
38 |
#' table_3 <- data.frame(Info = c("E", "F"), Text = c("G", "H"))
|
|
39 |
#'
|
|
40 |
#' table_input <- list(
|
|
41 |
#' "Table 1" = table_1,
|
|
42 |
#' "Table 2" = table_2,
|
|
43 |
#' "Table 3" = table_3
|
|
44 |
#' )
|
|
45 |
#'
|
|
46 |
#' app <- init(
|
|
47 |
#' data = data,
|
|
48 |
#' modules = modules(
|
|
49 |
#' tm_front_page(
|
|
50 |
#' header_text = c(
|
|
51 |
#' "Important information" = "It can go here.",
|
|
52 |
#' "Other information" = "Can go here."
|
|
53 |
#' ),
|
|
54 |
#' tables = table_input,
|
|
55 |
#' additional_tags = HTML("Additional HTML or shiny tags go here <br>"),
|
|
56 |
#' footnotes = c("X" = "is the first footnote", "Y is the second footnote")
|
|
57 |
#' )
|
|
58 |
#' )
|
|
59 |
#' ) |>
|
|
60 |
#' modify_header(tags$h1("Sample Application")) |>
|
|
61 |
#' modify_footer(tags$p("Application footer"))
|
|
62 |
#'
|
|
63 |
#' if (interactive()) {
|
|
64 |
#' shinyApp(app$ui, app$server)
|
|
65 |
#' }
|
|
66 |
#'
|
|
67 |
#' @export
|
|
68 |
#'
|
|
69 |
tm_front_page <- function(label = "Front page", |
|
70 |
header_text = character(0), |
|
71 |
tables = list(), |
|
72 |
additional_tags = tagList(), |
|
73 |
footnotes = character(0), |
|
74 |
show_metadata = deprecated(), |
|
75 |
datanames = if (missing(show_metadata)) NULL else "all", |
|
76 |
transformators = list()) { |
|
77 | ! |
message("Initializing tm_front_page") |
78 | ||
79 |
# Start of assertions
|
|
80 | ! |
checkmate::assert_string(label) |
81 | ! |
checkmate::assert_character(header_text, min.len = 0, any.missing = FALSE) |
82 | ! |
checkmate::assert_list(tables, types = "data.frame", names = "named", any.missing = FALSE) |
83 | ! |
checkmate::assert_multi_class(additional_tags, classes = c("shiny.tag.list", "html")) |
84 | ! |
checkmate::assert_character(footnotes, min.len = 0, any.missing = FALSE) |
85 | ! |
if (!missing(show_metadata)) { |
86 | ! |
lifecycle::deprecate_soft( |
87 | ! |
when = "0.4.0", |
88 | ! |
what = "tm_front_page(show_metadata)", |
89 | ! |
with = "tm_front_page(datanames)", |
90 | ! |
details = c( |
91 | ! |
"With `datanames` you can select which datasets are displayed.",
|
92 | ! |
i = "Use `tm_front_page(datanames = 'all')` to keep the previous behavior and avoid this warning." |
93 |
)
|
|
94 |
)
|
|
95 |
}
|
|
96 | ! |
checkmate::assert_character(datanames, min.len = 0, min.chars = 1, null.ok = TRUE) |
97 | ||
98 |
# End of assertions
|
|
99 | ||
100 |
# Make UI args
|
|
101 | ! |
args <- as.list(environment()) |
102 | ||
103 | ! |
ans <- module( |
104 | ! |
label = label, |
105 | ! |
server = srv_front_page, |
106 | ! |
ui = ui_front_page, |
107 | ! |
ui_args = args, |
108 | ! |
server_args = list(tables = tables), |
109 | ! |
datanames = datanames, , |
110 | ! |
transformators = transformators |
111 |
)
|
|
112 | ! |
attr(ans, "teal_bookmarkable") <- TRUE |
113 | ! |
ans
|
114 |
}
|
|
115 | ||
116 |
# UI function for the front page module
|
|
117 |
ui_front_page <- function(id, ...) { |
|
118 | ! |
args <- list(...) |
119 | ! |
ns <- NS(id) |
120 | ||
121 | ! |
tagList( |
122 | ! |
include_css_files("custom"), |
123 | ! |
tags$div( |
124 | ! |
id = "front_page_content", |
125 | ! |
class = "ml-8", |
126 | ! |
tags$div( |
127 | ! |
id = "front_page_headers", |
128 | ! |
get_header_tags(args$header_text) |
129 |
),
|
|
130 | ! |
tags$div( |
131 | ! |
id = "front_page_tables", |
132 | ! |
class = "ml-4", |
133 | ! |
get_table_tags(args$tables, ns) |
134 |
),
|
|
135 | ! |
tags$div( |
136 | ! |
id = "front_page_custom_html", |
137 | ! |
class = "my-4", |
138 | ! |
args$additional_tags |
139 |
),
|
|
140 | ! |
if (length(args$datanames) > 0L) { |
141 | ! |
tags$div( |
142 | ! |
id = "front_page_metabutton", |
143 | ! |
class = "m-4", |
144 | ! |
actionButton(ns("metadata_button"), "Show metadata") |
145 |
)
|
|
146 |
},
|
|
147 | ! |
tags$footer( |
148 | ! |
class = ".small", |
149 | ! |
get_footer_tags(args$footnotes) |
150 |
)
|
|
151 |
)
|
|
152 |
)
|
|
153 |
}
|
|
154 | ||
155 |
# Server function for the front page module
|
|
156 |
srv_front_page <- function(id, data, tables) { |
|
157 | ! |
checkmate::assert_class(data, "reactive") |
158 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
159 | ! |
moduleServer(id, function(input, output, session) { |
160 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
161 | ||
162 | ! |
ns <- session$ns |
163 | ||
164 | ! |
setBookmarkExclude("metadata_button") |
165 | ||
166 | ! |
lapply(seq_along(tables), function(idx) { |
167 | ! |
output[[paste0("table_", idx)]] <- renderTable( |
168 | ! |
tables[[idx]], |
169 | ! |
bordered = TRUE, |
170 | ! |
caption = names(tables)[idx], |
171 | ! |
caption.placement = "top" |
172 |
)
|
|
173 |
}) |
|
174 | ! |
if (length(isolate(names(data()))) > 0L) { |
175 | ! |
observeEvent( |
176 | ! |
input$metadata_button, showModal( |
177 | ! |
modalDialog( |
178 | ! |
title = "Metadata", |
179 | ! |
dataTableOutput(ns("metadata_table")), |
180 | ! |
size = "l", |
181 | ! |
easyClose = TRUE |
182 |
)
|
|
183 |
)
|
|
184 |
)
|
|
185 | ||
186 | ! |
metadata_data_frame <- reactive({ |
187 | ! |
datanames <- names(data()) |
188 | ! |
convert_metadata_to_dataframe( |
189 | ! |
lapply(datanames, function(dataname) attr(data()[[dataname]], "metadata")), |
190 | ! |
datanames
|
191 |
)
|
|
192 |
}) |
|
193 | ||
194 | ! |
output$metadata_table <- renderDataTable({ |
195 | ! |
validate(need(nrow(metadata_data_frame()) > 0, "The data has no associated metadata")) |
196 | ! |
metadata_data_frame() |
197 |
}) |
|
198 |
}
|
|
199 |
}) |
|
200 |
}
|
|
201 | ||
202 |
## utils functions
|
|
203 | ||
204 |
get_header_tags <- function(header_text) { |
|
205 | ! |
if (length(header_text) == 0) { |
206 | ! |
return(list()) |
207 |
}
|
|
208 | ||
209 | ! |
get_single_header_tags <- function(header_text, p_text, header_tag = tags$h4) { |
210 | ! |
tagList( |
211 | ! |
tags$div( |
212 | ! |
if (!is.null(header_text) && nchar(header_text) > 0) header_tag(header_text), |
213 | ! |
tags$p(p_text) |
214 |
)
|
|
215 |
)
|
|
216 |
}
|
|
217 | ||
218 | ! |
header_tags <- get_single_header_tags(names(header_text[1]), header_text[1], header_tag = tags$h3) |
219 | ! |
c(header_tags, mapply(get_single_header_tags, utils::tail(names(header_text), -1), utils::tail(header_text, -1))) |
220 |
}
|
|
221 | ||
222 |
get_table_tags <- function(tables, ns) { |
|
223 | ! |
if (length(tables) == 0) { |
224 | ! |
return(list()) |
225 |
}
|
|
226 | ! |
table_tags <- c(lapply(seq_along(tables), function(idx) { |
227 | ! |
list( |
228 | ! |
tableOutput(ns(paste0("table_", idx))) |
229 |
)
|
|
230 |
})) |
|
231 | ! |
return(table_tags) |
232 |
}
|
|
233 | ||
234 |
get_footer_tags <- function(footnotes) { |
|
235 | ! |
if (length(footnotes) == 0) { |
236 | ! |
return(list()) |
237 |
}
|
|
238 | ! |
bold_texts <- if (is.null(names(footnotes))) rep("", length(footnotes)) else names(footnotes) |
239 | ! |
footnote_tags <- mapply(function(bold_text, value) { |
240 | ! |
list( |
241 | ! |
tags$div( |
242 | ! |
tags$b(bold_text), |
243 | ! |
value,
|
244 | ! |
tags$br() |
245 |
)
|
|
246 |
)
|
|
247 | ! |
}, bold_text = bold_texts, value = footnotes) |
248 |
}
|
|
249 | ||
250 |
# take a list of metadata, one item per dataset (raw_metadata each element from datasets$get_metadata())
|
|
251 |
# and the corresponding datanames and output a data.frame with columns {Dataset, Name, Value}.
|
|
252 |
# which are, the Dataset the metadata came from, the metadata's name and value
|
|
253 |
convert_metadata_to_dataframe <- function(raw_metadata, datanames) { |
|
254 | 4x |
output <- mapply(function(metadata, dataname) { |
255 | 6x |
if (is.null(metadata)) { |
256 | 2x |
return(data.frame(Dataset = character(0), Name = character(0), Value = character(0))) |
257 |
}
|
|
258 | 4x |
return(data.frame( |
259 | 4x |
Dataset = dataname, |
260 | 4x |
Name = names(metadata), |
261 | 4x |
Value = unname(unlist(lapply(metadata, as.character))) |
262 |
)) |
|
263 | 4x |
}, raw_metadata, datanames, SIMPLIFY = FALSE) |
264 | 4x |
do.call(rbind, output) |
265 |
}
|
1 |
#' `teal` module: Stack plots of variables and show association with reference variable
|
|
2 |
#'
|
|
3 |
#' Module provides functionality for visualizing the distribution of variables and
|
|
4 |
#' their association with a reference variable.
|
|
5 |
#' It supports configuring the appearance of the plots, including themes and whether to show associations.
|
|
6 |
#'
|
|
7 |
#'
|
|
8 |
#' @note For more examples, please see the vignette "Using association plot" via
|
|
9 |
#' `vignette("using-association-plot", package = "teal.modules.general")`.
|
|
10 |
#'
|
|
11 |
#' @inheritParams teal::module
|
|
12 |
#' @inheritParams shared_params
|
|
13 |
#' @param ref (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
14 |
#' Reference variable, must accepts a `data_extract_spec` with `select_spec(multiple = FALSE)`
|
|
15 |
#' to ensure single selection option.
|
|
16 |
#' @param vars (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
17 |
#' Variables to be associated with the reference variable.
|
|
18 |
#' @param show_association (`logical`) optional, whether show association of `vars`
|
|
19 |
#' with reference variable. Defaults to `TRUE`.
|
|
20 |
#' @param distribution_theme,association_theme (`character`) optional, `ggplot2` themes to be used by default.
|
|
21 |
#' Default to `"gray"`.
|
|
22 |
#'
|
|
23 |
#' @param ggplot2_args `r roxygen_ggplot2_args_param("Bivariate1", "Bivariate2")`
|
|
24 |
#'
|
|
25 |
#' @inherit shared_params return
|
|
26 |
#'
|
|
27 |
#' @section Decorating Module:
|
|
28 |
#'
|
|
29 |
#' This module generates the following objects, which can be modified in place using decorators:
|
|
30 |
#' - `plot` (`grob` created with [ggplot2::ggplotGrob()])
|
|
31 |
#'
|
|
32 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
33 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
34 |
#' See code snippet below:
|
|
35 |
#'
|
|
36 |
#' ```
|
|
37 |
#' tm_g_association(
|
|
38 |
#' ..., # arguments for module
|
|
39 |
#' decorators = list(
|
|
40 |
#' plot = teal_transform_module(...) # applied to the `plot` output
|
|
41 |
#' )
|
|
42 |
#' )
|
|
43 |
#' ```
|
|
44 |
#'
|
|
45 |
#' For additional details and examples of decorators, refer to the vignette
|
|
46 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
47 |
#'
|
|
48 |
#' To learn more please refer to the vignette
|
|
49 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
50 |
#'
|
|
51 |
#' @examplesShinylive
|
|
52 |
#' library(teal.modules.general)
|
|
53 |
#' interactive <- function() TRUE
|
|
54 |
#' {{ next_example }}
|
|
55 |
#' @examples
|
|
56 |
#' # general data example
|
|
57 |
#' data <- teal_data()
|
|
58 |
#' data <- within(data, {
|
|
59 |
#' require(nestcolor)
|
|
60 |
#' CO2 <- CO2
|
|
61 |
#' factors <- names(Filter(isTRUE, vapply(CO2, is.factor, logical(1L))))
|
|
62 |
#' CO2[factors] <- lapply(CO2[factors], as.character)
|
|
63 |
#' })
|
|
64 |
#'
|
|
65 |
#' app <- init(
|
|
66 |
#' data = data,
|
|
67 |
#' modules = modules(
|
|
68 |
#' tm_g_association(
|
|
69 |
#' ref = data_extract_spec(
|
|
70 |
#' dataname = "CO2",
|
|
71 |
#' select = select_spec(
|
|
72 |
#' label = "Select variable:",
|
|
73 |
#' choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
|
|
74 |
#' selected = "Plant",
|
|
75 |
#' fixed = FALSE
|
|
76 |
#' )
|
|
77 |
#' ),
|
|
78 |
#' vars = data_extract_spec(
|
|
79 |
#' dataname = "CO2",
|
|
80 |
#' select = select_spec(
|
|
81 |
#' label = "Select variables:",
|
|
82 |
#' choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
|
|
83 |
#' selected = "Treatment",
|
|
84 |
#' multiple = TRUE,
|
|
85 |
#' fixed = FALSE
|
|
86 |
#' )
|
|
87 |
#' )
|
|
88 |
#' )
|
|
89 |
#' )
|
|
90 |
#' )
|
|
91 |
#' if (interactive()) {
|
|
92 |
#' shinyApp(app$ui, app$server)
|
|
93 |
#' }
|
|
94 |
#'
|
|
95 |
#' @examplesShinylive
|
|
96 |
#' library(teal.modules.general)
|
|
97 |
#' interactive <- function() TRUE
|
|
98 |
#' {{ next_example }}
|
|
99 |
#' @examples
|
|
100 |
#' # CDISC data example
|
|
101 |
#' data <- teal_data()
|
|
102 |
#' data <- within(data, {
|
|
103 |
#' require(nestcolor)
|
|
104 |
#' ADSL <- teal.data::rADSL
|
|
105 |
#' })
|
|
106 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
107 |
#'
|
|
108 |
#' app <- init(
|
|
109 |
#' data = data,
|
|
110 |
#' modules = modules(
|
|
111 |
#' tm_g_association(
|
|
112 |
#' ref = data_extract_spec(
|
|
113 |
#' dataname = "ADSL",
|
|
114 |
#' select = select_spec(
|
|
115 |
#' label = "Select variable:",
|
|
116 |
#' choices = variable_choices(
|
|
117 |
#' data[["ADSL"]],
|
|
118 |
#' c("SEX", "RACE", "COUNTRY", "ARM", "STRATA1", "STRATA2", "ITTFL", "BMRKR2")
|
|
119 |
#' ),
|
|
120 |
#' selected = "RACE",
|
|
121 |
#' fixed = FALSE
|
|
122 |
#' )
|
|
123 |
#' ),
|
|
124 |
#' vars = data_extract_spec(
|
|
125 |
#' dataname = "ADSL",
|
|
126 |
#' select = select_spec(
|
|
127 |
#' label = "Select variables:",
|
|
128 |
#' choices = variable_choices(
|
|
129 |
#' data[["ADSL"]],
|
|
130 |
#' c("SEX", "RACE", "COUNTRY", "ARM", "STRATA1", "STRATA2", "ITTFL", "BMRKR2")
|
|
131 |
#' ),
|
|
132 |
#' selected = "BMRKR2",
|
|
133 |
#' multiple = TRUE,
|
|
134 |
#' fixed = FALSE
|
|
135 |
#' )
|
|
136 |
#' )
|
|
137 |
#' )
|
|
138 |
#' )
|
|
139 |
#' )
|
|
140 |
#' if (interactive()) {
|
|
141 |
#' shinyApp(app$ui, app$server)
|
|
142 |
#' }
|
|
143 |
#'
|
|
144 |
#' @export
|
|
145 |
#'
|
|
146 |
tm_g_association <- function(label = "Association", |
|
147 |
ref,
|
|
148 |
vars,
|
|
149 |
show_association = TRUE, |
|
150 |
plot_height = c(600, 400, 5000), |
|
151 |
plot_width = NULL, |
|
152 |
distribution_theme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), # nolint: line_length. |
|
153 |
association_theme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), # nolint: line_length. |
|
154 |
pre_output = NULL, |
|
155 |
post_output = NULL, |
|
156 |
ggplot2_args = teal.widgets::ggplot2_args(), |
|
157 |
transformators = list(), |
|
158 |
decorators = list()) { |
|
159 | ! |
message("Initializing tm_g_association") |
160 | ||
161 |
# Normalize the parameters
|
|
162 | ! |
if (inherits(ref, "data_extract_spec")) ref <- list(ref) |
163 | ! |
if (inherits(vars, "data_extract_spec")) vars <- list(vars) |
164 | ! |
if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args) |
165 | ||
166 |
# Start of assertions
|
|
167 | ! |
checkmate::assert_string(label) |
168 | ||
169 | ! |
checkmate::assert_list(ref, types = "data_extract_spec") |
170 | ! |
if (!all(vapply(ref, function(x) !x$select$multiple, logical(1)))) { |
171 | ! |
stop("'ref' should not allow multiple selection") |
172 |
}
|
|
173 | ||
174 | ! |
checkmate::assert_list(vars, types = "data_extract_spec") |
175 | ! |
checkmate::assert_flag(show_association) |
176 | ||
177 | ! |
checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
178 | ! |
checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
179 | ! |
checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
180 | ! |
checkmate::assert_numeric( |
181 | ! |
plot_width[1], |
182 | ! |
lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width" |
183 |
)
|
|
184 | ||
185 | ! |
distribution_theme <- match.arg(distribution_theme) |
186 | ! |
association_theme <- match.arg(association_theme) |
187 | ||
188 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
189 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
190 | ||
191 | ! |
plot_choices <- c("Bivariate1", "Bivariate2") |
192 | ! |
checkmate::assert_list(ggplot2_args, types = "ggplot2_args") |
193 | ! |
checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices)) |
194 | ||
195 | ! |
assert_decorators(decorators, "plot") |
196 |
# End of assertions
|
|
197 | ||
198 |
# Make UI args
|
|
199 | ! |
args <- as.list(environment()) |
200 | ||
201 | ! |
data_extract_list <- list( |
202 | ! |
ref = ref, |
203 | ! |
vars = vars |
204 |
)
|
|
205 | ||
206 | ! |
ans <- module( |
207 | ! |
label = label, |
208 | ! |
server = srv_tm_g_association, |
209 | ! |
ui = ui_tm_g_association, |
210 | ! |
ui_args = args, |
211 | ! |
server_args = c( |
212 | ! |
data_extract_list,
|
213 | ! |
list(plot_height = plot_height, plot_width = plot_width, ggplot2_args = ggplot2_args, decorators = decorators) |
214 |
),
|
|
215 | ! |
transformators = transformators, |
216 | ! |
datanames = teal.transform::get_extract_datanames(data_extract_list) |
217 |
)
|
|
218 | ! |
attr(ans, "teal_bookmarkable") <- TRUE |
219 | ! |
ans
|
220 |
}
|
|
221 | ||
222 |
# UI function for the association module
|
|
223 |
ui_tm_g_association <- function(id, ...) { |
|
224 | ! |
ns <- NS(id) |
225 | ! |
args <- list(...) |
226 | ! |
is_single_dataset_value <- teal.transform::is_single_dataset(args$ref, args$vars) |
227 | ||
228 | ! |
teal.widgets::standard_layout( |
229 | ! |
output = teal.widgets::white_small_well( |
230 | ! |
textOutput(ns("title")), |
231 | ! |
tags$br(), |
232 | ! |
teal.widgets::plot_with_settings_ui(id = ns("myplot")) |
233 |
),
|
|
234 | ! |
encoding = tags$div( |
235 |
### Reporter
|
|
236 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
237 |
###
|
|
238 | ! |
tags$label("Encodings", class = "text-primary"), |
239 | ! |
teal.transform::datanames_input(args[c("ref", "vars")]), |
240 | ! |
teal.transform::data_extract_ui( |
241 | ! |
id = ns("ref"), |
242 | ! |
label = "Reference variable", |
243 | ! |
data_extract_spec = args$ref, |
244 | ! |
is_single_dataset = is_single_dataset_value |
245 |
),
|
|
246 | ! |
teal.transform::data_extract_ui( |
247 | ! |
id = ns("vars"), |
248 | ! |
label = "Associated variables", |
249 | ! |
data_extract_spec = args$vars, |
250 | ! |
is_single_dataset = is_single_dataset_value |
251 |
),
|
|
252 | ! |
checkboxInput( |
253 | ! |
ns("association"), |
254 | ! |
"Association with reference variable",
|
255 | ! |
value = args$show_association |
256 |
),
|
|
257 | ! |
checkboxInput( |
258 | ! |
ns("show_dist"), |
259 | ! |
"Scaled frequencies",
|
260 | ! |
value = FALSE |
261 |
),
|
|
262 | ! |
checkboxInput( |
263 | ! |
ns("log_transformation"), |
264 | ! |
"Log transformed",
|
265 | ! |
value = FALSE |
266 |
),
|
|
267 | ! |
ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")), |
268 | ! |
teal.widgets::panel_group( |
269 | ! |
teal.widgets::panel_item( |
270 | ! |
title = "Plot settings", |
271 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("alpha"), "Scatterplot opacity:", c(0.5, 0, 1), ticks = FALSE), |
272 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("size"), "Scatterplot points size:", c(2, 1, 8), ticks = FALSE), |
273 | ! |
checkboxInput(ns("swap_axes"), "Swap axes", value = FALSE), |
274 | ! |
checkboxInput(ns("rotate_xaxis_labels"), "Rotate X axis labels", value = FALSE), |
275 | ! |
selectInput( |
276 | ! |
inputId = ns("distribution_theme"), |
277 | ! |
label = "Distribution theme (by ggplot):", |
278 | ! |
choices = ggplot_themes, |
279 | ! |
selected = args$distribution_theme, |
280 | ! |
multiple = FALSE |
281 |
),
|
|
282 | ! |
selectInput( |
283 | ! |
inputId = ns("association_theme"), |
284 | ! |
label = "Association theme (by ggplot):", |
285 | ! |
choices = ggplot_themes, |
286 | ! |
selected = args$association_theme, |
287 | ! |
multiple = FALSE |
288 |
)
|
|
289 |
)
|
|
290 |
)
|
|
291 |
),
|
|
292 | ! |
forms = tagList( |
293 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
294 |
),
|
|
295 | ! |
pre_output = args$pre_output, |
296 | ! |
post_output = args$post_output |
297 |
)
|
|
298 |
}
|
|
299 | ||
300 |
# Server function for the association module
|
|
301 |
srv_tm_g_association <- function(id, |
|
302 |
data,
|
|
303 |
reporter,
|
|
304 |
filter_panel_api,
|
|
305 |
ref,
|
|
306 |
vars,
|
|
307 |
plot_height,
|
|
308 |
plot_width,
|
|
309 |
ggplot2_args,
|
|
310 |
decorators) { |
|
311 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
312 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
313 | ! |
checkmate::assert_class(data, "reactive") |
314 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
315 | ||
316 | ! |
moduleServer(id, function(input, output, session) { |
317 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
318 | ||
319 | ! |
selector_list <- teal.transform::data_extract_multiple_srv( |
320 | ! |
data_extract = list(ref = ref, vars = vars), |
321 | ! |
datasets = data, |
322 | ! |
select_validation_rule = list( |
323 | ! |
ref = shinyvalidate::compose_rules( |
324 | ! |
shinyvalidate::sv_required("A reference variable needs to be selected."), |
325 | ! |
~ if ((.) %in% selector_list()$vars()$select) { |
326 | ! |
"Associated variables and reference variable cannot overlap"
|
327 |
}
|
|
328 |
),
|
|
329 | ! |
vars = shinyvalidate::compose_rules( |
330 | ! |
shinyvalidate::sv_required("An associated variable needs to be selected."), |
331 | ! |
~ if (length(selector_list()$ref()$select) != 0 && selector_list()$ref()$select %in% (.)) { |
332 | ! |
"Associated variables and reference variable cannot overlap"
|
333 |
}
|
|
334 |
)
|
|
335 |
)
|
|
336 |
)
|
|
337 | ||
338 | ! |
iv_r <- reactive({ |
339 | ! |
iv <- shinyvalidate::InputValidator$new() |
340 | ! |
teal.transform::compose_and_enable_validators(iv, selector_list) |
341 |
}) |
|
342 | ||
343 | ! |
anl_merged_input <- teal.transform::merge_expression_srv( |
344 | ! |
datasets = data, |
345 | ! |
selector_list = selector_list |
346 |
)
|
|
347 | ||
348 | ! |
qenv <- teal.code::eval_code( |
349 | ! |
data(), |
350 | ! |
'library("ggplot2");library("dplyr");library("tern");library("ggmosaic")' # nolint quotes |
351 |
)
|
|
352 | ! |
anl_merged_q <- reactive({ |
353 | ! |
req(anl_merged_input()) |
354 | ! |
qenv %>% teal.code::eval_code(as.expression(anl_merged_input()$expr)) |
355 |
}) |
|
356 | ||
357 | ! |
merged <- list( |
358 | ! |
anl_input_r = anl_merged_input, |
359 | ! |
anl_q_r = anl_merged_q |
360 |
)
|
|
361 | ||
362 | ! |
output_q <- reactive({ |
363 | ! |
teal::validate_inputs(iv_r()) |
364 | ||
365 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
366 | ! |
teal::validate_has_data(ANL, 3) |
367 | ||
368 | ! |
vars_names <- merged$anl_input_r()$columns_source$vars |
369 | ||
370 | ! |
ref_name <- as.vector(merged$anl_input_r()$columns_source$ref) |
371 | ! |
association <- input$association |
372 | ! |
show_dist <- input$show_dist |
373 | ! |
log_transformation <- input$log_transformation |
374 | ! |
rotate_xaxis_labels <- input$rotate_xaxis_labels |
375 | ! |
swap_axes <- input$swap_axes |
376 | ! |
distribution_theme <- input$distribution_theme |
377 | ! |
association_theme <- input$association_theme |
378 | ||
379 | ! |
is_scatterplot <- is.numeric(ANL[[ref_name]]) && any(vapply(ANL[vars_names], is.numeric, logical(1))) |
380 | ! |
if (is_scatterplot) { |
381 | ! |
shinyjs::show("alpha") |
382 | ! |
shinyjs::show("size") |
383 | ! |
alpha <- input$alpha |
384 | ! |
size <- input$size |
385 |
} else { |
|
386 | ! |
shinyjs::hide("alpha") |
387 | ! |
shinyjs::hide("size") |
388 | ! |
alpha <- 0.5 |
389 | ! |
size <- 2 |
390 |
}
|
|
391 | ||
392 | ! |
teal::validate_has_data(ANL[, c(ref_name, vars_names)], 3, complete = TRUE, allow_inf = FALSE) |
393 | ||
394 |
# reference
|
|
395 | ! |
ref_class <- class(ANL[[ref_name]])[1] |
396 | ! |
if (is.numeric(ANL[[ref_name]]) && log_transformation) { |
397 |
# works for both integers and doubles
|
|
398 | ! |
ref_cl_name <- call("log", as.name(ref_name)) |
399 | ! |
ref_cl_lbl <- varname_w_label(ref_name, ANL, prefix = "Log of ") |
400 |
} else { |
|
401 |
# silently ignore when non-numeric even if `log` is selected because some
|
|
402 |
# variables may be numeric and others not
|
|
403 | ! |
ref_cl_name <- as.name(ref_name) |
404 | ! |
ref_cl_lbl <- varname_w_label(ref_name, ANL) |
405 |
}
|
|
406 | ||
407 | ! |
user_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
408 | ! |
user_plot = ggplot2_args[["Bivariate1"]], |
409 | ! |
user_default = ggplot2_args$default |
410 |
)
|
|
411 | ||
412 | ! |
ref_call <- bivariate_plot_call( |
413 | ! |
data_name = "ANL", |
414 | ! |
x = ref_cl_name, |
415 | ! |
x_class = ref_class, |
416 | ! |
x_label = ref_cl_lbl, |
417 | ! |
freq = !show_dist, |
418 | ! |
theme = distribution_theme, |
419 | ! |
rotate_xaxis_labels = rotate_xaxis_labels, |
420 | ! |
swap_axes = FALSE, |
421 | ! |
size = size, |
422 | ! |
alpha = alpha, |
423 | ! |
ggplot2_args = user_ggplot2_args |
424 |
)
|
|
425 | ||
426 |
# association
|
|
427 | ! |
ref_class_cov <- ifelse(association, ref_class, "NULL") |
428 | ||
429 | ! |
var_calls <- lapply(vars_names, function(var_i) { |
430 | ! |
var_class <- class(ANL[[var_i]])[1] |
431 | ! |
if (is.numeric(ANL[[var_i]]) && log_transformation) { |
432 |
# works for both integers and doubles
|
|
433 | ! |
var_cl_name <- call("log", as.name(var_i)) |
434 | ! |
var_cl_lbl <- varname_w_label(var_i, ANL, prefix = "Log of ") |
435 |
} else { |
|
436 |
# silently ignore when non-numeric even if `log` is selected because some
|
|
437 |
# variables may be numeric and others not
|
|
438 | ! |
var_cl_name <- as.name(var_i) |
439 | ! |
var_cl_lbl <- varname_w_label(var_i, ANL) |
440 |
}
|
|
441 | ||
442 | ! |
user_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
443 | ! |
user_plot = ggplot2_args[["Bivariate2"]], |
444 | ! |
user_default = ggplot2_args$default |
445 |
)
|
|
446 | ||
447 | ! |
bivariate_plot_call( |
448 | ! |
data_name = "ANL", |
449 | ! |
x = ref_cl_name, |
450 | ! |
y = var_cl_name, |
451 | ! |
x_class = ref_class_cov, |
452 | ! |
y_class = var_class, |
453 | ! |
x_label = ref_cl_lbl, |
454 | ! |
y_label = var_cl_lbl, |
455 | ! |
theme = association_theme, |
456 | ! |
freq = !show_dist, |
457 | ! |
rotate_xaxis_labels = rotate_xaxis_labels, |
458 | ! |
swap_axes = swap_axes, |
459 | ! |
alpha = alpha, |
460 | ! |
size = size, |
461 | ! |
ggplot2_args = user_ggplot2_args |
462 |
)
|
|
463 |
}) |
|
464 | ||
465 |
# helper function to format variable name
|
|
466 | ! |
format_varnames <- function(x) { |
467 | ! |
if (is.numeric(ANL[[x]]) && log_transformation) { |
468 | ! |
varname_w_label(x, ANL, prefix = "Log of ") |
469 |
} else { |
|
470 | ! |
varname_w_label(x, ANL) |
471 |
}
|
|
472 |
}
|
|
473 | ! |
new_title <- |
474 | ! |
if (association) { |
475 | ! |
switch(as.character(length(vars_names)), |
476 | ! |
"0" = sprintf("Value distribution for %s", ref_cl_lbl), |
477 | ! |
"1" = sprintf( |
478 | ! |
"Association between %s and %s",
|
479 | ! |
ref_cl_lbl,
|
480 | ! |
format_varnames(vars_names) |
481 |
),
|
|
482 | ! |
sprintf( |
483 | ! |
"Associations between %s and: %s",
|
484 | ! |
ref_cl_lbl,
|
485 | ! |
paste(lapply(vars_names, format_varnames), collapse = ", ") |
486 |
)
|
|
487 |
)
|
|
488 |
} else { |
|
489 | ! |
switch(as.character(length(vars_names)), |
490 | ! |
"0" = sprintf("Value distribution for %s", ref_cl_lbl), |
491 | ! |
sprintf( |
492 | ! |
"Value distributions for %s and %s",
|
493 | ! |
ref_cl_lbl,
|
494 | ! |
paste(lapply(vars_names, format_varnames), collapse = ", ") |
495 |
)
|
|
496 |
)
|
|
497 |
}
|
|
498 | ! |
teal.code::eval_code( |
499 | ! |
merged$anl_q_r(), |
500 | ! |
substitute( |
501 | ! |
expr = title <- new_title, |
502 | ! |
env = list(new_title = new_title) |
503 |
)
|
|
504 |
) %>% |
|
505 | ! |
teal.code::eval_code( |
506 | ! |
substitute( |
507 | ! |
expr = { |
508 | ! |
plots <- plot_calls |
509 | ! |
plot_top <- plots[[1]] |
510 | ! |
plot_bottom <- plots[[2]] |
511 | ! |
plot <- tern::stack_grobs(grobs = lapply(list(plot_top, plot_bottom), ggplot2::ggplotGrob)) |
512 |
},
|
|
513 | ! |
env = list( |
514 | ! |
plot_calls = do.call( |
515 | ! |
"call",
|
516 | ! |
c(list("list", ref_call), var_calls), |
517 | ! |
quote = TRUE |
518 |
)
|
|
519 |
)
|
|
520 |
)
|
|
521 |
)
|
|
522 |
}) |
|
523 | ||
524 | ! |
decorated_output_grob_q <- srv_decorate_teal_data( |
525 | ! |
id = "decorator", |
526 | ! |
data = output_q, |
527 | ! |
decorators = select_decorators(decorators, "plot"), |
528 | ! |
expr = { |
529 | ! |
grid::grid.newpage() |
530 | ! |
grid::grid.draw(plot) |
531 |
}
|
|
532 |
)
|
|
533 | ||
534 | ! |
plot_r <- reactive({ |
535 | ! |
req(iv_r()$is_valid()) |
536 | ! |
req(decorated_output_grob_q())[["plot"]] |
537 |
}) |
|
538 | ||
539 | ! |
pws <- teal.widgets::plot_with_settings_srv( |
540 | ! |
id = "myplot", |
541 | ! |
plot_r = plot_r, |
542 | ! |
height = plot_height, |
543 | ! |
width = plot_width |
544 |
)
|
|
545 | ||
546 | ! |
output$title <- renderText({ |
547 | ! |
teal.code::dev_suppress(output_q()[["title"]]) |
548 |
}) |
|
549 | ||
550 |
# Render R code.
|
|
551 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_output_grob_q()))) |
552 | ||
553 | ! |
teal.widgets::verbatim_popup_srv( |
554 | ! |
id = "rcode", |
555 | ! |
verbatim_content = source_code_r, |
556 | ! |
title = "Association Plot" |
557 |
)
|
|
558 | ||
559 |
### REPORTER
|
|
560 | ! |
if (with_reporter) { |
561 | ! |
card_fun <- function(comment, label) { |
562 | ! |
card <- teal::report_card_template( |
563 | ! |
title = "Association Plot", |
564 | ! |
label = label, |
565 | ! |
with_filter = with_filter, |
566 | ! |
filter_panel_api = filter_panel_api |
567 |
)
|
|
568 | ! |
card$append_text("Plot", "header3") |
569 | ! |
card$append_plot(plot_r(), dim = pws$dim()) |
570 | ! |
if (!comment == "") { |
571 | ! |
card$append_text("Comment", "header3") |
572 | ! |
card$append_text(comment) |
573 |
}
|
|
574 | ! |
card$append_src(source_code_r()) |
575 | ! |
card
|
576 |
}
|
|
577 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
578 |
}
|
|
579 |
###
|
|
580 |
}) |
|
581 |
}
|
1 |
#' `teal` module: Variable browser
|
|
2 |
#'
|
|
3 |
#' Module provides provides a detailed summary and visualization of variable distributions
|
|
4 |
#' for `data.frame` objects, with interactive features to customize analysis.
|
|
5 |
#'
|
|
6 |
#' Numeric columns with fewer than 30 distinct values can be treated as either discrete
|
|
7 |
#' or continuous with a checkbox allowing users to switch how they are treated(if < 6 unique values
|
|
8 |
#' then the default is discrete, otherwise it is continuous).
|
|
9 |
#'
|
|
10 |
#' @inheritParams teal::module
|
|
11 |
#' @inheritParams shared_params
|
|
12 |
#' @param parent_dataname (`character(1)`) string specifying a parent dataset.
|
|
13 |
#' If it exists in `datanames` then an extra checkbox will be shown to
|
|
14 |
#' allow users to not show variables in other datasets which exist in this `dataname`.
|
|
15 |
#' This is typically used to remove `ADSL` columns in `CDISC` data.
|
|
16 |
#' In non `CDISC` data this can be ignored. Defaults to `"ADSL"`.
|
|
17 |
#' @param datasets_selected (`character`) `r lifecycle::badge("deprecated")` vector of datasets to show, please
|
|
18 |
#' use the `datanames` argument.
|
|
19 |
#'
|
|
20 |
#' @inherit shared_params return
|
|
21 |
#'
|
|
22 |
#' @examplesShinylive
|
|
23 |
#' library(teal.modules.general)
|
|
24 |
#' interactive <- function() TRUE
|
|
25 |
#' {{ next_example }}
|
|
26 |
# nolint start: line_length_linter.
|
|
27 |
#' @examples
|
|
28 |
# nolint end: line_length_linter.
|
|
29 |
#' # general data example
|
|
30 |
#' data <- teal_data()
|
|
31 |
#' data <- within(data, {
|
|
32 |
#' iris <- iris
|
|
33 |
#' mtcars <- mtcars
|
|
34 |
#' women <- women
|
|
35 |
#' faithful <- faithful
|
|
36 |
#' CO2 <- CO2
|
|
37 |
#' })
|
|
38 |
#'
|
|
39 |
#' app <- init(
|
|
40 |
#' data = data,
|
|
41 |
#' modules = modules(
|
|
42 |
#' tm_variable_browser(
|
|
43 |
#' label = "Variable browser"
|
|
44 |
#' )
|
|
45 |
#' )
|
|
46 |
#' )
|
|
47 |
#' if (interactive()) {
|
|
48 |
#' shinyApp(app$ui, app$server)
|
|
49 |
#' }
|
|
50 |
#'
|
|
51 |
#' @examplesShinylive
|
|
52 |
#' library(teal.modules.general)
|
|
53 |
#' interactive <- function() TRUE
|
|
54 |
#' {{ next_example }}
|
|
55 |
# nolint start: line_length_linter.
|
|
56 |
#' @examples
|
|
57 |
# nolint end: line_length_linter.
|
|
58 |
#' # CDISC example data
|
|
59 |
#' library(sparkline)
|
|
60 |
#' data <- teal_data()
|
|
61 |
#' data <- within(data, {
|
|
62 |
#' ADSL <- teal.data::rADSL
|
|
63 |
#' ADTTE <- teal.data::rADTTE
|
|
64 |
#' })
|
|
65 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
66 |
#'
|
|
67 |
#' app <- init(
|
|
68 |
#' data = data,
|
|
69 |
#' modules = modules(
|
|
70 |
#' tm_variable_browser(
|
|
71 |
#' label = "Variable browser"
|
|
72 |
#' )
|
|
73 |
#' )
|
|
74 |
#' )
|
|
75 |
#' if (interactive()) {
|
|
76 |
#' shinyApp(app$ui, app$server)
|
|
77 |
#' }
|
|
78 |
#'
|
|
79 |
#' @export
|
|
80 |
#'
|
|
81 |
tm_variable_browser <- function(label = "Variable Browser", |
|
82 |
datasets_selected = deprecated(), |
|
83 |
datanames = if (missing(datasets_selected)) "all" else datasets_selected, |
|
84 |
parent_dataname = "ADSL", |
|
85 |
pre_output = NULL, |
|
86 |
post_output = NULL, |
|
87 |
ggplot2_args = teal.widgets::ggplot2_args(), |
|
88 |
transformators = list()) { |
|
89 | ! |
message("Initializing tm_variable_browser") |
90 | ||
91 |
# Start of assertions
|
|
92 | ! |
checkmate::assert_string(label) |
93 | ! |
if (!missing(datasets_selected)) { |
94 | ! |
lifecycle::deprecate_soft( |
95 | ! |
when = "0.4.0", |
96 | ! |
what = "tm_variable_browser(datasets_selected)", |
97 | ! |
with = "tm_variable_browser(datanames)", |
98 | ! |
details = c( |
99 | ! |
"If both `datasets_selected` and `datanames` are set `datasets_selected` will be silently ignored.",
|
100 | ! |
i = 'Use `tm_variable_browser(datanames = "all")` to keep the previous behavior and avoid this warning.' |
101 |
)
|
|
102 |
)
|
|
103 |
}
|
|
104 | ! |
checkmate::assert_character(datanames, min.len = 0, min.chars = 1, null.ok = TRUE) |
105 | ! |
checkmate::assert_character(parent_dataname, min.len = 0, max.len = 1) |
106 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
107 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
108 | ! |
checkmate::assert_class(ggplot2_args, "ggplot2_args") |
109 |
# End of assertions
|
|
110 | ||
111 | ! |
datanames_module <- if (identical(datanames, "all") || is.null(datanames)) { |
112 | ! |
datanames
|
113 |
} else { |
|
114 | ! |
union(datanames, parent_dataname) |
115 |
}
|
|
116 | ||
117 | ! |
ans <- module( |
118 | ! |
label,
|
119 | ! |
server = srv_variable_browser, |
120 | ! |
ui = ui_variable_browser, |
121 | ! |
datanames = datanames_module, |
122 | ! |
server_args = list( |
123 | ! |
datanames = if (is.null(datanames)) "all" else datanames, |
124 | ! |
parent_dataname = parent_dataname, |
125 | ! |
ggplot2_args = ggplot2_args |
126 |
),
|
|
127 | ! |
ui_args = list( |
128 | ! |
pre_output = pre_output, |
129 | ! |
post_output = post_output |
130 |
),
|
|
131 | ! |
transformators = transformators |
132 |
)
|
|
133 |
# `shiny` inputs are stored properly but the majority of the module is state of `datatable` which is not stored.
|
|
134 | ! |
attr(ans, "teal_bookmarkable") <- NULL |
135 | ! |
ans
|
136 |
}
|
|
137 | ||
138 |
# UI function for the variable browser module
|
|
139 |
ui_variable_browser <- function(id, |
|
140 |
pre_output = NULL, |
|
141 |
post_output = NULL) { |
|
142 | ! |
ns <- NS(id) |
143 | ||
144 | ! |
tagList( |
145 | ! |
include_css_files("custom"), |
146 | ! |
shinyjs::useShinyjs(), |
147 | ! |
teal.widgets::standard_layout( |
148 | ! |
output = fluidRow( |
149 | ! |
htmlwidgets::getDependency("sparkline"), # needed for sparklines to work |
150 | ! |
column( |
151 | ! |
6,
|
152 |
# variable browser
|
|
153 | ! |
teal.widgets::white_small_well( |
154 | ! |
uiOutput(ns("ui_variable_browser")), |
155 | ! |
shinyjs::hidden({ |
156 | ! |
checkboxInput(ns("show_parent_vars"), "Show parent dataset variables", value = FALSE) |
157 |
}) |
|
158 |
)
|
|
159 |
),
|
|
160 | ! |
column( |
161 | ! |
6,
|
162 | ! |
teal.widgets::white_small_well( |
163 |
### Reporter
|
|
164 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
165 |
###
|
|
166 | ! |
tags$div( |
167 | ! |
class = "block", |
168 | ! |
uiOutput(ns("ui_histogram_display")) |
169 |
),
|
|
170 | ! |
tags$div( |
171 | ! |
class = "block", |
172 | ! |
uiOutput(ns("ui_numeric_display")) |
173 |
),
|
|
174 | ! |
teal.widgets::plot_with_settings_ui(ns("variable_plot")), |
175 | ! |
tags$br(), |
176 |
# input user-defined text size
|
|
177 | ! |
teal.widgets::panel_item( |
178 | ! |
title = "Plot settings", |
179 | ! |
collapsed = TRUE, |
180 | ! |
selectInput( |
181 | ! |
inputId = ns("ggplot_theme"), label = "ggplot2 theme", |
182 | ! |
choices = ggplot_themes, |
183 | ! |
selected = "grey" |
184 |
),
|
|
185 | ! |
fluidRow( |
186 | ! |
column(6, sliderInput( |
187 | ! |
inputId = ns("font_size"), label = "font size", |
188 | ! |
min = 5L, max = 30L, value = 15L, step = 1L, ticks = FALSE |
189 |
)), |
|
190 | ! |
column(6, sliderInput( |
191 | ! |
inputId = ns("label_rotation"), label = "rotate x labels", |
192 | ! |
min = 0L, max = 90L, value = 45L, step = 1, ticks = FALSE |
193 |
)) |
|
194 |
)
|
|
195 |
),
|
|
196 | ! |
tags$br(), |
197 | ! |
teal.widgets::get_dt_rows(ns("variable_summary_table"), ns("variable_summary_table_rows")), |
198 | ! |
DT::dataTableOutput(ns("variable_summary_table")) |
199 |
)
|
|
200 |
)
|
|
201 |
),
|
|
202 | ! |
pre_output = pre_output, |
203 | ! |
post_output = post_output |
204 |
)
|
|
205 |
)
|
|
206 |
}
|
|
207 | ||
208 |
# Server function for the variable browser module
|
|
209 |
srv_variable_browser <- function(id, |
|
210 |
data,
|
|
211 |
reporter,
|
|
212 |
filter_panel_api,
|
|
213 |
datanames, parent_dataname, ggplot2_args) { |
|
214 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
215 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
216 | ! |
checkmate::assert_class(data, "reactive") |
217 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
218 | ! |
moduleServer(id, function(input, output, session) { |
219 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
220 | ||
221 |
# if there are < this number of unique records then a numeric
|
|
222 |
# variable can be treated as a factor and all factors with < this groups
|
|
223 |
# have their values plotted
|
|
224 | ! |
.unique_records_for_factor <- 30 |
225 |
# if there are < this number of unique records then a numeric
|
|
226 |
# variable is by default treated as a factor
|
|
227 | ! |
.unique_records_default_as_factor <- 6 # nolint: object_length. |
228 | ||
229 | ! |
varname_numeric_as_factor <- reactiveValues() |
230 | ||
231 | ! |
datanames <- Filter(function(name) { |
232 | ! |
is.data.frame(isolate(data())[[name]]) |
233 | ! |
}, if (identical(datanames, "all")) names(isolate(data())) else datanames) |
234 | ||
235 | ! |
output$ui_variable_browser <- renderUI({ |
236 | ! |
ns <- session$ns |
237 | ! |
do.call( |
238 | ! |
tabsetPanel,
|
239 | ! |
c( |
240 | ! |
id = ns("tabset_panel"), |
241 | ! |
do.call( |
242 | ! |
tagList,
|
243 | ! |
lapply(datanames, function(dataname) { |
244 | ! |
tabPanel( |
245 | ! |
dataname,
|
246 | ! |
tags$div( |
247 | ! |
class = "mt-4", |
248 | ! |
textOutput(ns(paste0("dataset_summary_", dataname))) |
249 |
),
|
|
250 | ! |
tags$div( |
251 | ! |
class = "mt-4", |
252 | ! |
teal.widgets::get_dt_rows( |
253 | ! |
ns(paste0("variable_browser_", dataname)), |
254 | ! |
ns(paste0("variable_browser_", dataname, "_rows")) |
255 |
),
|
|
256 | ! |
DT::dataTableOutput(ns(paste0("variable_browser_", dataname)), width = "100%") |
257 |
)
|
|
258 |
)
|
|
259 |
}) |
|
260 |
)
|
|
261 |
)
|
|
262 |
)
|
|
263 |
}) |
|
264 | ||
265 |
# conditionally display checkbox
|
|
266 | ! |
shinyjs::toggle( |
267 | ! |
id = "show_parent_vars", |
268 | ! |
condition = length(parent_dataname) > 0 && parent_dataname %in% datanames |
269 |
)
|
|
270 | ||
271 | ! |
columns_names <- new.env() |
272 | ||
273 |
# plot_var$data holds the name of the currently selected dataset
|
|
274 |
# plot_var$variable[[<dataset_name>]] holds the name of the currently selected
|
|
275 |
# variable for dataset <dataset_name>
|
|
276 | ! |
plot_var <- reactiveValues(data = NULL, variable = list()) |
277 | ||
278 | ! |
establish_updating_selection(datanames, input, plot_var, columns_names) |
279 | ||
280 |
# validations
|
|
281 | ! |
validation_checks <- validate_input(input, plot_var, data) |
282 | ||
283 |
# data_for_analysis is a list with two elements: a column from a dataset and the column label
|
|
284 | ! |
plotted_data <- reactive({ |
285 | ! |
validation_checks() |
286 | ||
287 | ! |
get_plotted_data(input, plot_var, data) |
288 |
}) |
|
289 | ||
290 | ! |
treat_numeric_as_factor <- reactive({ |
291 | ! |
if (is_num_var_short(.unique_records_for_factor, input, plotted_data)) { |
292 | ! |
input$numeric_as_factor |
293 |
} else { |
|
294 | ! |
FALSE
|
295 |
}
|
|
296 |
}) |
|
297 | ||
298 | ! |
render_tabset_panel_content( |
299 | ! |
input = input, |
300 | ! |
output = output, |
301 | ! |
data = data, |
302 | ! |
datanames = datanames, |
303 | ! |
parent_dataname = parent_dataname, |
304 | ! |
columns_names = columns_names, |
305 | ! |
plot_var = plot_var |
306 |
)
|
|
307 |
# add used-defined text size to ggplot arguments passed from caller frame
|
|
308 | ! |
all_ggplot2_args <- reactive({ |
309 | ! |
user_text <- teal.widgets::ggplot2_args( |
310 | ! |
theme = list( |
311 | ! |
"text" = ggplot2::element_text(size = input[["font_size"]]), |
312 | ! |
"axis.text.x" = ggplot2::element_text(angle = input[["label_rotation"]], hjust = 1) |
313 |
)
|
|
314 |
)
|
|
315 | ! |
user_theme <- utils::getFromNamespace(sprintf("theme_%s", input[["ggplot_theme"]]), ns = "ggplot2") |
316 | ! |
user_theme <- user_theme() |
317 |
# temporary fix to circumvent assertion issue with resolve_ggplot2_args
|
|
318 |
# drop problematic elements
|
|
319 | ! |
user_theme <- user_theme[grep("strip.text.y.left", names(user_theme), fixed = TRUE, invert = TRUE)] |
320 | ||
321 | ! |
teal.widgets::resolve_ggplot2_args( |
322 | ! |
user_plot = user_text, |
323 | ! |
user_default = teal.widgets::ggplot2_args(theme = user_theme), |
324 | ! |
module_plot = ggplot2_args |
325 |
)
|
|
326 |
}) |
|
327 | ||
328 | ! |
output$ui_numeric_display <- renderUI({ |
329 | ! |
validation_checks() |
330 | ! |
dataname <- input$tabset_panel |
331 | ! |
varname <- plot_var$variable[[dataname]] |
332 | ! |
df <- data()[[dataname]] |
333 | ||
334 | ! |
numeric_ui <- tagList( |
335 | ! |
fluidRow( |
336 | ! |
tags$div( |
337 | ! |
class = "col-md-4", |
338 | ! |
tags$br(), |
339 | ! |
shinyWidgets::switchInput( |
340 | ! |
inputId = session$ns("display_density"), |
341 | ! |
label = "Show density", |
342 | ! |
value = `if`(is.null(isolate(input$display_density)), TRUE, isolate(input$display_density)), |
343 | ! |
width = "50%", |
344 | ! |
labelWidth = "100px", |
345 | ! |
handleWidth = "50px" |
346 |
)
|
|
347 |
),
|
|
348 | ! |
tags$div( |
349 | ! |
class = "col-md-4", |
350 | ! |
tags$br(), |
351 | ! |
shinyWidgets::switchInput( |
352 | ! |
inputId = session$ns("remove_outliers"), |
353 | ! |
label = "Remove outliers", |
354 | ! |
value = `if`(is.null(isolate(input$remove_outliers)), FALSE, isolate(input$remove_outliers)), |
355 | ! |
width = "50%", |
356 | ! |
labelWidth = "100px", |
357 | ! |
handleWidth = "50px" |
358 |
)
|
|
359 |
),
|
|
360 | ! |
tags$div( |
361 | ! |
class = "col-md-4", |
362 | ! |
uiOutput(session$ns("outlier_definition_slider_ui")) |
363 |
)
|
|
364 |
),
|
|
365 | ! |
tags$div( |
366 | ! |
class = "ml-4", |
367 | ! |
uiOutput(session$ns("ui_density_help")), |
368 | ! |
uiOutput(session$ns("ui_outlier_help")) |
369 |
)
|
|
370 |
)
|
|
371 | ||
372 | ! |
observeEvent(input$numeric_as_factor, ignoreInit = TRUE, { |
373 | ! |
varname_numeric_as_factor[[plot_var$variable[[dataname]]]] <- input$numeric_as_factor |
374 |
}) |
|
375 | ||
376 | ! |
if (is.numeric(df[[varname]])) { |
377 | ! |
unique_entries <- length(unique(df[[varname]])) |
378 | ! |
if (unique_entries < .unique_records_for_factor && unique_entries > 0) { |
379 | ! |
list( |
380 | ! |
checkboxInput( |
381 | ! |
session$ns("numeric_as_factor"), |
382 | ! |
"Treat variable as factor",
|
383 | ! |
value = `if`( |
384 | ! |
is.null(varname_numeric_as_factor[[varname]]), |
385 | ! |
unique_entries < .unique_records_default_as_factor, |
386 | ! |
varname_numeric_as_factor[[varname]] |
387 |
)
|
|
388 |
),
|
|
389 | ! |
conditionalPanel("!input.numeric_as_factor", ns = session$ns, numeric_ui) |
390 |
)
|
|
391 | ! |
} else if (unique_entries > 0) { |
392 | ! |
numeric_ui
|
393 |
}
|
|
394 |
} else { |
|
395 | ! |
NULL
|
396 |
}
|
|
397 |
}) |
|
398 | ||
399 | ! |
output$ui_histogram_display <- renderUI({ |
400 | ! |
validation_checks() |
401 | ! |
dataname <- input$tabset_panel |
402 | ! |
varname <- plot_var$variable[[dataname]] |
403 | ! |
df <- data()[[dataname]] |
404 | ||
405 | ! |
numeric_ui <- tagList(fluidRow( |
406 | ! |
tags$div( |
407 | ! |
class = "col-md-4", |
408 | ! |
shinyWidgets::switchInput( |
409 | ! |
inputId = session$ns("remove_NA_hist"), |
410 | ! |
label = "Remove NA values", |
411 | ! |
value = FALSE, |
412 | ! |
width = "50%", |
413 | ! |
labelWidth = "100px", |
414 | ! |
handleWidth = "50px" |
415 |
)
|
|
416 |
)
|
|
417 |
)) |
|
418 | ||
419 | ! |
var <- df[[varname]] |
420 | ! |
if (anyNA(var) && (is.factor(var) || is.character(var) || is.logical(var))) { |
421 | ! |
groups <- unique(as.character(var)) |
422 | ! |
len_groups <- length(groups) |
423 | ! |
if (len_groups >= .unique_records_for_factor) { |
424 | ! |
NULL
|
425 |
} else { |
|
426 | ! |
numeric_ui
|
427 |
}
|
|
428 |
} else { |
|
429 | ! |
NULL
|
430 |
}
|
|
431 |
}) |
|
432 | ||
433 | ! |
output$outlier_definition_slider_ui <- renderUI({ |
434 | ! |
req(input$remove_outliers) |
435 | ! |
sliderInput( |
436 | ! |
inputId = session$ns("outlier_definition_slider"), |
437 | ! |
tags$div( |
438 | ! |
class = "teal-tooltip", |
439 | ! |
tagList( |
440 | ! |
"Outlier definition:",
|
441 | ! |
icon("circle-info"), |
442 | ! |
tags$span( |
443 | ! |
class = "tooltiptext", |
444 | ! |
paste( |
445 | ! |
"Use the slider to choose the cut-off value to define outliers; the larger the value the",
|
446 | ! |
"further below Q1/above Q3 points have to be in order to be classed as outliers"
|
447 |
)
|
|
448 |
)
|
|
449 |
)
|
|
450 |
),
|
|
451 | ! |
min = 1, |
452 | ! |
max = 5, |
453 | ! |
value = 3, |
454 | ! |
step = 0.5 |
455 |
)
|
|
456 |
}) |
|
457 | ||
458 | ! |
output$ui_density_help <- renderUI({ |
459 | ! |
req(is.logical(input$display_density)) |
460 | ! |
if (input$display_density) { |
461 | ! |
tags$small(helpText(paste( |
462 | ! |
"Kernel density estimation with gaussian kernel",
|
463 | ! |
"and bandwidth function bw.nrd0 (R default)"
|
464 |
))) |
|
465 |
} else { |
|
466 | ! |
NULL
|
467 |
}
|
|
468 |
}) |
|
469 | ||
470 | ! |
output$ui_outlier_help <- renderUI({ |
471 | ! |
req(is.logical(input$remove_outliers), input$outlier_definition_slider) |
472 | ! |
if (input$remove_outliers) { |
473 | ! |
tags$small( |
474 | ! |
helpText( |
475 | ! |
withMathJax(paste0( |
476 | ! |
"Outlier data points (\\( X \\lt Q1 - ", input$outlier_definition_slider, "\\times IQR \\) or |
477 | ! |
\\(Q3 + ", input$outlier_definition_slider, "\\times IQR \\lt X\\)) |
478 | ! |
have not been displayed on the graph and will not be used for any kernel density estimations, ", |
479 | ! |
"although their values remain in the statisics table below."
|
480 |
)) |
|
481 |
)
|
|
482 |
)
|
|
483 |
} else { |
|
484 | ! |
NULL
|
485 |
}
|
|
486 |
}) |
|
487 | ||
488 | ||
489 | ! |
variable_plot_r <- reactive({ |
490 | ! |
display_density <- `if`(is.null(input$display_density), FALSE, input$display_density) |
491 | ! |
remove_outliers <- `if`(is.null(input$remove_outliers), FALSE, input$remove_outliers) |
492 | ||
493 | ! |
if (remove_outliers) { |
494 | ! |
req(input$outlier_definition_slider) |
495 | ! |
outlier_definition <- as.numeric(input$outlier_definition_slider) |
496 |
} else { |
|
497 | ! |
outlier_definition <- 0 |
498 |
}
|
|
499 | ||
500 | ! |
plot_var_summary( |
501 | ! |
var = plotted_data()$data, |
502 | ! |
var_lab = plotted_data()$var_description, |
503 | ! |
wrap_character = 15, |
504 | ! |
numeric_as_factor = treat_numeric_as_factor(), |
505 | ! |
remove_NA_hist = input$remove_NA_hist, |
506 | ! |
display_density = display_density, |
507 | ! |
outlier_definition = outlier_definition, |
508 | ! |
records_for_factor = .unique_records_for_factor, |
509 | ! |
ggplot2_args = all_ggplot2_args() |
510 |
)
|
|
511 |
}) |
|
512 | ||
513 | ! |
pws <- teal.widgets::plot_with_settings_srv( |
514 | ! |
id = "variable_plot", |
515 | ! |
plot_r = variable_plot_r, |
516 | ! |
height = c(500, 200, 2000) |
517 |
)
|
|
518 | ||
519 | ! |
output$variable_summary_table <- DT::renderDataTable({ |
520 | ! |
var_summary_table( |
521 | ! |
plotted_data()$data, |
522 | ! |
treat_numeric_as_factor(), |
523 | ! |
input$variable_summary_table_rows, |
524 | ! |
if (!is.null(input$remove_outliers) && input$remove_outliers) { |
525 | ! |
req(input$outlier_definition_slider) |
526 | ! |
as.numeric(input$outlier_definition_slider) |
527 |
} else { |
|
528 | ! |
0
|
529 |
}
|
|
530 |
)
|
|
531 |
}) |
|
532 | ||
533 |
### REPORTER
|
|
534 | ! |
if (with_reporter) { |
535 | ! |
card_fun <- function(comment) { |
536 | ! |
card <- teal::TealReportCard$new() |
537 | ! |
card$set_name("Variable Browser Plot") |
538 | ! |
card$append_text("Variable Browser Plot", "header2") |
539 | ! |
if (with_filter) card$append_fs(filter_panel_api$get_filter_state()) |
540 | ! |
card$append_text("Plot", "header3") |
541 | ! |
card$append_plot(variable_plot_r(), dim = pws$dim()) |
542 | ! |
if (!comment == "") { |
543 | ! |
card$append_text("Comment", "header3") |
544 | ! |
card$append_text(comment) |
545 |
}
|
|
546 | ! |
card
|
547 |
}
|
|
548 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
549 |
}
|
|
550 |
###
|
|
551 |
}) |
|
552 |
}
|
|
553 | ||
554 |
#' Summarize NAs.
|
|
555 |
#'
|
|
556 |
#' Summarizes occurrence of missing values in vector.
|
|
557 |
#' @param x vector of any type and length
|
|
558 |
#' @return Character string describing `NA` occurrence.
|
|
559 |
#' @keywords internal
|
|
560 |
var_missings_info <- function(x) { |
|
561 | ! |
sprintf("%s [%s%%]", sum(is.na(x)), round(mean(is.na(x) * 100), 2)) |
562 |
}
|
|
563 | ||
564 |
#' Summarizes variable
|
|
565 |
#'
|
|
566 |
#' Creates html summary with statistics relevant to data type. For numeric values it returns central
|
|
567 |
#' tendency measures, for factor returns level counts, for Date date range, for other just
|
|
568 |
#' number of levels.
|
|
569 |
#'
|
|
570 |
#' @param x vector of any type
|
|
571 |
#' @param numeric_as_factor `logical` should the numeric variable be treated as a factor
|
|
572 |
#' @param dt_rows `numeric` current/latest `DT` page length
|
|
573 |
#' @param outlier_definition If 0 no outliers are removed, otherwise
|
|
574 |
#' outliers (those more than `outlier_definition*IQR below/above Q1/Q3` be removed)
|
|
575 |
#' @return text with simple statistics.
|
|
576 |
#' @keywords internal
|
|
577 |
var_summary_table <- function(x, numeric_as_factor, dt_rows, outlier_definition) { |
|
578 | ! |
if (is.null(dt_rows)) { |
579 | ! |
dt_rows <- 10 |
580 |
}
|
|
581 | ! |
if (is.numeric(x) && !numeric_as_factor) { |
582 | ! |
req(!any(is.infinite(x))) |
583 | ||
584 | ! |
x <- remove_outliers_from(x, outlier_definition) |
585 | ||
586 | ! |
qvals <- round(stats::quantile(x, na.rm = TRUE, probs = c(0.25, 0.5, 0.75), type = 2), 2) |
587 |
# classical central tendency measures
|
|
588 | ||
589 | ! |
summary <- |
590 | ! |
data.frame( |
591 | ! |
Statistic = c("min", "Q1", "median", "mean", "Q3", "max", "sd", "n"), |
592 | ! |
Value = c( |
593 | ! |
round(min(x, na.rm = TRUE), 2), |
594 | ! |
qvals[1], |
595 | ! |
qvals[2], |
596 | ! |
round(mean(x, na.rm = TRUE), 2), |
597 | ! |
qvals[3], |
598 | ! |
round(max(x, na.rm = TRUE), 2), |
599 | ! |
round(stats::sd(x, na.rm = TRUE), 2), |
600 | ! |
length(x[!is.na(x)]) |
601 |
)
|
|
602 |
)
|
|
603 | ||
604 | ! |
DT::datatable(summary, rownames = FALSE, options = list(dom = "<t>", pageLength = dt_rows)) |
605 | ! |
} else if (is.factor(x) || is.character(x) || (is.numeric(x) && numeric_as_factor) || is.logical(x)) { |
606 |
# make sure factor is ordered numeric
|
|
607 | ! |
if (is.numeric(x)) { |
608 | ! |
x <- factor(x, levels = sort(unique(x))) |
609 |
}
|
|
610 | ||
611 | ! |
level_counts <- table(x) |
612 | ! |
max_levels_signif <- nchar(level_counts) |
613 | ||
614 | ! |
if (!all(is.na(x))) { |
615 | ! |
levels <- names(level_counts) |
616 | ! |
counts <- sprintf( |
617 | ! |
"%s [%.2f%%]",
|
618 | ! |
format(level_counts, width = max_levels_signif), prop.table(level_counts) * 100 |
619 |
)
|
|
620 |
} else { |
|
621 | ! |
levels <- character(0) |
622 | ! |
counts <- numeric(0) |
623 |
}
|
|
624 | ||
625 | ! |
summary <- data.frame( |
626 | ! |
Level = levels, |
627 | ! |
Count = counts, |
628 | ! |
stringsAsFactors = FALSE |
629 |
)
|
|
630 | ||
631 |
# sort the dataset in decreasing order of counts (needed as character variables default to alphabetical)
|
|
632 | ! |
summary <- summary[order(summary$Count, decreasing = TRUE), ] |
633 | ||
634 | ! |
dom_opts <- if (nrow(summary) <= 10) { |
635 | ! |
"<t>"
|
636 |
} else { |
|
637 | ! |
"<lf<t>ip>"
|
638 |
}
|
|
639 | ! |
DT::datatable(summary, rownames = FALSE, options = list(dom = dom_opts, pageLength = dt_rows)) |
640 | ! |
} else if (inherits(x, "Date") || inherits(x, "POSIXct") || inherits(x, "POSIXlt")) { |
641 | ! |
summary <- |
642 | ! |
data.frame( |
643 | ! |
Statistic = c("min", "median", "max"), |
644 | ! |
Value = c( |
645 | ! |
min(x, na.rm = TRUE), |
646 | ! |
stats::median(x, na.rm = TRUE), |
647 | ! |
max(x, na.rm = TRUE) |
648 |
)
|
|
649 |
)
|
|
650 | ! |
DT::datatable(summary, rownames = FALSE, options = list(dom = "<t>", pageLength = dt_rows)) |
651 |
} else { |
|
652 | ! |
NULL
|
653 |
}
|
|
654 |
}
|
|
655 | ||
656 |
#' Plot variable
|
|
657 |
#'
|
|
658 |
#' Creates summary plot with statistics relevant to data type.
|
|
659 |
#'
|
|
660 |
#' @inheritParams shared_params
|
|
661 |
#' @param var vector of any type to be plotted. For numeric variables it produces histogram with
|
|
662 |
#' density line, for factors it creates frequency plot
|
|
663 |
#' @param var_lab text describing selected variable to be displayed on the plot
|
|
664 |
#' @param wrap_character (`numeric`) number of characters at which to wrap text values of `var`
|
|
665 |
#' @param numeric_as_factor (`logical`) should the numeric variable be treated as a factor
|
|
666 |
#' @param display_density (`logical`) should density estimation be displayed for numeric values
|
|
667 |
#' @param remove_NA_hist (`logical`) should `NA` values be removed for histogram of factor like variables
|
|
668 |
#' @param outlier_definition if 0 no outliers are removed, otherwise
|
|
669 |
#' outliers (those more than outlier_definition*IQR below/above Q1/Q3 be removed)
|
|
670 |
#' @param records_for_factor (`numeric`) if the number of factor levels is >= than this value then
|
|
671 |
#' a graph of the factors isn't shown, only a list of values
|
|
672 |
#'
|
|
673 |
#' @return plot
|
|
674 |
#' @keywords internal
|
|
675 |
plot_var_summary <- function(var, |
|
676 |
var_lab,
|
|
677 |
wrap_character = NULL, |
|
678 |
numeric_as_factor,
|
|
679 |
display_density = is.numeric(var), |
|
680 |
remove_NA_hist = FALSE, # nolint: object_name. |
|
681 |
outlier_definition,
|
|
682 |
records_for_factor,
|
|
683 |
ggplot2_args) { |
|
684 | ! |
checkmate::assert_character(var_lab) |
685 | ! |
checkmate::assert_numeric(wrap_character, null.ok = TRUE) |
686 | ! |
checkmate::assert_flag(numeric_as_factor) |
687 | ! |
checkmate::assert_flag(display_density) |
688 | ! |
checkmate::assert_logical(remove_NA_hist, null.ok = TRUE) |
689 | ! |
checkmate::assert_number(outlier_definition, lower = 0, finite = TRUE) |
690 | ! |
checkmate::assert_integerish(records_for_factor, lower = 0, len = 1, any.missing = FALSE) |
691 | ! |
checkmate::assert_class(ggplot2_args, "ggplot2_args") |
692 | ||
693 | ! |
grid::grid.newpage() |
694 | ||
695 | ! |
plot_main <- if (is.factor(var) || is.character(var) || is.logical(var)) { |
696 | ! |
groups <- unique(as.character(var)) |
697 | ! |
len_groups <- length(groups) |
698 | ! |
if (len_groups >= records_for_factor) { |
699 | ! |
grid::textGrob( |
700 | ! |
sprintf( |
701 | ! |
"%s unique values\n%s:\n %s\n ...\n %s",
|
702 | ! |
len_groups,
|
703 | ! |
var_lab,
|
704 | ! |
paste(utils::head(groups), collapse = ",\n "), |
705 | ! |
paste(utils::tail(groups), collapse = ",\n ") |
706 |
),
|
|
707 | ! |
x = grid::unit(1, "line"), |
708 | ! |
y = grid::unit(1, "npc") - grid::unit(1, "line"), |
709 | ! |
just = c("left", "top") |
710 |
)
|
|
711 |
} else { |
|
712 | ! |
if (!is.null(wrap_character)) { |
713 | ! |
var <- stringr::str_wrap(var, width = wrap_character) |
714 |
}
|
|
715 | ! |
var <- if (isTRUE(remove_NA_hist)) as.vector(stats::na.omit(var)) else var |
716 | ! |
ggplot2::ggplot(data.frame(var), ggplot2::aes(x = forcats::fct_infreq(as.factor(var)))) + |
717 | ! |
ggplot2::geom_bar( |
718 | ! |
stat = "count", ggplot2::aes(fill = ifelse(is.na(var), "withcolor", "")), show.legend = FALSE |
719 |
) + |
|
720 | ! |
ggplot2::scale_fill_manual(values = c("gray50", "tan")) |
721 |
}
|
|
722 | ! |
} else if (is.numeric(var)) { |
723 | ! |
validate(need(any(!is.na(var)), "No data left to visualize.")) |
724 | ||
725 |
# Filter out NA
|
|
726 | ! |
var <- var[which(!is.na(var))] |
727 | ||
728 | ! |
validate(need(!any(is.infinite(var)), "Cannot display graph when data includes infinite values")) |
729 | ||
730 | ! |
if (numeric_as_factor) { |
731 | ! |
var <- factor(var) |
732 | ! |
ggplot2::ggplot(NULL, ggplot2::aes(x = var)) + |
733 | ! |
ggplot2::geom_histogram(stat = "count") |
734 |
} else { |
|
735 |
# remove outliers
|
|
736 | ! |
if (outlier_definition != 0) { |
737 | ! |
number_records <- length(var) |
738 | ! |
var <- remove_outliers_from(var, outlier_definition) |
739 | ! |
number_outliers <- number_records - length(var) |
740 | ! |
outlier_text <- paste0( |
741 | ! |
number_outliers, " outliers (", |
742 | ! |
round(number_outliers / number_records * 100, 2), |
743 | ! |
"% of non-missing records) not shown"
|
744 |
)
|
|
745 | ! |
validate(need( |
746 | ! |
length(var) > 1, |
747 | ! |
"At least two data points must remain after removing outliers for this graph to be displayed"
|
748 |
)) |
|
749 |
}
|
|
750 |
## histogram
|
|
751 | ! |
binwidth <- get_bin_width(var) |
752 | ! |
p <- ggplot2::ggplot(data = data.frame(var = var), ggplot2::aes(x = var, y = ggplot2::after_stat(count))) + |
753 | ! |
ggplot2::geom_histogram(binwidth = binwidth) + |
754 | ! |
ggplot2::scale_y_continuous( |
755 | ! |
sec.axis = ggplot2::sec_axis( |
756 | ! |
trans = ~ . / nrow(data.frame(var = var)), |
757 | ! |
labels = scales::percent, |
758 | ! |
name = "proportion (in %)" |
759 |
)
|
|
760 |
)
|
|
761 | ||
762 | ! |
if (display_density) { |
763 | ! |
p <- p + ggplot2::geom_density(ggplot2::aes(y = ggplot2::after_stat(count * binwidth))) |
764 |
}
|
|
765 | ||
766 | ! |
if (outlier_definition != 0) { |
767 | ! |
p <- p + ggplot2::annotate( |
768 | ! |
geom = "text", |
769 | ! |
label = outlier_text, |
770 | ! |
x = Inf, y = Inf, |
771 | ! |
hjust = 1.02, vjust = 1.2, |
772 | ! |
color = "black", |
773 |
# explicitly modify geom text size according
|
|
774 | ! |
size = ggplot2_args[["theme"]][["text"]][["size"]] / 3.5 |
775 |
)
|
|
776 |
}
|
|
777 | ! |
p
|
778 |
}
|
|
779 | ! |
} else if (inherits(var, "Date") || inherits(var, "POSIXct") || inherits(var, "POSIXlt")) { |
780 | ! |
var_num <- as.numeric(var) |
781 | ! |
binwidth <- get_bin_width(var_num, 1) |
782 | ! |
p <- ggplot2::ggplot(data = data.frame(var = var), ggplot2::aes(x = var, y = ggplot2::after_stat(count))) + |
783 | ! |
ggplot2::geom_histogram(binwidth = binwidth) |
784 |
} else { |
|
785 | ! |
grid::textGrob( |
786 | ! |
paste(strwrap( |
787 | ! |
utils::capture.output(utils::str(var)), |
788 | ! |
width = .9 * grid::convertWidth(grid::unit(1, "npc"), "char", TRUE) |
789 | ! |
), collapse = "\n"), |
790 | ! |
x = grid::unit(1, "line"), y = grid::unit(1, "npc") - grid::unit(1, "line"), just = c("left", "top") |
791 |
)
|
|
792 |
}
|
|
793 | ||
794 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
795 | ! |
labs = list(x = var_lab) |
796 |
)
|
|
797 |
###
|
|
798 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
799 | ! |
ggplot2_args,
|
800 | ! |
module_plot = dev_ggplot2_args |
801 |
)
|
|
802 | ||
803 | ! |
if (is.ggplot(plot_main)) { |
804 | ! |
if (is.numeric(var) && !numeric_as_factor) { |
805 |
# numeric not as factor
|
|
806 | ! |
plot_main <- plot_main + |
807 | ! |
theme_light() + |
808 | ! |
list( |
809 | ! |
labs = do.call("labs", all_ggplot2_args$labs), |
810 | ! |
theme = do.call("theme", all_ggplot2_args$theme) |
811 |
)
|
|
812 |
} else { |
|
813 |
# factor low number of levels OR numeric as factor OR Date
|
|
814 | ! |
plot_main <- plot_main + |
815 | ! |
theme_light() + |
816 | ! |
list( |
817 | ! |
labs = do.call("labs", all_ggplot2_args$labs), |
818 | ! |
theme = do.call("theme", all_ggplot2_args$theme) |
819 |
)
|
|
820 |
}
|
|
821 | ! |
plot_main <- ggplot2::ggplotGrob(plot_main) |
822 |
}
|
|
823 | ||
824 | ! |
grid::grid.draw(plot_main) |
825 | ! |
plot_main
|
826 |
}
|
|
827 | ||
828 |
is_num_var_short <- function(.unique_records_for_factor, input, data_for_analysis) { |
|
829 | ! |
length(unique(data_for_analysis()$data)) < .unique_records_for_factor && !is.null(input$numeric_as_factor) |
830 |
}
|
|
831 | ||
832 |
#' Validates the variable browser inputs
|
|
833 |
#'
|
|
834 |
#' @param input (`session$input`) the `shiny` session input
|
|
835 |
#' @param plot_var (`list`) list of a data frame and an array of variable names
|
|
836 |
#' @param data (`teal_data`) the datasets passed to the module
|
|
837 |
#'
|
|
838 |
#' @returns `logical` TRUE if validations pass; a `shiny` validation error otherwise
|
|
839 |
#' @keywords internal
|
|
840 |
validate_input <- function(input, plot_var, data) { |
|
841 | ! |
reactive({ |
842 | ! |
dataset_name <- req(input$tabset_panel) |
843 | ! |
varname <- plot_var$variable[[dataset_name]] |
844 | ||
845 | ! |
validate(need(dataset_name, "No data selected")) |
846 | ! |
validate(need(varname, "No variable selected")) |
847 | ! |
df <- data()[[dataset_name]] |
848 | ! |
teal::validate_has_data(df, 1) |
849 | ! |
teal::validate_has_variable(varname = varname, data = df, "Variable not available") |
850 | ||
851 | ! |
TRUE
|
852 |
}) |
|
853 |
}
|
|
854 | ||
855 |
get_plotted_data <- function(input, plot_var, data) { |
|
856 | ! |
dataset_name <- input$tabset_panel |
857 | ! |
varname <- plot_var$variable[[dataset_name]] |
858 | ! |
df <- data()[[dataset_name]] |
859 | ||
860 | ! |
var_description <- teal.data::col_labels(df)[[varname]] |
861 | ! |
list(data = df[[varname]], var_description = var_description) |
862 |
}
|
|
863 | ||
864 |
#' Renders the left-hand side `tabset` panel of the module
|
|
865 |
#'
|
|
866 |
#' @param datanames (`character`) the name of the dataset
|
|
867 |
#' @param parent_dataname (`character`) the name of a parent `dataname` to filter out variables from
|
|
868 |
#' @param data (`teal_data`) the object containing all datasets
|
|
869 |
#' @param input (`session$input`) the `shiny` session input
|
|
870 |
#' @param output (`session$output`) the `shiny` session output
|
|
871 |
#' @param columns_names (`environment`) the environment containing bindings for each dataset
|
|
872 |
#' @param plot_var (`list`) the list containing the currently selected dataset (tab) and its column names
|
|
873 |
#' @keywords internal
|
|
874 |
render_tabset_panel_content <- function(datanames, parent_dataname, output, data, input, columns_names, plot_var) { |
|
875 | ! |
lapply(datanames, render_single_tab, |
876 | ! |
input = input, |
877 | ! |
output = output, |
878 | ! |
data = data, |
879 | ! |
parent_dataname = parent_dataname, |
880 | ! |
columns_names = columns_names, |
881 | ! |
plot_var = plot_var |
882 |
)
|
|
883 |
}
|
|
884 | ||
885 |
#' Renders a single tab in the left-hand side tabset panel
|
|
886 |
#'
|
|
887 |
#' Renders a single tab in the left-hand side tabset panel. The rendered tab contains
|
|
888 |
#' information about one dataset out of many presented in the module.
|
|
889 |
#'
|
|
890 |
#' @param dataset_name (`character`) the name of the dataset contained in the rendered tab
|
|
891 |
#' @param parent_dataname (`character`) the name of a parent `dataname` to filter out variables from
|
|
892 |
#' @inheritParams render_tabset_panel_content
|
|
893 |
#' @keywords internal
|
|
894 |
render_single_tab <- function(dataset_name, parent_dataname, output, data, input, columns_names, plot_var) { |
|
895 | ! |
render_tab_header(dataset_name, output, data) |
896 | ||
897 | ! |
render_tab_table( |
898 | ! |
dataset_name = dataset_name, |
899 | ! |
parent_dataname = parent_dataname, |
900 | ! |
output = output, |
901 | ! |
data = data, |
902 | ! |
input = input, |
903 | ! |
columns_names = columns_names, |
904 | ! |
plot_var = plot_var |
905 |
)
|
|
906 |
}
|
|
907 | ||
908 |
#' Renders the text headlining a single tab in the left-hand side tabset panel
|
|
909 |
#'
|
|
910 |
#' @param dataset_name (`character`) the name of the dataset of the tab
|
|
911 |
#' @inheritParams render_tabset_panel_content
|
|
912 |
#' @keywords internal
|
|
913 |
render_tab_header <- function(dataset_name, output, data) { |
|
914 | ! |
dataset_ui_id <- paste0("dataset_summary_", dataset_name) |
915 | ! |
output[[dataset_ui_id]] <- renderText({ |
916 | ! |
df <- data()[[dataset_name]] |
917 | ! |
join_keys <- teal.data::join_keys(data()) |
918 | ! |
if (!is.null(join_keys)) { |
919 | ! |
key <- teal.data::join_keys(data())[dataset_name, dataset_name] |
920 |
} else { |
|
921 | ! |
key <- NULL |
922 |
}
|
|
923 | ! |
sprintf( |
924 | ! |
"Dataset with %s unique key rows and %s variables",
|
925 | ! |
nrow(unique(`if`(length(key) > 0, df[, key, drop = FALSE], df))), |
926 | ! |
ncol(df) |
927 |
)
|
|
928 |
}) |
|
929 |
}
|
|
930 | ||
931 |
#' Renders the table for a single dataset in the left-hand side tabset panel
|
|
932 |
#'
|
|
933 |
#' The table contains column names, column labels,
|
|
934 |
#' small summary about NA values and `sparkline` (if appropriate).
|
|
935 |
#'
|
|
936 |
#' @param dataset_name (`character`) the name of the dataset
|
|
937 |
#' @param parent_dataname (`character`) the name of a parent `dataname` to filter out variables from
|
|
938 |
#' @inheritParams render_tabset_panel_content
|
|
939 |
#' @keywords internal
|
|
940 |
render_tab_table <- function(dataset_name, parent_dataname, output, data, input, columns_names, plot_var) { |
|
941 | ! |
table_ui_id <- paste0("variable_browser_", dataset_name) |
942 | ||
943 | ! |
output[[table_ui_id]] <- DT::renderDataTable({ |
944 | ! |
df <- data()[[dataset_name]] |
945 | ||
946 | ! |
get_vars_df <- function(input, dataset_name, parent_name, data) { |
947 | ! |
data_cols <- colnames(df) |
948 | ! |
if (isTRUE(input$show_parent_vars)) { |
949 | ! |
data_cols
|
950 | ! |
} else if (dataset_name != parent_name && parent_name %in% names(data)) { |
951 | ! |
setdiff(data_cols, colnames(data()[[parent_name]])) |
952 |
} else { |
|
953 | ! |
data_cols
|
954 |
}
|
|
955 |
}
|
|
956 | ||
957 | ! |
if (length(parent_dataname) > 0) { |
958 | ! |
df_vars <- get_vars_df(input, dataset_name, parent_dataname, data) |
959 | ! |
df <- df[df_vars] |
960 |
}
|
|
961 | ||
962 | ! |
if (is.null(df) || ncol(df) == 0) { |
963 | ! |
columns_names[[dataset_name]] <- character(0) |
964 | ! |
df_output <- data.frame( |
965 | ! |
Type = character(0), |
966 | ! |
Variable = character(0), |
967 | ! |
Label = character(0), |
968 | ! |
Missings = character(0), |
969 | ! |
Sparklines = character(0), |
970 | ! |
stringsAsFactors = FALSE |
971 |
)
|
|
972 |
} else { |
|
973 |
# extract data variable labels
|
|
974 | ! |
labels <- teal.data::col_labels(df) |
975 | ||
976 | ! |
columns_names[[dataset_name]] <- names(labels) |
977 | ||
978 |
# calculate number of missing values
|
|
979 | ! |
missings <- vapply( |
980 | ! |
df,
|
981 | ! |
var_missings_info,
|
982 | ! |
FUN.VALUE = character(1), |
983 | ! |
USE.NAMES = FALSE |
984 |
)
|
|
985 | ||
986 |
# get icons proper for the data types
|
|
987 | ! |
icons <- vapply(df, function(x) class(x)[1L], character(1L)) |
988 | ||
989 | ! |
join_keys <- teal.data::join_keys(data()) |
990 | ! |
if (!is.null(join_keys)) { |
991 | ! |
icons[intersect(join_keys[dataset_name, dataset_name], colnames(df))] <- "primary_key" |
992 |
}
|
|
993 | ! |
icons <- variable_type_icons(icons) |
994 | ||
995 |
# generate sparklines
|
|
996 | ! |
sparklines_html <- vapply( |
997 | ! |
df,
|
998 | ! |
create_sparklines,
|
999 | ! |
FUN.VALUE = character(1), |
1000 | ! |
USE.NAMES = FALSE |
1001 |
)
|
|
1002 | ||
1003 | ! |
df_output <- data.frame( |
1004 | ! |
Type = icons, |
1005 | ! |
Variable = names(labels), |
1006 | ! |
Label = labels, |
1007 | ! |
Missings = missings, |
1008 | ! |
Sparklines = sparklines_html, |
1009 | ! |
stringsAsFactors = FALSE |
1010 |
)
|
|
1011 |
}
|
|
1012 | ||
1013 |
# Select row 1 as default / fallback
|
|
1014 | ! |
selected_ix <- 1 |
1015 |
# Define starting page index (base-0 index of the first item on page
|
|
1016 |
# note: in many cases it's not the item itself
|
|
1017 | ! |
selected_page_ix <- 0 |
1018 | ||
1019 |
# Retrieve current selected variable if any
|
|
1020 | ! |
isolated_variable <- isolate(plot_var$variable[[dataset_name]]) |
1021 | ||
1022 | ! |
if (!is.null(isolated_variable)) { |
1023 | ! |
index <- which(columns_names[[dataset_name]] == isolated_variable)[1] |
1024 | ! |
if (!is.null(index) && !is.na(index) && length(index) > 0) selected_ix <- index |
1025 |
}
|
|
1026 | ||
1027 |
# Retrieve the index of the first item of the current page
|
|
1028 |
# it works with varying number of entries on the page (10, 25, ...)
|
|
1029 | ! |
table_id_sel <- paste0("variable_browser_", dataset_name, "_state") |
1030 | ! |
dt_state <- isolate(input[[table_id_sel]]) |
1031 | ! |
if (selected_ix != 1 && !is.null(dt_state)) { |
1032 | ! |
selected_page_ix <- floor(selected_ix / dt_state$length) * dt_state$length |
1033 |
}
|
|
1034 | ||
1035 | ! |
DT::datatable( |
1036 | ! |
df_output,
|
1037 | ! |
escape = FALSE, |
1038 | ! |
rownames = FALSE, |
1039 | ! |
selection = list(mode = "single", target = "row", selected = selected_ix), |
1040 | ! |
options = list( |
1041 | ! |
fnDrawCallback = htmlwidgets::JS("function() { HTMLWidgets.staticRender(); }"), |
1042 | ! |
pageLength = input[[paste0(table_ui_id, "_rows")]], |
1043 | ! |
displayStart = selected_page_ix |
1044 |
)
|
|
1045 |
)
|
|
1046 |
}) |
|
1047 |
}
|
|
1048 | ||
1049 |
#' Creates observers updating the currently selected column
|
|
1050 |
#'
|
|
1051 |
#' The created observers update the column currently selected in the left-hand side
|
|
1052 |
#' tabset panel.
|
|
1053 |
#'
|
|
1054 |
#' @note
|
|
1055 |
#' Creates an observer for each dataset (each tab in the tabset panel).
|
|
1056 |
#'
|
|
1057 |
#' @inheritParams render_tabset_panel_content
|
|
1058 |
#' @keywords internal
|
|
1059 |
establish_updating_selection <- function(datanames, input, plot_var, columns_names) { |
|
1060 | ! |
lapply(datanames, function(dataset_name) { |
1061 | ! |
table_ui_id <- paste0("variable_browser_", dataset_name) |
1062 | ! |
table_id_sel <- paste0(table_ui_id, "_rows_selected") |
1063 | ! |
observeEvent(input[[table_id_sel]], { |
1064 | ! |
plot_var$data <- dataset_name |
1065 | ! |
plot_var$variable[[dataset_name]] <- columns_names[[dataset_name]][input[[table_id_sel]]] |
1066 |
}) |
|
1067 |
}) |
|
1068 |
}
|
|
1069 | ||
1070 |
get_bin_width <- function(x_vec, scaling_factor = 2) { |
|
1071 | ! |
x_vec <- x_vec[!is.na(x_vec)] |
1072 | ! |
qntls <- stats::quantile(x_vec, probs = c(0.1, 0.25, 0.75, 0.9), type = 2) |
1073 | ! |
iqr <- qntls[3] - qntls[2] |
1074 | ! |
binwidth <- max(scaling_factor * iqr / length(x_vec) ^ (1 / 3), sqrt(qntls[4] - qntls[1])) # styler: off |
1075 | ! |
binwidth <- ifelse(binwidth == 0, 1, binwidth) |
1076 |
# to ensure at least two bins when variable span is very small
|
|
1077 | ! |
x_span <- diff(range(x_vec)) |
1078 | ! |
if (isTRUE(x_span / binwidth >= 2)) binwidth else x_span / 2 |
1079 |
}
|
|
1080 | ||
1081 |
#' Removes the outlier observation from an array
|
|
1082 |
#'
|
|
1083 |
#' @param var (`numeric`) a numeric vector
|
|
1084 |
#' @param outlier_definition (`numeric`) if `0` then no outliers are removed, otherwise
|
|
1085 |
#' outliers (those more than `outlier_definition*IQR below/above Q1/Q3`) are removed
|
|
1086 |
#' @returns (`numeric`) vector without the outlier values
|
|
1087 |
#' @keywords internal
|
|
1088 |
remove_outliers_from <- function(var, outlier_definition) { |
|
1089 | 3x |
if (outlier_definition == 0) { |
1090 | 1x |
return(var) |
1091 |
}
|
|
1092 | 2x |
q1_q3 <- stats::quantile(var, probs = c(0.25, 0.75), type = 2, na.rm = TRUE) |
1093 | 2x |
iqr <- q1_q3[2] - q1_q3[1] |
1094 | 2x |
var[var >= q1_q3[1] - outlier_definition * iqr & var <= q1_q3[2] + outlier_definition * iqr] |
1095 |
}
|
|
1096 | ||
1097 | ||
1098 |
# sparklines ----
|
|
1099 | ||
1100 |
#' S3 generic for `sparkline` widget HTML
|
|
1101 |
#'
|
|
1102 |
#' Generates the `sparkline` HTML code corresponding to the input array.
|
|
1103 |
#' For numeric variables creates a box plot, for character and factors - bar plot.
|
|
1104 |
#' Produces an empty string for variables of other types.
|
|
1105 |
#'
|
|
1106 |
#' @param arr vector of any type and length
|
|
1107 |
#' @param width `numeric` the width of the `sparkline` widget (pixels)
|
|
1108 |
#' @param bar_spacing `numeric` the spacing between the bars (in pixels)
|
|
1109 |
#' @param bar_width `numeric` the width of the bars (in pixels)
|
|
1110 |
#' @param ... `list` additional options passed to bar plots of `jquery.sparkline`;
|
|
1111 |
#' see [`jquery.sparkline docs`](https://omnipotent.net/jquery.sparkline/#common)
|
|
1112 |
#'
|
|
1113 |
#' @return Character string containing HTML code of the `sparkline` HTML widget.
|
|
1114 |
#' @keywords internal
|
|
1115 |
create_sparklines <- function(arr, width = 150, ...) { |
|
1116 | ! |
if (all(is.null(arr))) { |
1117 | ! |
return("") |
1118 |
}
|
|
1119 | ! |
UseMethod("create_sparklines") |
1120 |
}
|
|
1121 | ||
1122 |
#' @rdname create_sparklines
|
|
1123 |
#' @keywords internal
|
|
1124 |
#' @export
|
|
1125 |
create_sparklines.logical <- function(arr, ...) { |
|
1126 | ! |
create_sparklines(as.factor(arr)) |
1127 |
}
|
|
1128 | ||
1129 |
#' @rdname create_sparklines
|
|
1130 |
#' @keywords internal
|
|
1131 |
#' @export
|
|
1132 |
create_sparklines.numeric <- function(arr, width = 150, ...) { |
|
1133 | ! |
if (any(is.infinite(arr))) { |
1134 | ! |
return(as.character(tags$code("infinite values", class = "text-blue"))) |
1135 |
}
|
|
1136 | ! |
if (length(arr) > 100000) { |
1137 | ! |
return(as.character(tags$code("Too many rows (>100000)", class = "text-blue"))) |
1138 |
}
|
|
1139 | ||
1140 | ! |
arr <- arr[!is.na(arr)] |
1141 | ! |
sparkline::spk_chr(unname(arr), type = "box", width = width, ...) |
1142 |
}
|
|
1143 | ||
1144 |
#' @rdname create_sparklines
|
|
1145 |
#' @keywords internal
|
|
1146 |
#' @export
|
|
1147 |
create_sparklines.character <- function(arr, ...) { |
|
1148 | ! |
return(create_sparklines(as.factor(arr))) |
1149 |
}
|
|
1150 | ||
1151 | ||
1152 |
#' @rdname create_sparklines
|
|
1153 |
#' @keywords internal
|
|
1154 |
#' @export
|
|
1155 |
create_sparklines.factor <- function(arr, width = 150, bar_spacing = 5, bar_width = 20, ...) { |
|
1156 | ! |
decreasing_order <- TRUE |
1157 | ||
1158 | ! |
counts <- table(arr) |
1159 | ! |
if (length(counts) >= 100) { |
1160 | ! |
return(as.character(tags$code("> 99 levels", class = "text-blue"))) |
1161 | ! |
} else if (length(counts) == 0) { |
1162 | ! |
return(as.character(tags$code("no levels", class = "text-blue"))) |
1163 | ! |
} else if (length(counts) == 1) { |
1164 | ! |
return(as.character(tags$code("one level", class = "text-blue"))) |
1165 |
}
|
|
1166 | ||
1167 |
# Summarize the occurences of different levels
|
|
1168 |
# and get the maximum and minimum number of occurences
|
|
1169 |
# This is needed for the sparkline to correctly display the bar plots
|
|
1170 |
# Otherwise they are cropped
|
|
1171 | ! |
counts <- sort(counts, decreasing = decreasing_order, method = "radix") |
1172 | ! |
max_value <- if (decreasing_order) counts[1] else counts[length[counts]] |
1173 | ! |
max_value <- unname(max_value) |
1174 | ||
1175 | ! |
sparkline::spk_chr( |
1176 | ! |
unname(counts), |
1177 | ! |
type = "bar", |
1178 | ! |
chartRangeMin = 0, |
1179 | ! |
chartRangeMax = max_value, |
1180 | ! |
width = width, |
1181 | ! |
barWidth = bar_width, |
1182 | ! |
barSpacing = bar_spacing, |
1183 | ! |
tooltipFormatter = custom_sparkline_formatter(names(counts), as.vector(counts)) |
1184 |
)
|
|
1185 |
}
|
|
1186 | ||
1187 |
#' @rdname create_sparklines
|
|
1188 |
#' @keywords internal
|
|
1189 |
#' @export
|
|
1190 |
create_sparklines.Date <- function(arr, width = 150, bar_spacing = 5, bar_width = 20, ...) { |
|
1191 | ! |
arr_num <- as.numeric(arr) |
1192 | ! |
arr_num <- sort(arr_num, decreasing = FALSE, method = "radix") |
1193 | ! |
binwidth <- get_bin_width(arr_num, 1) |
1194 | ! |
bins <- floor(diff(range(arr_num)) / binwidth) + 1 |
1195 | ! |
if (all(is.na(bins))) { |
1196 | ! |
return(as.character(tags$code("only NA", class = "text-blue"))) |
1197 | ! |
} else if (bins == 1) { |
1198 | ! |
return(as.character(tags$code("one date", class = "text-blue"))) |
1199 |
}
|
|
1200 | ! |
counts <- as.vector(unname(base::table(cut(arr_num, breaks = bins)))) |
1201 | ! |
max_value <- max(counts) |
1202 | ||
1203 | ! |
start_bins <- as.integer(seq(1, length(arr_num), length.out = bins)) |
1204 | ! |
labels_start <- as.character(as.Date(arr_num[start_bins], origin = as.Date("1970-01-01"))) |
1205 | ! |
labels <- paste("Start:", labels_start) |
1206 | ||
1207 | ! |
sparkline::spk_chr( |
1208 | ! |
unname(counts), |
1209 | ! |
type = "bar", |
1210 | ! |
chartRangeMin = 0, |
1211 | ! |
chartRangeMax = max_value, |
1212 | ! |
width = width, |
1213 | ! |
barWidth = bar_width, |
1214 | ! |
barSpacing = bar_spacing, |
1215 | ! |
tooltipFormatter = custom_sparkline_formatter(labels, counts) |
1216 |
)
|
|
1217 |
}
|
|
1218 | ||
1219 |
#' @rdname create_sparklines
|
|
1220 |
#' @keywords internal
|
|
1221 |
#' @export
|
|
1222 |
create_sparklines.POSIXct <- function(arr, width = 150, bar_spacing = 5, bar_width = 20, ...) { |
|
1223 | ! |
arr_num <- as.numeric(arr) |
1224 | ! |
arr_num <- sort(arr_num, decreasing = FALSE, method = "radix") |
1225 | ! |
binwidth <- get_bin_width(arr_num, 1) |
1226 | ! |
bins <- floor(diff(range(arr_num)) / binwidth) + 1 |
1227 | ! |
if (all(is.na(bins))) { |
1228 | ! |
return(as.character(tags$code("only NA", class = "text-blue"))) |
1229 | ! |
} else if (bins == 1) { |
1230 | ! |
return(as.character(tags$code("one date-time", class = "text-blue"))) |
1231 |
}
|
|
1232 | ! |
counts <- as.vector(unname(base::table(cut(arr_num, breaks = bins)))) |
1233 | ! |
max_value <- max(counts) |
1234 | ||
1235 | ! |
start_bins <- as.integer(seq(1, length(arr_num), length.out = bins)) |
1236 | ! |
labels_start <- as.character(format(as.POSIXct(arr_num[start_bins], origin = as.Date("1970-01-01")), "%Y-%m-%d")) |
1237 | ! |
labels <- paste("Start:", labels_start) |
1238 | ||
1239 | ! |
sparkline::spk_chr( |
1240 | ! |
unname(counts), |
1241 | ! |
type = "bar", |
1242 | ! |
chartRangeMin = 0, |
1243 | ! |
chartRangeMax = max_value, |
1244 | ! |
width = width, |
1245 | ! |
barWidth = bar_width, |
1246 | ! |
barSpacing = bar_spacing, |
1247 | ! |
tooltipFormatter = custom_sparkline_formatter(labels, counts) |
1248 |
)
|
|
1249 |
}
|
|
1250 | ||
1251 |
#' @rdname create_sparklines
|
|
1252 |
#' @keywords internal
|
|
1253 |
#' @export
|
|
1254 |
create_sparklines.POSIXlt <- function(arr, width = 150, bar_spacing = 5, bar_width = 20, ...) { |
|
1255 | ! |
arr_num <- as.numeric(arr) |
1256 | ! |
arr_num <- sort(arr_num, decreasing = FALSE, method = "radix") |
1257 | ! |
binwidth <- get_bin_width(arr_num, 1) |
1258 | ! |
bins <- floor(diff(range(arr_num)) / binwidth) + 1 |
1259 | ! |
if (all(is.na(bins))) { |
1260 | ! |
return(as.character(tags$code("only NA", class = "text-blue"))) |
1261 | ! |
} else if (bins == 1) { |
1262 | ! |
return(as.character(tags$code("one date-time", class = "text-blue"))) |
1263 |
}
|
|
1264 | ! |
counts <- as.vector(unname(base::table(cut(arr_num, breaks = bins)))) |
1265 | ! |
max_value <- max(counts) |
1266 | ||
1267 | ! |
start_bins <- as.integer(seq(1, length(arr_num), length.out = bins)) |
1268 | ! |
labels_start <- as.character(format(as.POSIXct(arr_num[start_bins], origin = as.Date("1970-01-01")), "%Y-%m-%d")) |
1269 | ! |
labels <- paste("Start:", labels_start) |
1270 | ||
1271 | ! |
sparkline::spk_chr( |
1272 | ! |
unname(counts), |
1273 | ! |
type = "bar", |
1274 | ! |
chartRangeMin = 0, |
1275 | ! |
chartRangeMax = max_value, |
1276 | ! |
width = width, |
1277 | ! |
barWidth = bar_width, |
1278 | ! |
barSpacing = bar_spacing, |
1279 | ! |
tooltipFormatter = custom_sparkline_formatter(labels, counts) |
1280 |
)
|
|
1281 |
}
|
|
1282 | ||
1283 |
#' @rdname create_sparklines
|
|
1284 |
#' @keywords internal
|
|
1285 |
#' @export
|
|
1286 |
create_sparklines.default <- function(arr, width = 150, ...) { |
|
1287 | ! |
as.character(tags$code("unsupported variable type", class = "text-blue")) |
1288 |
}
|
|
1289 | ||
1290 |
custom_sparkline_formatter <- function(labels, counts) { |
|
1291 | ! |
htmlwidgets::JS( |
1292 | ! |
sprintf( |
1293 | ! |
"function(sparkline, options, field) { |
1294 | ! |
return 'ID: ' + %s[field[0].offset] + '<br>' + 'Count: ' + %s[field[0].offset]; |
1295 |
}", |
|
1296 | ! |
jsonlite::toJSON(labels), |
1297 | ! |
jsonlite::toJSON(counts) |
1298 |
)
|
|
1299 |
)
|
|
1300 |
}
|
1 |
#' `teal` module: Scatterplot matrix
|
|
2 |
#'
|
|
3 |
#' Generates a scatterplot matrix from selected `variables` from datasets.
|
|
4 |
#' Each plot within the matrix represents the relationship between two variables,
|
|
5 |
#' providing the overview of correlations and distributions across selected data.
|
|
6 |
#'
|
|
7 |
#' @note For more examples, please see the vignette "Using scatterplot matrix" via
|
|
8 |
#' `vignette("using-scatterplot-matrix", package = "teal.modules.general")`.
|
|
9 |
#'
|
|
10 |
#' @inheritParams teal::module
|
|
11 |
#' @inheritParams tm_g_scatterplot
|
|
12 |
#' @inheritParams shared_params
|
|
13 |
#'
|
|
14 |
#' @param variables (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
15 |
#' Specifies plotting variables from an incoming dataset with filtering and selecting. In case of
|
|
16 |
#' `data_extract_spec` use `select_spec(..., ordered = TRUE)` if plot elements should be
|
|
17 |
#' rendered according to selection order.
|
|
18 |
#'
|
|
19 |
#' @inherit shared_params return
|
|
20 |
#'
|
|
21 |
#' @section Decorating Module:
|
|
22 |
#'
|
|
23 |
#' This module generates the following objects, which can be modified in place using decorators:
|
|
24 |
#' - `plot` (`trellis` - output of `lattice::splom`)
|
|
25 |
#'
|
|
26 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
27 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
28 |
#' See code snippet below:
|
|
29 |
#'
|
|
30 |
#' ```
|
|
31 |
#' tm_g_scatterplotmatrix(
|
|
32 |
#' ..., # arguments for module
|
|
33 |
#' decorators = list(
|
|
34 |
#' plot = teal_transform_module(...) # applied to the `plot` output
|
|
35 |
#' )
|
|
36 |
#' )
|
|
37 |
#' ```
|
|
38 |
#'
|
|
39 |
#' For additional details and examples of decorators, refer to the vignette
|
|
40 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
41 |
#'
|
|
42 |
#' To learn more please refer to the vignette
|
|
43 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
44 |
#'
|
|
45 |
#' @examplesShinylive
|
|
46 |
#' library(teal.modules.general)
|
|
47 |
#' interactive <- function() TRUE
|
|
48 |
#' {{ next_example }}
|
|
49 |
#' @examples
|
|
50 |
#' # general data example
|
|
51 |
#' data <- teal_data()
|
|
52 |
#' data <- within(data, {
|
|
53 |
#' countries <- data.frame(
|
|
54 |
#' id = c("DE", "FR", "IT", "ES", "PT", "GR", "NL", "BE", "LU", "AT"),
|
|
55 |
#' government = factor(
|
|
56 |
#' c(2, 2, 2, 1, 2, 2, 1, 1, 1, 2),
|
|
57 |
#' labels = c("Monarchy", "Republic")
|
|
58 |
#' ),
|
|
59 |
#' language_family = factor(
|
|
60 |
#' c(1, 3, 3, 3, 3, 2, 1, 1, 3, 1),
|
|
61 |
#' labels = c("Germanic", "Hellenic", "Romance")
|
|
62 |
#' ),
|
|
63 |
#' population = c(83, 67, 60, 47, 10, 11, 17, 11, 0.6, 9),
|
|
64 |
#' area = c(357, 551, 301, 505, 92, 132, 41, 30, 2.6, 83),
|
|
65 |
#' gdp = c(3.4, 2.7, 2.1, 1.4, 0.3, 0.2, 0.7, 0.5, 0.1, 0.4),
|
|
66 |
#' debt = c(2.1, 2.3, 2.4, 2.6, 2.3, 2.4, 2.3, 2.4, 2.3, 2.4)
|
|
67 |
#' )
|
|
68 |
#' sales <- data.frame(
|
|
69 |
#' id = 1:50,
|
|
70 |
#' country_id = sample(
|
|
71 |
#' c("DE", "FR", "IT", "ES", "PT", "GR", "NL", "BE", "LU", "AT"),
|
|
72 |
#' size = 50,
|
|
73 |
#' replace = TRUE
|
|
74 |
#' ),
|
|
75 |
#' year = sort(sample(2010:2020, 50, replace = TRUE)),
|
|
76 |
#' venue = sample(c("small", "medium", "large", "online"), 50, replace = TRUE),
|
|
77 |
#' cancelled = sample(c(TRUE, FALSE), 50, replace = TRUE),
|
|
78 |
#' quantity = rnorm(50, 100, 20),
|
|
79 |
#' costs = rnorm(50, 80, 20),
|
|
80 |
#' profit = rnorm(50, 20, 10)
|
|
81 |
#' )
|
|
82 |
#' })
|
|
83 |
#' join_keys(data) <- join_keys(
|
|
84 |
#' join_key("countries", "countries", "id"),
|
|
85 |
#' join_key("sales", "sales", "id"),
|
|
86 |
#' join_key("countries", "sales", c("id" = "country_id"))
|
|
87 |
#' )
|
|
88 |
#'
|
|
89 |
#' app <- init(
|
|
90 |
#' data = data,
|
|
91 |
#' modules = modules(
|
|
92 |
#' tm_g_scatterplotmatrix(
|
|
93 |
#' label = "Scatterplot matrix",
|
|
94 |
#' variables = list(
|
|
95 |
#' data_extract_spec(
|
|
96 |
#' dataname = "countries",
|
|
97 |
#' select = select_spec(
|
|
98 |
#' label = "Select variables:",
|
|
99 |
#' choices = variable_choices(data[["countries"]]),
|
|
100 |
#' selected = c("area", "gdp", "debt"),
|
|
101 |
#' multiple = TRUE,
|
|
102 |
#' ordered = TRUE,
|
|
103 |
#' fixed = FALSE
|
|
104 |
#' )
|
|
105 |
#' ),
|
|
106 |
#' data_extract_spec(
|
|
107 |
#' dataname = "sales",
|
|
108 |
#' filter = filter_spec(
|
|
109 |
#' label = "Select variable:",
|
|
110 |
#' vars = "country_id",
|
|
111 |
#' choices = value_choices(data[["sales"]], "country_id"),
|
|
112 |
#' selected = c("DE", "FR", "IT", "PT", "GR", "NL", "BE", "LU", "AT"),
|
|
113 |
#' multiple = TRUE
|
|
114 |
#' ),
|
|
115 |
#' select = select_spec(
|
|
116 |
#' label = "Select variables:",
|
|
117 |
#' choices = variable_choices(data[["sales"]], c("quantity", "costs", "profit")),
|
|
118 |
#' selected = c("quantity", "costs", "profit"),
|
|
119 |
#' multiple = TRUE,
|
|
120 |
#' ordered = TRUE,
|
|
121 |
#' fixed = FALSE
|
|
122 |
#' )
|
|
123 |
#' )
|
|
124 |
#' )
|
|
125 |
#' )
|
|
126 |
#' )
|
|
127 |
#' )
|
|
128 |
#' if (interactive()) {
|
|
129 |
#' shinyApp(app$ui, app$server)
|
|
130 |
#' }
|
|
131 |
#'
|
|
132 |
#' @examplesShinylive
|
|
133 |
#' library(teal.modules.general)
|
|
134 |
#' interactive <- function() TRUE
|
|
135 |
#' {{ next_example }}
|
|
136 |
#' @examples
|
|
137 |
#' # CDISC data example
|
|
138 |
#' data <- teal_data()
|
|
139 |
#' data <- within(data, {
|
|
140 |
#' ADSL <- teal.data::rADSL
|
|
141 |
#' ADRS <- teal.data::rADRS
|
|
142 |
#' })
|
|
143 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
144 |
#'
|
|
145 |
#' app <- init(
|
|
146 |
#' data = data,
|
|
147 |
#' modules = modules(
|
|
148 |
#' tm_g_scatterplotmatrix(
|
|
149 |
#' label = "Scatterplot matrix",
|
|
150 |
#' variables = list(
|
|
151 |
#' data_extract_spec(
|
|
152 |
#' dataname = "ADSL",
|
|
153 |
#' select = select_spec(
|
|
154 |
#' label = "Select variables:",
|
|
155 |
#' choices = variable_choices(data[["ADSL"]]),
|
|
156 |
#' selected = c("AGE", "RACE", "SEX"),
|
|
157 |
#' multiple = TRUE,
|
|
158 |
#' ordered = TRUE,
|
|
159 |
#' fixed = FALSE
|
|
160 |
#' )
|
|
161 |
#' ),
|
|
162 |
#' data_extract_spec(
|
|
163 |
#' dataname = "ADRS",
|
|
164 |
#' filter = filter_spec(
|
|
165 |
#' label = "Select endpoints:",
|
|
166 |
#' vars = c("PARAMCD", "AVISIT"),
|
|
167 |
#' choices = value_choices(data[["ADRS"]], c("PARAMCD", "AVISIT"), c("PARAM", "AVISIT")),
|
|
168 |
#' selected = "INVET - END OF INDUCTION",
|
|
169 |
#' multiple = TRUE
|
|
170 |
#' ),
|
|
171 |
#' select = select_spec(
|
|
172 |
#' label = "Select variables:",
|
|
173 |
#' choices = variable_choices(data[["ADRS"]]),
|
|
174 |
#' selected = c("AGE", "AVAL", "ADY"),
|
|
175 |
#' multiple = TRUE,
|
|
176 |
#' ordered = TRUE,
|
|
177 |
#' fixed = FALSE
|
|
178 |
#' )
|
|
179 |
#' )
|
|
180 |
#' )
|
|
181 |
#' )
|
|
182 |
#' )
|
|
183 |
#' )
|
|
184 |
#' if (interactive()) {
|
|
185 |
#' shinyApp(app$ui, app$server)
|
|
186 |
#' }
|
|
187 |
#'
|
|
188 |
#' @export
|
|
189 |
#'
|
|
190 |
tm_g_scatterplotmatrix <- function(label = "Scatterplot Matrix", |
|
191 |
variables,
|
|
192 |
plot_height = c(600, 200, 2000), |
|
193 |
plot_width = NULL, |
|
194 |
pre_output = NULL, |
|
195 |
post_output = NULL, |
|
196 |
transformators = list(), |
|
197 |
decorators = list()) { |
|
198 | ! |
message("Initializing tm_g_scatterplotmatrix") |
199 | ||
200 |
# Normalize the parameters
|
|
201 | ! |
if (inherits(variables, "data_extract_spec")) variables <- list(variables) |
202 | ||
203 |
# Start of assertions
|
|
204 | ! |
checkmate::assert_string(label) |
205 | ! |
checkmate::assert_list(variables, types = "data_extract_spec") |
206 | ||
207 | ! |
checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
208 | ! |
checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
209 | ! |
checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
210 | ! |
checkmate::assert_numeric( |
211 | ! |
plot_width[1], |
212 | ! |
lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width" |
213 |
)
|
|
214 | ||
215 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
216 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
217 | ||
218 | ! |
assert_decorators(decorators, "plot") |
219 |
# End of assertions
|
|
220 | ||
221 |
# Make UI args
|
|
222 | ! |
args <- as.list(environment()) |
223 | ||
224 | ! |
ans <- module( |
225 | ! |
label = label, |
226 | ! |
server = srv_g_scatterplotmatrix, |
227 | ! |
ui = ui_g_scatterplotmatrix, |
228 | ! |
ui_args = args, |
229 | ! |
server_args = list( |
230 | ! |
variables = variables, |
231 | ! |
plot_height = plot_height, |
232 | ! |
plot_width = plot_width, |
233 | ! |
decorators = decorators |
234 |
),
|
|
235 | ! |
transformators = transformators, |
236 | ! |
datanames = teal.transform::get_extract_datanames(variables) |
237 |
)
|
|
238 | ! |
attr(ans, "teal_bookmarkable") <- TRUE |
239 | ! |
ans
|
240 |
}
|
|
241 | ||
242 |
# UI function for the scatterplot matrix module
|
|
243 |
ui_g_scatterplotmatrix <- function(id, ...) { |
|
244 | ! |
args <- list(...) |
245 | ! |
is_single_dataset_value <- teal.transform::is_single_dataset(args$variables) |
246 | ! |
ns <- NS(id) |
247 | ! |
teal.widgets::standard_layout( |
248 | ! |
output = teal.widgets::white_small_well( |
249 | ! |
textOutput(ns("message")), |
250 | ! |
tags$br(), |
251 | ! |
teal.widgets::plot_with_settings_ui(id = ns("myplot")) |
252 |
),
|
|
253 | ! |
encoding = tags$div( |
254 |
### Reporter
|
|
255 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
256 |
###
|
|
257 | ! |
tags$label("Encodings", class = "text-primary"), |
258 | ! |
teal.transform::datanames_input(args$variables), |
259 | ! |
teal.transform::data_extract_ui( |
260 | ! |
id = ns("variables"), |
261 | ! |
label = "Variables", |
262 | ! |
data_extract_spec = args$variables, |
263 | ! |
is_single_dataset = is_single_dataset_value |
264 |
),
|
|
265 | ! |
tags$hr(), |
266 | ! |
ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")), |
267 | ! |
teal.widgets::panel_group( |
268 | ! |
teal.widgets::panel_item( |
269 | ! |
title = "Plot settings", |
270 | ! |
sliderInput( |
271 | ! |
ns("alpha"), "Opacity:", |
272 | ! |
min = 0, max = 1, |
273 | ! |
step = .05, value = .5, ticks = FALSE |
274 |
),
|
|
275 | ! |
sliderInput( |
276 | ! |
ns("cex"), "Points size:", |
277 | ! |
min = 0.2, max = 3, |
278 | ! |
step = .05, value = .65, ticks = FALSE |
279 |
),
|
|
280 | ! |
checkboxInput(ns("cor"), "Add Correlation", value = FALSE), |
281 | ! |
radioButtons( |
282 | ! |
ns("cor_method"), "Select Correlation Method", |
283 | ! |
choiceNames = c("Pearson", "Kendall", "Spearman"), |
284 | ! |
choiceValues = c("pearson", "kendall", "spearman"), |
285 | ! |
inline = TRUE |
286 |
),
|
|
287 | ! |
checkboxInput(ns("cor_na_omit"), "Omit Missing Values", value = TRUE) |
288 |
)
|
|
289 |
)
|
|
290 |
),
|
|
291 | ! |
forms = tagList( |
292 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
293 |
),
|
|
294 | ! |
pre_output = args$pre_output, |
295 | ! |
post_output = args$post_output |
296 |
)
|
|
297 |
}
|
|
298 | ||
299 |
# Server function for the scatterplot matrix module
|
|
300 |
srv_g_scatterplotmatrix <- function(id, |
|
301 |
data,
|
|
302 |
reporter,
|
|
303 |
filter_panel_api,
|
|
304 |
variables,
|
|
305 |
plot_height,
|
|
306 |
plot_width,
|
|
307 |
decorators) { |
|
308 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
309 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
310 | ! |
checkmate::assert_class(data, "reactive") |
311 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
312 | ! |
moduleServer(id, function(input, output, session) { |
313 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
314 | ||
315 | ! |
selector_list <- teal.transform::data_extract_multiple_srv( |
316 | ! |
data_extract = list(variables = variables), |
317 | ! |
datasets = data, |
318 | ! |
select_validation_rule = list( |
319 | ! |
variables = ~ if (length(.) <= 1) "Please select at least 2 columns." |
320 |
)
|
|
321 |
)
|
|
322 | ||
323 | ! |
iv_r <- reactive({ |
324 | ! |
iv <- shinyvalidate::InputValidator$new() |
325 | ! |
teal.transform::compose_and_enable_validators(iv, selector_list) |
326 |
}) |
|
327 | ||
328 | ! |
anl_merged_input <- teal.transform::merge_expression_srv( |
329 | ! |
datasets = data, |
330 | ! |
selector_list = selector_list |
331 |
)
|
|
332 | ||
333 | ! |
anl_merged_q <- reactive({ |
334 | ! |
req(anl_merged_input()) |
335 | ! |
qenv <- teal.code::eval_code(data(), 'library("dplyr");library("lattice")') # nolint quotes |
336 | ! |
teal.code::eval_code(qenv, as.expression(anl_merged_input()$expr)) |
337 |
}) |
|
338 | ||
339 | ! |
merged <- list( |
340 | ! |
anl_input_r = anl_merged_input, |
341 | ! |
anl_q_r = anl_merged_q |
342 |
)
|
|
343 | ||
344 |
# plot
|
|
345 | ! |
output_q <- reactive({ |
346 | ! |
teal::validate_inputs(iv_r()) |
347 | ||
348 | ! |
qenv <- merged$anl_q_r() |
349 | ! |
ANL <- qenv[["ANL"]] |
350 | ||
351 | ! |
cols_names <- merged$anl_input_r()$columns_source$variables |
352 | ! |
alpha <- input$alpha |
353 | ! |
cex <- input$cex |
354 | ! |
add_cor <- input$cor |
355 | ! |
cor_method <- input$cor_method |
356 | ! |
cor_na_omit <- input$cor_na_omit |
357 | ||
358 | ! |
cor_na_action <- if (isTruthy(cor_na_omit)) { |
359 | ! |
"na.omit"
|
360 |
} else { |
|
361 | ! |
"na.fail"
|
362 |
}
|
|
363 | ||
364 | ! |
teal::validate_has_data(ANL, 10) |
365 | ! |
teal::validate_has_data(ANL[, cols_names, drop = FALSE], 10, complete = TRUE, allow_inf = FALSE) |
366 | ||
367 |
# get labels and proper variable names
|
|
368 | ! |
varnames <- varname_w_label(cols_names, ANL, wrap_width = 20) |
369 | ||
370 |
# check character columns. If any, then those are converted to factors
|
|
371 | ! |
check_char <- vapply(ANL[, cols_names], is.character, logical(1)) |
372 | ! |
qenv <- teal.code::eval_code(qenv, 'library("dplyr")') # nolint quotes |
373 | ! |
if (any(check_char)) { |
374 | ! |
qenv <- teal.code::eval_code( |
375 | ! |
qenv,
|
376 | ! |
substitute( |
377 | ! |
expr = ANL <- ANL[, cols_names] %>% |
378 | ! |
dplyr::mutate_if(is.character, as.factor) %>% |
379 | ! |
droplevels(), |
380 | ! |
env = list(cols_names = cols_names) |
381 |
)
|
|
382 |
)
|
|
383 |
} else { |
|
384 | ! |
qenv <- teal.code::eval_code( |
385 | ! |
qenv,
|
386 | ! |
substitute( |
387 | ! |
expr = ANL <- ANL[, cols_names] %>% |
388 | ! |
droplevels(), |
389 | ! |
env = list(cols_names = cols_names) |
390 |
)
|
|
391 |
)
|
|
392 |
}
|
|
393 | ||
394 | ||
395 |
# create plot
|
|
396 | ! |
if (add_cor) { |
397 | ! |
shinyjs::show("cor_method") |
398 | ! |
shinyjs::show("cor_use") |
399 | ! |
shinyjs::show("cor_na_omit") |
400 | ||
401 | ! |
qenv <- teal.code::eval_code( |
402 | ! |
qenv,
|
403 | ! |
substitute( |
404 | ! |
expr = { |
405 | ! |
plot <- lattice::splom( |
406 | ! |
ANL,
|
407 | ! |
varnames = varnames_value, |
408 | ! |
panel = function(x, y, ...) { |
409 | ! |
lattice::panel.splom(x = x, y = y, ...) |
410 | ! |
cpl <- lattice::current.panel.limits() |
411 | ! |
lattice::panel.text( |
412 | ! |
mean(cpl$xlim), |
413 | ! |
mean(cpl$ylim), |
414 | ! |
get_scatterplotmatrix_stats( |
415 | ! |
x,
|
416 | ! |
y,
|
417 | ! |
.f = stats::cor.test, |
418 | ! |
.f_args = list(method = cor_method, na.action = cor_na_action) |
419 |
),
|
|
420 | ! |
alpha = 0.6, |
421 | ! |
fontsize = 18, |
422 | ! |
fontface = "bold" |
423 |
)
|
|
424 |
},
|
|
425 | ! |
pch = 16, |
426 | ! |
alpha = alpha_value, |
427 | ! |
cex = cex_value |
428 |
)
|
|
429 |
},
|
|
430 | ! |
env = list( |
431 | ! |
varnames_value = varnames, |
432 | ! |
cor_method = cor_method, |
433 | ! |
cor_na_action = cor_na_action, |
434 | ! |
alpha_value = alpha, |
435 | ! |
cex_value = cex |
436 |
)
|
|
437 |
)
|
|
438 |
)
|
|
439 |
} else { |
|
440 | ! |
shinyjs::hide("cor_method") |
441 | ! |
shinyjs::hide("cor_use") |
442 | ! |
shinyjs::hide("cor_na_omit") |
443 | ! |
qenv <- teal.code::eval_code( |
444 | ! |
qenv,
|
445 | ! |
substitute( |
446 | ! |
expr = { |
447 | ! |
plot <- lattice::splom( |
448 | ! |
ANL,
|
449 | ! |
varnames = varnames_value, |
450 | ! |
pch = 16, |
451 | ! |
alpha = alpha_value, |
452 | ! |
cex = cex_value |
453 |
)
|
|
454 |
},
|
|
455 | ! |
env = list(varnames_value = varnames, alpha_value = alpha, cex_value = cex) |
456 |
)
|
|
457 |
)
|
|
458 |
}
|
|
459 | ! |
qenv
|
460 |
}) |
|
461 | ||
462 | ! |
decorated_output_q <- srv_decorate_teal_data( |
463 | ! |
id = "decorator", |
464 | ! |
data = output_q, |
465 | ! |
decorators = select_decorators(decorators, "plot"), |
466 | ! |
expr = print(plot) |
467 |
)
|
|
468 | ||
469 | ! |
plot_r <- reactive(req(decorated_output_q())[["plot"]]) |
470 | ||
471 |
# Insert the plot into a plot_with_settings module
|
|
472 | ! |
pws <- teal.widgets::plot_with_settings_srv( |
473 | ! |
id = "myplot", |
474 | ! |
plot_r = plot_r, |
475 | ! |
height = plot_height, |
476 | ! |
width = plot_width |
477 |
)
|
|
478 | ||
479 |
# show a message if conversion to factors took place
|
|
480 | ! |
output$message <- renderText({ |
481 | ! |
req(iv_r()$is_valid()) |
482 | ! |
req(selector_list()$variables()) |
483 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
484 | ! |
cols_names <- unique(unname(do.call(c, merged$anl_input_r()$columns_source))) |
485 | ! |
check_char <- vapply(ANL[, cols_names], is.character, logical(1)) |
486 | ! |
if (any(check_char)) { |
487 | ! |
is_single <- sum(check_char) == 1 |
488 | ! |
paste( |
489 | ! |
"Character",
|
490 | ! |
ifelse(is_single, "variable", "variables"), |
491 | ! |
paste0("(", paste(cols_names[check_char], collapse = ", "), ")"), |
492 | ! |
ifelse(is_single, "was", "were"), |
493 | ! |
"converted to",
|
494 | ! |
ifelse(is_single, "factor.", "factors.") |
495 |
)
|
|
496 |
} else { |
|
497 |
""
|
|
498 |
}
|
|
499 |
}) |
|
500 | ||
501 |
# Render R code.
|
|
502 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_output_q()))) |
503 | ||
504 | ! |
teal.widgets::verbatim_popup_srv( |
505 | ! |
id = "rcode", |
506 | ! |
verbatim_content = source_code_r, |
507 | ! |
title = "Show R Code for Scatterplotmatrix" |
508 |
)
|
|
509 | ||
510 |
### REPORTER
|
|
511 | ! |
if (with_reporter) { |
512 | ! |
card_fun <- function(comment, label) { |
513 | ! |
card <- teal::report_card_template( |
514 | ! |
title = "Scatter Plot Matrix", |
515 | ! |
label = label, |
516 | ! |
with_filter = with_filter, |
517 | ! |
filter_panel_api = filter_panel_api |
518 |
)
|
|
519 | ! |
card$append_text("Plot", "header3") |
520 | ! |
card$append_plot(plot_r(), dim = pws$dim()) |
521 | ! |
if (!comment == "") { |
522 | ! |
card$append_text("Comment", "header3") |
523 | ! |
card$append_text(comment) |
524 |
}
|
|
525 | ! |
card$append_src(source_code_r()) |
526 | ! |
card
|
527 |
}
|
|
528 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
529 |
}
|
|
530 |
###
|
|
531 |
}) |
|
532 |
}
|
|
533 | ||
534 |
#' Get stats for x-y pairs in scatterplot matrix
|
|
535 |
#'
|
|
536 |
#' Uses [stats::cor.test()] per default for all numerical input variables and converts results
|
|
537 |
#' to character vector.
|
|
538 |
#' Could be extended if different stats for different variable types are needed.
|
|
539 |
#' Meant to be called from [lattice::panel.text()].
|
|
540 |
#'
|
|
541 |
#' Presently we need to use a formula input for `stats::cor.test` because
|
|
542 |
#' `na.fail` only gets evaluated when a formula is passed (see below).
|
|
543 |
#' ```
|
|
544 |
#' x = c(1,3,5,7,NA)
|
|
545 |
#' y = c(3,6,7,8,1)
|
|
546 |
#' stats::cor.test(x, y, na.action = "na.fail")
|
|
547 |
#' stats::cor.test(~ x + y, na.action = "na.fail")
|
|
548 |
#' ```
|
|
549 |
#'
|
|
550 |
#' @param x,y (`numeric`) vectors of data values. `x` and `y` must have the same length.
|
|
551 |
#' @param .f (`function`) function that accepts x and y as formula input `~ x + y`.
|
|
552 |
#' Default `stats::cor.test`.
|
|
553 |
#' @param .f_args (`list`) of arguments to be passed to `.f`.
|
|
554 |
#' @param round_stat (`integer(1)`) optional, number of decimal places to use when rounding the estimate.
|
|
555 |
#' @param round_pval (`integer(1)`) optional, number of decimal places to use when rounding the p-value.
|
|
556 |
#'
|
|
557 |
#' @return Character with stats. For [stats::cor.test()] correlation coefficient and p-value.
|
|
558 |
#'
|
|
559 |
#' @examples
|
|
560 |
#' set.seed(1)
|
|
561 |
#' x <- runif(25, 0, 1)
|
|
562 |
#' y <- runif(25, 0, 1)
|
|
563 |
#' x[c(3, 10, 18)] <- NA
|
|
564 |
#'
|
|
565 |
#' get_scatterplotmatrix_stats(x, y, .f = stats::cor.test, .f_args = list(method = "pearson"))
|
|
566 |
#' get_scatterplotmatrix_stats(x, y, .f = stats::cor.test, .f_args = list(
|
|
567 |
#' method = "pearson",
|
|
568 |
#' na.action = na.fail
|
|
569 |
#' ))
|
|
570 |
#'
|
|
571 |
#' @export
|
|
572 |
#'
|
|
573 |
get_scatterplotmatrix_stats <- function(x, y, |
|
574 |
.f = stats::cor.test, |
|
575 |
.f_args = list(), |
|
576 |
round_stat = 2, |
|
577 |
round_pval = 4) { |
|
578 | 6x |
if (is.numeric(x) && is.numeric(y)) { |
579 | 3x |
stat <- tryCatch(do.call(.f, c(list(~ x + y), .f_args)), error = function(e) NA) |
580 | ||
581 | 3x |
if (anyNA(stat)) { |
582 | 1x |
return("NA") |
583 | 2x |
} else if (all(c("estimate", "p.value") %in% names(stat))) { |
584 | 2x |
return(paste( |
585 | 2x |
c( |
586 | 2x |
paste0(names(stat$estimate), ":", round(stat$estimate, round_stat)), |
587 | 2x |
paste0("P:", round(stat$p.value, round_pval)) |
588 |
),
|
|
589 | 2x |
collapse = "\n" |
590 |
)) |
|
591 |
} else { |
|
592 | ! |
stop("function not supported") |
593 |
}
|
|
594 |
} else { |
|
595 | 3x |
if ("method" %in% names(.f_args)) { |
596 | 3x |
if (.f_args$method == "pearson") { |
597 | 1x |
return("cor:-") |
598 |
}
|
|
599 | 2x |
if (.f_args$method == "kendall") { |
600 | 1x |
return("tau:-") |
601 |
}
|
|
602 | 1x |
if (.f_args$method == "spearman") { |
603 | 1x |
return("rho:-") |
604 |
}
|
|
605 |
}
|
|
606 | ! |
return("-") |
607 |
}
|
|
608 |
}
|
1 |
#' `teal` module: Scatterplot and regression analysis
|
|
2 |
#'
|
|
3 |
#' Module for visualizing regression analysis, including scatterplots and
|
|
4 |
#' various regression diagnostics plots.
|
|
5 |
#' It allows users to explore the relationship between a set of regressors and a response variable,
|
|
6 |
#' visualize residuals, and identify outliers.
|
|
7 |
#'
|
|
8 |
#' @note For more examples, please see the vignette "Using regression plots" via
|
|
9 |
#' `vignette("using-regression-plots", package = "teal.modules.general")`.
|
|
10 |
#'
|
|
11 |
#' @inheritParams teal::module
|
|
12 |
#' @inheritParams shared_params
|
|
13 |
#' @param regressor (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
14 |
#' Regressor variables from an incoming dataset with filtering and selecting.
|
|
15 |
#' @param response (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
16 |
#' Response variables from an incoming dataset with filtering and selecting.
|
|
17 |
#' @param default_outlier_label (`character`) optional, default column selected to label outliers.
|
|
18 |
#' @param default_plot_type (`numeric`) optional, defaults to "Response vs Regressor".
|
|
19 |
#' 1. Response vs Regressor
|
|
20 |
#' 2. Residuals vs Fitted
|
|
21 |
#' 3. Normal Q-Q
|
|
22 |
#' 4. Scale-Location
|
|
23 |
#' 5. Cook's distance
|
|
24 |
#' 6. Residuals vs Leverage
|
|
25 |
#' 7. Cook's dist vs Leverage
|
|
26 |
#' @param label_segment_threshold (`numeric(1)` or `numeric(3)`)
|
|
27 |
#' Minimum distance between label and point on the plot that triggers the creation of
|
|
28 |
#' a line segment between the two.
|
|
29 |
#' This may happen when the label cannot be placed next to the point as it overlaps another
|
|
30 |
#' label or point.
|
|
31 |
#' The value is used as the `min.segment.length` parameter to the [ggrepel::geom_text_repel()] function.
|
|
32 |
#'
|
|
33 |
#' It can take the following forms:
|
|
34 |
#' - `numeric(1)`: Fixed value used for the minimum distance and the slider is not presented in the UI.
|
|
35 |
#' - `numeric(3)`: A slider is presented in the UI (under "Plot settings") to adjust the minimum distance dynamically.
|
|
36 |
#'
|
|
37 |
#' It takes the form of `c(value, min, max)` and it is passed to the `value_min_max`
|
|
38 |
#' argument in `teal.widgets::optionalSliderInputValMinMax`.
|
|
39 |
#'
|
|
40 |
# nolint start: line_length.
|
|
41 |
#' @param ggplot2_args `r roxygen_ggplot2_args_param("Response vs Regressor", "Residuals vs Fitted", "Scale-Location", "Cook's distance", "Residuals vs Leverage", "Cook's dist vs Leverage")`
|
|
42 |
# nolint end: line_length.
|
|
43 |
#'
|
|
44 |
#' @inherit shared_params return
|
|
45 |
#'
|
|
46 |
#' @section Decorating Module:
|
|
47 |
#'
|
|
48 |
#' This module generates the following objects, which can be modified in place using decorators:
|
|
49 |
#' - `plot` (`ggplot`)
|
|
50 |
#'
|
|
51 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
52 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
53 |
#' See code snippet below:
|
|
54 |
#'
|
|
55 |
#' ```
|
|
56 |
#' tm_a_regression(
|
|
57 |
#' ..., # arguments for module
|
|
58 |
#' decorators = list(
|
|
59 |
#' plot = teal_transform_module(...) # applied to the `plot` output
|
|
60 |
#' )
|
|
61 |
#' )
|
|
62 |
#' ```
|
|
63 |
#'
|
|
64 |
#' For additional details and examples of decorators, refer to the vignette
|
|
65 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
66 |
#'
|
|
67 |
#' To learn more please refer to the vignette
|
|
68 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
69 |
#'
|
|
70 |
#' @examplesShinylive
|
|
71 |
#' library(teal.modules.general)
|
|
72 |
#' interactive <- function() TRUE
|
|
73 |
#' {{ next_example }}
|
|
74 |
#' @examples
|
|
75 |
#'
|
|
76 |
#' # general data example
|
|
77 |
#' data <- teal_data()
|
|
78 |
#' data <- within(data, {
|
|
79 |
#' require(nestcolor)
|
|
80 |
#' CO2 <- CO2
|
|
81 |
#' })
|
|
82 |
#'
|
|
83 |
#' app <- init(
|
|
84 |
#' data = data,
|
|
85 |
#' modules = modules(
|
|
86 |
#' tm_a_regression(
|
|
87 |
#' label = "Regression",
|
|
88 |
#' response = data_extract_spec(
|
|
89 |
#' dataname = "CO2",
|
|
90 |
#' select = select_spec(
|
|
91 |
#' label = "Select variable:",
|
|
92 |
#' choices = "uptake",
|
|
93 |
#' selected = "uptake",
|
|
94 |
#' multiple = FALSE,
|
|
95 |
#' fixed = TRUE
|
|
96 |
#' )
|
|
97 |
#' ),
|
|
98 |
#' regressor = data_extract_spec(
|
|
99 |
#' dataname = "CO2",
|
|
100 |
#' select = select_spec(
|
|
101 |
#' label = "Select variables:",
|
|
102 |
#' choices = variable_choices(data[["CO2"]], c("conc", "Treatment")),
|
|
103 |
#' selected = "conc",
|
|
104 |
#' multiple = TRUE,
|
|
105 |
#' fixed = FALSE
|
|
106 |
#' )
|
|
107 |
#' )
|
|
108 |
#' )
|
|
109 |
#' )
|
|
110 |
#' )
|
|
111 |
#' if (interactive()) {
|
|
112 |
#' shinyApp(app$ui, app$server)
|
|
113 |
#' }
|
|
114 |
#'
|
|
115 |
#' @examplesShinylive
|
|
116 |
#' library(teal.modules.general)
|
|
117 |
#' interactive <- function() TRUE
|
|
118 |
#' {{ next_example }}
|
|
119 |
#' @examples
|
|
120 |
#' # CDISC data example
|
|
121 |
#' data <- teal_data()
|
|
122 |
#' data <- within(data, {
|
|
123 |
#' require(nestcolor)
|
|
124 |
#' ADSL <- teal.data::rADSL
|
|
125 |
#' })
|
|
126 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
127 |
#'
|
|
128 |
#' app <- init(
|
|
129 |
#' data = data,
|
|
130 |
#' modules = modules(
|
|
131 |
#' tm_a_regression(
|
|
132 |
#' label = "Regression",
|
|
133 |
#' response = data_extract_spec(
|
|
134 |
#' dataname = "ADSL",
|
|
135 |
#' select = select_spec(
|
|
136 |
#' label = "Select variable:",
|
|
137 |
#' choices = "BMRKR1",
|
|
138 |
#' selected = "BMRKR1",
|
|
139 |
#' multiple = FALSE,
|
|
140 |
#' fixed = TRUE
|
|
141 |
#' )
|
|
142 |
#' ),
|
|
143 |
#' regressor = data_extract_spec(
|
|
144 |
#' dataname = "ADSL",
|
|
145 |
#' select = select_spec(
|
|
146 |
#' label = "Select variables:",
|
|
147 |
#' choices = variable_choices(data[["ADSL"]], c("AGE", "SEX", "RACE")),
|
|
148 |
#' selected = "AGE",
|
|
149 |
#' multiple = TRUE,
|
|
150 |
#' fixed = FALSE
|
|
151 |
#' )
|
|
152 |
#' )
|
|
153 |
#' )
|
|
154 |
#' )
|
|
155 |
#' )
|
|
156 |
#' if (interactive()) {
|
|
157 |
#' shinyApp(app$ui, app$server)
|
|
158 |
#' }
|
|
159 |
#'
|
|
160 |
#' @export
|
|
161 |
#'
|
|
162 |
tm_a_regression <- function(label = "Regression Analysis", |
|
163 |
regressor,
|
|
164 |
response,
|
|
165 |
plot_height = c(600, 200, 2000), |
|
166 |
plot_width = NULL, |
|
167 |
alpha = c(1, 0, 1), |
|
168 |
size = c(2, 1, 8), |
|
169 |
ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), |
|
170 |
ggplot2_args = teal.widgets::ggplot2_args(), |
|
171 |
pre_output = NULL, |
|
172 |
post_output = NULL, |
|
173 |
default_plot_type = 1, |
|
174 |
default_outlier_label = "USUBJID", |
|
175 |
label_segment_threshold = c(0.5, 0, 10), |
|
176 |
transformators = list(), |
|
177 |
decorators = list()) { |
|
178 | ! |
message("Initializing tm_a_regression") |
179 | ||
180 |
# Normalize the parameters
|
|
181 | ! |
if (inherits(regressor, "data_extract_spec")) regressor <- list(regressor) |
182 | ! |
if (inherits(response, "data_extract_spec")) response <- list(response) |
183 | ! |
if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args) |
184 | ||
185 |
# Start of assertions
|
|
186 | ! |
checkmate::assert_string(label) |
187 | ! |
checkmate::assert_list(regressor, types = "data_extract_spec") |
188 | ||
189 | ! |
checkmate::assert_list(response, types = "data_extract_spec") |
190 | ! |
assert_single_selection(response) |
191 | ||
192 | ! |
checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
193 | ! |
checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
194 | ||
195 | ! |
checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
196 | ! |
checkmate::assert_numeric( |
197 | ! |
plot_width[1], |
198 | ! |
lower = plot_width[2], |
199 | ! |
upper = plot_width[3], |
200 | ! |
null.ok = TRUE, |
201 | ! |
.var.name = "plot_width" |
202 |
)
|
|
203 | ||
204 | ! |
if (length(alpha) == 1) { |
205 | ! |
checkmate::assert_numeric(alpha, any.missing = FALSE, finite = TRUE) |
206 |
} else { |
|
207 | ! |
checkmate::assert_numeric(alpha, len = 3, any.missing = FALSE, finite = TRUE) |
208 | ! |
checkmate::assert_numeric(alpha[1], lower = alpha[2], upper = alpha[3], .var.name = "alpha") |
209 |
}
|
|
210 | ||
211 | ! |
if (length(size) == 1) { |
212 | ! |
checkmate::assert_numeric(size, any.missing = FALSE, finite = TRUE) |
213 |
} else { |
|
214 | ! |
checkmate::assert_numeric(size, len = 3, any.missing = FALSE, finite = TRUE) |
215 | ! |
checkmate::assert_numeric(size[1], lower = size[2], upper = size[3], .var.name = "size") |
216 |
}
|
|
217 | ||
218 | ! |
ggtheme <- match.arg(ggtheme) |
219 | ||
220 | ! |
plot_choices <- c( |
221 | ! |
"Response vs Regressor", "Residuals vs Fitted", "Normal Q-Q", "Scale-Location", |
222 | ! |
"Cook's distance", "Residuals vs Leverage", "Cook's dist vs Leverage" |
223 |
)
|
|
224 | ! |
checkmate::assert_list(ggplot2_args, types = "ggplot2_args") |
225 | ! |
checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices)) |
226 | ||
227 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
228 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
229 | ! |
checkmate::assert_choice(default_plot_type, seq.int(1L, length(plot_choices))) |
230 | ! |
checkmate::assert_string(default_outlier_label) |
231 | ! |
checkmate::assert_list(decorators, "teal_transform_module") |
232 | ||
233 | ! |
if (length(label_segment_threshold) == 1) { |
234 | ! |
checkmate::assert_numeric(label_segment_threshold, any.missing = FALSE, finite = TRUE) |
235 |
} else { |
|
236 | ! |
checkmate::assert_numeric(label_segment_threshold, len = 3, any.missing = FALSE, finite = TRUE) |
237 | ! |
checkmate::assert_numeric( |
238 | ! |
label_segment_threshold[1], |
239 | ! |
lower = label_segment_threshold[2], |
240 | ! |
upper = label_segment_threshold[3], |
241 | ! |
.var.name = "label_segment_threshold" |
242 |
)
|
|
243 |
}
|
|
244 | ! |
assert_decorators(decorators, "plot") |
245 |
# End of assertions
|
|
246 | ||
247 |
# Make UI args
|
|
248 | ! |
args <- as.list(environment()) |
249 | ! |
args[["plot_choices"]] <- plot_choices |
250 | ! |
data_extract_list <- list( |
251 | ! |
regressor = regressor, |
252 | ! |
response = response |
253 |
)
|
|
254 | ||
255 | ! |
ans <- module( |
256 | ! |
label = label, |
257 | ! |
server = srv_a_regression, |
258 | ! |
ui = ui_a_regression, |
259 | ! |
ui_args = args, |
260 | ! |
server_args = c( |
261 | ! |
data_extract_list,
|
262 | ! |
list( |
263 | ! |
plot_height = plot_height, |
264 | ! |
plot_width = plot_width, |
265 | ! |
default_outlier_label = default_outlier_label, |
266 | ! |
ggplot2_args = ggplot2_args, |
267 | ! |
decorators = decorators |
268 |
)
|
|
269 |
),
|
|
270 | ! |
transformators = transformators, |
271 | ! |
datanames = teal.transform::get_extract_datanames(data_extract_list) |
272 |
)
|
|
273 | ! |
attr(ans, "teal_bookmarkable") <- FALSE |
274 | ! |
ans
|
275 |
}
|
|
276 | ||
277 |
# UI function for the regression module
|
|
278 |
ui_a_regression <- function(id, ...) { |
|
279 | ! |
ns <- NS(id) |
280 | ! |
args <- list(...) |
281 | ! |
is_single_dataset_value <- teal.transform::is_single_dataset(args$regressor, args$response) |
282 | ! |
teal.widgets::standard_layout( |
283 | ! |
output = teal.widgets::white_small_well(tags$div( |
284 | ! |
teal.widgets::plot_with_settings_ui(id = ns("myplot")), |
285 | ! |
tags$div(verbatimTextOutput(ns("text"))) |
286 |
)), |
|
287 | ! |
encoding = tags$div( |
288 |
### Reporter
|
|
289 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
290 |
###
|
|
291 | ! |
tags$label("Encodings", class = "text-primary"), |
292 | ! |
teal.transform::datanames_input(args[c("response", "regressor")]), |
293 | ! |
teal.transform::data_extract_ui( |
294 | ! |
id = ns("response"), |
295 | ! |
label = "Response variable", |
296 | ! |
data_extract_spec = args$response, |
297 | ! |
is_single_dataset = is_single_dataset_value |
298 |
),
|
|
299 | ! |
teal.transform::data_extract_ui( |
300 | ! |
id = ns("regressor"), |
301 | ! |
label = "Regressor variables", |
302 | ! |
data_extract_spec = args$regressor, |
303 | ! |
is_single_dataset = is_single_dataset_value |
304 |
),
|
|
305 | ! |
radioButtons( |
306 | ! |
ns("plot_type"), |
307 | ! |
label = "Plot type:", |
308 | ! |
choices = args$plot_choices, |
309 | ! |
selected = args$plot_choices[args$default_plot_type] |
310 |
),
|
|
311 | ! |
checkboxInput(ns("show_outlier"), label = "Display outlier labels", value = TRUE), |
312 | ! |
conditionalPanel( |
313 | ! |
condition = "input['show_outlier']", |
314 | ! |
ns = ns, |
315 | ! |
teal.widgets::optionalSliderInput( |
316 | ! |
ns("outlier"), |
317 | ! |
tags$div( |
318 | ! |
class = "teal-tooltip", |
319 | ! |
tagList( |
320 | ! |
"Outlier definition:",
|
321 | ! |
icon("circle-info"), |
322 | ! |
tags$span( |
323 | ! |
class = "tooltiptext", |
324 | ! |
paste( |
325 | ! |
"Use the slider to choose the cut-off value to define outliers.",
|
326 | ! |
"Points with a Cook's distance greater than",
|
327 | ! |
"the value on the slider times the mean of the Cook's distance of the dataset will have labels."
|
328 |
)
|
|
329 |
)
|
|
330 |
)
|
|
331 |
),
|
|
332 | ! |
min = 1, max = 10, value = 9, ticks = FALSE, step = .1 |
333 |
),
|
|
334 | ! |
teal.widgets::optionalSelectInput( |
335 | ! |
ns("label_var"), |
336 | ! |
multiple = FALSE, |
337 | ! |
label = "Outlier label" |
338 |
)
|
|
339 |
),
|
|
340 | ! |
ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")), |
341 | ! |
teal.widgets::panel_group( |
342 | ! |
teal.widgets::panel_item( |
343 | ! |
title = "Plot settings", |
344 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("alpha"), "Opacity:", args$alpha, ticks = FALSE), |
345 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("size"), "Points size:", args$size, ticks = FALSE), |
346 | ! |
teal.widgets::optionalSliderInputValMinMax( |
347 | ! |
inputId = ns("label_min_segment"), |
348 | ! |
label = tags$div( |
349 | ! |
class = "teal-tooltip", |
350 | ! |
tagList( |
351 | ! |
"Label min. segment:",
|
352 | ! |
icon("circle-info"), |
353 | ! |
tags$span( |
354 | ! |
class = "tooltiptext", |
355 | ! |
paste( |
356 | ! |
"Use the slider to choose the cut-off value to define minimum distance between label and point",
|
357 | ! |
"that generates a line segment.",
|
358 | ! |
"It's only valid when 'Display outlier labels' is checked."
|
359 |
)
|
|
360 |
)
|
|
361 |
)
|
|
362 |
),
|
|
363 | ! |
value_min_max = args$label_segment_threshold, |
364 |
# Extra parameters to sliderInput
|
|
365 | ! |
ticks = FALSE, |
366 | ! |
step = .1, |
367 | ! |
round = FALSE |
368 |
),
|
|
369 | ! |
selectInput( |
370 | ! |
inputId = ns("ggtheme"), |
371 | ! |
label = "Theme (by ggplot):", |
372 | ! |
choices = ggplot_themes, |
373 | ! |
selected = args$ggtheme, |
374 | ! |
multiple = FALSE |
375 |
)
|
|
376 |
)
|
|
377 |
)
|
|
378 |
),
|
|
379 | ! |
forms = tagList( |
380 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
381 |
),
|
|
382 | ! |
pre_output = args$pre_output, |
383 | ! |
post_output = args$post_output |
384 |
)
|
|
385 |
}
|
|
386 | ||
387 |
# Server function for the regression module
|
|
388 |
srv_a_regression <- function(id, |
|
389 |
data,
|
|
390 |
reporter,
|
|
391 |
filter_panel_api,
|
|
392 |
response,
|
|
393 |
regressor,
|
|
394 |
plot_height,
|
|
395 |
plot_width,
|
|
396 |
ggplot2_args,
|
|
397 |
default_outlier_label,
|
|
398 |
decorators) { |
|
399 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
400 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
401 | ! |
checkmate::assert_class(data, "reactive") |
402 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
403 | ! |
moduleServer(id, function(input, output, session) { |
404 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
405 | ||
406 | ! |
ns <- session$ns |
407 | ||
408 | ! |
rule_rvr1 <- function(value) { |
409 | ! |
if (isTRUE(input$plot_type == "Response vs Regressor")) { |
410 | ! |
if (length(value) > 1L) { |
411 | ! |
"This plot can only have one regressor."
|
412 |
}
|
|
413 |
}
|
|
414 |
}
|
|
415 | ! |
rule_rvr2 <- function(other) { |
416 | ! |
function(value) { |
417 | ! |
if (isTRUE(input$plot_type == "Response vs Regressor")) { |
418 | ! |
otherval <- selector_list()[[other]]()$select |
419 | ! |
if (isTRUE(value == otherval)) { |
420 | ! |
"Response and Regressor must be different."
|
421 |
}
|
|
422 |
}
|
|
423 |
}
|
|
424 |
}
|
|
425 | ||
426 | ! |
selector_list <- teal.transform::data_extract_multiple_srv( |
427 | ! |
data_extract = list(response = response, regressor = regressor), |
428 | ! |
datasets = data, |
429 | ! |
select_validation_rule = list( |
430 | ! |
regressor = shinyvalidate::compose_rules( |
431 | ! |
shinyvalidate::sv_required("At least one regressor should be selected."), |
432 | ! |
rule_rvr1,
|
433 | ! |
rule_rvr2("response") |
434 |
),
|
|
435 | ! |
response = shinyvalidate::compose_rules( |
436 | ! |
shinyvalidate::sv_required("At least one response should be selected."), |
437 | ! |
rule_rvr2("regressor") |
438 |
)
|
|
439 |
)
|
|
440 |
)
|
|
441 | ||
442 | ! |
iv_r <- reactive({ |
443 | ! |
iv <- shinyvalidate::InputValidator$new() |
444 | ! |
teal.transform::compose_and_enable_validators(iv, selector_list) |
445 |
}) |
|
446 | ||
447 | ! |
iv_out <- shinyvalidate::InputValidator$new() |
448 | ! |
iv_out$condition(~ isTRUE(input$show_outlier)) |
449 | ! |
iv_out$add_rule("label_var", shinyvalidate::sv_required("Please provide an `Outlier label` variable")) |
450 | ! |
iv_out$enable() |
451 | ||
452 | ! |
anl_merged_input <- teal.transform::merge_expression_srv( |
453 | ! |
selector_list = selector_list, |
454 | ! |
datasets = data |
455 |
)
|
|
456 | ||
457 | ! |
regression_var <- reactive({ |
458 | ! |
teal::validate_inputs(iv_r()) |
459 | ||
460 | ! |
list( |
461 | ! |
response = as.vector(anl_merged_input()$columns_source$response), |
462 | ! |
regressor = as.vector(anl_merged_input()$columns_source$regressor) |
463 |
)
|
|
464 |
}) |
|
465 | ||
466 | ! |
qenv <- teal.code::eval_code( |
467 | ! |
data(), |
468 | ! |
'library("ggplot2");library("dplyr")' # nolint quotes |
469 |
)
|
|
470 | ||
471 | ! |
anl_merged_q <- reactive({ |
472 | ! |
req(anl_merged_input()) |
473 | ! |
qenv %>% |
474 | ! |
teal.code::eval_code(as.expression(anl_merged_input()$expr)) |
475 |
}) |
|
476 | ||
477 |
# sets qenv object and populates it with data merge call and fit expression
|
|
478 | ! |
fit_r <- reactive({ |
479 | ! |
ANL <- anl_merged_q()[["ANL"]] |
480 | ! |
teal::validate_has_data(ANL, 10) |
481 | ||
482 | ! |
validate(need(is.numeric(ANL[regression_var()$response][[1]]), "Response variable should be numeric.")) |
483 | ||
484 | ! |
teal::validate_has_data( |
485 | ! |
ANL[, c(regression_var()$response, regression_var()$regressor)], 10, |
486 | ! |
complete = TRUE, allow_inf = FALSE |
487 |
)
|
|
488 | ||
489 | ! |
form <- stats::as.formula( |
490 | ! |
paste( |
491 | ! |
regression_var()$response, |
492 | ! |
paste( |
493 | ! |
regression_var()$regressor, |
494 | ! |
collapse = " + " |
495 |
),
|
|
496 | ! |
sep = " ~ " |
497 |
)
|
|
498 |
)
|
|
499 | ||
500 | ! |
if (input$show_outlier) { |
501 | ! |
opts <- teal.transform::variable_choices(ANL) |
502 | ! |
selected <- if (!is.null(isolate(input$label_var)) && isolate(input$label_var) %in% as.character(opts)) { |
503 | ! |
isolate(input$label_var) |
504 |
} else { |
|
505 | ! |
if (length(opts[as.character(opts) == default_outlier_label]) == 0) { |
506 | ! |
opts[[1]] |
507 |
} else { |
|
508 | ! |
opts[as.character(opts) == default_outlier_label] |
509 |
}
|
|
510 |
}
|
|
511 | ! |
teal.widgets::updateOptionalSelectInput( |
512 | ! |
session = session, |
513 | ! |
inputId = "label_var", |
514 | ! |
choices = opts, |
515 | ! |
selected = restoreInput(ns("label_var"), selected) |
516 |
)
|
|
517 | ||
518 | ! |
data <- ggplot2::fortify(stats::lm(form, data = ANL)) |
519 | ! |
cooksd <- data$.cooksd[!is.nan(data$.cooksd)] |
520 | ! |
max_outlier <- max(ceiling(max(cooksd) / mean(cooksd)), 2) |
521 | ! |
cur_outlier <- isolate(input$outlier) |
522 | ! |
updateSliderInput( |
523 | ! |
session = session, |
524 | ! |
inputId = "outlier", |
525 | ! |
min = 1, |
526 | ! |
max = max_outlier, |
527 | ! |
value = restoreInput(ns("outlier"), if (cur_outlier < max_outlier) cur_outlier else max_outlier * .9) |
528 |
)
|
|
529 |
}
|
|
530 | ||
531 | ! |
anl_merged_q() %>% |
532 | ! |
teal.code::eval_code(substitute(fit <- stats::lm(form, data = ANL), env = list(form = form))) %>% |
533 | ! |
teal.code::eval_code(quote({ |
534 | ! |
for (regressor in names(fit$contrasts)) { |
535 | ! |
alts <- paste0(levels(ANL[[regressor]]), collapse = "|") |
536 | ! |
names(fit$coefficients) <- gsub( |
537 | ! |
paste0("^(", regressor, ")(", alts, ")$"), paste0("\\1", ": ", "\\2"), names(fit$coefficients) |
538 |
)
|
|
539 |
}
|
|
540 |
})) %>% |
|
541 | ! |
teal.code::eval_code(quote(summary(fit))) |
542 |
}) |
|
543 | ||
544 | ! |
label_col <- reactive({ |
545 | ! |
teal::validate_inputs(iv_out) |
546 | ||
547 | ! |
substitute( |
548 | ! |
expr = dplyr::if_else( |
549 | ! |
data$.cooksd > outliers * mean(data$.cooksd, na.rm = TRUE), |
550 | ! |
as.character(stats::na.omit(ANL)[[label_var]]), |
551 |
""
|
|
552 |
) %>% |
|
553 | ! |
dplyr::if_else(is.na(.), "cooksd == NaN", .), |
554 | ! |
env = list(outliers = input$outlier, label_var = input$label_var) |
555 |
)
|
|
556 |
}) |
|
557 | ||
558 | ! |
label_min_segment <- reactive({ |
559 | ! |
input$label_min_segment |
560 |
}) |
|
561 | ||
562 | ! |
outlier_label <- reactive({ |
563 | ! |
substitute( |
564 | ! |
expr = ggrepel::geom_text_repel( |
565 | ! |
label = label_col, |
566 | ! |
color = "red", |
567 | ! |
hjust = 0, |
568 | ! |
vjust = 1, |
569 | ! |
max.overlaps = Inf, |
570 | ! |
min.segment.length = label_min_segment, |
571 | ! |
segment.alpha = 0.5, |
572 | ! |
seed = 123 |
573 |
),
|
|
574 | ! |
env = list(label_col = label_col(), label_min_segment = label_min_segment()) |
575 |
)
|
|
576 |
}) |
|
577 | ||
578 | ! |
output_plot_base <- reactive({ |
579 | ! |
base_fit <- fit_r() |
580 | ! |
teal.code::eval_code( |
581 | ! |
base_fit,
|
582 | ! |
quote({ |
583 | ! |
class(fit$residuals) <- NULL |
584 | ||
585 | ! |
data <- ggplot2::fortify(fit) |
586 | ||
587 | ! |
smooth <- function(x, y) { |
588 | ! |
as.data.frame(stats::lowess(x, y, f = 2 / 3, iter = 3)) |
589 |
}
|
|
590 | ||
591 | ! |
smoothy_aes <- ggplot2::aes_string(x = "x", y = "y") |
592 | ||
593 | ! |
reg_form <- deparse(fit$call[[2]]) |
594 |
}) |
|
595 |
)
|
|
596 |
}) |
|
597 | ||
598 | ! |
output_plot_0 <- reactive({ |
599 | ! |
fit <- fit_r()[["fit"]] |
600 | ! |
ANL <- anl_merged_q()[["ANL"]] |
601 | ||
602 | ! |
stopifnot(ncol(fit$model) == 2) |
603 | ||
604 | ! |
if (!is.factor(ANL[[regression_var()$regressor]])) { |
605 | ! |
shinyjs::show("size") |
606 | ! |
shinyjs::show("alpha") |
607 | ! |
plot <- substitute( |
608 | ! |
expr = ggplot2::ggplot(fit$model[, 2:1], ggplot2::aes_string(regressor, response)) + |
609 | ! |
ggplot2::geom_point(size = size, alpha = alpha) + |
610 | ! |
ggplot2::stat_smooth(method = "lm", formula = y ~ x, se = FALSE), |
611 | ! |
env = list( |
612 | ! |
regressor = regression_var()$regressor, |
613 | ! |
response = regression_var()$response, |
614 | ! |
size = input$size, |
615 | ! |
alpha = input$alpha |
616 |
)
|
|
617 |
)
|
|
618 | ! |
if (input$show_outlier) { |
619 | ! |
plot <- substitute( |
620 | ! |
expr = plot + outlier_label, |
621 | ! |
env = list(plot = plot, outlier_label = outlier_label()) |
622 |
)
|
|
623 |
}
|
|
624 |
} else { |
|
625 | ! |
shinyjs::hide("size") |
626 | ! |
shinyjs::hide("alpha") |
627 | ! |
plot <- substitute( |
628 | ! |
expr = ggplot2::ggplot(fit$model[, 2:1], ggplot2::aes_string(regressor, response)) + |
629 | ! |
ggplot2::geom_boxplot(), |
630 | ! |
env = list(regressor = regression_var()$regressor, response = regression_var()$response) |
631 |
)
|
|
632 | ! |
if (input$show_outlier) { |
633 | ! |
plot <- substitute(expr = plot + outlier_label, env = list(plot = plot, outlier_label = outlier_label())) |
634 |
}
|
|
635 |
}
|
|
636 | ||
637 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
638 | ! |
teal.widgets::resolve_ggplot2_args( |
639 | ! |
user_plot = ggplot2_args[["Response vs Regressor"]], |
640 | ! |
user_default = ggplot2_args$default, |
641 | ! |
module_plot = teal.widgets::ggplot2_args( |
642 | ! |
labs = list( |
643 | ! |
title = "Response vs Regressor", |
644 | ! |
x = varname_w_label(regression_var()$regressor, ANL), |
645 | ! |
y = varname_w_label(regression_var()$response, ANL) |
646 |
),
|
|
647 | ! |
theme = list() |
648 |
)
|
|
649 |
),
|
|
650 | ! |
ggtheme = input$ggtheme |
651 |
)
|
|
652 | ||
653 | ! |
teal.code::eval_code( |
654 | ! |
fit_r(), |
655 | ! |
substitute( |
656 | ! |
expr = { |
657 | ! |
class(fit$residuals) <- NULL |
658 | ! |
data <- ggplot2::fortify(fit) |
659 | ! |
plot <- graph |
660 |
},
|
|
661 | ! |
env = list( |
662 | ! |
graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args)) |
663 |
)
|
|
664 |
)
|
|
665 |
)
|
|
666 |
}) |
|
667 | ||
668 | ! |
output_plot_1 <- reactive({ |
669 | ! |
plot_base <- output_plot_base() |
670 | ! |
shinyjs::show("size") |
671 | ! |
shinyjs::show("alpha") |
672 | ! |
plot <- substitute( |
673 | ! |
expr = ggplot2::ggplot(data = data, ggplot2::aes(.fitted, .resid)) + |
674 | ! |
ggplot2::geom_point(size = size, alpha = alpha) + |
675 | ! |
ggplot2::geom_hline(yintercept = 0, linetype = "dashed", size = 1) + |
676 | ! |
ggplot2::geom_line(data = smoothy, mapping = smoothy_aes), |
677 | ! |
env = list(size = input$size, alpha = input$alpha) |
678 |
)
|
|
679 | ! |
if (input$show_outlier) { |
680 | ! |
plot <- substitute(expr = plot + outlier_label, env = list(plot = plot, outlier_label = outlier_label())) |
681 |
}
|
|
682 | ||
683 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
684 | ! |
teal.widgets::resolve_ggplot2_args( |
685 | ! |
user_plot = ggplot2_args[["Residuals vs Fitted"]], |
686 | ! |
user_default = ggplot2_args$default, |
687 | ! |
module_plot = teal.widgets::ggplot2_args( |
688 | ! |
labs = list( |
689 | ! |
x = quote(paste0("Fitted values\nlm(", reg_form, ")")), |
690 | ! |
y = "Residuals", |
691 | ! |
title = "Residuals vs Fitted" |
692 |
)
|
|
693 |
)
|
|
694 |
),
|
|
695 | ! |
ggtheme = input$ggtheme |
696 |
)
|
|
697 | ||
698 | ! |
teal.code::eval_code( |
699 | ! |
plot_base,
|
700 | ! |
substitute( |
701 | ! |
expr = { |
702 | ! |
smoothy <- smooth(data$.fitted, data$.resid) |
703 | ! |
plot <- graph |
704 |
},
|
|
705 | ! |
env = list( |
706 | ! |
graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args)) |
707 |
)
|
|
708 |
)
|
|
709 |
)
|
|
710 |
}) |
|
711 | ||
712 | ! |
output_plot_2 <- reactive({ |
713 | ! |
shinyjs::show("size") |
714 | ! |
shinyjs::show("alpha") |
715 | ! |
plot_base <- output_plot_base() |
716 | ! |
plot <- substitute( |
717 | ! |
expr = ggplot2::ggplot(data = data, ggplot2::aes(sample = .stdresid)) + |
718 | ! |
ggplot2::stat_qq(size = size, alpha = alpha) + |
719 | ! |
ggplot2::geom_abline(linetype = "dashed"), |
720 | ! |
env = list(size = input$size, alpha = input$alpha) |
721 |
)
|
|
722 | ! |
if (input$show_outlier) { |
723 | ! |
plot <- substitute( |
724 | ! |
expr = plot + |
725 | ! |
ggplot2::stat_qq( |
726 | ! |
geom = ggrepel::GeomTextRepel, |
727 | ! |
label = label_col %>% |
728 | ! |
data.frame(label = .) %>% |
729 | ! |
dplyr::filter(label != "cooksd == NaN") %>% |
730 | ! |
unlist(), |
731 | ! |
color = "red", |
732 | ! |
hjust = 0, |
733 | ! |
vjust = 0, |
734 | ! |
max.overlaps = Inf, |
735 | ! |
min.segment.length = label_min_segment, |
736 | ! |
segment.alpha = .5, |
737 | ! |
seed = 123 |
738 |
),
|
|
739 | ! |
env = list(plot = plot, label_col = label_col(), label_min_segment = label_min_segment()) |
740 |
)
|
|
741 |
}
|
|
742 | ||
743 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
744 | ! |
teal.widgets::resolve_ggplot2_args( |
745 | ! |
user_plot = ggplot2_args[["Normal Q-Q"]], |
746 | ! |
user_default = ggplot2_args$default, |
747 | ! |
module_plot = teal.widgets::ggplot2_args( |
748 | ! |
labs = list( |
749 | ! |
x = quote(paste0("Theoretical Quantiles\nlm(", reg_form, ")")), |
750 | ! |
y = "Standardized residuals", |
751 | ! |
title = "Normal Q-Q" |
752 |
)
|
|
753 |
)
|
|
754 |
),
|
|
755 | ! |
ggtheme = input$ggtheme |
756 |
)
|
|
757 | ||
758 | ! |
teal.code::eval_code( |
759 | ! |
plot_base,
|
760 | ! |
substitute( |
761 | ! |
expr = { |
762 | ! |
plot <- graph |
763 |
},
|
|
764 | ! |
env = list( |
765 | ! |
graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args)) |
766 |
)
|
|
767 |
)
|
|
768 |
)
|
|
769 |
}) |
|
770 | ||
771 | ! |
output_plot_3 <- reactive({ |
772 | ! |
shinyjs::show("size") |
773 | ! |
shinyjs::show("alpha") |
774 | ! |
plot_base <- output_plot_base() |
775 | ! |
plot <- substitute( |
776 | ! |
expr = ggplot2::ggplot(data = data, ggplot2::aes(.fitted, sqrt(abs(.stdresid)))) + |
777 | ! |
ggplot2::geom_point(size = size, alpha = alpha) + |
778 | ! |
ggplot2::geom_line(data = smoothy, mapping = smoothy_aes), |
779 | ! |
env = list(size = input$size, alpha = input$alpha) |
780 |
)
|
|
781 | ! |
if (input$show_outlier) { |
782 | ! |
plot <- substitute(expr = plot + outlier_label, env = list(plot = plot, outlier_label = outlier_label())) |
783 |
}
|
|
784 | ||
785 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
786 | ! |
teal.widgets::resolve_ggplot2_args( |
787 | ! |
user_plot = ggplot2_args[["Scale-Location"]], |
788 | ! |
user_default = ggplot2_args$default, |
789 | ! |
module_plot = teal.widgets::ggplot2_args( |
790 | ! |
labs = list( |
791 | ! |
x = quote(paste0("Fitted values\nlm(", reg_form, ")")), |
792 | ! |
y = quote(expression(sqrt(abs(`Standardized residuals`)))), |
793 | ! |
title = "Scale-Location" |
794 |
)
|
|
795 |
)
|
|
796 |
),
|
|
797 | ! |
ggtheme = input$ggtheme |
798 |
)
|
|
799 | ||
800 | ! |
teal.code::eval_code( |
801 | ! |
plot_base,
|
802 | ! |
substitute( |
803 | ! |
expr = { |
804 | ! |
smoothy <- smooth(data$.fitted, sqrt(abs(data$.stdresid))) |
805 | ! |
plot <- graph |
806 |
},
|
|
807 | ! |
env = list( |
808 | ! |
graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args)) |
809 |
)
|
|
810 |
)
|
|
811 |
)
|
|
812 |
}) |
|
813 | ||
814 | ! |
output_plot_4 <- reactive({ |
815 | ! |
shinyjs::hide("size") |
816 | ! |
shinyjs::show("alpha") |
817 | ! |
plot_base <- output_plot_base() |
818 | ! |
plot <- substitute( |
819 | ! |
expr = ggplot2::ggplot(data = data, ggplot2::aes(seq_along(.cooksd), .cooksd)) + |
820 | ! |
ggplot2::geom_col(alpha = alpha), |
821 | ! |
env = list(alpha = input$alpha) |
822 |
)
|
|
823 | ! |
if (input$show_outlier) { |
824 | ! |
plot <- substitute( |
825 | ! |
expr = plot + |
826 | ! |
ggplot2::geom_hline( |
827 | ! |
yintercept = c( |
828 | ! |
outlier * mean(data$.cooksd, na.rm = TRUE), |
829 | ! |
mean(data$.cooksd, na.rm = TRUE) |
830 |
),
|
|
831 | ! |
color = "red", |
832 | ! |
linetype = "dashed" |
833 |
) + |
|
834 | ! |
ggplot2::geom_text( |
835 | ! |
ggplot2::aes( |
836 | ! |
x = 0, |
837 | ! |
y = mean(data$.cooksd, na.rm = TRUE), |
838 | ! |
label = paste("mu", "=", round(mean(data$.cooksd, na.rm = TRUE), 4)), |
839 | ! |
vjust = -1, |
840 | ! |
hjust = 0, |
841 | ! |
color = "red", |
842 | ! |
angle = 90 |
843 |
),
|
|
844 | ! |
parse = TRUE, |
845 | ! |
show.legend = FALSE |
846 |
) + |
|
847 | ! |
outlier_label,
|
848 | ! |
env = list(plot = plot, outlier = input$outlier, outlier_label = outlier_label()) |
849 |
)
|
|
850 |
}
|
|
851 | ||
852 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
853 | ! |
teal.widgets::resolve_ggplot2_args( |
854 | ! |
user_plot = ggplot2_args[["Cook's distance"]], |
855 | ! |
user_default = ggplot2_args$default, |
856 | ! |
module_plot = teal.widgets::ggplot2_args( |
857 | ! |
labs = list( |
858 | ! |
x = quote(paste0("Obs. number\nlm(", reg_form, ")")), |
859 | ! |
y = "Cook's distance", |
860 | ! |
title = "Cook's distance" |
861 |
)
|
|
862 |
)
|
|
863 |
),
|
|
864 | ! |
ggtheme = input$ggtheme |
865 |
)
|
|
866 | ||
867 | ! |
teal.code::eval_code( |
868 | ! |
plot_base,
|
869 | ! |
substitute( |
870 | ! |
expr = { |
871 | ! |
plot <- graph |
872 |
},
|
|
873 | ! |
env = list( |
874 | ! |
graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args)) |
875 |
)
|
|
876 |
)
|
|
877 |
)
|
|
878 |
}) |
|
879 | ||
880 | ! |
output_plot_5 <- reactive({ |
881 | ! |
shinyjs::show("size") |
882 | ! |
shinyjs::show("alpha") |
883 | ! |
plot_base <- output_plot_base() |
884 | ! |
plot <- substitute( |
885 | ! |
expr = ggplot2::ggplot(data = data, ggplot2::aes(.hat, .stdresid)) + |
886 | ! |
ggplot2::geom_vline( |
887 | ! |
size = 1, |
888 | ! |
colour = "black", |
889 | ! |
linetype = "dashed", |
890 | ! |
xintercept = 0 |
891 |
) + |
|
892 | ! |
ggplot2::geom_hline( |
893 | ! |
size = 1, |
894 | ! |
colour = "black", |
895 | ! |
linetype = "dashed", |
896 | ! |
yintercept = 0 |
897 |
) + |
|
898 | ! |
ggplot2::geom_point(size = size, alpha = alpha) + |
899 | ! |
ggplot2::geom_line(data = smoothy, mapping = smoothy_aes), |
900 | ! |
env = list(size = input$size, alpha = input$alpha) |
901 |
)
|
|
902 | ! |
if (input$show_outlier) { |
903 | ! |
plot <- substitute(expr = plot + outlier_label, env = list(plot = plot, outlier_label = outlier_label())) |
904 |
}
|
|
905 | ||
906 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
907 | ! |
teal.widgets::resolve_ggplot2_args( |
908 | ! |
user_plot = ggplot2_args[["Residuals vs Leverage"]], |
909 | ! |
user_default = ggplot2_args$default, |
910 | ! |
module_plot = teal.widgets::ggplot2_args( |
911 | ! |
labs = list( |
912 | ! |
x = quote(paste0("Standardized residuals\nlm(", reg_form, ")")), |
913 | ! |
y = "Leverage", |
914 | ! |
title = "Residuals vs Leverage" |
915 |
)
|
|
916 |
)
|
|
917 |
),
|
|
918 | ! |
ggtheme = input$ggtheme |
919 |
)
|
|
920 | ||
921 | ! |
teal.code::eval_code( |
922 | ! |
plot_base,
|
923 | ! |
substitute( |
924 | ! |
expr = { |
925 | ! |
smoothy <- smooth(data$.hat, data$.stdresid) |
926 | ! |
plot <- graph |
927 |
},
|
|
928 | ! |
env = list( |
929 | ! |
graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args)) |
930 |
)
|
|
931 |
)
|
|
932 |
)
|
|
933 |
}) |
|
934 | ||
935 | ! |
output_plot_6 <- reactive({ |
936 | ! |
shinyjs::show("size") |
937 | ! |
shinyjs::show("alpha") |
938 | ! |
plot_base <- output_plot_base() |
939 | ! |
plot <- substitute( |
940 | ! |
expr = ggplot2::ggplot(data = data, ggplot2::aes(.hat, .cooksd)) + |
941 | ! |
ggplot2::geom_vline(xintercept = 0, colour = NA) + |
942 | ! |
ggplot2::geom_abline( |
943 | ! |
slope = seq(0, 3, by = 0.5), |
944 | ! |
colour = "black", |
945 | ! |
linetype = "dashed", |
946 | ! |
size = 1 |
947 |
) + |
|
948 | ! |
ggplot2::geom_line(data = smoothy, mapping = smoothy_aes) + |
949 | ! |
ggplot2::geom_point(size = size, alpha = alpha), |
950 | ! |
env = list(size = input$size, alpha = input$alpha) |
951 |
)
|
|
952 | ! |
if (input$show_outlier) { |
953 | ! |
plot <- substitute(expr = plot + outlier_label, env = list(plot = plot, outlier_label = outlier_label())) |
954 |
}
|
|
955 | ||
956 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
957 | ! |
teal.widgets::resolve_ggplot2_args( |
958 | ! |
user_plot = ggplot2_args[["Cook's dist vs Leverage"]], |
959 | ! |
user_default = ggplot2_args$default, |
960 | ! |
module_plot = teal.widgets::ggplot2_args( |
961 | ! |
labs = list( |
962 | ! |
x = quote(paste0("Leverage\nlm(", reg_form, ")")), |
963 | ! |
y = "Cooks's distance", |
964 | ! |
title = "Cook's dist vs Leverage" |
965 |
)
|
|
966 |
)
|
|
967 |
),
|
|
968 | ! |
ggtheme = input$ggtheme |
969 |
)
|
|
970 | ||
971 | ! |
teal.code::eval_code( |
972 | ! |
plot_base,
|
973 | ! |
substitute( |
974 | ! |
expr = { |
975 | ! |
smoothy <- smooth(data$.hat, data$.cooksd) |
976 | ! |
plot <- graph |
977 |
},
|
|
978 | ! |
env = list( |
979 | ! |
graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args)) |
980 |
)
|
|
981 |
)
|
|
982 |
)
|
|
983 |
}) |
|
984 | ||
985 | ! |
output_q <- reactive({ |
986 | ! |
teal::validate_inputs(iv_r()) |
987 | ! |
switch(input$plot_type, |
988 | ! |
"Response vs Regressor" = output_plot_0(), |
989 | ! |
"Residuals vs Fitted" = output_plot_1(), |
990 | ! |
"Normal Q-Q" = output_plot_2(), |
991 | ! |
"Scale-Location" = output_plot_3(), |
992 | ! |
"Cook's distance" = output_plot_4(), |
993 | ! |
"Residuals vs Leverage" = output_plot_5(), |
994 | ! |
"Cook's dist vs Leverage" = output_plot_6() |
995 |
)
|
|
996 |
}) |
|
997 | ||
998 | ! |
decorated_output_q <- srv_decorate_teal_data( |
999 | ! |
"decorator",
|
1000 | ! |
data = output_q, |
1001 | ! |
decorators = select_decorators(decorators, "plot"), |
1002 | ! |
expr = print(plot) |
1003 |
)
|
|
1004 | ||
1005 | ! |
fitted <- reactive({ |
1006 | ! |
req(output_q()) |
1007 | ! |
decorated_output_q()[["fit"]] |
1008 |
}) |
|
1009 | ! |
plot_r <- reactive({ |
1010 | ! |
req(output_q()) |
1011 | ! |
decorated_output_q()[["plot"]] |
1012 |
}) |
|
1013 | ||
1014 |
# Insert the plot into a plot_with_settings module from teal.widgets
|
|
1015 | ! |
pws <- teal.widgets::plot_with_settings_srv( |
1016 | ! |
id = "myplot", |
1017 | ! |
plot_r = plot_r, |
1018 | ! |
height = plot_height, |
1019 | ! |
width = plot_width |
1020 |
)
|
|
1021 | ||
1022 | ! |
output$text <- renderText({ |
1023 | ! |
req(iv_r()$is_valid()) |
1024 | ! |
req(iv_out$is_valid()) |
1025 | ! |
paste(utils::capture.output(summary(teal.code::dev_suppress(fitted())))[-1], |
1026 | ! |
collapse = "\n" |
1027 |
)
|
|
1028 |
}) |
|
1029 | ||
1030 |
# Render R code.
|
|
1031 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_output_q()))) |
1032 | ||
1033 | ! |
teal.widgets::verbatim_popup_srv( |
1034 | ! |
id = "rcode", |
1035 | ! |
verbatim_content = source_code_r, |
1036 | ! |
title = "R code for the regression plot", |
1037 |
)
|
|
1038 | ||
1039 |
### REPORTER
|
|
1040 | ! |
if (with_reporter) { |
1041 | ! |
card_fun <- function(comment, label) { |
1042 | ! |
card <- teal::report_card_template( |
1043 | ! |
title = "Linear Regression Plot", |
1044 | ! |
label = label, |
1045 | ! |
with_filter = with_filter, |
1046 | ! |
filter_panel_api = filter_panel_api |
1047 |
)
|
|
1048 | ! |
card$append_text("Plot", "header3") |
1049 | ! |
card$append_plot(plot_r(), dim = pws$dim()) |
1050 | ! |
if (!comment == "") { |
1051 | ! |
card$append_text("Comment", "header3") |
1052 | ! |
card$append_text(comment) |
1053 |
}
|
|
1054 | ! |
card$append_src(source_code_r()) |
1055 | ! |
card
|
1056 |
}
|
|
1057 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
1058 |
}
|
|
1059 |
###
|
|
1060 |
}) |
|
1061 |
}
|
1 |
#' `teal` module: Response plot
|
|
2 |
#'
|
|
3 |
#' Generates a response plot for a given `response` and `x` variables.
|
|
4 |
#' This module allows users customize and add annotations to the plot depending
|
|
5 |
#' on the module's arguments.
|
|
6 |
#' It supports showing the counts grouped by other variable facets (by row / column),
|
|
7 |
#' swapping the coordinates, show count annotations and displaying the response plot
|
|
8 |
#' as frequency or density.
|
|
9 |
#'
|
|
10 |
#' @inheritParams teal::module
|
|
11 |
#' @inheritParams shared_params
|
|
12 |
#' @param response (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
13 |
#' Which variable to use as the response.
|
|
14 |
#' You can define one fixed column by setting `fixed = TRUE` inside the `select_spec`.
|
|
15 |
#'
|
|
16 |
#' The `data_extract_spec` must not allow multiple selection in this case.
|
|
17 |
#' @param x (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
18 |
#' Specifies which variable to use on the X-axis of the response plot.
|
|
19 |
#' Allow the user to select multiple columns from the `data` allowed in teal.
|
|
20 |
#'
|
|
21 |
#' The `data_extract_spec` must not allow multiple selection in this case.
|
|
22 |
#' @param row_facet (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
23 |
#' optional specification of the data variable(s) to use for faceting rows.
|
|
24 |
#' @param col_facet (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
25 |
#' optional specification of the data variable(s) to use for faceting columns.
|
|
26 |
#' @param coord_flip (`logical(1)`)
|
|
27 |
#' Indicates whether to flip coordinates between `x` and `response`.
|
|
28 |
#' The default value is `FALSE` and it will show the `x` variable on the x-axis
|
|
29 |
#' and the `response` variable on the y-axis.
|
|
30 |
#' @param count_labels (`logical(1)`)
|
|
31 |
#' Indicates whether to show count labels.
|
|
32 |
#' Defaults to `TRUE`.
|
|
33 |
#' @param freq (`logical(1)`)
|
|
34 |
#' Indicates whether to display frequency (`TRUE`) or density (`FALSE`).
|
|
35 |
#' Defaults to density (`FALSE`).
|
|
36 |
#'
|
|
37 |
#' @inherit shared_params return
|
|
38 |
#'
|
|
39 |
#' @note For more examples, please see the vignette "Using response plot" via
|
|
40 |
#' `vignette("using-response-plot", package = "teal.modules.general")`.
|
|
41 |
#'
|
|
42 |
#' @section Decorating Module:
|
|
43 |
#'
|
|
44 |
#' This module generates the following objects, which can be modified in place using decorators:
|
|
45 |
#' - `plot` (`ggplot`)
|
|
46 |
#'
|
|
47 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
48 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
49 |
#' See code snippet below:
|
|
50 |
#'
|
|
51 |
#' ```
|
|
52 |
#' tm_g_response(
|
|
53 |
#' ..., # arguments for module
|
|
54 |
#' decorators = list(
|
|
55 |
#' plot = teal_transform_module(...) # applied to the `plot` output
|
|
56 |
#' )
|
|
57 |
#' )
|
|
58 |
#' ```
|
|
59 |
#'
|
|
60 |
#' For additional details and examples of decorators, refer to the vignette
|
|
61 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
62 |
#'
|
|
63 |
#' To learn more please refer to the vignette
|
|
64 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
65 |
#'
|
|
66 |
#' @examplesShinylive
|
|
67 |
#' library(teal.modules.general)
|
|
68 |
#' interactive <- function() TRUE
|
|
69 |
#' {{ next_example }}
|
|
70 |
#' @examples
|
|
71 |
#' # general data example
|
|
72 |
#' data <- teal_data()
|
|
73 |
#' data <- within(data, {
|
|
74 |
#' require(nestcolor)
|
|
75 |
#' mtcars <- mtcars
|
|
76 |
#' for (v in c("cyl", "vs", "am", "gear")) {
|
|
77 |
#' mtcars[[v]] <- as.factor(mtcars[[v]])
|
|
78 |
#' }
|
|
79 |
#' })
|
|
80 |
#'
|
|
81 |
#' app <- init(
|
|
82 |
#' data = data,
|
|
83 |
#' modules = modules(
|
|
84 |
#' tm_g_response(
|
|
85 |
#' label = "Response Plots",
|
|
86 |
#' response = data_extract_spec(
|
|
87 |
#' dataname = "mtcars",
|
|
88 |
#' select = select_spec(
|
|
89 |
#' label = "Select variable:",
|
|
90 |
#' choices = variable_choices(data[["mtcars"]], c("cyl", "gear")),
|
|
91 |
#' selected = "cyl",
|
|
92 |
#' multiple = FALSE,
|
|
93 |
#' fixed = FALSE
|
|
94 |
#' )
|
|
95 |
#' ),
|
|
96 |
#' x = data_extract_spec(
|
|
97 |
#' dataname = "mtcars",
|
|
98 |
#' select = select_spec(
|
|
99 |
#' label = "Select variable:",
|
|
100 |
#' choices = variable_choices(data[["mtcars"]], c("vs", "am")),
|
|
101 |
#' selected = "vs",
|
|
102 |
#' multiple = FALSE,
|
|
103 |
#' fixed = FALSE
|
|
104 |
#' )
|
|
105 |
#' )
|
|
106 |
#' )
|
|
107 |
#' )
|
|
108 |
#' )
|
|
109 |
#' if (interactive()) {
|
|
110 |
#' shinyApp(app$ui, app$server)
|
|
111 |
#' }
|
|
112 |
#'
|
|
113 |
#' @examplesShinylive
|
|
114 |
#' library(teal.modules.general)
|
|
115 |
#' interactive <- function() TRUE
|
|
116 |
#' {{ next_example }}
|
|
117 |
#' @examples
|
|
118 |
#' # CDISC data example
|
|
119 |
#' data <- teal_data()
|
|
120 |
#' data <- within(data, {
|
|
121 |
#' require(nestcolor)
|
|
122 |
#' ADSL <- teal.data::rADSL
|
|
123 |
#' })
|
|
124 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
125 |
#'
|
|
126 |
#' app <- init(
|
|
127 |
#' data = data,
|
|
128 |
#' modules = modules(
|
|
129 |
#' tm_g_response(
|
|
130 |
#' label = "Response Plots",
|
|
131 |
#' response = data_extract_spec(
|
|
132 |
#' dataname = "ADSL",
|
|
133 |
#' select = select_spec(
|
|
134 |
#' label = "Select variable:",
|
|
135 |
#' choices = variable_choices(data[["ADSL"]], c("BMRKR2", "COUNTRY")),
|
|
136 |
#' selected = "BMRKR2",
|
|
137 |
#' multiple = FALSE,
|
|
138 |
#' fixed = FALSE
|
|
139 |
#' )
|
|
140 |
#' ),
|
|
141 |
#' x = data_extract_spec(
|
|
142 |
#' dataname = "ADSL",
|
|
143 |
#' select = select_spec(
|
|
144 |
#' label = "Select variable:",
|
|
145 |
#' choices = variable_choices(data[["ADSL"]], c("SEX", "RACE")),
|
|
146 |
#' selected = "RACE",
|
|
147 |
#' multiple = FALSE,
|
|
148 |
#' fixed = FALSE
|
|
149 |
#' )
|
|
150 |
#' )
|
|
151 |
#' )
|
|
152 |
#' )
|
|
153 |
#' )
|
|
154 |
#' if (interactive()) {
|
|
155 |
#' shinyApp(app$ui, app$server)
|
|
156 |
#' }
|
|
157 |
#'
|
|
158 |
#' @export
|
|
159 |
#'
|
|
160 |
tm_g_response <- function(label = "Response Plot", |
|
161 |
response,
|
|
162 |
x,
|
|
163 |
row_facet = NULL, |
|
164 |
col_facet = NULL, |
|
165 |
coord_flip = FALSE, |
|
166 |
count_labels = TRUE, |
|
167 |
rotate_xaxis_labels = FALSE, |
|
168 |
freq = FALSE, |
|
169 |
plot_height = c(600, 400, 5000), |
|
170 |
plot_width = NULL, |
|
171 |
ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), |
|
172 |
ggplot2_args = teal.widgets::ggplot2_args(), |
|
173 |
pre_output = NULL, |
|
174 |
post_output = NULL, |
|
175 |
transformators = list(), |
|
176 |
decorators = list()) { |
|
177 | ! |
message("Initializing tm_g_response") |
178 | ||
179 |
# Normalize the parameters
|
|
180 | ! |
if (inherits(response, "data_extract_spec")) response <- list(response) |
181 | ! |
if (inherits(x, "data_extract_spec")) x <- list(x) |
182 | ! |
if (inherits(row_facet, "data_extract_spec")) row_facet <- list(row_facet) |
183 | ! |
if (inherits(col_facet, "data_extract_spec")) col_facet <- list(col_facet) |
184 | ||
185 |
# Start of assertions
|
|
186 | ! |
checkmate::assert_string(label) |
187 | ||
188 | ! |
checkmate::assert_list(response, types = "data_extract_spec") |
189 | ! |
if (!all(vapply(response, function(x) !("" %in% x$select$choices), logical(1)))) { |
190 | ! |
stop("'response' should not allow empty values") |
191 |
}
|
|
192 | ! |
assert_single_selection(response) |
193 | ||
194 | ! |
checkmate::assert_list(x, types = "data_extract_spec") |
195 | ! |
if (!all(vapply(x, function(x) !("" %in% x$select$choices), logical(1)))) { |
196 | ! |
stop("'x' should not allow empty values") |
197 |
}
|
|
198 | ! |
assert_single_selection(x) |
199 | ||
200 | ! |
checkmate::assert_list(row_facet, types = "data_extract_spec", null.ok = TRUE) |
201 | ! |
checkmate::assert_list(col_facet, types = "data_extract_spec", null.ok = TRUE) |
202 | ! |
checkmate::assert_flag(coord_flip) |
203 | ! |
checkmate::assert_flag(count_labels) |
204 | ! |
checkmate::assert_flag(rotate_xaxis_labels) |
205 | ! |
checkmate::assert_flag(freq) |
206 | ||
207 | ! |
checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
208 | ! |
checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
209 | ! |
checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
210 | ! |
checkmate::assert_numeric( |
211 | ! |
plot_width[1], |
212 | ! |
lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width" |
213 |
)
|
|
214 | ||
215 | ! |
ggtheme <- match.arg(ggtheme) |
216 | ! |
checkmate::assert_class(ggplot2_args, "ggplot2_args") |
217 | ||
218 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
219 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
220 | ||
221 | ! |
assert_decorators(decorators, "plot") |
222 |
# End of assertions
|
|
223 | ||
224 |
# Make UI args
|
|
225 | ! |
args <- as.list(environment()) |
226 | ||
227 | ! |
data_extract_list <- list( |
228 | ! |
response = response, |
229 | ! |
x = x, |
230 | ! |
row_facet = row_facet, |
231 | ! |
col_facet = col_facet |
232 |
)
|
|
233 | ||
234 | ! |
ans <- module( |
235 | ! |
label = label, |
236 | ! |
server = srv_g_response, |
237 | ! |
ui = ui_g_response, |
238 | ! |
ui_args = args, |
239 | ! |
server_args = c( |
240 | ! |
data_extract_list,
|
241 | ! |
list( |
242 | ! |
plot_height = plot_height, |
243 | ! |
plot_width = plot_width, |
244 | ! |
ggplot2_args = ggplot2_args, |
245 | ! |
decorators = decorators |
246 |
)
|
|
247 |
),
|
|
248 | ! |
transformators = transformators, |
249 | ! |
datanames = teal.transform::get_extract_datanames(data_extract_list) |
250 |
)
|
|
251 | ! |
attr(ans, "teal_bookmarkable") <- TRUE |
252 | ! |
ans
|
253 |
}
|
|
254 | ||
255 |
# UI function for the response module
|
|
256 |
ui_g_response <- function(id, ...) { |
|
257 | ! |
ns <- NS(id) |
258 | ! |
args <- list(...) |
259 | ! |
is_single_dataset_value <- teal.transform::is_single_dataset(args$response, args$x, args$row_facet, args$col_facet) |
260 | ||
261 | ! |
teal.widgets::standard_layout( |
262 | ! |
output = teal.widgets::white_small_well( |
263 | ! |
teal.widgets::plot_with_settings_ui(id = ns("myplot")) |
264 |
),
|
|
265 | ! |
encoding = tags$div( |
266 |
### Reporter
|
|
267 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
268 |
###
|
|
269 | ! |
tags$label("Encodings", class = "text-primary"), |
270 | ! |
teal.transform::datanames_input(args[c("response", "x", "row_facet", "col_facet")]), |
271 | ! |
teal.transform::data_extract_ui( |
272 | ! |
id = ns("response"), |
273 | ! |
label = "Response variable", |
274 | ! |
data_extract_spec = args$response, |
275 | ! |
is_single_dataset = is_single_dataset_value |
276 |
),
|
|
277 | ! |
teal.transform::data_extract_ui( |
278 | ! |
id = ns("x"), |
279 | ! |
label = "X variable", |
280 | ! |
data_extract_spec = args$x, |
281 | ! |
is_single_dataset = is_single_dataset_value |
282 |
),
|
|
283 | ! |
if (!is.null(args$row_facet)) { |
284 | ! |
teal.transform::data_extract_ui( |
285 | ! |
id = ns("row_facet"), |
286 | ! |
label = "Row facetting", |
287 | ! |
data_extract_spec = args$row_facet, |
288 | ! |
is_single_dataset = is_single_dataset_value |
289 |
)
|
|
290 |
},
|
|
291 | ! |
if (!is.null(args$col_facet)) { |
292 | ! |
teal.transform::data_extract_ui( |
293 | ! |
id = ns("col_facet"), |
294 | ! |
label = "Column facetting", |
295 | ! |
data_extract_spec = args$col_facet, |
296 | ! |
is_single_dataset = is_single_dataset_value |
297 |
)
|
|
298 |
},
|
|
299 | ! |
shinyWidgets::radioGroupButtons( |
300 | ! |
inputId = ns("freq"), |
301 | ! |
label = NULL, |
302 | ! |
choices = c("frequency", "density"), |
303 | ! |
selected = ifelse(args$freq, "frequency", "density"), |
304 | ! |
justified = TRUE |
305 |
),
|
|
306 | ! |
ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")), |
307 | ! |
teal.widgets::panel_group( |
308 | ! |
teal.widgets::panel_item( |
309 | ! |
title = "Plot settings", |
310 | ! |
checkboxInput(ns("count_labels"), "Add count labels", value = args$count_labels), |
311 | ! |
checkboxInput(ns("coord_flip"), "Swap axes", value = args$coord_flip), |
312 | ! |
checkboxInput(ns("rotate_xaxis_labels"), "Rotate X axis labels", value = args$rotate_xaxis_labels), |
313 | ! |
selectInput( |
314 | ! |
inputId = ns("ggtheme"), |
315 | ! |
label = "Theme (by ggplot):", |
316 | ! |
choices = ggplot_themes, |
317 | ! |
selected = args$ggtheme, |
318 | ! |
multiple = FALSE |
319 |
)
|
|
320 |
)
|
|
321 |
)
|
|
322 |
),
|
|
323 | ! |
forms = tagList( |
324 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
325 |
),
|
|
326 | ! |
pre_output = args$pre_output, |
327 | ! |
post_output = args$post_output |
328 |
)
|
|
329 |
}
|
|
330 | ||
331 |
# Server function for the response module
|
|
332 |
srv_g_response <- function(id, |
|
333 |
data,
|
|
334 |
reporter,
|
|
335 |
filter_panel_api,
|
|
336 |
response,
|
|
337 |
x,
|
|
338 |
row_facet,
|
|
339 |
col_facet,
|
|
340 |
plot_height,
|
|
341 |
plot_width,
|
|
342 |
ggplot2_args,
|
|
343 |
decorators) { |
|
344 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
345 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
346 | ! |
checkmate::assert_class(data, "reactive") |
347 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
348 | ! |
moduleServer(id, function(input, output, session) { |
349 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
350 | ||
351 | ! |
data_extract <- list(response = response, x = x, row_facet = row_facet, col_facet = col_facet) |
352 | ||
353 | ! |
rule_diff <- function(other) { |
354 | ! |
function(value) { |
355 | ! |
if (other %in% names(selector_list())) { |
356 | ! |
othervalue <- selector_list()[[other]]()[["select"]] |
357 | ! |
if (!is.null(othervalue)) { |
358 | ! |
if (identical(value, othervalue)) { |
359 | ! |
"Row and column facetting variables must be different."
|
360 |
}
|
|
361 |
}
|
|
362 |
}
|
|
363 |
}
|
|
364 |
}
|
|
365 | ||
366 | ! |
selector_list <- teal.transform::data_extract_multiple_srv( |
367 | ! |
data_extract = data_extract, |
368 | ! |
datasets = data, |
369 | ! |
select_validation_rule = list( |
370 | ! |
response = shinyvalidate::sv_required("Please define a column for the response variable"), |
371 | ! |
x = shinyvalidate::sv_required("Please define a column for X variable"), |
372 | ! |
row_facet = shinyvalidate::compose_rules( |
373 | ! |
shinyvalidate::sv_optional(), |
374 | ! |
~ if (length(.) > 1) "There must be 1 or no row facetting variable.", |
375 | ! |
rule_diff("col_facet") |
376 |
),
|
|
377 | ! |
col_facet = shinyvalidate::compose_rules( |
378 | ! |
shinyvalidate::sv_optional(), |
379 | ! |
~ if (length(.) > 1) "There must be 1 or no column facetting variable.", |
380 | ! |
rule_diff("row_facet") |
381 |
)
|
|
382 |
)
|
|
383 |
)
|
|
384 | ||
385 | ! |
iv_r <- reactive({ |
386 | ! |
iv <- shinyvalidate::InputValidator$new() |
387 | ! |
iv$add_rule("ggtheme", shinyvalidate::sv_required("Please select a theme")) |
388 | ! |
teal.transform::compose_and_enable_validators(iv, selector_list) |
389 |
}) |
|
390 | ||
391 | ! |
anl_merged_input <- teal.transform::merge_expression_srv( |
392 | ! |
selector_list = selector_list, |
393 | ! |
datasets = data |
394 |
)
|
|
395 | ||
396 | ! |
qenv <- teal.code::eval_code( |
397 | ! |
data(), |
398 | ! |
'library("ggplot2");library("dplyr")' # nolint quotes |
399 |
)
|
|
400 | ||
401 | ! |
anl_merged_q <- reactive({ |
402 | ! |
req(anl_merged_input()) |
403 | ! |
qenv %>% |
404 | ! |
teal.code::eval_code(as.expression(anl_merged_input()$expr)) |
405 |
}) |
|
406 | ||
407 | ! |
merged <- list( |
408 | ! |
anl_input_r = anl_merged_input, |
409 | ! |
anl_q_r = anl_merged_q |
410 |
)
|
|
411 | ||
412 | ! |
output_q <- reactive({ |
413 | ! |
teal::validate_inputs(iv_r()) |
414 | ||
415 | ! |
qenv <- merged$anl_q_r() |
416 | ! |
ANL <- qenv[["ANL"]] |
417 | ! |
resp_var <- as.vector(merged$anl_input_r()$columns_source$response) |
418 | ! |
x <- as.vector(merged$anl_input_r()$columns_source$x) |
419 | ||
420 | ! |
validate(need(is.factor(ANL[[resp_var]]), "Please select a factor variable as the response.")) |
421 | ! |
validate(need(is.factor(ANL[[x]]), "Please select a factor variable as the X-Variable.")) |
422 | ! |
teal::validate_has_data(ANL, 10) |
423 | ! |
teal::validate_has_data(ANL[, c(resp_var, x)], 10, complete = TRUE, allow_inf = FALSE) |
424 | ||
425 | ! |
row_facet_name <- if (length(merged$anl_input_r()$columns_source$row_facet) == 0) { |
426 | ! |
character(0) |
427 |
} else { |
|
428 | ! |
as.vector(merged$anl_input_r()$columns_source$row_facet) |
429 |
}
|
|
430 | ! |
col_facet_name <- if (length(merged$anl_input_r()$columns_source$col_facet) == 0) { |
431 | ! |
character(0) |
432 |
} else { |
|
433 | ! |
as.vector(merged$anl_input_r()$columns_source$col_facet) |
434 |
}
|
|
435 | ||
436 | ! |
freq <- input$freq == "frequency" |
437 | ! |
swap_axes <- input$coord_flip |
438 | ! |
counts <- input$count_labels |
439 | ! |
rotate_xaxis_labels <- input$rotate_xaxis_labels |
440 | ! |
ggtheme <- input$ggtheme |
441 | ||
442 | ! |
arg_position <- if (freq) "stack" else "fill" |
443 | ||
444 | ! |
rowf <- if (length(row_facet_name) != 0) as.name(row_facet_name) |
445 | ! |
colf <- if (length(col_facet_name) != 0) as.name(col_facet_name) |
446 | ! |
resp_cl <- as.name(resp_var) |
447 | ! |
x_cl <- as.name(x) |
448 | ||
449 | ! |
if (swap_axes) { |
450 | ! |
qenv <- teal.code::eval_code( |
451 | ! |
qenv,
|
452 | ! |
substitute( |
453 | ! |
expr = ANL[[x]] <- with(ANL, forcats::fct_rev(x_cl)), |
454 | ! |
env = list(x = x, x_cl = x_cl) |
455 |
)
|
|
456 |
)
|
|
457 |
}
|
|
458 | ||
459 | ! |
qenv <- teal.code::eval_code( |
460 | ! |
qenv,
|
461 | ! |
substitute( |
462 | ! |
expr = ANL[[resp_var]] <- factor(ANL[[resp_var]]), |
463 | ! |
env = list(resp_var = resp_var) |
464 |
)
|
|
465 |
) %>% |
|
466 |
# rowf and colf will be a NULL if not set by a user
|
|
467 | ! |
teal.code::eval_code( |
468 | ! |
substitute( |
469 | ! |
expr = ANL2 <- ANL %>% |
470 | ! |
dplyr::group_by_at(dplyr::vars(x_cl, resp_cl, rowf, colf)) %>% |
471 | ! |
dplyr::summarise(ns = dplyr::n()) %>% |
472 | ! |
dplyr::group_by_at(dplyr::vars(x_cl, rowf, colf)) %>% |
473 | ! |
dplyr::mutate(sums = sum(ns), percent = round(ns / sums * 100, 1)), |
474 | ! |
env = list(x_cl = x_cl, resp_cl = resp_cl, rowf = rowf, colf = colf) |
475 |
)
|
|
476 |
) %>% |
|
477 | ! |
teal.code::eval_code( |
478 | ! |
substitute( |
479 | ! |
expr = ANL3 <- ANL %>% |
480 | ! |
dplyr::group_by_at(dplyr::vars(x_cl, rowf, colf)) %>% |
481 | ! |
dplyr::summarise(ns = dplyr::n()), |
482 | ! |
env = list(x_cl = x_cl, rowf = rowf, colf = colf) |
483 |
)
|
|
484 |
)
|
|
485 | ||
486 | ! |
plot_call <- substitute( |
487 | ! |
expr = ggplot2::ggplot(ANL2, ggplot2::aes(x = x_cl, y = ns)) + |
488 | ! |
ggplot2::geom_bar(ggplot2::aes(fill = resp_cl), stat = "identity", position = arg_position), |
489 | ! |
env = list( |
490 | ! |
x_cl = x_cl, |
491 | ! |
resp_cl = resp_cl, |
492 | ! |
arg_position = arg_position |
493 |
)
|
|
494 |
)
|
|
495 | ||
496 | ! |
if (!freq) { |
497 | ! |
plot_call <- substitute( |
498 | ! |
plot_call + ggplot2::expand_limits(y = c(0, 1.1)), |
499 | ! |
env = list(plot_call = plot_call) |
500 |
)
|
|
501 |
}
|
|
502 | ||
503 | ! |
if (counts) { |
504 | ! |
plot_call <- substitute( |
505 | ! |
expr = plot_call + |
506 | ! |
ggplot2::geom_text( |
507 | ! |
data = ANL2, |
508 | ! |
ggplot2::aes(label = ns, x = x_cl, y = ns, group = resp_cl), |
509 | ! |
col = "white", |
510 | ! |
vjust = "middle", |
511 | ! |
hjust = "middle", |
512 | ! |
position = position_anl2_value |
513 |
) + |
|
514 | ! |
ggplot2::geom_text( |
515 | ! |
data = ANL3, ggplot2::aes(label = ns, x = x_cl, y = anl3_y), |
516 | ! |
hjust = hjust_value, |
517 | ! |
vjust = vjust_value, |
518 | ! |
position = position_anl3_value |
519 |
),
|
|
520 | ! |
env = list( |
521 | ! |
plot_call = plot_call, |
522 | ! |
x_cl = x_cl, |
523 | ! |
resp_cl = resp_cl, |
524 | ! |
hjust_value = if (swap_axes) "left" else "middle", |
525 | ! |
vjust_value = if (swap_axes) "middle" else -1, |
526 | ! |
position_anl2_value = if (!freq) quote(position_fill(0.5)) else quote(position_stack(0.5)), # nolint: line_length. |
527 | ! |
anl3_y = if (!freq) 1.1 else as.name("ns"), |
528 | ! |
position_anl3_value = if (!freq) "fill" else "stack" |
529 |
)
|
|
530 |
)
|
|
531 |
}
|
|
532 | ||
533 | ! |
if (swap_axes) { |
534 | ! |
plot_call <- substitute(plot_call + coord_flip(), env = list(plot_call = plot_call)) |
535 |
}
|
|
536 | ||
537 | ! |
facet_cl <- facet_ggplot_call(row_facet_name, col_facet_name) |
538 | ||
539 | ! |
if (!is.null(facet_cl)) { |
540 | ! |
plot_call <- substitute(expr = plot_call + facet_cl, env = list(plot_call = plot_call, facet_cl = facet_cl)) |
541 |
}
|
|
542 | ||
543 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
544 | ! |
labs = list( |
545 | ! |
x = varname_w_label(x, ANL), |
546 | ! |
y = varname_w_label(resp_var, ANL, prefix = "Proportion of "), |
547 | ! |
fill = varname_w_label(resp_var, ANL) |
548 |
),
|
|
549 | ! |
theme = list(legend.position = "bottom") |
550 |
)
|
|
551 | ||
552 | ! |
if (rotate_xaxis_labels) { |
553 | ! |
dev_ggplot2_args$theme[["axis.text.x"]] <- quote(ggplot2::element_text(angle = 45, hjust = 1)) |
554 |
}
|
|
555 | ||
556 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
557 | ! |
user_plot = ggplot2_args, |
558 | ! |
module_plot = dev_ggplot2_args |
559 |
)
|
|
560 | ||
561 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
562 | ! |
all_ggplot2_args,
|
563 | ! |
ggtheme = ggtheme |
564 |
)
|
|
565 | ||
566 | ! |
plot_call <- substitute(expr = { |
567 | ! |
plot <- plot_call + labs + ggthemes + themes |
568 | ! |
}, env = list( |
569 | ! |
plot_call = plot_call, |
570 | ! |
labs = parsed_ggplot2_args$labs, |
571 | ! |
themes = parsed_ggplot2_args$theme, |
572 | ! |
ggthemes = parsed_ggplot2_args$ggtheme |
573 |
)) |
|
574 | ||
575 | ! |
teal.code::eval_code(qenv, plot_call) |
576 |
}) |
|
577 | ||
578 | ! |
decorated_output_plot_q <- srv_decorate_teal_data( |
579 | ! |
id = "decorator", |
580 | ! |
data = output_q, |
581 | ! |
decorators = select_decorators(decorators, "plot"), |
582 | ! |
expr = print(plot) |
583 |
)
|
|
584 | ||
585 | ! |
plot_r <- reactive(req(decorated_output_plot_q())[["plot"]]) |
586 | ||
587 |
# Insert the plot into a plot_with_settings module from teal.widgets
|
|
588 | ! |
pws <- teal.widgets::plot_with_settings_srv( |
589 | ! |
id = "myplot", |
590 | ! |
plot_r = plot_r, |
591 | ! |
height = plot_height, |
592 | ! |
width = plot_width |
593 |
)
|
|
594 | ||
595 |
# Render R code.
|
|
596 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_output_plot_q()))) |
597 | ||
598 | ! |
teal.widgets::verbatim_popup_srv( |
599 | ! |
id = "rcode", |
600 | ! |
verbatim_content = source_code_r, |
601 | ! |
title = "Show R Code for Response" |
602 |
)
|
|
603 | ||
604 |
### REPORTER
|
|
605 | ! |
if (with_reporter) { |
606 | ! |
card_fun <- function(comment, label) { |
607 | ! |
card <- teal::report_card_template( |
608 | ! |
title = "Response Plot", |
609 | ! |
label = label, |
610 | ! |
with_filter = with_filter, |
611 | ! |
filter_panel_api = filter_panel_api |
612 |
)
|
|
613 | ! |
card$append_text("Plot", "header3") |
614 | ! |
card$append_plot(plot_r(), dim = pws$dim()) |
615 | ! |
if (!comment == "") { |
616 | ! |
card$append_text("Comment", "header3") |
617 | ! |
card$append_text(comment) |
618 |
}
|
|
619 | ! |
card$append_src(source_code_r()) |
620 | ! |
card
|
621 |
}
|
|
622 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
623 |
}
|
|
624 |
###
|
|
625 |
}) |
|
626 |
}
|
1 |
#' `teal` module: Distribution analysis
|
|
2 |
#'
|
|
3 |
#' Module is designed to explore the distribution of a single variable within a given dataset.
|
|
4 |
#' It offers several tools, such as histograms, Q-Q plots, and various statistical tests to
|
|
5 |
#' visually and statistically analyze the variable's distribution.
|
|
6 |
#'
|
|
7 |
#' @inheritParams teal::module
|
|
8 |
#' @inheritParams teal.widgets::standard_layout
|
|
9 |
#' @inheritParams shared_params
|
|
10 |
#'
|
|
11 |
#' @param dist_var (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
12 |
#' Variable(s) for which the distribution will be analyzed.
|
|
13 |
#' @param strata_var (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
14 |
#' Categorical variable used to split the distribution analysis.
|
|
15 |
#' @param group_var (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
16 |
#' Variable used for faceting plot into multiple panels.
|
|
17 |
#' @param freq (`logical`) optional, whether to display frequency (`TRUE`) or density (`FALSE`).
|
|
18 |
#' Defaults to density (`FALSE`).
|
|
19 |
#' @param bins (`integer(1)` or `integer(3)`) optional, specifies the number of bins for the histogram.
|
|
20 |
#' - When the length of `bins` is one: The histogram bins will have a fixed size based on the `bins` provided.
|
|
21 |
#' - When the length of `bins` is three: The histogram bins are dynamically adjusted based on vector of `value`, `min`,
|
|
22 |
#' and `max`.
|
|
23 |
#' Defaults to `c(30L, 1L, 100L)`.
|
|
24 |
#'
|
|
25 |
#' @param ggplot2_args `r roxygen_ggplot2_args_param("Histogram", "QQplot")`
|
|
26 |
#'
|
|
27 |
#' @inherit shared_params return
|
|
28 |
#'
|
|
29 |
#' @section Decorating Module:
|
|
30 |
#'
|
|
31 |
#' This module generates the following objects, which can be modified in place using decorators::
|
|
32 |
#' - `histogram_plot` (`ggplot`)
|
|
33 |
#' - `qq_plot` (`ggplot`)
|
|
34 |
#' - `summary_table` (`datatables` created with [DT::datatable()])
|
|
35 |
#' - `test_table` (`datatables` created with [DT::datatable()])
|
|
36 |
#'
|
|
37 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
38 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
39 |
#' See code snippet below:
|
|
40 |
#'
|
|
41 |
#' ```
|
|
42 |
#' tm_g_distribution(
|
|
43 |
#' ..., # arguments for module
|
|
44 |
#' decorators = list(
|
|
45 |
#' histogram_plot = teal_transform_module(...), # applied only to `histogram_plot` output
|
|
46 |
#' qq_plot = teal_transform_module(...), # applied only to `qq_plot` output
|
|
47 |
#' summary_table = teal_transform_module(...), # applied only to `summary_table` output
|
|
48 |
#' test_table = teal_transform_module(...) # applied only to `test_table` output
|
|
49 |
#' )
|
|
50 |
#' )
|
|
51 |
#' ```
|
|
52 |
#'
|
|
53 |
#' For additional details and examples of decorators, refer to the vignette
|
|
54 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
55 |
#'
|
|
56 |
#' To learn more please refer to the vignette
|
|
57 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
58 |
#'
|
|
59 |
#' @examplesShinylive
|
|
60 |
#' library(teal.modules.general)
|
|
61 |
#' interactive <- function() TRUE
|
|
62 |
#' {{ next_example }}
|
|
63 |
# nolint start: line_length_linter.
|
|
64 |
#' @examples
|
|
65 |
# nolint end: line_length_linter.
|
|
66 |
#' # general data example
|
|
67 |
#' data <- teal_data()
|
|
68 |
#' data <- within(data, {
|
|
69 |
#' iris <- iris
|
|
70 |
#' })
|
|
71 |
#'
|
|
72 |
#' app <- init(
|
|
73 |
#' data = data,
|
|
74 |
#' modules = list(
|
|
75 |
#' tm_g_distribution(
|
|
76 |
#' dist_var = data_extract_spec(
|
|
77 |
#' dataname = "iris",
|
|
78 |
#' select = select_spec(variable_choices("iris"), "Petal.Length")
|
|
79 |
#' )
|
|
80 |
#' )
|
|
81 |
#' )
|
|
82 |
#' )
|
|
83 |
#' if (interactive()) {
|
|
84 |
#' shinyApp(app$ui, app$server)
|
|
85 |
#' }
|
|
86 |
#'
|
|
87 |
#' @examplesShinylive
|
|
88 |
#' library(teal.modules.general)
|
|
89 |
#' interactive <- function() TRUE
|
|
90 |
#' {{ next_example }}
|
|
91 |
# nolint start: line_length_linter.
|
|
92 |
#' @examples
|
|
93 |
# nolint end: line_length_linter.
|
|
94 |
#' # CDISC data example
|
|
95 |
#' data <- teal_data()
|
|
96 |
#' data <- within(data, {
|
|
97 |
#' ADSL <- teal.data::rADSL
|
|
98 |
#' })
|
|
99 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
100 |
#'
|
|
101 |
#' vars1 <- choices_selected(
|
|
102 |
#' variable_choices(data[["ADSL"]], c("ARM", "COUNTRY", "SEX")),
|
|
103 |
#' selected = NULL
|
|
104 |
#' )
|
|
105 |
#'
|
|
106 |
#' app <- init(
|
|
107 |
#' data = data,
|
|
108 |
#' modules = modules(
|
|
109 |
#' tm_g_distribution(
|
|
110 |
#' dist_var = data_extract_spec(
|
|
111 |
#' dataname = "ADSL",
|
|
112 |
#' select = select_spec(
|
|
113 |
#' choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1")),
|
|
114 |
#' selected = "BMRKR1",
|
|
115 |
#' multiple = FALSE,
|
|
116 |
#' fixed = FALSE
|
|
117 |
#' )
|
|
118 |
#' ),
|
|
119 |
#' strata_var = data_extract_spec(
|
|
120 |
#' dataname = "ADSL",
|
|
121 |
#' filter = filter_spec(
|
|
122 |
#' vars = vars1,
|
|
123 |
#' multiple = TRUE
|
|
124 |
#' )
|
|
125 |
#' ),
|
|
126 |
#' group_var = data_extract_spec(
|
|
127 |
#' dataname = "ADSL",
|
|
128 |
#' filter = filter_spec(
|
|
129 |
#' vars = vars1,
|
|
130 |
#' multiple = TRUE
|
|
131 |
#' )
|
|
132 |
#' )
|
|
133 |
#' )
|
|
134 |
#' )
|
|
135 |
#' )
|
|
136 |
#' if (interactive()) {
|
|
137 |
#' shinyApp(app$ui, app$server)
|
|
138 |
#' }
|
|
139 |
#'
|
|
140 |
#' @export
|
|
141 |
#'
|
|
142 |
tm_g_distribution <- function(label = "Distribution Module", |
|
143 |
dist_var,
|
|
144 |
strata_var = NULL, |
|
145 |
group_var = NULL, |
|
146 |
freq = FALSE, |
|
147 |
ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), |
|
148 |
ggplot2_args = teal.widgets::ggplot2_args(), |
|
149 |
bins = c(30L, 1L, 100L), |
|
150 |
plot_height = c(600, 200, 2000), |
|
151 |
plot_width = NULL, |
|
152 |
pre_output = NULL, |
|
153 |
post_output = NULL, |
|
154 |
transformators = list(), |
|
155 |
decorators = list()) { |
|
156 | ! |
message("Initializing tm_g_distribution") |
157 | ||
158 |
# Normalize the parameters
|
|
159 | ! |
if (inherits(dist_var, "data_extract_spec")) dist_var <- list(dist_var) |
160 | ! |
if (inherits(strata_var, "data_extract_spec")) strata_var <- list(strata_var) |
161 | ! |
if (inherits(group_var, "data_extract_spec")) group_var <- list(group_var) |
162 | ! |
if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args) |
163 | ||
164 |
# Start of assertions
|
|
165 | ! |
checkmate::assert_string(label) |
166 | ||
167 | ! |
checkmate::assert_list(dist_var, "data_extract_spec") |
168 | ! |
checkmate::assert_false(dist_var[[1L]]$select$multiple) |
169 | ||
170 | ! |
checkmate::assert_list(strata_var, types = "data_extract_spec", null.ok = TRUE) |
171 | ! |
checkmate::assert_list(group_var, types = "data_extract_spec", null.ok = TRUE) |
172 | ! |
checkmate::assert_flag(freq) |
173 | ! |
ggtheme <- match.arg(ggtheme) |
174 | ||
175 | ! |
plot_choices <- c("Histogram", "QQplot") |
176 | ! |
checkmate::assert_list(ggplot2_args, types = "ggplot2_args") |
177 | ! |
checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices)) |
178 | ||
179 | ! |
if (length(bins) == 1) { |
180 | ! |
checkmate::assert_numeric(bins, any.missing = FALSE, lower = 1) |
181 |
} else { |
|
182 | ! |
checkmate::assert_numeric(bins, len = 3, any.missing = FALSE, lower = 1) |
183 | ! |
checkmate::assert_numeric(bins[1], lower = bins[2], upper = bins[3], .var.name = "bins") |
184 |
}
|
|
185 | ||
186 | ! |
checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
187 | ! |
checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
188 | ! |
checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
189 | ! |
checkmate::assert_numeric( |
190 | ! |
plot_width[1], |
191 | ! |
lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width" |
192 |
)
|
|
193 | ||
194 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
195 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
196 | ||
197 | ! |
available_decorators <- c("histogram_plot", "qq_plot", "test_table", "summary_table") |
198 | ! |
assert_decorators(decorators, names = available_decorators) |
199 | ||
200 |
# End of assertions
|
|
201 | ||
202 |
# Make UI args
|
|
203 | ! |
args <- as.list(environment()) |
204 | ||
205 | ! |
data_extract_list <- list( |
206 | ! |
dist_var = dist_var, |
207 | ! |
strata_var = strata_var, |
208 | ! |
group_var = group_var |
209 |
)
|
|
210 | ||
211 | ! |
ans <- module( |
212 | ! |
label = label, |
213 | ! |
server = srv_distribution, |
214 | ! |
server_args = c( |
215 | ! |
data_extract_list,
|
216 | ! |
list( |
217 | ! |
plot_height = plot_height, |
218 | ! |
plot_width = plot_width, |
219 | ! |
ggplot2_args = ggplot2_args, |
220 | ! |
decorators = decorators |
221 |
)
|
|
222 |
),
|
|
223 | ! |
ui = ui_distribution, |
224 | ! |
ui_args = args, |
225 | ! |
transformators = transformators, |
226 | ! |
datanames = teal.transform::get_extract_datanames(data_extract_list) |
227 |
)
|
|
228 | ! |
attr(ans, "teal_bookmarkable") <- TRUE |
229 | ! |
ans
|
230 |
}
|
|
231 | ||
232 |
# UI function for the distribution module
|
|
233 |
ui_distribution <- function(id, ...) { |
|
234 | ! |
args <- list(...) |
235 | ! |
ns <- NS(id) |
236 | ! |
is_single_dataset_value <- teal.transform::is_single_dataset(args$dist_var, args$strata_var, args$group_var) |
237 | ||
238 | ! |
teal.widgets::standard_layout( |
239 | ! |
output = teal.widgets::white_small_well( |
240 | ! |
tabsetPanel( |
241 | ! |
id = ns("tabs"), |
242 | ! |
tabPanel("Histogram", teal.widgets::plot_with_settings_ui(id = ns("hist_plot"))), |
243 | ! |
tabPanel("QQplot", teal.widgets::plot_with_settings_ui(id = ns("qq_plot"))) |
244 |
),
|
|
245 | ! |
tags$h3("Statistics Table"), |
246 | ! |
DT::dataTableOutput(ns("summary_table")), |
247 | ! |
tags$h3("Tests"), |
248 | ! |
conditionalPanel( |
249 | ! |
sprintf("input['%s'].length === 0", ns("dist_tests")), |
250 | ! |
div( |
251 | ! |
id = ns("please_select_a_test"), |
252 | ! |
"Please select a test"
|
253 |
)
|
|
254 |
),
|
|
255 | ! |
conditionalPanel( |
256 | ! |
sprintf("input['%s'].length > 0", ns("dist_tests")), |
257 | ! |
DT::dataTableOutput(ns("t_stats")) |
258 |
)
|
|
259 |
),
|
|
260 | ! |
encoding = tags$div( |
261 |
### Reporter
|
|
262 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
263 |
###
|
|
264 | ! |
tags$label("Encodings", class = "text-primary"), |
265 | ! |
teal.transform::datanames_input(args[c("dist_var", "strata_var")]), |
266 | ! |
teal.transform::data_extract_ui( |
267 | ! |
id = ns("dist_i"), |
268 | ! |
label = "Variable", |
269 | ! |
data_extract_spec = args$dist_var, |
270 | ! |
is_single_dataset = is_single_dataset_value |
271 |
),
|
|
272 | ! |
if (!is.null(args$group_var)) { |
273 | ! |
tagList( |
274 | ! |
teal.transform::data_extract_ui( |
275 | ! |
id = ns("group_i"), |
276 | ! |
label = "Group by", |
277 | ! |
data_extract_spec = args$group_var, |
278 | ! |
is_single_dataset = is_single_dataset_value |
279 |
),
|
|
280 | ! |
uiOutput(ns("scales_types_ui")) |
281 |
)
|
|
282 |
},
|
|
283 | ! |
if (!is.null(args$strata_var)) { |
284 | ! |
teal.transform::data_extract_ui( |
285 | ! |
id = ns("strata_i"), |
286 | ! |
label = "Stratify by", |
287 | ! |
data_extract_spec = args$strata_var, |
288 | ! |
is_single_dataset = is_single_dataset_value |
289 |
)
|
|
290 |
},
|
|
291 | ! |
teal.widgets::panel_group( |
292 | ! |
conditionalPanel( |
293 | ! |
condition = paste0("input['", ns("tabs"), "'] == 'Histogram'"), |
294 | ! |
teal.widgets::panel_item( |
295 | ! |
"Histogram",
|
296 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("bins"), "Bins", args$bins, ticks = FALSE, step = 1), |
297 | ! |
shinyWidgets::prettyRadioButtons( |
298 | ! |
ns("main_type"), |
299 | ! |
label = "Plot Type:", |
300 | ! |
choices = c("Density", "Frequency"), |
301 | ! |
selected = if (!args$freq) "Density" else "Frequency", |
302 | ! |
bigger = FALSE, |
303 | ! |
inline = TRUE |
304 |
),
|
|
305 | ! |
checkboxInput(ns("add_dens"), label = "Overlay Density", value = TRUE), |
306 | ! |
ui_decorate_teal_data( |
307 | ! |
ns("d_density"), |
308 | ! |
decorators = select_decorators(args$decorators, "histogram_plot") |
309 |
),
|
|
310 | ! |
collapsed = FALSE |
311 |
)
|
|
312 |
),
|
|
313 | ! |
conditionalPanel( |
314 | ! |
condition = paste0("input['", ns("tabs"), "'] == 'QQplot'"), |
315 | ! |
teal.widgets::panel_item( |
316 | ! |
"QQ Plot",
|
317 | ! |
checkboxInput(ns("qq_line"), label = "Add diagonal line(s)", TRUE), |
318 | ! |
ui_decorate_teal_data( |
319 | ! |
ns("d_qq"), |
320 | ! |
decorators = select_decorators(args$decorators, "qq_plot") |
321 |
),
|
|
322 | ! |
collapsed = FALSE |
323 |
)
|
|
324 |
),
|
|
325 | ! |
ui_decorate_teal_data( |
326 | ! |
ns("d_summary"), |
327 | ! |
decorators = select_decorators(args$decorators, "summary_table") |
328 |
),
|
|
329 | ! |
ui_decorate_teal_data( |
330 | ! |
ns("d_test"), |
331 | ! |
decorators = select_decorators(args$decorators, "test_table") |
332 |
),
|
|
333 | ! |
conditionalPanel( |
334 | ! |
condition = paste0("input['", ns("main_type"), "'] == 'Density'"), |
335 | ! |
teal.widgets::panel_item( |
336 | ! |
"Theoretical Distribution",
|
337 | ! |
teal.widgets::optionalSelectInput( |
338 | ! |
ns("t_dist"), |
339 | ! |
tags$div( |
340 | ! |
class = "teal-tooltip", |
341 | ! |
tagList( |
342 | ! |
"Distribution:",
|
343 | ! |
icon("circle-info"), |
344 | ! |
tags$span( |
345 | ! |
class = "tooltiptext", |
346 | ! |
"Default parameters are optimized with MASS::fitdistr function."
|
347 |
)
|
|
348 |
)
|
|
349 |
),
|
|
350 | ! |
choices = c("normal", "lognormal", "gamma", "unif"), |
351 | ! |
selected = NULL, |
352 | ! |
multiple = FALSE |
353 |
),
|
|
354 | ! |
numericInput(ns("dist_param1"), label = "param1", value = NULL), |
355 | ! |
numericInput(ns("dist_param2"), label = "param2", value = NULL), |
356 | ! |
tags$span(actionButton(ns("params_reset"), "Default params")), |
357 | ! |
collapsed = FALSE |
358 |
)
|
|
359 |
)
|
|
360 |
),
|
|
361 | ! |
teal.widgets::panel_item( |
362 | ! |
"Tests",
|
363 | ! |
teal.widgets::optionalSelectInput( |
364 | ! |
ns("dist_tests"), |
365 | ! |
"Tests:",
|
366 | ! |
choices = c( |
367 | ! |
"Shapiro-Wilk",
|
368 | ! |
if (!is.null(args$strata_var)) "t-test (two-samples, not paired)", |
369 | ! |
if (!is.null(args$strata_var)) "one-way ANOVA", |
370 | ! |
if (!is.null(args$strata_var)) "Fligner-Killeen", |
371 | ! |
if (!is.null(args$strata_var)) "F-test", |
372 | ! |
"Kolmogorov-Smirnov (one-sample)",
|
373 | ! |
"Anderson-Darling (one-sample)",
|
374 | ! |
"Cramer-von Mises (one-sample)",
|
375 | ! |
if (!is.null(args$strata_var)) "Kolmogorov-Smirnov (two-samples)" |
376 |
),
|
|
377 | ! |
selected = NULL |
378 |
)
|
|
379 |
),
|
|
380 | ! |
teal.widgets::panel_item( |
381 | ! |
"Statistics Table",
|
382 | ! |
sliderInput(ns("roundn"), "Round to n digits", min = 0, max = 10, value = 2) |
383 |
),
|
|
384 | ! |
teal.widgets::panel_item( |
385 | ! |
title = "Plot settings", |
386 | ! |
selectInput( |
387 | ! |
inputId = ns("ggtheme"), |
388 | ! |
label = "Theme (by ggplot):", |
389 | ! |
choices = ggplot_themes, |
390 | ! |
selected = args$ggtheme, |
391 | ! |
multiple = FALSE |
392 |
)
|
|
393 |
)
|
|
394 |
),
|
|
395 | ! |
forms = tagList( |
396 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
397 |
),
|
|
398 | ! |
pre_output = args$pre_output, |
399 | ! |
post_output = args$post_output |
400 |
)
|
|
401 |
}
|
|
402 | ||
403 |
# Server function for the distribution module
|
|
404 |
srv_distribution <- function(id, |
|
405 |
data,
|
|
406 |
reporter,
|
|
407 |
filter_panel_api,
|
|
408 |
dist_var,
|
|
409 |
strata_var,
|
|
410 |
group_var,
|
|
411 |
plot_height,
|
|
412 |
plot_width,
|
|
413 |
ggplot2_args,
|
|
414 |
decorators) { |
|
415 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
416 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
417 | ! |
checkmate::assert_class(data, "reactive") |
418 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
419 | ! |
moduleServer(id, function(input, output, session) { |
420 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
421 | ||
422 | ! |
setBookmarkExclude("params_reset") |
423 | ||
424 | ! |
ns <- session$ns |
425 | ||
426 | ! |
rule_req <- function(value) { |
427 | ! |
if (isTRUE(input$dist_tests %in% c( |
428 | ! |
"Fligner-Killeen",
|
429 | ! |
"t-test (two-samples, not paired)",
|
430 | ! |
"F-test",
|
431 | ! |
"Kolmogorov-Smirnov (two-samples)",
|
432 | ! |
"one-way ANOVA"
|
433 |
))) { |
|
434 | ! |
if (!shinyvalidate::input_provided(value)) { |
435 | ! |
"Please select stratify variable."
|
436 |
}
|
|
437 |
}
|
|
438 |
}
|
|
439 | ! |
rule_dupl <- function(...) { |
440 | ! |
if (identical(input$dist_tests, "Fligner-Killeen")) { |
441 | ! |
strata <- selector_list()$strata_i()$select |
442 | ! |
group <- selector_list()$group_i()$select |
443 | ! |
if (isTRUE(strata == group)) { |
444 | ! |
"Please select different variables for strata and group."
|
445 |
}
|
|
446 |
}
|
|
447 |
}
|
|
448 | ||
449 | ! |
selector_list <- teal.transform::data_extract_multiple_srv( |
450 | ! |
data_extract = list( |
451 | ! |
dist_i = dist_var, |
452 | ! |
strata_i = strata_var, |
453 | ! |
group_i = group_var |
454 |
),
|
|
455 | ! |
data,
|
456 | ! |
select_validation_rule = list( |
457 | ! |
dist_i = shinyvalidate::sv_required("Please select a variable") |
458 |
),
|
|
459 | ! |
filter_validation_rule = list( |
460 | ! |
strata_i = shinyvalidate::compose_rules( |
461 | ! |
rule_req,
|
462 | ! |
rule_dupl
|
463 |
),
|
|
464 | ! |
group_i = rule_dupl |
465 |
)
|
|
466 |
)
|
|
467 | ||
468 | ! |
iv_r <- reactive({ |
469 | ! |
iv <- shinyvalidate::InputValidator$new() |
470 | ! |
teal.transform::compose_and_enable_validators(iv, selector_list, validator_names = "dist_i") |
471 |
}) |
|
472 | ||
473 | ! |
iv_r_dist <- reactive({ |
474 | ! |
iv <- shinyvalidate::InputValidator$new() |
475 | ! |
teal.transform::compose_and_enable_validators( |
476 | ! |
iv, selector_list, |
477 | ! |
validator_names = c("strata_i", "group_i") |
478 |
)
|
|
479 |
}) |
|
480 | ! |
rule_dist_1 <- function(value) { |
481 | ! |
if (!is.null(input$t_dist)) { |
482 | ! |
switch(input$t_dist, |
483 | ! |
"normal" = if (!shinyvalidate::input_provided(value)) "mean is required", |
484 | ! |
"lognormal" = if (!shinyvalidate::input_provided(value)) "meanlog is required", |
485 | ! |
"gamma" = { |
486 | ! |
if (!shinyvalidate::input_provided(value)) "shape is required" else if (value <= 0) "shape must be positive" |
487 |
},
|
|
488 | ! |
"unif" = NULL |
489 |
)
|
|
490 |
}
|
|
491 |
}
|
|
492 | ! |
rule_dist_2 <- function(value) { |
493 | ! |
if (!is.null(input$t_dist)) { |
494 | ! |
switch(input$t_dist, |
495 | ! |
"normal" = { |
496 | ! |
if (!shinyvalidate::input_provided(value)) { |
497 | ! |
"sd is required"
|
498 | ! |
} else if (value < 0) { |
499 | ! |
"sd must be non-negative"
|
500 |
}
|
|
501 |
},
|
|
502 | ! |
"lognormal" = { |
503 | ! |
if (!shinyvalidate::input_provided(value)) { |
504 | ! |
"sdlog is required"
|
505 | ! |
} else if (value < 0) { |
506 | ! |
"sdlog must be non-negative"
|
507 |
}
|
|
508 |
},
|
|
509 | ! |
"gamma" = { |
510 | ! |
if (!shinyvalidate::input_provided(value)) { |
511 | ! |
"rate is required"
|
512 | ! |
} else if (value <= 0) { |
513 | ! |
"rate must be positive"
|
514 |
}
|
|
515 |
},
|
|
516 | ! |
"unif" = NULL |
517 |
)
|
|
518 |
}
|
|
519 |
}
|
|
520 | ||
521 | ! |
rule_dist <- function(value) { |
522 | ! |
if (isTRUE(input$tabs == "QQplot") || |
523 | ! |
isTRUE(input$dist_tests %in% c( |
524 | ! |
"Kolmogorov-Smirnov (one-sample)",
|
525 | ! |
"Anderson-Darling (one-sample)",
|
526 | ! |
"Cramer-von Mises (one-sample)"
|
527 |
))) { |
|
528 | ! |
if (!shinyvalidate::input_provided(value)) { |
529 | ! |
"Please select the theoretical distribution."
|
530 |
}
|
|
531 |
}
|
|
532 |
}
|
|
533 | ||
534 | ! |
iv_dist <- shinyvalidate::InputValidator$new() |
535 | ! |
iv_dist$add_rule("t_dist", rule_dist) |
536 | ! |
iv_dist$add_rule("dist_param1", rule_dist_1) |
537 | ! |
iv_dist$add_rule("dist_param2", rule_dist_2) |
538 | ! |
iv_dist$enable() |
539 | ||
540 | ! |
anl_merged_input <- teal.transform::merge_expression_srv( |
541 | ! |
selector_list = selector_list, |
542 | ! |
datasets = data |
543 |
)
|
|
544 | ||
545 | ! |
qenv <- teal.code::eval_code( |
546 | ! |
data(), |
547 | ! |
'library("ggplot2");library("dplyr")' # nolint quotes |
548 |
)
|
|
549 | ||
550 | ! |
anl_merged_q <- reactive({ |
551 | ! |
req(anl_merged_input()) |
552 | ! |
qenv %>% |
553 | ! |
teal.code::eval_code(as.expression(anl_merged_input()$expr)) |
554 |
}) |
|
555 | ||
556 | ! |
merged <- list( |
557 | ! |
anl_input_r = anl_merged_input, |
558 | ! |
anl_q_r = anl_merged_q |
559 |
)
|
|
560 | ||
561 | ! |
output$scales_types_ui <- renderUI({ |
562 | ! |
if ("group_i" %in% names(selector_list()) && length(selector_list()$group_i()$filters[[1]]$selected) > 0) { |
563 | ! |
shinyWidgets::prettyRadioButtons( |
564 | ! |
ns("scales_type"), |
565 | ! |
label = "Scales:", |
566 | ! |
choices = c("Fixed", "Free"), |
567 | ! |
selected = "Fixed", |
568 | ! |
bigger = FALSE, |
569 | ! |
inline = TRUE |
570 |
)
|
|
571 |
}
|
|
572 |
}) |
|
573 | ||
574 | ! |
observeEvent( |
575 | ! |
eventExpr = list( |
576 | ! |
input$t_dist, |
577 | ! |
input$params_reset, |
578 | ! |
selector_list()$dist_i()$select |
579 |
),
|
|
580 | ! |
handlerExpr = { |
581 | ! |
params <- |
582 | ! |
if (length(input$t_dist) != 0) { |
583 | ! |
get_dist_params <- function(x, dist) { |
584 | ! |
if (dist == "unif") { |
585 | ! |
return(stats::setNames(range(x, na.rm = TRUE), c("min", "max"))) |
586 |
}
|
|
587 | ! |
tryCatch( |
588 | ! |
MASS::fitdistr(x, densfun = dist)$estimate, |
589 | ! |
error = function(e) c(param1 = NA_real_, param2 = NA_real_) |
590 |
)
|
|
591 |
}
|
|
592 | ||
593 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
594 | ! |
round(get_dist_params(as.numeric(stats::na.omit(ANL[[merge_vars()$dist_var]])), input$t_dist), 2) |
595 |
} else { |
|
596 | ! |
c("param1" = NA_real_, "param2" = NA_real_) |
597 |
}
|
|
598 | ||
599 | ! |
params_vals <- unname(params) |
600 | ! |
params_names <- names(params) |
601 | ||
602 | ! |
updateNumericInput( |
603 | ! |
inputId = "dist_param1", |
604 | ! |
label = params_names[1], |
605 | ! |
value = restoreInput(ns("dist_param1"), params_vals[1]) |
606 |
)
|
|
607 | ! |
updateNumericInput( |
608 | ! |
inputId = "dist_param2", |
609 | ! |
label = params_names[2], |
610 | ! |
value = restoreInput(ns("dist_param1"), params_vals[2]) |
611 |
)
|
|
612 |
},
|
|
613 | ! |
ignoreInit = TRUE |
614 |
)
|
|
615 | ||
616 | ! |
observeEvent(input$params_reset, { |
617 | ! |
updateActionButton(inputId = "params_reset", label = "Reset params") |
618 |
}) |
|
619 | ||
620 | ! |
merge_vars <- reactive({ |
621 | ! |
teal::validate_inputs(iv_r()) |
622 | ||
623 | ! |
dist_var <- as.vector(merged$anl_input_r()$columns_source$dist_i) |
624 | ! |
s_var <- as.vector(merged$anl_input_r()$columns_source$strata_i) |
625 | ! |
g_var <- as.vector(merged$anl_input_r()$columns_source$group_i) |
626 | ||
627 | ! |
dist_var_name <- if (length(dist_var)) as.name(dist_var) else NULL |
628 | ! |
s_var_name <- if (length(s_var)) as.name(s_var) else NULL |
629 | ! |
g_var_name <- if (length(g_var)) as.name(g_var) else NULL |
630 | ||
631 | ! |
list( |
632 | ! |
dist_var = dist_var, |
633 | ! |
s_var = s_var, |
634 | ! |
g_var = g_var, |
635 | ! |
dist_var_name = dist_var_name, |
636 | ! |
s_var_name = s_var_name, |
637 | ! |
g_var_name = g_var_name |
638 |
)
|
|
639 |
}) |
|
640 | ||
641 |
# common qenv
|
|
642 | ! |
common_q <- reactive({ |
643 |
# Create a private stack for this function only.
|
|
644 | ||
645 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
646 | ! |
dist_var <- merge_vars()$dist_var |
647 | ! |
s_var <- merge_vars()$s_var |
648 | ! |
g_var <- merge_vars()$g_var |
649 | ||
650 | ! |
dist_var_name <- merge_vars()$dist_var_name |
651 | ! |
s_var_name <- merge_vars()$s_var_name |
652 | ! |
g_var_name <- merge_vars()$g_var_name |
653 | ||
654 | ! |
roundn <- input$roundn |
655 | ! |
dist_param1 <- input$dist_param1 |
656 | ! |
dist_param2 <- input$dist_param2 |
657 |
# isolated as dist_param1/dist_param2 already triggered the reactivity
|
|
658 | ! |
t_dist <- isolate(input$t_dist) |
659 | ||
660 | ! |
qenv <- merged$anl_q_r() |
661 | ||
662 | ! |
if (length(g_var) > 0) { |
663 | ! |
validate( |
664 | ! |
need( |
665 | ! |
inherits(ANL[[g_var]], c("integer", "factor", "character")), |
666 | ! |
"Group by variable must be `factor`, `character`, or `integer`"
|
667 |
)
|
|
668 |
)
|
|
669 | ! |
qenv <- teal.code::eval_code(qenv, 'library("forcats")') # nolint quotes |
670 | ! |
qenv <- teal.code::eval_code( |
671 | ! |
qenv,
|
672 | ! |
substitute( |
673 | ! |
expr = ANL[[g_var]] <- forcats::fct_na_value_to_level(as.factor(ANL[[g_var]]), "NA"), |
674 | ! |
env = list(g_var = g_var) |
675 |
)
|
|
676 |
)
|
|
677 |
}
|
|
678 | ||
679 | ! |
if (length(s_var) > 0) { |
680 | ! |
validate( |
681 | ! |
need( |
682 | ! |
inherits(ANL[[s_var]], c("integer", "factor", "character")), |
683 | ! |
"Stratify by variable must be `factor`, `character`, or `integer`"
|
684 |
)
|
|
685 |
)
|
|
686 | ||
687 | ! |
qenv <- teal.code::eval_code(qenv, 'library("forcats")') # nolint quotes |
688 | ! |
qenv <- teal.code::eval_code( |
689 | ! |
qenv,
|
690 | ! |
substitute( |
691 | ! |
expr = ANL[[s_var]] <- forcats::fct_na_value_to_level(as.factor(ANL[[s_var]]), "NA"), |
692 | ! |
env = list(s_var = s_var) |
693 |
)
|
|
694 |
)
|
|
695 |
}
|
|
696 | ||
697 | ! |
validate(need(is.numeric(ANL[[dist_var]]), "Please select a numeric variable.")) |
698 | ! |
teal::validate_has_data(ANL, 1, complete = TRUE) |
699 | ||
700 | ! |
if (length(t_dist) != 0) { |
701 | ! |
map_distr_nams <- list( |
702 | ! |
normal = c("mean", "sd"), |
703 | ! |
lognormal = c("meanlog", "sdlog"), |
704 | ! |
gamma = c("shape", "rate"), |
705 | ! |
unif = c("min", "max") |
706 |
)
|
|
707 | ! |
params_names_raw <- map_distr_nams[[t_dist]] |
708 | ||
709 | ! |
qenv <- teal.code::eval_code( |
710 | ! |
qenv,
|
711 | ! |
substitute( |
712 | ! |
expr = { |
713 | ! |
params <- as.list(c(dist_param1, dist_param2)) |
714 | ! |
names(params) <- params_names_raw |
715 |
},
|
|
716 | ! |
env = list( |
717 | ! |
dist_param1 = dist_param1, |
718 | ! |
dist_param2 = dist_param2, |
719 | ! |
params_names_raw = params_names_raw |
720 |
)
|
|
721 |
)
|
|
722 |
)
|
|
723 |
}
|
|
724 | ||
725 | ! |
if (length(s_var) == 0 && length(g_var) == 0) { |
726 | ! |
teal.code::eval_code( |
727 | ! |
qenv,
|
728 | ! |
substitute( |
729 | ! |
expr = { |
730 | ! |
summary_table_data <- ANL %>% |
731 | ! |
dplyr::summarise( |
732 | ! |
min = round(min(dist_var_name, na.rm = TRUE), roundn), |
733 | ! |
median = round(stats::median(dist_var_name, na.rm = TRUE), roundn), |
734 | ! |
mean = round(mean(dist_var_name, na.rm = TRUE), roundn), |
735 | ! |
max = round(max(dist_var_name, na.rm = TRUE), roundn), |
736 | ! |
sd = round(stats::sd(dist_var_name, na.rm = TRUE), roundn), |
737 | ! |
count = dplyr::n() |
738 |
)
|
|
739 |
},
|
|
740 | ! |
env = list( |
741 | ! |
dist_var_name = as.name(dist_var), |
742 | ! |
roundn = roundn |
743 |
)
|
|
744 |
)
|
|
745 |
)
|
|
746 |
} else { |
|
747 | ! |
teal.code::eval_code( |
748 | ! |
qenv,
|
749 | ! |
substitute( |
750 | ! |
expr = { |
751 | ! |
strata_vars <- strata_vars_raw |
752 | ! |
summary_table_data <- ANL %>% |
753 | ! |
dplyr::group_by_at(dplyr::vars(dplyr::any_of(strata_vars))) %>% |
754 | ! |
dplyr::summarise( |
755 | ! |
min = round(min(dist_var_name, na.rm = TRUE), roundn), |
756 | ! |
median = round(stats::median(dist_var_name, na.rm = TRUE), roundn), |
757 | ! |
mean = round(mean(dist_var_name, na.rm = TRUE), roundn), |
758 | ! |
max = round(max(dist_var_name, na.rm = TRUE), roundn), |
759 | ! |
sd = round(stats::sd(dist_var_name, na.rm = TRUE), roundn), |
760 | ! |
count = dplyr::n() |
761 |
)
|
|
762 |
},
|
|
763 | ! |
env = list( |
764 | ! |
dist_var_name = dist_var_name, |
765 | ! |
strata_vars_raw = c(g_var, s_var), |
766 | ! |
roundn = roundn |
767 |
)
|
|
768 |
)
|
|
769 |
)
|
|
770 |
}
|
|
771 |
}) |
|
772 | ||
773 |
# distplot qenv ----
|
|
774 | ! |
dist_q <- eventReactive( |
775 | ! |
eventExpr = { |
776 | ! |
common_q() |
777 | ! |
input$scales_type |
778 | ! |
input$main_type |
779 | ! |
input$bins |
780 | ! |
input$add_dens |
781 | ! |
is.null(input$ggtheme) |
782 |
},
|
|
783 | ! |
valueExpr = { |
784 | ! |
dist_var <- merge_vars()$dist_var |
785 | ! |
s_var <- merge_vars()$s_var |
786 | ! |
g_var <- merge_vars()$g_var |
787 | ! |
dist_var_name <- merge_vars()$dist_var_name |
788 | ! |
s_var_name <- merge_vars()$s_var_name |
789 | ! |
g_var_name <- merge_vars()$g_var_name |
790 | ! |
t_dist <- input$t_dist |
791 | ! |
dist_param1 <- input$dist_param1 |
792 | ! |
dist_param2 <- input$dist_param2 |
793 | ||
794 | ! |
scales_type <- input$scales_type |
795 | ||
796 | ! |
ndensity <- 512 |
797 | ! |
main_type_var <- input$main_type |
798 | ! |
bins_var <- input$bins |
799 | ! |
add_dens_var <- input$add_dens |
800 | ! |
ggtheme <- input$ggtheme |
801 | ||
802 | ! |
teal::validate_inputs(iv_dist) |
803 | ||
804 | ! |
qenv <- common_q() |
805 | ||
806 | ! |
m_type <- if (main_type_var == "Density") "density" else "count" |
807 | ||
808 | ! |
plot_call <- if (length(s_var) == 0 && length(g_var) == 0) { |
809 | ! |
substitute( |
810 | ! |
expr = ggplot2::ggplot(ANL, ggplot2::aes(dist_var_name)) + |
811 | ! |
ggplot2::geom_histogram( |
812 | ! |
position = "identity", ggplot2::aes(y = ggplot2::after_stat(m_type)), bins = bins_var, alpha = 0.3 |
813 |
),
|
|
814 | ! |
env = list( |
815 | ! |
m_type = as.name(m_type), bins_var = bins_var, dist_var_name = as.name(dist_var) |
816 |
)
|
|
817 |
)
|
|
818 | ! |
} else if (length(s_var) != 0 && length(g_var) == 0) { |
819 | ! |
substitute( |
820 | ! |
expr = ggplot2::ggplot(ANL, ggplot2::aes(dist_var_name, col = s_var_name)) + |
821 | ! |
ggplot2::geom_histogram( |
822 | ! |
position = "identity", ggplot2::aes(y = ggplot2::after_stat(m_type), fill = s_var), |
823 | ! |
bins = bins_var, alpha = 0.3 |
824 |
),
|
|
825 | ! |
env = list( |
826 | ! |
m_type = as.name(m_type), |
827 | ! |
bins_var = bins_var, |
828 | ! |
dist_var_name = dist_var_name, |
829 | ! |
s_var = as.name(s_var), |
830 | ! |
s_var_name = s_var_name |
831 |
)
|
|
832 |
)
|
|
833 | ! |
} else if (length(s_var) == 0 && length(g_var) != 0) { |
834 | ! |
req(scales_type) |
835 | ! |
substitute( |
836 | ! |
expr = ggplot2::ggplot(ANL[ANL[[g_var]] != "NA", ], ggplot2::aes(dist_var_name)) + |
837 | ! |
ggplot2::geom_histogram( |
838 | ! |
position = "identity", ggplot2::aes(y = ggplot2::after_stat(m_type)), bins = bins_var, alpha = 0.3 |
839 |
) + |
|
840 | ! |
ggplot2::facet_wrap(~g_var_name, ncol = 1, scales = scales_raw), |
841 | ! |
env = list( |
842 | ! |
m_type = as.name(m_type), |
843 | ! |
bins_var = bins_var, |
844 | ! |
dist_var_name = dist_var_name, |
845 | ! |
g_var = g_var, |
846 | ! |
g_var_name = g_var_name, |
847 | ! |
scales_raw = tolower(scales_type) |
848 |
)
|
|
849 |
)
|
|
850 |
} else { |
|
851 | ! |
req(scales_type) |
852 | ! |
substitute( |
853 | ! |
expr = ggplot2::ggplot(ANL[ANL[[g_var]] != "NA", ], ggplot2::aes(dist_var_name, col = s_var_name)) + |
854 | ! |
ggplot2::geom_histogram( |
855 | ! |
position = "identity", |
856 | ! |
ggplot2::aes(y = ggplot2::after_stat(m_type), fill = s_var), bins = bins_var, alpha = 0.3 |
857 |
) + |
|
858 | ! |
ggplot2::facet_wrap(~g_var_name, ncol = 1, scales = scales_raw), |
859 | ! |
env = list( |
860 | ! |
m_type = as.name(m_type), |
861 | ! |
bins_var = bins_var, |
862 | ! |
dist_var_name = dist_var_name, |
863 | ! |
g_var = g_var, |
864 | ! |
s_var = as.name(s_var), |
865 | ! |
g_var_name = g_var_name, |
866 | ! |
s_var_name = s_var_name, |
867 | ! |
scales_raw = tolower(scales_type) |
868 |
)
|
|
869 |
)
|
|
870 |
}
|
|
871 | ||
872 | ! |
if (add_dens_var) { |
873 | ! |
plot_call <- substitute( |
874 | ! |
expr = plot_call + |
875 | ! |
ggplot2::stat_density( |
876 | ! |
ggplot2::aes(y = ggplot2::after_stat(const * m_type2)), |
877 | ! |
geom = "line", |
878 | ! |
position = "identity", |
879 | ! |
alpha = 0.5, |
880 | ! |
size = 2, |
881 | ! |
n = ndensity |
882 |
),
|
|
883 | ! |
env = list( |
884 | ! |
plot_call = plot_call, |
885 | ! |
const = if (main_type_var == "Density") { |
886 | ! |
1
|
887 |
} else { |
|
888 | ! |
diff(range(qenv[["ANL"]][[dist_var]], na.rm = TRUE)) / bins_var |
889 |
},
|
|
890 | ! |
m_type2 = if (main_type_var == "Density") as.name("density") else as.name("count"), |
891 | ! |
ndensity = ndensity |
892 |
)
|
|
893 |
)
|
|
894 |
}
|
|
895 | ||
896 | ! |
if (length(t_dist) != 0 && main_type_var == "Density" && length(g_var) == 0 && length(s_var) == 0) { |
897 | ! |
qenv <- teal.code::eval_code(qenv, 'library("ggpp")') # nolint quotes |
898 | ! |
qenv <- teal.code::eval_code( |
899 | ! |
qenv,
|
900 | ! |
substitute( |
901 | ! |
df_params <- as.data.frame(append(params, list(name = t_dist))), |
902 | ! |
env = list(t_dist = t_dist) |
903 |
)
|
|
904 |
)
|
|
905 | ! |
datas <- quote(data.frame(x = 0.7, y = 1, tb = I(list(df_params = df_params)))) |
906 | ! |
label <- quote(tb) |
907 | ||
908 | ! |
plot_call <- substitute( |
909 | ! |
expr = plot_call + ggpp::geom_table_npc( |
910 | ! |
data = data, |
911 | ! |
ggplot2::aes(npcx = x, npcy = y, label = label), |
912 | ! |
hjust = 0, vjust = 1, size = 4 |
913 |
),
|
|
914 | ! |
env = list(plot_call = plot_call, data = datas, label = label) |
915 |
)
|
|
916 |
}
|
|
917 | ||
918 | ! |
if ( |
919 | ! |
length(s_var) == 0 && |
920 | ! |
length(g_var) == 0 && |
921 | ! |
main_type_var == "Density" && |
922 | ! |
length(t_dist) != 0 && |
923 | ! |
main_type_var == "Density" |
924 |
) { |
|
925 | ! |
map_dist <- stats::setNames( |
926 | ! |
c("dnorm", "dlnorm", "dgamma", "dunif"), |
927 | ! |
c("normal", "lognormal", "gamma", "unif") |
928 |
)
|
|
929 | ! |
plot_call <- substitute( |
930 | ! |
expr = plot_call + stat_function( |
931 | ! |
data = data.frame(x = range(ANL[[dist_var]]), color = mapped_dist), |
932 | ! |
ggplot2::aes(x, color = color), |
933 | ! |
fun = mapped_dist_name, |
934 | ! |
n = ndensity, |
935 | ! |
size = 2, |
936 | ! |
args = params |
937 |
) + |
|
938 | ! |
ggplot2::scale_color_manual(values = stats::setNames("blue", mapped_dist), aesthetics = "color"), |
939 | ! |
env = list( |
940 | ! |
plot_call = plot_call, |
941 | ! |
dist_var = dist_var, |
942 | ! |
ndensity = ndensity, |
943 | ! |
mapped_dist = unname(map_dist[t_dist]), |
944 | ! |
mapped_dist_name = as.name(unname(map_dist[t_dist])) |
945 |
)
|
|
946 |
)
|
|
947 |
}
|
|
948 | ||
949 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
950 | ! |
user_plot = ggplot2_args[["Histogram"]], |
951 | ! |
user_default = ggplot2_args$default |
952 |
)
|
|
953 | ||
954 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
955 | ! |
all_ggplot2_args,
|
956 | ! |
ggtheme = ggtheme |
957 |
)
|
|
958 | ||
959 | ! |
teal.code::eval_code( |
960 | ! |
qenv,
|
961 | ! |
substitute( |
962 | ! |
expr = histogram_plot <- plot_call, |
963 | ! |
env = list(plot_call = Reduce(function(x, y) call("+", x, y), c(plot_call, parsed_ggplot2_args))) |
964 |
)
|
|
965 |
)
|
|
966 |
}
|
|
967 |
)
|
|
968 | ||
969 |
# qqplot qenv ----
|
|
970 | ! |
qq_q <- eventReactive( |
971 | ! |
eventExpr = { |
972 | ! |
common_q() |
973 | ! |
input$scales_type |
974 | ! |
input$qq_line |
975 | ! |
is.null(input$ggtheme) |
976 | ! |
input$tabs |
977 |
},
|
|
978 | ! |
valueExpr = { |
979 | ! |
dist_var <- merge_vars()$dist_var |
980 | ! |
s_var <- merge_vars()$s_var |
981 | ! |
g_var <- merge_vars()$g_var |
982 | ! |
dist_var_name <- merge_vars()$dist_var_name |
983 | ! |
s_var_name <- merge_vars()$s_var_name |
984 | ! |
g_var_name <- merge_vars()$g_var_name |
985 | ! |
dist_param1 <- input$dist_param1 |
986 | ! |
dist_param2 <- input$dist_param2 |
987 | ||
988 | ! |
scales_type <- input$scales_type |
989 | ! |
ggtheme <- input$ggtheme |
990 | ||
991 | ! |
teal::validate_inputs(iv_r_dist(), iv_dist) |
992 | ! |
t_dist <- req(input$t_dist) # Not validated when tab is not selected |
993 | ! |
qenv <- common_q() |
994 | ||
995 | ! |
plot_call <- if (length(s_var) == 0 && length(g_var) == 0) { |
996 | ! |
substitute( |
997 | ! |
expr = ggplot2::ggplot(ANL, ggplot2::aes_string(sample = dist_var)), |
998 | ! |
env = list(dist_var = dist_var) |
999 |
)
|
|
1000 | ! |
} else if (length(s_var) != 0 && length(g_var) == 0) { |
1001 | ! |
substitute( |
1002 | ! |
expr = ggplot2::ggplot(ANL, ggplot2::aes_string(sample = dist_var, color = s_var)), |
1003 | ! |
env = list(dist_var = dist_var, s_var = s_var) |
1004 |
)
|
|
1005 | ! |
} else if (length(s_var) == 0 && length(g_var) != 0) { |
1006 | ! |
substitute( |
1007 | ! |
expr = ggplot2::ggplot(ANL[ANL[[g_var]] != "NA", ], ggplot2::aes_string(sample = dist_var)) + |
1008 | ! |
ggplot2::facet_wrap(~g_var_name, ncol = 1, scales = scales_raw), |
1009 | ! |
env = list( |
1010 | ! |
dist_var = dist_var, |
1011 | ! |
g_var = g_var, |
1012 | ! |
g_var_name = g_var_name, |
1013 | ! |
scales_raw = tolower(scales_type) |
1014 |
)
|
|
1015 |
)
|
|
1016 |
} else { |
|
1017 | ! |
substitute( |
1018 | ! |
expr = ggplot2::ggplot(ANL[ANL[[g_var]] != "NA", ], ggplot2::aes_string(sample = dist_var, color = s_var)) + |
1019 | ! |
ggplot2::facet_wrap(~g_var_name, ncol = 1, scales = scales_raw), |
1020 | ! |
env = list( |
1021 | ! |
dist_var = dist_var, |
1022 | ! |
g_var = g_var, |
1023 | ! |
s_var = s_var, |
1024 | ! |
g_var_name = g_var_name, |
1025 | ! |
scales_raw = tolower(scales_type) |
1026 |
)
|
|
1027 |
)
|
|
1028 |
}
|
|
1029 | ||
1030 | ! |
map_dist <- stats::setNames( |
1031 | ! |
c("qnorm", "qlnorm", "qgamma", "qunif"), |
1032 | ! |
c("normal", "lognormal", "gamma", "unif") |
1033 |
)
|
|
1034 | ||
1035 | ! |
plot_call <- substitute( |
1036 | ! |
expr = plot_call + |
1037 | ! |
ggplot2::stat_qq(distribution = mapped_dist, dparams = params), |
1038 | ! |
env = list(plot_call = plot_call, mapped_dist = as.name(unname(map_dist[t_dist]))) |
1039 |
)
|
|
1040 | ||
1041 | ! |
if (length(t_dist) != 0 && length(g_var) == 0 && length(s_var) == 0) { |
1042 | ! |
qenv <- teal.code::eval_code(qenv, 'library("ggpp")') # nolint quotes |
1043 | ! |
qenv <- teal.code::eval_code( |
1044 | ! |
qenv,
|
1045 | ! |
substitute( |
1046 | ! |
df_params <- as.data.frame(append(params, list(name = t_dist))), |
1047 | ! |
env = list(t_dist = t_dist) |
1048 |
)
|
|
1049 |
)
|
|
1050 | ! |
datas <- quote(data.frame(x = 0.7, y = 1, tb = I(list(df_params = df_params)))) |
1051 | ! |
label <- quote(tb) |
1052 | ||
1053 | ! |
plot_call <- substitute( |
1054 | ! |
expr = plot_call + |
1055 | ! |
ggpp::geom_table_npc( |
1056 | ! |
data = data, |
1057 | ! |
ggplot2::aes(npcx = x, npcy = y, label = label), |
1058 | ! |
hjust = 0, |
1059 | ! |
vjust = 1, |
1060 | ! |
size = 4 |
1061 |
),
|
|
1062 | ! |
env = list( |
1063 | ! |
plot_call = plot_call, |
1064 | ! |
data = datas, |
1065 | ! |
label = label |
1066 |
)
|
|
1067 |
)
|
|
1068 |
}
|
|
1069 | ||
1070 | ! |
if (isTRUE(input$qq_line)) { |
1071 | ! |
plot_call <- substitute( |
1072 | ! |
expr = plot_call + |
1073 | ! |
ggplot2::stat_qq_line(distribution = mapped_dist, dparams = params), |
1074 | ! |
env = list(plot_call = plot_call, mapped_dist = as.name(unname(map_dist[t_dist]))) |
1075 |
)
|
|
1076 |
}
|
|
1077 | ||
1078 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
1079 | ! |
user_plot = ggplot2_args[["QQplot"]], |
1080 | ! |
user_default = ggplot2_args$default, |
1081 | ! |
module_plot = teal.widgets::ggplot2_args(labs = list(x = "theoretical", y = "sample")) |
1082 |
)
|
|
1083 | ||
1084 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
1085 | ! |
all_ggplot2_args,
|
1086 | ! |
ggtheme = ggtheme |
1087 |
)
|
|
1088 | ||
1089 | ! |
teal.code::eval_code( |
1090 | ! |
qenv,
|
1091 | ! |
substitute( |
1092 | ! |
expr = qq_plot <- plot_call, |
1093 | ! |
env = list(plot_call = Reduce(function(x, y) call("+", x, y), c(plot_call, parsed_ggplot2_args))) |
1094 |
)
|
|
1095 |
)
|
|
1096 |
}
|
|
1097 |
)
|
|
1098 | ||
1099 |
# test qenv ----
|
|
1100 | ! |
test_q <- eventReactive( |
1101 | ! |
ignoreNULL = FALSE, |
1102 | ! |
eventExpr = { |
1103 | ! |
common_q() |
1104 | ! |
input$dist_param1 |
1105 | ! |
input$dist_param2 |
1106 | ! |
input$dist_tests |
1107 |
},
|
|
1108 | ! |
valueExpr = { |
1109 |
# Create a private stack for this function only.
|
|
1110 | ! |
ANL <- common_q()[["ANL"]] |
1111 | ||
1112 | ! |
dist_var <- merge_vars()$dist_var |
1113 | ! |
s_var <- merge_vars()$s_var |
1114 | ! |
g_var <- merge_vars()$g_var |
1115 | ||
1116 | ! |
dist_var_name <- merge_vars()$dist_var_name |
1117 | ! |
s_var_name <- merge_vars()$s_var_name |
1118 | ! |
g_var_name <- merge_vars()$g_var_name |
1119 | ||
1120 | ! |
dist_param1 <- input$dist_param1 |
1121 | ! |
dist_param2 <- input$dist_param2 |
1122 | ! |
dist_tests <- input$dist_tests |
1123 | ! |
t_dist <- input$t_dist |
1124 | ||
1125 | ! |
req(dist_tests) |
1126 | ||
1127 | ! |
teal::validate_inputs(iv_dist) |
1128 | ||
1129 | ! |
if (length(s_var) > 0 || length(g_var) > 0) { |
1130 | ! |
counts <- ANL %>% |
1131 | ! |
dplyr::group_by_at(dplyr::vars(dplyr::any_of(c(s_var, g_var)))) %>% |
1132 | ! |
dplyr::summarise(n = dplyr::n()) |
1133 | ||
1134 | ! |
validate(need(all(counts$n > 5), "Please select strata*group with at least 5 observation each.")) |
1135 |
}
|
|
1136 | ||
1137 | ||
1138 | ! |
if (dist_tests %in% c( |
1139 | ! |
"t-test (two-samples, not paired)",
|
1140 | ! |
"F-test",
|
1141 | ! |
"Kolmogorov-Smirnov (two-samples)"
|
1142 |
)) { |
|
1143 | ! |
if (length(g_var) == 0 && length(s_var) > 0) { |
1144 | ! |
validate(need( |
1145 | ! |
length(unique(ANL[[s_var]])) == 2, |
1146 | ! |
"Please select stratify variable with 2 levels."
|
1147 |
)) |
|
1148 |
}
|
|
1149 | ! |
if (length(g_var) > 0 && length(s_var) > 0) { |
1150 | ! |
validate(need( |
1151 | ! |
all(stats::na.omit(as.vector( |
1152 | ! |
tapply(ANL[[s_var]], list(ANL[[g_var]]), function(x) length(unique(x))) == 2 |
1153 |
))), |
|
1154 | ! |
"Please select stratify variable with 2 levels, per each group."
|
1155 |
)) |
|
1156 |
}
|
|
1157 |
}
|
|
1158 | ||
1159 | ! |
map_dist <- stats::setNames( |
1160 | ! |
c("pnorm", "plnorm", "pgamma", "punif"), |
1161 | ! |
c("normal", "lognormal", "gamma", "unif") |
1162 |
)
|
|
1163 | ! |
sks_args <- list( |
1164 | ! |
test = quote(stats::ks.test), |
1165 | ! |
args = bquote(append(list(.[[.(dist_var)]], .(map_dist[t_dist])), params)), |
1166 | ! |
groups = c(g_var, s_var) |
1167 |
)
|
|
1168 | ! |
ssw_args <- list( |
1169 | ! |
test = quote(stats::shapiro.test), |
1170 | ! |
args = bquote(list(.[[.(dist_var)]])), |
1171 | ! |
groups = c(g_var, s_var) |
1172 |
)
|
|
1173 | ! |
mfil_args <- list( |
1174 | ! |
test = quote(stats::fligner.test), |
1175 | ! |
args = bquote(list(.[[.(dist_var)]], .[[.(s_var)]])), |
1176 | ! |
groups = c(g_var) |
1177 |
)
|
|
1178 | ! |
sad_args <- list( |
1179 | ! |
test = quote(goftest::ad.test), |
1180 | ! |
args = bquote(append(list(.[[.(dist_var)]], .(map_dist[t_dist])), params)), |
1181 | ! |
groups = c(g_var, s_var) |
1182 |
)
|
|
1183 | ! |
scvm_args <- list( |
1184 | ! |
test = quote(goftest::cvm.test), |
1185 | ! |
args = bquote(append(list(.[[.(dist_var)]], .(map_dist[t_dist])), params)), |
1186 | ! |
groups = c(g_var, s_var) |
1187 |
)
|
|
1188 | ! |
manov_args <- list( |
1189 | ! |
test = quote(stats::aov), |
1190 | ! |
args = bquote(list(stats::formula(.(dist_var_name) ~ .(s_var_name)), .)), |
1191 | ! |
groups = c(g_var) |
1192 |
)
|
|
1193 | ! |
mt_args <- list( |
1194 | ! |
test = quote(stats::t.test), |
1195 | ! |
args = bquote(unname(split(.[[.(dist_var)]], .[[.(s_var)]], drop = TRUE))), |
1196 | ! |
groups = c(g_var) |
1197 |
)
|
|
1198 | ! |
mv_args <- list( |
1199 | ! |
test = quote(stats::var.test), |
1200 | ! |
args = bquote(unname(split(.[[.(dist_var)]], .[[.(s_var)]], drop = TRUE))), |
1201 | ! |
groups = c(g_var) |
1202 |
)
|
|
1203 | ! |
mks_args <- list( |
1204 | ! |
test = quote(stats::ks.test), |
1205 | ! |
args = bquote(unname(split(.[[.(dist_var)]], .[[.(s_var)]], drop = TRUE))), |
1206 | ! |
groups = c(g_var) |
1207 |
)
|
|
1208 | ||
1209 | ! |
tests_base <- switch(dist_tests, |
1210 | ! |
"Kolmogorov-Smirnov (one-sample)" = sks_args, |
1211 | ! |
"Shapiro-Wilk" = ssw_args, |
1212 | ! |
"Fligner-Killeen" = mfil_args, |
1213 | ! |
"one-way ANOVA" = manov_args, |
1214 | ! |
"t-test (two-samples, not paired)" = mt_args, |
1215 | ! |
"F-test" = mv_args, |
1216 | ! |
"Kolmogorov-Smirnov (two-samples)" = mks_args, |
1217 | ! |
"Anderson-Darling (one-sample)" = sad_args, |
1218 | ! |
"Cramer-von Mises (one-sample)" = scvm_args |
1219 |
)
|
|
1220 | ||
1221 | ! |
env <- list( |
1222 | ! |
t_test = t_dist, |
1223 | ! |
dist_var = dist_var, |
1224 | ! |
g_var = g_var, |
1225 | ! |
s_var = s_var, |
1226 | ! |
args = tests_base$args, |
1227 | ! |
groups = tests_base$groups, |
1228 | ! |
test = tests_base$test, |
1229 | ! |
dist_var_name = dist_var_name, |
1230 | ! |
g_var_name = g_var_name, |
1231 | ! |
s_var_name = s_var_name |
1232 |
)
|
|
1233 | ||
1234 | ! |
qenv <- common_q() |
1235 | ||
1236 | ! |
if (length(s_var) == 0 && length(g_var) == 0) { |
1237 | ! |
qenv <- teal.code::eval_code(qenv, 'library("generics")') # nolint quotes |
1238 | ! |
qenv <- teal.code::eval_code( |
1239 | ! |
qenv,
|
1240 | ! |
substitute( |
1241 | ! |
expr = { |
1242 | ! |
test_table_data <- ANL %>% |
1243 | ! |
dplyr::select(dist_var) %>% |
1244 | ! |
with(., generics::glance(do.call(test, args))) %>% |
1245 | ! |
dplyr::mutate_if(is.numeric, round, 3) |
1246 |
},
|
|
1247 | ! |
env = env |
1248 |
)
|
|
1249 |
)
|
|
1250 |
} else { |
|
1251 | ! |
qenv <- teal.code::eval_code(qenv, 'library("tidyr")') # nolint quotes |
1252 | ! |
qenv <- teal.code::eval_code( |
1253 | ! |
qenv,
|
1254 | ! |
substitute( |
1255 | ! |
expr = { |
1256 | ! |
test_table_data <- ANL %>% |
1257 | ! |
dplyr::select(dist_var, s_var, g_var) %>% |
1258 | ! |
dplyr::group_by_at(dplyr::vars(dplyr::any_of(groups))) %>% |
1259 | ! |
dplyr::do(tests = generics::glance(do.call(test, args))) %>% |
1260 | ! |
tidyr::unnest(tests) %>% |
1261 | ! |
dplyr::mutate_if(is.numeric, round, 3) |
1262 |
},
|
|
1263 | ! |
env = env |
1264 |
)
|
|
1265 |
)
|
|
1266 |
}
|
|
1267 |
}
|
|
1268 |
)
|
|
1269 | ||
1270 |
# outputs ----
|
|
1271 | ! |
output_dist_q <- reactive(c(common_q(), req(dist_q()))) |
1272 | ! |
output_qq_q <- reactive(c(common_q(), req(qq_q()))) |
1273 | ||
1274 |
# Summary table listing has to be created separately to allow for qenv join
|
|
1275 | ! |
output_summary_q <- reactive({ |
1276 | ! |
if (iv_r()$is_valid()) { |
1277 | ! |
within(common_q(), summary_table <- DT::datatable(summary_table_data)) |
1278 |
} else { |
|
1279 | ! |
within(common_q(), summary_table <- DT::datatable(summary_table_data[0L, ])) |
1280 |
}
|
|
1281 |
}) |
|
1282 | ||
1283 | ! |
output_test_q <- reactive({ |
1284 |
# wrapped in if since could lead into validate error - we do want to continue
|
|
1285 | ! |
test_q_out <- try(test_q(), silent = TRUE) |
1286 | ! |
if (!inherits(test_q_out, c("try-error", "error"))) { |
1287 | ! |
c( |
1288 | ! |
common_q(), |
1289 | ! |
within(test_q_out, { |
1290 | ! |
test_table <- DT::datatable(test_table_data) |
1291 |
}) |
|
1292 |
)
|
|
1293 |
} else { |
|
1294 | ! |
within(common_q(), test_table <- DT::datatable(data.frame(missing = character(0L)))) |
1295 |
}
|
|
1296 |
}) |
|
1297 | ||
1298 | ! |
decorated_output_dist_q <- srv_decorate_teal_data( |
1299 | ! |
"d_density",
|
1300 | ! |
data = output_dist_q, |
1301 | ! |
decorators = select_decorators(decorators, "histogram_plot"), |
1302 | ! |
expr = print(histogram_plot) |
1303 |
)
|
|
1304 | ||
1305 | ! |
decorated_output_qq_q <- srv_decorate_teal_data( |
1306 | ! |
"d_qq",
|
1307 | ! |
data = output_qq_q, |
1308 | ! |
decorators = select_decorators(decorators, "qq_plot"), |
1309 | ! |
expr = print(qq_plot) |
1310 |
)
|
|
1311 | ||
1312 | ! |
decorated_output_summary_q <- srv_decorate_teal_data( |
1313 | ! |
"d_summary",
|
1314 | ! |
data = output_summary_q, |
1315 | ! |
decorators = select_decorators(decorators, "summary_table"), |
1316 | ! |
expr = summary_table |
1317 |
)
|
|
1318 | ||
1319 | ! |
decorated_output_test_q <- srv_decorate_teal_data( |
1320 | ! |
"d_test",
|
1321 | ! |
data = output_test_q, |
1322 | ! |
decorators = select_decorators(decorators, "test_table"), |
1323 | ! |
expr = test_table |
1324 |
)
|
|
1325 | ||
1326 | ! |
decorated_output_q <- reactive({ |
1327 | ! |
tab <- req(input$tabs) # tab is NULL upon app launch, hence will crash without this statement |
1328 | ! |
test_q_out <- try(test_q(), silent = TRUE) |
1329 | ! |
decorated_test_q_out <- if (inherits(test_q_out, c("try-error", "error"))) { |
1330 | ! |
teal.code::qenv() |
1331 |
} else { |
|
1332 | ! |
decorated_output_test_q() |
1333 |
}
|
|
1334 | ||
1335 | ! |
out_q <- switch(tab, |
1336 | ! |
Histogram = decorated_output_dist_q(), |
1337 | ! |
QQplot = decorated_output_qq_q() |
1338 |
)
|
|
1339 | ! |
c(out_q, decorated_output_summary_q(), decorated_test_q_out) |
1340 |
}) |
|
1341 | ||
1342 | ! |
dist_r <- reactive(req(decorated_output_dist_q())[["histogram_plot"]]) |
1343 | ||
1344 | ! |
qq_r <- reactive(req(decorated_output_qq_q())[["qq_plot"]]) |
1345 | ||
1346 | ! |
output$summary_table <- DT::renderDataTable( |
1347 | ! |
expr = decorated_output_summary_q()[["summary_table"]], |
1348 | ! |
options = list( |
1349 | ! |
autoWidth = TRUE, |
1350 | ! |
columnDefs = list(list(width = "200px", targets = "_all")) |
1351 |
),
|
|
1352 | ! |
rownames = FALSE |
1353 |
)
|
|
1354 | ||
1355 | ! |
tests_r <- reactive({ |
1356 | ! |
req(iv_r()$is_valid()) |
1357 | ! |
teal::validate_inputs(iv_r_dist()) |
1358 | ! |
req(test_q()) # Ensure original errors are displayed |
1359 | ! |
decorated_output_test_q()[["test_table"]] |
1360 |
}) |
|
1361 | ||
1362 | ! |
pws1 <- teal.widgets::plot_with_settings_srv( |
1363 | ! |
id = "hist_plot", |
1364 | ! |
plot_r = dist_r, |
1365 | ! |
height = plot_height, |
1366 | ! |
width = plot_width, |
1367 | ! |
brushing = FALSE |
1368 |
)
|
|
1369 | ||
1370 | ! |
pws2 <- teal.widgets::plot_with_settings_srv( |
1371 | ! |
id = "qq_plot", |
1372 | ! |
plot_r = qq_r, |
1373 | ! |
height = plot_height, |
1374 | ! |
width = plot_width, |
1375 | ! |
brushing = FALSE |
1376 |
)
|
|
1377 | ||
1378 | ! |
output$t_stats <- DT::renderDataTable(expr = tests_r()) |
1379 | ||
1380 |
# Render R code.
|
|
1381 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_output_q()))) |
1382 | ||
1383 | ! |
teal.widgets::verbatim_popup_srv( |
1384 | ! |
id = "rcode", |
1385 | ! |
verbatim_content = source_code_r, |
1386 | ! |
title = "R Code for distribution" |
1387 |
)
|
|
1388 | ||
1389 |
### REPORTER
|
|
1390 | ! |
if (with_reporter) { |
1391 | ! |
card_fun <- function(comment, label) { |
1392 | ! |
card <- teal::report_card_template( |
1393 | ! |
title = "Distribution Plot", |
1394 | ! |
label = label, |
1395 | ! |
with_filter = with_filter, |
1396 | ! |
filter_panel_api = filter_panel_api |
1397 |
)
|
|
1398 | ! |
card$append_text("Plot", "header3") |
1399 | ! |
if (input$tabs == "Histogram") { |
1400 | ! |
card$append_plot(dist_r(), dim = pws1$dim()) |
1401 | ! |
} else if (input$tabs == "QQplot") { |
1402 | ! |
card$append_plot(qq_r(), dim = pws2$dim()) |
1403 |
}
|
|
1404 | ! |
card$append_text("Statistics table", "header3") |
1405 | ! |
card$append_table(decorated_output_summary_q()[["summary_table"]]) |
1406 | ! |
tests_error <- tryCatch(expr = tests_r(), error = function(e) "error") |
1407 | ! |
if (inherits(tests_error, "data.frame")) { |
1408 | ! |
card$append_text("Tests table", "header3") |
1409 | ! |
card$append_table(tests_r()) |
1410 |
}
|
|
1411 | ||
1412 | ! |
if (!comment == "") { |
1413 | ! |
card$append_text("Comment", "header3") |
1414 | ! |
card$append_text(comment) |
1415 |
}
|
|
1416 | ! |
card$append_src(source_code_r()) |
1417 | ! |
card
|
1418 |
}
|
|
1419 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
1420 |
}
|
|
1421 |
###
|
|
1422 |
}) |
|
1423 |
}
|
1 |
#' `teal` module: Scatterplot
|
|
2 |
#'
|
|
3 |
#' Generates a customizable scatterplot using `ggplot2`.
|
|
4 |
#' This module allows users to select variables for the x and y axes,
|
|
5 |
#' color and size encodings, faceting options, and more. It supports log transformations,
|
|
6 |
#' trend line additions, and dynamic adjustments of point opacity and size through UI controls.
|
|
7 |
#'
|
|
8 |
#' @note For more examples, please see the vignette "Using scatterplot" via
|
|
9 |
#' `vignette("using-scatterplot", package = "teal.modules.general")`.
|
|
10 |
#'
|
|
11 |
#' @inheritParams teal::module
|
|
12 |
#' @inheritParams shared_params
|
|
13 |
#' @param x (`data_extract_spec` or `list` of multiple `data_extract_spec`) Specifies
|
|
14 |
#' variable names selected to plot along the x-axis by default.
|
|
15 |
#' @param y (`data_extract_spec` or `list` of multiple `data_extract_spec`) Specifies
|
|
16 |
#' variable names selected to plot along the y-axis by default.
|
|
17 |
#' @param color_by (`data_extract_spec` or `list` of multiple `data_extract_spec`) optional,
|
|
18 |
#' defines the color encoding. If `NULL` then no color encoding option will be displayed.
|
|
19 |
#' @param size_by (`data_extract_spec` or `list` of multiple `data_extract_spec`) optional,
|
|
20 |
#' defines the point size encoding. If `NULL` then no size encoding option will be displayed.
|
|
21 |
#' @param row_facet (`data_extract_spec` or `list` of multiple `data_extract_spec`) optional,
|
|
22 |
#' specifies the variable(s) for faceting rows.
|
|
23 |
#' @param col_facet (`data_extract_spec` or `list` of multiple `data_extract_spec`) optional,
|
|
24 |
#' specifies the variable(s) for faceting columns.
|
|
25 |
#' @param shape (`character`) optional, character vector with the names of the
|
|
26 |
#' shape, e.g. `c("triangle", "square", "circle")`. It defaults to `shape_names`. This is a complete list from
|
|
27 |
#' `vignette("ggplot2-specs", package="ggplot2")`.
|
|
28 |
#' @param max_deg (`integer`) optional, maximum degree for the polynomial trend line. Must not be less than 1.
|
|
29 |
#' @param table_dec (`integer`) optional, number of decimal places used to round numeric values in the table.
|
|
30 |
#'
|
|
31 |
#' @inherit shared_params return
|
|
32 |
#'
|
|
33 |
#' @section Decorating Module:
|
|
34 |
#'
|
|
35 |
#' This module generates the following objects, which can be modified in place using decorators:
|
|
36 |
#' - `plot` (`ggplot`)
|
|
37 |
#'
|
|
38 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
39 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
40 |
#' See code snippet below:
|
|
41 |
#'
|
|
42 |
#' ```
|
|
43 |
#' tm_g_scatterplot(
|
|
44 |
#' ..., # arguments for module
|
|
45 |
#' decorators = list(
|
|
46 |
#' plot = teal_transform_module(...) # applied to the `plot` output
|
|
47 |
#' )
|
|
48 |
#' )
|
|
49 |
#' ```
|
|
50 |
#'
|
|
51 |
#' For additional details and examples of decorators, refer to the vignette
|
|
52 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
53 |
#'
|
|
54 |
#' To learn more please refer to the vignette
|
|
55 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
56 |
#'
|
|
57 |
#'
|
|
58 |
#' @examplesShinylive
|
|
59 |
#' library(teal.modules.general)
|
|
60 |
#' interactive <- function() TRUE
|
|
61 |
#' {{ next_example }}
|
|
62 |
# nolint start: line_length_linter.
|
|
63 |
#' @examples
|
|
64 |
# nolint end: line_length_linter.
|
|
65 |
#' # general data example
|
|
66 |
#' data <- teal_data()
|
|
67 |
#' data <- within(data, {
|
|
68 |
#' require(nestcolor)
|
|
69 |
#' CO2 <- CO2
|
|
70 |
#' })
|
|
71 |
#'
|
|
72 |
#' app <- init(
|
|
73 |
#' data = data,
|
|
74 |
#' modules = modules(
|
|
75 |
#' tm_g_scatterplot(
|
|
76 |
#' label = "Scatterplot Choices",
|
|
77 |
#' x = data_extract_spec(
|
|
78 |
#' dataname = "CO2",
|
|
79 |
#' select = select_spec(
|
|
80 |
#' label = "Select variable:",
|
|
81 |
#' choices = variable_choices(data[["CO2"]], c("conc", "uptake")),
|
|
82 |
#' selected = "conc",
|
|
83 |
#' multiple = FALSE,
|
|
84 |
#' fixed = FALSE
|
|
85 |
#' )
|
|
86 |
#' ),
|
|
87 |
#' y = data_extract_spec(
|
|
88 |
#' dataname = "CO2",
|
|
89 |
#' select = select_spec(
|
|
90 |
#' label = "Select variable:",
|
|
91 |
#' choices = variable_choices(data[["CO2"]], c("conc", "uptake")),
|
|
92 |
#' selected = "uptake",
|
|
93 |
#' multiple = FALSE,
|
|
94 |
#' fixed = FALSE
|
|
95 |
#' )
|
|
96 |
#' ),
|
|
97 |
#' color_by = data_extract_spec(
|
|
98 |
#' dataname = "CO2",
|
|
99 |
#' select = select_spec(
|
|
100 |
#' label = "Select variable:",
|
|
101 |
#' choices = variable_choices(
|
|
102 |
#' data[["CO2"]],
|
|
103 |
#' c("Plant", "Type", "Treatment", "conc", "uptake")
|
|
104 |
#' ),
|
|
105 |
#' selected = NULL,
|
|
106 |
#' multiple = FALSE,
|
|
107 |
#' fixed = FALSE
|
|
108 |
#' )
|
|
109 |
#' ),
|
|
110 |
#' size_by = data_extract_spec(
|
|
111 |
#' dataname = "CO2",
|
|
112 |
#' select = select_spec(
|
|
113 |
#' label = "Select variable:",
|
|
114 |
#' choices = variable_choices(data[["CO2"]], c("conc", "uptake")),
|
|
115 |
#' selected = "uptake",
|
|
116 |
#' multiple = FALSE,
|
|
117 |
#' fixed = FALSE
|
|
118 |
#' )
|
|
119 |
#' ),
|
|
120 |
#' row_facet = data_extract_spec(
|
|
121 |
#' dataname = "CO2",
|
|
122 |
#' select = select_spec(
|
|
123 |
#' label = "Select variable:",
|
|
124 |
#' choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
|
|
125 |
#' selected = NULL,
|
|
126 |
#' multiple = FALSE,
|
|
127 |
#' fixed = FALSE
|
|
128 |
#' )
|
|
129 |
#' ),
|
|
130 |
#' col_facet = data_extract_spec(
|
|
131 |
#' dataname = "CO2",
|
|
132 |
#' select = select_spec(
|
|
133 |
#' label = "Select variable:",
|
|
134 |
#' choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
|
|
135 |
#' selected = NULL,
|
|
136 |
#' multiple = FALSE,
|
|
137 |
#' fixed = FALSE
|
|
138 |
#' )
|
|
139 |
#' )
|
|
140 |
#' )
|
|
141 |
#' )
|
|
142 |
#' )
|
|
143 |
#' if (interactive()) {
|
|
144 |
#' shinyApp(app$ui, app$server)
|
|
145 |
#' }
|
|
146 |
#'
|
|
147 |
#' @examplesShinylive
|
|
148 |
#' library(teal.modules.general)
|
|
149 |
#' interactive <- function() TRUE
|
|
150 |
#' {{ next_example }}
|
|
151 |
# nolint start: line_length_linter.
|
|
152 |
#' @examples
|
|
153 |
# nolint end: line_length_linter.
|
|
154 |
#' # CDISC data example
|
|
155 |
#' data <- teal_data()
|
|
156 |
#' data <- within(data, {
|
|
157 |
#' require(nestcolor)
|
|
158 |
#' ADSL <- teal.data::rADSL
|
|
159 |
#' })
|
|
160 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
161 |
#'
|
|
162 |
#' app <- init(
|
|
163 |
#' data = data,
|
|
164 |
#' modules = modules(
|
|
165 |
#' tm_g_scatterplot(
|
|
166 |
#' label = "Scatterplot Choices",
|
|
167 |
#' x = data_extract_spec(
|
|
168 |
#' dataname = "ADSL",
|
|
169 |
#' select = select_spec(
|
|
170 |
#' label = "Select variable:",
|
|
171 |
#' choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1", "BMRKR2")),
|
|
172 |
#' selected = "AGE",
|
|
173 |
#' multiple = FALSE,
|
|
174 |
#' fixed = FALSE
|
|
175 |
#' )
|
|
176 |
#' ),
|
|
177 |
#' y = data_extract_spec(
|
|
178 |
#' dataname = "ADSL",
|
|
179 |
#' select = select_spec(
|
|
180 |
#' label = "Select variable:",
|
|
181 |
#' choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1", "BMRKR2")),
|
|
182 |
#' selected = "BMRKR1",
|
|
183 |
#' multiple = FALSE,
|
|
184 |
#' fixed = FALSE
|
|
185 |
#' )
|
|
186 |
#' ),
|
|
187 |
#' color_by = data_extract_spec(
|
|
188 |
#' dataname = "ADSL",
|
|
189 |
#' select = select_spec(
|
|
190 |
#' label = "Select variable:",
|
|
191 |
#' choices = variable_choices(
|
|
192 |
#' data[["ADSL"]],
|
|
193 |
#' c("AGE", "BMRKR1", "BMRKR2", "RACE", "REGION1")
|
|
194 |
#' ),
|
|
195 |
#' selected = NULL,
|
|
196 |
#' multiple = FALSE,
|
|
197 |
#' fixed = FALSE
|
|
198 |
#' )
|
|
199 |
#' ),
|
|
200 |
#' size_by = data_extract_spec(
|
|
201 |
#' dataname = "ADSL",
|
|
202 |
#' select = select_spec(
|
|
203 |
#' label = "Select variable:",
|
|
204 |
#' choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1")),
|
|
205 |
#' selected = "AGE",
|
|
206 |
#' multiple = FALSE,
|
|
207 |
#' fixed = FALSE
|
|
208 |
#' )
|
|
209 |
#' ),
|
|
210 |
#' row_facet = data_extract_spec(
|
|
211 |
#' dataname = "ADSL",
|
|
212 |
#' select = select_spec(
|
|
213 |
#' label = "Select variable:",
|
|
214 |
#' choices = variable_choices(data[["ADSL"]], c("BMRKR2", "RACE", "REGION1")),
|
|
215 |
#' selected = NULL,
|
|
216 |
#' multiple = FALSE,
|
|
217 |
#' fixed = FALSE
|
|
218 |
#' )
|
|
219 |
#' ),
|
|
220 |
#' col_facet = data_extract_spec(
|
|
221 |
#' dataname = "ADSL",
|
|
222 |
#' select = select_spec(
|
|
223 |
#' label = "Select variable:",
|
|
224 |
#' choices = variable_choices(data[["ADSL"]], c("BMRKR2", "RACE", "REGION1")),
|
|
225 |
#' selected = NULL,
|
|
226 |
#' multiple = FALSE,
|
|
227 |
#' fixed = FALSE
|
|
228 |
#' )
|
|
229 |
#' )
|
|
230 |
#' )
|
|
231 |
#' )
|
|
232 |
#' )
|
|
233 |
#' if (interactive()) {
|
|
234 |
#' shinyApp(app$ui, app$server)
|
|
235 |
#' }
|
|
236 |
#'
|
|
237 |
#' @export
|
|
238 |
#'
|
|
239 |
tm_g_scatterplot <- function(label = "Scatterplot", |
|
240 |
x,
|
|
241 |
y,
|
|
242 |
color_by = NULL, |
|
243 |
size_by = NULL, |
|
244 |
row_facet = NULL, |
|
245 |
col_facet = NULL, |
|
246 |
plot_height = c(600, 200, 2000), |
|
247 |
plot_width = NULL, |
|
248 |
alpha = c(1, 0, 1), |
|
249 |
shape = shape_names, |
|
250 |
size = c(5, 1, 15), |
|
251 |
max_deg = 5L, |
|
252 |
rotate_xaxis_labels = FALSE, |
|
253 |
ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), |
|
254 |
pre_output = NULL, |
|
255 |
post_output = NULL, |
|
256 |
table_dec = 4, |
|
257 |
ggplot2_args = teal.widgets::ggplot2_args(), |
|
258 |
transformators = list(), |
|
259 |
decorators = list()) { |
|
260 | ! |
message("Initializing tm_g_scatterplot") |
261 | ||
262 |
# Normalize the parameters
|
|
263 | ! |
if (inherits(x, "data_extract_spec")) x <- list(x) |
264 | ! |
if (inherits(y, "data_extract_spec")) y <- list(y) |
265 | ! |
if (inherits(color_by, "data_extract_spec")) color_by <- list(color_by) |
266 | ! |
if (inherits(size_by, "data_extract_spec")) size_by <- list(size_by) |
267 | ! |
if (inherits(row_facet, "data_extract_spec")) row_facet <- list(row_facet) |
268 | ! |
if (inherits(col_facet, "data_extract_spec")) col_facet <- list(col_facet) |
269 | ! |
if (is.double(max_deg)) max_deg <- as.integer(max_deg) |
270 | ||
271 |
# Start of assertions
|
|
272 | ! |
checkmate::assert_string(label) |
273 | ! |
checkmate::assert_list(x, types = "data_extract_spec") |
274 | ! |
checkmate::assert_list(y, types = "data_extract_spec") |
275 | ! |
checkmate::assert_list(color_by, types = "data_extract_spec", null.ok = TRUE) |
276 | ! |
checkmate::assert_list(size_by, types = "data_extract_spec", null.ok = TRUE) |
277 | ||
278 | ! |
checkmate::assert_list(row_facet, types = "data_extract_spec", null.ok = TRUE) |
279 | ! |
assert_single_selection(row_facet) |
280 | ||
281 | ! |
checkmate::assert_list(col_facet, types = "data_extract_spec", null.ok = TRUE) |
282 | ! |
assert_single_selection(col_facet) |
283 | ||
284 | ! |
checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
285 | ! |
checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
286 | ! |
checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
287 | ! |
checkmate::assert_numeric( |
288 | ! |
plot_width[1], |
289 | ! |
lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width" |
290 |
)
|
|
291 | ||
292 | ! |
if (length(alpha) == 1) { |
293 | ! |
checkmate::assert_numeric(alpha, any.missing = FALSE, finite = TRUE) |
294 |
} else { |
|
295 | ! |
checkmate::assert_numeric(alpha, len = 3, any.missing = FALSE, finite = TRUE) |
296 | ! |
checkmate::assert_numeric(alpha[1], lower = alpha[2], upper = alpha[3], .var.name = "alpha") |
297 |
}
|
|
298 | ||
299 | ! |
checkmate::assert_character(shape) |
300 | ||
301 | ! |
if (length(size) == 1) { |
302 | ! |
checkmate::assert_numeric(size, any.missing = FALSE, finite = TRUE) |
303 |
} else { |
|
304 | ! |
checkmate::assert_numeric(size, len = 3, any.missing = FALSE, finite = TRUE) |
305 | ! |
checkmate::assert_numeric(size[1], lower = size[2], upper = size[3], .var.name = "size") |
306 |
}
|
|
307 | ||
308 | ! |
checkmate::assert_int(max_deg, lower = 1L) |
309 | ! |
checkmate::assert_flag(rotate_xaxis_labels) |
310 | ! |
ggtheme <- match.arg(ggtheme) |
311 | ||
312 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
313 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
314 | ||
315 | ! |
checkmate::assert_scalar(table_dec) |
316 | ! |
checkmate::assert_class(ggplot2_args, "ggplot2_args") |
317 | ||
318 | ! |
assert_decorators(decorators, "plot") |
319 | ||
320 |
# End of assertions
|
|
321 | ||
322 |
# Make UI args
|
|
323 | ! |
args <- as.list(environment()) |
324 | ||
325 | ! |
data_extract_list <- list( |
326 | ! |
x = x, |
327 | ! |
y = y, |
328 | ! |
color_by = color_by, |
329 | ! |
size_by = size_by, |
330 | ! |
row_facet = row_facet, |
331 | ! |
col_facet = col_facet |
332 |
)
|
|
333 | ||
334 | ! |
ans <- module( |
335 | ! |
label = label, |
336 | ! |
server = srv_g_scatterplot, |
337 | ! |
ui = ui_g_scatterplot, |
338 | ! |
ui_args = args, |
339 | ! |
server_args = c( |
340 | ! |
data_extract_list,
|
341 | ! |
list( |
342 | ! |
plot_height = plot_height, |
343 | ! |
plot_width = plot_width, |
344 | ! |
table_dec = table_dec, |
345 | ! |
ggplot2_args = ggplot2_args, |
346 | ! |
decorators = decorators |
347 |
)
|
|
348 |
),
|
|
349 | ! |
transformators = transformators, |
350 | ! |
datanames = teal.transform::get_extract_datanames(data_extract_list) |
351 |
)
|
|
352 | ! |
attr(ans, "teal_bookmarkable") <- TRUE |
353 | ! |
ans
|
354 |
}
|
|
355 | ||
356 |
# UI function for the scatterplot module
|
|
357 |
ui_g_scatterplot <- function(id, ...) { |
|
358 | ! |
args <- list(...) |
359 | ! |
ns <- NS(id) |
360 | ! |
is_single_dataset_value <- teal.transform::is_single_dataset( |
361 | ! |
args$x, args$y, args$color_by, args$size_by, args$row_facet, args$col_facet |
362 |
)
|
|
363 | ||
364 | ! |
tagList( |
365 | ! |
include_css_files("custom"), |
366 | ! |
teal.widgets::standard_layout( |
367 | ! |
output = teal.widgets::white_small_well( |
368 | ! |
teal.widgets::plot_with_settings_ui(id = ns("scatter_plot")), |
369 | ! |
tags$h1(tags$strong("Selected points:"), class = "text-center font-150p"), |
370 | ! |
teal.widgets::get_dt_rows(ns("data_table"), ns("data_table_rows")), |
371 | ! |
DT::dataTableOutput(ns("data_table"), width = "100%") |
372 |
),
|
|
373 | ! |
encoding = tags$div( |
374 |
### Reporter
|
|
375 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
376 |
###
|
|
377 | ! |
tags$label("Encodings", class = "text-primary"), |
378 | ! |
teal.transform::datanames_input(args[c("x", "y", "color_by", "size_by", "row_facet", "col_facet")]), |
379 | ! |
teal.transform::data_extract_ui( |
380 | ! |
id = ns("x"), |
381 | ! |
label = "X variable", |
382 | ! |
data_extract_spec = args$x, |
383 | ! |
is_single_dataset = is_single_dataset_value |
384 |
),
|
|
385 | ! |
checkboxInput(ns("log_x"), "Use log transformation", value = FALSE), |
386 | ! |
conditionalPanel( |
387 | ! |
condition = paste0("input['", ns("log_x"), "'] == true"), |
388 | ! |
radioButtons( |
389 | ! |
ns("log_x_base"), |
390 | ! |
label = NULL, |
391 | ! |
inline = TRUE, |
392 | ! |
choices = c("Natural" = "log", "Base 10" = "log10", "Base 2" = "log2") |
393 |
)
|
|
394 |
),
|
|
395 | ! |
teal.transform::data_extract_ui( |
396 | ! |
id = ns("y"), |
397 | ! |
label = "Y variable", |
398 | ! |
data_extract_spec = args$y, |
399 | ! |
is_single_dataset = is_single_dataset_value |
400 |
),
|
|
401 | ! |
checkboxInput(ns("log_y"), "Use log transformation", value = FALSE), |
402 | ! |
conditionalPanel( |
403 | ! |
condition = paste0("input['", ns("log_y"), "'] == true"), |
404 | ! |
radioButtons( |
405 | ! |
ns("log_y_base"), |
406 | ! |
label = NULL, |
407 | ! |
inline = TRUE, |
408 | ! |
choices = c("Natural" = "log", "Base 10" = "log10", "Base 2" = "log2") |
409 |
)
|
|
410 |
),
|
|
411 | ! |
if (!is.null(args$color_by)) { |
412 | ! |
teal.transform::data_extract_ui( |
413 | ! |
id = ns("color_by"), |
414 | ! |
label = "Color by variable", |
415 | ! |
data_extract_spec = args$color_by, |
416 | ! |
is_single_dataset = is_single_dataset_value |
417 |
)
|
|
418 |
},
|
|
419 | ! |
if (!is.null(args$size_by)) { |
420 | ! |
teal.transform::data_extract_ui( |
421 | ! |
id = ns("size_by"), |
422 | ! |
label = "Size by variable", |
423 | ! |
data_extract_spec = args$size_by, |
424 | ! |
is_single_dataset = is_single_dataset_value |
425 |
)
|
|
426 |
},
|
|
427 | ! |
if (!is.null(args$row_facet)) { |
428 | ! |
teal.transform::data_extract_ui( |
429 | ! |
id = ns("row_facet"), |
430 | ! |
label = "Row facetting", |
431 | ! |
data_extract_spec = args$row_facet, |
432 | ! |
is_single_dataset = is_single_dataset_value |
433 |
)
|
|
434 |
},
|
|
435 | ! |
if (!is.null(args$col_facet)) { |
436 | ! |
teal.transform::data_extract_ui( |
437 | ! |
id = ns("col_facet"), |
438 | ! |
label = "Column facetting", |
439 | ! |
data_extract_spec = args$col_facet, |
440 | ! |
is_single_dataset = is_single_dataset_value |
441 |
)
|
|
442 |
},
|
|
443 | ! |
ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")), |
444 | ! |
teal.widgets::panel_group( |
445 | ! |
teal.widgets::panel_item( |
446 | ! |
title = "Plot settings", |
447 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("alpha"), "Opacity:", args$alpha, ticks = FALSE), |
448 | ! |
teal.widgets::optionalSelectInput( |
449 | ! |
inputId = ns("shape"), |
450 | ! |
label = "Points shape:", |
451 | ! |
choices = args$shape, |
452 | ! |
selected = args$shape[1], |
453 | ! |
multiple = FALSE |
454 |
),
|
|
455 | ! |
colourpicker::colourInput(ns("color"), "Points color:", "black"), |
456 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("size"), "Points size:", args$size, ticks = FALSE, step = .1), |
457 | ! |
checkboxInput(ns("rotate_xaxis_labels"), "Rotate X axis labels", value = args$rotate_xaxis_labels), |
458 | ! |
checkboxInput(ns("add_density"), "Add marginal density", value = FALSE), |
459 | ! |
checkboxInput(ns("rug_plot"), "Include rug plot", value = FALSE), |
460 | ! |
checkboxInput(ns("show_count"), "Show N (number of observations)", value = FALSE), |
461 | ! |
shinyjs::hidden(helpText(id = ns("line_msg"), "Trendline needs numeric X and Y variables")), |
462 | ! |
teal.widgets::optionalSelectInput(ns("smoothing_degree"), "Smoothing degree", seq_len(args$max_deg)), |
463 | ! |
shinyjs::hidden(teal.widgets::optionalSelectInput(ns("color_sub"), label = "", multiple = TRUE)), |
464 | ! |
teal.widgets::optionalSliderInputValMinMax(ns("ci"), "Confidence", c(.95, .8, .99), ticks = FALSE), |
465 | ! |
shinyjs::hidden(checkboxInput(ns("show_form"), "Show formula", value = TRUE)), |
466 | ! |
shinyjs::hidden(checkboxInput(ns("show_r2"), "Show adj-R Squared", value = TRUE)), |
467 | ! |
uiOutput(ns("num_na_removed")), |
468 | ! |
tags$div( |
469 | ! |
id = ns("label_pos"), |
470 | ! |
tags$div(tags$strong("Stats position")), |
471 | ! |
tags$div(class = "inline-block w-10", helpText("Left")), |
472 | ! |
tags$div( |
473 | ! |
class = "inline-block w-70", |
474 | ! |
teal.widgets::optionalSliderInput( |
475 | ! |
ns("pos"), |
476 | ! |
label = NULL, |
477 | ! |
min = 0, max = 1, value = .99, ticks = FALSE, step = .01 |
478 |
)
|
|
479 |
),
|
|
480 | ! |
tags$div(class = "inline-block w-10", helpText("Right")) |
481 |
),
|
|
482 | ! |
teal.widgets::optionalSliderInput( |
483 | ! |
ns("label_size"), "Stats font size", |
484 | ! |
min = 3, max = 10, value = 5, ticks = FALSE, step = .1 |
485 |
),
|
|
486 | ! |
if (!is.null(args$row_facet) || !is.null(args$col_facet)) { |
487 | ! |
checkboxInput(ns("free_scales"), "Free scales", value = FALSE) |
488 |
},
|
|
489 | ! |
selectInput( |
490 | ! |
inputId = ns("ggtheme"), |
491 | ! |
label = "Theme (by ggplot):", |
492 | ! |
choices = ggplot_themes, |
493 | ! |
selected = args$ggtheme, |
494 | ! |
multiple = FALSE |
495 |
)
|
|
496 |
)
|
|
497 |
)
|
|
498 |
),
|
|
499 | ! |
forms = tagList( |
500 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
501 |
),
|
|
502 | ! |
pre_output = args$pre_output, |
503 | ! |
post_output = args$post_output |
504 |
)
|
|
505 |
)
|
|
506 |
}
|
|
507 | ||
508 |
# Server function for the scatterplot module
|
|
509 |
srv_g_scatterplot <- function(id, |
|
510 |
data,
|
|
511 |
reporter,
|
|
512 |
filter_panel_api,
|
|
513 |
x,
|
|
514 |
y,
|
|
515 |
color_by,
|
|
516 |
size_by,
|
|
517 |
row_facet,
|
|
518 |
col_facet,
|
|
519 |
plot_height,
|
|
520 |
plot_width,
|
|
521 |
table_dec,
|
|
522 |
ggplot2_args,
|
|
523 |
decorators) { |
|
524 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
525 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
526 | ! |
checkmate::assert_class(data, "reactive") |
527 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
528 | ! |
moduleServer(id, function(input, output, session) { |
529 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
530 | ||
531 | ! |
data_extract <- list( |
532 | ! |
x = x, |
533 | ! |
y = y, |
534 | ! |
color_by = color_by, |
535 | ! |
size_by = size_by, |
536 | ! |
row_facet = row_facet, |
537 | ! |
col_facet = col_facet |
538 |
)
|
|
539 | ||
540 | ! |
rule_diff <- function(other) { |
541 | ! |
function(value) { |
542 | ! |
othervalue <- selector_list()[[other]]()[["select"]] |
543 | ! |
if (!is.null(othervalue)) { |
544 | ! |
if (identical(value, othervalue)) { |
545 | ! |
"Row and column facetting variables must be different."
|
546 |
}
|
|
547 |
}
|
|
548 |
}
|
|
549 |
}
|
|
550 | ||
551 | ! |
selector_list <- teal.transform::data_extract_multiple_srv( |
552 | ! |
data_extract = data_extract, |
553 | ! |
datasets = data, |
554 | ! |
select_validation_rule = list( |
555 | ! |
x = ~ if (length(.) != 1) "Please select exactly one x var.", |
556 | ! |
y = ~ if (length(.) != 1) "Please select exactly one y var.", |
557 | ! |
color_by = ~ if (length(.) > 1) "There cannot be more than 1 color variable.", |
558 | ! |
size_by = ~ if (length(.) > 1) "There cannot be more than 1 size variable.", |
559 | ! |
row_facet = shinyvalidate::compose_rules( |
560 | ! |
shinyvalidate::sv_optional(), |
561 | ! |
rule_diff("col_facet") |
562 |
),
|
|
563 | ! |
col_facet = shinyvalidate::compose_rules( |
564 | ! |
shinyvalidate::sv_optional(), |
565 | ! |
rule_diff("row_facet") |
566 |
)
|
|
567 |
)
|
|
568 |
)
|
|
569 | ||
570 | ! |
iv_r <- reactive({ |
571 | ! |
iv_facet <- shinyvalidate::InputValidator$new() |
572 | ! |
iv <- shinyvalidate::InputValidator$new() |
573 | ! |
teal.transform::compose_and_enable_validators(iv, selector_list) |
574 |
}) |
|
575 | ! |
iv_facet <- shinyvalidate::InputValidator$new() |
576 | ! |
iv_facet$add_rule("add_density", ~ if ( |
577 | ! |
isTRUE(.) && |
578 |
(
|
|
579 | ! |
length(selector_list()$row_facet()$select) > 0L || |
580 | ! |
length(selector_list()$col_facet()$select) > 0L |
581 |
)
|
|
582 |
) { |
|
583 | ! |
"Cannot add marginal density when Row or Column facetting has been selected"
|
584 |
}) |
|
585 | ! |
iv_facet$enable() |
586 | ||
587 | ! |
anl_merged_input <- teal.transform::merge_expression_srv( |
588 | ! |
selector_list = selector_list, |
589 | ! |
datasets = data, |
590 | ! |
merge_function = "dplyr::inner_join" |
591 |
)
|
|
592 | ! |
qenv <- teal.code::eval_code(data(), 'library("ggplot2");library("dplyr")') # nolint quotes |
593 | ||
594 | ! |
anl_merged_q <- reactive({ |
595 | ! |
req(anl_merged_input()) |
596 | ! |
qenv %>% |
597 | ! |
teal.code::eval_code(as.expression(anl_merged_input()$expr)) %>% |
598 | ! |
teal.code::eval_code(quote(ANL)) # used to display table when running show-r-code code |
599 |
}) |
|
600 | ||
601 | ! |
merged <- list( |
602 | ! |
anl_input_r = anl_merged_input, |
603 | ! |
anl_q_r = anl_merged_q |
604 |
)
|
|
605 | ||
606 | ! |
trend_line_is_applicable <- reactive({ |
607 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
608 | ! |
x_var <- as.vector(merged$anl_input_r()$columns_source$x) |
609 | ! |
y_var <- as.vector(merged$anl_input_r()$columns_source$y) |
610 | ! |
length(x_var) > 0 && length(y_var) > 0 && is.numeric(ANL[[x_var]]) && is.numeric(ANL[[y_var]]) |
611 |
}) |
|
612 | ||
613 | ! |
add_trend_line <- reactive({ |
614 | ! |
smoothing_degree <- as.integer(input$smoothing_degree) |
615 | ! |
trend_line_is_applicable() && length(smoothing_degree) > 0 |
616 |
}) |
|
617 | ||
618 | ! |
if (!is.null(color_by)) { |
619 | ! |
observeEvent( |
620 | ! |
eventExpr = merged$anl_input_r()$columns_source$color_by, |
621 | ! |
handlerExpr = { |
622 | ! |
color_by_var <- as.vector(merged$anl_input_r()$columns_source$color_by) |
623 | ! |
if (length(color_by_var) > 0) { |
624 | ! |
shinyjs::hide("color") |
625 |
} else { |
|
626 | ! |
shinyjs::show("color") |
627 |
}
|
|
628 |
}
|
|
629 |
)
|
|
630 |
}
|
|
631 | ||
632 | ! |
output$num_na_removed <- renderUI({ |
633 | ! |
if (add_trend_line()) { |
634 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
635 | ! |
x_var <- as.vector(merged$anl_input_r()$columns_source$x) |
636 | ! |
y_var <- as.vector(merged$anl_input_r()$columns_source$y) |
637 | ! |
if ((num_total_na <- nrow(ANL) - nrow(stats::na.omit(ANL[, c(x_var, y_var)]))) > 0) { |
638 | ! |
tags$div(paste(num_total_na, "row(s) with missing values were removed"), tags$hr()) |
639 |
}
|
|
640 |
}
|
|
641 |
}) |
|
642 | ||
643 | ! |
observeEvent( |
644 | ! |
eventExpr = merged$anl_input_r()$columns_source[c("col_facet", "row_facet")], |
645 | ! |
handlerExpr = { |
646 | ! |
if ( |
647 | ! |
length(merged$anl_input_r()$columns_source$col_facet) == 0 && |
648 | ! |
length(merged$anl_input_r()$columns_source$row_facet) == 0 |
649 |
) { |
|
650 | ! |
shinyjs::hide("free_scales") |
651 |
} else { |
|
652 | ! |
shinyjs::show("free_scales") |
653 |
}
|
|
654 |
}
|
|
655 |
)
|
|
656 | ||
657 | ! |
output_q <- reactive({ |
658 | ! |
teal::validate_inputs(iv_r(), iv_facet) |
659 | ||
660 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
661 | ||
662 | ! |
x_var <- as.vector(merged$anl_input_r()$columns_source$x) |
663 | ! |
y_var <- as.vector(merged$anl_input_r()$columns_source$y) |
664 | ! |
color_by_var <- as.vector(merged$anl_input_r()$columns_source$color_by) |
665 | ! |
size_by_var <- as.vector(merged$anl_input_r()$columns_source$size_by) |
666 | ! |
row_facet_name <- if (length(merged$anl_input_r()$columns_source$row_facet) == 0) { |
667 | ! |
character(0) |
668 |
} else { |
|
669 | ! |
as.vector(merged$anl_input_r()$columns_source$row_facet) |
670 |
}
|
|
671 | ! |
col_facet_name <- if (length(merged$anl_input_r()$columns_source$col_facet) == 0) { |
672 | ! |
character(0) |
673 |
} else { |
|
674 | ! |
as.vector(merged$anl_input_r()$columns_source$col_facet) |
675 |
}
|
|
676 | ! |
alpha <- input$alpha |
677 | ! |
size <- input$size |
678 | ! |
rotate_xaxis_labels <- input$rotate_xaxis_labels |
679 | ! |
add_density <- input$add_density |
680 | ! |
ggtheme <- input$ggtheme |
681 | ! |
rug_plot <- input$rug_plot |
682 | ! |
color <- input$color |
683 | ! |
shape <- `if`(is.null(input$shape) || identical(input$shape, ""), "circle", input$shape) |
684 | ! |
smoothing_degree <- as.integer(input$smoothing_degree) |
685 | ! |
ci <- input$ci |
686 | ||
687 | ! |
log_x <- input$log_x |
688 | ! |
log_y <- input$log_y |
689 | ||
690 | ! |
validate(need( |
691 | ! |
length(row_facet_name) == 0 || inherits(ANL[[row_facet_name]], c("character", "factor", "Date", "integer")), |
692 | ! |
"`Row facetting` variable must be of class `character`, `factor`, `Date`, or `integer`"
|
693 |
)) |
|
694 | ! |
validate(need( |
695 | ! |
length(col_facet_name) == 0 || inherits(ANL[[col_facet_name]], c("character", "factor", "Date", "integer")), |
696 | ! |
"`Column facetting` variable must be of class `character`, `factor`, `Date`, or `integer`"
|
697 |
)) |
|
698 | ||
699 | ! |
if (add_density && length(color_by_var) > 0) { |
700 | ! |
validate(need( |
701 | ! |
!is.numeric(ANL[[color_by_var]]), |
702 | ! |
"Marginal plots cannot be produced when the points are colored by numeric variables. |
703 | ! |
\n Uncheck the 'Add marginal density' checkbox to display the plot." |
704 |
)) |
|
705 | ! |
validate(need( |
706 |
!( |
|
707 | ! |
inherits(ANL[[color_by_var]], "Date") || |
708 | ! |
inherits(ANL[[color_by_var]], "POSIXct") || |
709 | ! |
inherits(ANL[[color_by_var]], "POSIXlt") |
710 |
),
|
|
711 | ! |
"Marginal plots cannot be produced when the points are colored by Date or POSIX variables. |
712 | ! |
\n Uncheck the 'Add marginal density' checkbox to display the plot." |
713 |
)) |
|
714 |
}
|
|
715 | ||
716 | ! |
teal::validate_has_data(ANL[, c(x_var, y_var)], 10, complete = TRUE, allow_inf = FALSE) |
717 | ||
718 | ! |
if (log_x) { |
719 | ! |
validate( |
720 | ! |
need( |
721 | ! |
is.numeric(ANL[[x_var]]) && all( |
722 | ! |
ANL[[x_var]] > 0 | is.na(ANL[[x_var]]) |
723 |
),
|
|
724 | ! |
"X variable can only be log transformed if variable is numeric and all values are positive."
|
725 |
)
|
|
726 |
)
|
|
727 |
}
|
|
728 | ! |
if (log_y) { |
729 | ! |
validate( |
730 | ! |
need( |
731 | ! |
is.numeric(ANL[[y_var]]) && all( |
732 | ! |
ANL[[y_var]] > 0 | is.na(ANL[[y_var]]) |
733 |
),
|
|
734 | ! |
"Y variable can only be log transformed if variable is numeric and all values are positive."
|
735 |
)
|
|
736 |
)
|
|
737 |
}
|
|
738 | ||
739 | ! |
facet_cl <- facet_ggplot_call( |
740 | ! |
row_facet_name,
|
741 | ! |
col_facet_name,
|
742 | ! |
free_x_scales = isTRUE(input$free_scales), |
743 | ! |
free_y_scales = isTRUE(input$free_scales) |
744 |
)
|
|
745 | ||
746 | ! |
point_sizes <- if (length(size_by_var) > 0) { |
747 | ! |
validate(need(is.numeric(ANL[[size_by_var]]), "Variable to size by must be numeric")) |
748 | ! |
substitute( |
749 | ! |
expr = size * ANL[[size_by_var]] / max(ANL[[size_by_var]], na.rm = TRUE), |
750 | ! |
env = list(size = size, size_by_var = size_by_var) |
751 |
)
|
|
752 |
} else { |
|
753 | ! |
size
|
754 |
}
|
|
755 | ||
756 | ! |
plot_q <- merged$anl_q_r() |
757 | ||
758 | ! |
if (log_x) { |
759 | ! |
log_x_fn <- input$log_x_base |
760 | ! |
plot_q <- teal.code::eval_code( |
761 | ! |
object = plot_q, |
762 | ! |
code = substitute( |
763 | ! |
expr = ANL[, log_x_var] <- log_x_fn(ANL[, x_var]), |
764 | ! |
env = list( |
765 | ! |
x_var = x_var, |
766 | ! |
log_x_fn = as.name(log_x_fn), |
767 | ! |
log_x_var = paste0(log_x_fn, "_", x_var) |
768 |
)
|
|
769 |
)
|
|
770 |
)
|
|
771 |
}
|
|
772 | ||
773 | ! |
if (log_y) { |
774 | ! |
log_y_fn <- input$log_y_base |
775 | ! |
plot_q <- teal.code::eval_code( |
776 | ! |
object = plot_q, |
777 | ! |
code = substitute( |
778 | ! |
expr = ANL[, log_y_var] <- log_y_fn(ANL[, y_var]), |
779 | ! |
env = list( |
780 | ! |
y_var = y_var, |
781 | ! |
log_y_fn = as.name(log_y_fn), |
782 | ! |
log_y_var = paste0(log_y_fn, "_", y_var) |
783 |
)
|
|
784 |
)
|
|
785 |
)
|
|
786 |
}
|
|
787 | ||
788 | ! |
pre_pro_anl <- if (input$show_count) { |
789 | ! |
paste0( |
790 | ! |
"ANL %>% dplyr::group_by(",
|
791 | ! |
paste( |
792 | ! |
c( |
793 | ! |
if (length(color_by_var) > 0 && inherits(ANL[[color_by_var]], c("factor", "character"))) color_by_var, |
794 | ! |
row_facet_name,
|
795 | ! |
col_facet_name
|
796 |
),
|
|
797 | ! |
collapse = ", " |
798 |
),
|
|
799 | ! |
") %>% dplyr::mutate(n = dplyr::n()) %>% dplyr::ungroup()"
|
800 |
)
|
|
801 |
} else { |
|
802 | ! |
"ANL"
|
803 |
}
|
|
804 | ||
805 | ! |
plot_call <- substitute(expr = pre_pro_anl %>% ggplot2::ggplot(), env = list(pre_pro_anl = str2lang(pre_pro_anl))) |
806 | ||
807 | ! |
plot_call <- if (length(color_by_var) == 0) { |
808 | ! |
substitute( |
809 | ! |
expr = plot_call + |
810 | ! |
ggplot2::aes(x = x_name, y = y_name) + |
811 | ! |
ggplot2::geom_point(alpha = alpha_value, size = point_sizes, shape = shape_value, color = color_value), |
812 | ! |
env = list( |
813 | ! |
plot_call = plot_call, |
814 | ! |
x_name = if (log_x) as.name(paste0(log_x_fn, "_", x_var)) else as.name(x_var), |
815 | ! |
y_name = if (log_y) as.name(paste0(log_y_fn, "_", y_var)) else as.name(y_var), |
816 | ! |
alpha_value = alpha, |
817 | ! |
point_sizes = point_sizes, |
818 | ! |
shape_value = shape, |
819 | ! |
color_value = color |
820 |
)
|
|
821 |
)
|
|
822 |
} else { |
|
823 | ! |
substitute( |
824 | ! |
expr = plot_call + |
825 | ! |
ggplot2::aes(x = x_name, y = y_name, color = color_by_var_name) + |
826 | ! |
ggplot2::geom_point(alpha = alpha_value, size = point_sizes, shape = shape_value), |
827 | ! |
env = list( |
828 | ! |
plot_call = plot_call, |
829 | ! |
x_name = if (log_x) as.name(paste0(log_x_fn, "_", x_var)) else as.name(x_var), |
830 | ! |
y_name = if (log_y) as.name(paste0(log_y_fn, "_", y_var)) else as.name(y_var), |
831 | ! |
color_by_var_name = as.name(color_by_var), |
832 | ! |
alpha_value = alpha, |
833 | ! |
point_sizes = point_sizes, |
834 | ! |
shape_value = shape |
835 |
)
|
|
836 |
)
|
|
837 |
}
|
|
838 | ||
839 | ! |
if (rug_plot) plot_call <- substitute(expr = plot_call + geom_rug(), env = list(plot_call = plot_call)) |
840 | ||
841 | ! |
plot_label_generator <- function(rhs_formula = quote(y ~ 1), |
842 | ! |
show_form = input$show_form, |
843 | ! |
show_r2 = input$show_r2, |
844 | ! |
show_count = input$show_count, |
845 | ! |
pos = input$pos, |
846 | ! |
label_size = input$label_size) { |
847 | ! |
stopifnot(sum(show_form, show_r2, show_count) >= 1) |
848 | ! |
aes_label <- paste0( |
849 | ! |
"aes(",
|
850 | ! |
if (show_count) "n = n, ", |
851 | ! |
"label = ",
|
852 | ! |
if (sum(show_form, show_r2, show_count) > 1) "paste(", |
853 | ! |
paste( |
854 | ! |
c( |
855 | ! |
if (show_form) "stat(eq.label)", |
856 | ! |
if (show_r2) "stat(adj.rr.label)", |
857 | ! |
if (show_count) "paste('N ~`=`~', n)" |
858 |
),
|
|
859 | ! |
collapse = ", " |
860 |
),
|
|
861 | ! |
if (sum(show_form, show_r2, show_count) > 1) ", sep = '*\", \"*'))" else ")" |
862 |
)
|
|
863 | ! |
label_geom <- substitute( |
864 | ! |
expr = ggpmisc::stat_poly_eq( |
865 | ! |
mapping = aes_label, |
866 | ! |
formula = rhs_formula, |
867 | ! |
parse = TRUE, |
868 | ! |
label.x = pos, |
869 | ! |
size = label_size |
870 |
),
|
|
871 | ! |
env = list( |
872 | ! |
rhs_formula = rhs_formula, |
873 | ! |
pos = pos, |
874 | ! |
aes_label = str2lang(aes_label), |
875 | ! |
label_size = label_size |
876 |
)
|
|
877 |
)
|
|
878 | ! |
substitute( |
879 | ! |
expr = plot_call + label_geom, |
880 | ! |
env = list( |
881 | ! |
plot_call = plot_call, |
882 | ! |
label_geom = label_geom |
883 |
)
|
|
884 |
)
|
|
885 |
}
|
|
886 | ||
887 | ! |
if (trend_line_is_applicable()) { |
888 | ! |
shinyjs::hide("line_msg") |
889 | ! |
shinyjs::show("smoothing_degree") |
890 | ! |
if (!add_trend_line()) { |
891 | ! |
shinyjs::hide("ci") |
892 | ! |
shinyjs::hide("color_sub") |
893 | ! |
shinyjs::hide("show_form") |
894 | ! |
shinyjs::hide("show_r2") |
895 | ! |
if (input$show_count) { |
896 | ! |
plot_call <- plot_label_generator(show_form = FALSE, show_r2 = FALSE) |
897 | ! |
shinyjs::show("label_pos") |
898 | ! |
shinyjs::show("label_size") |
899 |
} else { |
|
900 | ! |
shinyjs::hide("label_pos") |
901 | ! |
shinyjs::hide("label_size") |
902 |
}
|
|
903 |
} else { |
|
904 | ! |
shinyjs::show("ci") |
905 | ! |
shinyjs::show("show_form") |
906 | ! |
shinyjs::show("show_r2") |
907 | ! |
if (nrow(ANL) - nrow(stats::na.omit(ANL[, c(x_var, y_var)])) > 0) { |
908 | ! |
plot_q <- teal.code::eval_code( |
909 | ! |
plot_q,
|
910 | ! |
substitute( |
911 | ! |
expr = ANL <- dplyr::filter(ANL, !is.na(x_var) & !is.na(y_var)), |
912 | ! |
env = list(x_var = as.name(x_var), y_var = as.name(y_var)) |
913 |
)
|
|
914 |
)
|
|
915 |
}
|
|
916 | ! |
rhs_formula <- substitute( |
917 | ! |
expr = y ~ poly(x, smoothing_degree, raw = TRUE), |
918 | ! |
env = list(smoothing_degree = smoothing_degree) |
919 |
)
|
|
920 | ! |
if (input$show_form || input$show_r2 || input$show_count) { |
921 | ! |
plot_call <- plot_label_generator(rhs_formula = rhs_formula) |
922 | ! |
shinyjs::show("label_pos") |
923 | ! |
shinyjs::show("label_size") |
924 |
} else { |
|
925 | ! |
shinyjs::hide("label_pos") |
926 | ! |
shinyjs::hide("label_size") |
927 |
}
|
|
928 | ! |
plot_call <- substitute( |
929 | ! |
expr = plot_call + ggplot2::geom_smooth(formula = rhs_formula, se = TRUE, level = ci, method = "lm"), |
930 | ! |
env = list(plot_call = plot_call, rhs_formula = rhs_formula, ci = ci) |
931 |
)
|
|
932 |
}
|
|
933 |
} else { |
|
934 | ! |
shinyjs::hide("smoothing_degree") |
935 | ! |
shinyjs::hide("ci") |
936 | ! |
shinyjs::hide("color_sub") |
937 | ! |
shinyjs::hide("show_form") |
938 | ! |
shinyjs::hide("show_r2") |
939 | ! |
if (input$show_count) { |
940 | ! |
plot_call <- plot_label_generator(show_form = FALSE, show_r2 = FALSE) |
941 | ! |
shinyjs::show("label_pos") |
942 | ! |
shinyjs::show("label_size") |
943 |
} else { |
|
944 | ! |
shinyjs::hide("label_pos") |
945 | ! |
shinyjs::hide("label_size") |
946 |
}
|
|
947 | ! |
shinyjs::show("line_msg") |
948 |
}
|
|
949 | ||
950 | ! |
if (!is.null(facet_cl)) { |
951 | ! |
plot_call <- substitute(expr = plot_call + facet_cl, env = list(plot_call = plot_call, facet_cl = facet_cl)) |
952 |
}
|
|
953 | ||
954 | ! |
y_label <- varname_w_label( |
955 | ! |
y_var,
|
956 | ! |
ANL,
|
957 | ! |
prefix = if (log_y) paste(log_y_fn, "(") else NULL, |
958 | ! |
suffix = if (log_y) ")" else NULL |
959 |
)
|
|
960 | ! |
x_label <- varname_w_label( |
961 | ! |
x_var,
|
962 | ! |
ANL,
|
963 | ! |
prefix = if (log_x) paste(log_x_fn, "(") else NULL, |
964 | ! |
suffix = if (log_x) ")" else NULL |
965 |
)
|
|
966 | ||
967 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
968 | ! |
labs = list(y = y_label, x = x_label), |
969 | ! |
theme = list(legend.position = "bottom") |
970 |
)
|
|
971 | ||
972 | ! |
if (rotate_xaxis_labels) { |
973 | ! |
dev_ggplot2_args$theme[["axis.text.x"]] <- quote(ggplot2::element_text(angle = 45, hjust = 1)) |
974 |
}
|
|
975 | ||
976 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
977 | ! |
user_plot = ggplot2_args, |
978 | ! |
module_plot = dev_ggplot2_args |
979 |
)
|
|
980 | ||
981 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(all_ggplot2_args, ggtheme = ggtheme) |
982 | ||
983 | ||
984 | ! |
if (add_density) { |
985 | ! |
plot_call <- substitute( |
986 | ! |
expr = ggExtra::ggMarginal( |
987 | ! |
plot_call + labs + ggthemes + themes, |
988 | ! |
type = "density", |
989 | ! |
groupColour = group_colour |
990 |
),
|
|
991 | ! |
env = list( |
992 | ! |
plot_call = plot_call, |
993 | ! |
group_colour = if (length(color_by_var) > 0) TRUE else FALSE, |
994 | ! |
labs = parsed_ggplot2_args$labs, |
995 | ! |
ggthemes = parsed_ggplot2_args$ggtheme, |
996 | ! |
themes = parsed_ggplot2_args$theme |
997 |
)
|
|
998 |
)
|
|
999 |
} else { |
|
1000 | ! |
plot_call <- substitute( |
1001 | ! |
expr = plot_call + |
1002 | ! |
labs + |
1003 | ! |
ggthemes + |
1004 | ! |
themes,
|
1005 | ! |
env = list( |
1006 | ! |
plot_call = plot_call, |
1007 | ! |
labs = parsed_ggplot2_args$labs, |
1008 | ! |
ggthemes = parsed_ggplot2_args$ggtheme, |
1009 | ! |
themes = parsed_ggplot2_args$theme |
1010 |
)
|
|
1011 |
)
|
|
1012 |
}
|
|
1013 | ||
1014 | ! |
plot_call <- substitute(expr = plot <- plot_call, env = list(plot_call = plot_call)) |
1015 | ||
1016 | ! |
teal.code::eval_code(plot_q, plot_call) |
1017 |
}) |
|
1018 | ||
1019 | ! |
decorated_output_plot_q <- srv_decorate_teal_data( |
1020 | ! |
id = "decorator", |
1021 | ! |
data = output_q, |
1022 | ! |
decorators = select_decorators(decorators, "plot"), |
1023 | ! |
expr = print(plot) |
1024 |
)
|
|
1025 | ||
1026 | ! |
plot_r <- reactive(req(decorated_output_plot_q())[["plot"]]) |
1027 | ||
1028 |
# Insert the plot into a plot_with_settings module from teal.widgets
|
|
1029 | ! |
pws <- teal.widgets::plot_with_settings_srv( |
1030 | ! |
id = "scatter_plot", |
1031 | ! |
plot_r = plot_r, |
1032 | ! |
height = plot_height, |
1033 | ! |
width = plot_width, |
1034 | ! |
brushing = TRUE |
1035 |
)
|
|
1036 | ||
1037 | ! |
output$data_table <- DT::renderDataTable({ |
1038 | ! |
plot_brush <- pws$brush() |
1039 | ||
1040 | ! |
if (!is.null(plot_brush)) { |
1041 | ! |
validate(need(!input$add_density, "Brushing feature is currently not supported when plot has marginal density")) |
1042 |
}
|
|
1043 | ||
1044 | ! |
merged_data <- isolate(teal.code::dev_suppress(output_q()[["ANL"]])) |
1045 | ||
1046 | ! |
brushed_df <- teal.widgets::clean_brushedPoints(merged_data, plot_brush) |
1047 | ! |
numeric_cols <- names(brushed_df)[ |
1048 | ! |
vapply(brushed_df, function(x) is.numeric(x) && !is.integer(x), FUN.VALUE = logical(1)) |
1049 |
]
|
|
1050 | ||
1051 | ! |
if (length(numeric_cols) > 0) { |
1052 | ! |
DT::formatRound( |
1053 | ! |
DT::datatable(brushed_df, |
1054 | ! |
rownames = FALSE, |
1055 | ! |
options = list(scrollX = TRUE, pageLength = input$data_table_rows) |
1056 |
),
|
|
1057 | ! |
numeric_cols,
|
1058 | ! |
table_dec
|
1059 |
)
|
|
1060 |
} else { |
|
1061 | ! |
DT::datatable(brushed_df, rownames = FALSE, options = list(scrollX = TRUE, pageLength = input$data_table_rows)) |
1062 |
}
|
|
1063 |
}) |
|
1064 | ||
1065 |
# Render R code.
|
|
1066 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_output_plot_q()))) |
1067 | ||
1068 | ! |
teal.widgets::verbatim_popup_srv( |
1069 | ! |
id = "rcode", |
1070 | ! |
verbatim_content = source_code_r, |
1071 | ! |
title = "R Code for scatterplot" |
1072 |
)
|
|
1073 | ||
1074 |
### REPORTER
|
|
1075 | ! |
if (with_reporter) { |
1076 | ! |
card_fun <- function(comment, label) { |
1077 | ! |
card <- teal::report_card_template( |
1078 | ! |
title = "Scatter Plot", |
1079 | ! |
label = label, |
1080 | ! |
with_filter = with_filter, |
1081 | ! |
filter_panel_api = filter_panel_api |
1082 |
)
|
|
1083 | ! |
card$append_text("Plot", "header3") |
1084 | ! |
card$append_plot(plot_r(), dim = pws$dim()) |
1085 | ! |
if (!comment == "") { |
1086 | ! |
card$append_text("Comment", "header3") |
1087 | ! |
card$append_text(comment) |
1088 |
}
|
|
1089 | ! |
card$append_src(source_code_r()) |
1090 | ! |
card
|
1091 |
}
|
|
1092 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
1093 |
}
|
|
1094 |
###
|
|
1095 |
}) |
|
1096 |
}
|
1 |
#' `teal` module: Missing data analysis
|
|
2 |
#'
|
|
3 |
#' This module analyzes missing data in `data.frame`s to help users explore missing observations and
|
|
4 |
#' gain insights into the completeness of their data.
|
|
5 |
#' It is useful for clinical data analysis within the context of `CDISC` standards and
|
|
6 |
#' adaptable for general data analysis purposes.
|
|
7 |
#'
|
|
8 |
#' @inheritParams teal::module
|
|
9 |
#' @inheritParams shared_params
|
|
10 |
#' @param parent_dataname (`character(1)`) Specifies the parent dataset name. Default is `ADSL` for `CDISC` data.
|
|
11 |
#' If provided and exists, enables additional analysis "by subject". For non-`CDISC` data, this parameter can be
|
|
12 |
#' ignored.
|
|
13 |
# nolint start: line_length.
|
|
14 |
#' @param ggtheme (`character`) optional, specifies the default `ggplot2` theme for plots. Defaults to `classic`.
|
|
15 |
#' @param ggplot2_args `r roxygen_ggplot2_args_param("Summary Obs", "Summary Patients", "Combinations Main", "Combinations Hist", "By Subject")`
|
|
16 |
# nolint end: line_length.
|
|
17 |
#'
|
|
18 |
#' @inherit shared_params return
|
|
19 |
#'
|
|
20 |
#' @section Decorating Module:
|
|
21 |
#'
|
|
22 |
#' This module generates the following objects, which can be modified in place using decorators:
|
|
23 |
#' - `summary_plot` (`grob` created with [ggplot2::ggplotGrob()])
|
|
24 |
#' - `combination_plot` (`grob` created with [ggplot2::ggplotGrob()])
|
|
25 |
#' - `by_subject_plot` (`ggplot`)
|
|
26 |
#' - `table` (`datatables` created with [DT::datatable()])
|
|
27 |
#'
|
|
28 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
29 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
30 |
#' See code snippet below:
|
|
31 |
#'
|
|
32 |
#' ```
|
|
33 |
#' tm_missing_data(
|
|
34 |
#' ..., # arguments for module
|
|
35 |
#' decorators = list(
|
|
36 |
#' summary_plot = teal_transform_module(...), # applied only to `summary_plot` output
|
|
37 |
#' combination_plot = teal_transform_module(...), # applied only to `combination_plot` output
|
|
38 |
#' by_subject_plot = teal_transform_module(...), # applied only to `by_subject_plot` output
|
|
39 |
#' table = teal_transform_module(...) # applied only to `table` output
|
|
40 |
#' )
|
|
41 |
#' )
|
|
42 |
#' ```
|
|
43 |
#'
|
|
44 |
#' For additional details and examples of decorators, refer to the vignette
|
|
45 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
46 |
#'
|
|
47 |
#' To learn more please refer to the vignette
|
|
48 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
49 |
#'
|
|
50 |
#' @examplesShinylive
|
|
51 |
#' library(teal.modules.general)
|
|
52 |
#' interactive <- function() TRUE
|
|
53 |
#' {{ next_example }}
|
|
54 |
#' @examples
|
|
55 |
#' # general example data
|
|
56 |
#' data <- teal_data()
|
|
57 |
#' data <- within(data, {
|
|
58 |
#' require(nestcolor)
|
|
59 |
#'
|
|
60 |
#' add_nas <- function(x) {
|
|
61 |
#' x[sample(seq_along(x), floor(length(x) * runif(1, .05, .17)))] <- NA
|
|
62 |
#' x
|
|
63 |
#' }
|
|
64 |
#'
|
|
65 |
#' iris <- iris
|
|
66 |
#' mtcars <- mtcars
|
|
67 |
#'
|
|
68 |
#' iris[] <- lapply(iris, add_nas)
|
|
69 |
#' mtcars[] <- lapply(mtcars, add_nas)
|
|
70 |
#' mtcars[["cyl"]] <- as.factor(mtcars[["cyl"]])
|
|
71 |
#' mtcars[["gear"]] <- as.factor(mtcars[["gear"]])
|
|
72 |
#' })
|
|
73 |
#'
|
|
74 |
#' app <- init(
|
|
75 |
#' data = data,
|
|
76 |
#' modules = modules(
|
|
77 |
#' tm_missing_data(parent_dataname = "mtcars")
|
|
78 |
#' )
|
|
79 |
#' )
|
|
80 |
#' if (interactive()) {
|
|
81 |
#' shinyApp(app$ui, app$server)
|
|
82 |
#' }
|
|
83 |
#'
|
|
84 |
#' @examplesShinylive
|
|
85 |
#' library(teal.modules.general)
|
|
86 |
#' interactive <- function() TRUE
|
|
87 |
#' {{ next_example }}
|
|
88 |
#' @examples
|
|
89 |
#' # CDISC example data
|
|
90 |
#' data <- teal_data()
|
|
91 |
#' data <- within(data, {
|
|
92 |
#' require(nestcolor)
|
|
93 |
#' ADSL <- teal.data::rADSL
|
|
94 |
#' ADRS <- rADRS
|
|
95 |
#' })
|
|
96 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
97 |
#'
|
|
98 |
#' app <- init(
|
|
99 |
#' data = data,
|
|
100 |
#' modules = modules(
|
|
101 |
#' tm_missing_data()
|
|
102 |
#' )
|
|
103 |
#' )
|
|
104 |
#' if (interactive()) {
|
|
105 |
#' shinyApp(app$ui, app$server)
|
|
106 |
#' }
|
|
107 |
#'
|
|
108 |
#' @export
|
|
109 |
#'
|
|
110 |
tm_missing_data <- function(label = "Missing data", |
|
111 |
plot_height = c(600, 400, 5000), |
|
112 |
plot_width = NULL, |
|
113 |
datanames = "all", |
|
114 |
parent_dataname = "ADSL", |
|
115 |
ggtheme = c("classic", "gray", "bw", "linedraw", "light", "dark", "minimal", "void"), |
|
116 |
ggplot2_args = list( |
|
117 |
"Combinations Hist" = teal.widgets::ggplot2_args(labs = list(caption = NULL)), |
|
118 |
"Combinations Main" = teal.widgets::ggplot2_args(labs = list(title = NULL)) |
|
119 |
),
|
|
120 |
pre_output = NULL, |
|
121 |
post_output = NULL, |
|
122 |
transformators = list(), |
|
123 |
decorators = list()) { |
|
124 | ! |
message("Initializing tm_missing_data") |
125 | ||
126 |
# Normalize the parameters
|
|
127 | ! |
if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args) |
128 | ||
129 |
# Start of assertions
|
|
130 | ! |
checkmate::assert_string(label) |
131 | ||
132 | ! |
checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
133 | ! |
checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
134 | ! |
checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
135 | ! |
checkmate::assert_numeric( |
136 | ! |
plot_width[1], |
137 | ! |
lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width" |
138 |
)
|
|
139 | ||
140 | ! |
checkmate::assert_character(datanames, min.len = 0, min.chars = 1, null.ok = TRUE) |
141 | ! |
checkmate::assert_character(parent_dataname, min.len = 0, max.len = 1) |
142 | ! |
ggtheme <- match.arg(ggtheme) |
143 | ||
144 | ! |
plot_choices <- c("Summary Obs", "Summary Patients", "Combinations Main", "Combinations Hist", "By Subject") |
145 | ! |
checkmate::assert_list(ggplot2_args, types = "ggplot2_args") |
146 | ! |
checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices)) |
147 | ||
148 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
149 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
150 | ||
151 | ! |
available_decorators <- c("summary_plot", "combination_plot", "by_subject_plot", "table") |
152 | ! |
assert_decorators(decorators, names = available_decorators) |
153 |
# End of assertions
|
|
154 | ||
155 | ! |
datanames_module <- if (identical(datanames, "all") || is.null(datanames)) { |
156 | ! |
datanames
|
157 |
} else { |
|
158 | ! |
union(datanames, parent_dataname) |
159 |
}
|
|
160 | ||
161 | ! |
ans <- module( |
162 | ! |
label,
|
163 | ! |
server = srv_page_missing_data, |
164 | ! |
datanames = datanames_module, |
165 | ! |
server_args = list( |
166 | ! |
datanames = if (is.null(datanames)) "all" else datanames, |
167 | ! |
parent_dataname = parent_dataname, |
168 | ! |
plot_height = plot_height, |
169 | ! |
plot_width = plot_width, |
170 | ! |
ggplot2_args = ggplot2_args, |
171 | ! |
ggtheme = ggtheme, |
172 | ! |
decorators = decorators |
173 |
),
|
|
174 | ! |
ui = ui_page_missing_data, |
175 | ! |
transformators = transformators, |
176 | ! |
ui_args = list(pre_output = pre_output, post_output = post_output) |
177 |
)
|
|
178 | ! |
attr(ans, "teal_bookmarkable") <- TRUE |
179 | ! |
ans
|
180 |
}
|
|
181 | ||
182 |
# UI function for the missing data module (all datasets)
|
|
183 |
ui_page_missing_data <- function(id, pre_output = NULL, post_output = NULL) { |
|
184 | ! |
ns <- NS(id) |
185 | ! |
tagList( |
186 | ! |
include_css_files("custom"), |
187 | ! |
teal.widgets::standard_layout( |
188 | ! |
output = teal.widgets::white_small_well( |
189 | ! |
tags$div( |
190 | ! |
class = "flex", |
191 | ! |
column( |
192 | ! |
width = 12, |
193 | ! |
uiOutput(ns("dataset_tabs")) |
194 |
)
|
|
195 |
)
|
|
196 |
),
|
|
197 | ! |
encoding = tags$div( |
198 | ! |
uiOutput(ns("dataset_encodings")) |
199 |
),
|
|
200 | ! |
uiOutput(ns("dataset_reporter")), |
201 | ! |
pre_output = pre_output, |
202 | ! |
post_output = post_output |
203 |
)
|
|
204 |
)
|
|
205 |
}
|
|
206 | ||
207 |
# Server function for the missing data module (all datasets)
|
|
208 |
srv_page_missing_data <- function(id, data, reporter, filter_panel_api, datanames, parent_dataname, |
|
209 |
plot_height, plot_width, ggplot2_args, ggtheme, decorators) { |
|
210 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
211 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
212 | ! |
moduleServer(id, function(input, output, session) { |
213 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
214 | ||
215 | ! |
datanames <- Filter(function(name) { |
216 | ! |
is.data.frame(isolate(data())[[name]]) |
217 | ! |
}, if (identical(datanames, "all")) names(isolate(data())) else datanames) |
218 | ||
219 | ! |
if_subject_plot <- length(parent_dataname) > 0 && parent_dataname %in% datanames |
220 | ||
221 | ! |
ns <- session$ns |
222 | ||
223 | ! |
output$dataset_tabs <- renderUI({ |
224 | ! |
do.call( |
225 | ! |
tabsetPanel,
|
226 | ! |
c( |
227 | ! |
id = ns("dataname_tab"), |
228 | ! |
lapply( |
229 | ! |
datanames,
|
230 | ! |
function(x) { |
231 | ! |
tabPanel( |
232 | ! |
title = x, |
233 | ! |
column( |
234 | ! |
width = 12, |
235 | ! |
tags$div( |
236 | ! |
class = "mt-4", |
237 | ! |
ui_missing_data(id = ns(x), by_subject_plot = if_subject_plot) |
238 |
)
|
|
239 |
)
|
|
240 |
)
|
|
241 |
}
|
|
242 |
)
|
|
243 |
)
|
|
244 |
)
|
|
245 |
}) |
|
246 | ||
247 | ! |
output$dataset_encodings <- renderUI({ |
248 | ! |
tagList( |
249 | ! |
lapply( |
250 | ! |
datanames,
|
251 | ! |
function(x) { |
252 | ! |
conditionalPanel( |
253 | ! |
is_tab_active_js(ns("dataname_tab"), x), |
254 | ! |
encoding_missing_data( |
255 | ! |
id = ns(x), |
256 | ! |
summary_per_patient = if_subject_plot, |
257 | ! |
ggtheme = ggtheme, |
258 | ! |
datanames = datanames, |
259 | ! |
decorators = decorators |
260 |
)
|
|
261 |
)
|
|
262 |
}
|
|
263 |
)
|
|
264 |
)
|
|
265 |
}) |
|
266 | ||
267 | ! |
output$dataset_reporter <- renderUI({ |
268 | ! |
lapply(datanames, function(x) { |
269 | ! |
dataname_ns <- NS(ns(x)) |
270 | ||
271 | ! |
conditionalPanel( |
272 | ! |
is_tab_active_js(ns("dataname_tab"), x), |
273 | ! |
tagList( |
274 | ! |
teal.widgets::verbatim_popup_ui(dataname_ns("rcode"), "Show R code") |
275 |
)
|
|
276 |
)
|
|
277 |
}) |
|
278 |
}) |
|
279 | ||
280 | ! |
lapply( |
281 | ! |
datanames,
|
282 | ! |
function(x) { |
283 | ! |
srv_missing_data( |
284 | ! |
id = x, |
285 | ! |
data = data, |
286 | ! |
reporter = if (with_reporter) reporter, |
287 | ! |
filter_panel_api = if (with_filter) filter_panel_api, |
288 | ! |
dataname = x, |
289 | ! |
parent_dataname = parent_dataname, |
290 | ! |
plot_height = plot_height, |
291 | ! |
plot_width = plot_width, |
292 | ! |
ggplot2_args = ggplot2_args, |
293 | ! |
decorators = decorators |
294 |
)
|
|
295 |
}
|
|
296 |
)
|
|
297 |
}) |
|
298 |
}
|
|
299 | ||
300 |
# UI function for the missing data module (single dataset)
|
|
301 |
ui_missing_data <- function(id, by_subject_plot = FALSE) { |
|
302 | ! |
ns <- NS(id) |
303 | ||
304 | ! |
tab_list <- list( |
305 | ! |
tabPanel( |
306 | ! |
"Summary",
|
307 | ! |
teal.widgets::plot_with_settings_ui(id = ns("summary_plot")), |
308 | ! |
helpText( |
309 | ! |
tags$p(paste( |
310 | ! |
'The "Summary" graph shows the number of missing values per variable (both absolute and percentage),',
|
311 | ! |
"sorted by magnitude."
|
312 |
)), |
|
313 | ! |
tags$p( |
314 | ! |
'The "summary per patients" graph is showing how many subjects have at least one missing observation',
|
315 | ! |
"for each variable. It will be most useful for panel datasets."
|
316 |
)
|
|
317 |
)
|
|
318 |
),
|
|
319 | ! |
tabPanel( |
320 | ! |
"Combinations",
|
321 | ! |
teal.widgets::plot_with_settings_ui(id = ns("combination_plot")), |
322 | ! |
helpText( |
323 | ! |
tags$p(paste( |
324 | ! |
'The "Combinations" graph is used to explore the relationship between the missing data within',
|
325 | ! |
"different columns of the dataset.",
|
326 | ! |
"It shows the different patterns of missingness in the rows of the data.",
|
327 | ! |
'For example, suppose that 70 rows of the data have exactly columns "A" and "B" missing.',
|
328 | ! |
"In this case there would be a bar of height 70 in the top graph and",
|
329 | ! |
'the column below this in the second graph would have rows "A" and "B" cells shaded red.'
|
330 |
)), |
|
331 | ! |
tags$p(paste( |
332 | ! |
"Due to the large number of missing data patterns possible, only those with a large set of observations",
|
333 | ! |
'are shown in the graph and the "Combination cut-off" slider can be used to adjust the number shown.'
|
334 |
)) |
|
335 |
)
|
|
336 |
),
|
|
337 | ! |
tabPanel( |
338 | ! |
"By Variable Levels",
|
339 | ! |
teal.widgets::get_dt_rows(ns("levels_table"), ns("levels_table_rows")), |
340 | ! |
DT::dataTableOutput(ns("levels_table")) |
341 |
)
|
|
342 |
)
|
|
343 | ! |
if (isTRUE(by_subject_plot)) { |
344 | ! |
tab_list <- append( |
345 | ! |
tab_list,
|
346 | ! |
list(tabPanel( |
347 | ! |
"Grouped by Subject",
|
348 | ! |
teal.widgets::plot_with_settings_ui(id = ns("by_subject_plot")), |
349 | ! |
helpText( |
350 | ! |
tags$p(paste( |
351 | ! |
"This graph shows the missingness with respect to subjects rather than individual rows of the",
|
352 | ! |
"dataset. Each row represents one dataset variable and each column a single subject. Only subjects",
|
353 | ! |
"with at least one record in this dataset are shown. For a given subject, if they have any missing",
|
354 | ! |
"values of a specific variable then the appropriate cell in the graph is marked as missing."
|
355 |
)) |
|
356 |
)
|
|
357 |
)) |
|
358 |
)
|
|
359 |
}
|
|
360 | ||
361 | ! |
do.call( |
362 | ! |
tabsetPanel,
|
363 | ! |
c( |
364 | ! |
id = ns("summary_type"), |
365 | ! |
tab_list
|
366 |
)
|
|
367 |
)
|
|
368 |
}
|
|
369 | ||
370 |
# UI encoding for the missing data module (all datasets)
|
|
371 |
encoding_missing_data <- function(id, summary_per_patient = FALSE, ggtheme, datanames, decorators) { |
|
372 | ! |
ns <- NS(id) |
373 | ||
374 | ! |
tagList( |
375 |
### Reporter
|
|
376 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
377 |
###
|
|
378 | ! |
tags$label("Encodings", class = "text-primary"), |
379 | ! |
helpText( |
380 | ! |
paste0("Dataset", `if`(length(datanames) > 1, "s", ""), ":"), |
381 | ! |
tags$code(paste(datanames, collapse = ", ")) |
382 |
),
|
|
383 | ! |
uiOutput(ns("variables")), |
384 | ! |
actionButton( |
385 | ! |
ns("filter_na"), |
386 | ! |
tags$span("Select only vars with missings", class = "whitespace-normal"), |
387 | ! |
width = "100%", |
388 | ! |
class = "mb-4" |
389 |
),
|
|
390 | ! |
conditionalPanel( |
391 | ! |
is_tab_active_js(ns("summary_type"), "Summary"), |
392 | ! |
checkboxInput( |
393 | ! |
ns("any_na"), |
394 | ! |
tags$div( |
395 | ! |
class = "teal-tooltip", |
396 | ! |
tagList( |
397 | ! |
"Add **anyna** variable",
|
398 | ! |
icon("circle-info"), |
399 | ! |
tags$span( |
400 | ! |
class = "tooltiptext", |
401 | ! |
"Describes the number of observations with at least one missing value in any variable."
|
402 |
)
|
|
403 |
)
|
|
404 |
),
|
|
405 | ! |
value = FALSE |
406 |
),
|
|
407 | ! |
if (summary_per_patient) { |
408 | ! |
checkboxInput( |
409 | ! |
ns("if_patients_plot"), |
410 | ! |
tags$div( |
411 | ! |
class = "teal-tooltip", |
412 | ! |
tagList( |
413 | ! |
"Add summary per patients",
|
414 | ! |
icon("circle-info"), |
415 | ! |
tags$span( |
416 | ! |
class = "tooltiptext", |
417 | ! |
paste( |
418 | ! |
"Displays the number of missing values per observation,",
|
419 | ! |
"where the x-axis is sorted by observation appearance in the table."
|
420 |
)
|
|
421 |
)
|
|
422 |
)
|
|
423 |
),
|
|
424 | ! |
value = FALSE |
425 |
)
|
|
426 |
},
|
|
427 | ! |
ui_decorate_teal_data(ns("dec_summary_plot"), decorators = select_decorators(decorators, "summary_plot")) |
428 |
),
|
|
429 | ! |
conditionalPanel( |
430 | ! |
is_tab_active_js(ns("summary_type"), "Combinations"), |
431 | ! |
uiOutput(ns("cutoff")), |
432 | ! |
ui_decorate_teal_data(ns("dec_combination_plot"), decorators = select_decorators(decorators, "combination_plot")) |
433 |
),
|
|
434 | ! |
conditionalPanel( |
435 | ! |
is_tab_active_js(ns("summary_type"), "Grouped by Subject"), |
436 | ! |
ui_decorate_teal_data(ns("dec_by_subject_plot"), decorators = select_decorators(decorators, "by_subject_plot")) |
437 |
),
|
|
438 | ! |
conditionalPanel( |
439 | ! |
is_tab_active_js(ns("summary_type"), "By Variable Levels"), |
440 | ! |
uiOutput(ns("group_by_var_ui")), |
441 | ! |
uiOutput(ns("group_by_vals_ui")), |
442 | ! |
radioButtons( |
443 | ! |
ns("count_type"), |
444 | ! |
label = "Display missing as", |
445 | ! |
choices = c("counts", "proportions"), |
446 | ! |
selected = "counts", |
447 | ! |
inline = TRUE |
448 |
),
|
|
449 | ! |
ui_decorate_teal_data(ns("dec_summary_table"), decorators = select_decorators(decorators, "table")) |
450 |
),
|
|
451 | ! |
teal.widgets::panel_item( |
452 | ! |
title = "Plot settings", |
453 | ! |
selectInput( |
454 | ! |
inputId = ns("ggtheme"), |
455 | ! |
label = "Theme (by ggplot):", |
456 | ! |
choices = ggplot_themes, |
457 | ! |
selected = ggtheme, |
458 | ! |
multiple = FALSE |
459 |
)
|
|
460 |
)
|
|
461 |
)
|
|
462 |
}
|
|
463 | ||
464 |
# Server function for the missing data (single dataset)
|
|
465 |
srv_missing_data <- function(id, |
|
466 |
data,
|
|
467 |
reporter,
|
|
468 |
filter_panel_api,
|
|
469 |
dataname,
|
|
470 |
parent_dataname,
|
|
471 |
plot_height,
|
|
472 |
plot_width,
|
|
473 |
ggplot2_args,
|
|
474 |
decorators) { |
|
475 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
476 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
477 | ! |
checkmate::assert_class(data, "reactive") |
478 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
479 | ! |
moduleServer(id, function(input, output, session) { |
480 | ! |
ns <- session$ns |
481 | ||
482 | ! |
prev_group_by_var <- reactiveVal("") |
483 | ! |
data_r <- reactive(data()[[dataname]]) |
484 | ! |
data_keys <- reactive(unlist(teal.data::join_keys(data())[[dataname]])) |
485 | ||
486 | ! |
iv_r <- reactive({ |
487 | ! |
iv <- shinyvalidate::InputValidator$new() |
488 | ! |
iv$add_rule( |
489 | ! |
"variables_select",
|
490 | ! |
shinyvalidate::sv_required("At least one reference variable needs to be selected.") |
491 |
)
|
|
492 | ! |
iv$add_rule( |
493 | ! |
"variables_select",
|
494 | ! |
~ if (length(setdiff((.), data_keys())) < 1) "Please also select non-key columns." |
495 |
)
|
|
496 | ! |
iv_summary_table <- shinyvalidate::InputValidator$new() |
497 | ! |
iv_summary_table$condition(~ isTRUE(input$summary_type == "By Variable Levels")) |
498 | ! |
iv_summary_table$add_rule("count_type", shinyvalidate::sv_required("Please select type of counts")) |
499 | ! |
iv_summary_table$add_rule( |
500 | ! |
"group_by_vals",
|
501 | ! |
shinyvalidate::sv_required("Please select both group-by variable and values") |
502 |
)
|
|
503 | ! |
iv_summary_table$add_rule( |
504 | ! |
"group_by_var",
|
505 | ! |
~ if (length(.) > 0 && length(input$variables_select) == 1 && (.) == input$variables_select) { |
506 | ! |
"If only one reference variable is selected it must not be the grouping variable."
|
507 |
}
|
|
508 |
)
|
|
509 | ! |
iv_summary_table$add_rule( |
510 | ! |
"variables_select",
|
511 | ! |
~ if (length(input$group_by_var) > 0 && length(.) == 1 && (.) == input$group_by_var) { |
512 | ! |
"If only one reference variable is selected it must not be the grouping variable."
|
513 |
}
|
|
514 |
)
|
|
515 | ! |
iv$add_validator(iv_summary_table) |
516 | ! |
iv$enable() |
517 | ! |
iv
|
518 |
}) |
|
519 | ||
520 | ! |
data_parent_keys <- reactive({ |
521 | ! |
if (length(parent_dataname) > 0 && parent_dataname %in% names(data())) { |
522 | ! |
keys <- teal.data::join_keys(data())[[dataname]] |
523 | ! |
if (parent_dataname %in% names(keys)) { |
524 | ! |
keys[[parent_dataname]] |
525 |
} else { |
|
526 | ! |
keys[[dataname]] |
527 |
}
|
|
528 |
} else { |
|
529 | ! |
NULL
|
530 |
}
|
|
531 |
}) |
|
532 | ||
533 | ! |
common_code_q <- reactive({ |
534 | ! |
teal::validate_inputs(iv_r()) |
535 | ||
536 | ! |
group_var <- input$group_by_var |
537 | ! |
anl <- data_r() |
538 | ! |
qenv <- teal.code::eval_code(data(), { |
539 | ! |
'library("dplyr");library("ggplot2");library("tidyr");library("gridExtra")' # nolint quotes |
540 |
}) |
|
541 | ||
542 | ! |
qenv <- if (!is.null(selected_vars()) && length(selected_vars()) != ncol(anl)) { |
543 | ! |
teal.code::eval_code( |
544 | ! |
qenv,
|
545 | ! |
substitute( |
546 | ! |
expr = ANL <- anl_name[, selected_vars, drop = FALSE], |
547 | ! |
env = list(anl_name = as.name(dataname), selected_vars = selected_vars()) |
548 |
)
|
|
549 |
)
|
|
550 |
} else { |
|
551 | ! |
teal.code::eval_code( |
552 | ! |
qenv,
|
553 | ! |
substitute(expr = ANL <- anl_name, env = list(anl_name = as.name(dataname))) |
554 |
)
|
|
555 |
}
|
|
556 | ||
557 | ! |
if (input$summary_type == "By Variable Levels" && !is.null(group_var) && !(group_var %in% selected_vars())) { |
558 | ! |
qenv <- teal.code::eval_code( |
559 | ! |
qenv,
|
560 | ! |
substitute( |
561 | ! |
expr = ANL[[group_var]] <- anl_name[[group_var]], |
562 | ! |
env = list(group_var = group_var, anl_name = as.name(dataname)) |
563 |
)
|
|
564 |
)
|
|
565 |
}
|
|
566 | ||
567 | ! |
new_col_name <- "**anyna**" |
568 | ||
569 | ! |
qenv <- teal.code::eval_code( |
570 | ! |
qenv,
|
571 | ! |
substitute( |
572 | ! |
expr = |
573 | ! |
create_cols_labels <- function(cols, just_label = FALSE) { |
574 | ! |
column_labels <- column_labels_value |
575 | ! |
column_labels[is.na(column_labels) | length(column_labels) == 0] <- "" |
576 | ! |
if (just_label) { |
577 | ! |
labels <- column_labels[cols] |
578 |
} else { |
|
579 | ! |
labels <- ifelse(cols == new_col_name | cols == "", cols, paste0(column_labels[cols], " [", cols, "]")) |
580 |
}
|
|
581 | ! |
labels
|
582 |
},
|
|
583 | ! |
env = list( |
584 | ! |
new_col_name = new_col_name, |
585 | ! |
column_labels_value = c(teal.data::col_labels(data_r())[selected_vars()], |
586 | ! |
new_col_name = new_col_name |
587 |
)
|
|
588 |
)
|
|
589 |
)
|
|
590 |
)
|
|
591 | ! |
qenv
|
592 |
}) |
|
593 | ||
594 | ! |
selected_vars <- reactive({ |
595 | ! |
req(input$variables_select) |
596 | ! |
keys <- data_keys() |
597 | ! |
vars <- unique(c(keys, input$variables_select)) |
598 | ! |
vars
|
599 |
}) |
|
600 | ||
601 | ! |
vars_summary <- reactive({ |
602 | ! |
na_count <- data_r() %>% |
603 | ! |
sapply(function(x) mean(is.na(x)), USE.NAMES = TRUE) %>% |
604 | ! |
sort(decreasing = TRUE) |
605 | ||
606 | ! |
tibble::tibble( |
607 | ! |
key = names(na_count), |
608 | ! |
value = unname(na_count), |
609 | ! |
label = cut(na_count, breaks = seq(from = 0, to = 1, by = 0.1), include.lowest = TRUE) |
610 |
)
|
|
611 |
}) |
|
612 | ||
613 |
# Keep encoding panel up-to-date
|
|
614 | ! |
output$variables <- renderUI({ |
615 | ! |
choices <- split(x = vars_summary()$key, f = vars_summary()$label, drop = TRUE) %>% rev() |
616 | ! |
selected <- choices <- unname(unlist(choices)) |
617 | ||
618 | ! |
teal.widgets::optionalSelectInput( |
619 | ! |
ns("variables_select"), |
620 | ! |
label = "Select variables", |
621 | ! |
label_help = HTML(paste0("Dataset: ", tags$code(dataname))), |
622 | ! |
choices = teal.transform::variable_choices(data_r(), choices), |
623 | ! |
selected = selected, |
624 | ! |
multiple = TRUE |
625 |
)
|
|
626 |
}) |
|
627 | ||
628 | ! |
observeEvent(input$filter_na, { |
629 | ! |
choices <- vars_summary() %>% |
630 | ! |
dplyr::select(!!as.name("key")) %>% |
631 | ! |
getElement(name = 1) |
632 | ||
633 | ! |
selected <- vars_summary() %>% |
634 | ! |
dplyr::filter(!!as.name("value") > 0) %>% |
635 | ! |
dplyr::select(!!as.name("key")) %>% |
636 | ! |
getElement(name = 1) |
637 | ||
638 | ! |
teal.widgets::updateOptionalSelectInput( |
639 | ! |
session = session, |
640 | ! |
inputId = "variables_select", |
641 | ! |
choices = teal.transform::variable_choices(data_r()), |
642 | ! |
selected = restoreInput(ns("variables_select"), selected) |
643 |
)
|
|
644 |
}) |
|
645 | ||
646 | ! |
output$group_by_var_ui <- renderUI({ |
647 | ! |
all_choices <- teal.transform::variable_choices(data_r()) |
648 | ! |
cat_choices <- all_choices[!sapply(data_r(), function(x) is.numeric(x) || inherits(x, "POSIXct"))] |
649 | ! |
validate( |
650 | ! |
need(cat_choices, "Dataset does not have any non-numeric or non-datetime variables to use to group data with") |
651 |
)
|
|
652 | ! |
teal.widgets::optionalSelectInput( |
653 | ! |
ns("group_by_var"), |
654 | ! |
label = "Group by variable", |
655 | ! |
choices = cat_choices, |
656 | ! |
selected = `if`( |
657 | ! |
is.null(isolate(input$group_by_var)), |
658 | ! |
cat_choices[1], |
659 | ! |
isolate(input$group_by_var) |
660 |
),
|
|
661 | ! |
multiple = FALSE, |
662 | ! |
label_help = paste0("Dataset: ", dataname) |
663 |
)
|
|
664 |
}) |
|
665 | ||
666 | ! |
output$group_by_vals_ui <- renderUI({ |
667 | ! |
req(input$group_by_var) |
668 | ||
669 | ! |
choices <- teal.transform::value_choices(data_r(), input$group_by_var, input$group_by_var) |
670 | ! |
prev_choices <- isolate(input$group_by_vals) |
671 | ||
672 |
# determine selected value based on filtered data
|
|
673 |
# display those previously selected values that are still available
|
|
674 | ! |
selected <- if (!is.null(prev_choices) && any(prev_choices %in% choices)) { |
675 | ! |
prev_choices[match(choices[choices %in% prev_choices], prev_choices)] |
676 | ! |
} else if ( |
677 | ! |
!is.null(prev_choices) && |
678 | ! |
!any(prev_choices %in% choices) && |
679 | ! |
isolate(prev_group_by_var()) == input$group_by_var |
680 |
) { |
|
681 |
# if not any previously selected value is available and the grouping variable is the same,
|
|
682 |
# then display NULL
|
|
683 | ! |
NULL
|
684 |
} else { |
|
685 |
# if new grouping variable (i.e. not any previously selected value is available),
|
|
686 |
# then display all choices
|
|
687 | ! |
choices
|
688 |
}
|
|
689 | ||
690 | ! |
prev_group_by_var(input$group_by_var) # set current group_by_var |
691 | ! |
validate(need(length(choices) < 100, "Please select group-by variable with fewer than 100 unique values")) |
692 | ! |
teal.widgets::optionalSelectInput( |
693 | ! |
ns("group_by_vals"), |
694 | ! |
label = "Filter levels", |
695 | ! |
choices = choices, |
696 | ! |
selected = selected, |
697 | ! |
multiple = TRUE, |
698 | ! |
label_help = paste0("Dataset: ", dataname) |
699 |
)
|
|
700 |
}) |
|
701 | ||
702 | ! |
combination_cutoff_q <- reactive({ |
703 | ! |
req(common_code_q()) |
704 | ! |
teal.code::eval_code( |
705 | ! |
common_code_q(), |
706 | ! |
quote( |
707 | ! |
combination_cutoff <- ANL %>% |
708 | ! |
dplyr::mutate_all(is.na) %>% |
709 | ! |
dplyr::group_by_all() %>% |
710 | ! |
dplyr::tally() %>% |
711 | ! |
dplyr::ungroup() |
712 |
)
|
|
713 |
)
|
|
714 |
}) |
|
715 | ||
716 | ! |
output$cutoff <- renderUI({ |
717 | ! |
x <- combination_cutoff_q()[["combination_cutoff"]]$n |
718 | ||
719 |
# select 10-th from the top
|
|
720 | ! |
n <- length(x) |
721 | ! |
idx <- max(1, n - 10) |
722 | ! |
prev_value <- isolate(input$combination_cutoff) |
723 | ! |
value <- if (is.null(prev_value) || prev_value > max(x) || prev_value < min(x)) { |
724 | ! |
sort(x, partial = idx)[idx] |
725 |
} else { |
|
726 | ! |
prev_value
|
727 |
}
|
|
728 | ||
729 | ! |
teal.widgets::optionalSliderInputValMinMax( |
730 | ! |
ns("combination_cutoff"), |
731 | ! |
"Combination cut-off",
|
732 | ! |
c(value, range(x)) |
733 |
)
|
|
734 |
}) |
|
735 | ||
736 |
# Prepare qenvs for output objects
|
|
737 | ||
738 | ! |
summary_plot_q <- reactive({ |
739 | ! |
req(input$summary_type == "Summary") # needed to trigger show r code update on tab change |
740 | ! |
teal::validate_has_data(data_r(), 1) |
741 | ||
742 | ! |
qenv <- common_code_q() |
743 | ! |
if (input$any_na) { |
744 | ! |
new_col_name <- "**anyna**" |
745 | ! |
qenv <- teal.code::eval_code( |
746 | ! |
qenv,
|
747 | ! |
substitute( |
748 | ! |
expr = ANL[[new_col_name]] <- ifelse(rowSums(is.na(ANL)) > 0, NA, FALSE), |
749 | ! |
env = list(new_col_name = new_col_name) |
750 |
)
|
|
751 |
)
|
|
752 |
}
|
|
753 | ||
754 | ! |
qenv <- teal.code::eval_code( |
755 | ! |
qenv,
|
756 | ! |
substitute( |
757 | ! |
expr = analysis_vars <- setdiff(colnames(ANL), data_keys), |
758 | ! |
env = list(data_keys = data_keys()) |
759 |
)
|
|
760 |
) %>% |
|
761 | ! |
teal.code::eval_code( |
762 | ! |
substitute( |
763 | ! |
expr = summary_plot_obs <- data_frame_call[, analysis_vars] %>% |
764 | ! |
dplyr::summarise_all(list(function(x) sum(is.na(x)))) %>% |
765 | ! |
tidyr::pivot_longer(dplyr::everything(), names_to = "col", values_to = "n_na") %>% |
766 | ! |
dplyr::mutate(n_not_na = nrow(ANL) - n_na) %>% |
767 | ! |
tidyr::pivot_longer(-col, names_to = "isna", values_to = "n") %>% |
768 | ! |
dplyr::mutate(isna = isna == "n_na", n_pct = n / nrow(ANL) * 100), |
769 | ! |
env = list(data_frame_call = if (!inherits(data_r(), "tbl_df")) { |
770 | ! |
quote(tibble::as_tibble(ANL)) |
771 |
} else { |
|
772 | ! |
quote(ANL) |
773 |
}) |
|
774 |
)
|
|
775 |
) %>% |
|
776 |
# x axis ordering according to number of missing values and alphabet
|
|
777 | ! |
teal.code::eval_code( |
778 | ! |
quote( |
779 | ! |
expr = x_levels <- dplyr::filter(summary_plot_obs, isna) %>% |
780 | ! |
dplyr::arrange(n_pct, dplyr::desc(col)) %>% |
781 | ! |
dplyr::pull(col) %>% |
782 | ! |
create_cols_labels() |
783 |
)
|
|
784 |
)
|
|
785 | ||
786 |
# always set "**anyna**" level as the last one
|
|
787 | ! |
if (isolate(input$any_na)) { |
788 | ! |
qenv <- teal.code::eval_code( |
789 | ! |
qenv,
|
790 | ! |
quote(x_levels <- c(setdiff(x_levels, "**anyna**"), "**anyna**")) |
791 |
)
|
|
792 |
}
|
|
793 | ||
794 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
795 | ! |
labs = list(x = "Variable", y = "Missing observations"), |
796 | ! |
theme = list(legend.position = "bottom", axis.text.x = quote(ggplot2::element_text(angle = 45, hjust = 1))) |
797 |
)
|
|
798 | ||
799 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
800 | ! |
user_plot = ggplot2_args[["Summary Obs"]], |
801 | ! |
user_default = ggplot2_args$default, |
802 | ! |
module_plot = dev_ggplot2_args |
803 |
)
|
|
804 | ||
805 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
806 | ! |
all_ggplot2_args,
|
807 | ! |
ggtheme = input$ggtheme |
808 |
)
|
|
809 | ||
810 | ! |
qenv <- teal.code::eval_code( |
811 | ! |
qenv,
|
812 | ! |
substitute( |
813 | ! |
summary_plot_top <- summary_plot_obs %>% |
814 | ! |
ggplot2::ggplot() + |
815 | ! |
ggplot2::aes( |
816 | ! |
x = factor(create_cols_labels(col), levels = x_levels), |
817 | ! |
y = n_pct, |
818 | ! |
fill = isna |
819 |
) + |
|
820 | ! |
ggplot2::geom_bar(position = "fill", stat = "identity") + |
821 | ! |
ggplot2::scale_fill_manual( |
822 | ! |
name = "", |
823 | ! |
values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]), |
824 | ! |
labels = c("Present", "Missing") |
825 |
) + |
|
826 | ! |
ggplot2::scale_y_continuous( |
827 | ! |
labels = scales::percent_format(), |
828 | ! |
breaks = seq(0, 1, by = 0.1), |
829 | ! |
expand = c(0, 0) |
830 |
) + |
|
831 | ! |
ggplot2::geom_text( |
832 | ! |
ggplot2::aes(label = ifelse(isna == TRUE, sprintf("%d [%.02f%%]", n, n_pct), ""), y = 1), |
833 | ! |
hjust = 1, |
834 | ! |
color = "black" |
835 |
) + |
|
836 | ! |
labs + |
837 | ! |
ggthemes + |
838 | ! |
themes + |
839 | ! |
ggplot2::coord_flip(), |
840 | ! |
env = list( |
841 | ! |
labs = parsed_ggplot2_args$labs, |
842 | ! |
themes = parsed_ggplot2_args$theme, |
843 | ! |
ggthemes = parsed_ggplot2_args$ggtheme |
844 |
)
|
|
845 |
)
|
|
846 |
)
|
|
847 | ||
848 | ! |
if (isTRUE(input$if_patients_plot)) { |
849 | ! |
qenv <- teal.code::eval_code( |
850 | ! |
qenv,
|
851 | ! |
substitute( |
852 | ! |
expr = parent_keys <- keys, |
853 | ! |
env = list(keys = data_parent_keys()) |
854 |
)
|
|
855 |
) %>% |
|
856 | ! |
teal.code::eval_code(quote(ndistinct_subjects <- dplyr::n_distinct(ANL[, parent_keys]))) %>% |
857 | ! |
teal.code::eval_code( |
858 | ! |
quote( |
859 | ! |
summary_plot_patients <- ANL[, c(parent_keys, analysis_vars)] %>% |
860 | ! |
dplyr::group_by_at(parent_keys) %>% |
861 | ! |
dplyr::summarise_all(anyNA) %>% |
862 | ! |
tidyr::pivot_longer(cols = !dplyr::all_of(parent_keys), names_to = "col", values_to = "anyna") %>% |
863 | ! |
dplyr::group_by_at(c("col")) %>% |
864 | ! |
dplyr::summarise(count_na = sum(anyna)) %>% |
865 | ! |
dplyr::mutate(count_not_na = ndistinct_subjects - count_na) %>% |
866 | ! |
tidyr::pivot_longer(-c(col), names_to = "isna", values_to = "n") %>% |
867 | ! |
dplyr::mutate(isna = isna == "count_na", n_pct = n / ndistinct_subjects * 100) %>% |
868 | ! |
dplyr::arrange_at(c("isna", "n"), .funs = dplyr::desc) |
869 |
)
|
|
870 |
)
|
|
871 | ||
872 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
873 | ! |
labs = list(x = "", y = "Missing patients"), |
874 | ! |
theme = list( |
875 | ! |
legend.position = "bottom", |
876 | ! |
axis.text.x = quote(ggplot2::element_text(angle = 45, hjust = 1)), |
877 | ! |
axis.text.y = quote(ggplot2::element_blank()) |
878 |
)
|
|
879 |
)
|
|
880 | ||
881 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
882 | ! |
user_plot = ggplot2_args[["Summary Patients"]], |
883 | ! |
user_default = ggplot2_args$default, |
884 | ! |
module_plot = dev_ggplot2_args |
885 |
)
|
|
886 | ||
887 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
888 | ! |
all_ggplot2_args,
|
889 | ! |
ggtheme = input$ggtheme |
890 |
)
|
|
891 | ||
892 | ! |
qenv <- teal.code::eval_code( |
893 | ! |
qenv,
|
894 | ! |
substitute( |
895 | ! |
summary_plot_bottom <- summary_plot_patients %>% |
896 | ! |
ggplot2::ggplot() + |
897 | ! |
ggplot2::aes_( |
898 | ! |
x = ~ factor(create_cols_labels(col), levels = x_levels), |
899 | ! |
y = ~n_pct, |
900 | ! |
fill = ~isna |
901 |
) + |
|
902 | ! |
ggplot2::geom_bar(alpha = 1, stat = "identity", position = "fill") + |
903 | ! |
ggplot2::scale_y_continuous( |
904 | ! |
labels = scales::percent_format(), |
905 | ! |
breaks = seq(0, 1, by = 0.1), |
906 | ! |
expand = c(0, 0) |
907 |
) + |
|
908 | ! |
ggplot2::scale_fill_manual( |
909 | ! |
name = "", |
910 | ! |
values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]), |
911 | ! |
labels = c("Present", "Missing") |
912 |
) + |
|
913 | ! |
ggplot2::geom_text( |
914 | ! |
ggplot2::aes(label = ifelse(isna == TRUE, sprintf("%d [%.02f%%]", n, n_pct), ""), y = 1), |
915 | ! |
hjust = 1, |
916 | ! |
color = "black" |
917 |
) + |
|
918 | ! |
labs + |
919 | ! |
ggthemes + |
920 | ! |
themes + |
921 | ! |
ggplot2::coord_flip(), |
922 | ! |
env = list( |
923 | ! |
labs = parsed_ggplot2_args$labs, |
924 | ! |
themes = parsed_ggplot2_args$theme, |
925 | ! |
ggthemes = parsed_ggplot2_args$ggtheme |
926 |
)
|
|
927 |
)
|
|
928 |
)
|
|
929 |
}
|
|
930 | ||
931 | ! |
if (isTRUE(input$if_patients_plot)) { |
932 | ! |
within(qenv, { |
933 | ! |
g1 <- ggplot2::ggplotGrob(summary_plot_top) |
934 | ! |
g2 <- ggplot2::ggplotGrob(summary_plot_bottom) |
935 | ! |
summary_plot <- gridExtra::gtable_cbind(g1, g2, size = "first") |
936 | ! |
summary_plot$heights <- grid::unit.pmax(g1$heights, g2$heights) |
937 |
}) |
|
938 |
} else { |
|
939 | ! |
within(qenv, { |
940 | ! |
g1 <- ggplot2::ggplotGrob(summary_plot_top) |
941 | ! |
summary_plot <- g1 |
942 |
}) |
|
943 |
}
|
|
944 |
}) |
|
945 | ||
946 | ! |
combination_plot_q <- reactive({ |
947 | ! |
req(input$summary_type == "Combinations", input$combination_cutoff, combination_cutoff_q()) |
948 | ! |
teal::validate_has_data(data_r(), 1) |
949 | ||
950 | ! |
qenv <- teal.code::eval_code( |
951 | ! |
combination_cutoff_q(), |
952 | ! |
substitute( |
953 | ! |
expr = data_combination_plot_cutoff <- combination_cutoff %>% |
954 | ! |
dplyr::filter(n >= combination_cutoff_value) %>% |
955 | ! |
dplyr::mutate(id = rank(-n, ties.method = "first")) %>% |
956 | ! |
tidyr::pivot_longer(-c(n, id), names_to = "key", values_to = "value") %>% |
957 | ! |
dplyr::arrange(n), |
958 | ! |
env = list(combination_cutoff_value = input$combination_cutoff) |
959 |
)
|
|
960 |
)
|
|
961 | ||
962 |
# find keys in dataset not selected in the UI and remove them from dataset
|
|
963 | ! |
keys_not_selected <- setdiff(data_keys(), input$variables_select) |
964 | ! |
if (length(keys_not_selected) > 0) { |
965 | ! |
qenv <- teal.code::eval_code( |
966 | ! |
qenv,
|
967 | ! |
substitute( |
968 | ! |
expr = data_combination_plot_cutoff <- data_combination_plot_cutoff %>% |
969 | ! |
dplyr::filter(!key %in% keys_not_selected), |
970 | ! |
env = list(keys_not_selected = keys_not_selected) |
971 |
)
|
|
972 |
)
|
|
973 |
}
|
|
974 | ||
975 | ! |
qenv <- teal.code::eval_code( |
976 | ! |
qenv,
|
977 | ! |
quote( |
978 | ! |
labels <- data_combination_plot_cutoff %>% |
979 | ! |
dplyr::filter(key == key[[1]]) %>% |
980 | ! |
getElement(name = 1) |
981 |
)
|
|
982 |
)
|
|
983 | ||
984 | ! |
dev_ggplot2_args1 <- teal.widgets::ggplot2_args( |
985 | ! |
labs = list(x = "", y = ""), |
986 | ! |
theme = list( |
987 | ! |
legend.position = "bottom", |
988 | ! |
axis.text.x = quote(ggplot2::element_blank()) |
989 |
)
|
|
990 |
)
|
|
991 | ||
992 | ! |
all_ggplot2_args1 <- teal.widgets::resolve_ggplot2_args( |
993 | ! |
user_plot = ggplot2_args[["Combinations Hist"]], |
994 | ! |
user_default = ggplot2_args$default, |
995 | ! |
module_plot = dev_ggplot2_args1 |
996 |
)
|
|
997 | ||
998 | ! |
parsed_ggplot2_args1 <- teal.widgets::parse_ggplot2_args( |
999 | ! |
all_ggplot2_args1,
|
1000 | ! |
ggtheme = "void" |
1001 |
)
|
|
1002 | ||
1003 | ! |
dev_ggplot2_args2 <- teal.widgets::ggplot2_args( |
1004 | ! |
labs = list(x = "", y = ""), |
1005 | ! |
theme = list( |
1006 | ! |
legend.position = "bottom", |
1007 | ! |
axis.text.x = quote(ggplot2::element_blank()), |
1008 | ! |
axis.ticks = quote(ggplot2::element_blank()), |
1009 | ! |
panel.grid.major = quote(ggplot2::element_blank()) |
1010 |
)
|
|
1011 |
)
|
|
1012 | ||
1013 | ! |
all_ggplot2_args2 <- teal.widgets::resolve_ggplot2_args( |
1014 | ! |
user_plot = ggplot2_args[["Combinations Main"]], |
1015 | ! |
user_default = ggplot2_args$default, |
1016 | ! |
module_plot = dev_ggplot2_args2 |
1017 |
)
|
|
1018 | ||
1019 | ! |
parsed_ggplot2_args2 <- teal.widgets::parse_ggplot2_args( |
1020 | ! |
all_ggplot2_args2,
|
1021 | ! |
ggtheme = input$ggtheme |
1022 |
)
|
|
1023 | ||
1024 | ! |
qenv <- teal.code::eval_code( |
1025 | ! |
qenv,
|
1026 | ! |
substitute( |
1027 | ! |
expr = { |
1028 | ! |
combination_plot_top <- data_combination_plot_cutoff %>% |
1029 | ! |
dplyr::select(id, n) %>% |
1030 | ! |
dplyr::distinct() %>% |
1031 | ! |
ggplot2::ggplot(ggplot2::aes(x = id, y = n)) + |
1032 | ! |
ggplot2::geom_bar(stat = "identity", fill = c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]) + |
1033 | ! |
ggplot2::geom_text( |
1034 | ! |
ggplot2::aes(label = n), |
1035 | ! |
position = ggplot2::position_dodge(width = 0.9), |
1036 | ! |
vjust = -0.25 |
1037 |
) + |
|
1038 | ! |
ggplot2::ylim(c(0, max(data_combination_plot_cutoff$n) * 1.5)) + |
1039 | ! |
labs1 + |
1040 | ! |
ggthemes1 + |
1041 | ! |
themes1
|
1042 | ||
1043 | ! |
graph_number_rows <- length(unique(data_combination_plot_cutoff$id)) |
1044 | ! |
graph_number_cols <- nrow(data_combination_plot_cutoff) / graph_number_rows |
1045 | ||
1046 | ! |
combination_plot_bottom <- data_combination_plot_cutoff %>% ggplot2::ggplot() + |
1047 | ! |
ggplot2::aes(x = create_cols_labels(key), y = id - 0.5, fill = value) + |
1048 | ! |
ggplot2::geom_tile(alpha = 0.85, height = 0.95) + |
1049 | ! |
ggplot2::scale_fill_manual( |
1050 | ! |
name = "", |
1051 | ! |
values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]), |
1052 | ! |
labels = c("Present", "Missing") |
1053 |
) + |
|
1054 | ! |
ggplot2::geom_hline(yintercept = seq_len(1 + graph_number_rows) - 1) + |
1055 | ! |
ggplot2::geom_vline(xintercept = seq_len(1 + graph_number_cols) - 0.5, linetype = "dotted") + |
1056 | ! |
ggplot2::coord_flip() + |
1057 | ! |
labs2 + |
1058 | ! |
ggthemes2 + |
1059 | ! |
themes2
|
1060 |
},
|
|
1061 | ! |
env = list( |
1062 | ! |
labs1 = parsed_ggplot2_args1$labs, |
1063 | ! |
themes1 = parsed_ggplot2_args1$theme, |
1064 | ! |
ggthemes1 = parsed_ggplot2_args1$ggtheme, |
1065 | ! |
labs2 = parsed_ggplot2_args2$labs, |
1066 | ! |
themes2 = parsed_ggplot2_args2$theme, |
1067 | ! |
ggthemes2 = parsed_ggplot2_args2$ggtheme |
1068 |
)
|
|
1069 |
)
|
|
1070 |
)
|
|
1071 | ||
1072 | ! |
within(qenv, { |
1073 | ! |
g1 <- ggplot2::ggplotGrob(combination_plot_top) |
1074 | ! |
g2 <- ggplot2::ggplotGrob(combination_plot_bottom) |
1075 | ||
1076 | ! |
combination_plot <- gridExtra::gtable_rbind(g1, g2, size = "last") |
1077 | ! |
combination_plot$heights[7] <- grid::unit(0.2, "null") # rescale to get the bar chart smaller |
1078 |
}) |
|
1079 |
}) |
|
1080 | ||
1081 | ! |
summary_table_q <- reactive({ |
1082 | ! |
req( |
1083 | ! |
input$summary_type == "By Variable Levels", # needed to trigger show r code update on tab change |
1084 | ! |
common_code_q() |
1085 |
)
|
|
1086 | ! |
teal::validate_has_data(data_r(), 1) |
1087 | ||
1088 |
# extract the ANL dataset for use in further validation
|
|
1089 | ! |
anl <- common_code_q()[["ANL"]] |
1090 | ||
1091 | ! |
group_var <- input$group_by_var |
1092 | ! |
validate( |
1093 | ! |
need( |
1094 | ! |
is.null(group_var) || |
1095 | ! |
length(unique(anl[[group_var]])) < 100, |
1096 | ! |
"Please select group-by variable with fewer than 100 unique values"
|
1097 |
)
|
|
1098 |
)
|
|
1099 | ||
1100 | ! |
group_vals <- input$group_by_vals |
1101 | ! |
variables_select <- input$variables_select |
1102 | ! |
vars <- unique(variables_select, group_var) |
1103 | ! |
count_type <- input$count_type |
1104 | ||
1105 | ! |
if (!is.null(selected_vars()) && length(selected_vars()) != ncol(anl)) { |
1106 | ! |
variables <- selected_vars() |
1107 |
} else { |
|
1108 | ! |
variables <- colnames(anl) |
1109 |
}
|
|
1110 | ||
1111 | ! |
summ_fn <- if (input$count_type == "counts") { |
1112 | ! |
function(x) sum(is.na(x)) |
1113 |
} else { |
|
1114 | ! |
function(x) round(sum(is.na(x)) / length(x), 4) |
1115 |
}
|
|
1116 | ||
1117 | ! |
qenv <- if (!is.null(group_var)) { |
1118 | ! |
common_code_libraries_q <- teal.code::eval_code( |
1119 | ! |
common_code_q(), |
1120 | ! |
'library("forcats");library("glue");' # nolint quotes |
1121 |
)
|
|
1122 | ! |
teal.code::eval_code( |
1123 | ! |
common_code_libraries_q,
|
1124 | ! |
substitute( |
1125 | ! |
expr = { |
1126 | ! |
summary_data <- ANL %>% |
1127 | ! |
dplyr::mutate(group_var_name := forcats::fct_na_value_to_level(as.factor(group_var_name), "NA")) %>% |
1128 | ! |
dplyr::group_by_at(group_var) %>% |
1129 | ! |
dplyr::filter(group_var_name %in% group_vals) |
1130 | ||
1131 | ! |
count_data <- dplyr::summarise(summary_data, n = dplyr::n()) |
1132 | ||
1133 | ! |
summary_data <- dplyr::summarise_all(summary_data, summ_fn) %>% |
1134 | ! |
dplyr::mutate(group_var_name := paste0(group_var, ":", group_var_name, "(N=", count_data$n, ")")) %>% |
1135 | ! |
tidyr::pivot_longer(!dplyr::all_of(group_var), names_to = "Variable", values_to = "out") %>% |
1136 | ! |
tidyr::pivot_wider(names_from = group_var, values_from = "out") %>% |
1137 | ! |
dplyr::mutate(`Variable label` = create_cols_labels(Variable, just_label = TRUE), .after = Variable) |
1138 |
},
|
|
1139 | ! |
env = list( |
1140 | ! |
group_var = group_var, group_var_name = as.name(group_var), group_vals = group_vals, summ_fn = summ_fn |
1141 |
)
|
|
1142 |
)
|
|
1143 |
)
|
|
1144 |
} else { |
|
1145 | ! |
teal.code::eval_code( |
1146 | ! |
common_code_q(), |
1147 | ! |
substitute( |
1148 | ! |
expr = summary_data <- ANL %>% |
1149 | ! |
dplyr::summarise_all(summ_fn) %>% |
1150 | ! |
tidyr::pivot_longer(dplyr::everything(), |
1151 | ! |
names_to = "Variable", |
1152 | ! |
values_to = paste0("Missing (N=", nrow(ANL), ")") |
1153 |
) %>% |
|
1154 | ! |
dplyr::mutate(`Variable label` = create_cols_labels(Variable), .after = Variable), |
1155 | ! |
env = list(summ_fn = summ_fn) |
1156 |
)
|
|
1157 |
)
|
|
1158 |
}
|
|
1159 | ||
1160 | ! |
within(qenv, table <- DT::datatable(summary_data)) |
1161 |
}) |
|
1162 | ||
1163 | ! |
by_subject_plot_q <- reactive({ |
1164 |
# needed to trigger show r code update on tab change
|
|
1165 | ! |
req(input$summary_type == "Grouped by Subject", common_code_q()) |
1166 | ||
1167 | ! |
teal::validate_has_data(data_r(), 1) |
1168 | ||
1169 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
1170 | ! |
labs = list(x = "", y = ""), |
1171 | ! |
theme = list(legend.position = "bottom", axis.text.x = quote(ggplot2::element_blank())) |
1172 |
)
|
|
1173 | ||
1174 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
1175 | ! |
user_plot = ggplot2_args[["By Subject"]], |
1176 | ! |
user_default = ggplot2_args$default, |
1177 | ! |
module_plot = dev_ggplot2_args |
1178 |
)
|
|
1179 | ||
1180 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
1181 | ! |
all_ggplot2_args,
|
1182 | ! |
ggtheme = input$ggtheme |
1183 |
)
|
|
1184 | ||
1185 |
# Unlikely that `rlang` is not available, new hashing may be expensive
|
|
1186 | ! |
hashing_function <- if (requireNamespace("rlang", quietly = TRUE)) { |
1187 | ! |
quote(rlang::hash) |
1188 |
} else { |
|
1189 | ! |
function(x) paste(as.integer(x), collapse = "") |
1190 |
}
|
|
1191 | ||
1192 | ! |
teal.code::eval_code( |
1193 | ! |
common_code_q(), |
1194 | ! |
substitute( |
1195 | ! |
expr = parent_keys <- keys, |
1196 | ! |
env = list(keys = data_parent_keys()) |
1197 |
)
|
|
1198 |
) %>% |
|
1199 | ! |
teal.code::eval_code( |
1200 | ! |
substitute( |
1201 | ! |
expr = analysis_vars <- setdiff(colnames(ANL), data_keys), |
1202 | ! |
env = list(data_keys = data_keys()) |
1203 |
)
|
|
1204 |
) %>% |
|
1205 | ! |
teal.code::eval_code( |
1206 | ! |
substitute( |
1207 | ! |
expr = { |
1208 | ! |
summary_plot_patients <- ANL[, c(parent_keys, analysis_vars)] %>% |
1209 | ! |
dplyr::group_by_at(parent_keys) %>% |
1210 | ! |
dplyr::mutate(id = dplyr::cur_group_id()) %>% |
1211 | ! |
dplyr::ungroup() %>% |
1212 | ! |
dplyr::group_by_at(c(parent_keys, "id")) %>% |
1213 | ! |
dplyr::summarise_all(anyNA) %>% |
1214 | ! |
dplyr::ungroup() |
1215 | ||
1216 |
# order subjects by decreasing number of missing and then by
|
|
1217 |
# missingness pattern (defined using sha1)
|
|
1218 | ! |
order_subjects <- summary_plot_patients %>% |
1219 | ! |
dplyr::select(-"id", -dplyr::all_of(parent_keys)) %>% |
1220 | ! |
dplyr::transmute( |
1221 | ! |
id = dplyr::row_number(), |
1222 | ! |
number_NA = apply(., 1, sum), |
1223 | ! |
sha = apply(., 1, hashing_function) |
1224 |
) %>% |
|
1225 | ! |
dplyr::arrange(dplyr::desc(number_NA), sha) %>% |
1226 | ! |
getElement(name = "id") |
1227 | ||
1228 |
# order columns by decreasing percent of missing values
|
|
1229 | ! |
ordered_columns <- summary_plot_patients %>% |
1230 | ! |
dplyr::select(-"id", -dplyr::all_of(parent_keys)) %>% |
1231 | ! |
dplyr::summarise( |
1232 | ! |
column = create_cols_labels(colnames(.)), |
1233 | ! |
na_count = apply(., MARGIN = 2, FUN = sum), |
1234 | ! |
na_percent = na_count / nrow(.) * 100 |
1235 |
) %>% |
|
1236 | ! |
dplyr::arrange(na_percent, dplyr::desc(column)) |
1237 | ||
1238 | ! |
summary_plot_patients <- summary_plot_patients %>% |
1239 | ! |
tidyr::gather("col", "isna", -"id", -dplyr::all_of(parent_keys)) %>% |
1240 | ! |
dplyr::mutate(col = create_cols_labels(col)) |
1241 |
},
|
|
1242 | ! |
env = list(hashing_function = hashing_function) |
1243 |
)
|
|
1244 |
) %>% |
|
1245 | ! |
teal.code::eval_code( |
1246 | ! |
substitute( |
1247 | ! |
expr = { |
1248 | ! |
by_subject_plot <- ggplot2::ggplot(summary_plot_patients, ggplot2::aes( |
1249 | ! |
x = factor(id, levels = order_subjects), |
1250 | ! |
y = factor(col, levels = ordered_columns[["column"]]), |
1251 | ! |
fill = isna |
1252 |
)) + |
|
1253 | ! |
ggplot2::geom_raster() + |
1254 | ! |
ggplot2::annotate( |
1255 | ! |
"text",
|
1256 | ! |
x = length(order_subjects), |
1257 | ! |
y = seq_len(nrow(ordered_columns)), |
1258 | ! |
hjust = 1, |
1259 | ! |
label = sprintf("%d [%.02f%%]", ordered_columns[["na_count"]], ordered_columns[["na_percent"]]) |
1260 |
) + |
|
1261 | ! |
ggplot2::scale_fill_manual( |
1262 | ! |
name = "", |
1263 | ! |
values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]), |
1264 | ! |
labels = c("Present", "Missing (at least one)") |
1265 |
) + |
|
1266 | ! |
labs + |
1267 | ! |
ggthemes + |
1268 | ! |
themes
|
1269 |
},
|
|
1270 | ! |
env = list( |
1271 | ! |
labs = parsed_ggplot2_args$labs, |
1272 | ! |
themes = parsed_ggplot2_args$theme, |
1273 | ! |
ggthemes = parsed_ggplot2_args$ggtheme |
1274 |
)
|
|
1275 |
)
|
|
1276 |
)
|
|
1277 |
}) |
|
1278 | ||
1279 |
# Decorated outputs
|
|
1280 | ||
1281 |
# Summary_plot_q
|
|
1282 | ! |
decorated_summary_plot_q <- srv_decorate_teal_data( |
1283 | ! |
id = "dec_summary_plot", |
1284 | ! |
data = summary_plot_q, |
1285 | ! |
decorators = select_decorators(decorators, "summary_plot"), |
1286 | ! |
expr = { |
1287 | ! |
grid::grid.newpage() |
1288 | ! |
grid::grid.draw(summary_plot) |
1289 |
}
|
|
1290 |
)
|
|
1291 | ||
1292 | ! |
decorated_combination_plot_q <- srv_decorate_teal_data( |
1293 | ! |
id = "dec_combination_plot", |
1294 | ! |
data = combination_plot_q, |
1295 | ! |
decorators = select_decorators(decorators, "combination_plot"), |
1296 | ! |
expr = { |
1297 | ! |
grid::grid.newpage() |
1298 | ! |
grid::grid.draw(combination_plot) |
1299 |
}
|
|
1300 |
)
|
|
1301 | ||
1302 | ! |
decorated_summary_table_q <- srv_decorate_teal_data( |
1303 | ! |
id = "dec_summary_table", |
1304 | ! |
data = summary_table_q, |
1305 | ! |
decorators = select_decorators(decorators, "table"), |
1306 | ! |
expr = table |
1307 |
)
|
|
1308 | ||
1309 | ! |
decorated_by_subject_plot_q <- srv_decorate_teal_data( |
1310 | ! |
id = "dec_by_subject_plot", |
1311 | ! |
data = by_subject_plot_q, |
1312 | ! |
decorators = select_decorators(decorators, "by_subject_plot"), |
1313 | ! |
expr = print(by_subject_plot) |
1314 |
)
|
|
1315 | ||
1316 |
# Plots & tables reactives
|
|
1317 | ||
1318 | ! |
summary_plot_r <- reactive({ |
1319 | ! |
req(decorated_summary_plot_q())[["summary_plot"]] |
1320 |
}) |
|
1321 | ||
1322 | ! |
combination_plot_r <- reactive({ |
1323 | ! |
req(decorated_combination_plot_q())[["combination_plot"]] |
1324 |
}) |
|
1325 | ||
1326 | ! |
summary_table_r <- reactive({ |
1327 | ! |
req(decorated_summary_table_q()) |
1328 | ||
1329 | ! |
if (length(input$variables_select) == 0) { |
1330 |
# so that zeroRecords message gets printed
|
|
1331 |
# using tibble as it supports weird column names, such as " "
|
|
1332 | ! |
DT::datatable( |
1333 | ! |
tibble::tibble(` ` = logical(0)), |
1334 | ! |
options = list(language = list(zeroRecords = "No variable selected."), pageLength = input$levels_table_rows) |
1335 |
)
|
|
1336 |
} else { |
|
1337 | ! |
decorated_summary_table_q()[["table"]] |
1338 |
}
|
|
1339 |
}) |
|
1340 | ||
1341 | ! |
by_subject_plot_r <- reactive({ |
1342 | ! |
req(decorated_by_subject_plot_q()[["by_subject_plot"]]) |
1343 |
}) |
|
1344 | ||
1345 |
# Generate output
|
|
1346 | ! |
pws1 <- teal.widgets::plot_with_settings_srv( |
1347 | ! |
id = "summary_plot", |
1348 | ! |
plot_r = summary_plot_r, |
1349 | ! |
height = plot_height, |
1350 | ! |
width = plot_width |
1351 |
)
|
|
1352 | ||
1353 | ! |
pws2 <- teal.widgets::plot_with_settings_srv( |
1354 | ! |
id = "combination_plot", |
1355 | ! |
plot_r = combination_plot_r, |
1356 | ! |
height = plot_height, |
1357 | ! |
width = plot_width |
1358 |
)
|
|
1359 | ||
1360 | ! |
output$levels_table <- DT::renderDataTable(summary_table_r()) |
1361 | ||
1362 | ! |
pws3 <- teal.widgets::plot_with_settings_srv( |
1363 | ! |
id = "by_subject_plot", |
1364 | ! |
plot_r = by_subject_plot_r, |
1365 | ! |
height = plot_height, |
1366 | ! |
width = plot_width |
1367 |
)
|
|
1368 | ||
1369 | ! |
decorated_final_q <- reactive({ |
1370 | ! |
sum_type <- req(input$summary_type) |
1371 | ! |
if (sum_type == "Summary") { |
1372 | ! |
decorated_summary_plot_q() |
1373 | ! |
} else if (sum_type == "Combinations") { |
1374 | ! |
decorated_combination_plot_q() |
1375 | ! |
} else if (sum_type == "By Variable Levels") { |
1376 | ! |
decorated_summary_table_q() |
1377 | ! |
} else if (sum_type == "Grouped by Subject") { |
1378 | ! |
decorated_by_subject_plot_q() |
1379 |
}
|
|
1380 |
}) |
|
1381 | ||
1382 |
# Render R code.
|
|
1383 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_final_q()))) |
1384 | ||
1385 | ! |
teal.widgets::verbatim_popup_srv( |
1386 | ! |
id = "rcode", |
1387 | ! |
verbatim_content = source_code_r, |
1388 | ! |
title = "Show R Code for Missing Data" |
1389 |
)
|
|
1390 | ||
1391 |
### REPORTER
|
|
1392 | ! |
if (with_reporter) { |
1393 | ! |
card_fun <- function(comment, label) { |
1394 | ! |
card <- teal::TealReportCard$new() |
1395 | ! |
sum_type <- input$summary_type |
1396 | ! |
title <- if (sum_type == "By Variable Levels") paste0(sum_type, " Table") else paste0(sum_type, " Plot") |
1397 | ! |
title_dataname <- paste(title, dataname, sep = " - ") |
1398 | ! |
label <- if (label == "") { |
1399 | ! |
paste("Missing Data", sum_type, dataname, sep = " - ") |
1400 |
} else { |
|
1401 | ! |
label
|
1402 |
}
|
|
1403 | ! |
card$set_name(label) |
1404 | ! |
card$append_text(title_dataname, "header2") |
1405 | ! |
if (with_filter) card$append_fs(filter_panel_api$get_filter_state()) |
1406 | ! |
if (sum_type == "Summary") { |
1407 | ! |
card$append_text("Plot", "header3") |
1408 | ! |
card$append_plot(summary_plot_r(), dim = pws1$dim()) |
1409 | ! |
} else if (sum_type == "Combinations") { |
1410 | ! |
card$append_text("Plot", "header3") |
1411 | ! |
card$append_plot(combination_plot_r(), dim = pws2$dim()) |
1412 | ! |
} else if (sum_type == "By Variable Levels") { |
1413 | ! |
card$append_text("Table", "header3") |
1414 | ! |
table <- decorated_summary_table_q()[["table"]] |
1415 | ! |
if (nrow(table) == 0L) { |
1416 | ! |
card$append_text("No data available for table.") |
1417 |
} else { |
|
1418 | ! |
card$append_table(table) |
1419 |
}
|
|
1420 | ! |
} else if (sum_type == "Grouped by Subject") { |
1421 | ! |
card$append_text("Plot", "header3") |
1422 | ! |
card$append_plot(by_subject_plot_r(), dim = pws3$dim()) |
1423 |
}
|
|
1424 | ! |
if (!comment == "") { |
1425 | ! |
card$append_text("Comment", "header3") |
1426 | ! |
card$append_text(comment) |
1427 |
}
|
|
1428 | ! |
card$append_src(source_code_r()) |
1429 | ! |
card
|
1430 |
}
|
|
1431 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
1432 |
}
|
|
1433 |
###
|
|
1434 |
}) |
|
1435 |
}
|
1 |
#' `teal` module: Data table viewer
|
|
2 |
#'
|
|
3 |
#' Module provides a dynamic and interactive way to view `data.frame`s in a `teal` application.
|
|
4 |
#' It uses the `DT` package to display data tables in a paginated, searchable, and sortable format,
|
|
5 |
#' which helps to enhance data exploration and analysis.
|
|
6 |
#'
|
|
7 |
#' The `DT` package has an option `DT.TOJSON_ARGS` to show `Inf` and `NA` in data tables.
|
|
8 |
#' Configure the `DT.TOJSON_ARGS` option via
|
|
9 |
#' `options(DT.TOJSON_ARGS = list(na = "string"))` before running the module.
|
|
10 |
#' Note though that sorting of numeric columns with `NA`/`Inf` will be lexicographic not numerical.
|
|
11 |
#'
|
|
12 |
#' @inheritParams teal::module
|
|
13 |
#' @inheritParams shared_params
|
|
14 |
#' @param variables_selected (`named list`) Character vectors of the variables (i.e. columns)
|
|
15 |
#' which should be initially shown for each dataset.
|
|
16 |
#' Names of list elements should correspond to the names of the datasets available in the app.
|
|
17 |
#' If no entry is specified for a dataset, the first six variables from that
|
|
18 |
#' dataset will initially be shown.
|
|
19 |
#' @param datasets_selected (`character`) `r lifecycle::badge("deprecated")` A vector of datasets which should be
|
|
20 |
#' shown and in what order. Use `datanames` instead.
|
|
21 |
#' @param dt_args (`named list`) Additional arguments to be passed to [DT::datatable()]
|
|
22 |
#' (must not include `data` or `options`).
|
|
23 |
#' @param dt_options (`named list`) The `options` argument to `DT::datatable`. By default
|
|
24 |
#' `list(searching = FALSE, pageLength = 30, lengthMenu = c(5, 15, 30, 100), scrollX = TRUE)`
|
|
25 |
#' @param server_rendering (`logical`) should the data table be rendered server side
|
|
26 |
#' (see `server` argument of [DT::renderDataTable()])
|
|
27 |
#'
|
|
28 |
#' @inherit shared_params return
|
|
29 |
#'
|
|
30 |
#' @examplesShinylive
|
|
31 |
#' library(teal.modules.general)
|
|
32 |
#' interactive <- function() TRUE
|
|
33 |
#' {{ next_example }}
|
|
34 |
#' @examples
|
|
35 |
#' # general data example
|
|
36 |
#' data <- teal_data()
|
|
37 |
#' data <- within(data, {
|
|
38 |
#' require(nestcolor)
|
|
39 |
#' iris <- iris
|
|
40 |
#' })
|
|
41 |
#'
|
|
42 |
#' app <- init(
|
|
43 |
#' data = data,
|
|
44 |
#' modules = modules(
|
|
45 |
#' tm_data_table(
|
|
46 |
#' variables_selected = list(
|
|
47 |
#' iris = c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species")
|
|
48 |
#' ),
|
|
49 |
#' dt_args = list(caption = "IRIS Table Caption")
|
|
50 |
#' )
|
|
51 |
#' )
|
|
52 |
#' )
|
|
53 |
#' if (interactive()) {
|
|
54 |
#' shinyApp(app$ui, app$server)
|
|
55 |
#' }
|
|
56 |
#'
|
|
57 |
#' @examplesShinylive
|
|
58 |
#' library(teal.modules.general)
|
|
59 |
#' interactive <- function() TRUE
|
|
60 |
#' {{ next_example }}
|
|
61 |
#' @examples
|
|
62 |
#' # CDISC data example
|
|
63 |
#' data <- teal_data()
|
|
64 |
#' data <- within(data, {
|
|
65 |
#' require(nestcolor)
|
|
66 |
#' ADSL <- teal.data::rADSL
|
|
67 |
#' })
|
|
68 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
69 |
#'
|
|
70 |
#' app <- init(
|
|
71 |
#' data = data,
|
|
72 |
#' modules = modules(
|
|
73 |
#' tm_data_table(
|
|
74 |
#' variables_selected = list(ADSL = c("STUDYID", "USUBJID", "SUBJID", "SITEID", "AGE", "SEX")),
|
|
75 |
#' dt_args = list(caption = "ADSL Table Caption")
|
|
76 |
#' )
|
|
77 |
#' )
|
|
78 |
#' )
|
|
79 |
#' if (interactive()) {
|
|
80 |
#' shinyApp(app$ui, app$server)
|
|
81 |
#' }
|
|
82 |
#'
|
|
83 |
#' @export
|
|
84 |
#'
|
|
85 |
tm_data_table <- function(label = "Data Table", |
|
86 |
variables_selected = list(), |
|
87 |
datasets_selected = deprecated(), |
|
88 |
datanames = if (missing(datasets_selected)) "all" else datasets_selected, |
|
89 |
dt_args = list(), |
|
90 |
dt_options = list( |
|
91 |
searching = FALSE, |
|
92 |
pageLength = 30, |
|
93 |
lengthMenu = c(5, 15, 30, 100), |
|
94 |
scrollX = TRUE |
|
95 |
),
|
|
96 |
server_rendering = FALSE, |
|
97 |
pre_output = NULL, |
|
98 |
post_output = NULL, |
|
99 |
transformators = list()) { |
|
100 | ! |
message("Initializing tm_data_table") |
101 | ||
102 |
# Start of assertions
|
|
103 | ! |
checkmate::assert_string(label) |
104 | ||
105 | ! |
checkmate::assert_list(variables_selected, min.len = 0, types = "character", names = "named") |
106 | ! |
if (length(variables_selected) > 0) { |
107 | ! |
lapply(seq_along(variables_selected), function(i) { |
108 | ! |
checkmate::assert_character(variables_selected[[i]], min.chars = 1, min.len = 1) |
109 | ! |
if (!is.null(names(variables_selected[[i]]))) { |
110 | ! |
checkmate::assert_names(names(variables_selected[[i]])) |
111 |
}
|
|
112 |
}) |
|
113 |
}
|
|
114 | ! |
if (!missing(datasets_selected)) { |
115 | ! |
lifecycle::deprecate_soft( |
116 | ! |
when = "0.4.0", |
117 | ! |
what = "tm_data_table(datasets_selected)", |
118 | ! |
with = "tm_data_table(datanames)", |
119 | ! |
details = 'Use tm_data_table(datanames = "all") to keep the previous behavior and avoid this warning.', |
120 |
)
|
|
121 |
}
|
|
122 | ! |
checkmate::assert_character(datanames, min.len = 0, min.chars = 1, null.ok = TRUE) |
123 | ! |
checkmate::assert( |
124 | ! |
checkmate::check_list(dt_args, len = 0), |
125 | ! |
checkmate::check_subset(names(dt_args), choices = names(formals(DT::datatable))) |
126 |
)
|
|
127 | ! |
checkmate::assert_list(dt_options, names = "named") |
128 | ! |
checkmate::assert_flag(server_rendering) |
129 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
130 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
131 | ||
132 |
# End of assertions
|
|
133 | ||
134 | ! |
ans <- module( |
135 | ! |
label,
|
136 | ! |
server = srv_page_data_table, |
137 | ! |
ui = ui_page_data_table, |
138 | ! |
datanames = datanames, |
139 | ! |
server_args = list( |
140 | ! |
datanames = if (is.null(datanames)) "all" else datanames, |
141 | ! |
variables_selected = variables_selected, |
142 | ! |
dt_args = dt_args, |
143 | ! |
dt_options = dt_options, |
144 | ! |
server_rendering = server_rendering |
145 |
),
|
|
146 | ! |
ui_args = list( |
147 | ! |
pre_output = pre_output, |
148 | ! |
post_output = post_output |
149 |
),
|
|
150 | ! |
transformators = transformators |
151 |
)
|
|
152 | ! |
attr(ans, "teal_bookmarkable") <- TRUE |
153 | ! |
ans
|
154 |
}
|
|
155 | ||
156 |
# UI page module
|
|
157 |
ui_page_data_table <- function(id, pre_output = NULL, post_output = NULL) { |
|
158 | ! |
ns <- NS(id) |
159 | ||
160 | ! |
tagList( |
161 | ! |
include_css_files("custom"), |
162 | ! |
teal.widgets::standard_layout( |
163 | ! |
output = teal.widgets::white_small_well( |
164 | ! |
fluidRow( |
165 | ! |
column( |
166 | ! |
width = 12, |
167 | ! |
checkboxInput( |
168 | ! |
ns("if_distinct"), |
169 | ! |
"Show only distinct rows:",
|
170 | ! |
value = FALSE |
171 |
)
|
|
172 |
)
|
|
173 |
),
|
|
174 | ! |
fluidRow( |
175 | ! |
class = "mb-8", |
176 | ! |
column( |
177 | ! |
width = 12, |
178 | ! |
uiOutput(ns("dataset_table")) |
179 |
)
|
|
180 |
)
|
|
181 |
),
|
|
182 | ! |
pre_output = pre_output, |
183 | ! |
post_output = post_output |
184 |
)
|
|
185 |
)
|
|
186 |
}
|
|
187 | ||
188 |
# Server page module
|
|
189 |
srv_page_data_table <- function(id, |
|
190 |
data,
|
|
191 |
datanames,
|
|
192 |
variables_selected,
|
|
193 |
dt_args,
|
|
194 |
dt_options,
|
|
195 |
server_rendering) { |
|
196 | ! |
checkmate::assert_class(data, "reactive") |
197 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
198 | ! |
moduleServer(id, function(input, output, session) { |
199 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
200 | ||
201 | ! |
if_filtered <- reactive(as.logical(input$if_filtered)) |
202 | ! |
if_distinct <- reactive(as.logical(input$if_distinct)) |
203 | ||
204 | ! |
datanames <- Filter(function(name) { |
205 | ! |
is.data.frame(isolate(data())[[name]]) |
206 | ! |
}, if (identical(datanames, "all")) names(isolate(data())) else datanames) |
207 | ||
208 | ||
209 | ! |
output$dataset_table <- renderUI({ |
210 | ! |
do.call( |
211 | ! |
tabsetPanel,
|
212 | ! |
c( |
213 | ! |
list(id = session$ns("dataname_tab")), |
214 | ! |
lapply( |
215 | ! |
datanames,
|
216 | ! |
function(x) { |
217 | ! |
dataset <- isolate(data()[[x]]) |
218 | ! |
choices <- names(dataset) |
219 | ! |
labels <- vapply( |
220 | ! |
dataset,
|
221 | ! |
function(x) ifelse(is.null(attr(x, "label")), "", attr(x, "label")), |
222 | ! |
character(1) |
223 |
)
|
|
224 | ! |
names(choices) <- ifelse( |
225 | ! |
is.na(labels) | labels == "", |
226 | ! |
choices,
|
227 | ! |
paste(choices, labels, sep = ": ") |
228 |
)
|
|
229 | ! |
variables_selected <- if (!is.null(variables_selected[[x]])) { |
230 | ! |
variables_selected[[x]] |
231 |
} else { |
|
232 | ! |
utils::head(choices) |
233 |
}
|
|
234 | ! |
tabPanel( |
235 | ! |
title = x, |
236 | ! |
column( |
237 | ! |
width = 12, |
238 | ! |
div( |
239 | ! |
class = "mt-4", |
240 | ! |
ui_data_table( |
241 | ! |
id = session$ns(x), |
242 | ! |
choices = choices, |
243 | ! |
selected = variables_selected |
244 |
)
|
|
245 |
)
|
|
246 |
)
|
|
247 |
)
|
|
248 |
}
|
|
249 |
)
|
|
250 |
)
|
|
251 |
)
|
|
252 |
}) |
|
253 | ||
254 | ! |
lapply( |
255 | ! |
datanames,
|
256 | ! |
function(x) { |
257 | ! |
srv_data_table( |
258 | ! |
id = x, |
259 | ! |
data = data, |
260 | ! |
dataname = x, |
261 | ! |
if_filtered = if_filtered, |
262 | ! |
if_distinct = if_distinct, |
263 | ! |
dt_args = dt_args, |
264 | ! |
dt_options = dt_options, |
265 | ! |
server_rendering = server_rendering |
266 |
)
|
|
267 |
}
|
|
268 |
)
|
|
269 |
}) |
|
270 |
}
|
|
271 | ||
272 |
# UI function for the data_table module
|
|
273 |
ui_data_table <- function(id, choices, selected) { |
|
274 | ! |
ns <- NS(id) |
275 | ||
276 | ! |
if (!is.null(selected)) { |
277 | ! |
all_choices <- choices |
278 | ! |
choices <- c(selected, setdiff(choices, selected)) |
279 | ! |
names(choices) <- names(all_choices)[match(choices, all_choices)] |
280 |
}
|
|
281 | ||
282 | ! |
tagList( |
283 | ! |
teal.widgets::get_dt_rows(ns("data_table"), ns("dt_rows")), |
284 | ! |
fluidRow( |
285 | ! |
teal.widgets::optionalSelectInput( |
286 | ! |
ns("variables"), |
287 | ! |
"Select variables:",
|
288 | ! |
choices = choices, |
289 | ! |
selected = selected, |
290 | ! |
multiple = TRUE, |
291 | ! |
width = "100%" |
292 |
)
|
|
293 |
),
|
|
294 | ! |
fluidRow( |
295 | ! |
DT::dataTableOutput(ns("data_table"), width = "100%") |
296 |
)
|
|
297 |
)
|
|
298 |
}
|
|
299 | ||
300 |
# Server function for the data_table module
|
|
301 |
srv_data_table <- function(id, |
|
302 |
data,
|
|
303 |
dataname,
|
|
304 |
if_filtered,
|
|
305 |
if_distinct,
|
|
306 |
dt_args,
|
|
307 |
dt_options,
|
|
308 |
server_rendering) { |
|
309 | ! |
moduleServer(id, function(input, output, session) { |
310 | ! |
iv <- shinyvalidate::InputValidator$new() |
311 | ! |
iv$add_rule("variables", shinyvalidate::sv_required("Please select valid variable names")) |
312 | ! |
iv$add_rule("variables", shinyvalidate::sv_in_set( |
313 | ! |
set = names(isolate(data())[[dataname]]), message_fmt = "Not all selected variables exist in the data" |
314 |
)) |
|
315 | ! |
iv$enable() |
316 | ||
317 | ! |
data_table_data <- reactive({ |
318 | ! |
df <- data()[[dataname]] |
319 | ||
320 | ! |
teal::validate_has_data(df, min_nrow = 1L, msg = paste("data", dataname, "is empty")) |
321 | ! |
qenv <- teal.code::eval_code( |
322 | ! |
data(), |
323 | ! |
'library("dplyr");library("DT")' # nolint quotes |
324 |
)
|
|
325 | ! |
teal.code::eval_code( |
326 | ! |
qenv,
|
327 | ! |
substitute( |
328 | ! |
expr = { |
329 | ! |
variables <- vars |
330 | ! |
dataframe_selected <- if (if_distinct) { |
331 | ! |
dplyr::count(dataname, dplyr::across(dplyr::all_of(variables))) |
332 |
} else { |
|
333 | ! |
dataname[variables] |
334 |
}
|
|
335 | ! |
dt_args <- args |
336 | ! |
dt_args$options <- dt_options |
337 | ! |
if (!is.null(dt_rows)) { |
338 | ! |
dt_args$options$pageLength <- dt_rows |
339 |
}
|
|
340 | ! |
dt_args$data <- dataframe_selected |
341 | ! |
table <- do.call(DT::datatable, dt_args) |
342 |
},
|
|
343 | ! |
env = list( |
344 | ! |
dataname = as.name(dataname), |
345 | ! |
if_distinct = if_distinct(), |
346 | ! |
vars = input$variables, |
347 | ! |
args = dt_args, |
348 | ! |
dt_options = dt_options, |
349 | ! |
dt_rows = input$dt_rows |
350 |
)
|
|
351 |
)
|
|
352 |
)
|
|
353 |
}) |
|
354 | ||
355 | ! |
output$data_table <- DT::renderDataTable(server = server_rendering, { |
356 | ! |
teal::validate_inputs(iv) |
357 | ! |
req(data_table_data())[["table"]] |
358 |
}) |
|
359 |
}) |
|
360 |
}
|
1 |
#' Shared parameters documentation
|
|
2 |
#'
|
|
3 |
#' Defines common arguments shared across multiple functions in the package
|
|
4 |
#' to avoid repetition by using `inheritParams`.
|
|
5 |
#'
|
|
6 |
#' @param plot_height (`numeric`) optional, specifies the plot height as a three-element vector of
|
|
7 |
#' `value`, `min`, and `max` intended for use with a slider UI element.
|
|
8 |
#' @param plot_width (`numeric`) optional, specifies the plot width as a three-element vector of
|
|
9 |
#' `value`, `min`, and `max` for a slider encoding the plot width.
|
|
10 |
#' @param rotate_xaxis_labels (`logical`) optional, whether to rotate plot X axis labels. Does not
|
|
11 |
#' rotate by default (`FALSE`).
|
|
12 |
#' @param ggtheme (`character`) optional, `ggplot2` theme to be used by default. Defaults to `"gray"`.
|
|
13 |
#' @param ggplot2_args (`ggplot2_args`) object created by [teal.widgets::ggplot2_args()]
|
|
14 |
#' with settings for the module plot.
|
|
15 |
#' The argument is merged with options variable `teal.ggplot2_args` and default module setup.
|
|
16 |
#'
|
|
17 |
#' For more details see the vignette: `vignette("custom-ggplot2-arguments", package = "teal.widgets")`
|
|
18 |
#' @param basic_table_args (`basic_table_args`) object created by [teal.widgets::basic_table_args()]
|
|
19 |
#' with settings for the module table.
|
|
20 |
#' The argument is merged with options variable `teal.basic_table_args` and default module setup.
|
|
21 |
#'
|
|
22 |
#' For more details see the vignette: `vignette("custom-basic-table-arguments", package = "teal.widgets")`
|
|
23 |
#' @param pre_output (`shiny.tag`) optional, text or UI element to be displayed before the module's output,
|
|
24 |
#' providing context or a title.
|
|
25 |
#' with text placed before the output to put the output into context. For example a title.
|
|
26 |
#' @param post_output (`shiny.tag`) optional, text or UI element to be displayed after the module's output,
|
|
27 |
#' adding context or further instructions. Elements like `shiny::helpText()` are useful.
|
|
28 |
#' @param alpha (`integer(1)` or `integer(3)`) optional, specifies point opacity.
|
|
29 |
#' - When the length of `alpha` is one: the plot points will have a fixed opacity.
|
|
30 |
#' - When the length of `alpha` is three: the plot points opacity are dynamically adjusted based on
|
|
31 |
#' vector of `value`, `min`, and `max`.
|
|
32 |
#' @param size (`integer(1)` or `integer(3)`) optional, specifies point size.
|
|
33 |
#' - When the length of `size` is one: the plot point sizes will have a fixed size.
|
|
34 |
#' - When the length of `size` is three: the plot points size are dynamically adjusted based on
|
|
35 |
#' vector of `value`, `min`, and `max`.
|
|
36 |
#' @param decorators `r lifecycle::badge("experimental")`
|
|
37 |
#' (named `list` of lists of `teal_transform_module`) optional,
|
|
38 |
#' decorator for tables or plots included in the module output reported.
|
|
39 |
#' The decorators are applied to the respective output objects.
|
|
40 |
#'
|
|
41 |
#' See section "Decorating Module" below for more details.
|
|
42 |
#'
|
|
43 |
#' @return Object of class `teal_module` to be used in `teal` applications.
|
|
44 |
#'
|
|
45 |
#' @name shared_params
|
|
46 |
#' @keywords internal
|
|
47 |
NULL
|
|
48 | ||
49 |
#' Add labels for facets to a `ggplot2` object
|
|
50 |
#'
|
|
51 |
#' Enhances a `ggplot2` plot by adding labels that describe
|
|
52 |
#' the faceting variables along the x and y axes.
|
|
53 |
#'
|
|
54 |
#' @param p (`ggplot2`) object to which facet labels will be added.
|
|
55 |
#' @param xfacet_label (`character`) Label for the facet along the x-axis.
|
|
56 |
#' If `NULL`, no label is added. If a vector, labels are joined with " & ".
|
|
57 |
#' @param yfacet_label (`character`) Label for the facet along the y-axis.
|
|
58 |
#' Similar behavior to `xfacet_label`.
|
|
59 |
#'
|
|
60 |
#' @return Returns `grid` or `grob` object (to be drawn with `grid.draw`)
|
|
61 |
#'
|
|
62 |
#' @examples
|
|
63 |
#' library(ggplot2)
|
|
64 |
#' library(grid)
|
|
65 |
#'
|
|
66 |
#' p <- ggplot(mtcars) +
|
|
67 |
#' aes(x = mpg, y = disp) +
|
|
68 |
#' geom_point() +
|
|
69 |
#' facet_grid(gear ~ cyl)
|
|
70 |
#'
|
|
71 |
#' xfacet_label <- "cylinders"
|
|
72 |
#' yfacet_label <- "gear"
|
|
73 |
#' res <- add_facet_labels(p, xfacet_label, yfacet_label)
|
|
74 |
#' grid.newpage()
|
|
75 |
#' grid.draw(res)
|
|
76 |
#'
|
|
77 |
#' grid.newpage()
|
|
78 |
#' grid.draw(add_facet_labels(p, xfacet_label = NULL, yfacet_label))
|
|
79 |
#' grid.newpage()
|
|
80 |
#' grid.draw(add_facet_labels(p, xfacet_label, yfacet_label = NULL))
|
|
81 |
#' grid.newpage()
|
|
82 |
#' grid.draw(add_facet_labels(p, xfacet_label = NULL, yfacet_label = NULL))
|
|
83 |
#'
|
|
84 |
#' @export
|
|
85 |
#'
|
|
86 |
add_facet_labels <- function(p, xfacet_label = NULL, yfacet_label = NULL) { |
|
87 | ! |
checkmate::assert_class(p, classes = "ggplot") |
88 | ! |
checkmate::assert_character(xfacet_label, null.ok = TRUE, min.len = 1) |
89 | ! |
checkmate::assert_character(yfacet_label, null.ok = TRUE, min.len = 1) |
90 | ! |
if (is.null(xfacet_label) && is.null(yfacet_label)) { |
91 | ! |
return(ggplot2::ggplotGrob(p)) |
92 |
}
|
|
93 | ! |
grid::grid.grabExpr({ |
94 | ! |
g <- ggplot2::ggplotGrob(p) |
95 | ||
96 |
# we are going to replace these, so we make sure they have nothing in them
|
|
97 | ! |
checkmate::assert_class(g$grobs[[grep("xlab-t", g$layout$name, fixed = TRUE)]], "zeroGrob") |
98 | ! |
checkmate::assert_class(g$grobs[[grep("ylab-r", g$layout$name, fixed = TRUE)]], "zeroGrob") |
99 | ||
100 | ! |
xaxis_label_grob <- g$grobs[[grep("xlab-b", g$layout$name, fixed = TRUE)]] |
101 | ! |
xaxis_label_grob$children[[1]]$label <- paste(xfacet_label, collapse = " & ") |
102 | ! |
yaxis_label_grob <- g$grobs[[grep("ylab-l", g$layout$name, fixed = TRUE)]] |
103 | ! |
yaxis_label_grob$children[[1]]$label <- paste(yfacet_label, collapse = " & ") |
104 | ! |
yaxis_label_grob$children[[1]]$rot <- 270 |
105 | ||
106 | ! |
top_height <- if (is.null(xfacet_label)) 0 else grid::unit(2, "line") |
107 | ! |
right_width <- if (is.null(yfacet_label)) 0 else grid::unit(2, "line") |
108 | ||
109 | ! |
grid::grid.newpage() |
110 | ! |
grid::pushViewport(grid::plotViewport(margins = c(0, 0, top_height, right_width), name = "ggplot")) |
111 | ! |
grid::grid.draw(g) |
112 | ! |
grid::upViewport(1) |
113 | ||
114 |
# draw x facet
|
|
115 | ! |
if (!is.null(xfacet_label)) { |
116 | ! |
grid::pushViewport(grid::viewport( |
117 | ! |
x = 0, y = grid::unit(1, "npc") - top_height, width = grid::unit(1, "npc"), |
118 | ! |
height = top_height, just = c("left", "bottom"), name = "topxaxis" |
119 |
)) |
|
120 | ! |
grid::grid.draw(xaxis_label_grob) |
121 | ! |
grid::upViewport(1) |
122 |
}
|
|
123 | ||
124 |
# draw y facet
|
|
125 | ! |
if (!is.null(yfacet_label)) { |
126 | ! |
grid::pushViewport(grid::viewport( |
127 | ! |
x = grid::unit(1, "npc") - grid::unit(as.numeric(right_width) / 2, "line"), y = 0, width = right_width, |
128 | ! |
height = grid::unit(1, "npc"), just = c("left", "bottom"), name = "rightyaxis" |
129 |
)) |
|
130 | ! |
grid::grid.draw(yaxis_label_grob) |
131 | ! |
grid::upViewport(1) |
132 |
}
|
|
133 |
}) |
|
134 |
}
|
|
135 | ||
136 |
#' Call a function with a character vector for the `...` argument
|
|
137 |
#'
|
|
138 |
#' @param fun (`character`) Name of a function where the `...` argument shall be replaced by values from `str_args`.
|
|
139 |
#' @param str_args (`character`) A character vector that the function shall be executed with
|
|
140 |
#'
|
|
141 |
#' @return
|
|
142 |
#' Value of call to `fun` with arguments specified in `str_args`.
|
|
143 |
#'
|
|
144 |
#' @keywords internal
|
|
145 |
call_fun_dots <- function(fun, str_args) { |
|
146 | ! |
do.call("call", c(list(fun), lapply(str_args, as.name)), quote = TRUE) |
147 |
}
|
|
148 | ||
149 |
#' Generate a string for a variable including its label
|
|
150 |
#'
|
|
151 |
#' @param var_names (`character`) Name of variable to extract labels from.
|
|
152 |
#' @param dataset (`dataset`) Name of analysis dataset.
|
|
153 |
#' @param prefix,suffix (`character`) String to paste to the beginning/end of the variable name with label.
|
|
154 |
#' @param wrap_width (`numeric`) Number of characters to wrap original label to. Defaults to 80.
|
|
155 |
#'
|
|
156 |
#' @return (`character`) String with variable name and label.
|
|
157 |
#'
|
|
158 |
#' @keywords internal
|
|
159 |
#'
|
|
160 |
varname_w_label <- function(var_names, |
|
161 |
dataset,
|
|
162 |
wrap_width = 80, |
|
163 |
prefix = NULL, |
|
164 |
suffix = NULL) { |
|
165 | ! |
add_label <- function(var_names) { |
166 | ! |
label <- vapply( |
167 | ! |
dataset[var_names], function(x) { |
168 | ! |
attr_label <- attr(x, "label") |
169 | ! |
`if`(is.null(attr_label), "", attr_label) |
170 |
},
|
|
171 | ! |
character(1) |
172 |
)
|
|
173 | ||
174 | ! |
if (length(label) == 1 && !is.na(label) && !identical(label, "")) { |
175 | ! |
paste0(prefix, label, " [", var_names, "]", suffix) |
176 |
} else { |
|
177 | ! |
var_names
|
178 |
}
|
|
179 |
}
|
|
180 | ||
181 | ! |
if (length(var_names) < 1) { |
182 | ! |
NULL
|
183 | ! |
} else if (length(var_names) == 1) { |
184 | ! |
stringr::str_wrap(add_label(var_names), width = wrap_width) |
185 | ! |
} else if (length(var_names) > 1) { |
186 | ! |
stringr::str_wrap(vapply(var_names, add_label, character(1)), width = wrap_width) |
187 |
}
|
|
188 |
}
|
|
189 | ||
190 |
# see vignette("ggplot2-specs", package="ggplot2")
|
|
191 |
shape_names <- c( |
|
192 |
"circle", paste("circle", c("open", "filled", "cross", "plus", "small")), "bullet", |
|
193 |
"square", paste("square", c("open", "filled", "cross", "plus", "triangle")), |
|
194 |
"diamond", paste("diamond", c("open", "filled", "plus")), |
|
195 |
"triangle", paste("triangle", c("open", "filled", "square")), |
|
196 |
paste("triangle down", c("open", "filled")), |
|
197 |
"plus", "cross", "asterisk" |
|
198 |
)
|
|
199 | ||
200 |
#' Get icons to represent variable types in dataset
|
|
201 |
#'
|
|
202 |
#' @param var_type (`character`) of R internal types (classes).
|
|
203 |
#' @return (`character`) vector of HTML icons corresponding to data type in each column.
|
|
204 |
#' @keywords internal
|
|
205 |
variable_type_icons <- function(var_type) { |
|
206 | ! |
checkmate::assert_character(var_type, any.missing = FALSE) |
207 | ||
208 | ! |
class_to_icon <- list( |
209 | ! |
numeric = "arrow-up-1-9", |
210 | ! |
integer = "arrow-up-1-9", |
211 | ! |
logical = "pause", |
212 | ! |
Date = "calendar", |
213 | ! |
POSIXct = "calendar", |
214 | ! |
POSIXlt = "calendar", |
215 | ! |
factor = "chart-bar", |
216 | ! |
character = "keyboard", |
217 | ! |
primary_key = "key", |
218 | ! |
unknown = "circle-question" |
219 |
)
|
|
220 | ! |
class_to_icon <- lapply(class_to_icon, function(icon_name) toString(icon(icon_name, lib = "font-awesome"))) |
221 | ||
222 | ! |
unname(vapply( |
223 | ! |
var_type,
|
224 | ! |
FUN.VALUE = character(1), |
225 | ! |
FUN = function(class) { |
226 | ! |
if (class == "") { |
227 | ! |
class
|
228 | ! |
} else if (is.null(class_to_icon[[class]])) { |
229 | ! |
class_to_icon[["unknown"]] |
230 |
} else { |
|
231 | ! |
class_to_icon[[class]] |
232 |
}
|
|
233 |
}
|
|
234 |
)) |
|
235 |
}
|
|
236 | ||
237 |
#' Include `CSS` files from `/inst/css/` package directory to application header
|
|
238 |
#'
|
|
239 |
#' `system.file` should not be used to access files in other packages, it does
|
|
240 |
#' not work with `devtools`. Therefore, we redefine this method in each package
|
|
241 |
#' as needed. Thus, we do not export this method
|
|
242 |
#'
|
|
243 |
#' @param pattern (`character`) optional, regular expression to match the file names to be included.
|
|
244 |
#'
|
|
245 |
#' @return HTML code that includes `CSS` files.
|
|
246 |
#' @keywords internal
|
|
247 |
#'
|
|
248 |
include_css_files <- function(pattern = "*") { |
|
249 | ! |
css_files <- list.files( |
250 | ! |
system.file("css", package = "teal.modules.general", mustWork = TRUE), |
251 | ! |
pattern = pattern, full.names = TRUE |
252 |
)
|
|
253 | ! |
if (length(css_files) == 0) { |
254 | ! |
return(NULL) |
255 |
}
|
|
256 | ! |
singleton(tags$head(lapply(css_files, includeCSS))) |
257 |
}
|
|
258 | ||
259 |
#' JavaScript condition to check if a specific tab is active
|
|
260 |
#'
|
|
261 |
#' @param id (`character(1)`) the id of the tab panel with tabs.
|
|
262 |
#' @param name (`character(1)`) the name of the tab.
|
|
263 |
#' @return JavaScript expression to be used in `shiny::conditionalPanel()` to determine
|
|
264 |
#' if the specified tab is active.
|
|
265 |
#' @keywords internal
|
|
266 |
#'
|
|
267 |
is_tab_active_js <- function(id, name) { |
|
268 |
# supporting the bs3 and higher version at the same time
|
|
269 | ! |
sprintf( |
270 | ! |
"$(\"#%1$s > li.active\").text().trim() == '%2$s' || $(\"#%1$s > li a.active\").text().trim() == '%2$s'",
|
271 | ! |
id, name |
272 |
)
|
|
273 |
}
|
|
274 | ||
275 |
#' Assert single selection on `data_extract_spec` object
|
|
276 |
#' Helper to reduce code in assertions
|
|
277 |
#' @noRd
|
|
278 |
#'
|
|
279 |
assert_single_selection <- function(x, |
|
280 |
.var.name = checkmate::vname(x)) { # nolint: object_name. |
|
281 | 104x |
if (any(vapply(x, function(.x) .x$select$multiple, logical(1)))) { |
282 | 4x |
stop("'", .var.name, "' should not allow multiple selection") |
283 |
}
|
|
284 | 100x |
invisible(TRUE) |
285 |
}
|
|
286 | ||
287 |
#' Wrappers around `srv_transform_teal_data` that allows to decorate the data
|
|
288 |
#' @inheritParams teal::srv_transform_teal_data
|
|
289 |
#' @param expr (`expression` or `reactive`) to evaluate on the output of the decoration.
|
|
290 |
#' When an expression it must be inline code. See [within()]
|
|
291 |
#' Default is `NULL` which won't evaluate any appending code.
|
|
292 |
#' @param expr_is_reactive (`logical(1)`) whether `expr` is a reactive expression
|
|
293 |
#' that skips defusing the argument.
|
|
294 |
#' @details
|
|
295 |
#' `srv_decorate_teal_data` is a wrapper around `srv_transform_teal_data` that
|
|
296 |
#' allows to decorate the data with additional expressions.
|
|
297 |
#' When original `teal_data` object is in error state, it will show that error
|
|
298 |
#' first.
|
|
299 |
#'
|
|
300 |
#' @keywords internal
|
|
301 |
srv_decorate_teal_data <- function(id, data, decorators, expr, expr_is_reactive = FALSE) { |
|
302 | ! |
checkmate::assert_class(data, classes = "reactive") |
303 | ! |
checkmate::assert_list(decorators, "teal_transform_module") |
304 | ! |
checkmate::assert_flag(expr_is_reactive) |
305 | ||
306 | ! |
missing_expr <- missing(expr) |
307 | ! |
if (!missing_expr && !expr_is_reactive) { |
308 | ! |
expr <- dplyr::enexpr(expr) # Using dplyr re-export to avoid adding rlang to Imports |
309 |
}
|
|
310 | ||
311 | ! |
moduleServer(id, function(input, output, session) { |
312 | ! |
decorated_output <- srv_transform_teal_data("inner", data = data, transformators = decorators) |
313 | ||
314 | ! |
reactive({ |
315 | ! |
data_out <- try(data(), silent = TRUE) |
316 | ! |
if (inherits(data_out, "qenv.error")) { |
317 | ! |
data() |
318 |
} else { |
|
319 |
# ensure original errors are displayed and `eval_code` is never executed with NULL
|
|
320 | ! |
req(data(), decorated_output()) |
321 | ! |
if (missing_expr) { |
322 | ! |
decorated_output() |
323 | ! |
} else if (expr_is_reactive) { |
324 | ! |
teal.code::eval_code(decorated_output(), expr()) |
325 |
} else { |
|
326 | ! |
teal.code::eval_code(decorated_output(), expr) |
327 |
}
|
|
328 |
}
|
|
329 |
}) |
|
330 |
}) |
|
331 |
}
|
|
332 | ||
333 |
#' @rdname srv_decorate_teal_data
|
|
334 |
#' @details
|
|
335 |
#' `ui_decorate_teal_data` is a wrapper around `ui_transform_teal_data`.
|
|
336 |
#' @keywords internal
|
|
337 |
ui_decorate_teal_data <- function(id, decorators, ...) { |
|
338 | ! |
teal::ui_transform_teal_data(NS(id, "inner"), transformators = decorators, ...) |
339 |
}
|
|
340 | ||
341 |
#' Internal function to check if decorators is a valid object
|
|
342 |
#' @noRd
|
|
343 |
check_decorators <- function(x, names = NULL) { # nolint: object_name. |
|
344 | ||
345 | 5x |
check_message <- checkmate::check_list(x, names = "named") |
346 | ||
347 | 5x |
if (!is.null(names)) { |
348 | 5x |
if (isTRUE(check_message)) { |
349 | 5x |
if (length(names(x)) != length(unique(names(x)))) { |
350 | ! |
check_message <- sprintf( |
351 | ! |
"The `decorators` must contain unique names from these names: %s.",
|
352 | ! |
paste(names, collapse = ", ") |
353 |
)
|
|
354 |
}
|
|
355 |
} else { |
|
356 | ! |
check_message <- sprintf( |
357 | ! |
"The `decorators` must be a named list from these names: %s.",
|
358 | ! |
paste(names, collapse = ", ") |
359 |
)
|
|
360 |
}
|
|
361 |
}
|
|
362 | ||
363 | 5x |
if (!isTRUE(check_message)) { |
364 | ! |
return(check_message) |
365 |
}
|
|
366 | ||
367 | 5x |
valid_elements <- vapply( |
368 | 5x |
x,
|
369 | 5x |
checkmate::test_class, |
370 | 5x |
classes = "teal_transform_module", |
371 | 5x |
FUN.VALUE = logical(1L) |
372 |
)
|
|
373 | ||
374 | 5x |
if (all(valid_elements)) { |
375 | 5x |
return(TRUE) |
376 |
}
|
|
377 | ||
378 | ! |
"Make sure that the named list contains 'teal_transform_module' objects created using `teal_transform_module()`."
|
379 |
}
|
|
380 |
#' Internal assertion on decorators
|
|
381 |
#' @noRd
|
|
382 |
assert_decorators <- checkmate::makeAssertionFunction(check_decorators) |
|
383 | ||
384 |
#' Subset decorators based on the scope
|
|
385 |
#'
|
|
386 |
#' @param scope (`character`) a character vector of decorator names to include.
|
|
387 |
#' @param decorators (named `list`) of list decorators to subset.
|
|
388 |
#'
|
|
389 |
#' @return Subsetted list with all decorators to include.
|
|
390 |
#' It can be an empty list if none of the scope exists in `decorators` argument.
|
|
391 |
#' @keywords internal
|
|
392 |
select_decorators <- function(decorators, scope) { |
|
393 | ! |
checkmate::assert_character(scope, null.ok = TRUE) |
394 | ! |
if (scope %in% names(decorators)) { |
395 | ! |
decorators[scope] |
396 |
} else { |
|
397 | ! |
list() |
398 |
}
|
|
399 |
}
|
1 |
#' `teal` module: Cross-table
|
|
2 |
#'
|
|
3 |
#' Generates a simple cross-table of two variables from a dataset with custom
|
|
4 |
#' options for showing percentages and sub-totals.
|
|
5 |
#'
|
|
6 |
#' @inheritParams teal::module
|
|
7 |
#' @inheritParams shared_params
|
|
8 |
#' @param x (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
9 |
#' Object with all available choices with pre-selected option for variable X - row values.
|
|
10 |
#' In case of `data_extract_spec` use `select_spec(..., ordered = TRUE)` if table elements should be
|
|
11 |
#' rendered according to selection order.
|
|
12 |
#' @param y (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
13 |
#' Object with all available choices with pre-selected option for variable Y - column values.
|
|
14 |
#'
|
|
15 |
#' `data_extract_spec` must not allow multiple selection in this case.
|
|
16 |
#' @param show_percentage (`logical(1)`)
|
|
17 |
#' Indicates whether to show percentages (relevant only when `x` is a `factor`).
|
|
18 |
#' Defaults to `TRUE`.
|
|
19 |
#' @param show_total (`logical(1)`)
|
|
20 |
#' Indicates whether to show total column.
|
|
21 |
#' Defaults to `TRUE`.
|
|
22 |
#'
|
|
23 |
#' @note For more examples, please see the vignette "Using cross table" via
|
|
24 |
#' `vignette("using-cross-table", package = "teal.modules.general")`.
|
|
25 |
#'
|
|
26 |
#' @inherit shared_params return
|
|
27 |
#'
|
|
28 |
#' @section Decorating Module:
|
|
29 |
#'
|
|
30 |
#' This module generates the following objects, which can be modified in place using decorators:
|
|
31 |
#' - `table` (`ElementaryTable` - output of `rtables::build_table`)
|
|
32 |
#'
|
|
33 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
34 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
35 |
#' See code snippet below:
|
|
36 |
#'
|
|
37 |
#' ```
|
|
38 |
#' tm_t_crosstable(
|
|
39 |
#' ..., # arguments for module
|
|
40 |
#' decorators = list(
|
|
41 |
#' table = teal_transform_module(...) # applied to the `table` output
|
|
42 |
#' )
|
|
43 |
#' )
|
|
44 |
#' ```
|
|
45 |
#' For additional details and examples of decorators, refer to the vignette
|
|
46 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
47 |
#'
|
|
48 |
#' To learn more please refer to the vignette
|
|
49 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
50 |
#'
|
|
51 |
#' @examplesShinylive
|
|
52 |
#' library(teal.modules.general)
|
|
53 |
#' interactive <- function() TRUE
|
|
54 |
#' {{ next_example }}
|
|
55 |
#' @examples
|
|
56 |
#' # general data example
|
|
57 |
#' data <- teal_data()
|
|
58 |
#' data <- within(data, {
|
|
59 |
#' mtcars <- mtcars
|
|
60 |
#' for (v in c("cyl", "vs", "am", "gear")) {
|
|
61 |
#' mtcars[[v]] <- as.factor(mtcars[[v]])
|
|
62 |
#' }
|
|
63 |
#' mtcars[["primary_key"]] <- seq_len(nrow(mtcars))
|
|
64 |
#' })
|
|
65 |
#' join_keys(data) <- join_keys(join_key("mtcars", "mtcars", "primary_key"))
|
|
66 |
#'
|
|
67 |
#' app <- init(
|
|
68 |
#' data = data,
|
|
69 |
#' modules = modules(
|
|
70 |
#' tm_t_crosstable(
|
|
71 |
#' label = "Cross Table",
|
|
72 |
#' x = data_extract_spec(
|
|
73 |
#' dataname = "mtcars",
|
|
74 |
#' select = select_spec(
|
|
75 |
#' label = "Select variable:",
|
|
76 |
#' choices = variable_choices(data[["mtcars"]], c("cyl", "vs", "am", "gear")),
|
|
77 |
#' selected = c("cyl", "gear"),
|
|
78 |
#' multiple = TRUE,
|
|
79 |
#' ordered = TRUE,
|
|
80 |
#' fixed = FALSE
|
|
81 |
#' )
|
|
82 |
#' ),
|
|
83 |
#' y = data_extract_spec(
|
|
84 |
#' dataname = "mtcars",
|
|
85 |
#' select = select_spec(
|
|
86 |
#' label = "Select variable:",
|
|
87 |
#' choices = variable_choices(data[["mtcars"]], c("cyl", "vs", "am", "gear")),
|
|
88 |
#' selected = "vs",
|
|
89 |
#' multiple = FALSE,
|
|
90 |
#' fixed = FALSE
|
|
91 |
#' )
|
|
92 |
#' )
|
|
93 |
#' )
|
|
94 |
#' )
|
|
95 |
#' )
|
|
96 |
#' if (interactive()) {
|
|
97 |
#' shinyApp(app$ui, app$server)
|
|
98 |
#' }
|
|
99 |
#'
|
|
100 |
#' @examplesShinylive
|
|
101 |
#' library(teal.modules.general)
|
|
102 |
#' interactive <- function() TRUE
|
|
103 |
#' {{ next_example }}
|
|
104 |
#' @examples
|
|
105 |
#' # CDISC data example
|
|
106 |
#' data <- teal_data()
|
|
107 |
#' data <- within(data, {
|
|
108 |
#' ADSL <- teal.data::rADSL
|
|
109 |
#' })
|
|
110 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
111 |
#'
|
|
112 |
#' app <- init(
|
|
113 |
#' data = data,
|
|
114 |
#' modules = modules(
|
|
115 |
#' tm_t_crosstable(
|
|
116 |
#' label = "Cross Table",
|
|
117 |
#' x = data_extract_spec(
|
|
118 |
#' dataname = "ADSL",
|
|
119 |
#' select = select_spec(
|
|
120 |
#' label = "Select variable:",
|
|
121 |
#' choices = variable_choices(data[["ADSL"]], subset = function(data) {
|
|
122 |
#' idx <- !vapply(data, inherits, logical(1), c("Date", "POSIXct", "POSIXlt"))
|
|
123 |
#' return(names(data)[idx])
|
|
124 |
#' }),
|
|
125 |
#' selected = "COUNTRY",
|
|
126 |
#' multiple = TRUE,
|
|
127 |
#' ordered = TRUE,
|
|
128 |
#' fixed = FALSE
|
|
129 |
#' )
|
|
130 |
#' ),
|
|
131 |
#' y = data_extract_spec(
|
|
132 |
#' dataname = "ADSL",
|
|
133 |
#' select = select_spec(
|
|
134 |
#' label = "Select variable:",
|
|
135 |
#' choices = variable_choices(data[["ADSL"]], subset = function(data) {
|
|
136 |
#' idx <- vapply(data, is.factor, logical(1))
|
|
137 |
#' return(names(data)[idx])
|
|
138 |
#' }),
|
|
139 |
#' selected = "SEX",
|
|
140 |
#' multiple = FALSE,
|
|
141 |
#' fixed = FALSE
|
|
142 |
#' )
|
|
143 |
#' )
|
|
144 |
#' )
|
|
145 |
#' )
|
|
146 |
#' )
|
|
147 |
#' if (interactive()) {
|
|
148 |
#' shinyApp(app$ui, app$server)
|
|
149 |
#' }
|
|
150 |
#'
|
|
151 |
#' @export
|
|
152 |
#'
|
|
153 |
tm_t_crosstable <- function(label = "Cross Table", |
|
154 |
x,
|
|
155 |
y,
|
|
156 |
show_percentage = TRUE, |
|
157 |
show_total = TRUE, |
|
158 |
pre_output = NULL, |
|
159 |
post_output = NULL, |
|
160 |
basic_table_args = teal.widgets::basic_table_args(), |
|
161 |
transformators = list(), |
|
162 |
decorators = list()) { |
|
163 | ! |
message("Initializing tm_t_crosstable") |
164 | ||
165 |
# Normalize the parameters
|
|
166 | ! |
if (inherits(x, "data_extract_spec")) x <- list(x) |
167 | ! |
if (inherits(y, "data_extract_spec")) y <- list(y) |
168 | ||
169 |
# Start of assertions
|
|
170 | ! |
checkmate::assert_string(label) |
171 | ! |
checkmate::assert_list(x, types = "data_extract_spec") |
172 | ||
173 | ! |
checkmate::assert_list(y, types = "data_extract_spec") |
174 | ! |
assert_single_selection(y) |
175 | ||
176 | ! |
checkmate::assert_flag(show_percentage) |
177 | ! |
checkmate::assert_flag(show_total) |
178 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
179 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
180 | ! |
checkmate::assert_class(basic_table_args, classes = "basic_table_args") |
181 | ||
182 | ! |
assert_decorators(decorators, "table") |
183 |
# End of assertions
|
|
184 | ||
185 |
# Make UI args
|
|
186 | ! |
ui_args <- as.list(environment()) |
187 | ||
188 | ! |
server_args <- list( |
189 | ! |
label = label, |
190 | ! |
x = x, |
191 | ! |
y = y, |
192 | ! |
basic_table_args = basic_table_args, |
193 | ! |
decorators = decorators |
194 |
)
|
|
195 | ||
196 | ! |
ans <- module( |
197 | ! |
label = label, |
198 | ! |
server = srv_t_crosstable, |
199 | ! |
ui = ui_t_crosstable, |
200 | ! |
ui_args = ui_args, |
201 | ! |
server_args = server_args, |
202 | ! |
transformators = transformators, |
203 | ! |
datanames = teal.transform::get_extract_datanames(list(x = x, y = y)) |
204 |
)
|
|
205 | ! |
attr(ans, "teal_bookmarkable") <- TRUE |
206 | ! |
ans
|
207 |
}
|
|
208 | ||
209 |
# UI function for the cross-table module
|
|
210 |
ui_t_crosstable <- function(id, x, y, show_percentage, show_total, pre_output, post_output, ...) { |
|
211 | ! |
args <- list(...) |
212 | ! |
ns <- NS(id) |
213 | ! |
is_single_dataset <- teal.transform::is_single_dataset(x, y) |
214 | ||
215 | ! |
join_default_options <- c( |
216 | ! |
"Full Join" = "dplyr::full_join", |
217 | ! |
"Inner Join" = "dplyr::inner_join", |
218 | ! |
"Left Join" = "dplyr::left_join", |
219 | ! |
"Right Join" = "dplyr::right_join" |
220 |
)
|
|
221 | ||
222 | ! |
teal.widgets::standard_layout( |
223 | ! |
output = teal.widgets::white_small_well( |
224 | ! |
textOutput(ns("title")), |
225 | ! |
teal.widgets::table_with_settings_ui(ns("table")) |
226 |
),
|
|
227 | ! |
encoding = tags$div( |
228 |
### Reporter
|
|
229 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
230 |
###
|
|
231 | ! |
tags$label("Encodings", class = "text-primary"), |
232 | ! |
teal.transform::datanames_input(list(x, y)), |
233 | ! |
teal.transform::data_extract_ui(ns("x"), label = "Row values", x, is_single_dataset = is_single_dataset), |
234 | ! |
teal.transform::data_extract_ui(ns("y"), label = "Column values", y, is_single_dataset = is_single_dataset), |
235 | ! |
teal.widgets::optionalSelectInput( |
236 | ! |
ns("join_fun"), |
237 | ! |
label = "Row to Column type of join", |
238 | ! |
choices = join_default_options, |
239 | ! |
selected = join_default_options[1], |
240 | ! |
multiple = FALSE |
241 |
),
|
|
242 | ! |
tags$hr(), |
243 | ! |
teal.widgets::panel_group( |
244 | ! |
teal.widgets::panel_item( |
245 | ! |
title = "Table settings", |
246 | ! |
checkboxInput(ns("show_percentage"), "Show column percentage", value = show_percentage), |
247 | ! |
checkboxInput(ns("show_total"), "Show total column", value = show_total) |
248 |
)
|
|
249 |
),
|
|
250 | ! |
ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "table")) |
251 |
),
|
|
252 | ! |
forms = tagList( |
253 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
254 |
),
|
|
255 | ! |
pre_output = pre_output, |
256 | ! |
post_output = post_output |
257 |
)
|
|
258 |
}
|
|
259 | ||
260 |
# Server function for the cross-table module
|
|
261 |
srv_t_crosstable <- function(id, data, reporter, filter_panel_api, label, x, y, basic_table_args, decorators) { |
|
262 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
263 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
264 | ! |
checkmate::assert_class(data, "reactive") |
265 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
266 | ! |
moduleServer(id, function(input, output, session) { |
267 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
268 | ||
269 | ! |
selector_list <- teal.transform::data_extract_multiple_srv( |
270 | ! |
data_extract = list(x = x, y = y), |
271 | ! |
datasets = data, |
272 | ! |
select_validation_rule = list( |
273 | ! |
x = shinyvalidate::sv_required("Please define column for row variable."), |
274 | ! |
y = shinyvalidate::sv_required("Please define column for column variable.") |
275 |
)
|
|
276 |
)
|
|
277 | ||
278 | ! |
iv_r <- reactive({ |
279 | ! |
iv <- shinyvalidate::InputValidator$new() |
280 | ! |
iv$add_rule("join_fun", function(value) { |
281 | ! |
if (!identical(selector_list()$x()$dataname, selector_list()$y()$dataname)) { |
282 | ! |
if (!shinyvalidate::input_provided(value)) { |
283 | ! |
"Please select a joining function."
|
284 |
}
|
|
285 |
}
|
|
286 |
}) |
|
287 | ! |
teal.transform::compose_and_enable_validators(iv, selector_list) |
288 |
}) |
|
289 | ||
290 | ! |
observeEvent( |
291 | ! |
eventExpr = { |
292 | ! |
req(!is.null(selector_list()$x()) && !is.null(selector_list()$y())) |
293 | ! |
list(selector_list()$x(), selector_list()$y()) |
294 |
},
|
|
295 | ! |
handlerExpr = { |
296 | ! |
if (identical(selector_list()$x()$dataname, selector_list()$y()$dataname)) { |
297 | ! |
shinyjs::hide("join_fun") |
298 |
} else { |
|
299 | ! |
shinyjs::show("join_fun") |
300 |
}
|
|
301 |
}
|
|
302 |
)
|
|
303 | ||
304 | ! |
merge_function <- reactive({ |
305 | ! |
if (is.null(input$join_fun)) { |
306 | ! |
"dplyr::full_join"
|
307 |
} else { |
|
308 | ! |
input$join_fun |
309 |
}
|
|
310 |
}) |
|
311 | ||
312 | ! |
anl_merged_input <- teal.transform::merge_expression_srv( |
313 | ! |
datasets = data, |
314 | ! |
selector_list = selector_list, |
315 | ! |
merge_function = merge_function |
316 |
)
|
|
317 | ! |
qenv <- teal.code::eval_code(data(), 'library("rtables");library("tern");library("dplyr")') # nolint quotes |
318 | ! |
anl_merged_q <- reactive({ |
319 | ! |
req(anl_merged_input()) |
320 | ! |
qenv %>% |
321 | ! |
teal.code::eval_code(as.expression(anl_merged_input()$expr)) |
322 |
}) |
|
323 | ||
324 | ! |
merged <- list( |
325 | ! |
anl_input_r = anl_merged_input, |
326 | ! |
anl_q_r = anl_merged_q |
327 |
)
|
|
328 | ||
329 | ! |
output_q <- reactive({ |
330 | ! |
teal::validate_inputs(iv_r()) |
331 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
332 | ||
333 |
# As this is a summary
|
|
334 | ! |
x_name <- as.vector(merged$anl_input_r()$columns_source$x) |
335 | ! |
y_name <- as.vector(merged$anl_input_r()$columns_source$y) |
336 | ||
337 | ! |
teal::validate_has_data(ANL, 3) |
338 | ! |
teal::validate_has_data(ANL[, c(x_name, y_name)], 3, complete = TRUE, allow_inf = FALSE) |
339 | ||
340 | ! |
is_allowed_class <- function(x) is.numeric(x) || is.factor(x) || is.character(x) || is.logical(x) |
341 | ! |
validate(need( |
342 | ! |
all(vapply(ANL[x_name], is_allowed_class, logical(1))), |
343 | ! |
"Selected row variable has an unsupported data type."
|
344 |
)) |
|
345 | ! |
validate(need( |
346 | ! |
is_allowed_class(ANL[[y_name]]), |
347 | ! |
"Selected column variable has an unsupported data type."
|
348 |
)) |
|
349 | ||
350 | ! |
show_percentage <- input$show_percentage |
351 | ! |
show_total <- input$show_total |
352 | ||
353 | ! |
plot_title <- paste( |
354 | ! |
"Cross-Table of",
|
355 | ! |
paste0(varname_w_label(x_name, ANL), collapse = ", "), |
356 | ! |
"(rows)", "vs.", |
357 | ! |
varname_w_label(y_name, ANL), |
358 | ! |
"(columns)"
|
359 |
)
|
|
360 | ||
361 | ! |
labels_vec <- vapply( |
362 | ! |
x_name,
|
363 | ! |
varname_w_label,
|
364 | ! |
character(1), |
365 | ! |
ANL
|
366 |
)
|
|
367 | ||
368 | ! |
teal.code::eval_code( |
369 | ! |
merged$anl_q_r(), |
370 | ! |
substitute( |
371 | ! |
expr = { |
372 | ! |
title <- plot_title |
373 |
},
|
|
374 | ! |
env = list(plot_title = plot_title) |
375 |
)
|
|
376 |
) %>% |
|
377 | ! |
teal.code::eval_code( |
378 | ! |
substitute( |
379 | ! |
expr = { |
380 | ! |
table <- basic_tables %>% |
381 | ! |
split_call %>% # styler: off |
382 | ! |
rtables::add_colcounts() %>% |
383 | ! |
tern::analyze_vars( |
384 | ! |
vars = x_name, |
385 | ! |
var_labels = labels_vec, |
386 | ! |
na.rm = FALSE, |
387 | ! |
denom = "N_col", |
388 | ! |
.stats = c("mean_sd", "median", "range", count_value) |
389 |
)
|
|
390 |
},
|
|
391 | ! |
env = list( |
392 | ! |
basic_tables = teal.widgets::parse_basic_table_args( |
393 | ! |
basic_table_args = teal.widgets::resolve_basic_table_args(basic_table_args) |
394 |
),
|
|
395 | ! |
split_call = if (show_total) { |
396 | ! |
substitute( |
397 | ! |
expr = rtables::split_cols_by( |
398 | ! |
y_name,
|
399 | ! |
split_fun = rtables::add_overall_level(label = "Total", first = FALSE) |
400 |
),
|
|
401 | ! |
env = list(y_name = y_name) |
402 |
)
|
|
403 |
} else { |
|
404 | ! |
substitute(rtables::split_cols_by(y_name), env = list(y_name = y_name)) |
405 |
},
|
|
406 | ! |
x_name = x_name, |
407 | ! |
labels_vec = labels_vec, |
408 | ! |
count_value = ifelse(show_percentage, "count_fraction", "count") |
409 |
)
|
|
410 |
)
|
|
411 |
) %>% |
|
412 | ! |
teal.code::eval_code( |
413 | ! |
substitute( |
414 | ! |
expr = { |
415 | ! |
ANL <- tern::df_explicit_na(ANL) |
416 | ! |
table <- rtables::build_table(lyt = table, df = ANL[order(ANL[[y_name]]), ]) |
417 |
},
|
|
418 | ! |
env = list(y_name = y_name) |
419 |
)
|
|
420 |
)
|
|
421 |
}) |
|
422 | ||
423 | ! |
decorated_output_q <- srv_decorate_teal_data( |
424 | ! |
id = "decorator", |
425 | ! |
data = output_q, |
426 | ! |
decorators = select_decorators(decorators, "table"), |
427 | ! |
expr = table |
428 |
)
|
|
429 | ||
430 | ! |
output$title <- renderText(req(decorated_output_q())[["title"]]) |
431 | ||
432 | ! |
table_r <- reactive({ |
433 | ! |
req(iv_r()$is_valid()) |
434 | ! |
req(decorated_output_q())[["table"]] |
435 |
}) |
|
436 | ||
437 | ! |
teal.widgets::table_with_settings_srv( |
438 | ! |
id = "table", |
439 | ! |
table_r = table_r |
440 |
)
|
|
441 | ||
442 |
# Render R code.
|
|
443 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_output_q()))) |
444 | ||
445 | ! |
teal.widgets::verbatim_popup_srv( |
446 | ! |
id = "rcode", |
447 | ! |
verbatim_content = source_code_r, |
448 | ! |
title = "Show R Code for Cross-Table" |
449 |
)
|
|
450 | ||
451 |
### REPORTER
|
|
452 | ! |
if (with_reporter) { |
453 | ! |
card_fun <- function(comment, label) { |
454 | ! |
card <- teal::report_card_template( |
455 | ! |
title = "Cross Table", |
456 | ! |
label = label, |
457 | ! |
with_filter = with_filter, |
458 | ! |
filter_panel_api = filter_panel_api |
459 |
)
|
|
460 | ! |
card$append_text("Table", "header3") |
461 | ! |
card$append_table(table_r()) |
462 | ! |
if (!comment == "") { |
463 | ! |
card$append_text("Comment", "header3") |
464 | ! |
card$append_text(comment) |
465 |
}
|
|
466 | ! |
card$append_src(source_code_r()) |
467 | ! |
card
|
468 |
}
|
|
469 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
470 |
}
|
|
471 |
###
|
|
472 |
}) |
|
473 |
}
|
1 |
#' `teal` module: Outliers analysis
|
|
2 |
#'
|
|
3 |
#' Module to analyze and identify outliers using different methods
|
|
4 |
#' such as IQR, Z-score, and Percentiles, and offers visualizations including
|
|
5 |
#' box plots, density plots, and cumulative distribution plots to help interpret the outliers.
|
|
6 |
#'
|
|
7 |
#' @inheritParams teal::module
|
|
8 |
#' @inheritParams shared_params
|
|
9 |
#'
|
|
10 |
#' @param outlier_var (`data_extract_spec` or `list` of multiple `data_extract_spec`)
|
|
11 |
#' Specifies variable(s) to be analyzed for outliers.
|
|
12 |
#' @param categorical_var (`data_extract_spec` or `list` of multiple `data_extract_spec`) optional,
|
|
13 |
#' specifies the categorical variable(s) to split the selected outlier variables on.
|
|
14 |
#' @param ggplot2_args `r roxygen_ggplot2_args_param("Boxplot", "Density Plot", "Cumulative Distribution Plot")`
|
|
15 |
#'
|
|
16 |
#' @inherit shared_params return
|
|
17 |
#'
|
|
18 |
#' @section Decorating Module:
|
|
19 |
#'
|
|
20 |
#' This module generates the following objects, which can be modified in place using decorators:
|
|
21 |
#' - `box_plot` (`ggplot`)
|
|
22 |
#' - `density_plot` (`ggplot`)
|
|
23 |
#' - `cumulative_plot` (`ggplot`)
|
|
24 |
#' - `table` (`datatables` created with [DT::datatable()])
|
|
25 |
#'
|
|
26 |
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
|
|
27 |
#' The name of this list corresponds to the name of the output to which the decorator is applied.
|
|
28 |
#' See code snippet below:
|
|
29 |
#'
|
|
30 |
#' ```
|
|
31 |
#' tm_outliers(
|
|
32 |
#' ..., # arguments for module
|
|
33 |
#' decorators = list(
|
|
34 |
#' box_plot = teal_transform_module(...), # applied only to `box_plot` output
|
|
35 |
#' density_plot = teal_transform_module(...), # applied only to `density_plot` output
|
|
36 |
#' cumulative_plot = teal_transform_module(...), # applied only to `cumulative_plot` output
|
|
37 |
#' table = teal_transform_module(...) # applied only to `table` output
|
|
38 |
#' )
|
|
39 |
#' )
|
|
40 |
#' ```
|
|
41 |
#'
|
|
42 |
#' For additional details and examples of decorators, refer to the vignette
|
|
43 |
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
|
|
44 |
#'
|
|
45 |
#' To learn more please refer to the vignette
|
|
46 |
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
|
|
47 |
#'
|
|
48 |
#' @examplesShinylive
|
|
49 |
#' library(teal.modules.general)
|
|
50 |
#' interactive <- function() TRUE
|
|
51 |
#' {{ next_example }}
|
|
52 |
#' @examples
|
|
53 |
#'
|
|
54 |
#' # general data example
|
|
55 |
#' data <- teal_data()
|
|
56 |
#' data <- within(data, {
|
|
57 |
#' CO2 <- CO2
|
|
58 |
#' CO2[["primary_key"]] <- seq_len(nrow(CO2))
|
|
59 |
#' })
|
|
60 |
#' join_keys(data) <- join_keys(join_key("CO2", "CO2", "primary_key"))
|
|
61 |
#'
|
|
62 |
#' vars <- choices_selected(variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")))
|
|
63 |
#'
|
|
64 |
#' app <- init(
|
|
65 |
#' data = data,
|
|
66 |
#' modules = modules(
|
|
67 |
#' tm_outliers(
|
|
68 |
#' outlier_var = list(
|
|
69 |
#' data_extract_spec(
|
|
70 |
#' dataname = "CO2",
|
|
71 |
#' select = select_spec(
|
|
72 |
#' label = "Select variable:",
|
|
73 |
#' choices = variable_choices(data[["CO2"]], c("conc", "uptake")),
|
|
74 |
#' selected = "uptake",
|
|
75 |
#' multiple = FALSE,
|
|
76 |
#' fixed = FALSE
|
|
77 |
#' )
|
|
78 |
#' )
|
|
79 |
#' ),
|
|
80 |
#' categorical_var = list(
|
|
81 |
#' data_extract_spec(
|
|
82 |
#' dataname = "CO2",
|
|
83 |
#' filter = filter_spec(
|
|
84 |
#' vars = vars,
|
|
85 |
#' choices = value_choices(data[["CO2"]], vars$selected),
|
|
86 |
#' selected = value_choices(data[["CO2"]], vars$selected),
|
|
87 |
#' multiple = TRUE
|
|
88 |
#' )
|
|
89 |
#' )
|
|
90 |
#' )
|
|
91 |
#' )
|
|
92 |
#' )
|
|
93 |
#' )
|
|
94 |
#' if (interactive()) {
|
|
95 |
#' shinyApp(app$ui, app$server)
|
|
96 |
#' }
|
|
97 |
#'
|
|
98 |
#' @examplesShinylive
|
|
99 |
#' library(teal.modules.general)
|
|
100 |
#' interactive <- function() TRUE
|
|
101 |
#' {{ next_example }}
|
|
102 |
#' @examples
|
|
103 |
#'
|
|
104 |
#' # CDISC data example
|
|
105 |
#' data <- teal_data()
|
|
106 |
#' data <- within(data, {
|
|
107 |
#' ADSL <- teal.data::rADSL
|
|
108 |
#' })
|
|
109 |
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
|
|
110 |
#'
|
|
111 |
#' fact_vars_adsl <- names(Filter(isTRUE, sapply(data[["ADSL"]], is.factor)))
|
|
112 |
#' vars <- choices_selected(variable_choices(data[["ADSL"]], fact_vars_adsl))
|
|
113 |
#'
|
|
114 |
#'
|
|
115 |
#'
|
|
116 |
#' app <- init(
|
|
117 |
#' data = data,
|
|
118 |
#' modules = modules(
|
|
119 |
#' tm_outliers(
|
|
120 |
#' outlier_var = list(
|
|
121 |
#' data_extract_spec(
|
|
122 |
#' dataname = "ADSL",
|
|
123 |
#' select = select_spec(
|
|
124 |
#' label = "Select variable:",
|
|
125 |
#' choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1")),
|
|
126 |
#' selected = "AGE",
|
|
127 |
#' multiple = FALSE,
|
|
128 |
#' fixed = FALSE
|
|
129 |
#' )
|
|
130 |
#' )
|
|
131 |
#' ),
|
|
132 |
#' categorical_var = list(
|
|
133 |
#' data_extract_spec(
|
|
134 |
#' dataname = "ADSL",
|
|
135 |
#' filter = filter_spec(
|
|
136 |
#' vars = vars,
|
|
137 |
#' choices = value_choices(data[["ADSL"]], vars$selected),
|
|
138 |
#' selected = value_choices(data[["ADSL"]], vars$selected),
|
|
139 |
#' multiple = TRUE
|
|
140 |
#' )
|
|
141 |
#' )
|
|
142 |
#' )
|
|
143 |
#' )
|
|
144 |
#' )
|
|
145 |
#' )
|
|
146 |
#' if (interactive()) {
|
|
147 |
#' shinyApp(app$ui, app$server)
|
|
148 |
#' }
|
|
149 |
#'
|
|
150 |
#' @export
|
|
151 |
#'
|
|
152 |
tm_outliers <- function(label = "Outliers Module", |
|
153 |
outlier_var,
|
|
154 |
categorical_var = NULL, |
|
155 |
ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), |
|
156 |
ggplot2_args = teal.widgets::ggplot2_args(), |
|
157 |
plot_height = c(600, 200, 2000), |
|
158 |
plot_width = NULL, |
|
159 |
pre_output = NULL, |
|
160 |
post_output = NULL, |
|
161 |
transformators = list(), |
|
162 |
decorators = list()) { |
|
163 | ! |
message("Initializing tm_outliers") |
164 | ||
165 |
# Normalize the parameters
|
|
166 | ! |
if (inherits(outlier_var, "data_extract_spec")) outlier_var <- list(outlier_var) |
167 | ! |
if (inherits(categorical_var, "data_extract_spec")) categorical_var <- list(categorical_var) |
168 | ! |
if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args) |
169 | ||
170 |
# Start of assertions
|
|
171 | ! |
checkmate::assert_string(label) |
172 | ! |
checkmate::assert_list(outlier_var, types = "data_extract_spec") |
173 | ||
174 | ! |
checkmate::assert_list(categorical_var, types = "data_extract_spec", null.ok = TRUE) |
175 | ! |
if (is.list(categorical_var)) { |
176 | ! |
lapply(categorical_var, function(x) { |
177 | ! |
if (length(x$filter) > 1L) { |
178 | ! |
stop("tm_outliers: categorical_var data_extract_specs may only specify one filter_spec", call. = FALSE) |
179 |
}
|
|
180 |
}) |
|
181 |
}
|
|
182 | ||
183 | ! |
ggtheme <- match.arg(ggtheme) |
184 | ||
185 | ! |
plot_choices <- c("Boxplot", "Density Plot", "Cumulative Distribution Plot") |
186 | ! |
checkmate::assert_list(ggplot2_args, types = "ggplot2_args") |
187 | ! |
checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices)) |
188 | ||
189 | ! |
checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
190 | ! |
checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
191 | ! |
checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
192 | ! |
checkmate::assert_numeric( |
193 | ! |
plot_width[1], |
194 | ! |
lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width" |
195 |
)
|
|
196 | ||
197 | ! |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
198 | ! |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
199 | ||
200 | ! |
available_decorators <- c("box_plot", "density_plot", "cumulative_plot", "table") |
201 | ! |
assert_decorators(decorators, names = available_decorators) |
202 |
# End of assertions
|
|
203 | ||
204 |
# Make UI args
|
|
205 | ! |
args <- as.list(environment()) |
206 | ||
207 | ! |
data_extract_list <- list( |
208 | ! |
outlier_var = outlier_var, |
209 | ! |
categorical_var = categorical_var |
210 |
)
|
|
211 | ||
212 | ||
213 | ! |
ans <- module( |
214 | ! |
label = label, |
215 | ! |
server = srv_outliers, |
216 | ! |
server_args = c( |
217 | ! |
data_extract_list,
|
218 | ! |
list( |
219 | ! |
plot_height = plot_height, plot_width = plot_width, ggplot2_args = ggplot2_args, |
220 | ! |
decorators = decorators |
221 |
)
|
|
222 |
),
|
|
223 | ! |
ui = ui_outliers, |
224 | ! |
ui_args = args, |
225 | ! |
transformators = transformators, |
226 | ! |
datanames = teal.transform::get_extract_datanames(data_extract_list) |
227 |
)
|
|
228 | ! |
attr(ans, "teal_bookmarkable") <- TRUE |
229 | ! |
ans
|
230 |
}
|
|
231 | ||
232 |
# UI function for the outliers module
|
|
233 |
ui_outliers <- function(id, ...) { |
|
234 | ! |
args <- list(...) |
235 | ! |
ns <- NS(id) |
236 | ! |
is_single_dataset_value <- teal.transform::is_single_dataset(args$outlier_var, args$categorical_var) |
237 | ||
238 | ! |
teal.widgets::standard_layout( |
239 | ! |
output = teal.widgets::white_small_well( |
240 | ! |
uiOutput(ns("total_outliers")), |
241 | ! |
DT::dataTableOutput(ns("summary_table")), |
242 | ! |
uiOutput(ns("total_missing")), |
243 | ! |
tags$br(), tags$hr(), |
244 | ! |
tabsetPanel( |
245 | ! |
id = ns("tabs"), |
246 | ! |
tabPanel( |
247 | ! |
"Boxplot",
|
248 | ! |
teal.widgets::plot_with_settings_ui(id = ns("box_plot")) |
249 |
),
|
|
250 | ! |
tabPanel( |
251 | ! |
"Density Plot",
|
252 | ! |
teal.widgets::plot_with_settings_ui(id = ns("density_plot")) |
253 |
),
|
|
254 | ! |
tabPanel( |
255 | ! |
"Cumulative Distribution Plot",
|
256 | ! |
teal.widgets::plot_with_settings_ui(id = ns("cum_density_plot")) |
257 |
)
|
|
258 |
),
|
|
259 | ! |
tags$br(), tags$hr(), |
260 | ! |
uiOutput(ns("table_ui_wrap")), |
261 | ! |
DT::dataTableOutput(ns("table_ui")) |
262 |
),
|
|
263 | ! |
encoding = tags$div( |
264 |
### Reporter
|
|
265 | ! |
teal.reporter::simple_reporter_ui(ns("simple_reporter")), |
266 |
###
|
|
267 | ! |
tags$label("Encodings", class = "text-primary"), |
268 | ! |
teal.transform::datanames_input(args[c("outlier_var", "categorical_var")]), |
269 | ! |
teal.transform::data_extract_ui( |
270 | ! |
id = ns("outlier_var"), |
271 | ! |
label = "Variable", |
272 | ! |
data_extract_spec = args$outlier_var, |
273 | ! |
is_single_dataset = is_single_dataset_value |
274 |
),
|
|
275 | ! |
if (!is.null(args$categorical_var)) { |
276 | ! |
teal.transform::data_extract_ui( |
277 | ! |
id = ns("categorical_var"), |
278 | ! |
label = "Categorical factor", |
279 | ! |
data_extract_spec = args$categorical_var, |
280 | ! |
is_single_dataset = is_single_dataset_value |
281 |
)
|
|
282 |
},
|
|
283 | ! |
conditionalPanel( |
284 | ! |
condition = paste0("input['", ns("tabs"), "'] == 'Boxplot'"), |
285 | ! |
teal.widgets::optionalSelectInput( |
286 | ! |
inputId = ns("boxplot_alts"), |
287 | ! |
label = "Plot type", |
288 | ! |
choices = c("Box plot", "Violin plot"), |
289 | ! |
selected = "Box plot", |
290 | ! |
multiple = FALSE |
291 |
)
|
|
292 |
),
|
|
293 | ! |
shinyjs::hidden(checkboxInput(ns("split_outliers"), "Define outliers based on group splitting", value = FALSE)), |
294 | ! |
shinyjs::hidden(checkboxInput(ns("order_by_outlier"), "Re-order categories by outliers [by %]", value = FALSE)), |
295 | ! |
teal.widgets::panel_group( |
296 | ! |
teal.widgets::panel_item( |
297 | ! |
title = "Method parameters", |
298 | ! |
collapsed = FALSE, |
299 | ! |
teal.widgets::optionalSelectInput( |
300 | ! |
inputId = ns("method"), |
301 | ! |
label = "Method", |
302 | ! |
choices = c("IQR", "Z-score", "Percentile"), |
303 | ! |
selected = "IQR", |
304 | ! |
multiple = FALSE |
305 |
),
|
|
306 | ! |
conditionalPanel( |
307 | ! |
condition = |
308 | ! |
paste0("input['", ns("method"), "'] == 'IQR'"), |
309 | ! |
sliderInput( |
310 | ! |
ns("iqr_slider"), |
311 | ! |
"Outlier range:",
|
312 | ! |
min = 1, |
313 | ! |
max = 5, |
314 | ! |
value = 3, |
315 | ! |
step = 0.5 |
316 |
)
|
|
317 |
),
|
|
318 | ! |
conditionalPanel( |
319 | ! |
condition = |
320 | ! |
paste0("input['", ns("method"), "'] == 'Z-score'"), |
321 | ! |
sliderInput( |
322 | ! |
ns("zscore_slider"), |
323 | ! |
"Outlier range:",
|
324 | ! |
min = 1, |
325 | ! |
max = 5, |
326 | ! |
value = 3, |
327 | ! |
step = 0.5 |
328 |
)
|
|
329 |
),
|
|
330 | ! |
conditionalPanel( |
331 | ! |
condition = |
332 | ! |
paste0("input['", ns("method"), "'] == 'Percentile'"), |
333 | ! |
sliderInput( |
334 | ! |
ns("percentile_slider"), |
335 | ! |
"Outlier range:",
|
336 | ! |
min = 0.001, |
337 | ! |
max = 0.5, |
338 | ! |
value = 0.01, |
339 | ! |
step = 0.001 |
340 |
)
|
|
341 |
),
|
|
342 | ! |
uiOutput(ns("ui_outlier_help")) |
343 |
)
|
|
344 |
),
|
|
345 | ! |
conditionalPanel( |
346 | ! |
condition = paste0("input['", ns("tabs"), "'] == 'Boxplot'"), |
347 | ! |
ui_decorate_teal_data( |
348 | ! |
ns("d_box_plot"), |
349 | ! |
decorators = select_decorators(args$decorators, "box_plot") |
350 |
)
|
|
351 |
),
|
|
352 | ! |
conditionalPanel( |
353 | ! |
condition = paste0("input['", ns("tabs"), "'] == 'Density Plot'"), |
354 | ! |
ui_decorate_teal_data( |
355 | ! |
ns("d_density_plot"), |
356 | ! |
decorators = select_decorators(args$decorators, "density_plot") |
357 |
)
|
|
358 |
),
|
|
359 | ! |
conditionalPanel( |
360 | ! |
condition = paste0("input['", ns("tabs"), "'] == 'Cumulative Distribution Plot'"), |
361 | ! |
ui_decorate_teal_data( |
362 | ! |
ns("d_cumulative_plot"), |
363 | ! |
decorators = select_decorators(args$decorators, "cumulative_plot") |
364 |
)
|
|
365 |
),
|
|
366 | ! |
ui_decorate_teal_data(ns("d_table"), decorators = select_decorators(args$decorators, "table")), |
367 | ! |
teal.widgets::panel_item( |
368 | ! |
title = "Plot settings", |
369 | ! |
selectInput( |
370 | ! |
inputId = ns("ggtheme"), |
371 | ! |
label = "Theme (by ggplot):", |
372 | ! |
choices = ggplot_themes, |
373 | ! |
selected = args$ggtheme, |
374 | ! |
multiple = FALSE |
375 |
)
|
|
376 |
)
|
|
377 |
),
|
|
378 | ! |
forms = tagList( |
379 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
380 |
),
|
|
381 | ! |
pre_output = args$pre_output, |
382 | ! |
post_output = args$post_output |
383 |
)
|
|
384 |
}
|
|
385 | ||
386 |
# Server function for the outliers module
|
|
387 |
# Server function for the outliers module
|
|
388 |
srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, |
|
389 |
categorical_var, plot_height, plot_width, ggplot2_args, decorators) { |
|
390 | ! |
with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") |
391 | ! |
with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") |
392 | ! |
checkmate::assert_class(data, "reactive") |
393 | ! |
checkmate::assert_class(isolate(data()), "teal_data") |
394 | ! |
moduleServer(id, function(input, output, session) { |
395 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
396 | ||
397 | ! |
ns <- session$ns |
398 | ||
399 | ! |
vars <- list(outlier_var = outlier_var, categorical_var = categorical_var) |
400 | ||
401 | ! |
rule_diff <- function(other) { |
402 | ! |
function(value) { |
403 | ! |
othervalue <- tryCatch(selector_list()[[other]]()[["select"]], error = function(e) NULL) |
404 | ! |
if (!is.null(othervalue) && identical(othervalue, value)) { |
405 | ! |
"`Variable` and `Categorical factor` cannot be the same"
|
406 |
}
|
|
407 |
}
|
|
408 |
}
|
|
409 | ||
410 | ! |
selector_list <- teal.transform::data_extract_multiple_srv( |
411 | ! |
data_extract = vars, |
412 | ! |
datasets = data, |
413 | ! |
select_validation_rule = list( |
414 | ! |
outlier_var = shinyvalidate::compose_rules( |
415 | ! |
shinyvalidate::sv_required("Please select a variable"), |
416 | ! |
rule_diff("categorical_var") |
417 |
),
|
|
418 | ! |
categorical_var = rule_diff("outlier_var") |
419 |
)
|
|
420 |
)
|
|
421 | ||
422 | ! |
iv_r <- reactive({ |
423 | ! |
iv <- shinyvalidate::InputValidator$new() |
424 | ! |
iv$add_rule("method", shinyvalidate::sv_required("Please select a method")) |
425 | ! |
iv$add_rule("boxplot_alts", shinyvalidate::sv_required("Please select Plot Type")) |
426 | ! |
teal.transform::compose_and_enable_validators(iv, selector_list) |
427 |
}) |
|
428 | ||
429 | ! |
reactive_select_input <- reactive({ |
430 | ! |
if (is.null(selector_list()$categorical_var) || length(selector_list()$categorical_var()$select) == 0) { |
431 | ! |
selector_list()[names(selector_list()) != "categorical_var"] |
432 |
} else { |
|
433 | ! |
selector_list() |
434 |
}
|
|
435 |
}) |
|
436 | ||
437 | ! |
anl_merged_input <- teal.transform::merge_expression_srv( |
438 | ! |
selector_list = reactive_select_input, |
439 | ! |
datasets = data, |
440 | ! |
merge_function = "dplyr::inner_join" |
441 |
)
|
|
442 | ||
443 | ! |
anl_merged_q <- reactive({ |
444 | ! |
req(anl_merged_input()) |
445 | ! |
teal.code::eval_code( |
446 | ! |
data(), |
447 | ! |
paste0( |
448 | ! |
'library("dplyr");library("tidyr");', # nolint quotes |
449 | ! |
'library("tibble");library("ggplot2");'
|
450 |
)
|
|
451 | ! |
) %>% # nolint quotes |
452 | ! |
teal.code::eval_code(as.expression(anl_merged_input()$expr)) |
453 |
}) |
|
454 | ||
455 | ! |
merged <- list( |
456 | ! |
anl_input_r = anl_merged_input, |
457 | ! |
anl_q_r = anl_merged_q |
458 |
)
|
|
459 | ||
460 | ! |
n_outlier_missing <- reactive({ |
461 | ! |
req(iv_r()$is_valid()) |
462 | ! |
outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var) |
463 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
464 | ! |
sum(is.na(ANL[[outlier_var]])) |
465 |
}) |
|
466 | ||
467 |
# Used to create outlier table and the dropdown with additional columns
|
|
468 | ! |
dataname_first <- isolate(names(data())[[1]]) |
469 | ||
470 | ! |
common_code_q <- reactive({ |
471 | ! |
req(iv_r()$is_valid()) |
472 | ||
473 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
474 | ! |
qenv <- merged$anl_q_r() |
475 | ||
476 | ! |
outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var) |
477 | ! |
categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var) |
478 | ! |
order_by_outlier <- input$order_by_outlier |
479 | ! |
method <- input$method |
480 | ! |
split_outliers <- input$split_outliers |
481 | ! |
teal::validate_has_data( |
482 |
# missing values in the categorical variable may be used to form a category of its own
|
|
483 | ! |
`if`( |
484 | ! |
length(categorical_var) == 0, |
485 | ! |
ANL,
|
486 | ! |
ANL[, names(ANL) != categorical_var, drop = FALSE] |
487 |
),
|
|
488 | ! |
min_nrow = 10, |
489 | ! |
complete = TRUE, |
490 | ! |
allow_inf = FALSE |
491 |
)
|
|
492 | ! |
validate(need(is.numeric(ANL[[outlier_var]]), "`Variable` is not numeric")) |
493 | ! |
validate(need(length(unique(ANL[[outlier_var]])) > 1, "Variable has no variation, i.e. only one unique value")) |
494 | ||
495 |
# show/hide split_outliers
|
|
496 | ! |
if (length(categorical_var) == 0) { |
497 | ! |
shinyjs::hide("split_outliers") |
498 | ! |
if (n_outlier_missing() > 0) { |
499 | ! |
qenv <- teal.code::eval_code( |
500 | ! |
qenv,
|
501 | ! |
substitute( |
502 | ! |
expr = ANL <- ANL %>% dplyr::filter(!is.na(outlier_var_name)), |
503 | ! |
env = list(outlier_var_name = as.name(outlier_var)) |
504 |
)
|
|
505 |
)
|
|
506 |
}
|
|
507 |
} else { |
|
508 | ! |
validate(need( |
509 | ! |
is.factor(ANL[[categorical_var]]) || |
510 | ! |
is.character(ANL[[categorical_var]]) || |
511 | ! |
is.integer(ANL[[categorical_var]]), |
512 | ! |
"`Categorical factor` must be `factor`, `character`, or `integer`"
|
513 |
)) |
|
514 | ||
515 | ! |
if (n_outlier_missing() > 0) { |
516 | ! |
qenv <- teal.code::eval_code( |
517 | ! |
qenv,
|
518 | ! |
substitute( |
519 | ! |
expr = ANL <- ANL %>% dplyr::filter(!is.na(outlier_var_name)), |
520 | ! |
env = list(outlier_var_name = as.name(outlier_var)) |
521 |
)
|
|
522 |
)
|
|
523 |
}
|
|
524 | ! |
shinyjs::show("split_outliers") |
525 |
}
|
|
526 | ||
527 |
# slider
|
|
528 | ! |
outlier_definition_param <- if (method == "IQR") { |
529 | ! |
input$iqr_slider |
530 | ! |
} else if (method == "Z-score") { |
531 | ! |
input$zscore_slider |
532 | ! |
} else if (method == "Percentile") { |
533 | ! |
input$percentile_slider |
534 |
}
|
|
535 | ||
536 |
# this is utils function that converts a %>% NULL %>% b into a %>% b
|
|
537 | ! |
remove_pipe_null <- function(x) { |
538 | ! |
if (length(x) == 1) { |
539 | ! |
x
|
540 | ! |
} else if (identical(x[[1]], as.name("%>%")) && is.null(x[[3]])) { |
541 | ! |
remove_pipe_null(x[[2]]) |
542 |
} else { |
|
543 | ! |
as.call(c(x[[1]], lapply(x[-1], remove_pipe_null))) |
544 |
}
|
|
545 |
}
|
|
546 | ||
547 | ! |
qenv <- teal.code::eval_code( |
548 | ! |
qenv,
|
549 | ! |
substitute( |
550 | ! |
expr = { |
551 | ! |
ANL_OUTLIER <- ANL %>% |
552 | ! |
group_expr %>% # styler: off |
553 | ! |
dplyr::mutate(is_outlier = { |
554 | ! |
q1_q3 <- stats::quantile(outlier_var_name, probs = c(0.25, 0.75)) |
555 | ! |
iqr <- q1_q3[2] - q1_q3[1] |
556 | ! |
!(outlier_var_name >= q1_q3[1] - 1.5 * iqr & outlier_var_name <= q1_q3[2] + 1.5 * iqr) |
557 |
}) %>% |
|
558 | ! |
calculate_outliers %>% # styler: off |
559 | ! |
ungroup_expr %>% # styler: off |
560 | ! |
dplyr::filter(is_outlier | is_outlier_selected) %>% |
561 | ! |
dplyr::select(-is_outlier) |
562 |
},
|
|
563 | ! |
env = list( |
564 | ! |
calculate_outliers = if (method == "IQR") { |
565 | ! |
substitute( |
566 | ! |
expr = dplyr::mutate(is_outlier_selected = { |
567 | ! |
q1_q3 <- stats::quantile(outlier_var_name, probs = c(0.25, 0.75)) |
568 | ! |
iqr <- q1_q3[2] - q1_q3[1] |
569 |
!( |
|
570 | ! |
outlier_var_name >= q1_q3[1] - outlier_definition_param * iqr & |
571 | ! |
outlier_var_name <= q1_q3[2] + outlier_definition_param * iqr |
572 |
)
|
|
573 |
}), |
|
574 | ! |
env = list( |
575 | ! |
outlier_var_name = as.name(outlier_var), |
576 | ! |
outlier_definition_param = outlier_definition_param |
577 |
)
|
|
578 |
)
|
|
579 | ! |
} else if (method == "Z-score") { |
580 | ! |
substitute( |
581 | ! |
expr = dplyr::mutate( |
582 | ! |
is_outlier_selected = abs(outlier_var_name - mean(outlier_var_name)) / |
583 | ! |
stats::sd(outlier_var_name) > outlier_definition_param |
584 |
),
|
|
585 | ! |
env = list( |
586 | ! |
outlier_var_name = as.name(outlier_var), |
587 | ! |
outlier_definition_param = outlier_definition_param |
588 |
)
|
|
589 |
)
|
|
590 | ! |
} else if (method == "Percentile") { |
591 | ! |
substitute( |
592 | ! |
expr = dplyr::mutate( |
593 | ! |
is_outlier_selected = outlier_var_name < stats::quantile(outlier_var_name, outlier_definition_param) | |
594 | ! |
outlier_var_name > stats::quantile(outlier_var_name, 1 - outlier_definition_param) |
595 |
),
|
|
596 | ! |
env = list( |
597 | ! |
outlier_var_name = as.name(outlier_var), |
598 | ! |
outlier_definition_param = outlier_definition_param |
599 |
)
|
|
600 |
)
|
|
601 |
},
|
|
602 | ! |
outlier_var_name = as.name(outlier_var), |
603 | ! |
group_expr = if (isTRUE(split_outliers) && length(categorical_var) != 0) { |
604 | ! |
substitute(dplyr::group_by(x), list(x = as.name(categorical_var))) |
605 |
},
|
|
606 | ! |
ungroup_expr = if (isTRUE(split_outliers) && length(categorical_var) != 0) { |
607 | ! |
substitute(dplyr::ungroup()) |
608 |
}
|
|
609 |
)
|
|
610 |
) %>% |
|
611 | ! |
remove_pipe_null() |
612 |
)
|
|
613 | ||
614 |
# ANL_OUTLIER_EXTENDED is the base table
|
|
615 | ! |
qenv <- teal.code::eval_code( |
616 | ! |
qenv,
|
617 | ! |
substitute( |
618 | ! |
expr = { |
619 | ! |
ANL_OUTLIER_EXTENDED <- dplyr::left_join( |
620 | ! |
ANL_OUTLIER,
|
621 | ! |
dplyr::select( |
622 | ! |
dataname,
|
623 | ! |
dplyr::setdiff(names(dataname), dplyr::setdiff(names(ANL_OUTLIER), join_keys)) |
624 |
),
|
|
625 | ! |
by = join_keys |
626 |
)
|
|
627 |
},
|
|
628 | ! |
env = list( |
629 | ! |
dataname = as.name(dataname_first), |
630 | ! |
join_keys = as.character(teal.data::join_keys(data())[dataname_first, dataname_first]) |
631 |
)
|
|
632 |
)
|
|
633 |
)
|
|
634 | ||
635 | ! |
qenv <- if (length(categorical_var) > 0) { |
636 | ! |
qenv <- teal.code::eval_code( |
637 | ! |
qenv,
|
638 | ! |
substitute( |
639 | ! |
expr = summary_table_pre <- ANL_OUTLIER %>% |
640 | ! |
dplyr::filter(is_outlier_selected) %>% |
641 | ! |
dplyr::select(outlier_var_name, categorical_var_name) %>% |
642 | ! |
dplyr::group_by(categorical_var_name) %>% |
643 | ! |
dplyr::summarise(n_outliers = dplyr::n()) %>% |
644 | ! |
dplyr::right_join( |
645 | ! |
ANL %>% |
646 | ! |
dplyr::select(outlier_var_name, categorical_var_name) %>% |
647 | ! |
dplyr::group_by(categorical_var_name) %>% |
648 | ! |
dplyr::summarise( |
649 | ! |
total_in_cat = dplyr::n(), |
650 | ! |
n_na = sum(is.na(outlier_var_name) | is.na(categorical_var_name)) |
651 |
),
|
|
652 | ! |
by = categorical_var |
653 |
) %>% |
|
654 |
# This is important as there may be categorical variables with natural orderings, e.g. AGE.
|
|
655 |
# The plots should be displayed by default in increasing order in these situations.
|
|
656 |
# dplyr::arrange will sort integer, factor, and character data types in the expected way.
|
|
657 | ! |
dplyr::arrange(categorical_var_name) %>% |
658 | ! |
dplyr::mutate( |
659 | ! |
n_outliers = dplyr::if_else(is.na(n_outliers), 0, as.numeric(n_outliers)), |
660 | ! |
display_str = dplyr::if_else( |
661 | ! |
n_outliers > 0, |
662 | ! |
sprintf("%d [%.02f%%]", n_outliers, 100 * n_outliers / total_in_cat), |
663 | ! |
"0"
|
664 |
),
|
|
665 | ! |
display_str_na = dplyr::if_else( |
666 | ! |
n_na > 0, |
667 | ! |
sprintf("%d [%.02f%%]", n_na, 100 * n_na / total_in_cat), |
668 | ! |
"0"
|
669 |
),
|
|
670 | ! |
order = seq_along(n_outliers) |
671 |
),
|
|
672 | ! |
env = list( |
673 | ! |
categorical_var = categorical_var, |
674 | ! |
categorical_var_name = as.name(categorical_var), |
675 | ! |
outlier_var_name = as.name(outlier_var) |
676 |
)
|
|
677 |
)
|
|
678 |
)
|
|
679 |
# now to handle when user chooses to order based on amount of outliers
|
|
680 | ! |
if (order_by_outlier) { |
681 | ! |
qenv <- teal.code::eval_code( |
682 | ! |
qenv,
|
683 | ! |
quote( |
684 | ! |
summary_table_pre <- summary_table_pre %>% |
685 | ! |
dplyr::arrange(desc(n_outliers / total_in_cat)) %>% |
686 | ! |
dplyr::mutate(order = seq_len(nrow(summary_table_pre))) |
687 |
)
|
|
688 |
)
|
|
689 |
}
|
|
690 | ||
691 | ! |
teal.code::eval_code( |
692 | ! |
qenv,
|
693 | ! |
substitute( |
694 | ! |
expr = { |
695 |
# In order for geom_rug to work properly when reordering takes place inside facet_grid,
|
|
696 |
# all tables must have the column used for reording.
|
|
697 |
# In this case, the column used for reordering is `order`.
|
|
698 | ! |
ANL_OUTLIER <- dplyr::left_join( |
699 | ! |
ANL_OUTLIER,
|
700 | ! |
summary_table_pre[, c("order", categorical_var)], |
701 | ! |
by = categorical_var |
702 |
)
|
|
703 |
# so that x axis of plot aligns with columns of summary table, from most outliers to least by percentage
|
|
704 | ! |
ANL <- ANL %>% |
705 | ! |
dplyr::left_join( |
706 | ! |
dplyr::select(summary_table_pre, categorical_var_name, order), |
707 | ! |
by = categorical_var |
708 |
) %>% |
|
709 | ! |
dplyr::arrange(order) |
710 | ! |
summary_table <- summary_table_pre %>% |
711 | ! |
dplyr::select( |
712 | ! |
categorical_var_name,
|
713 | ! |
Outliers = display_str, Missings = display_str_na, Total = total_in_cat |
714 |
) %>% |
|
715 | ! |
dplyr::mutate_all(as.character) %>% |
716 | ! |
tidyr::pivot_longer(-categorical_var_name) %>% |
717 | ! |
tidyr::pivot_wider(names_from = categorical_var, values_from = value) %>% |
718 | ! |
tibble::column_to_rownames("name") |
719 |
},
|
|
720 | ! |
env = list( |
721 | ! |
categorical_var = categorical_var, |
722 | ! |
categorical_var_name = as.name(categorical_var) |
723 |
)
|
|
724 |
)
|
|
725 |
)
|
|
726 |
} else { |
|
727 | ! |
within(qenv, summary_table <- data.frame()) |
728 |
}
|
|
729 | ||
730 |
# Generate decoratable object from data
|
|
731 | ! |
qenv <- within(qenv, { |
732 | ! |
table <- DT::datatable( |
733 | ! |
summary_table,
|
734 | ! |
options = list( |
735 | ! |
dom = "t", |
736 | ! |
autoWidth = TRUE, |
737 | ! |
columnDefs = list(list(width = "200px", targets = "_all")) |
738 |
)
|
|
739 |
)
|
|
740 |
}) |
|
741 | ||
742 | ! |
if (length(categorical_var) > 0 && nrow(qenv[["ANL_OUTLIER"]]) > 0) { |
743 | ! |
shinyjs::show("order_by_outlier") |
744 |
} else { |
|
745 | ! |
shinyjs::hide("order_by_outlier") |
746 |
}
|
|
747 | ||
748 | ! |
qenv
|
749 |
}) |
|
750 | ||
751 |
# boxplot/violinplot # nolint commented_code
|
|
752 | ! |
box_plot_q <- reactive({ |
753 | ! |
req(common_code_q()) |
754 | ! |
ANL <- common_code_q()[["ANL"]] |
755 | ! |
ANL_OUTLIER <- common_code_q()[["ANL_OUTLIER"]] |
756 | ||
757 | ! |
outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var) |
758 | ! |
categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var) |
759 | ||
760 |
# validation
|
|
761 | ! |
teal::validate_has_data(ANL, 1) |
762 | ||
763 |
# boxplot
|
|
764 | ! |
plot_call <- quote(ANL %>% ggplot()) |
765 | ||
766 | ! |
plot_call <- if (input$boxplot_alts == "Box plot") { |
767 | ! |
substitute(expr = plot_call + ggplot2::geom_boxplot(outlier.shape = NA), env = list(plot_call = plot_call)) |
768 | ! |
} else if (input$boxplot_alts == "Violin plot") { |
769 | ! |
substitute(expr = plot_call + ggplot2::geom_violin(), env = list(plot_call = plot_call)) |
770 |
} else { |
|
771 | ! |
NULL
|
772 |
}
|
|
773 | ||
774 | ! |
plot_call <- if (identical(categorical_var, character(0)) || is.null(categorical_var)) { |
775 | ! |
inner_call <- substitute( |
776 | ! |
expr = plot_call + |
777 | ! |
ggplot2::aes(x = "Entire dataset", y = outlier_var_name) + |
778 | ! |
ggplot2::scale_x_discrete(), |
779 | ! |
env = list(plot_call = plot_call, outlier_var_name = as.name(outlier_var)) |
780 |
)
|
|
781 | ! |
if (nrow(ANL_OUTLIER) > 0) { |
782 | ! |
substitute( |
783 | ! |
expr = inner_call + ggplot2::geom_point( |
784 | ! |
data = ANL_OUTLIER, |
785 | ! |
ggplot2::aes(x = "Entire dataset", y = outlier_var_name, color = is_outlier_selected) |
786 |
),
|
|
787 | ! |
env = list(inner_call = inner_call, outlier_var_name = as.name(outlier_var)) |
788 |
)
|
|
789 |
} else { |
|
790 | ! |
inner_call
|
791 |
}
|
|
792 |
} else { |
|
793 | ! |
substitute( |
794 | ! |
expr = plot_call + |
795 | ! |
ggplot2::aes(y = outlier_var_name, x = reorder(categorical_var_name, order)) + |
796 | ! |
ggplot2::xlab(categorical_var) + |
797 | ! |
ggplot2::scale_x_discrete() + |
798 | ! |
ggplot2::geom_point( |
799 | ! |
data = ANL_OUTLIER, |
800 | ! |
ggplot2::aes(x = as.factor(categorical_var_name), y = outlier_var_name, color = is_outlier_selected) |
801 |
),
|
|
802 | ! |
env = list( |
803 | ! |
plot_call = plot_call, |
804 | ! |
outlier_var_name = as.name(outlier_var), |
805 | ! |
categorical_var_name = as.name(categorical_var), |
806 | ! |
categorical_var = categorical_var |
807 |
)
|
|
808 |
)
|
|
809 |
}
|
|
810 | ||
811 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
812 | ! |
labs = list(color = "Is outlier?"), |
813 | ! |
theme = list(legend.position = "top") |
814 |
)
|
|
815 | ||
816 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
817 | ! |
user_plot = ggplot2_args[["Boxplot"]], |
818 | ! |
user_default = ggplot2_args$default, |
819 | ! |
module_plot = dev_ggplot2_args |
820 |
)
|
|
821 | ||
822 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
823 | ! |
all_ggplot2_args,
|
824 | ! |
ggtheme = input$ggtheme |
825 |
)
|
|
826 | ||
827 | ! |
teal.code::eval_code( |
828 | ! |
common_code_q(), |
829 | ! |
substitute( |
830 | ! |
expr = box_plot <- plot_call + |
831 | ! |
ggplot2::scale_color_manual(values = c("TRUE" = "red", "FALSE" = "black")) + |
832 | ! |
labs + ggthemes + themes, |
833 | ! |
env = list( |
834 | ! |
plot_call = plot_call, |
835 | ! |
labs = parsed_ggplot2_args$labs, |
836 | ! |
ggthemes = parsed_ggplot2_args$ggtheme, |
837 | ! |
themes = parsed_ggplot2_args$theme |
838 |
)
|
|
839 |
)
|
|
840 |
)
|
|
841 |
}) |
|
842 | ||
843 |
# density plot
|
|
844 | ! |
density_plot_q <- reactive({ |
845 | ! |
ANL <- common_code_q()[["ANL"]] |
846 | ! |
ANL_OUTLIER <- common_code_q()[["ANL_OUTLIER"]] |
847 | ||
848 | ! |
outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var) |
849 | ! |
categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var) |
850 | ||
851 |
# validation
|
|
852 | ! |
teal::validate_has_data(ANL, 1) |
853 |
# plot
|
|
854 | ! |
plot_call <- substitute( |
855 | ! |
expr = ANL %>% |
856 | ! |
ggplot2::ggplot(ggplot2::aes(x = outlier_var_name)) + |
857 | ! |
ggplot2::geom_density() + |
858 | ! |
ggplot2::geom_rug(data = ANL_OUTLIER, ggplot2::aes(x = outlier_var_name, color = is_outlier_selected)) + |
859 | ! |
ggplot2::scale_color_manual(values = c("TRUE" = "red", "FALSE" = "black")), |
860 | ! |
env = list(outlier_var_name = as.name(outlier_var)) |
861 |
)
|
|
862 | ||
863 | ! |
plot_call <- if (identical(categorical_var, character(0)) || is.null(categorical_var)) { |
864 | ! |
substitute(expr = plot_call, env = list(plot_call = plot_call)) |
865 |
} else { |
|
866 | ! |
substitute( |
867 | ! |
expr = plot_call + ggplot2::facet_grid(~ reorder(categorical_var_name, order)), |
868 | ! |
env = list(plot_call = plot_call, categorical_var_name = as.name(categorical_var)) |
869 |
)
|
|
870 |
}
|
|
871 | ||
872 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
873 | ! |
labs = list(color = "Is outlier?"), |
874 | ! |
theme = list(legend.position = "top") |
875 |
)
|
|
876 | ||
877 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
878 | ! |
user_plot = ggplot2_args[["Density Plot"]], |
879 | ! |
user_default = ggplot2_args$default, |
880 | ! |
module_plot = dev_ggplot2_args |
881 |
)
|
|
882 | ||
883 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
884 | ! |
all_ggplot2_args,
|
885 | ! |
ggtheme = input$ggtheme |
886 |
)
|
|
887 | ||
888 | ! |
teal.code::eval_code( |
889 | ! |
common_code_q(), |
890 | ! |
substitute( |
891 | ! |
expr = density_plot <- plot_call + labs + ggthemes + themes, |
892 | ! |
env = list( |
893 | ! |
plot_call = plot_call, |
894 | ! |
labs = parsed_ggplot2_args$labs, |
895 | ! |
themes = parsed_ggplot2_args$theme, |
896 | ! |
ggthemes = parsed_ggplot2_args$ggtheme |
897 |
)
|
|
898 |
)
|
|
899 |
)
|
|
900 |
}) |
|
901 | ||
902 |
# Cumulative distribution plot
|
|
903 | ! |
cumulative_plot_q <- reactive({ |
904 | ! |
qenv <- common_code_q() |
905 | ||
906 | ! |
ANL <- qenv[["ANL"]] |
907 | ! |
ANL_OUTLIER <- qenv[["ANL_OUTLIER"]] |
908 | ||
909 | ! |
outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var) |
910 | ! |
categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var) |
911 | ||
912 |
# validation
|
|
913 | ! |
teal::validate_has_data(ANL, 1) |
914 | ||
915 |
# plot
|
|
916 | ! |
plot_call <- substitute( |
917 | ! |
expr = ANL %>% ggplot2::ggplot(ggplot2::aes(x = outlier_var_name)) + |
918 | ! |
ggplot2::stat_ecdf(), |
919 | ! |
env = list(outlier_var_name = as.name(outlier_var)) |
920 |
)
|
|
921 | ! |
if (length(categorical_var) == 0) { |
922 | ! |
qenv <- teal.code::eval_code( |
923 | ! |
qenv,
|
924 | ! |
substitute( |
925 | ! |
expr = { |
926 | ! |
ecdf_df <- ANL %>% |
927 | ! |
dplyr::mutate( |
928 | ! |
y = stats::ecdf(ANL[[outlier_var]])(ANL[[outlier_var]]) |
929 |
)
|
|
930 | ||
931 | ! |
outlier_points <- dplyr::left_join( |
932 | ! |
ecdf_df,
|
933 | ! |
ANL_OUTLIER,
|
934 | ! |
by = dplyr::setdiff(names(ecdf_df), "y") |
935 |
) %>% |
|
936 | ! |
dplyr::filter(!is.na(is_outlier_selected)) |
937 |
},
|
|
938 | ! |
env = list(outlier_var = outlier_var) |
939 |
)
|
|
940 |
)
|
|
941 |
} else { |
|
942 | ! |
qenv <- teal.code::eval_code( |
943 | ! |
qenv,
|
944 | ! |
substitute( |
945 | ! |
expr = { |
946 | ! |
all_categories <- lapply( |
947 | ! |
unique(ANL[[categorical_var]]), |
948 | ! |
function(x) { |
949 | ! |
ANL <- ANL %>% dplyr::filter(get(categorical_var) == x) |
950 | ! |
anl_outlier2 <- ANL_OUTLIER %>% dplyr::filter(get(categorical_var) == x) |
951 | ! |
ecdf_df <- ANL %>% |
952 | ! |
dplyr::mutate(y = stats::ecdf(ANL[[outlier_var]])(ANL[[outlier_var]])) |
953 | ||
954 | ! |
dplyr::left_join( |
955 | ! |
ecdf_df,
|
956 | ! |
anl_outlier2,
|
957 | ! |
by = dplyr::setdiff(names(ecdf_df), "y") |
958 |
) %>% |
|
959 | ! |
dplyr::filter(!is.na(is_outlier_selected)) |
960 |
}
|
|
961 |
)
|
|
962 | ! |
outlier_points <- do.call(rbind, all_categories) |
963 |
},
|
|
964 | ! |
env = list(categorical_var = categorical_var, outlier_var = outlier_var) |
965 |
)
|
|
966 |
)
|
|
967 | ! |
plot_call <- substitute( |
968 | ! |
expr = plot_call + ggplot2::facet_grid(~ reorder(categorical_var_name, order)), |
969 | ! |
env = list(plot_call = plot_call, categorical_var_name = as.name(categorical_var)) |
970 |
)
|
|
971 |
}
|
|
972 | ||
973 | ! |
dev_ggplot2_args <- teal.widgets::ggplot2_args( |
974 | ! |
labs = list(color = "Is outlier?"), |
975 | ! |
theme = list(legend.position = "top") |
976 |
)
|
|
977 | ||
978 | ! |
all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
979 | ! |
user_plot = ggplot2_args[["Cumulative Distribution Plot"]], |
980 | ! |
user_default = ggplot2_args$default, |
981 | ! |
module_plot = dev_ggplot2_args |
982 |
)
|
|
983 | ||
984 | ! |
parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( |
985 | ! |
all_ggplot2_args,
|
986 | ! |
ggtheme = input$ggtheme |
987 |
)
|
|
988 | ||
989 | ! |
teal.code::eval_code( |
990 | ! |
qenv,
|
991 | ! |
substitute( |
992 | ! |
expr = cumulative_plot <- plot_call + |
993 | ! |
ggplot2::geom_point( |
994 | ! |
data = outlier_points, |
995 | ! |
ggplot2::aes(x = outlier_var_name, y = y, color = is_outlier_selected) |
996 |
) + |
|
997 | ! |
ggplot2::scale_color_manual(values = c("TRUE" = "red", "FALSE" = "black")) + |
998 | ! |
labs + ggthemes + themes, |
999 | ! |
env = list( |
1000 | ! |
plot_call = plot_call, |
1001 | ! |
outlier_var_name = as.name(outlier_var), |
1002 | ! |
labs = parsed_ggplot2_args$labs, |
1003 | ! |
themes = parsed_ggplot2_args$theme, |
1004 | ! |
ggthemes = parsed_ggplot2_args$ggtheme |
1005 |
)
|
|
1006 |
)
|
|
1007 |
)
|
|
1008 |
}) |
|
1009 | ||
1010 | ! |
current_tab_r <- reactive({ |
1011 | ! |
switch(req(input$tabs), |
1012 | ! |
"Boxplot" = "box_plot", |
1013 | ! |
"Density Plot" = "density_plot", |
1014 | ! |
"Cumulative Distribution Plot" = "cumulative_plot" |
1015 |
)
|
|
1016 |
}) |
|
1017 | ||
1018 | ! |
decorated_q <- mapply( |
1019 | ! |
function(obj_name, q) { |
1020 | ! |
srv_decorate_teal_data( |
1021 | ! |
id = sprintf("d_%s", obj_name), |
1022 | ! |
data = q, |
1023 | ! |
decorators = select_decorators(decorators, obj_name), |
1024 | ! |
expr = reactive({ |
1025 | ! |
substitute( |
1026 | ! |
expr = { |
1027 | ! |
columns_index <- union( |
1028 | ! |
setdiff(names(ANL_OUTLIER), c("is_outlier_selected", "order")), |
1029 | ! |
table_columns
|
1030 |
)
|
|
1031 | ! |
ANL_OUTLIER_EXTENDED[ANL_OUTLIER_EXTENDED$is_outlier_selected, columns_index] |
1032 | ! |
print(.plot) |
1033 |
},
|
|
1034 | ! |
env = list(table_columns = input$table_ui_columns, .plot = as.name(obj_name)) |
1035 |
)
|
|
1036 |
}), |
|
1037 | ! |
expr_is_reactive = TRUE |
1038 |
)
|
|
1039 |
},
|
|
1040 | ! |
stats::setNames(nm = c("box_plot", "density_plot", "cumulative_plot")), |
1041 | ! |
c(box_plot_q, density_plot_q, cumulative_plot_q) |
1042 |
)
|
|
1043 | ||
1044 | ! |
decorated_final_q_no_table <- reactive(decorated_q[[req(current_tab_r())]]()) |
1045 | ||
1046 | ! |
decorated_final_q <- srv_decorate_teal_data( |
1047 | ! |
"d_table",
|
1048 | ! |
data = decorated_final_q_no_table, |
1049 | ! |
decorators = select_decorators(decorators, "table"), |
1050 | ! |
expr = table |
1051 |
)
|
|
1052 | ||
1053 | ! |
output$summary_table <- DT::renderDataTable( |
1054 | ! |
expr = { |
1055 | ! |
if (iv_r()$is_valid()) { |
1056 | ! |
categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var) |
1057 | ! |
if (!is.null(categorical_var)) { |
1058 | ! |
decorated_final_q()[["table"]] |
1059 |
}
|
|
1060 |
}
|
|
1061 |
}
|
|
1062 |
)
|
|
1063 | ||
1064 |
# slider text
|
|
1065 | ! |
output$ui_outlier_help <- renderUI({ |
1066 | ! |
req(input$method) |
1067 | ! |
if (input$method == "IQR") { |
1068 | ! |
req(input$iqr_slider) |
1069 | ! |
tags$small( |
1070 | ! |
withMathJax( |
1071 | ! |
helpText( |
1072 | ! |
"Outlier data points (\\(x \\lt Q1 - ", input$iqr_slider, "\\times IQR\\) or \\( |
1073 | ! |
Q3 + ", input$iqr_slider, "\\times IQR \\lt x\\)) |
1074 | ! |
are displayed in red on the plot and can be visualized in the table below." |
1075 |
),
|
|
1076 | ! |
if (input$split_outliers) { |
1077 | ! |
withMathJax(helpText("Note: Quantiles are calculated per group.")) |
1078 |
}
|
|
1079 |
)
|
|
1080 |
)
|
|
1081 | ! |
} else if (input$method == "Z-score") { |
1082 | ! |
req(input$zscore_slider) |
1083 | ! |
tags$small( |
1084 | ! |
withMathJax( |
1085 | ! |
helpText( |
1086 | ! |
"Outlier data points (\\(Zscore(x) < -", input$zscore_slider, |
1087 | ! |
"\\) or \\(", input$zscore_slider, "< Zscore(x) \\)) |
1088 | ! |
are displayed in red on the plot and can be visualized in the table below." |
1089 |
),
|
|
1090 | ! |
if (input$split_outliers) { |
1091 | ! |
withMathJax(helpText(" Note: Z-scores are calculated per group.")) |
1092 |
}
|
|
1093 |
)
|
|
1094 |
)
|
|
1095 | ! |
} else if (input$method == "Percentile") { |
1096 | ! |
req(input$percentile_slider) |
1097 | ! |
tags$small( |
1098 | ! |
withMathJax( |
1099 | ! |
helpText( |
1100 | ! |
"Outlier/extreme data points (\\( Percentile(x) <", input$percentile_slider, |
1101 | ! |
"\\) or \\(", 1 - input$percentile_slider, " < Percentile(x) \\)) |
1102 | ! |
are displayed in red on the plot and can be visualized in the table below." |
1103 |
),
|
|
1104 | ! |
if (input$split_outliers) { |
1105 | ! |
withMathJax(helpText("Note: Percentiles are calculated per group.")) |
1106 |
}
|
|
1107 |
)
|
|
1108 |
)
|
|
1109 |
}
|
|
1110 |
}) |
|
1111 | ||
1112 | ! |
box_plot_r <- reactive({ |
1113 | ! |
teal::validate_inputs(iv_r()) |
1114 | ! |
req(decorated_q$box_plot())[["box_plot"]] |
1115 |
}) |
|
1116 | ! |
density_plot_r <- reactive({ |
1117 | ! |
teal::validate_inputs(iv_r()) |
1118 | ! |
req(decorated_q$density_plot())[["density_plot"]] |
1119 |
}) |
|
1120 | ! |
cumulative_plot_r <- reactive({ |
1121 | ! |
teal::validate_inputs(iv_r()) |
1122 | ! |
req(decorated_q$cumulative_plot())[["cumulative_plot"]] |
1123 |
}) |
|
1124 | ||
1125 | ! |
box_pws <- teal.widgets::plot_with_settings_srv( |
1126 | ! |
id = "box_plot", |
1127 | ! |
plot_r = box_plot_r, |
1128 | ! |
height = plot_height, |
1129 | ! |
width = plot_width, |
1130 | ! |
brushing = TRUE |
1131 |
)
|
|
1132 | ||
1133 | ! |
density_pws <- teal.widgets::plot_with_settings_srv( |
1134 | ! |
id = "density_plot", |
1135 | ! |
plot_r = density_plot_r, |
1136 | ! |
height = plot_height, |
1137 | ! |
width = plot_width, |
1138 | ! |
brushing = TRUE |
1139 |
)
|
|
1140 | ||
1141 | ! |
cum_density_pws <- teal.widgets::plot_with_settings_srv( |
1142 | ! |
id = "cum_density_plot", |
1143 | ! |
plot_r = cumulative_plot_r, |
1144 | ! |
height = plot_height, |
1145 | ! |
width = plot_width, |
1146 | ! |
brushing = TRUE |
1147 |
)
|
|
1148 | ||
1149 | ! |
choices <- reactive(teal.transform::variable_choices(data()[[dataname_first]])) |
1150 | ||
1151 | ! |
observeEvent(common_code_q(), { |
1152 | ! |
ANL_OUTLIER <- common_code_q()[["ANL_OUTLIER"]] |
1153 | ! |
teal.widgets::updateOptionalSelectInput( |
1154 | ! |
session,
|
1155 | ! |
inputId = "table_ui_columns", |
1156 | ! |
choices = dplyr::setdiff(choices(), names(ANL_OUTLIER)), |
1157 | ! |
selected = restoreInput(ns("table_ui_columns"), isolate(input$table_ui_columns)) |
1158 |
)
|
|
1159 |
}) |
|
1160 | ||
1161 | ! |
output$table_ui <- DT::renderDataTable( |
1162 | ! |
expr = { |
1163 | ! |
tab <- input$tabs |
1164 | ! |
req(tab) # tab is NULL upon app launch, hence will crash without this statement |
1165 | ! |
req(iv_r()$is_valid()) # Same validation as output$table_ui_wrap |
1166 | ! |
outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var) |
1167 | ! |
categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var) |
1168 | ||
1169 | ! |
ANL_OUTLIER <- common_code_q()[["ANL_OUTLIER"]] |
1170 | ! |
ANL_OUTLIER_EXTENDED <- common_code_q()[["ANL_OUTLIER_EXTENDED"]] |
1171 | ! |
ANL <- common_code_q()[["ANL"]] |
1172 | ||
1173 | ! |
plot_brush <- switch(current_tab_r(), |
1174 | ! |
box_plot = { |
1175 | ! |
box_plot_r() |
1176 | ! |
box_pws$brush() |
1177 |
},
|
|
1178 | ! |
density_plot = { |
1179 | ! |
density_plot_r() |
1180 | ! |
density_pws$brush() |
1181 |
},
|
|
1182 | ! |
cumulative_plot = { |
1183 | ! |
cumulative_plot_r() |
1184 | ! |
cum_density_pws$brush() |
1185 |
}
|
|
1186 |
)
|
|
1187 | ||
1188 |
# removing unused column ASAP
|
|
1189 | ! |
ANL_OUTLIER$order <- ANL$order <- NULL |
1190 | ||
1191 | ! |
display_table <- if (!is.null(plot_brush)) { |
1192 | ! |
if (length(categorical_var) > 0) { |
1193 |
# due to reordering, the x-axis label may be changed to something like "reorder(categorical_var, order)"
|
|
1194 | ! |
if (tab == "Boxplot") { |
1195 | ! |
plot_brush$mapping$x <- categorical_var |
1196 |
} else { |
|
1197 |
# the other plots use facetting
|
|
1198 |
# so it is panelvar1 that gets relabelled to "reorder(categorical_var, order)"
|
|
1199 | ! |
plot_brush$mapping$panelvar1 <- categorical_var |
1200 |
}
|
|
1201 |
} else { |
|
1202 | ! |
if (tab == "Boxplot") { |
1203 |
# in boxplot with no categorical variable, there is no column in ANL that would correspond to x-axis
|
|
1204 |
# so a column needs to be inserted with the value "Entire dataset" because that's the label used in plot
|
|
1205 | ! |
ANL[[plot_brush$mapping$x]] <- "Entire dataset" |
1206 |
}
|
|
1207 |
}
|
|
1208 | ||
1209 |
# in density and cumulative plots, ANL does not have a column corresponding to y-axis.
|
|
1210 |
# so they need to be computed and attached to ANL
|
|
1211 | ! |
if (tab == "Density Plot") { |
1212 | ! |
plot_brush$mapping$y <- "density" |
1213 | ! |
ANL$density <- plot_brush$ymin |
1214 |
# either ymin or ymax will work
|
|
1215 | ! |
} else if (tab == "Cumulative Distribution Plot") { |
1216 | ! |
plot_brush$mapping$y <- "cdf" |
1217 | ! |
if (length(categorical_var) > 0) { |
1218 | ! |
ANL <- ANL %>% |
1219 | ! |
dplyr::group_by(!!as.name(plot_brush$mapping$panelvar1)) %>% |
1220 | ! |
dplyr::mutate(cdf = stats::ecdf(!!as.name(outlier_var))(!!as.name(outlier_var))) |
1221 |
} else { |
|
1222 | ! |
ANL$cdf <- stats::ecdf(ANL[[outlier_var]])(ANL[[outlier_var]]) |
1223 |
}
|
|
1224 |
}
|
|
1225 | ||
1226 | ! |
brushed_rows <- brushedPoints(ANL, plot_brush) |
1227 | ! |
if (nrow(brushed_rows) > 0) { |
1228 |
# now we need to remove extra column from ANL so that it will have the same columns as ANL_OUTLIER
|
|
1229 |
# so that dplyr::intersect will work
|
|
1230 | ! |
if (tab == "Density Plot") { |
1231 | ! |
brushed_rows$density <- NULL |
1232 | ! |
} else if (tab == "Cumulative Distribution Plot") { |
1233 | ! |
brushed_rows$cdf <- NULL |
1234 | ! |
} else if (tab == "Boxplot" && length(categorical_var) == 0) { |
1235 | ! |
brushed_rows[[plot_brush$mapping$x]] <- NULL |
1236 |
}
|
|
1237 |
# is_outlier_selected is part of ANL_OUTLIER so needed here
|
|
1238 | ! |
brushed_rows$is_outlier_selected <- TRUE |
1239 | ! |
dplyr::intersect(ANL_OUTLIER, brushed_rows) |
1240 |
} else { |
|
1241 | ! |
ANL_OUTLIER[0, ] |
1242 |
}
|
|
1243 |
} else { |
|
1244 | ! |
ANL_OUTLIER[ANL_OUTLIER$is_outlier_selected, ] |
1245 |
}
|
|
1246 | ||
1247 | ! |
display_table$is_outlier_selected <- NULL |
1248 | ||
1249 |
# Extend the brushed ANL_OUTLIER with additional columns
|
|
1250 | ! |
dplyr::left_join( |
1251 | ! |
display_table,
|
1252 | ! |
dplyr::select(ANL_OUTLIER_EXTENDED, -"is_outlier_selected"), |
1253 | ! |
by = names(display_table) |
1254 |
) %>% |
|
1255 | ! |
dplyr::select(union(names(display_table), input$table_ui_columns)) |
1256 |
},
|
|
1257 | ! |
options = list( |
1258 | ! |
searching = FALSE, language = list( |
1259 | ! |
zeroRecords = "The brushed area does not contain outlier observations for the currently defined threshold" |
1260 |
),
|
|
1261 | ! |
pageLength = input$table_ui_rows |
1262 |
)
|
|
1263 |
)
|
|
1264 | ||
1265 | ! |
output$total_outliers <- renderUI({ |
1266 | ! |
req(iv_r()$is_valid()) |
1267 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
1268 | ! |
ANL_OUTLIER <- common_code_q()[["ANL_OUTLIER"]] |
1269 | ! |
teal::validate_has_data(ANL, 1) |
1270 | ! |
ANL_OUTLIER_SELECTED <- ANL_OUTLIER[ANL_OUTLIER$is_outlier_selected, ] |
1271 | ! |
tags$h5( |
1272 | ! |
sprintf( |
1273 | ! |
"%s %d / %d [%.02f%%]",
|
1274 | ! |
"Total number of outlier(s):",
|
1275 | ! |
nrow(ANL_OUTLIER_SELECTED), |
1276 | ! |
nrow(ANL), |
1277 | ! |
100 * nrow(ANL_OUTLIER_SELECTED) / nrow(ANL) |
1278 |
)
|
|
1279 |
)
|
|
1280 |
}) |
|
1281 | ||
1282 | ! |
output$total_missing <- renderUI({ |
1283 | ! |
if (n_outlier_missing() > 0) { |
1284 | ! |
ANL <- merged$anl_q_r()[["ANL"]] |
1285 | ! |
helpText( |
1286 | ! |
sprintf( |
1287 | ! |
"%s %d / %d [%.02f%%]",
|
1288 | ! |
"Total number of row(s) with missing values:",
|
1289 | ! |
n_outlier_missing(), |
1290 | ! |
nrow(ANL), |
1291 | ! |
100 * (n_outlier_missing()) / nrow(ANL) |
1292 |
)
|
|
1293 |
)
|
|
1294 |
}
|
|
1295 |
}) |
|
1296 | ||
1297 | ! |
output$table_ui_wrap <- renderUI({ |
1298 | ! |
req(iv_r()$is_valid()) |
1299 | ! |
tagList( |
1300 | ! |
teal.widgets::optionalSelectInput( |
1301 | ! |
inputId = ns("table_ui_columns"), |
1302 | ! |
label = "Choose additional columns", |
1303 | ! |
choices = NULL, |
1304 | ! |
selected = NULL, |
1305 | ! |
multiple = TRUE |
1306 |
),
|
|
1307 | ! |
tags$h4("Outlier Table"), |
1308 | ! |
teal.widgets::get_dt_rows(ns("table_ui"), ns("table_ui_rows")) |
1309 |
)
|
|
1310 |
}) |
|
1311 | ||
1312 |
# Render R code.
|
|
1313 | ! |
source_code_r <- reactive(teal.code::get_code(req(decorated_final_q()))) |
1314 | ||
1315 | ! |
teal.widgets::verbatim_popup_srv( |
1316 | ! |
id = "rcode", |
1317 | ! |
verbatim_content = source_code_r, |
1318 | ! |
title = "Show R Code for Outlier" |
1319 |
)
|
|
1320 | ||
1321 |
### REPORTER
|
|
1322 | ! |
if (with_reporter) { |
1323 | ! |
card_fun <- function(comment, label) { |
1324 | ! |
tab_type <- input$tabs |
1325 | ! |
card <- teal::report_card_template( |
1326 | ! |
title = paste0("Outliers - ", tab_type), |
1327 | ! |
label = label, |
1328 | ! |
with_filter = with_filter, |
1329 | ! |
filter_panel_api = filter_panel_api |
1330 |
)
|
|
1331 | ! |
categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var) |
1332 | ! |
if (length(categorical_var) > 0) { |
1333 | ! |
summary_table <- decorated_final_q()[["table"]] |
1334 | ! |
card$append_text("Summary Table", "header3") |
1335 | ! |
card$append_table(summary_table) |
1336 |
}
|
|
1337 | ! |
card$append_text("Plot", "header3") |
1338 | ! |
if (tab_type == "Boxplot") { |
1339 | ! |
card$append_plot(box_plot_r(), dim = box_pws$dim()) |
1340 | ! |
} else if (tab_type == "Density Plot") { |
1341 | ! |
card$append_plot(density_plot_r(), dim = density_pws$dim()) |
1342 | ! |
} else if (tab_type == "Cumulative Distribution Plot") { |
1343 | ! |
card$append_plot(cumulative_plot_r(), dim = cum_density_pws$dim()) |
1344 |
}
|
|
1345 | ! |
if (!comment == "") { |
1346 | ! |
card$append_text("Comment", "header3") |
1347 | ! |
card$append_text(comment) |
1348 |
}
|
|
1349 | ! |
card$append_src(source_code_r()) |
1350 | ! |
card
|
1351 |
}
|
|
1352 | ! |
teal.reporter::simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) |
1353 |
}
|
|
1354 |
###
|
|
1355 |
}) |
|
1356 |
}
|
1 |
#' `teal` module: File viewer
|
|
2 |
#'
|
|
3 |
#' The file viewer module provides a tool to view static files.
|
|
4 |
#' Supported formats include text formats, `PDF`, `PNG` `APNG`,
|
|
5 |
#' `JPEG` `SVG`, `WEBP`, `GIF` and `BMP`.
|
|
6 |
#'
|
|
7 |
#' @inheritParams teal::module
|
|
8 |
#' @inheritParams shared_params
|
|
9 |
#' @param input_path (`list`) of the input paths, optional. Each element can be:
|
|
10 |
#'
|
|
11 |
#' Paths can be specified as absolute paths or relative to the running directory of the application.
|
|
12 |
#' Default to the current working directory if not supplied.
|
|
13 |
#'
|
|
14 |
#' @inherit shared_params return
|
|
15 |
#'
|
|
16 |
#' @examplesShinylive
|
|
17 |
#' library(teal.modules.general)
|
|
18 |
#' interactive <- function() TRUE
|
|
19 |
#' {{ next_example }}
|
|
20 |
#' @examples
|
|
21 |
#' data <- teal_data()
|
|
22 |
#' data <- within(data, {
|
|
23 |
#' data <- data.frame(1)
|
|
24 |
#' })
|
|
25 |
#'
|
|
26 |
#' app <- init(
|
|
27 |
#' data = data,
|
|
28 |
#' modules = modules(
|
|
29 |
#' tm_file_viewer(
|
|
30 |
#' input_path = list(
|
|
31 |
#' folder = system.file("sample_files", package = "teal.modules.general"),
|
|
32 |
#' png = system.file("sample_files/sample_file.png", package = "teal.modules.general"),
|
|
33 |
#' txt = system.file("sample_files/sample_file.txt", package = "teal.modules.general"),
|
|
34 |
#' url = "https://fda.gov/files/drugs/published/Portable-Document-Format-Specifications.pdf"
|
|
35 |
#' )
|
|
36 |
#' )
|
|
37 |
#' )
|
|
38 |
#' )
|
|
39 |
#' if (interactive()) {
|
|
40 |
#' shinyApp(app$ui, app$server)
|
|
41 |
#' }
|
|
42 |
#'
|
|
43 |
#' @export
|
|
44 |
#'
|
|
45 |
tm_file_viewer <- function(label = "File Viewer Module", |
|
46 |
input_path = list("Current Working Directory" = ".")) { |
|
47 | ! |
message("Initializing tm_file_viewer") |
48 | ||
49 |
# Normalize the parameters
|
|
50 | ! |
if (length(label) == 0 || identical(label, "")) label <- " " |
51 | ! |
if (length(input_path) == 0 || identical(input_path, "")) input_path <- list() |
52 | ||
53 |
# Start of assertions
|
|
54 | ! |
checkmate::assert_string(label) |
55 | ||
56 | ! |
checkmate::assert( |
57 | ! |
checkmate::check_list(input_path, types = "character", min.len = 0), |
58 | ! |
checkmate::check_character(input_path, min.len = 1) |
59 |
)
|
|
60 | ! |
if (length(input_path) > 0) { |
61 | ! |
valid_url <- function(url_input, timeout = 2) { |
62 | ! |
con <- try(url(url_input), silent = TRUE) |
63 | ! |
check <- suppressWarnings(try(open.connection(con, open = "rt", timeout = timeout), silent = TRUE)[1]) |
64 | ! |
try(close.connection(con), silent = TRUE) |
65 | ! |
is.null(check) |
66 |
}
|
|
67 | ! |
idx <- vapply(input_path, function(x) file.exists(x) || valid_url(x), logical(1)) |
68 | ||
69 | ! |
if (!all(idx)) { |
70 | ! |
warning( |
71 | ! |
paste0( |
72 | ! |
"Non-existent file or url path. Please provide valid paths for:\n",
|
73 | ! |
paste0(input_path[!idx], collapse = "\n") |
74 |
)
|
|
75 |
)
|
|
76 |
}
|
|
77 | ! |
input_path <- input_path[idx] |
78 |
} else { |
|
79 | ! |
warning( |
80 | ! |
"No file or url paths were provided."
|
81 |
)
|
|
82 |
}
|
|
83 |
# End of assertions
|
|
84 | ||
85 |
# Make UI args
|
|
86 | ! |
args <- as.list(environment()) |
87 | ||
88 | ! |
ans <- module( |
89 | ! |
label = label, |
90 | ! |
server = srv_viewer, |
91 | ! |
server_args = list(input_path = input_path), |
92 | ! |
ui = ui_viewer, |
93 | ! |
ui_args = args, |
94 | ! |
datanames = NULL |
95 |
)
|
|
96 | ! |
attr(ans, "teal_bookmarkable") <- FALSE |
97 | ! |
ans
|
98 |
}
|
|
99 | ||
100 |
# UI function for the file viewer module
|
|
101 |
ui_viewer <- function(id, ...) { |
|
102 | ! |
args <- list(...) |
103 | ! |
ns <- NS(id) |
104 | ||
105 | ! |
tagList( |
106 | ! |
include_css_files("custom"), |
107 | ! |
teal.widgets::standard_layout( |
108 | ! |
output = tags$div( |
109 | ! |
uiOutput(ns("output")) |
110 |
),
|
|
111 | ! |
encoding = tags$div( |
112 | ! |
class = "file_viewer_encoding", |
113 | ! |
tags$label("Encodings", class = "text-primary"), |
114 | ! |
shinyTree::shinyTree( |
115 | ! |
ns("tree"), |
116 | ! |
dragAndDrop = FALSE, |
117 | ! |
sort = FALSE, |
118 | ! |
wholerow = TRUE, |
119 | ! |
theme = "proton", |
120 | ! |
multiple = FALSE |
121 |
)
|
|
122 |
)
|
|
123 |
)
|
|
124 |
)
|
|
125 |
}
|
|
126 | ||
127 |
# Server function for the file viewer module
|
|
128 |
srv_viewer <- function(id, input_path) { |
|
129 | ! |
moduleServer(id, function(input, output, session) { |
130 | ! |
teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general") |
131 | ||
132 | ! |
temp_dir <- tempfile() |
133 | ! |
if (!dir.exists(temp_dir)) { |
134 | ! |
dir.create(temp_dir, recursive = TRUE) |
135 |
}
|
|
136 | ! |
addResourcePath(basename(temp_dir), temp_dir) |
137 | ||
138 | ! |
test_path_text <- function(selected_path, type) { |
139 | ! |
out <- tryCatch( |
140 | ! |
expr = { |
141 | ! |
if (type != "url") { |
142 | ! |
selected_path <- normalizePath(selected_path, winslash = "/") |
143 |
}
|
|
144 | ! |
readLines(con = selected_path) |
145 |
},
|
|
146 | ! |
error = function(cond) FALSE, |
147 | ! |
warning = function(cond) { |
148 | ! |
`if`(grepl("^incomplete final line found on", cond[[1]]), suppressWarnings(eval(cond[[2]])), FALSE) |
149 |
}
|
|
150 |
)
|
|
151 |
}
|
|
152 | ||
153 | ! |
handle_connection_type <- function(selected_path) { |
154 | ! |
file_extension <- tools::file_ext(selected_path) |
155 | ! |
file_class <- suppressWarnings(file(selected_path)) |
156 | ! |
close(file_class) |
157 | ||
158 | ! |
output_text <- test_path_text(selected_path, type = class(file_class)[1]) |
159 | ||
160 | ! |
if (class(file_class)[1] == "url") { |
161 | ! |
list(selected_path = selected_path, output_text = output_text) |
162 |
} else { |
|
163 | ! |
file.copy(normalizePath(selected_path, winslash = "/"), temp_dir) |
164 | ! |
selected_path <- file.path(basename(temp_dir), basename(selected_path)) |
165 | ! |
list(selected_path = selected_path, output_text = output_text) |
166 |
}
|
|
167 |
}
|
|
168 | ||
169 | ! |
display_file <- function(selected_path) { |
170 | ! |
con_type <- handle_connection_type(selected_path) |
171 | ! |
file_extension <- tools::file_ext(selected_path) |
172 | ! |
if (file_extension %in% c("png", "apng", "jpg", "jpeg", "svg", "gif", "webp", "bmp")) { |
173 | ! |
tags$img(src = con_type$selected_path, alt = "file does not exist") |
174 | ! |
} else if (file_extension == "pdf") { |
175 | ! |
tags$embed( |
176 | ! |
class = "embed_pdf", |
177 | ! |
src = con_type$selected_path |
178 |
)
|
|
179 | ! |
} else if (!isFALSE(con_type$output_text[1])) { |
180 | ! |
tags$pre(paste0(con_type$output_text, collapse = "\n")) |
181 |
} else { |
|
182 | ! |
tags$p("Please select a supported format.") |
183 |
}
|
|
184 |
}
|
|
185 | ||
186 | ! |
tree_list <- function(file_or_dir) { |
187 | ! |
nested_list <- lapply(file_or_dir, function(path) { |
188 | ! |
file_class <- suppressWarnings(file(path)) |
189 | ! |
close(file_class) |
190 | ! |
if (class(file_class)[[1]] != "url") { |
191 | ! |
isdir <- file.info(path)$isdir |
192 | ! |
if (!isdir) { |
193 | ! |
structure(path, ancestry = path, sticon = "file") |
194 |
} else { |
|
195 | ! |
files <- list.files(path, full.names = TRUE, include.dirs = TRUE) |
196 | ! |
out <- lapply(files, function(x) tree_list(x)) |
197 | ! |
out <- unlist(out, recursive = FALSE) |
198 | ! |
if (length(files) > 0) names(out) <- basename(files) |
199 | ! |
out
|
200 |
}
|
|
201 |
} else { |
|
202 | ! |
structure(path, ancestry = path, sticon = "file") |
203 |
}
|
|
204 |
}) |
|
205 | ||
206 | ! |
missing_labels <- if (is.null(names(nested_list))) seq_along(nested_list) else which(names(nested_list) == "") |
207 | ! |
names(nested_list)[missing_labels] <- file_or_dir[missing_labels] |
208 | ! |
nested_list
|
209 |
}
|
|
210 | ||
211 | ! |
output$tree <- shinyTree::renderTree({ |
212 | ! |
if (length(input_path) > 0) { |
213 | ! |
tree_list(input_path) |
214 |
} else { |
|
215 | ! |
list("Empty Path" = NULL) |
216 |
}
|
|
217 |
}) |
|
218 | ||
219 | ! |
output$output <- renderUI({ |
220 | ! |
validate( |
221 | ! |
need( |
222 | ! |
length(shinyTree::get_selected(input$tree)) > 0, |
223 | ! |
"Please select a file."
|
224 |
)
|
|
225 |
)
|
|
226 | ||
227 | ! |
obj <- shinyTree::get_selected(input$tree, format = "names")[[1]] |
228 | ! |
repo <- attr(obj, "ancestry") |
229 | ! |
repo_collapsed <- if (length(repo) > 1) paste0(repo, collapse = "/") else repo |
230 | ! |
is_not_named <- file.exists(file.path(c(repo_collapsed, obj[1])))[1] |
231 | ||
232 | ! |
if (is_not_named) { |
233 | ! |
selected_path <- do.call("file.path", as.list(c(repo, obj[1]))) |
234 |
} else { |
|
235 | ! |
if (length(repo) == 0) { |
236 | ! |
selected_path <- do.call("file.path", as.list(attr(input$tree[[obj[1]]], "ancestry"))) |
237 |
} else { |
|
238 | ! |
selected_path <- do.call("file.path", as.list(attr(input$tree[[repo]][[obj[1]]], "ancestry"))) |
239 |
}
|
|
240 |
}
|
|
241 | ||
242 | ! |
validate( |
243 | ! |
need( |
244 | ! |
!isTRUE(file.info(selected_path)$isdir) && length(selected_path) > 0, |
245 | ! |
"Please select a single file."
|
246 |
)
|
|
247 |
)
|
|
248 | ! |
display_file(selected_path) |
249 |
}) |
|
250 | ||
251 | ! |
onStop(function() { |
252 | ! |
removeResourcePath(basename(temp_dir)) |
253 | ! |
unlink(temp_dir) |
254 |
}) |
|
255 |
}) |
|
256 |
}
|
1 |
.onLoad <- function(libname, pkgname) { |
|
2 | ! |
teal.logger::register_logger(namespace = "teal.modules.general") |
3 | ! |
teal.logger::register_handlers("teal.modules.general") |
4 |
}
|
|
5 | ||
6 |
### global variables
|
|
7 |
ggplot_themes <- c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void") |
|
8 | ||
9 |
#' @importFrom lifecycle deprecated
|
|
10 |
interactive <- NULL |