Movatterモバイル変換


[0]ホーム

URL:


teal.modules.general coverage - 4.84%

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
#' @param remove_zero_columns (`logical(1)`)
23
#' Indicates whether to remove columns that contain only zeros from the output table.
24
#' Defaults to `FALSE`.
25
#'
26
#' @note For more examples, please see the vignette "Using cross table" via
27
#' `vignette("using-cross-table", package = "teal.modules.general")`.
28
#'
29
#' @inherit shared_params return
30
#'
31
#' @section Table Settings:
32
#' The module provides several table settings that can be adjusted:
33
#' \itemize{
34
#'   \item \code{Show column percentage}: Shows column percentages when enabled
35
#'   \item \code{Show total column}: Shows a total column when enabled
36
#'   \item \code{Remove zero-only columns}: Removes columns that contain only zeros from the output table
37
#' }
38
#'
39
#' @section Decorating Module:
40
#'
41
#' This module generates the following objects, which can be modified in place using decorators:
42
#' - `table` (`ElementaryTable` - output of `rtables::build_table`)
43
#'
44
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
45
#' The name of this list corresponds to the name of the output to which the decorator is applied.
46
#' See code snippet below:
47
#'
48
#' ```
49
#' tm_t_crosstable(
50
#'    ..., # arguments for module
51
#'    decorators = list(
52
#'      table = teal_transform_module(...) # applied to the `table` output
53
#'    )
54
#' )
55
#' ```
56
#' For additional details and examples of decorators, refer to the vignette
57
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
58
#'
59
#' To learn more please refer to the vignette
60
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
61
#'
62
#' @inheritSection teal::example_module Reporting
63
#'
64
#' @examplesShinylive
65
#' library(teal.modules.general)
66
#' interactive <- function() TRUE
67
#' {{ next_example }}
68
#' @examples
69
#' # general data example
70
#' data <- teal_data()
71
#' data <- within(data, {
72
#'   mtcars <- mtcars
73
#'   for (v in c("cyl", "vs", "am", "gear")) {
74
#'     mtcars[[v]] <- as.factor(mtcars[[v]])
75
#'   }
76
#'   mtcars[["primary_key"]] <- seq_len(nrow(mtcars))
77
#' })
78
#' join_keys(data) <- join_keys(join_key("mtcars", "mtcars", "primary_key"))
79
#'
80
#' app <- init(
81
#'   data = data,
82
#'   modules = modules(
83
#'     tm_t_crosstable(
84
#'       label = "Cross Table",
85
#'       x = data_extract_spec(
86
#'         dataname = "mtcars",
87
#'         select = select_spec(
88
#'           label = "Select variable:",
89
#'           choices = variable_choices(data[["mtcars"]], c("cyl", "vs", "am", "gear")),
90
#'           selected = c("cyl", "gear"),
91
#'           multiple = TRUE,
92
#'           ordered = TRUE,
93
#'           fixed = FALSE
94
#'         )
95
#'       ),
96
#'       y = data_extract_spec(
97
#'         dataname = "mtcars",
98
#'         select = select_spec(
99
#'           label = "Select variable:",
100
#'           choices = variable_choices(data[["mtcars"]], c("cyl", "vs", "am", "gear")),
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
#'   ADSL <- teal.data::rADSL
122
#' })
123
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
124
#'
125
#' app <- init(
126
#'   data = data,
127
#'   modules = modules(
128
#'     tm_t_crosstable(
129
#'       label = "Cross Table",
130
#'       x = data_extract_spec(
131
#'         dataname = "ADSL",
132
#'         select = select_spec(
133
#'           label = "Select variable:",
134
#'           choices = variable_choices(data[["ADSL"]], subset = function(data) {
135
#'             idx <- !vapply(data, inherits, logical(1), c("Date", "POSIXct", "POSIXlt"))
136
#'             return(names(data)[idx])
137
#'           }),
138
#'           selected = "COUNTRY",
139
#'           multiple = TRUE,
140
#'           ordered = TRUE,
141
#'           fixed = FALSE
142
#'         )
143
#'       ),
144
#'       y = data_extract_spec(
145
#'         dataname = "ADSL",
146
#'         select = select_spec(
147
#'           label = "Select variable:",
148
#'           choices = variable_choices(data[["ADSL"]], subset = function(data) {
149
#'             idx <- vapply(data, is.factor, logical(1))
150
#'             return(names(data)[idx])
151
#'           }),
152
#'           selected = "SEX",
153
#'           multiple = FALSE,
154
#'           fixed = FALSE
155
#'         )
156
#'       )
157
#'     )
158
#'   )
159
#' )
160
#' if (interactive()) {
161
#'   shinyApp(app$ui, app$server)
162
#' }
163
#'
164
#' @export
165
#'
166
tm_t_crosstable <- function(label = "Cross Table",
167
                            x,
168
                            y,
169
                            show_percentage = TRUE,
170
                            show_total = TRUE,
171
                            remove_zero_columns = FALSE,
172
                            pre_output = NULL,
173
                            post_output = NULL,
174
                            basic_table_args = teal.widgets::basic_table_args(),
175
                            transformators = list(),
176
                            decorators = list()) {
177!
  message("Initializing tm_t_crosstable")
178
179
  # Normalize the parameters
180!
  if (inherits(x, "data_extract_spec")) x <- list(x)
181!
  if (inherits(y, "data_extract_spec")) y <- list(y)
182
183
  # Start of assertions
184!
  checkmate::assert_string(label)
185!
  checkmate::assert_list(x, types = "data_extract_spec")
186
187!
  checkmate::assert_list(y, types = "data_extract_spec")
188!
  assert_single_selection(y)
189
190!
  checkmate::assert_flag(show_percentage)
191!
  checkmate::assert_flag(show_total)
192!
  checkmate::assert_flag(remove_zero_columns)
193!
  checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
194!
  checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
195!
  checkmate::assert_class(basic_table_args, classes = "basic_table_args")
196
197!
  assert_decorators(decorators, "table")
198
  # End of assertions
199
200
  # Make UI args
201!
  ui_args <- as.list(environment())
202
203!
  server_args <- list(
204!
    label = label,
205!
    x = x,
206!
    y = y,
207!
    remove_zero_columns = remove_zero_columns,
208!
    basic_table_args = basic_table_args,
209!
    decorators = decorators
210
  )
211
212!
  ans <- module(
213!
    label = label,
214!
    server = srv_t_crosstable,
215!
    ui = ui_t_crosstable,
216!
    ui_args = ui_args,
217!
    server_args = server_args,
218!
    transformators = transformators,
219!
    datanames = teal.transform::get_extract_datanames(list(x = x, y = y))
220
  )
221!
  attr(ans, "teal_bookmarkable") <- TRUE
222!
  ans
223
}
224
225
# UI function for the cross-table module
226
ui_t_crosstable <- function(id, x, y, show_percentage, show_total, remove_zero_columns, pre_output, post_output, ...) {
227!
  args <- list(...)
228!
  ns <- NS(id)
229!
  is_single_dataset <- teal.transform::is_single_dataset(x, y)
230
231!
  join_default_options <- c(
232!
    "Full Join" = "dplyr::full_join",
233!
    "Inner Join" = "dplyr::inner_join",
234!
    "Left Join" = "dplyr::left_join",
235!
    "Right Join" = "dplyr::right_join"
236
  )
237
238!
  teal.widgets::standard_layout(
239!
    output = teal.widgets::white_small_well(
240!
      textOutput(ns("title")),
241!
      teal.widgets::table_with_settings_ui(ns("table"))
242
    ),
243!
    encoding = tags$div(
244!
      tags$label("Encodings", class = "text-primary"),
245!
      teal.transform::datanames_input(list(x, y)),
246!
      teal.transform::data_extract_ui(ns("x"), label = "Row values", x, is_single_dataset = is_single_dataset),
247!
      teal.transform::data_extract_ui(ns("y"), label = "Column values", y, is_single_dataset = is_single_dataset),
248!
      teal.widgets::optionalSelectInput(
249!
        ns("join_fun"),
250!
        label = "Row to Column type of join",
251!
        choices = join_default_options,
252!
        selected = join_default_options[1],
253!
        multiple = FALSE
254
      ),
255!
      tags$hr(),
256!
      bslib::accordion(
257!
        open = TRUE,
258!
        bslib::accordion_panel(
259!
          title = "Table settings",
260!
          checkboxInput(ns("show_percentage"), "Show column percentage", value = show_percentage),
261!
          checkboxInput(ns("show_total"), "Show total column", value = show_total),
262!
          checkboxInput(ns("remove_zero_columns"), "Remove zero-only columns", value = remove_zero_columns)
263
        )
264
      ),
265!
      ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "table"))
266
    ),
267!
    pre_output = pre_output,
268!
    post_output = post_output
269
  )
270
}
271
272
# Server function for the cross-table module
273
srv_t_crosstable <- function(id, data, label, x, y, remove_zero_columns, basic_table_args, decorators) {
274!
  checkmate::assert_class(data, "reactive")
275!
  checkmate::assert_class(isolate(data()), "teal_data")
276!
  moduleServer(id, function(input, output, session) {
277!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
278
279!
    selector_list <- teal.transform::data_extract_multiple_srv(
280!
      data_extract = list(x = x, y = y),
281!
      datasets = data,
282!
      select_validation_rule = list(
283!
        x = shinyvalidate::sv_required("Please define column for row variable."),
284!
        y = shinyvalidate::sv_required("Please define column for column variable.")
285
      )
286
    )
287
288!
    iv_r <- reactive({
289!
      iv <- shinyvalidate::InputValidator$new()
290!
      iv$add_rule("join_fun", function(value) {
291!
        if (!identical(selector_list()$x()$dataname, selector_list()$y()$dataname)) {
292!
          if (!shinyvalidate::input_provided(value)) {
293!
            "Please select a joining function."
294
          }
295
        }
296
      })
297!
      teal.transform::compose_and_enable_validators(iv, selector_list)
298
    })
299
300!
    observeEvent(
301!
      eventExpr = {
302!
        req(!is.null(selector_list()$x()) && !is.null(selector_list()$y()))
303!
        list(selector_list()$x(), selector_list()$y())
304
      },
305!
      handlerExpr = {
306!
        if (identical(selector_list()$x()$dataname, selector_list()$y()$dataname)) {
307!
          shinyjs::hide("join_fun")
308
        } else {
309!
          shinyjs::show("join_fun")
310
        }
311
      }
312
    )
313
314!
    merge_function <- reactive({
315!
      if (is.null(input$join_fun)) {
316!
        "dplyr::full_join"
317
      } else {
318!
        input$join_fun
319
      }
320
    })
321
322!
    anl_merged_input <- teal.transform::merge_expression_srv(
323!
      datasets = data,
324!
      selector_list = selector_list,
325!
      merge_function = merge_function
326
    )
327!
    qenv <- reactive({
328!
      obj <- data()
329!
      teal.reporter::teal_card(obj) <-
330!
        c(
331!
          teal.reporter::teal_card(obj),
332!
          teal.reporter::teal_card("## Module's output(s)")
333
        )
334!
      teal.code::eval_code(obj, "library(rtables);library(tern);library(dplyr)")
335
    })
336!
    anl_merged_q <- reactive({
337!
      req(anl_merged_input())
338!
      qenv() %>%
339!
        teal.code::eval_code(as.expression(anl_merged_input()$expr))
340
    })
341
342!
    merged <- list(
343!
      anl_input_r = anl_merged_input,
344!
      anl_q_r = anl_merged_q
345
    )
346
347!
    output_q <- reactive({
348!
      teal::validate_inputs(iv_r())
349!
      ANL <- merged$anl_q_r()[["ANL"]]
350
351
      # As this is a summary
352!
      x_name <- as.vector(merged$anl_input_r()$columns_source$x)
353!
      y_name <- as.vector(merged$anl_input_r()$columns_source$y)
354
355!
      teal::validate_has_data(ANL, 3)
356!
      teal::validate_has_data(ANL[, c(x_name, y_name)], 3, complete = TRUE, allow_inf = FALSE)
357
358!
      is_allowed_class <- function(x) is.numeric(x) || is.factor(x) || is.character(x) || is.logical(x)
359!
      validate(need(
360!
        all(vapply(ANL[x_name], is_allowed_class, logical(1))),
361!
        "Selected row variable has an unsupported data type."
362
      ))
363!
      validate(need(
364!
        is_allowed_class(ANL[[y_name]]),
365!
        "Selected column variable has an unsupported data type."
366
      ))
367
368!
      show_percentage <- input$show_percentage
369!
      show_total <- input$show_total
370!
      remove_zero_columns <- input$remove_zero_columns
371
372!
      plot_title <- paste(
373!
        "Cross-Table of",
374!
        paste0(varname_w_label(x_name, ANL), collapse = ", "),
375!
        "(rows)", "vs.",
376!
        varname_w_label(y_name, ANL),
377!
        "(columns)"
378
      )
379
380!
      labels_vec <- vapply(
381!
        x_name,
382!
        varname_w_label,
383!
        character(1),
384!
        ANL
385
      )
386
387!
      obj <- merged$anl_q_r()
388!
      teal.reporter::teal_card(obj) <- c(teal.reporter::teal_card(obj), "### Table")
389!
      obj <- teal.code::eval_code(
390!
        obj,
391!
        substitute(
392!
          expr = {
393!
            title <- plot_title
394
          },
395!
          env = list(plot_title = plot_title)
396
        )
397
      ) %>%
398!
        teal.code::eval_code(
399!
          substitute(
400!
            expr = {
401!
              table <- basic_tables %>%
402!
                split_call %>% # styler: off
403!
                rtables::add_colcounts() %>%
404!
                tern::analyze_vars(
405!
                  vars = x_name,
406!
                  var_labels = labels_vec,
407!
                  na.rm = FALSE,
408!
                  denom = "N_col",
409!
                  .stats = c("mean_sd", "median", "range", count_value)
410
                )
411
            },
412!
            env = list(
413!
              basic_tables = teal.widgets::parse_basic_table_args(
414!
                basic_table_args = teal.widgets::resolve_basic_table_args(basic_table_args)
415
              ),
416!
              split_call = if (show_total) {
417!
                substitute(
418!
                  expr = rtables::split_cols_by(
419!
                    y_name,
420!
                    split_fun = rtables::add_overall_level(label = "Total", first = FALSE)
421
                  ),
422!
                  env = list(y_name = y_name)
423
                )
424
              } else {
425!
                substitute(rtables::split_cols_by(y_name), env = list(y_name = y_name))
426
              },
427!
              x_name = x_name,
428!
              labels_vec = labels_vec,
429!
              count_value = ifelse(show_percentage, "count_fraction", "count")
430
            )
431
          )
432
        ) %>%
433!
        teal.code::eval_code(
434!
          expression(ANL <- tern::df_explicit_na(ANL))
435
        )
436
437!
      if (remove_zero_columns) {
438!
        obj <- obj %>%
439!
          teal.code::eval_code(
440!
            substitute(
441!
              expr = {
442!
                ANL[[y_name]] <- droplevels(ANL[[y_name]])
443!
                table <- rtables::build_table(lyt = table, df = ANL[order(ANL[[y_name]]), ])
444
              },
445!
              env = list(y_name = y_name)
446
            )
447
          )
448
      } else {
449!
        obj <- obj %>%
450!
          teal.code::eval_code(
451!
            substitute(
452!
              expr = {
453!
                table <- rtables::build_table(lyt = table, df = ANL[order(ANL[[y_name]]), ])
454
              },
455!
              env = list(y_name = y_name)
456
            )
457
          )
458
      }
459!
      obj
460
    })
461
462!
    decorated_output_q <- srv_decorate_teal_data(
463!
      id = "decorator",
464!
      data = output_q,
465!
      decorators = select_decorators(decorators, "table"),
466!
      expr = quote(table)
467
    )
468
469!
    output$title <- renderText(req(decorated_output_q())[["title"]])
470
471!
    table_r <- reactive({
472!
      req(iv_r()$is_valid())
473!
      req(decorated_output_q())[["table"]]
474
    })
475
476!
    teal.widgets::table_with_settings_srv(
477!
      id = "table",
478!
      table_r = table_r
479
    )
480
481!
    decorated_output_q
482
  })
483
}
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
#'
35
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
36
#' The name of this list corresponds to the name of the output to which the decorator is applied.
37
#' See code snippet below:
38
#'
39
#' ```
40
#' tm_g_distribution(
41
#'    ..., # arguments for module
42
#'    decorators = list(
43
#'      histogram_plot = teal_transform_module(...), # applied only to `histogram_plot` output
44
#'      qq_plot = teal_transform_module(...) # applied only to `qq_plot` output
45
#'    )
46
#' )
47
#' ```
48
#'
49
#' For additional details and examples of decorators, refer to the vignette
50
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
51
#'
52
#' To learn more please refer to the vignette
53
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
54
#'
55
#' @inheritSection teal::example_module Reporting
56
#'
57
#' @examplesShinylive
58
#' library(teal.modules.general)
59
#' interactive <- function() TRUE
60
#' {{ next_example }}
61
# nolint start: line_length_linter.
62
#' @examples
63
# nolint end: line_length_linter.
64
#' # general data example
65
#' data <- teal_data()
66
#' data <- within(data, {
67
#'   iris <- iris
68
#' })
69
#'
70
#' app <- init(
71
#'   data = data,
72
#'   modules = list(
73
#'     tm_g_distribution(
74
#'       dist_var = data_extract_spec(
75
#'         dataname = "iris",
76
#'         select = select_spec(variable_choices("iris"), "Petal.Length")
77
#'       )
78
#'     )
79
#'   )
80
#' )
81
#' if (interactive()) {
82
#'   shinyApp(app$ui, app$server)
83
#' }
84
#'
85
#' @examplesShinylive
86
#' library(teal.modules.general)
87
#' interactive <- function() TRUE
88
#' {{ next_example }}
89
# nolint start: line_length_linter.
90
#' @examples
91
# nolint end: line_length_linter.
92
#' # CDISC data example
93
#' data <- teal_data()
94
#' data <- within(data, {
95
#'   ADSL <- teal.data::rADSL
96
#' })
97
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
98
#'
99
#'
100
#' app <- init(
101
#'   data = data,
102
#'   modules = modules(
103
#'     tm_g_distribution(
104
#'       dist_var = data_extract_spec(
105
#'         dataname = "ADSL",
106
#'         select = select_spec(
107
#'           choices = variable_choices("ADSL", c("AGE", "BMRKR1")),
108
#'           selected = "BMRKR1",
109
#'           multiple = FALSE,
110
#'           fixed = FALSE
111
#'         )
112
#'       ),
113
#'       strata_var = data_extract_spec(
114
#'         dataname = "ADSL",
115
#'         filter = filter_spec(
116
#'           vars = choices_selected(
117
#'             variable_choices("ADSL", c("ARM", "COUNTRY", "SEX")),
118
#'             selected = NULL
119
#'           ),
120
#'           multiple = TRUE
121
#'         )
122
#'       ),
123
#'       group_var = data_extract_spec(
124
#'         dataname = "ADSL",
125
#'         filter = filter_spec(
126
#'           vars = choices_selected(
127
#'             variable_choices("ADSL", c("ARM", "COUNTRY", "SEX")),
128
#'             selected = "ARM"
129
#'           ),
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!
  assert_decorators(decorators, names = c("histogram_plot", "qq_plot"))
198
199
  # End of assertions
200
201
  # Make UI args
202!
  args <- as.list(environment())
203
204!
  data_extract_list <- list(
205!
    dist_var = dist_var,
206!
    strata_var = strata_var,
207!
    group_var = group_var
208
  )
209
210!
  ans <- module(
211!
    label = label,
212!
    server = srv_distribution,
213!
    server_args = c(
214!
      data_extract_list,
215!
      list(
216!
        plot_height = plot_height,
217!
        plot_width = plot_width,
218!
        ggplot2_args = ggplot2_args,
219!
        decorators = decorators
220
      )
221
    ),
222!
    ui = ui_distribution,
223!
    ui_args = args,
224!
    transformators = transformators,
225!
    datanames = teal.transform::get_extract_datanames(data_extract_list)
226
  )
227!
  attr(ans, "teal_bookmarkable") <- TRUE
228!
  ans
229
}
230
231
# UI function for the distribution module
232
ui_distribution <- function(id, ...) {
233!
  args <- list(...)
234!
  ns <- NS(id)
235!
  is_single_dataset_value <- teal.transform::is_single_dataset(args$dist_var, args$strata_var, args$group_var)
236
237!
  teal.widgets::standard_layout(
238!
    output = teal.widgets::white_small_well(
239!
      tabsetPanel(
240!
        id = ns("tabs"),
241!
        tabPanel("Histogram", teal.widgets::plot_with_settings_ui(id = ns("hist_plot"))),
242!
        tabPanel("QQplot", teal.widgets::plot_with_settings_ui(id = ns("qq_plot")))
243
      ),
244!
      tags$h3("Statistics Table"),
245!
      DT::dataTableOutput(ns("summary_table")),
246!
      tags$h3("Tests"),
247!
      conditionalPanel(
248!
        sprintf("input['%s'].length === 0", ns("dist_tests")),
249!
        div(
250!
          id = ns("please_select_a_test"),
251!
          "Please select a test"
252
        )
253
      ),
254!
      conditionalPanel(
255!
        sprintf("input['%s'].length > 0", ns("dist_tests")),
256!
        DT::dataTableOutput(ns("t_stats"))
257
      )
258
    ),
259!
    encoding = tags$div(
260!
      tags$label("Encodings", class = "text-primary"),
261!
      teal.transform::datanames_input(args[c("dist_var", "strata_var")]),
262!
      teal.transform::data_extract_ui(
263!
        id = ns("dist_i"),
264!
        label = "Variable",
265!
        data_extract_spec = args$dist_var,
266!
        is_single_dataset = is_single_dataset_value
267
      ),
268!
      if (!is.null(args$group_var)) {
269!
        tagList(
270!
          teal.transform::data_extract_ui(
271!
            id = ns("group_i"),
272!
            label = "Group by",
273!
            data_extract_spec = args$group_var,
274!
            is_single_dataset = is_single_dataset_value
275
          ),
276!
          uiOutput(ns("scales_types_ui"))
277
        )
278
      },
279!
      if (!is.null(args$strata_var)) {
280!
        teal.transform::data_extract_ui(
281!
          id = ns("strata_i"),
282!
          label = "Stratify by",
283!
          data_extract_spec = args$strata_var,
284!
          is_single_dataset = is_single_dataset_value
285
        )
286
      },
287!
      bslib::accordion(
288!
        conditionalPanel(
289!
          condition = paste0("input['", ns("tabs"), "'] == 'Histogram'"),
290!
          bslib::accordion_panel(
291!
            "Histogram",
292!
            teal.widgets::optionalSliderInputValMinMax(ns("bins"), "Bins", args$bins, ticks = FALSE, step = 1),
293!
            shinyWidgets::prettyRadioButtons(
294!
              ns("main_type"),
295!
              label = "Plot Type:",
296!
              choices = c("Density", "Frequency"),
297!
              selected = if (!args$freq) "Density" else "Frequency",
298!
              bigger = FALSE,
299!
              inline = TRUE
300
            ),
301!
            checkboxInput(ns("add_dens"), label = "Overlay Density", value = TRUE),
302!
            ui_decorate_teal_data(
303!
              ns("d_density"),
304!
              decorators = select_decorators(args$decorators, "histogram_plot")
305
            )
306
          )
307
        ),
308!
        conditionalPanel(
309!
          condition = paste0("input['", ns("tabs"), "'] == 'QQplot'"),
310!
          bslib::accordion_panel(
311!
            "QQ Plot",
312!
            checkboxInput(ns("qq_line"), label = "Add diagonal line(s)", TRUE),
313!
            ui_decorate_teal_data(
314!
              ns("d_qq"),
315!
              decorators = select_decorators(args$decorators, "qq_plot")
316
            ),
317!
            collapsed = FALSE
318
          )
319
        ),
320!
        conditionalPanel(
321!
          condition = paste0("input['", ns("main_type"), "'] == 'Density'"),
322!
          bslib::accordion_panel(
323!
            "Theoretical Distribution",
324!
            teal.widgets::optionalSelectInput(
325!
              ns("t_dist"),
326!
              tags$div(
327!
                tagList(
328!
                  "Distribution:",
329!
                  bslib::tooltip(
330!
                    icon("circle-info"),
331!
                    tags$span(
332!
                      "Default parameters are optimized with MASS::fitdistr function."
333
                    )
334
                  )
335
                )
336
              ),
337!
              choices = c("normal", "lognormal", "gamma", "unif"),
338!
              selected = NULL,
339!
              multiple = FALSE
340
            ),
341!
            numericInput(ns("dist_param1"), label = "param1", value = NULL),
342!
            numericInput(ns("dist_param2"), label = "param2", value = NULL),
343!
            tags$span(actionButton(ns("params_reset"), "Default params")),
344!
            collapsed = FALSE
345
          )
346
        ),
347!
        bslib::accordion_panel(
348!
          title = "Tests",
349!
          teal.widgets::optionalSelectInput(
350!
            ns("dist_tests"),
351!
            "Tests:",
352!
            choices = c(
353!
              "Shapiro-Wilk",
354!
              if (!is.null(args$strata_var)) "t-test (two-samples, not paired)",
355!
              if (!is.null(args$strata_var)) "one-way ANOVA",
356!
              if (!is.null(args$strata_var)) "Fligner-Killeen",
357!
              if (!is.null(args$strata_var)) "F-test",
358!
              "Kolmogorov-Smirnov (one-sample)",
359!
              "Anderson-Darling (one-sample)",
360!
              "Cramer-von Mises (one-sample)",
361!
              if (!is.null(args$strata_var)) "Kolmogorov-Smirnov (two-samples)"
362
            ),
363!
            selected = NULL
364
          )
365
        ),
366!
        bslib::accordion_panel(
367!
          title = "Statistics Table",
368!
          sliderInput(ns("roundn"), "Round to n digits", min = 0, max = 10, value = 2)
369
        ),
370!
        bslib::accordion_panel(
371!
          title = "Plot settings",
372!
          selectInput(
373!
            inputId = ns("ggtheme"),
374!
            label = "Theme (by ggplot):",
375!
            choices = ggplot_themes,
376!
            selected = args$ggtheme,
377!
            multiple = FALSE
378
          )
379
        )
380
      )
381
    ),
382!
    pre_output = args$pre_output,
383!
    post_output = args$post_output
384
  )
385
}
386
387
# Server function for the distribution module
388
srv_distribution <- function(id,
389
                             data,
390
                             dist_var,
391
                             strata_var,
392
                             group_var,
393
                             plot_height,
394
                             plot_width,
395
                             ggplot2_args,
396
                             decorators) {
397!
  checkmate::assert_class(data, "reactive")
398!
  checkmate::assert_class(isolate(data()), "teal_data")
399!
  moduleServer(id, function(input, output, session) {
400!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
401
402!
    setBookmarkExclude("params_reset")
403
404!
    ns <- session$ns
405
406!
    rule_req <- function(value) {
407!
      if (isTRUE(input$dist_tests %in% c(
408!
        "Fligner-Killeen",
409!
        "t-test (two-samples, not paired)",
410!
        "F-test",
411!
        "Kolmogorov-Smirnov (two-samples)",
412!
        "one-way ANOVA"
413
      ))) {
414!
        if (!shinyvalidate::input_provided(value)) {
415!
          "Please select stratify variable."
416
        }
417
      }
418
    }
419!
    rule_dupl <- function(...) {
420!
      if (identical(input$dist_tests, "Fligner-Killeen")) {
421!
        strata <- selector_list()$strata_i()$select
422!
        group <- selector_list()$group_i()$select
423!
        if (isTRUE(strata == group)) {
424!
          "Please select different variables for strata and group."
425
        }
426
      }
427
    }
428
429!
    selector_list <- teal.transform::data_extract_multiple_srv(
430!
      data_extract = list(
431!
        dist_i = dist_var,
432!
        strata_i = strata_var,
433!
        group_i = group_var
434
      ),
435!
      data,
436!
      select_validation_rule = list(
437!
        dist_i = shinyvalidate::sv_required("Please select a variable")
438
      ),
439!
      filter_validation_rule = list(
440!
        strata_i = shinyvalidate::compose_rules(
441!
          rule_req,
442!
          rule_dupl
443
        ),
444!
        group_i = rule_dupl
445
      )
446
    )
447
448!
    iv_r <- reactive({
449!
      iv <- shinyvalidate::InputValidator$new()
450!
      teal.transform::compose_and_enable_validators(iv, selector_list, validator_names = "dist_i")
451
    })
452
453!
    iv_r_dist <- reactive({
454!
      iv <- shinyvalidate::InputValidator$new()
455!
      teal.transform::compose_and_enable_validators(
456!
        iv, selector_list,
457!
        validator_names = c("strata_i", "group_i")
458
      )
459
    })
460!
    rule_dist_1 <- function(value) {
461!
      if (!is.null(input$t_dist)) {
462!
        switch(input$t_dist,
463!
          "normal" = if (!shinyvalidate::input_provided(value)) "mean is required",
464!
          "lognormal" = if (!shinyvalidate::input_provided(value)) "meanlog is required",
465!
          "gamma" = {
466!
            if (!shinyvalidate::input_provided(value)) "shape is required" else if (value <= 0) "shape must be positive"
467
          },
468!
          "unif" = NULL
469
        )
470
      }
471
    }
472!
    rule_dist_2 <- function(value) {
473!
      if (!is.null(input$t_dist)) {
474!
        switch(input$t_dist,
475!
          "normal" = {
476!
            if (!shinyvalidate::input_provided(value)) {
477!
              "sd is required"
478!
            } else if (value < 0) {
479!
              "sd must be non-negative"
480
            }
481
          },
482!
          "lognormal" = {
483!
            if (!shinyvalidate::input_provided(value)) {
484!
              "sdlog is required"
485!
            } else if (value < 0) {
486!
              "sdlog must be non-negative"
487
            }
488
          },
489!
          "gamma" = {
490!
            if (!shinyvalidate::input_provided(value)) {
491!
              "rate is required"
492!
            } else if (value <= 0) {
493!
              "rate must be positive"
494
            }
495
          },
496!
          "unif" = NULL
497
        )
498
      }
499
    }
500
501!
    rule_dist <- function(value) {
502!
      if (isTRUE(input$tabs == "QQplot") ||
503!
        isTRUE(input$dist_tests %in% c(
504!
          "Kolmogorov-Smirnov (one-sample)",
505!
          "Anderson-Darling (one-sample)",
506!
          "Cramer-von Mises (one-sample)"
507
        ))) {
508!
        if (!shinyvalidate::input_provided(value)) {
509!
          "Please select the theoretical distribution."
510
        }
511
      }
512
    }
513
514!
    iv_dist <- shinyvalidate::InputValidator$new()
515!
    iv_dist$add_rule("t_dist", rule_dist)
516!
    iv_dist$add_rule("dist_param1", rule_dist_1)
517!
    iv_dist$add_rule("dist_param2", rule_dist_2)
518!
    iv_dist$enable()
519
520!
    anl_merged_input <- teal.transform::merge_expression_srv(
521!
      selector_list = selector_list,
522!
      datasets = data
523
    )
524
525!
    qenv <- reactive(
526!
      teal.code::eval_code(data(), "library(ggplot2);library(dplyr)")
527
    )
528
529!
    anl_merged_q <- reactive({
530!
      req(anl_merged_input())
531!
      qenv() %>%
532!
        teal.code::eval_code(as.expression(anl_merged_input()$expr))
533
    })
534
535!
    merged <- list(
536!
      anl_input_r = anl_merged_input,
537!
      anl_q_r = anl_merged_q
538
    )
539
540!
    output$scales_types_ui <- renderUI({
541!
      if ("group_i" %in% names(selector_list()) && length(selector_list()$group_i()$filters[[1]]$selected) > 0) {
542!
        shinyWidgets::prettyRadioButtons(
543!
          ns("scales_type"),
544!
          label = "Scales:",
545!
          choices = c("Fixed", "Free"),
546!
          selected = "Fixed",
547!
          bigger = FALSE,
548!
          inline = TRUE
549
        )
550
      }
551
    })
552
553!
    observeEvent(
554!
      eventExpr = list(
555!
        input$t_dist,
556!
        input$params_reset,
557!
        selector_list()$dist_i()$select
558
      ),
559!
      handlerExpr = {
560!
        params <-
561!
          if (length(input$t_dist) != 0) {
562!
            get_dist_params <- function(x, dist) {
563!
              if (dist == "unif") {
564!
                return(stats::setNames(range(x, na.rm = TRUE), c("min", "max")))
565
              }
566!
              tryCatch(
567!
                MASS::fitdistr(x, densfun = dist)$estimate,
568!
                error = function(e) c(param1 = NA_real_, param2 = NA_real_)
569
              )
570
            }
571
572!
            ANL <- merged$anl_q_r()[["ANL"]]
573!
            round(get_dist_params(as.numeric(stats::na.omit(ANL[[merge_vars()$dist_var]])), input$t_dist), 2)
574
          } else {
575!
            c("param1" = NA_real_, "param2" = NA_real_)
576
          }
577
578!
        params_vals <- unname(params)
579!
        map_distr_nams <- list(
580!
          normal = c("mean", "sd"),
581!
          lognormal = c("meanlog", "sdlog"),
582!
          gamma = c("shape", "rate"),
583!
          unif = c("min", "max")
584
        )
585
586!
        if (!is.null(input$t_dist) && input$t_dist %in% names(map_distr_nams)) {
587!
          params_names <- map_distr_nams[[input$t_dist]]
588
        } else {
589!
          params_names <- names(params)
590
        }
591
592!
        updateNumericInput(
593!
          inputId = "dist_param1",
594!
          label = params_names[1],
595!
          value = restoreInput(ns("dist_param1"), params_vals[1])
596
        )
597!
        updateNumericInput(
598!
          inputId = "dist_param2",
599!
          label = params_names[2],
600!
          value = restoreInput(ns("dist_param1"), params_vals[2])
601
        )
602
      },
603!
      ignoreInit = TRUE
604
    )
605
606!
    observeEvent(input$params_reset, {
607!
      updateActionButton(inputId = "params_reset", label = "Reset params")
608
    })
609
610!
    merge_vars <- reactive({
611!
      teal::validate_inputs(iv_r())
612
613!
      dist_var <- as.vector(merged$anl_input_r()$columns_source$dist_i)
614!
      s_var <- as.vector(merged$anl_input_r()$columns_source$strata_i)
615!
      g_var <- as.vector(merged$anl_input_r()$columns_source$group_i)
616
617!
      dist_var_name <- if (length(dist_var)) as.name(dist_var) else NULL
618!
      s_var_name <- if (length(s_var)) as.name(s_var) else NULL
619!
      g_var_name <- if (length(g_var)) as.name(g_var) else NULL
620
621!
      list(
622!
        dist_var = dist_var,
623!
        s_var = s_var,
624!
        g_var = g_var,
625!
        dist_var_name = dist_var_name,
626!
        s_var_name = s_var_name,
627!
        g_var_name = g_var_name
628
      )
629
    })
630
631
    # common qenv
632!
    common_q <- reactive({
633
      # Create a private stack for this function only.
634
635!
      obj <- merged$anl_q_r()
636!
      teal.reporter::teal_card(obj) <-
637!
        c(
638!
          teal.reporter::teal_card(obj),
639!
          teal.reporter::teal_card("## Module's output(s)")
640
        )
641
642!
      ANL <- obj[["ANL"]]
643!
      dist_var <- merge_vars()$dist_var
644!
      s_var <- merge_vars()$s_var
645!
      g_var <- merge_vars()$g_var
646
647!
      dist_var_name <- merge_vars()$dist_var_name
648!
      s_var_name <- merge_vars()$s_var_name
649!
      g_var_name <- merge_vars()$g_var_name
650
651!
      roundn <- input$roundn
652!
      dist_param1 <- input$dist_param1
653!
      dist_param2 <- input$dist_param2
654
      # isolated as dist_param1/dist_param2 already triggered the reactivity
655!
      t_dist <- isolate(input$t_dist)
656
657!
      qenv <- obj
658
659!
      if (length(g_var) > 0) {
660!
        validate(
661!
          need(
662!
            inherits(ANL[[g_var]], c("integer", "factor", "character")),
663!
            "Group by variable must be `factor`, `character`, or `integer`"
664
          )
665
        )
666!
        qenv <- teal.code::eval_code(qenv, "library(forcats)")
667!
        qenv <- teal.code::eval_code(
668!
          qenv,
669!
          substitute(
670!
            expr = ANL[[g_var]] <- forcats::fct_na_value_to_level(as.factor(ANL[[g_var]]), "NA"),
671!
            env = list(g_var = g_var)
672
          )
673
        )
674
      }
675
676!
      if (length(s_var) > 0) {
677!
        validate(
678!
          need(
679!
            inherits(ANL[[s_var]], c("integer", "factor", "character")),
680!
            "Stratify by variable must be `factor`, `character`, or `integer`"
681
          )
682
        )
683
684!
        qenv <- teal.code::eval_code(qenv, "library(forcats)")
685!
        qenv <- teal.code::eval_code(
686!
          qenv,
687!
          substitute(
688!
            expr = ANL[[s_var]] <- forcats::fct_na_value_to_level(as.factor(ANL[[s_var]]), "NA"),
689!
            env = list(s_var = s_var)
690
          )
691
        )
692
      }
693
694!
      validate(need(is.numeric(ANL[[dist_var]]), "Please select a numeric variable."))
695!
      teal::validate_has_data(ANL, 1, complete = TRUE)
696
697!
      if (length(t_dist) != 0) {
698!
        map_distr_nams <- list(
699!
          normal = c("mean", "sd"),
700!
          lognormal = c("meanlog", "sdlog"),
701!
          gamma = c("shape", "rate"),
702!
          unif = c("min", "max")
703
        )
704!
        params_names_raw <- map_distr_nams[[t_dist]]
705
706!
        qenv <- teal.code::eval_code(
707!
          qenv,
708!
          substitute(
709!
            expr = {
710!
              params <- as.list(c(dist_param1, dist_param2))
711!
              names(params) <- params_names_raw
712
            },
713!
            env = list(
714!
              dist_param1 = dist_param1,
715!
              dist_param2 = dist_param2,
716!
              params_names_raw = params_names_raw
717
            )
718
          )
719
        )
720
      }
721
722!
      if (length(s_var) == 0 && length(g_var) == 0) {
723!
        teal.code::eval_code(
724!
          qenv,
725!
          substitute(
726!
            expr = {
727!
              summary_table_data <- ANL %>%
728!
                dplyr::summarise(
729!
                  min = round(min(dist_var_name, na.rm = TRUE), roundn),
730!
                  median = round(stats::median(dist_var_name, na.rm = TRUE), roundn),
731!
                  mean = round(mean(dist_var_name, na.rm = TRUE), roundn),
732!
                  max = round(max(dist_var_name, na.rm = TRUE), roundn),
733!
                  sd = round(stats::sd(dist_var_name, na.rm = TRUE), roundn),
734!
                  count = dplyr::n()
735
                )
736
            },
737!
            env = list(
738!
              dist_var_name = as.name(dist_var),
739!
              roundn = roundn
740
            )
741
          )
742
        )
743
      } else {
744!
        teal.code::eval_code(
745!
          qenv,
746!
          substitute(
747!
            expr = {
748!
              strata_vars <- strata_vars_raw
749!
              summary_table_data <- ANL %>%
750!
                dplyr::group_by_at(dplyr::vars(dplyr::any_of(strata_vars))) %>%
751!
                dplyr::summarise(
752!
                  min = round(min(dist_var_name, na.rm = TRUE), roundn),
753!
                  median = round(stats::median(dist_var_name, na.rm = TRUE), roundn),
754!
                  mean = round(mean(dist_var_name, na.rm = TRUE), roundn),
755!
                  max = round(max(dist_var_name, na.rm = TRUE), roundn),
756!
                  sd = round(stats::sd(dist_var_name, na.rm = TRUE), roundn),
757!
                  count = dplyr::n()
758
                )
759
            },
760!
            env = list(
761!
              dist_var_name = dist_var_name,
762!
              strata_vars_raw = c(g_var, s_var),
763!
              roundn = roundn
764
            )
765
          )
766
        )
767
      }
768
    })
769
770
    # distplot qenv ----
771!
    dist_q <- eventReactive(
772!
      eventExpr = {
773!
        common_q()
774!
        input$scales_type
775!
        input$main_type
776!
        input$bins
777!
        input$add_dens
778!
        is.null(input$ggtheme)
779
      },
780!
      valueExpr = {
781!
        dist_var <- merge_vars()$dist_var
782!
        s_var <- merge_vars()$s_var
783!
        g_var <- merge_vars()$g_var
784!
        dist_var_name <- merge_vars()$dist_var_name
785!
        s_var_name <- merge_vars()$s_var_name
786!
        g_var_name <- merge_vars()$g_var_name
787!
        t_dist <- input$t_dist
788!
        dist_param1 <- input$dist_param1
789!
        dist_param2 <- input$dist_param2
790
791!
        scales_type <- input$scales_type
792
793!
        ndensity <- 512
794!
        main_type_var <- input$main_type
795!
        bins_var <- input$bins
796!
        add_dens_var <- input$add_dens
797!
        ggtheme <- input$ggtheme
798
799!
        teal::validate_inputs(iv_dist)
800
801!
        qenv <- common_q()
802
803!
        m_type <- if (main_type_var == "Density") "density" else "count"
804
805!
        plot_call <- if (length(s_var) == 0 && length(g_var) == 0) {
806!
          substitute(
807!
            expr = ggplot2::ggplot(ANL, ggplot2::aes(dist_var_name)) +
808!
              ggplot2::geom_histogram(
809!
                position = "identity", ggplot2::aes(y = ggplot2::after_stat(m_type)), bins = bins_var, alpha = 0.3
810
              ),
811!
            env = list(
812!
              m_type = as.name(m_type), bins_var = bins_var, dist_var_name = as.name(dist_var)
813
            )
814
          )
815!
        } else if (length(s_var) != 0 && length(g_var) == 0) {
816!
          substitute(
817!
            expr = ggplot2::ggplot(ANL, ggplot2::aes(dist_var_name, col = s_var_name)) +
818!
              ggplot2::geom_histogram(
819!
                position = "identity", ggplot2::aes(y = ggplot2::after_stat(m_type), fill = s_var),
820!
                bins = bins_var, alpha = 0.3
821
              ),
822!
            env = list(
823!
              m_type = as.name(m_type),
824!
              bins_var = bins_var,
825!
              dist_var_name = dist_var_name,
826!
              s_var = as.name(s_var),
827!
              s_var_name = s_var_name
828
            )
829
          )
830!
        } else if (length(s_var) == 0 && length(g_var) != 0) {
831!
          req(scales_type)
832!
          substitute(
833!
            expr = ggplot2::ggplot(ANL[ANL[[g_var]] != "NA", ], ggplot2::aes(dist_var_name)) +
834!
              ggplot2::geom_histogram(
835!
                position = "identity", ggplot2::aes(y = ggplot2::after_stat(m_type)), bins = bins_var, alpha = 0.3
836
              ) +
837!
              ggplot2::facet_wrap(~g_var_name, ncol = 1, scales = scales_raw),
838!
            env = list(
839!
              m_type = as.name(m_type),
840!
              bins_var = bins_var,
841!
              dist_var_name = dist_var_name,
842!
              g_var = g_var,
843!
              g_var_name = g_var_name,
844!
              scales_raw = tolower(scales_type)
845
            )
846
          )
847
        } else {
848!
          req(scales_type)
849!
          substitute(
850!
            expr = ggplot2::ggplot(ANL[ANL[[g_var]] != "NA", ], ggplot2::aes(dist_var_name, col = s_var_name)) +
851!
              ggplot2::geom_histogram(
852!
                position = "identity",
853!
                ggplot2::aes(y = ggplot2::after_stat(m_type), fill = s_var), bins = bins_var, alpha = 0.3
854
              ) +
855!
              ggplot2::facet_wrap(~g_var_name, ncol = 1, scales = scales_raw),
856!
            env = list(
857!
              m_type = as.name(m_type),
858!
              bins_var = bins_var,
859!
              dist_var_name = dist_var_name,
860!
              g_var = g_var,
861!
              s_var = as.name(s_var),
862!
              g_var_name = g_var_name,
863!
              s_var_name = s_var_name,
864!
              scales_raw = tolower(scales_type)
865
            )
866
          )
867
        }
868
869!
        if (add_dens_var) {
870!
          plot_call <- substitute(
871!
            expr = plot_call +
872!
              ggplot2::stat_density(
873!
                ggplot2::aes(y = ggplot2::after_stat(const * m_type2)),
874!
                geom = "line",
875!
                position = "identity",
876!
                alpha = 0.5,
877!
                size = 2,
878!
                n = ndensity
879
              ),
880!
            env = list(
881!
              plot_call = plot_call,
882!
              const = if (main_type_var == "Density") {
883!
                1
884
              } else {
885!
                diff(range(qenv[["ANL"]][[dist_var]], na.rm = TRUE)) / bins_var
886
              },
887!
              m_type2 = if (main_type_var == "Density") as.name("density") else as.name("count"),
888!
              ndensity = ndensity
889
            )
890
          )
891
        }
892
893!
        if (length(t_dist) != 0 && main_type_var == "Density" && length(g_var) == 0 && length(s_var) == 0) {
894!
          qenv <- teal.code::eval_code(qenv, "library(ggpp)")
895!
          qenv <- teal.code::eval_code(
896!
            qenv,
897!
            substitute(
898!
              df_params <- as.data.frame(append(params, list(name = t_dist))),
899!
              env = list(t_dist = t_dist)
900
            )
901
          )
902!
          datas <- quote(data.frame(x = 0.7, y = 1, tb = I(list(df_params = df_params))))
903!
          label <- quote(tb)
904
905!
          plot_call <- substitute(
906!
            expr = plot_call + ggpp::geom_table_npc(
907!
              data = data,
908!
              ggplot2::aes(npcx = x, npcy = y, label = label),
909!
              hjust = 0, vjust = 1, size = 4
910
            ),
911!
            env = list(plot_call = plot_call, data = datas, label = label)
912
          )
913
        }
914
915!
        if (
916!
          length(s_var) == 0 &&
917!
            length(g_var) == 0 &&
918!
            main_type_var == "Density" &&
919!
            length(t_dist) != 0 &&
920!
            main_type_var == "Density"
921
        ) {
922!
          map_dist <- stats::setNames(
923!
            c("dnorm", "dlnorm", "dgamma", "dunif"),
924!
            c("normal", "lognormal", "gamma", "unif")
925
          )
926!
          plot_call <- substitute(
927!
            expr = plot_call + stat_function(
928!
              data = data.frame(x = range(ANL[[dist_var]]), color = mapped_dist),
929!
              ggplot2::aes(x, color = color),
930!
              fun = mapped_dist_name,
931!
              n = ndensity,
932!
              size = 2,
933!
              args = params
934
            ) +
935!
              ggplot2::scale_color_manual(values = stats::setNames("blue", mapped_dist), aesthetics = "color"),
936!
            env = list(
937!
              plot_call = plot_call,
938!
              dist_var = dist_var,
939!
              ndensity = ndensity,
940!
              mapped_dist = unname(map_dist[t_dist]),
941!
              mapped_dist_name = as.name(unname(map_dist[t_dist]))
942
            )
943
          )
944
        }
945
946!
        all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
947!
          user_plot = ggplot2_args[["Histogram"]],
948!
          user_default = ggplot2_args$default
949
        )
950
951!
        parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
952!
          all_ggplot2_args,
953!
          ggtheme = ggtheme
954
        )
955
956!
        teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Histogram Plot")
957!
        teal.code::eval_code(
958!
          qenv,
959!
          substitute(
960!
            expr = histogram_plot <- plot_call,
961!
            env = list(plot_call = Reduce(function(x, y) call("+", x, y), c(plot_call, parsed_ggplot2_args)))
962
          )
963
        )
964
      }
965
    )
966
967
    # qqplot qenv ----
968!
    qq_q <- eventReactive(
969!
      eventExpr = {
970!
        common_q()
971!
        input$scales_type
972!
        input$qq_line
973!
        is.null(input$ggtheme)
974!
        input$tabs
975
      },
976!
      valueExpr = {
977!
        dist_var <- merge_vars()$dist_var
978!
        s_var <- merge_vars()$s_var
979!
        g_var <- merge_vars()$g_var
980!
        dist_var_name <- merge_vars()$dist_var_name
981!
        s_var_name <- merge_vars()$s_var_name
982!
        g_var_name <- merge_vars()$g_var_name
983!
        dist_param1 <- input$dist_param1
984!
        dist_param2 <- input$dist_param2
985
986!
        scales_type <- input$scales_type
987!
        ggtheme <- input$ggtheme
988
989!
        teal::validate_inputs(iv_r_dist(), iv_dist)
990!
        t_dist <- req(input$t_dist) # Not validated when tab is not selected
991!
        qenv <- common_q()
992
993!
        plot_call <- if (length(s_var) == 0 && length(g_var) == 0) {
994!
          substitute(
995!
            expr = ggplot2::ggplot(ANL, ggplot2::aes_string(sample = dist_var)),
996!
            env = list(dist_var = dist_var)
997
          )
998!
        } else if (length(s_var) != 0 && length(g_var) == 0) {
999!
          substitute(
1000!
            expr = ggplot2::ggplot(ANL, ggplot2::aes_string(sample = dist_var, color = s_var)),
1001!
            env = list(dist_var = dist_var, s_var = s_var)
1002
          )
1003!
        } else if (length(s_var) == 0 && length(g_var) != 0) {
1004!
          substitute(
1005!
            expr = ggplot2::ggplot(ANL[ANL[[g_var]] != "NA", ], ggplot2::aes_string(sample = dist_var)) +
1006!
              ggplot2::facet_wrap(~g_var_name, ncol = 1, scales = scales_raw),
1007!
            env = list(
1008!
              dist_var = dist_var,
1009!
              g_var = g_var,
1010!
              g_var_name = g_var_name,
1011!
              scales_raw = tolower(scales_type)
1012
            )
1013
          )
1014
        } else {
1015!
          substitute(
1016!
            expr = ggplot2::ggplot(ANL[ANL[[g_var]] != "NA", ], ggplot2::aes_string(sample = dist_var, color = s_var)) +
1017!
              ggplot2::facet_wrap(~g_var_name, ncol = 1, scales = scales_raw),
1018!
            env = list(
1019!
              dist_var = dist_var,
1020!
              g_var = g_var,
1021!
              s_var = s_var,
1022!
              g_var_name = g_var_name,
1023!
              scales_raw = tolower(scales_type)
1024
            )
1025
          )
1026
        }
1027
1028!
        map_dist <- stats::setNames(
1029!
          c("qnorm", "qlnorm", "qgamma", "qunif"),
1030!
          c("normal", "lognormal", "gamma", "unif")
1031
        )
1032
1033!
        plot_call <- substitute(
1034!
          expr = plot_call +
1035!
            ggplot2::stat_qq(distribution = mapped_dist, dparams = params),
1036!
          env = list(plot_call = plot_call, mapped_dist = as.name(unname(map_dist[t_dist])))
1037
        )
1038
1039!
        if (length(t_dist) != 0 && length(g_var) == 0 && length(s_var) == 0) {
1040!
          qenv <- teal.code::eval_code(qenv, "library(ggpp)")
1041!
          qenv <- teal.code::eval_code(
1042!
            qenv,
1043!
            substitute(
1044!
              df_params <- as.data.frame(append(params, list(name = t_dist))),
1045!
              env = list(t_dist = t_dist)
1046
            )
1047
          )
1048!
          datas <- quote(data.frame(x = 0.7, y = 1, tb = I(list(df_params = df_params))))
1049!
          label <- quote(tb)
1050
1051!
          plot_call <- substitute(
1052!
            expr = plot_call +
1053!
              ggpp::geom_table_npc(
1054!
                data = data,
1055!
                ggplot2::aes(npcx = x, npcy = y, label = label),
1056!
                hjust = 0,
1057!
                vjust = 1,
1058!
                size = 4
1059
              ),
1060!
            env = list(
1061!
              plot_call = plot_call,
1062!
              data = datas,
1063!
              label = label
1064
            )
1065
          )
1066
        }
1067
1068!
        if (isTRUE(input$qq_line)) {
1069!
          plot_call <- substitute(
1070!
            expr = plot_call +
1071!
              ggplot2::stat_qq_line(distribution = mapped_dist, dparams = params),
1072!
            env = list(plot_call = plot_call, mapped_dist = as.name(unname(map_dist[t_dist])))
1073
          )
1074
        }
1075
1076!
        all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
1077!
          user_plot = ggplot2_args[["QQplot"]],
1078!
          user_default = ggplot2_args$default,
1079!
          module_plot = teal.widgets::ggplot2_args(labs = list(x = "theoretical", y = "sample"))
1080
        )
1081
1082!
        parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
1083!
          all_ggplot2_args,
1084!
          ggtheme = ggtheme
1085
        )
1086
1087!
        teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### QQ Plot")
1088!
        teal.code::eval_code(
1089!
          qenv,
1090!
          substitute(
1091!
            expr = qq_plot <- plot_call,
1092!
            env = list(plot_call = Reduce(function(x, y) call("+", x, y), c(plot_call, parsed_ggplot2_args)))
1093
          )
1094
        )
1095
      }
1096
    )
1097
1098
    # test qenv ----
1099!
    test_q <- eventReactive(
1100!
      ignoreNULL = FALSE,
1101!
      eventExpr = {
1102!
        common_q()
1103!
        input$dist_param1
1104!
        input$dist_param2
1105!
        input$dist_tests
1106
      },
1107!
      valueExpr = {
1108
        # Create a private stack for this function only.
1109!
        ANL <- common_q()[["ANL"]]
1110
1111!
        dist_var <- merge_vars()$dist_var
1112!
        s_var <- merge_vars()$s_var
1113!
        g_var <- merge_vars()$g_var
1114
1115!
        dist_var_name <- merge_vars()$dist_var_name
1116!
        s_var_name <- merge_vars()$s_var_name
1117!
        g_var_name <- merge_vars()$g_var_name
1118
1119!
        dist_param1 <- input$dist_param1
1120!
        dist_param2 <- input$dist_param2
1121!
        dist_tests <- input$dist_tests
1122!
        t_dist <- input$t_dist
1123
1124!
        req(dist_tests)
1125
1126!
        teal::validate_inputs(iv_dist)
1127
1128!
        if (length(s_var) > 0 || length(g_var) > 0) {
1129!
          counts <- ANL %>%
1130!
            dplyr::group_by_at(dplyr::vars(dplyr::any_of(c(s_var, g_var)))) %>%
1131!
            dplyr::summarise(n = dplyr::n())
1132
1133!
          validate(need(all(counts$n > 5), "Please select strata*group with at least 5 observation each."))
1134
        }
1135
1136
1137!
        if (dist_tests %in% c(
1138!
          "t-test (two-samples, not paired)",
1139!
          "F-test",
1140!
          "Kolmogorov-Smirnov (two-samples)"
1141
        )) {
1142!
          if (length(g_var) == 0 && length(s_var) > 0) {
1143!
            validate(need(
1144!
              length(unique(ANL[[s_var]])) == 2,
1145!
              "Please select stratify variable with 2 levels."
1146
            ))
1147
          }
1148!
          if (length(g_var) > 0 && length(s_var) > 0) {
1149!
            validate(need(
1150!
              all(stats::na.omit(as.vector(
1151!
                tapply(ANL[[s_var]], list(ANL[[g_var]]), function(x) length(unique(x))) == 2
1152
              ))),
1153!
              "Please select stratify variable with 2 levels, per each group."
1154
            ))
1155
          }
1156
        }
1157
1158!
        map_dist <- stats::setNames(
1159!
          c("pnorm", "plnorm", "pgamma", "punif"),
1160!
          c("normal", "lognormal", "gamma", "unif")
1161
        )
1162!
        sks_args <- list(
1163!
          test = quote(stats::ks.test),
1164!
          args = bquote(append(list(.[[.(dist_var)]], .(map_dist[t_dist])), params)),
1165!
          groups = c(g_var, s_var)
1166
        )
1167!
        ssw_args <- list(
1168!
          test = quote(stats::shapiro.test),
1169!
          args = bquote(list(.[[.(dist_var)]])),
1170!
          groups = c(g_var, s_var)
1171
        )
1172!
        mfil_args <- list(
1173!
          test = quote(stats::fligner.test),
1174!
          args = bquote(list(.[[.(dist_var)]], .[[.(s_var)]])),
1175!
          groups = c(g_var)
1176
        )
1177!
        sad_args <- list(
1178!
          test = quote(goftest::ad.test),
1179!
          args = bquote(append(list(.[[.(dist_var)]], .(map_dist[t_dist])), params)),
1180!
          groups = c(g_var, s_var)
1181
        )
1182!
        scvm_args <- list(
1183!
          test = quote(goftest::cvm.test),
1184!
          args = bquote(append(list(.[[.(dist_var)]], .(map_dist[t_dist])), params)),
1185!
          groups = c(g_var, s_var)
1186
        )
1187!
        manov_args <- list(
1188!
          test = quote(stats::aov),
1189!
          args = bquote(list(stats::formula(.(dist_var_name) ~ .(s_var_name)), .)),
1190!
          groups = c(g_var)
1191
        )
1192!
        mt_args <- list(
1193!
          test = quote(stats::t.test),
1194!
          args = bquote(unname(split(.[[.(dist_var)]], .[[.(s_var)]], drop = TRUE))),
1195!
          groups = c(g_var)
1196
        )
1197!
        mv_args <- list(
1198!
          test = quote(stats::var.test),
1199!
          args = bquote(unname(split(.[[.(dist_var)]], .[[.(s_var)]], drop = TRUE))),
1200!
          groups = c(g_var)
1201
        )
1202!
        mks_args <- list(
1203!
          test = quote(stats::ks.test),
1204!
          args = bquote(unname(split(.[[.(dist_var)]], .[[.(s_var)]], drop = TRUE))),
1205!
          groups = c(g_var)
1206
        )
1207
1208!
        tests_base <- switch(dist_tests,
1209!
          "Kolmogorov-Smirnov (one-sample)" = sks_args,
1210!
          "Shapiro-Wilk" = ssw_args,
1211!
          "Fligner-Killeen" = mfil_args,
1212!
          "one-way ANOVA" = manov_args,
1213!
          "t-test (two-samples, not paired)" = mt_args,
1214!
          "F-test" = mv_args,
1215!
          "Kolmogorov-Smirnov (two-samples)" = mks_args,
1216!
          "Anderson-Darling (one-sample)" = sad_args,
1217!
          "Cramer-von Mises (one-sample)" = scvm_args
1218
        )
1219
1220!
        env <- list(
1221!
          t_test = t_dist,
1222!
          dist_var = dist_var,
1223!
          g_var = g_var,
1224!
          s_var = s_var,
1225!
          args = tests_base$args,
1226!
          groups = tests_base$groups,
1227!
          test = tests_base$test,
1228!
          dist_var_name = dist_var_name,
1229!
          g_var_name = g_var_name,
1230!
          s_var_name = s_var_name
1231
        )
1232
1233!
        qenv <- common_q()
1234
1235!
        if (length(s_var) == 0 && length(g_var) == 0) {
1236!
          qenv <- teal.code::eval_code(qenv, "library(generics)")
1237!
          qenv <- teal.code::eval_code(
1238!
            qenv,
1239!
            substitute(
1240!
              expr = {
1241!
                test_table_data <- ANL %>%
1242!
                  dplyr::select(dist_var) %>%
1243!
                  with(., generics::glance(do.call(test, args))) %>%
1244!
                  dplyr::mutate_if(is.numeric, round, 3)
1245
              },
1246!
              env = env
1247
            )
1248
          )
1249
        } else {
1250!
          qenv <- teal.code::eval_code(qenv, "library(tidyr)")
1251!
          qenv <- teal.code::eval_code(
1252!
            qenv,
1253!
            substitute(
1254!
              expr = {
1255!
                test_table_data <- ANL %>%
1256!
                  dplyr::select(dist_var, s_var, g_var) %>%
1257!
                  dplyr::group_by_at(dplyr::vars(dplyr::any_of(groups))) %>%
1258!
                  dplyr::do(tests = generics::glance(do.call(test, args))) %>%
1259!
                  tidyr::unnest(tests) %>%
1260!
                  dplyr::mutate_if(is.numeric, round, 3)
1261
              },
1262!
              env = env
1263
            )
1264
          )
1265
        }
1266
      }
1267
    )
1268
1269
    # outputs ----
1270!
    output_dist_q <- reactive(c(common_q(), req(dist_q())))
1271!
    output_qq_q <- reactive(c(common_q(), req(qq_q())))
1272
1273!
    output_summary_q <- reactive({
1274
      # Summary table listing has to be created separately to allow for qenv join
1275!
      q_common <- common_q()
1276!
      teal.reporter::teal_card(q_common) <- c(
1277!
        teal.reporter::teal_card(q_common),
1278!
        "### Statistics table"
1279
      )
1280!
      if (iv_r()$is_valid()) {
1281!
        within(q_common, {
1282!
          summary_table <- rtables::df_to_tt(summary_table_data)
1283
        })
1284
      } else {
1285!
        within(
1286!
          q_common,
1287!
          summary_table <- rtables::rtable(header = rtables::rheader(colnames(summary_table_data)))
1288
        )
1289
      }
1290
    })
1291
1292!
    output_test_q <- reactive({
1293
      # wrapped in if since could lead into validate error - we do want to continue
1294!
      test_q_out <- try(test_q(), silent = TRUE)
1295!
      q_common <- common_q()
1296!
      teal.reporter::teal_card(q_common) <- c(
1297!
        teal.reporter::teal_card(q_common),
1298!
        "### Distribution Tests table"
1299
      )
1300!
      if (inherits(test_q_out, c("try-error", "error"))) {
1301!
        within(
1302!
          q_common,
1303!
          test_table <- rtables::rtable(header = rtables::rheader("No data available in table"), rtables::rrow())
1304
        )
1305
      } else {
1306!
        within(c(q_common, test_q_out), {
1307!
          test_table <- rtables::df_to_tt(test_table_data)
1308
        })
1309
      }
1310
    })
1311
1312!
    decorated_output_dist_q <- srv_decorate_teal_data(
1313!
      "d_density",
1314!
      data = output_dist_q,
1315!
      decorators = select_decorators(decorators, "histogram_plot"),
1316!
      expr = quote(histogram_plot)
1317
    )
1318
1319!
    decorated_output_qq_q <- srv_decorate_teal_data(
1320!
      "d_qq",
1321!
      data = output_qq_q,
1322!
      decorators = select_decorators(decorators, "qq_plot"),
1323!
      expr = quote(qq_plot)
1324
    )
1325
1326!
    decorated_output_summary_q <- srv_decorate_teal_data(
1327!
      "d_summary",
1328!
      data = output_summary_q,
1329!
      decorators = select_decorators(decorators, "summary_table"),
1330!
      expr = quote(summary_table)
1331
    )
1332
1333!
    decorated_output_test_q <- srv_decorate_teal_data(
1334!
      "d_test",
1335!
      data = output_test_q,
1336!
      decorators = select_decorators(decorators, "test_table"),
1337!
      expr = quote(test_table)
1338
    )
1339
1340!
    dist_r <- reactive(req(decorated_output_dist_q())[["histogram_plot"]])
1341!
    qq_r <- reactive(req(decorated_output_qq_q())[["qq_plot"]])
1342
1343!
    summary_r <- reactive({
1344!
      q <- req(output_summary_q())
1345
1346!
      DT::datatable(
1347!
        q[["summary_table_data"]],
1348!
        options = list(
1349!
          autoWidth = TRUE,
1350!
          columnDefs = list(list(width = "200px", targets = "_all"))
1351
        ),
1352!
        rownames = FALSE
1353
      )
1354
    })
1355
1356!
    output$summary_table <- DT::renderDataTable(summary_r())
1357
1358!
    tests_r <- reactive({
1359!
      q <- req(decorated_output_test_q())
1360!
      DT::datatable(q[["test_table_data"]])
1361
    })
1362
1363!
    pws1 <- teal.widgets::plot_with_settings_srv(
1364!
      id = "hist_plot",
1365!
      plot_r = dist_r,
1366!
      height = plot_height,
1367!
      width = plot_width,
1368!
      brushing = FALSE
1369
    )
1370
1371!
    pws2 <- teal.widgets::plot_with_settings_srv(
1372!
      id = "qq_plot",
1373!
      plot_r = qq_r,
1374!
      height = plot_height,
1375!
      width = plot_width,
1376!
      brushing = FALSE
1377
    )
1378
1379!
    decorated_output_dist_dims_q <- set_chunk_dims(pws1, decorated_output_dist_q)
1380
1381!
    decorated_output_qq_dims_q <- set_chunk_dims(pws2, decorated_output_qq_q)
1382
1383!
    decorated_output_q <- reactive({
1384!
      tab <- req(input$tabs) # tab is NULL upon app launch, hence will crash without this statement
1385!
      test_q_out <- decorated_output_test_q()
1386
1387!
      out_q <- switch(tab,
1388!
        Histogram = decorated_output_dist_dims_q(),
1389!
        QQplot = decorated_output_qq_dims_q()
1390
      )
1391!
      withCallingHandlers(
1392!
        c(out_q, output_summary_q(), test_q_out),
1393!
        warning = function(w) {
1394!
          if (grepl("Restoring original content and adding only", conditionMessage(w))) {
1395!
            invokeRestart("muffleWarning")
1396
          }
1397
        }
1398
      )
1399
    })
1400
1401!
    output$t_stats <- DT::renderDataTable(tests_r())
1402
1403!
    decorated_output_q
1404
  })
1405
}
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
#' @inheritSection teal::example_module Reporting
67
#'
68
#' @examplesShinylive
69
#' library(teal.modules.general)
70
#' interactive <- function() TRUE
71
#' {{ next_example }}
72
#' @examples
73
#' # general data example
74
#' data <- teal_data()
75
#' data <- within(data, {
76
#'   require(nestcolor)
77
#'   mtcars <- mtcars
78
#'   for (v in c("cyl", "vs", "am", "gear")) {
79
#'     mtcars[[v]] <- as.factor(mtcars[[v]])
80
#'   }
81
#' })
82
#'
83
#' app <- init(
84
#'   data = data,
85
#'   modules = modules(
86
#'     tm_g_response(
87
#'       label = "Response Plots",
88
#'       response = data_extract_spec(
89
#'         dataname = "mtcars",
90
#'         select = select_spec(
91
#'           label = "Select variable:",
92
#'           choices = variable_choices(data[["mtcars"]], c("cyl", "gear")),
93
#'           selected = "cyl",
94
#'           multiple = FALSE,
95
#'           fixed = FALSE
96
#'         )
97
#'       ),
98
#'       x = data_extract_spec(
99
#'         dataname = "mtcars",
100
#'         select = select_spec(
101
#'           label = "Select variable:",
102
#'           choices = variable_choices(data[["mtcars"]], c("vs", "am")),
103
#'           selected = "vs",
104
#'           multiple = FALSE,
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_g_response(
132
#'       label = "Response Plots",
133
#'       response = data_extract_spec(
134
#'         dataname = "ADSL",
135
#'         select = select_spec(
136
#'           label = "Select variable:",
137
#'           choices = variable_choices(data[["ADSL"]], c("BMRKR2", "COUNTRY")),
138
#'           selected = "BMRKR2",
139
#'           multiple = FALSE,
140
#'           fixed = FALSE
141
#'         )
142
#'       ),
143
#'       x = data_extract_spec(
144
#'         dataname = "ADSL",
145
#'         select = select_spec(
146
#'           label = "Select variable:",
147
#'           choices = variable_choices(data[["ADSL"]], c("SEX", "RACE")),
148
#'           selected = "RACE",
149
#'           multiple = FALSE,
150
#'           fixed = FALSE
151
#'         )
152
#'       )
153
#'     )
154
#'   )
155
#' )
156
#' if (interactive()) {
157
#'   shinyApp(app$ui, app$server)
158
#' }
159
#'
160
#' @export
161
#'
162
tm_g_response <- function(label = "Response Plot",
163
                          response,
164
                          x,
165
                          row_facet = NULL,
166
                          col_facet = NULL,
167
                          coord_flip = FALSE,
168
                          count_labels = TRUE,
169
                          rotate_xaxis_labels = FALSE,
170
                          freq = FALSE,
171
                          plot_height = c(600, 400, 5000),
172
                          plot_width = NULL,
173
                          ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"),
174
                          ggplot2_args = teal.widgets::ggplot2_args(),
175
                          pre_output = NULL,
176
                          post_output = NULL,
177
                          transformators = list(),
178
                          decorators = list()) {
179!
  message("Initializing tm_g_response")
180
181
  # Normalize the parameters
182!
  if (inherits(response, "data_extract_spec")) response <- list(response)
183!
  if (inherits(x, "data_extract_spec")) x <- list(x)
184!
  if (inherits(row_facet, "data_extract_spec")) row_facet <- list(row_facet)
185!
  if (inherits(col_facet, "data_extract_spec")) col_facet <- list(col_facet)
186
187
  # Start of assertions
188!
  checkmate::assert_string(label)
189
190!
  checkmate::assert_list(response, types = "data_extract_spec")
191!
  if (!all(vapply(response, function(x) !("" %in% x$select$choices), logical(1)))) {
192!
    stop("'response' should not allow empty values")
193
  }
194!
  assert_single_selection(response)
195
196!
  checkmate::assert_list(x, types = "data_extract_spec")
197!
  if (!all(vapply(x, function(x) !("" %in% x$select$choices), logical(1)))) {
198!
    stop("'x' should not allow empty values")
199
  }
200!
  assert_single_selection(x)
201
202!
  checkmate::assert_list(row_facet, types = "data_extract_spec", null.ok = TRUE)
203!
  checkmate::assert_list(col_facet, types = "data_extract_spec", null.ok = TRUE)
204!
  checkmate::assert_flag(coord_flip)
205!
  checkmate::assert_flag(count_labels)
206!
  checkmate::assert_flag(rotate_xaxis_labels)
207!
  checkmate::assert_flag(freq)
208
209!
  checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE)
210!
  checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height")
211!
  checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE)
212!
  checkmate::assert_numeric(
213!
    plot_width[1],
214!
    lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width"
215
  )
216
217!
  ggtheme <- match.arg(ggtheme)
218!
  checkmate::assert_class(ggplot2_args, "ggplot2_args")
219
220!
  checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
221!
  checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
222
223!
  assert_decorators(decorators, "plot")
224
  # End of assertions
225
226
  # Make UI args
227!
  args <- as.list(environment())
228
229!
  data_extract_list <- list(
230!
    response = response,
231!
    x = x,
232!
    row_facet = row_facet,
233!
    col_facet = col_facet
234
  )
235
236!
  ans <- module(
237!
    label = label,
238!
    server = srv_g_response,
239!
    ui = ui_g_response,
240!
    ui_args = args,
241!
    server_args = c(
242!
      data_extract_list,
243!
      list(
244!
        plot_height = plot_height,
245!
        plot_width = plot_width,
246!
        ggplot2_args = ggplot2_args,
247!
        decorators = decorators
248
      )
249
    ),
250!
    transformators = transformators,
251!
    datanames = teal.transform::get_extract_datanames(data_extract_list)
252
  )
253!
  attr(ans, "teal_bookmarkable") <- TRUE
254!
  ans
255
}
256
257
# UI function for the response module
258
ui_g_response <- function(id, ...) {
259!
  ns <- NS(id)
260!
  args <- list(...)
261!
  is_single_dataset_value <- teal.transform::is_single_dataset(args$response, args$x, args$row_facet, args$col_facet)
262
263!
  teal.widgets::standard_layout(
264!
    output = teal.widgets::white_small_well(
265!
      teal.widgets::plot_with_settings_ui(id = ns("myplot"))
266
    ),
267!
    encoding = tags$div(
268!
      tags$label("Encodings", class = "text-primary"),
269!
      teal.transform::datanames_input(args[c("response", "x", "row_facet", "col_facet")]),
270!
      teal.transform::data_extract_ui(
271!
        id = ns("response"),
272!
        label = "Response variable",
273!
        data_extract_spec = args$response,
274!
        is_single_dataset = is_single_dataset_value
275
      ),
276!
      teal.transform::data_extract_ui(
277!
        id = ns("x"),
278!
        label = "X variable",
279!
        data_extract_spec = args$x,
280!
        is_single_dataset = is_single_dataset_value
281
      ),
282!
      if (!is.null(args$row_facet)) {
283!
        teal.transform::data_extract_ui(
284!
          id = ns("row_facet"),
285!
          label = "Row facetting",
286!
          data_extract_spec = args$row_facet,
287!
          is_single_dataset = is_single_dataset_value
288
        )
289
      },
290!
      if (!is.null(args$col_facet)) {
291!
        teal.transform::data_extract_ui(
292!
          id = ns("col_facet"),
293!
          label = "Column facetting",
294!
          data_extract_spec = args$col_facet,
295!
          is_single_dataset = is_single_dataset_value
296
        )
297
      },
298!
      shinyWidgets::radioGroupButtons(
299!
        inputId = ns("freq"),
300!
        label = NULL,
301!
        choices = c("frequency", "density"),
302!
        selected = ifelse(args$freq, "frequency", "density"),
303!
        justified = TRUE
304
      ),
305!
      ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")),
306!
      bslib::accordion(
307!
        open = TRUE,
308!
        bslib::accordion_panel(
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!
    pre_output = args$pre_output,
324!
    post_output = args$post_output
325
  )
326
}
327
328
# Server function for the response module
329
srv_g_response <- function(id,
330
                           data,
331
                           response,
332
                           x,
333
                           row_facet,
334
                           col_facet,
335
                           plot_height,
336
                           plot_width,
337
                           ggplot2_args,
338
                           decorators) {
339!
  checkmate::assert_class(data, "reactive")
340!
  checkmate::assert_class(isolate(data()), "teal_data")
341!
  moduleServer(id, function(input, output, session) {
342!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
343
344!
    data_extract <- list(response = response, x = x, row_facet = row_facet, col_facet = col_facet)
345
346!
    rule_diff <- function(other) {
347!
      function(value) {
348!
        if (other %in% names(selector_list())) {
349!
          othervalue <- selector_list()[[other]]()[["select"]]
350!
          if (!is.null(othervalue)) {
351!
            if (identical(value, othervalue)) {
352!
              "Row and column facetting variables must be different."
353
            }
354
          }
355
        }
356
      }
357
    }
358
359!
    selector_list <- teal.transform::data_extract_multiple_srv(
360!
      data_extract = data_extract,
361!
      datasets = data,
362!
      select_validation_rule = list(
363!
        response = shinyvalidate::sv_required("Please define a column for the response variable"),
364!
        x = shinyvalidate::sv_required("Please define a column for X variable"),
365!
        row_facet = shinyvalidate::compose_rules(
366!
          shinyvalidate::sv_optional(),
367!
          ~ if (length(.) > 1) "There must be 1 or no row facetting variable.",
368!
          rule_diff("col_facet")
369
        ),
370!
        col_facet = shinyvalidate::compose_rules(
371!
          shinyvalidate::sv_optional(),
372!
          ~ if (length(.) > 1) "There must be 1 or no column facetting variable.",
373!
          rule_diff("row_facet")
374
        )
375
      )
376
    )
377
378!
    iv_r <- reactive({
379!
      iv <- shinyvalidate::InputValidator$new()
380!
      iv$add_rule("ggtheme", shinyvalidate::sv_required("Please select a theme"))
381!
      teal.transform::compose_and_enable_validators(iv, selector_list)
382
    })
383
384!
    anl_merged_input <- teal.transform::merge_expression_srv(
385!
      selector_list = selector_list,
386!
      datasets = data
387
    )
388
389!
    qenv <- reactive(
390!
      teal.code::eval_code(data(), "library(ggplot2);library(dplyr)")
391
    )
392
393!
    anl_merged_q <- reactive({
394!
      req(anl_merged_input())
395!
      qenv() %>%
396!
        teal.code::eval_code(as.expression(anl_merged_input()$expr))
397
    })
398
399!
    merged <- list(
400!
      anl_input_r = anl_merged_input,
401!
      anl_q_r = anl_merged_q
402
    )
403
404!
    output_q <- reactive({
405!
      teal::validate_inputs(iv_r())
406
407!
      qenv <- merged$anl_q_r()
408!
      teal.reporter::teal_card(qenv) <-
409!
        c(
410!
          teal.reporter::teal_card(qenv),
411!
          teal.reporter::teal_card("## Module's output(s)")
412
        )
413!
      ANL <- qenv[["ANL"]]
414!
      resp_var <- as.vector(merged$anl_input_r()$columns_source$response)
415!
      x <- as.vector(merged$anl_input_r()$columns_source$x)
416
417!
      validate(need(is.factor(ANL[[resp_var]]), "Please select a factor variable as the response."))
418!
      validate(need(is.factor(ANL[[x]]), "Please select a factor variable as the X-Variable."))
419!
      teal::validate_has_data(ANL, 10)
420!
      teal::validate_has_data(ANL[, c(resp_var, x)], 10, complete = TRUE, allow_inf = FALSE)
421
422!
      row_facet_name <- if (length(merged$anl_input_r()$columns_source$row_facet) == 0) {
423!
        character(0)
424
      } else {
425!
        as.vector(merged$anl_input_r()$columns_source$row_facet)
426
      }
427!
      col_facet_name <- if (length(merged$anl_input_r()$columns_source$col_facet) == 0) {
428!
        character(0)
429
      } else {
430!
        as.vector(merged$anl_input_r()$columns_source$col_facet)
431
      }
432
433!
      freq <- input$freq == "frequency"
434!
      swap_axes <- input$coord_flip
435!
      counts <- input$count_labels
436!
      rotate_xaxis_labels <- input$rotate_xaxis_labels
437!
      ggtheme <- input$ggtheme
438
439!
      arg_position <- if (freq) "stack" else "fill"
440
441!
      rowf <- if (length(row_facet_name) != 0) as.name(row_facet_name)
442!
      colf <- if (length(col_facet_name) != 0) as.name(col_facet_name)
443!
      resp_cl <- as.name(resp_var)
444!
      x_cl <- as.name(x)
445
446!
      if (swap_axes) {
447!
        qenv <- teal.code::eval_code(
448!
          qenv,
449!
          substitute(
450!
            expr = ANL[[x]] <- with(ANL, forcats::fct_rev(x_cl)),
451!
            env = list(x = x, x_cl = x_cl)
452
          )
453
        )
454
      }
455
456!
      qenv <- teal.code::eval_code(
457!
        qenv,
458!
        substitute(
459!
          expr = ANL[[resp_var]] <- factor(ANL[[resp_var]]),
460!
          env = list(resp_var = resp_var)
461
        )
462
      ) %>%
463
        # rowf and colf will be a NULL if not set by a user
464!
        teal.code::eval_code(
465!
          substitute(
466!
            expr = ANL2 <- ANL %>%
467!
              dplyr::group_by_at(dplyr::vars(x_cl, resp_cl, rowf, colf)) %>%
468!
              dplyr::summarise(ns = dplyr::n()) %>%
469!
              dplyr::group_by_at(dplyr::vars(x_cl, rowf, colf)) %>%
470!
              dplyr::mutate(sums = sum(ns), percent = round(ns / sums * 100, 1)),
471!
            env = list(x_cl = x_cl, resp_cl = resp_cl, rowf = rowf, colf = colf)
472
          )
473
        ) %>%
474!
        teal.code::eval_code(
475!
          substitute(
476!
            expr = ANL3 <- ANL %>%
477!
              dplyr::group_by_at(dplyr::vars(x_cl, rowf, colf)) %>%
478!
              dplyr::summarise(ns = dplyr::n()),
479!
            env = list(x_cl = x_cl, rowf = rowf, colf = colf)
480
          )
481
        )
482
483!
      plot_call <- substitute(
484!
        expr = ggplot2::ggplot(ANL2, ggplot2::aes(x = x_cl, y = ns)) +
485!
          ggplot2::geom_bar(ggplot2::aes(fill = resp_cl), stat = "identity", position = arg_position),
486!
        env = list(
487!
          x_cl = x_cl,
488!
          resp_cl = resp_cl,
489!
          arg_position = arg_position
490
        )
491
      )
492
493!
      if (!freq) {
494!
        plot_call <- substitute(
495!
          plot_call + ggplot2::expand_limits(y = c(0, 1.1)),
496!
          env = list(plot_call = plot_call)
497
        )
498
      }
499
500!
      if (counts) {
501!
        plot_call <- substitute(
502!
          expr = plot_call +
503!
            ggplot2::geom_text(
504!
              data = ANL2,
505!
              ggplot2::aes(label = ns, x = x_cl, y = ns, group = resp_cl),
506!
              col = "white",
507!
              vjust = "middle",
508!
              hjust = "middle",
509!
              position = position_anl2_value
510
            ) +
511!
            ggplot2::geom_text(
512!
              data = ANL3, ggplot2::aes(label = ns, x = x_cl, y = anl3_y),
513!
              hjust = hjust_value,
514!
              vjust = vjust_value,
515!
              position = position_anl3_value
516
            ),
517!
          env = list(
518!
            plot_call = plot_call,
519!
            x_cl = x_cl,
520!
            resp_cl = resp_cl,
521!
            hjust_value = if (swap_axes) "left" else "middle",
522!
            vjust_value = if (swap_axes) "middle" else -1,
523!
            position_anl2_value = if (!freq) quote(position_fill(0.5)) else quote(position_stack(0.5)), # nolint: line_length_linter.
524!
            anl3_y = if (!freq) 1.1 else as.name("ns"),
525!
            position_anl3_value = if (!freq) "fill" else "stack"
526
          )
527
        )
528
      }
529
530!
      if (swap_axes) {
531!
        plot_call <- substitute(plot_call + coord_flip(), env = list(plot_call = plot_call))
532
      }
533
534!
      facet_cl <- facet_ggplot_call(row_facet_name, col_facet_name)
535
536!
      if (!is.null(facet_cl)) {
537!
        plot_call <- substitute(expr = plot_call + facet_cl, env = list(plot_call = plot_call, facet_cl = facet_cl))
538
      }
539
540!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
541!
        labs = list(
542!
          x = varname_w_label(x, ANL),
543!
          y = varname_w_label(resp_var, ANL, prefix = "Proportion of "),
544!
          fill = varname_w_label(resp_var, ANL)
545
        ),
546!
        theme = list(legend.position = "bottom")
547
      )
548
549!
      if (rotate_xaxis_labels) {
550!
        dev_ggplot2_args$theme[["axis.text.x"]] <- quote(ggplot2::element_text(angle = 45, hjust = 1))
551
      }
552
553!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
554!
        user_plot = ggplot2_args,
555!
        module_plot = dev_ggplot2_args
556
      )
557
558!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
559!
        all_ggplot2_args,
560!
        ggtheme = ggtheme
561
      )
562
563!
      plot_call <- substitute(expr = {
564!
        plot <- plot_call + labs + ggthemes + themes
565!
      }, env = list(
566!
        plot_call = plot_call,
567!
        labs = parsed_ggplot2_args$labs,
568!
        themes = parsed_ggplot2_args$theme,
569!
        ggthemes = parsed_ggplot2_args$ggtheme
570
      ))
571
572!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Plot")
573!
      teal.code::eval_code(qenv, plot_call)
574
    })
575
576!
    decorated_output_plot_q <- srv_decorate_teal_data(
577!
      id = "decorator",
578!
      data = output_q,
579!
      decorators = select_decorators(decorators, "plot"),
580!
      expr = quote(plot)
581
    )
582
583!
    plot_r <- reactive(req(decorated_output_plot_q())[["plot"]])
584
585
    # Insert the plot into a plot_with_settings module from teal.widgets
586!
    pws <- teal.widgets::plot_with_settings_srv(
587!
      id = "myplot",
588!
      plot_r = plot_r,
589!
      height = plot_height,
590!
      width = plot_width
591
    )
592
593!
    set_chunk_dims(pws, decorated_output_plot_q)
594
  })
595
}
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
#' @inheritSection teal::example_module Reporting
74
#'
75
#' @examplesShinylive
76
#' library(teal.modules.general)
77
#' interactive <- function() TRUE
78
#' {{ next_example }}
79
#' @examples
80
#' # general data example
81
#' data <- teal_data()
82
#' data <- within(data, {
83
#'   require(nestcolor)
84
#'   CO2 <- data.frame(CO2)
85
#' })
86
#'
87
#' app <- init(
88
#'   data = data,
89
#'   modules = tm_g_bivariate(
90
#'     x = data_extract_spec(
91
#'       dataname = "CO2",
92
#'       select = select_spec(
93
#'         label = "Select variable:",
94
#'         choices = variable_choices(data[["CO2"]]),
95
#'         selected = "conc",
96
#'         fixed = FALSE
97
#'       )
98
#'     ),
99
#'     y = data_extract_spec(
100
#'       dataname = "CO2",
101
#'       select = select_spec(
102
#'         label = "Select variable:",
103
#'         choices = variable_choices(data[["CO2"]]),
104
#'         selected = "uptake",
105
#'         multiple = FALSE,
106
#'         fixed = FALSE
107
#'       )
108
#'     ),
109
#'     row_facet = data_extract_spec(
110
#'       dataname = "CO2",
111
#'       select = select_spec(
112
#'         label = "Select variable:",
113
#'         choices = variable_choices(data[["CO2"]]),
114
#'         selected = "Type",
115
#'         fixed = FALSE
116
#'       )
117
#'     ),
118
#'     col_facet = data_extract_spec(
119
#'       dataname = "CO2",
120
#'       select = select_spec(
121
#'         label = "Select variable:",
122
#'         choices = variable_choices(data[["CO2"]]),
123
#'         selected = "Treatment",
124
#'         fixed = FALSE
125
#'       )
126
#'     )
127
#'   )
128
#' )
129
#' if (interactive()) {
130
#'   shinyApp(app$ui, app$server)
131
#' }
132
#'
133
#' @examplesShinylive
134
#' library(teal.modules.general)
135
#' interactive <- function() TRUE
136
#' {{ next_example }}
137
#' @examples
138
#' # CDISC data example
139
#' data <- teal_data()
140
#' data <- within(data, {
141
#'   require(nestcolor)
142
#'   ADSL <- teal.data::rADSL
143
#' })
144
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
145
#'
146
#' app <- init(
147
#'   data = data,
148
#'   modules = tm_g_bivariate(
149
#'     x = data_extract_spec(
150
#'       dataname = "ADSL",
151
#'       select = select_spec(
152
#'         label = "Select variable:",
153
#'         choices = variable_choices(data[["ADSL"]]),
154
#'         selected = "AGE",
155
#'         fixed = FALSE
156
#'       )
157
#'     ),
158
#'     y = data_extract_spec(
159
#'       dataname = "ADSL",
160
#'       select = select_spec(
161
#'         label = "Select variable:",
162
#'         choices = variable_choices(data[["ADSL"]]),
163
#'         selected = "SEX",
164
#'         multiple = FALSE,
165
#'         fixed = FALSE
166
#'       )
167
#'     ),
168
#'     row_facet = data_extract_spec(
169
#'       dataname = "ADSL",
170
#'       select = select_spec(
171
#'         label = "Select variable:",
172
#'         choices = variable_choices(data[["ADSL"]]),
173
#'         selected = "ARM",
174
#'         fixed = FALSE
175
#'       )
176
#'     ),
177
#'     col_facet = data_extract_spec(
178
#'       dataname = "ADSL",
179
#'       select = select_spec(
180
#'         label = "Select variable:",
181
#'         choices = variable_choices(data[["ADSL"]]),
182
#'         selected = "COUNTRY",
183
#'         fixed = FALSE
184
#'       )
185
#'     )
186
#'   )
187
#' )
188
#' if (interactive()) {
189
#'   shinyApp(app$ui, app$server)
190
#' }
191
#'
192
#' @export
193
#'
194
tm_g_bivariate <- function(label = "Bivariate Plots",
195
                           x,
196
                           y,
197
                           row_facet = NULL,
198
                           col_facet = NULL,
199
                           facet = !is.null(row_facet) || !is.null(col_facet),
200
                           color = NULL,
201
                           fill = NULL,
202
                           size = NULL,
203
                           use_density = FALSE,
204
                           color_settings = FALSE,
205
                           free_x_scales = FALSE,
206
                           free_y_scales = FALSE,
207
                           plot_height = c(600, 200, 2000),
208
                           plot_width = NULL,
209
                           rotate_xaxis_labels = FALSE,
210
                           swap_axes = FALSE,
211
                           ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"),
212
                           ggplot2_args = teal.widgets::ggplot2_args(),
213
                           pre_output = NULL,
214
                           post_output = NULL,
215
                           transformators = list(),
216
                           decorators = list()) {
21718x
  message("Initializing tm_g_bivariate")
218
219
  # Normalize the parameters
22014x
  if (inherits(x, "data_extract_spec")) x <- list(x)
22113x
  if (inherits(y, "data_extract_spec")) y <- list(y)
2221x
  if (inherits(row_facet, "data_extract_spec")) row_facet <- list(row_facet)
2231x
  if (inherits(col_facet, "data_extract_spec")) col_facet <- list(col_facet)
2241x
  if (inherits(color, "data_extract_spec")) color <- list(color)
2251x
  if (inherits(fill, "data_extract_spec")) fill <- list(fill)
2261x
  if (inherits(size, "data_extract_spec")) size <- list(size)
227
228
  # Start of assertions
22918x
  checkmate::assert_string(label)
230
23118x
  checkmate::assert_list(x, types = "data_extract_spec")
23218x
  assert_single_selection(x)
233
23416x
  checkmate::assert_list(y, types = "data_extract_spec")
23516x
  assert_single_selection(y)
236
23714x
  checkmate::assert_list(row_facet, types = "data_extract_spec", null.ok = TRUE)
23814x
  assert_single_selection(row_facet)
239
24014x
  checkmate::assert_list(col_facet, types = "data_extract_spec", null.ok = TRUE)
24114x
  assert_single_selection(col_facet)
242
24314x
  checkmate::assert_flag(facet)
244
24514x
  checkmate::assert_list(color, types = "data_extract_spec", null.ok = TRUE)
24614x
  assert_single_selection(color)
247
24814x
  checkmate::assert_list(fill, types = "data_extract_spec", null.ok = TRUE)
24914x
  assert_single_selection(fill)
250
25114x
  checkmate::assert_list(size, types = "data_extract_spec", null.ok = TRUE)
25214x
  assert_single_selection(size)
253
25414x
  checkmate::assert_flag(use_density)
255
256
  # Determines color, fill & size if they are not explicitly set
25714x
  checkmate::assert_flag(color_settings)
25814x
  if (color_settings) {
2592x
    if (is.null(color)) {
2602x
      color <- x
2612x
      color[[1]]$select <- teal.transform::select_spec(choices = color[[1]]$select$choices, selected = NULL)
262
    }
2632x
    if (is.null(fill)) {
2642x
      fill <- x
2652x
      fill[[1]]$select <- teal.transform::select_spec(choices = fill[[1]]$select$choices, selected = NULL)
266
    }
2672x
    if (is.null(size)) {
2682x
      size <- x
2692x
      size[[1]]$select <- teal.transform::select_spec(choices = size[[1]]$select$choices, selected = NULL)
270
    }
271
  } else {
27212x
    if (!is.null(c(color, fill, size))) {
2733x
      stop("'color_settings' argument needs to be set to TRUE if 'color', 'fill', and/or 'size' is/are supplied.")
274
    }
275
  }
276
27711x
  checkmate::assert_flag(free_x_scales)
27811x
  checkmate::assert_flag(free_y_scales)
279
28011x
  checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE)
28110x
  checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height")
2828x
  checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE)
2837x
  checkmate::assert_numeric(
2847x
    plot_width[1],
2857x
    lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width"
286
  )
287
2885x
  checkmate::assert_flag(rotate_xaxis_labels)
2895x
  checkmate::assert_flag(swap_axes)
290
2915x
  ggtheme <- match.arg(ggtheme)
2925x
  checkmate::assert_class(ggplot2_args, "ggplot2_args")
293
2945x
  checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
2955x
  checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
296
2975x
  assert_decorators(decorators, "plot")
298
  # End of assertions
299
300
  # Make UI args
3015x
  args <- as.list(environment())
302
3035x
  data_extract_list <- list(
3045x
    x = x,
3055x
    y = y,
3065x
    row_facet = row_facet,
3075x
    col_facet = col_facet,
3085x
    color_settings = color_settings,
3095x
    color = color,
3105x
    fill = fill,
3115x
    size = size
312
  )
313
3145x
  ans <- module(
3155x
    label = label,
3165x
    server = srv_g_bivariate,
3175x
    ui = ui_g_bivariate,
3185x
    ui_args = args,
3195x
    server_args = c(
3205x
      data_extract_list,
3215x
      list(plot_height = plot_height, plot_width = plot_width, ggplot2_args = ggplot2_args, decorators = decorators)
322
    ),
3235x
    transformators = transformators,
3245x
    datanames = teal.transform::get_extract_datanames(data_extract_list)
325
  )
3265x
  attr(ans, "teal_bookmarkable") <- TRUE
3275x
  ans
328
}
329
330
# UI function for the bivariate module
331
ui_g_bivariate <- function(id, ...) {
332!
  args <- list(...)
333!
  is_single_dataset_value <- teal.transform::is_single_dataset(
334!
    args$x, args$y, args$row_facet, args$col_facet, args$color, args$fill, args$size
335
  )
336
337!
  ns <- NS(id)
338!
  teal.widgets::standard_layout(
339!
    output = teal.widgets::white_small_well(
340!
      tags$div(teal.widgets::plot_with_settings_ui(id = ns("myplot")))
341
    ),
342!
    encoding = tags$div(
343!
      tags$label("Encodings", class = "text-primary"),
344!
      teal.transform::datanames_input(args[c("x", "y", "row_facet", "col_facet", "color", "fill", "size")]),
345!
      teal.transform::data_extract_ui(
346!
        id = ns("x"),
347!
        label = "X variable",
348!
        data_extract_spec = args$x,
349!
        is_single_dataset = is_single_dataset_value
350
      ),
351!
      teal.transform::data_extract_ui(
352!
        id = ns("y"),
353!
        label = "Y variable",
354!
        data_extract_spec = args$y,
355!
        is_single_dataset = is_single_dataset_value
356
      ),
357!
      conditionalPanel(
358!
        condition =
359!
          "$(\"button[data-id*='-x-dataset'][data-id$='-select']\").text() == '- Nothing selected - ' ||
360!
          $(\"button[data-id*='-y-dataset'][data-id$='-select']\").text() == '- Nothing selected - ' ",
361!
        shinyWidgets::radioGroupButtons(
362!
          inputId = ns("use_density"),
363!
          label = NULL,
364!
          choices = c("frequency", "density"),
365!
          selected = ifelse(args$use_density, "density", "frequency"),
366!
          justified = TRUE
367
        )
368
      ),
369!
      ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")),
370!
      if (!is.null(args$row_facet) || !is.null(args$col_facet)) {
371!
        tags$div(
372!
          class = "data-extract-box",
373!
          tags$br(),
374!
          bslib::input_switch(
375!
            id = ns("facetting"),
376!
            label = "Facetting",
377!
            value = args$facet
378
          ),
379!
          conditionalPanel(
380!
            condition = paste0("input['", ns("facetting"), "']"),
381!
            tags$div(
382!
              if (!is.null(args$row_facet)) {
383!
                teal.transform::data_extract_ui(
384!
                  id = ns("row_facet"),
385!
                  label = "Row facetting variable",
386!
                  data_extract_spec = args$row_facet,
387!
                  is_single_dataset = is_single_dataset_value
388
                )
389
              },
390!
              if (!is.null(args$col_facet)) {
391!
                teal.transform::data_extract_ui(
392!
                  id = ns("col_facet"),
393!
                  label = "Column facetting variable",
394!
                  data_extract_spec = args$col_facet,
395!
                  is_single_dataset = is_single_dataset_value
396
                )
397
              },
398!
              checkboxInput(ns("free_x_scales"), "free x scales", value = args$free_x_scales),
399!
              checkboxInput(ns("free_y_scales"), "free y scales", value = args$free_y_scales)
400
            )
401
          )
402
        )
403
      },
404!
      if (args$color_settings) {
405
        # Put a grey border around the coloring settings
406!
        tags$div(
407!
          class = "data-extract-box",
408!
          tags$label("Color settings"),
409!
          bslib::input_switch(
410!
            id = ns("coloring"),
411!
            label = "Color settings",
412!
            value = TRUE
413
          ),
414!
          conditionalPanel(
415!
            condition = paste0("input['", ns("coloring"), "']"),
416!
            tags$div(
417!
              teal.transform::data_extract_ui(
418!
                id = ns("color"),
419!
                label = "Outline color by variable",
420!
                data_extract_spec = args$color,
421!
                is_single_dataset = is_single_dataset_value
422
              ),
423!
              teal.transform::data_extract_ui(
424!
                id = ns("fill"),
425!
                label = "Fill color by variable",
426!
                data_extract_spec = args$fill,
427!
                is_single_dataset = is_single_dataset_value
428
              ),
429!
              tags$div(
430!
                id = ns("size_settings"),
431!
                teal.transform::data_extract_ui(
432!
                  id = ns("size"),
433!
                  label = "Size of points by variable (only if x and y are numeric)",
434!
                  data_extract_spec = args$size,
435!
                  is_single_dataset = is_single_dataset_value
436
                )
437
              )
438
            )
439
          )
440
        )
441
      },
442!
      bslib::accordion(
443!
        open = TRUE,
444!
        bslib::accordion_panel(
445!
          title = "Plot settings",
446!
          checkboxInput(ns("rotate_xaxis_labels"), "Rotate X axis labels", value = args$rotate_xaxis_labels),
447!
          checkboxInput(ns("swap_axes"), "Swap axes", value = args$swap_axes),
448!
          selectInput(
449!
            inputId = ns("ggtheme"),
450!
            label = "Theme (by ggplot):",
451!
            choices = ggplot_themes,
452!
            selected = args$ggtheme,
453!
            multiple = FALSE
454
          ),
455!
          sliderInput(
456!
            ns("alpha"), "Opacity Scatterplot:",
457!
            min = 0, max = 1,
458!
            step = .05, value = .5, ticks = FALSE
459
          ),
460!
          sliderInput(
461!
            ns("fixed_size"), "Scatterplot point size:",
462!
            min = 1, max = 8,
463!
            step = 1, value = 2, ticks = FALSE
464
          ),
465!
          checkboxInput(ns("add_lines"), "Add lines"),
466
        )
467
      )
468
    ),
469!
    pre_output = args$pre_output,
470!
    post_output = args$post_output
471
  )
472
}
473
474
# Server function for the bivariate module
475
srv_g_bivariate <- function(id,
476
                            data,
477
                            x,
478
                            y,
479
                            row_facet,
480
                            col_facet,
481
                            color_settings = FALSE,
482
                            color,
483
                            fill,
484
                            size,
485
                            plot_height,
486
                            plot_width,
487
                            ggplot2_args,
488
                            decorators) {
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
554!
    anl_merged_q <- reactive({
555!
      obj <- data()
556!
      teal.reporter::teal_card(obj) <-
557!
        c(
558!
          teal.reporter::teal_card(obj),
559!
          teal.reporter::teal_card("## Module's output(s)")
560
        )
561!
      obj |>
562!
        teal.code::eval_code("library(ggplot2);library(dplyr)") |>
563!
        teal.code::eval_code(as.expression(anl_merged_input()$expr))
564
    })
565
566!
    merged <- list(
567!
      anl_input_r = anl_merged_input,
568!
      anl_q_r = anl_merged_q
569
    )
570
571!
    output_q <- reactive({
572!
      teal::validate_inputs(iv_r())
573
574!
      ANL <- merged$anl_q_r()[["ANL"]]
575!
      teal::validate_has_data(ANL, 3)
576
577!
      x_col_vec <- as.vector(merged$anl_input_r()$columns_source$x)
578!
      x_name <- `if`(is.null(x_col_vec), character(0), x_col_vec)
579!
      y_col_vec <- as.vector(merged$anl_input_r()$columns_source$y)
580!
      y_name <- `if`(is.null(y_col_vec), character(0), y_col_vec)
581
582!
      row_facet_name <- as.vector(merged$anl_input_r()$columns_source$row_facet)
583!
      col_facet_name <- as.vector(merged$anl_input_r()$columns_source$col_facet)
584!
      color_name <- if ("color" %in% names(merged$anl_input_r()$columns_source)) {
585!
        as.vector(merged$anl_input_r()$columns_source$color)
586
      } else {
587!
        character(0)
588
      }
589!
      fill_name <- if ("fill" %in% names(merged$anl_input_r()$columns_source)) {
590!
        as.vector(merged$anl_input_r()$columns_source$fill)
591
      } else {
592!
        character(0)
593
      }
594!
      size_name <- if ("size" %in% names(merged$anl_input_r()$columns_source)) {
595!
        as.vector(merged$anl_input_r()$columns_source$size)
596
      } else {
597!
        character(0)
598
      }
599
600!
      use_density <- input$use_density == "density"
601!
      free_x_scales <- input$free_x_scales
602!
      free_y_scales <- input$free_y_scales
603!
      ggtheme <- input$ggtheme
604!
      rotate_xaxis_labels <- input$rotate_xaxis_labels
605!
      swap_axes <- input$swap_axes
606
607!
      is_scatterplot <- all(vapply(ANL[c(x_name, y_name)], is.numeric, logical(1))) &&
608!
        length(x_name) > 0 && length(y_name) > 0
609
610!
      if (is_scatterplot) {
611!
        shinyjs::show("alpha")
612!
        alpha <- input$alpha
613!
        shinyjs::show("add_lines")
614
615!
        if (color_settings && input$coloring) {
616!
          shinyjs::hide("fixed_size")
617!
          shinyjs::show("size_settings")
618!
          size <- NULL
619
        } else {
620!
          shinyjs::show("fixed_size")
621!
          size <- input$fixed_size
622
        }
623
      } else {
624!
        shinyjs::hide("add_lines")
625!
        updateCheckboxInput(session, "add_lines", value = restoreInput(ns("add_lines"), FALSE))
626!
        shinyjs::hide("alpha")
627!
        shinyjs::hide("fixed_size")
628!
        shinyjs::hide("size_settings")
629!
        alpha <- 1
630!
        size <- NULL
631
      }
632
633!
      teal::validate_has_data(ANL[, c(x_name, y_name), drop = FALSE], 3, complete = TRUE, allow_inf = FALSE)
634
635!
      cl <- bivariate_plot_call(
636!
        data_name = "ANL",
637!
        x = x_name,
638!
        y = y_name,
639!
        x_class = ifelse(!identical(x_name, character(0)), class(ANL[[x_name]]), "NULL"),
640!
        y_class = ifelse(!identical(y_name, character(0)), class(ANL[[y_name]]), "NULL"),
641!
        x_label = varname_w_label(x_name, ANL),
642!
        y_label = varname_w_label(y_name, ANL),
643!
        freq = !use_density,
644!
        theme = ggtheme,
645!
        rotate_xaxis_labels = rotate_xaxis_labels,
646!
        swap_axes = swap_axes,
647!
        alpha = alpha,
648!
        size = size,
649!
        ggplot2_args = ggplot2_args
650
      )
651
652!
      facetting <- (isTRUE(input$facetting) && (!is.null(row_facet_name) || !is.null(col_facet_name)))
653
654!
      if (facetting) {
655!
        facet_cl <- facet_ggplot_call(row_facet_name, col_facet_name, free_x_scales, free_y_scales)
656
657!
        if (!is.null(facet_cl)) {
658!
          cl <- call("+", cl, facet_cl)
659
        }
660
      }
661
662!
      if (input$add_lines) {
663!
        cl <- call("+", cl, quote(geom_line(size = 1)))
664
      }
665
666!
      coloring_cl <- NULL
667!
      if (color_settings) {
668!
        if (input$coloring) {
669!
          coloring_cl <- coloring_ggplot_call(
670!
            colour = color_name,
671!
            fill = fill_name,
672!
            size = size_name,
673!
            is_point = any(grepl("geom_point", cl %>% deparse()))
674
          )
675!
          legend_lbls <- substitute(
676!
            expr = labs(color = color_name, fill = fill_name, size = size_name),
677!
            env = list(
678!
              color_name = varname_w_label(color_name, ANL),
679!
              fill_name = varname_w_label(fill_name, ANL),
680!
              size_name = varname_w_label(size_name, ANL)
681
            )
682
          )
683
        }
684!
        if (!is.null(coloring_cl)) {
685!
          cl <- call("+", call("+", cl, coloring_cl), legend_lbls)
686
        }
687
      }
688
689!
      obj <- merged$anl_q_r()
690!
      teal.reporter::teal_card(obj) <- c(teal.reporter::teal_card(obj), "### Plot")
691!
      teal.code::eval_code(obj, substitute(expr = plot <- cl, env = list(cl = cl)))
692
    })
693
694!
    decorated_output_q_facets <- srv_decorate_teal_data(
695!
      "decorator",
696!
      data = output_q,
697!
      decorators = select_decorators(decorators, "plot"),
698!
      expr = reactive({
699!
        ANL <- merged$anl_q_r()[["ANL"]]
700!
        row_facet_name <- as.vector(merged$anl_input_r()$columns_source$row_facet)
701!
        col_facet_name <- as.vector(merged$anl_input_r()$columns_source$col_facet)
702
703
        # Add labels to facets
704!
        nulled_row_facet_name <- varname_w_label(row_facet_name, ANL)
705!
        nulled_col_facet_name <- varname_w_label(col_facet_name, ANL)
706!
        facetting <- (isTRUE(input$facetting) && (!is.null(row_facet_name) || !is.null(col_facet_name)))
707!
        without_facet <- (is.null(nulled_row_facet_name) && is.null(nulled_col_facet_name)) || !facetting
708
709!
        print_call <- if (without_facet) {
710!
          quote(plot)
711
        } else {
712!
          substitute(
713!
            expr = {
714!
              teal.modules.general::add_facet_labels(
715!
                plot,
716!
                xfacet_label = nulled_col_facet_name,
717!
                yfacet_label = nulled_row_facet_name
718
              )
719
            },
720!
            env = list(nulled_col_facet_name = nulled_col_facet_name, nulled_row_facet_name = nulled_row_facet_name)
721
          )
722
        }
723!
        print_call
724
      })
725
    )
726
727!
    plot_r <- reactive({
728!
      req(decorated_output_q_facets())[["plot"]]
729
    })
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!
    set_chunk_dims(pws, decorated_output_q_facets)
739
  })
740
}
741
742
# Get Substituted ggplot call
743
bivariate_plot_call <- function(data_name,
744
                                x = character(0),
745
                                y = character(0),
746
                                x_class = "NULL",
747
                                y_class = "NULL",
748
                                x_label = NULL,
749
                                y_label = NULL,
750
                                freq = TRUE,
751
                                theme = "gray",
752
                                rotate_xaxis_labels = FALSE,
753
                                swap_axes = FALSE,
754
                                alpha = double(0),
755
                                size = 2,
756
                                ggplot2_args = teal.widgets::ggplot2_args()) {
757!
  supported_types <- c("NULL", "numeric", "integer", "factor", "character", "logical", "ordered")
758!
  validate(need(x_class %in% supported_types, paste0("Data type '", x_class, "' is not supported.")))
759!
  validate(need(y_class %in% supported_types, paste0("Data type '", y_class, "' is not supported.")))
760
761
762!
  if (identical(x, character(0))) {
763!
    x <- x_label <- "-"
764
  } else {
765!
    x <- if (is.call(x)) x else as.name(x)
766
  }
767!
  if (identical(y, character(0))) {
768!
    y <- y_label <- "-"
769
  } else {
770!
    y <- if (is.call(y)) y else as.name(y)
771
  }
772
773!
  bivariate_ggplot_call(
774!
    x_class = x_class,
775!
    y_class = y_class,
776!
    freq = freq,
777!
    theme = theme,
778!
    rotate_xaxis_labels = rotate_xaxis_labels,
779!
    swap_axes = swap_axes,
780!
    alpha = alpha,
781!
    size = size,
782!
    ggplot2_args = ggplot2_args,
783!
    x = x,
784!
    y = y,
785!
    xlab = x_label,
786!
    ylab = y_label,
787!
    data_name = data_name
788
  )
789
}
790
791
# Create ggplot part of plot call
792
# Due to the type of the x and y variable the plot type is chosen
793
bivariate_ggplot_call <- function(x_class,
794
                                  y_class,
795
                                  freq = TRUE,
796
                                  theme = "gray",
797
                                  rotate_xaxis_labels = FALSE,
798
                                  swap_axes = FALSE,
799
                                  size = double(0),
800
                                  alpha = double(0),
801
                                  x = NULL,
802
                                  y = NULL,
803
                                  xlab = "-",
804
                                  ylab = "-",
805
                                  data_name = "ANL",
806
                                  ggplot2_args = teal.widgets::ggplot2_args()) {
80746x
  x_class <- switch(x_class,
80846x
    "character" = ,
80946x
    "ordered" = ,
81046x
    "logical" = ,
81146x
    "factor" = "factor",
81246x
    "integer" = ,
81346x
    "numeric" = "numeric",
81446x
    "NULL" = "NULL",
81546x
    stop("unsupported x_class: ", x_class)
816
  )
81746x
  y_class <- switch(y_class,
81846x
    "character" = ,
81946x
    "ordered" = ,
82046x
    "logical" = ,
82146x
    "factor" = "factor",
82246x
    "integer" = ,
82346x
    "numeric" = "numeric",
82446x
    "NULL" = "NULL",
82546x
    stop("unsupported y_class: ", y_class)
826
  )
827
82846x
  if (all(c(x_class, y_class) == "NULL")) {
829!
    stop("either x or y is required")
830
  }
831
83246x
  reduce_plot_call <- function(...) {
833112x
    args <- Filter(Negate(is.null), list(...))
834112x
    Reduce(function(x, y) call("+", x, y), args)
835
  }
836
83746x
  plot_call <- substitute(ggplot2::ggplot(data_name), env = list(data_name = as.name(data_name)))
838
839
  # Single data plots
84046x
  if (x_class == "numeric" && y_class == "NULL") {
8416x
    plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = xval), env = list(xval = x)))
842
8436x
    if (freq) {
8444x
      plot_call <- reduce_plot_call(
8454x
        plot_call,
8464x
        quote(ggplot2::geom_histogram(bins = 30)),
8474x
        quote(ggplot2::ylab("Frequency"))
848
      )
849
    } else {
8502x
      plot_call <- reduce_plot_call(
8512x
        plot_call,
8522x
        quote(ggplot2::geom_histogram(bins = 30, ggplot2::aes(y = ggplot2::after_stat(density)))),
8532x
        quote(ggplot2::geom_density(ggplot2::aes(y = ggplot2::after_stat(density)))),
8542x
        quote(ggplot2::ylab("Density"))
855
      )
856
    }
85740x
  } else if (x_class == "NULL" && y_class == "numeric") {
8586x
    plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = yval), env = list(yval = y)))
859
8606x
    if (freq) {
8614x
      plot_call <- reduce_plot_call(
8624x
        plot_call,
8634x
        quote(ggplot2::geom_histogram(bins = 30)),
8644x
        quote(ggplot2::ylab("Frequency"))
865
      )
866
    } else {
8672x
      plot_call <- reduce_plot_call(
8682x
        plot_call,
8692x
        quote(ggplot2::geom_histogram(bins = 30, ggplot2::aes(y = ggplot2::after_stat(density)))),
8702x
        quote(ggplot2::geom_density(ggplot2::aes(y = ggplot2::after_stat(density)))),
8712x
        quote(ggplot2::ylab("Density"))
872
      )
873
    }
87434x
  } else if (x_class == "factor" && y_class == "NULL") {
8754x
    plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = xval), env = list(xval = x)))
876
8774x
    if (freq) {
8782x
      plot_call <- reduce_plot_call(
8792x
        plot_call,
8802x
        quote(ggplot2::geom_bar()),
8812x
        quote(ggplot2::ylab("Frequency"))
882
      )
883
    } else {
8842x
      plot_call <- reduce_plot_call(
8852x
        plot_call,
8862x
        quote(ggplot2::geom_bar(ggplot2::aes(y = ggplot2::after_stat(prop), group = 1))),
8872x
        quote(ggplot2::ylab("Fraction"))
888
      )
889
    }
89030x
  } else if (x_class == "NULL" && y_class == "factor") {
8914x
    plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = yval), env = list(yval = y)))
892
8934x
    if (freq) {
8942x
      plot_call <- reduce_plot_call(
8952x
        plot_call,
8962x
        quote(ggplot2::geom_bar()),
8972x
        quote(ggplot2::ylab("Frequency"))
898
      )
899
    } else {
9002x
      plot_call <- reduce_plot_call(
9012x
        plot_call,
9022x
        quote(ggplot2::geom_bar(ggplot2::aes(y = ggplot2::after_stat(prop), group = 1))),
9032x
        quote(ggplot2::ylab("Fraction"))
904
      )
905
    }
906
    # Numeric Plots
90726x
  } else if (x_class == "numeric" && y_class == "numeric") {
9082x
    plot_call <- reduce_plot_call(
9092x
      plot_call,
9102x
      substitute(ggplot2::aes(x = xval, y = yval), env = list(xval = x, yval = y)),
911
      # pch = 21 for consistent coloring behaviour b/w all geoms (outline and fill properties)
9122x
      `if`(
9132x
        !is.null(size),
9142x
        substitute(
9152x
          ggplot2::geom_point(alpha = alphaval, size = sizeval, pch = 21),
9162x
          env = list(alphaval = alpha, sizeval = size)
917
        ),
9182x
        substitute(
9192x
          ggplot2::geom_point(alpha = alphaval, pch = 21),
9202x
          env = list(alphaval = alpha)
921
        )
922
      )
923
    )
92424x
  } else if ((x_class == "numeric" && y_class == "factor") || (x_class == "factor" && y_class == "numeric")) {
9256x
    plot_call <- reduce_plot_call(
9266x
      plot_call,
9276x
      substitute(ggplot2::aes(x = xval, y = yval), env = list(xval = x, yval = y)),
9286x
      quote(ggplot2::geom_boxplot())
929
    )
930
    # Factor and character plots
93118x
  } else if (x_class == "factor" && y_class == "factor") {
93218x
    plot_call <- reduce_plot_call(
93318x
      plot_call,
93418x
      substitute(
93518x
        teal.modules.general::geom_mosaic(ggplot2::aes(x = xval, fill = yval)),
93618x
        env = list(xval = x, yval = y)
937
      )
938
    )
939
  } else {
940!
    stop("x y type combination not allowed")
941
  }
942
94346x
  labs_base <- if (x_class == "NULL") {
94410x
    list(x = substitute(ylab, list(ylab = ylab)))
94546x
  } else if (y_class == "NULL") {
94610x
    list(x = substitute(xlab, list(xlab = xlab)))
947
  } else {
94826x
    list(
94926x
      x = substitute(xlab, list(xlab = xlab)),
95026x
      y = substitute(ylab, list(ylab = ylab))
951
    )
952
  }
953
95446x
  dev_ggplot2_args <- teal.widgets::ggplot2_args(labs = labs_base)
955
95646x
  if (rotate_xaxis_labels) {
957!
    dev_ggplot2_args$theme <- list(axis.text.x = quote(ggplot2::element_text(angle = 45, hjust = 1)))
958
  }
959
96046x
  all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
96146x
    user_plot = ggplot2_args,
96246x
    module_plot = dev_ggplot2_args
963
  )
964
96546x
  parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(all_ggplot2_args, ggtheme = theme)
966
96746x
  plot_call <- reduce_plot_call(
96846x
    plot_call,
96946x
    parsed_ggplot2_args$labs,
97046x
    parsed_ggplot2_args$ggtheme,
97146x
    parsed_ggplot2_args$theme
972
  )
973
97446x
  if (swap_axes) {
975!
    plot_call <- reduce_plot_call(plot_call, quote(coord_flip()))
976
  }
977
97846x
  plot_call
979
}
980
981
# Create facet call
982
facet_ggplot_call <- function(row_facet = character(0),
983
                              col_facet = character(0),
984
                              free_x_scales = FALSE,
985
                              free_y_scales = FALSE) {
986!
  scales <- if (free_x_scales && free_y_scales) {
987!
    "free"
988!
  } else if (free_x_scales) {
989!
    "free_x"
990!
  } else if (free_y_scales) {
991!
    "free_y"
992
  } else {
993!
    "fixed"
994
  }
995
996!
  if (identical(row_facet, character(0)) && identical(col_facet, character(0))) {
997!
    NULL
998!
  } else if (!identical(row_facet, character(0)) && !identical(col_facet, character(0))) {
999!
    call(
1000!
      "facet_grid",
1001!
      rows = call_fun_dots("vars", row_facet),
1002!
      cols = call_fun_dots("vars", col_facet),
1003!
      scales = scales
1004
    )
1005!
  } else if (identical(row_facet, character(0)) && !identical(col_facet, character(0))) {
1006!
    call("facet_grid", cols = call_fun_dots("vars", col_facet), scales = scales)
1007!
  } else if (!identical(row_facet, character(0)) && identical(col_facet, character(0))) {
1008!
    call("facet_grid", rows = call_fun_dots("vars", row_facet), scales = scales)
1009
  }
1010
}
1011
1012
coloring_ggplot_call <- function(colour,
1013
                                 fill,
1014
                                 size,
1015
                                 is_point = FALSE) {
1016
  if (
101715x
    !identical(colour, character(0)) &&
101815x
      !identical(fill, character(0)) &&
101915x
      is_point &&
102015x
      !identical(size, character(0))
1021
  ) {
10221x
    substitute(
10231x
      expr = ggplot2::aes(colour = colour_name, fill = fill_name, size = size_name),
10241x
      env = list(colour_name = as.name(colour), fill_name = as.name(fill), size_name = as.name(size))
1025
    )
1026
  } else if (
102714x
    identical(colour, character(0)) &&
102814x
      !identical(fill, character(0)) &&
102914x
      is_point &&
103014x
      identical(size, character(0))
1031
  ) {
10321x
    substitute(expr = ggplot2::aes(fill = fill_name), env = list(fill_name = as.name(fill)))
1033
  } else if (
103413x
    !identical(colour, character(0)) &&
103513x
      !identical(fill, character(0)) &&
103613x
      (!is_point || identical(size, character(0)))
1037
  ) {
10383x
    substitute(
10393x
      expr = ggplot2::aes(colour = colour_name, fill = fill_name),
10403x
      env = list(colour_name = as.name(colour), fill_name = as.name(fill))
1041
    )
1042
  } else if (
104310x
    !identical(colour, character(0)) &&
104410x
      identical(fill, character(0)) &&
104510x
      (!is_point || identical(size, character(0)))
1046
  ) {
10471x
    substitute(expr = ggplot2::aes(colour = colour_name), env = list(colour_name = as.name(colour)))
1048
  } else if (
10499x
    identical(colour, character(0)) &&
10509x
      !identical(fill, character(0)) &&
10519x
      (!is_point || identical(size, character(0)))
1052
  ) {
10532x
    substitute(expr = ggplot2::aes(fill = fill_name), env = list(fill_name = as.name(fill)))
1054
  } else if (
10557x
    identical(colour, character(0)) &&
10567x
      identical(fill, character(0)) &&
10577x
      is_point &&
10587x
      !identical(size, character(0))
1059
  ) {
10601x
    substitute(expr = ggplot2::aes(size = size_name), env = list(size_name = as.name(size)))
1061
  } else if (
10626x
    !identical(colour, character(0)) &&
10636x
      identical(fill, character(0)) &&
10646x
      is_point &&
10656x
      !identical(size, character(0))
1066
  ) {
10671x
    substitute(
10681x
      expr = ggplot2::aes(colour = colour_name, size = size_name),
10691x
      env = list(colour_name = as.name(colour), size_name = as.name(size))
1070
    )
1071
  } else if (
10725x
    identical(colour, character(0)) &&
10735x
      !identical(fill, character(0)) &&
10745x
      is_point &&
10755x
      !identical(size, character(0))
1076
  ) {
10771x
    substitute(
10781x
      expr = ggplot2::aes(colour = colour_name, fill = fill_name, size = size_name),
10791x
      env = list(colour_name = as.name(fill), fill_name = as.name(fill), size_name = as.name(size))
1080
    )
1081
  } else {
10824x
    NULL
1083
  }
1084
}
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
#' @inheritSection teal::example_module Reporting
52
#'
53
#' @examplesShinylive
54
#' library(teal.modules.general)
55
#' interactive <- function() TRUE
56
#' {{ next_example }}
57
#' @examples
58
#' # general data example
59
#' data <- teal_data()
60
#' data <- within(data, {
61
#'   require(nestcolor)
62
#'   CO2 <- CO2
63
#'   factors <- names(Filter(isTRUE, vapply(CO2, is.factor, logical(1L))))
64
#'   CO2[factors] <- lapply(CO2[factors], as.character)
65
#' })
66
#'
67
#' app <- init(
68
#'   data = data,
69
#'   modules = modules(
70
#'     tm_g_association(
71
#'       ref = data_extract_spec(
72
#'         dataname = "CO2",
73
#'         select = select_spec(
74
#'           label = "Select variable:",
75
#'           choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
76
#'           selected = "Plant",
77
#'           fixed = FALSE
78
#'         )
79
#'       ),
80
#'       vars = data_extract_spec(
81
#'         dataname = "CO2",
82
#'         select = select_spec(
83
#'           label = "Select variables:",
84
#'           choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
85
#'           selected = "Treatment",
86
#'           multiple = TRUE,
87
#'           fixed = FALSE
88
#'         )
89
#'       )
90
#'     )
91
#'   )
92
#' )
93
#' if (interactive()) {
94
#'   shinyApp(app$ui, app$server)
95
#' }
96
#'
97
#' @examplesShinylive
98
#' library(teal.modules.general)
99
#' interactive <- function() TRUE
100
#' {{ next_example }}
101
#' @examples
102
#' # CDISC data example
103
#' data <- teal_data()
104
#' data <- within(data, {
105
#'   require(nestcolor)
106
#'   ADSL <- teal.data::rADSL
107
#' })
108
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
109
#'
110
#' app <- init(
111
#'   data = data,
112
#'   modules = modules(
113
#'     tm_g_association(
114
#'       ref = data_extract_spec(
115
#'         dataname = "ADSL",
116
#'         select = select_spec(
117
#'           label = "Select variable:",
118
#'           choices = variable_choices(
119
#'             data[["ADSL"]],
120
#'             c("SEX", "RACE", "COUNTRY", "ARM", "STRATA1", "STRATA2", "ITTFL", "BMRKR2")
121
#'           ),
122
#'           selected = "RACE",
123
#'           fixed = FALSE
124
#'         )
125
#'       ),
126
#'       vars = data_extract_spec(
127
#'         dataname = "ADSL",
128
#'         select = select_spec(
129
#'           label = "Select variables:",
130
#'           choices = variable_choices(
131
#'             data[["ADSL"]],
132
#'             c("SEX", "RACE", "COUNTRY", "ARM", "STRATA1", "STRATA2", "ITTFL", "BMRKR2")
133
#'           ),
134
#'           selected = "BMRKR2",
135
#'           multiple = TRUE,
136
#'           fixed = FALSE
137
#'         )
138
#'       )
139
#'     )
140
#'   )
141
#' )
142
#' if (interactive()) {
143
#'   shinyApp(app$ui, app$server)
144
#' }
145
#'
146
#' @export
147
#'
148
tm_g_association <- function(label = "Association",
149
                             ref,
150
                             vars,
151
                             show_association = TRUE,
152
                             plot_height = c(600, 400, 5000),
153
                             plot_width = NULL,
154
                             distribution_theme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), # nolint line_length_linter.
155
                             association_theme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"), # nolint line_length_linter.
156
                             pre_output = NULL,
157
                             post_output = NULL,
158
                             ggplot2_args = teal.widgets::ggplot2_args(),
159
                             transformators = list(),
160
                             decorators = list()) {
161!
  message("Initializing tm_g_association")
162
163
  # Normalize the parameters
164!
  if (inherits(ref, "data_extract_spec")) ref <- list(ref)
165!
  if (inherits(vars, "data_extract_spec")) vars <- list(vars)
166!
  if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args)
167
168
  # Start of assertions
169!
  checkmate::assert_string(label)
170
171!
  checkmate::assert_list(ref, types = "data_extract_spec")
172!
  if (!all(vapply(ref, function(x) !x$select$multiple, logical(1)))) {
173!
    stop("'ref' should not allow multiple selection")
174
  }
175
176!
  checkmate::assert_list(vars, types = "data_extract_spec")
177!
  checkmate::assert_flag(show_association)
178
179!
  checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE)
180!
  checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height")
181!
  checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE)
182!
  checkmate::assert_numeric(
183!
    plot_width[1],
184!
    lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width"
185
  )
186
187!
  distribution_theme <- match.arg(distribution_theme)
188!
  association_theme <- match.arg(association_theme)
189
190!
  checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
191!
  checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
192
193!
  plot_choices <- c("Bivariate1", "Bivariate2")
194!
  checkmate::assert_list(ggplot2_args, types = "ggplot2_args")
195!
  checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices))
196
197!
  assert_decorators(decorators, "plot")
198
  # End of assertions
199
200
  # Make UI args
201!
  args <- as.list(environment())
202
203!
  data_extract_list <- list(
204!
    ref = ref,
205!
    vars = vars
206
  )
207
208!
  ans <- module(
209!
    label = label,
210!
    server = srv_tm_g_association,
211!
    ui = ui_tm_g_association,
212!
    ui_args = args,
213!
    server_args = c(
214!
      data_extract_list,
215!
      list(plot_height = plot_height, plot_width = plot_width, ggplot2_args = ggplot2_args, decorators = decorators)
216
    ),
217!
    transformators = transformators,
218!
    datanames = teal.transform::get_extract_datanames(data_extract_list)
219
  )
220!
  attr(ans, "teal_bookmarkable") <- TRUE
221!
  ans
222
}
223
224
# UI function for the association module
225
ui_tm_g_association <- function(id, ...) {
226!
  ns <- NS(id)
227!
  args <- list(...)
228!
  is_single_dataset_value <- teal.transform::is_single_dataset(args$ref, args$vars)
229
230!
  teal.widgets::standard_layout(
231!
    output = teal.widgets::white_small_well(
232!
      textOutput(ns("title")),
233!
      tags$br(),
234!
      teal.widgets::plot_with_settings_ui(id = ns("myplot"))
235
    ),
236!
    encoding = tags$div(
237!
      tags$label("Encodings", class = "text-primary"),
238!
      teal.transform::datanames_input(args[c("ref", "vars")]),
239!
      teal.transform::data_extract_ui(
240!
        id = ns("ref"),
241!
        label = "Reference variable",
242!
        data_extract_spec = args$ref,
243!
        is_single_dataset = is_single_dataset_value
244
      ),
245!
      teal.transform::data_extract_ui(
246!
        id = ns("vars"),
247!
        label = "Associated variables",
248!
        data_extract_spec = args$vars,
249!
        is_single_dataset = is_single_dataset_value
250
      ),
251!
      checkboxInput(
252!
        ns("association"),
253!
        "Association with reference variable",
254!
        value = args$show_association
255
      ),
256!
      checkboxInput(
257!
        ns("show_dist"),
258!
        "Scaled frequencies",
259!
        value = FALSE
260
      ),
261!
      checkboxInput(
262!
        ns("log_transformation"),
263!
        "Log transformed",
264!
        value = FALSE
265
      ),
266!
      ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")),
267!
      bslib::accordion(
268!
        open = TRUE,
269!
        bslib::accordion_panel(
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!
    pre_output = args$pre_output,
293!
    post_output = args$post_output
294
  )
295
}
296
297
# Server function for the association module
298
srv_tm_g_association <- function(id,
299
                                 data,
300
                                 ref,
301
                                 vars,
302
                                 plot_height,
303
                                 plot_width,
304
                                 ggplot2_args,
305
                                 decorators) {
306!
  checkmate::assert_class(data, "reactive")
307!
  checkmate::assert_class(isolate(data()), "teal_data")
308
309!
  moduleServer(id, function(input, output, session) {
310!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
311
312!
    selector_list <- teal.transform::data_extract_multiple_srv(
313!
      data_extract = list(ref = ref, vars = vars),
314!
      datasets = data,
315!
      select_validation_rule = list(
316!
        ref = shinyvalidate::compose_rules(
317!
          shinyvalidate::sv_required("A reference variable needs to be selected."),
318!
          ~ if ((.) %in% selector_list()$vars()$select) {
319!
            "Associated variables and reference variable cannot overlap"
320
          }
321
        ),
322!
        vars = shinyvalidate::compose_rules(
323!
          shinyvalidate::sv_required("An associated variable needs to be selected."),
324!
          ~ if (length(selector_list()$ref()$select) != 0 && selector_list()$ref()$select %in% (.)) {
325!
            "Associated variables and reference variable cannot overlap"
326
          }
327
        )
328
      )
329
    )
330
331!
    iv_r <- reactive({
332!
      iv <- shinyvalidate::InputValidator$new()
333!
      teal.transform::compose_and_enable_validators(iv, selector_list)
334
    })
335
336!
    anl_merged_input <- teal.transform::merge_expression_srv(
337!
      datasets = data,
338!
      selector_list = selector_list
339
    )
340
341!
    qenv <- reactive({
342!
      obj <- data()
343!
      teal.reporter::teal_card(obj) <-
344!
        c(
345!
          teal.reporter::teal_card(obj),
346!
          teal.reporter::teal_card("## Module's output(s)")
347
        )
348!
      teal.code::eval_code(obj, "library(ggplot2);library(dplyr)")
349
    })
350!
    anl_merged_q <- reactive({
351!
      req(anl_merged_input())
352!
      qenv() %>% teal.code::eval_code(as.expression(anl_merged_input()$expr))
353
    })
354
355!
    merged <- list(
356!
      anl_input_r = anl_merged_input,
357!
      anl_q_r = anl_merged_q
358
    )
359
360!
    output_q <- reactive({
361!
      teal::validate_inputs(iv_r())
362
363!
      ANL <- merged$anl_q_r()[["ANL"]]
364!
      teal::validate_has_data(ANL, 3)
365
366!
      vars_names <- merged$anl_input_r()$columns_source$vars
367
368!
      ref_name <- as.vector(merged$anl_input_r()$columns_source$ref)
369!
      association <- input$association
370!
      show_dist <- input$show_dist
371!
      log_transformation <- input$log_transformation
372!
      rotate_xaxis_labels <- input$rotate_xaxis_labels
373!
      swap_axes <- input$swap_axes
374!
      distribution_theme <- input$distribution_theme
375!
      association_theme <- input$association_theme
376
377!
      is_scatterplot <- is.numeric(ANL[[ref_name]]) && any(vapply(ANL[vars_names], is.numeric, logical(1)))
378!
      if (is_scatterplot) {
379!
        shinyjs::show("alpha")
380!
        shinyjs::show("size")
381!
        alpha <- input$alpha
382!
        size <- input$size
383
      } else {
384!
        shinyjs::hide("alpha")
385!
        shinyjs::hide("size")
386!
        alpha <- 0.5
387!
        size <- 2
388
      }
389
390!
      teal::validate_has_data(ANL[, c(ref_name, vars_names)], 3, complete = TRUE, allow_inf = FALSE)
391
392
      # reference
393!
      ref_class <- class(ANL[[ref_name]])[1]
394!
      if (is.numeric(ANL[[ref_name]]) && log_transformation) {
395
        # works for both integers and doubles
396!
        ref_cl_name <- call("log", as.name(ref_name))
397!
        ref_cl_lbl <- varname_w_label(ref_name, ANL, prefix = "Log of ")
398
      } else {
399
        # silently ignore when non-numeric even if `log` is selected because some
400
        # variables may be numeric and others not
401!
        ref_cl_name <- as.name(ref_name)
402!
        ref_cl_lbl <- varname_w_label(ref_name, ANL)
403
      }
404
405!
      user_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
406!
        user_plot = ggplot2_args[["Bivariate1"]],
407!
        user_default = ggplot2_args$default
408
      )
409
410!
      ref_call <- bivariate_plot_call(
411!
        data_name = "ANL",
412!
        x = ref_cl_name,
413!
        x_class = ref_class,
414!
        x_label = ref_cl_lbl,
415!
        freq = !show_dist,
416!
        theme = distribution_theme,
417!
        rotate_xaxis_labels = rotate_xaxis_labels,
418!
        swap_axes = FALSE,
419!
        size = size,
420!
        alpha = alpha,
421!
        ggplot2_args = user_ggplot2_args
422
      )
423
424
      # association
425!
      ref_class_cov <- ifelse(association, ref_class, "NULL")
426
427!
      var_calls <- lapply(vars_names, function(var_i) {
428!
        var_class <- class(ANL[[var_i]])[1]
429!
        if (is.numeric(ANL[[var_i]]) && log_transformation) {
430
          # works for both integers and doubles
431!
          var_cl_name <- call("log", as.name(var_i))
432!
          var_cl_lbl <- varname_w_label(var_i, ANL, prefix = "Log of ")
433
        } else {
434
          # silently ignore when non-numeric even if `log` is selected because some
435
          # variables may be numeric and others not
436!
          var_cl_name <- as.name(var_i)
437!
          var_cl_lbl <- varname_w_label(var_i, ANL)
438
        }
439
440!
        user_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
441!
          user_plot = ggplot2_args[["Bivariate2"]],
442!
          user_default = ggplot2_args$default
443
        )
444
445!
        bivariate_plot_call(
446!
          data_name = "ANL",
447!
          x = ref_cl_name,
448!
          y = var_cl_name,
449!
          x_class = ref_class_cov,
450!
          y_class = var_class,
451!
          x_label = ref_cl_lbl,
452!
          y_label = var_cl_lbl,
453!
          theme = association_theme,
454!
          freq = !show_dist,
455!
          rotate_xaxis_labels = rotate_xaxis_labels,
456!
          swap_axes = swap_axes,
457!
          alpha = alpha,
458!
          size = size,
459!
          ggplot2_args = user_ggplot2_args
460
        )
461
      })
462
463
      # helper function to format variable name
464!
      format_varnames <- function(x) {
465!
        if (is.numeric(ANL[[x]]) && log_transformation) {
466!
          varname_w_label(x, ANL, prefix = "Log of ")
467
        } else {
468!
          varname_w_label(x, ANL)
469
        }
470
      }
471!
      new_title <-
472!
        if (association) {
473!
          switch(as.character(length(vars_names)),
474!
            "0" = sprintf("Value distribution for %s", ref_cl_lbl),
475!
            "1" = sprintf(
476!
              "Association between %s and %s",
477!
              ref_cl_lbl,
478!
              format_varnames(vars_names)
479
            ),
480!
            sprintf(
481!
              "Associations between %s and: %s",
482!
              ref_cl_lbl,
483!
              paste(lapply(vars_names, format_varnames), collapse = ", ")
484
            )
485
          )
486
        } else {
487!
          switch(as.character(length(vars_names)),
488!
            "0" = sprintf("Value distribution for %s", ref_cl_lbl),
489!
            sprintf(
490!
              "Value distributions for %s and %s",
491!
              ref_cl_lbl,
492!
              paste(lapply(vars_names, format_varnames), collapse = ", ")
493
            )
494
          )
495
        }
496!
      obj <- merged$anl_q_r()
497!
      teal.reporter::teal_card(obj) <- c(teal.reporter::teal_card(obj), "### Plot")
498!
      teal.code::eval_code(
499!
        obj,
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 <- gridExtra::arrangeGrob(grobs = plots, ncol = 1)
510
            },
511!
            env = list(
512!
              plot_calls = do.call(
513!
                "call",
514!
                c(list("list", ref_call), unname(var_calls)),
515!
                quote = TRUE
516
              )
517
            )
518
          )
519
        )
520
    })
521
522!
    decorated_output_grob_q <- srv_decorate_teal_data(
523!
      id = "decorator",
524!
      data = output_q,
525!
      decorators = select_decorators(decorators, "plot"),
526!
      expr = quote({
527!
        grid::grid.newpage()
528!
        grid::grid.draw(plot)
529
      })
530
    )
531
532!
    plot_r <- reactive({
533!
      req(iv_r()$is_valid())
534!
      req(decorated_output_grob_q())[["plot"]]
535
    })
536
537!
    pws <- teal.widgets::plot_with_settings_srv(
538!
      id = "myplot",
539!
      plot_r = plot_r,
540!
      height = plot_height,
541!
      width = plot_width
542
    )
543
544!
    output$title <- renderText(output_q()[["title"]])
545
546!
    set_chunk_dims(pws, decorated_output_grob_q)
547
  })
548
}
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` (`ggplot`)
24
#' - `combination_plot` (`grob` created with [ggplot2::ggplotGrob()])
25
#' - `by_variable_plot` (`ggplot`)
26
#' - `by_subject_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_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_variable_plot = teal_transform_module(...) # applied only to `by_variable_plot` output
39
#'      by_subject_plot = teal_transform_module(...) # applied only to `by_subject_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
#' @inheritSection teal::example_module Reporting
51
#'
52
#' @examplesShinylive
53
#' library(teal.modules.general)
54
#' interactive <- function() TRUE
55
#' {{ next_example }}
56
#' @examples
57
#' # general example data
58
#' data <- teal_data()
59
#' data <- within(data, {
60
#'   require(nestcolor)
61
#'
62
#'   add_nas <- function(x) {
63
#'     x[sample(seq_along(x), floor(length(x) * runif(1, .05, .17)))] <- NA
64
#'     x
65
#'   }
66
#'
67
#'   iris <- iris
68
#'   mtcars <- mtcars
69
#'
70
#'   iris[] <- lapply(iris, add_nas)
71
#'   mtcars[] <- lapply(mtcars, add_nas)
72
#'   mtcars[["cyl"]] <- as.factor(mtcars[["cyl"]])
73
#'   mtcars[["gear"]] <- as.factor(mtcars[["gear"]])
74
#' })
75
#'
76
#' app <- init(
77
#'   data = data,
78
#'   modules = modules(
79
#'     tm_missing_data(parent_dataname = "mtcars")
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
#' # CDISC example data
92
#' data <- teal_data()
93
#' data <- within(data, {
94
#'   require(nestcolor)
95
#'   ADSL <- teal.data::rADSL
96
#'   ADRS <- rADRS
97
#' })
98
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
99
#'
100
#' app <- init(
101
#'   data = data,
102
#'   modules = modules(
103
#'     tm_missing_data()
104
#'   )
105
#' )
106
#' if (interactive()) {
107
#'   shinyApp(app$ui, app$server)
108
#' }
109
#'
110
#' @export
111
#'
112
tm_missing_data <- function(label = "Missing data",
113
                            plot_height = c(600, 400, 5000),
114
                            plot_width = NULL,
115
                            datanames = "all",
116
                            parent_dataname = "ADSL",
117
                            ggtheme = c("classic", "gray", "bw", "linedraw", "light", "dark", "minimal", "void"),
118
                            ggplot2_args = list(
119
                              "Combinations Hist" = teal.widgets::ggplot2_args(labs = list(caption = NULL)),
120
                              "Combinations Main" = teal.widgets::ggplot2_args(labs = list(title = NULL))
121
                            ),
122
                            pre_output = NULL,
123
                            post_output = NULL,
124
                            transformators = list(),
125
                            decorators = list()) {
126!
  message("Initializing tm_missing_data")
127
128
  # Normalize the parameters
129!
  if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args)
130
131
  # Start of assertions
132!
  checkmate::assert_string(label)
133
134!
  checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE)
135!
  checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height")
136!
  checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE)
137!
  checkmate::assert_numeric(
138!
    plot_width[1],
139!
    lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width"
140
  )
141
142!
  checkmate::assert_character(datanames, min.len = 0, min.chars = 1, null.ok = TRUE)
143!
  checkmate::assert_character(parent_dataname, min.len = 0, max.len = 1)
144!
  ggtheme <- match.arg(ggtheme)
145
146!
  plot_choices <- c("Summary Obs", "Summary Patients", "Combinations Main", "Combinations Hist", "By Subject")
147!
  checkmate::assert_list(ggplot2_args, types = "ggplot2_args")
148!
  checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices))
149
150!
  checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
151!
  checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
152
153!
  assert_decorators(decorators, names = c("summary_plot", "combination_plot", "by_subject_plot"))
154
  # End of assertions
155
156!
  datanames_module <- if (identical(datanames, "all") || is.null(datanames)) {
157!
    datanames
158
  } else {
159!
    union(datanames, parent_dataname)
160
  }
161
162!
  ans <- module(
163!
    label,
164!
    server = srv_page_missing_data,
165!
    datanames = datanames_module,
166!
    server_args = list(
167!
      datanames = if (is.null(datanames)) "all" else datanames,
168!
      parent_dataname = parent_dataname,
169!
      plot_height = plot_height,
170!
      plot_width = plot_width,
171!
      ggplot2_args = ggplot2_args,
172!
      ggtheme = ggtheme,
173!
      decorators = decorators
174
    ),
175!
    ui = ui_page_missing_data,
176!
    transformators = transformators,
177!
    ui_args = list(pre_output = pre_output, post_output = post_output)
178
  )
179!
  attr(ans, "teal_bookmarkable") <- TRUE
180!
  ans
181
}
182
183
# UI function for the missing data module (all datasets)
184
ui_page_missing_data <- function(id, pre_output = NULL, post_output = NULL) {
185!
  ns <- NS(id)
186!
  tagList(
187!
    teal.widgets::standard_layout(
188!
      output = teal.widgets::white_small_well(
189!
        uiOutput(ns("dataset_tabs"))
190
      ),
191!
      encoding = tags$div(
192!
        uiOutput(ns("dataset_encodings"))
193
      ),
194!
      pre_output = pre_output,
195!
      post_output = post_output
196
    )
197
  )
198
}
199
200
# Server function for the missing data module (all datasets)
201
srv_page_missing_data <- function(id, data, datanames, parent_dataname,
202
                                  plot_height, plot_width, ggplot2_args, ggtheme, decorators) {
203!
  moduleServer(id, function(input, output, session) {
204!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
205
206!
    datanames <- Filter(function(name) {
207!
      is.data.frame(isolate(data())[[name]])
208!
    }, if (identical(datanames, "all")) names(isolate(data())) else datanames)
209
210!
    if_subject_plot <- length(parent_dataname) > 0 && parent_dataname %in% datanames
211
212!
    ns <- session$ns
213
214!
    output$dataset_tabs <- renderUI({
215!
      do.call(
216!
        tabsetPanel,
217!
        c(
218!
          id = ns("dataname_tab"),
219!
          lapply(
220!
            datanames,
221!
            function(x) {
222!
              tabPanel(
223!
                title = x,
224!
                ui_missing_data(id = ns(x), by_subject_plot = if_subject_plot)
225
              )
226
            }
227
          )
228
        )
229
      )
230
    })
231
232!
    output$dataset_encodings <- renderUI({
233!
      req(ggtheme, datanames, is.logical(if_subject_plot))
234!
      tagList(
235!
        lapply(
236!
          datanames,
237!
          function(x) {
238!
            conditionalPanel(
239!
              is_tab_active_js(ns("dataname_tab"), x),
240!
              encoding_missing_data(
241!
                id = ns(x),
242!
                summary_per_patient = if_subject_plot,
243!
                ggtheme = ggtheme,
244!
                datanames = datanames,
245!
                decorators = decorators
246
              )
247
            )
248
          }
249
        )
250
      )
251
    })
252
253!
    result <- sapply(
254!
      datanames,
255!
      function(x) {
256!
        srv_missing_data(
257!
          id = x,
258!
          data = data,
259!
          dataname = x,
260!
          parent_dataname = parent_dataname,
261!
          plot_height = plot_height,
262!
          plot_width = plot_width,
263!
          ggplot2_args = ggplot2_args,
264!
          decorators = decorators
265
        )
266
      },
267!
      USE.NAMES = TRUE,
268!
      simplify = FALSE
269
    )
270
271!
    reactive({
272!
      if (is.null(input$dataname_tab)) {
273!
        teal.data::teal_data()
274
      } else {
275!
        result[[input$dataname_tab]]()
276
      }
277
    })
278
  })
279
}
280
281
# UI function for the missing data module (single dataset)
282
ui_missing_data <- function(id, by_subject_plot = FALSE) {
283!
  ns <- NS(id)
284
285!
  tab_list <- list(
286!
    tabPanel(
287!
      "Summary",
288!
      teal.widgets::plot_with_settings_ui(id = ns("summary_plot")),
289!
      helpText(
290!
        tags$p(paste(
291!
          'The "Summary" graph shows the number of missing values per variable (both absolute and percentage),',
292!
          "sorted by magnitude."
293
        )),
294!
        tags$p(
295!
          'The "summary per patients" graph is showing how many subjects have at least one missing observation',
296!
          "for each variable. It will be most useful for panel datasets."
297
        )
298
      )
299
    ),
300!
    tabPanel(
301!
      "Combinations",
302!
      teal.widgets::plot_with_settings_ui(id = ns("combination_plot")),
303!
      helpText(
304!
        tags$p(paste(
305!
          'The "Combinations" graph is used to explore the relationship between the missing data within',
306!
          "different columns of the dataset.",
307!
          "It shows the different patterns of missingness in the rows of the data.",
308!
          'For example, suppose that 70 rows of the data have exactly columns "A" and "B" missing.',
309!
          "In this case there would be a bar of height 70 in the top graph and",
310!
          'the column below this in the second graph would have rows "A" and "B" cells shaded red.'
311
        )),
312!
        tags$p(paste(
313!
          "Due to the large number of missing data patterns possible, only those with a large set of observations",
314!
          'are shown in the graph and the "Combination cut-off" slider can be used to adjust the number shown.'
315
        ))
316
      )
317
    ),
318!
    tabPanel(
319!
      "By Variable Levels",
320!
      teal.widgets::plot_with_settings_ui(id = ns("by_variable_plot"))
321
    )
322
  )
323!
  if (isTRUE(by_subject_plot)) {
324!
    tab_list <- append(
325!
      tab_list,
326!
      list(tabPanel(
327!
        "Grouped by Subject",
328!
        teal.widgets::plot_with_settings_ui(id = ns("by_subject_plot")),
329!
        helpText(
330!
          tags$p(paste(
331!
            "This graph shows the missingness with respect to subjects rather than individual rows of the",
332!
            "dataset. Each row represents one dataset variable and each column a single subject. Only subjects",
333!
            "with at least one record in this dataset are shown. For a given subject, if they have any missing",
334!
            "values of a specific variable then the appropriate cell in the graph is marked as missing."
335
          ))
336
        )
337
      ))
338
    )
339
  }
340
341!
  do.call(
342!
    tabsetPanel,
343!
    c(
344!
      id = ns("summary_type"),
345!
      tab_list
346
    )
347
  )
348
}
349
350
# UI encoding for the missing data module (all datasets)
351
encoding_missing_data <- function(id, summary_per_patient = FALSE, ggtheme, datanames, decorators) {
352!
  ns <- NS(id)
353
354!
  tagList(
355!
    tags$label("Encodings", class = "text-primary"),
356!
    helpText(
357!
      paste0("Dataset", `if`(length(datanames) > 1, "s", ""), ":"),
358!
      tags$code(paste(datanames, collapse = ", "))
359
    ),
360!
    uiOutput(ns("variables")),
361!
    actionButton(
362!
      ns("filter_na"),
363!
      tags$span("Select only vars with missings", style = "white-space: normal;"),
364!
      width = "100%",
365!
      style = "margin-bottom: 1rem;"
366
    ),
367!
    conditionalPanel(
368!
      is_tab_active_js(ns("summary_type"), "Summary"),
369!
      bslib::input_switch(
370!
        id = ns("any_na"),
371!
        label = tags$div(
372!
          HTML("Add <b>anyna</b> variable"),
373!
          bslib::tooltip(
374!
            icon("circle-info"),
375!
            tags$span(
376!
              "Describes the number of observations with at least one missing value in any variable."
377
            )
378
          )
379
        ),
380!
        value = FALSE
381
      ),
382!
      if (summary_per_patient) {
383!
        bslib::input_switch(
384!
          id = ns("if_patients_plot"),
385!
          label = tags$div(
386!
            "Add summary per patients",
387!
            bslib::tooltip(
388!
              icon("circle-info"),
389!
              tags$span(
390!
                paste(
391!
                  "Displays the number of missing values per observation,",
392!
                  "where the x-axis is sorted by observation appearance in the table."
393
                )
394
              )
395
            )
396
          ),
397!
          value = FALSE
398
        )
399
      },
400!
      ui_decorate_teal_data(ns("dec_summary_plot"), decorators = select_decorators(decorators, "summary_plot"))
401
    ),
402!
    conditionalPanel(
403!
      is_tab_active_js(ns("summary_type"), "Combinations"),
404!
      uiOutput(ns("cutoff")),
405!
      ui_decorate_teal_data(ns("dec_combination_plot"), decorators = select_decorators(decorators, "combination_plot"))
406
    ),
407!
    conditionalPanel(
408!
      is_tab_active_js(ns("summary_type"), "Grouped by Subject"),
409!
      ui_decorate_teal_data(ns("dec_by_subject_plot"), decorators = select_decorators(decorators, "by_subject_plot"))
410
    ),
411!
    conditionalPanel(
412!
      is_tab_active_js(ns("summary_type"), "By Variable Levels"),
413!
      uiOutput(ns("group_by_var_ui")),
414!
      uiOutput(ns("group_by_vals_ui")),
415!
      radioButtons(
416!
        ns("count_type"),
417!
        label = "Display missing as",
418!
        choices = c("counts", "proportions"),
419!
        selected = "counts",
420!
        inline = TRUE
421
      )
422
    ),
423!
    bslib::accordion(
424!
      bslib::accordion_panel(
425!
        title = "Plot settings",
426!
        selectInput(
427!
          inputId = ns("ggtheme"),
428!
          label = "Theme (by ggplot):",
429!
          choices = ggplot_themes,
430!
          selected = ggtheme,
431!
          multiple = FALSE
432
        )
433
      )
434
    )
435
  )
436
}
437
438
# Server function for the missing data (single dataset)
439
srv_missing_data <- function(id,
440
                             data,
441
                             dataname,
442
                             parent_dataname,
443
                             plot_height,
444
                             plot_width,
445
                             ggplot2_args,
446
                             decorators) {
447!
  checkmate::assert_class(data, "reactive")
448!
  checkmate::assert_class(isolate(data()), "teal_data")
449!
  moduleServer(id, function(input, output, session) {
450!
    ns <- session$ns
451
452!
    prev_group_by_var <- reactiveVal("")
453!
    data_r <- reactive(data()[[dataname]])
454!
    data_keys <- reactive(unlist(teal.data::join_keys(data())[[dataname]]))
455
456!
    iv_r <- reactive({
457!
      iv <- shinyvalidate::InputValidator$new()
458!
      iv$add_rule(
459!
        "variables_select",
460!
        shinyvalidate::sv_required("At least one reference variable needs to be selected.")
461
      )
462!
      iv$add_rule(
463!
        "variables_select",
464!
        ~ if (length(setdiff((.), data_keys())) < 1) "Please also select non-key columns."
465
      )
466!
      iv_summary_table <- shinyvalidate::InputValidator$new()
467!
      iv_summary_table$condition(~ isTRUE(input$summary_type == "By Variable Levels"))
468!
      iv_summary_table$add_rule("count_type", shinyvalidate::sv_required("Please select type of counts"))
469!
      iv_summary_table$add_rule(
470!
        "group_by_vals",
471!
        ~ if (length(input$group_by_var) >= 1L && length(.) == 0L) "Please select filter values"
472
      )
473!
      iv_summary_table$add_rule(
474!
        "group_by_var",
475!
        ~ if (length(.) == 0L) "Please select group-by variable"
476
      )
477!
      iv_summary_table$add_rule(
478!
        "group_by_var",
479!
        ~ if (length(.) > 0 && length(input$variables_select) == 1 && (.) == input$variables_select) {
480!
          "If only one reference variable is selected it must not be the grouping variable."
481
        }
482
      )
483!
      iv_summary_table$add_rule(
484!
        "variables_select",
485!
        ~ if (length(input$group_by_var) > 0 && length(.) == 1 && (.) == input$group_by_var) {
486!
          "If only one reference variable is selected it must not be the grouping variable."
487
        }
488
      )
489!
      iv$add_validator(iv_summary_table)
490!
      iv$enable()
491!
      iv
492
    })
493
494!
    data_parent_keys <- reactive({
495!
      req(data(), parent_dataname)
496!
      if (length(parent_dataname) > 0 && parent_dataname %in% names(data())) {
497!
        keys <- teal.data::join_keys(data())[[dataname]]
498!
        if (parent_dataname %in% names(keys)) {
499!
          keys[[parent_dataname]]
500
        } else {
501!
          keys[[dataname]]
502
        }
503
      } else {
504!
        NULL
505
      }
506
    })
507
508!
    common_code_q <- reactive({
509!
      teal::validate_inputs(iv_r())
510!
      req(data(), data_r(), input$summary_type)
511!
      group_var <- input$group_by_var
512!
      anl <- data_r()
513!
      obj <- data()
514!
      teal.reporter::teal_card(obj) <- c(
515!
        teal.reporter::teal_card(obj),
516!
        teal.reporter::teal_card("## Module's output(s)")
517
      )
518
519!
      qenv <- teal.code::eval_code(obj, {
520!
        "library(dplyr);library(ggplot2);library(tidyr);library(gridExtra)"
521
      })
522
523!
      qenv <- if (!is.null(selected_vars()) && length(selected_vars()) != ncol(anl)) {
524!
        teal.code::eval_code(
525!
          qenv,
526!
          substitute(
527!
            expr = ANL <- anl_name[, selected_vars, drop = FALSE],
528!
            env = list(anl_name = as.name(dataname), selected_vars = selected_vars())
529
          )
530
        )
531
      } else {
532!
        teal.code::eval_code(
533!
          qenv,
534!
          substitute(expr = ANL <- anl_name, env = list(anl_name = as.name(dataname)))
535
        )
536
      }
537
538!
      if (input$summary_type == "By Variable Levels" && !is.null(group_var) && !(group_var %in% selected_vars())) {
539!
        qenv <- teal.code::eval_code(
540!
          qenv,
541!
          substitute(
542!
            expr = ANL[[group_var]] <- anl_name[[group_var]],
543!
            env = list(group_var = group_var, anl_name = as.name(dataname))
544
          )
545
        )
546
      }
547
548!
      new_col_name <- "**anyna**"
549
550!
      qenv <- teal.code::eval_code(
551!
        qenv,
552!
        substitute(
553!
          expr =
554!
            create_cols_labels <- function(cols, just_label = FALSE) {
555!
              column_labels <- column_labels_value
556!
              column_labels[is.na(column_labels) | length(column_labels) == 0] <- ""
557!
              if (just_label) {
558!
                labels <- column_labels[cols]
559
              } else {
560!
                labels <- ifelse(cols == new_col_name | cols == "", cols, paste0(column_labels[cols], " [", cols, "]"))
561
              }
562!
              labels
563
            },
564!
          env = list(
565!
            new_col_name = new_col_name,
566!
            column_labels_value = c(teal.data::col_labels(data_r())[selected_vars()],
567!
              new_col_name = new_col_name
568
            )
569
          )
570
        )
571
      )
572!
      qenv
573
    })
574
575!
    selected_vars <- reactive({
576!
      req(input$variables_select)
577!
      keys <- data_keys()
578!
      vars <- unique(c(keys, input$variables_select))
579!
      vars
580
    })
581
582!
    vars_summary <- reactive({
583!
      req(data_r())
584!
      na_count <- data_r() %>%
585!
        sapply(function(x) mean(is.na(x)), USE.NAMES = TRUE) %>%
586!
        sort(decreasing = TRUE)
587
588!
      tibble::tibble(
589!
        key = names(na_count),
590!
        value = unname(na_count),
591!
        label = cut(na_count, breaks = seq(from = 0, to = 1, by = 0.1), include.lowest = TRUE)
592
      )
593
    })
594
595
    # Keep encoding panel up-to-date
596!
    output$variables <- renderUI({
597!
      req(vars_summary(), data_r())
598!
      choices <- split(x = vars_summary()$key, f = vars_summary()$label, drop = TRUE) %>% rev()
599!
      selected <- choices <- unname(unlist(choices))
600
601!
      teal.widgets::optionalSelectInput(
602!
        ns("variables_select"),
603!
        label = "Select variables",
604!
        label_help = HTML(paste0("Dataset: ", tags$code(dataname))),
605!
        choices = teal.transform::variable_choices(data_r(), choices),
606!
        selected = selected,
607!
        multiple = TRUE
608
      )
609
    })
610
611!
    observeEvent(input$filter_na, {
612!
      req(vars_summary(), data_r())
613!
      choices <- vars_summary() %>%
614!
        dplyr::select(!!as.name("key")) %>%
615!
        getElement(name = 1)
616
617!
      selected <- vars_summary() %>%
618!
        dplyr::filter(!!as.name("value") > 0) %>%
619!
        dplyr::select(!!as.name("key")) %>%
620!
        getElement(name = 1)
621
622!
      teal.widgets::updateOptionalSelectInput(
623!
        session = session,
624!
        inputId = "variables_select",
625!
        choices = teal.transform::variable_choices(data_r()),
626!
        selected = restoreInput(ns("variables_select"), selected)
627
      )
628
    })
629
630!
    output$group_by_var_ui <- renderUI({
631!
      req(data_r())
632!
      all_choices <- teal.transform::variable_choices(data_r())
633!
      cat_choices <- all_choices[!sapply(data_r(), function(x) is.numeric(x) || inherits(x, "POSIXct"))]
634!
      validate(
635!
        need(cat_choices, "Dataset does not have any non-numeric or non-datetime variables to use to group data with")
636
      )
637!
      teal.widgets::optionalSelectInput(
638!
        ns("group_by_var"),
639!
        label = "Group by variable",
640!
        choices = cat_choices,
641!
        selected = `if`(
642!
          is.null(isolate(input$group_by_var)),
643!
          cat_choices[1],
644!
          isolate(input$group_by_var)
645
        ),
646!
        multiple = FALSE,
647!
        label_help = paste0("Dataset: ", dataname)
648
      )
649
    })
650
651!
    output$group_by_vals_ui <- renderUI({
652!
      req(input$group_by_var, data_r())
653
654!
      choices <- teal.transform::value_choices(data_r(), input$group_by_var, input$group_by_var)
655!
      prev_choices <- isolate(input$group_by_vals)
656
657
      # determine selected value based on filtered data
658
      # display those previously selected values that are still available
659!
      selected <- if (!is.null(prev_choices) && any(prev_choices %in% choices)) {
660!
        prev_choices[match(choices[choices %in% prev_choices], prev_choices)]
661!
      } else if (
662!
        !is.null(prev_choices) &&
663!
          !any(prev_choices %in% choices) &&
664!
          isolate(prev_group_by_var()) == input$group_by_var
665
      ) {
666
        # if not any previously selected value is available and the grouping variable is the same,
667
        # then display NULL
668!
        NULL
669
      } else {
670
        # if new grouping variable (i.e. not any previously selected value is available),
671
        # then display all choices
672!
        choices
673
      }
674
675!
      prev_group_by_var(input$group_by_var) # set current group_by_var
676!
      validate(need(length(choices) < 100, "Please select group-by variable with fewer than 100 unique values"))
677!
      teal.widgets::optionalSelectInput(
678!
        ns("group_by_vals"),
679!
        label = "Filter levels",
680!
        choices = choices,
681!
        selected = selected,
682!
        multiple = TRUE,
683!
        label_help = paste0("Dataset: ", dataname)
684
      )
685
    })
686
687!
    combination_cutoff_q <- reactive({
688!
      qenv <- req(common_code_q())
689!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Combination Plot")
690!
      teal.code::eval_code(
691!
        qenv,
692!
        quote(
693!
          combination_cutoff <- ANL %>%
694!
            dplyr::mutate_all(is.na) %>%
695!
            dplyr::group_by_all() %>%
696!
            dplyr::tally() %>%
697!
            dplyr::ungroup()
698
        )
699
      )
700
    })
701
702!
    output$cutoff <- renderUI({
703!
      req(combination_cutoff_q())
704!
      x <- combination_cutoff_q()[["combination_cutoff"]]$n
705
706
      # select 10-th from the top
707!
      n <- length(x)
708!
      idx <- max(1, n - 10)
709!
      prev_value <- isolate(input$combination_cutoff)
710!
      value <- if (is.null(prev_value) || prev_value > max(x) || prev_value < min(x)) {
711!
        sort(x, partial = idx)[idx]
712
      } else {
713!
        prev_value
714
      }
715
716!
      teal.widgets::optionalSliderInputValMinMax(
717!
        ns("combination_cutoff"),
718!
        "Combination cut-off",
719!
        c(value, range(x))
720
      )
721
    })
722
723
    # Prepare qenvs for output objects
724
725!
    summary_plot_q <- reactive({
726!
      req(input$summary_type == "Summary") # needed to trigger update on tab change
727!
      teal::validate_has_data(req(data_r()), 1)
728!
      req(input$ggtheme)
729!
      qenv <- req(common_code_q())
730!
      if (input$any_na) {
731!
        new_col_name <- "**anyna**"
732!
        qenv <- teal.code::eval_code(
733!
          qenv,
734!
          substitute(
735!
            expr = ANL[[new_col_name]] <- ifelse(rowSums(is.na(ANL)) > 0, NA, FALSE),
736!
            env = list(new_col_name = new_col_name)
737
          )
738
        )
739
      }
740
741!
      qenv <- teal.code::eval_code(
742!
        qenv,
743!
        substitute(
744!
          expr = analysis_vars <- setdiff(colnames(ANL), data_keys),
745!
          env = list(data_keys = data_keys())
746
        )
747
      )
748
749!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Summary Plot")
750
751!
      qenv <- teal.code::eval_code(
752!
        qenv,
753!
        substitute(
754!
          expr = summary_plot_obs <- data_frame_call[, analysis_vars] %>%
755!
            dplyr::summarise_all(list(function(x) sum(is.na(x)))) %>%
756!
            tidyr::pivot_longer(dplyr::everything(), names_to = "col", values_to = "n_na") %>%
757!
            dplyr::mutate(n_not_na = nrow(ANL) - n_na) %>%
758!
            tidyr::pivot_longer(-col, names_to = "isna", values_to = "n") %>%
759!
            dplyr::mutate(isna = isna == "n_na", n_pct = n / nrow(ANL) * 100),
760!
          env = list(data_frame_call = if (!inherits(data_r(), "tbl_df")) {
761!
            quote(tibble::as_tibble(ANL))
762
          } else {
763!
            quote(ANL)
764
          })
765
        )
766
      ) %>%
767
        # x axis ordering according to number of missing values and alphabet
768!
        teal.code::eval_code(
769!
          quote(
770!
            expr = x_levels <- dplyr::filter(summary_plot_obs, isna) %>%
771!
              dplyr::arrange(n_pct, dplyr::desc(col)) %>%
772!
              dplyr::pull(col) %>%
773!
              create_cols_labels()
774
          )
775
        )
776
777
      # always set "**anyna**" level as the last one
778!
      if (input$any_na) {
779!
        qenv <- teal.code::eval_code(
780!
          qenv,
781!
          quote(x_levels <- c(setdiff(x_levels, "**anyna**"), "**anyna**"))
782
        )
783
      }
784
785!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
786!
        labs = list(x = "Variable", y = "Missing observations"),
787!
        theme = list(legend.position = "bottom", axis.text.x = quote(ggplot2::element_text(angle = 45, hjust = 1)))
788
      )
789
790!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
791!
        user_plot = ggplot2_args[["Summary Obs"]],
792!
        user_default = ggplot2_args$default,
793!
        module_plot = dev_ggplot2_args
794
      )
795
796!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
797!
        all_ggplot2_args,
798!
        ggtheme = input$ggtheme
799
      )
800
801!
      qenv <- teal.code::eval_code(
802!
        qenv,
803!
        substitute(
804!
          summary_plot_top <- summary_plot_obs %>%
805!
            ggplot2::ggplot() +
806!
            ggplot2::aes(
807!
              x = factor(create_cols_labels(col), levels = x_levels),
808!
              y = n_pct,
809!
              fill = isna
810
            ) +
811!
            ggplot2::geom_bar(position = "fill", stat = "identity") +
812!
            ggplot2::scale_fill_manual(
813!
              name = "",
814!
              values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]),
815!
              labels = c("Present", "Missing")
816
            ) +
817!
            ggplot2::scale_y_continuous(
818!
              labels = scales::percent_format(),
819!
              breaks = seq(0, 1, by = 0.1),
820!
              expand = c(0, 0)
821
            ) +
822!
            ggplot2::geom_text(
823!
              ggplot2::aes(label = ifelse(isna == TRUE, sprintf("%d [%.02f%%]", n, n_pct), ""), y = 1),
824!
              hjust = 1,
825!
              color = "black"
826
            ) +
827!
            labs +
828!
            ggthemes +
829!
            themes +
830!
            ggplot2::coord_flip(),
831!
          env = list(
832!
            labs = parsed_ggplot2_args$labs,
833!
            themes = parsed_ggplot2_args$theme,
834!
            ggthemes = parsed_ggplot2_args$ggtheme
835
          )
836
        )
837
      )
838
839!
      if (isTRUE(input$if_patients_plot)) {
840!
        qenv <- teal.code::eval_code(
841!
          qenv,
842!
          substitute(
843!
            expr = parent_keys <- keys,
844!
            env = list(keys = data_parent_keys())
845
          )
846
        ) %>%
847!
          teal.code::eval_code(quote(ndistinct_subjects <- dplyr::n_distinct(ANL[, parent_keys]))) %>%
848!
          teal.code::eval_code(
849!
            quote(
850!
              summary_plot_patients <- ANL[, c(parent_keys, analysis_vars)] %>%
851!
                dplyr::group_by_at(parent_keys) %>%
852!
                dplyr::summarise_all(anyNA) %>%
853!
                tidyr::pivot_longer(cols = !dplyr::all_of(parent_keys), names_to = "col", values_to = "anyna") %>%
854!
                dplyr::group_by_at(c("col")) %>%
855!
                dplyr::summarise(count_na = sum(anyna)) %>%
856!
                dplyr::mutate(count_not_na = ndistinct_subjects - count_na) %>%
857!
                tidyr::pivot_longer(-c(col), names_to = "isna", values_to = "n") %>%
858!
                dplyr::mutate(isna = isna == "count_na", n_pct = n / ndistinct_subjects * 100) %>%
859!
                dplyr::arrange_at(c("isna", "n"), .funs = dplyr::desc)
860
            )
861
          )
862
863!
        dev_ggplot2_args <- teal.widgets::ggplot2_args(
864!
          labs = list(x = "", y = "Missing patients"),
865!
          theme = list(
866!
            legend.position = "bottom",
867!
            axis.text.x = quote(ggplot2::element_text(angle = 45, hjust = 1)),
868!
            axis.text.y = quote(ggplot2::element_blank())
869
          )
870
        )
871
872!
        all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
873!
          user_plot = ggplot2_args[["Summary Patients"]],
874!
          user_default = ggplot2_args$default,
875!
          module_plot = dev_ggplot2_args
876
        )
877
878!
        parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
879!
          all_ggplot2_args,
880!
          ggtheme = input$ggtheme
881
        )
882
883!
        qenv <- teal.code::eval_code(
884!
          qenv,
885!
          substitute(
886!
            summary_plot_bottom <- summary_plot_patients %>%
887!
              ggplot2::ggplot() +
888!
              ggplot2::aes_(
889!
                x = ~ factor(create_cols_labels(col), levels = x_levels),
890!
                y = ~n_pct,
891!
                fill = ~isna
892
              ) +
893!
              ggplot2::geom_bar(alpha = 1, stat = "identity", position = "fill") +
894!
              ggplot2::scale_y_continuous(
895!
                labels = scales::percent_format(),
896!
                breaks = seq(0, 1, by = 0.1),
897!
                expand = c(0, 0)
898
              ) +
899!
              ggplot2::scale_fill_manual(
900!
                name = "",
901!
                values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]),
902!
                labels = c("Present", "Missing")
903
              ) +
904!
              ggplot2::geom_text(
905!
                ggplot2::aes(label = ifelse(isna == TRUE, sprintf("%d [%.02f%%]", n, n_pct), ""), y = 1),
906!
                hjust = 1,
907!
                color = "black"
908
              ) +
909!
              labs +
910!
              ggthemes +
911!
              themes +
912!
              ggplot2::coord_flip(),
913!
            env = list(
914!
              labs = parsed_ggplot2_args$labs,
915!
              themes = parsed_ggplot2_args$theme,
916!
              ggthemes = parsed_ggplot2_args$ggtheme
917
            )
918
          )
919
        )
920
      }
921
922!
      qenv <- if (isTRUE(input$if_patients_plot)) {
923!
        within(qenv, {
924!
          summary_plot <- gridExtra::grid.arrange(summary_plot_top, summary_plot_bottom, ncol = 2)
925
        })
926
      } else {
927!
        within(qenv, {
928!
          summary_plot <- summary_plot_top
929
        })
930
      }
931!
      qenv
932
    })
933
934!
    combination_plot_q <- reactive({
935!
      req(
936!
        input$summary_type == "Combinations", input$combination_cutoff,
937!
        combination_cutoff_q(), input$ggtheme
938
      )
939!
      teal::validate_has_data(req(data_r()), 1)
940
941!
      qenv <- teal.code::eval_code(
942!
        combination_cutoff_q(),
943!
        substitute(
944!
          expr = data_combination_plot_cutoff <- combination_cutoff %>%
945!
            dplyr::filter(n >= combination_cutoff_value) %>%
946!
            dplyr::mutate(id = rank(-n, ties.method = "first")) %>%
947!
            tidyr::pivot_longer(-c(n, id), names_to = "key", values_to = "value") %>%
948!
            dplyr::arrange(n),
949!
          env = list(combination_cutoff_value = input$combination_cutoff)
950
        )
951
      )
952
953
      # find keys in dataset not selected in the UI and remove them from dataset
954!
      keys_not_selected <- setdiff(data_keys(), input$variables_select)
955!
      if (length(keys_not_selected) > 0) {
956!
        qenv <- teal.code::eval_code(
957!
          qenv,
958!
          substitute(
959!
            expr = data_combination_plot_cutoff <- data_combination_plot_cutoff %>%
960!
              dplyr::filter(!key %in% keys_not_selected),
961!
            env = list(keys_not_selected = keys_not_selected)
962
          )
963
        )
964
      }
965
966!
      qenv <- teal.code::eval_code(
967!
        qenv,
968!
        quote(
969!
          labels <- data_combination_plot_cutoff %>%
970!
            dplyr::filter(key == key[[1]]) %>%
971!
            getElement(name = 1)
972
        )
973
      )
974
975!
      dev_ggplot2_args1 <- teal.widgets::ggplot2_args(
976!
        labs = list(x = "", y = ""),
977!
        theme = list(
978!
          legend.position = "bottom",
979!
          axis.text.x = quote(ggplot2::element_blank())
980
        )
981
      )
982
983!
      all_ggplot2_args1 <- teal.widgets::resolve_ggplot2_args(
984!
        user_plot = ggplot2_args[["Combinations Hist"]],
985!
        user_default = ggplot2_args$default,
986!
        module_plot = dev_ggplot2_args1
987
      )
988
989!
      parsed_ggplot2_args1 <- teal.widgets::parse_ggplot2_args(
990!
        all_ggplot2_args1,
991!
        ggtheme = "void"
992
      )
993
994!
      dev_ggplot2_args2 <- teal.widgets::ggplot2_args(
995!
        labs = list(x = "", y = ""),
996!
        theme = list(
997!
          legend.position = "bottom",
998!
          axis.text.x = quote(ggplot2::element_blank()),
999!
          axis.ticks = quote(ggplot2::element_blank()),
1000!
          panel.grid.major = quote(ggplot2::element_blank())
1001
        )
1002
      )
1003
1004!
      all_ggplot2_args2 <- teal.widgets::resolve_ggplot2_args(
1005!
        user_plot = ggplot2_args[["Combinations Main"]],
1006!
        user_default = ggplot2_args$default,
1007!
        module_plot = dev_ggplot2_args2
1008
      )
1009
1010!
      parsed_ggplot2_args2 <- teal.widgets::parse_ggplot2_args(
1011!
        all_ggplot2_args2,
1012!
        ggtheme = input$ggtheme
1013
      )
1014
1015!
      qenv <- teal.code::eval_code(
1016!
        qenv,
1017!
        substitute(
1018!
          expr = {
1019!
            combination_plot_top <- data_combination_plot_cutoff %>%
1020!
              dplyr::select(id, n) %>%
1021!
              dplyr::distinct() %>%
1022!
              ggplot2::ggplot(ggplot2::aes(x = id, y = n)) +
1023!
              ggplot2::geom_bar(stat = "identity", fill = c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]) +
1024!
              ggplot2::geom_text(
1025!
                ggplot2::aes(label = n),
1026!
                position = ggplot2::position_dodge(width = 0.9),
1027!
                vjust = -0.25
1028
              ) +
1029!
              ggplot2::ylim(c(0, max(data_combination_plot_cutoff$n) * 1.5)) +
1030!
              labs1 +
1031!
              ggthemes1 +
1032!
              themes1
1033
1034!
            graph_number_rows <- length(unique(data_combination_plot_cutoff$id))
1035!
            graph_number_cols <- nrow(data_combination_plot_cutoff) / graph_number_rows
1036
1037!
            combination_plot_bottom <- data_combination_plot_cutoff %>% ggplot2::ggplot() +
1038!
              ggplot2::aes(x = create_cols_labels(key), y = id - 0.5, fill = value) +
1039!
              ggplot2::geom_tile(alpha = 0.85, height = 0.95) +
1040!
              ggplot2::scale_fill_manual(
1041!
                name = "",
1042!
                values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]),
1043!
                labels = c("Present", "Missing")
1044
              ) +
1045!
              ggplot2::geom_hline(yintercept = seq_len(1 + graph_number_rows) - 1) +
1046!
              ggplot2::geom_vline(xintercept = seq_len(1 + graph_number_cols) - 0.5, linetype = "dotted") +
1047!
              ggplot2::coord_flip() +
1048!
              labs2 +
1049!
              ggthemes2 +
1050!
              themes2
1051
          },
1052!
          env = list(
1053!
            labs1 = parsed_ggplot2_args1$labs,
1054!
            themes1 = parsed_ggplot2_args1$theme,
1055!
            ggthemes1 = parsed_ggplot2_args1$ggtheme,
1056!
            labs2 = parsed_ggplot2_args2$labs,
1057!
            themes2 = parsed_ggplot2_args2$theme,
1058!
            ggthemes2 = parsed_ggplot2_args2$ggtheme
1059
          )
1060
        )
1061
      )
1062
1063!
      within(qenv, {
1064!
        g1 <- ggplot2::ggplotGrob(combination_plot_top)
1065!
        g2 <- ggplot2::ggplotGrob(combination_plot_bottom)
1066
1067!
        combination_plot <- gridExtra::gtable_rbind(g1, g2, size = "last")
1068!
        combination_plot$heights[7] <- grid::unit(0.2, "null") # rescale to get the bar chart smaller
1069
      })
1070
    })
1071
1072!
    by_variable_plot_q <- reactive({
1073!
      req(
1074!
        input$summary_type == "By Variable Levels", # needed to trigger update on tab change
1075!
        common_code_q()
1076
      )
1077!
      teal::validate_has_data(req(data_r()), 1)
1078
1079
      # extract the ANL dataset for use in further validation
1080!
      anl <- common_code_q()[["ANL"]]
1081
1082!
      req(input$group_by_var)
1083!
      group_var <- input$group_by_var
1084!
      validate(
1085!
        need(
1086!
          length(unique(anl[[group_var]])) < 100,
1087!
          "Please select group-by variable with fewer than 100 unique values"
1088
        )
1089
      )
1090!
      group_vals <- input$group_by_vals
1091!
      variables_select <- input$variables_select
1092!
      vars <- unique(variables_select, group_var)
1093!
      count_type <- input$count_type
1094
1095!
      if (!is.null(selected_vars()) && length(selected_vars()) != ncol(anl)) {
1096!
        variables <- selected_vars()
1097
      } else {
1098!
        variables <- colnames(anl)
1099
      }
1100
1101!
      summ_fn <- if (input$count_type == "counts") {
1102!
        function(x) sum(is.na(x))
1103
      } else {
1104!
        function(x) round(sum(is.na(x)) / length(x), 4)
1105
      }
1106
1107!
      qenv <- req(common_code_q())
1108!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Summary Table")
1109
1110
1111!
      common_code_libraries_q <- teal.code::eval_code(
1112!
        qenv,
1113!
        "library(forcats);library(glue)"
1114
      )
1115!
      qenv <- teal.code::eval_code(
1116!
        common_code_libraries_q,
1117!
        substitute(
1118!
          expr = {
1119!
            summary_data <- ANL %>%
1120!
              dplyr::mutate(group_var_name := forcats::fct_na_value_to_level(as.factor(group_var_name), "NA")) %>%
1121!
              dplyr::group_by_at(group_var) %>%
1122!
              dplyr::filter(group_var_name %in% group_vals)
1123
1124!
            count_data <- dplyr::summarise(summary_data, n = dplyr::n())
1125
1126!
            summary_data <- dplyr::summarise_all(summary_data, summ_fn) %>%
1127!
              dplyr::mutate(group_var_name := paste0(group_var, ":", group_var_name, "(N=", count_data$n, ")")) %>%
1128!
              tidyr::pivot_longer(!dplyr::all_of(group_var), names_to = "Variable", values_to = "out") %>%
1129!
              tidyr::pivot_wider(names_from = group_var, values_from = "out") %>%
1130!
              dplyr::mutate(`Variable label` = create_cols_labels(Variable, just_label = TRUE), .after = Variable)
1131
          },
1132!
          env = list(
1133!
            group_var = group_var, group_var_name = as.name(group_var), group_vals = group_vals, summ_fn = summ_fn
1134
          )
1135
        )
1136
      )
1137
1138!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
1139!
        labs = list(
1140!
          fill = if (input$count_type == "counts") "Missing counts" else "Missing percentage",
1141!
          y = NULL
1142
        )
1143
      )
1144
1145!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
1146!
        user_plot = ggplot2_args[["By Variable Levels"]],
1147!
        user_default = ggplot2_args$default,
1148!
        module_plot = dev_ggplot2_args
1149
      )
1150
1151!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
1152!
        all_ggplot2_args,
1153!
        ggtheme = input$ggtheme
1154
      )
1155
1156
      # convert to ggplot
1157!
      labels <- lapply(qenv$ANL, attr, which = "label")
1158!
      if (!any(lengths(labels))) {
1159!
        ANL_q <- within(qenv, # nolint object_name_linter
1160
          {
1161!
            keep_columns <- intersect(c(keys, group_var), colnames(ANL))
1162!
            ANL <- ANL %>%
1163!
              filter(group_var_name %in% group_vals) %>%
1164!
              pivot_longer(-keep_columns, values_transform = is.na) %>%
1165!
              summarise(
1166!
                .by = c(group_var_name, name),
1167!
                value = sum(value), perc = value / n()
1168
              )
1169
          },
1170!
          keys = join_keys(qenv) |> unlist() |> unique(),
1171!
          group_var_name = as.name(group_var),
1172!
          group_var = group_var,
1173!
          group_vals = req(group_vals)
1174
        )
1175!
        tile <- within(ANL_q,
1176
          {
1177!
            by_variable_plot <- ggplot2::ggplot(ANL, ggplot2::aes(group_var_name, name)) +
1178!
              ggplot2::geom_tile(ggplot2::aes(fill = column), color = "gray90") +
1179!
              ggplot2::geom_text(ggplot2::aes(label = text_label),
1180!
                data = ~ dplyr::filter(.x, perc > 0),
1181
              ) +
1182!
              ggplot2::scale_x_discrete(expand = ggplot2::expansion()) +
1183!
              ggplot2::scale_fill_gradient(high = "#ff2951ff", low = "grey90", labels = labels) +
1184!
              labs +
1185!
              ggthemes
1186
          },
1187!
          group_var_name = as.name(group_var),
1188!
          column = if (input$count_type == "counts") {
1189!
            as.name("value")
1190
          } else {
1191!
            as.name("perc")
1192
          },
1193!
          text_label = if (input$count_type == "counts") as.name("value") else quote(scales::percent(perc)),
1194!
          labs = parsed_ggplot2_args$labs,
1195!
          labels = if (input$count_type == "counts") quote(ggplot2::waiver()) else quote(scales::label_percent()),
1196!
          ggthemes = parsed_ggplot2_args$ggtheme
1197
        )
1198
      } else {
1199!
        ANL_q <- within(qenv, # nolint object_name_linter
1200
          {
1201!
            keep_columns <- intersect(c(keys, group_var), colnames(ANL))
1202!
            labels <- vapply(ANL, attr, which = "label", FUN.VALUE = character(1L))
1203!
            ANL <- ANL %>%
1204!
              dplyr::filter(group_var_name %in% group_vals) %>%
1205!
              tidyr::pivot_longer(-keep_columns, values_transform = is.na) %>%
1206!
              dplyr::group_by(group_var_name, name) %>%
1207!
              dplyr::summarise(value = sum(value), perc = value / n()) %>%
1208!
              dplyr::mutate(label = labels[name])
1209
          },
1210!
          keys = join_keys(qenv) |> unlist() |> unique(),
1211!
          group_var_name = as.name(group_var),
1212!
          group_var = group_var,
1213!
          group_vals = req(group_vals)
1214
        )
1215
1216!
        tile <- within(ANL_q,
1217
          {
1218!
            by_variable_plot <- ggplot2::ggplot(ANL, ggplot2::aes(group_var_name, label)) +
1219!
              ggplot2::geom_tile(ggplot2::aes(fill = column), color = "gray90") +
1220!
              ggplot2::geom_text(ggplot2::aes(label = text_label),
1221!
                data = ~ dplyr::filter(.x, perc > 0)
1222
              ) +
1223!
              ggplot2::scale_x_discrete(expand = ggplot2::expansion()) +
1224!
              ggplot2::scale_fill_gradient(high = "#ff2951ff", low = "grey90", labels = labels) +
1225!
              labs +
1226!
              ggthemes
1227
          },
1228!
          text_label = if (input$count_type == "counts") as.name("value") else quote(scales::percent(perc)),
1229!
          group_var_name = as.name(group_var),
1230!
          column = if (input$count_type == "counts") {
1231!
            as.name("value")
1232
          } else {
1233!
            as.name("perc")
1234
          },
1235!
          labs = parsed_ggplot2_args$labs,
1236!
          labels = if (input$count_type == "counts") quote(ggplot2::waiver()) else quote(scales::label_percent()),
1237!
          ggthemes = parsed_ggplot2_args$ggtheme
1238
        )
1239
      }
1240!
      tile
1241
    })
1242
1243!
    by_subject_plot_q <- reactive({
1244
      # needed to trigger update on tab change
1245!
      req(
1246!
        input$summary_type == "Grouped by Subject", common_code_q(),
1247!
        input$ggtheme
1248
      )
1249
1250!
      teal::validate_has_data(req(data_r()), 1)
1251
1252!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
1253!
        labs = list(x = NULL, y = NULL),
1254!
        theme = list(legend.position = "bottom", axis.text.x = NULL)
1255
      )
1256
1257!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
1258!
        user_plot = ggplot2_args[["By Subject"]],
1259!
        user_default = ggplot2_args$default,
1260!
        module_plot = dev_ggplot2_args
1261
      )
1262
1263!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
1264!
        all_ggplot2_args,
1265!
        ggtheme = input$ggtheme
1266
      )
1267
1268
      # Unlikely that `rlang` is not available, new hashing may be expensive
1269!
      hashing_function <- if (requireNamespace("rlang", quietly = TRUE)) {
1270!
        quote(rlang::hash)
1271
      } else {
1272!
        function(x) paste(as.integer(x), collapse = "")
1273
      }
1274
1275!
      qenv <- req(common_code_q())
1276!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### By Subject Plot")
1277
1278!
      qenv <- teal.code::eval_code(
1279!
        qenv,
1280!
        substitute(
1281!
          expr = parent_keys <- keys,
1282!
          env = list(keys = data_parent_keys())
1283
        )
1284
      ) %>%
1285!
        teal.code::eval_code(
1286!
          substitute(
1287!
            expr = analysis_vars <- setdiff(colnames(ANL), data_keys),
1288!
            env = list(data_keys = data_keys())
1289
          )
1290
        ) %>%
1291!
        teal.code::eval_code(
1292!
          substitute(
1293!
            expr = {
1294!
              summary_plot_patients <- ANL[, c(parent_keys, analysis_vars)] %>%
1295!
                dplyr::group_by_at(parent_keys) %>%
1296!
                dplyr::mutate(id = dplyr::cur_group_id()) %>%
1297!
                dplyr::ungroup() %>%
1298!
                dplyr::group_by_at(c(parent_keys, "id")) %>%
1299!
                dplyr::summarise_all(anyNA) %>%
1300!
                dplyr::ungroup()
1301
1302
              # order subjects by decreasing number of missing and then by
1303
              # missingness pattern (defined using sha1)
1304!
              order_subjects <- summary_plot_patients %>%
1305!
                dplyr::select(-"id", -dplyr::all_of(parent_keys)) %>%
1306!
                dplyr::transmute(
1307!
                  id = dplyr::row_number(),
1308!
                  number_NA = apply(., 1, sum),
1309!
                  sha = apply(., 1, hashing_function)
1310
                ) %>%
1311!
                dplyr::arrange(dplyr::desc(number_NA), sha) %>%
1312!
                getElement(name = "id")
1313
1314
              # order columns by decreasing percent of missing values
1315!
              ordered_columns <- summary_plot_patients %>%
1316!
                dplyr::select(-"id", -dplyr::all_of(parent_keys)) %>%
1317!
                dplyr::summarise(
1318!
                  column = create_cols_labels(colnames(.)),
1319!
                  na_count = apply(., MARGIN = 2, FUN = sum),
1320!
                  na_percent = na_count / nrow(.) * 100
1321
                ) %>%
1322!
                dplyr::arrange(na_percent, dplyr::desc(column))
1323
1324!
              summary_plot_patients <- summary_plot_patients %>%
1325!
                tidyr::gather("col", "isna", -"id", -dplyr::all_of(parent_keys)) %>%
1326!
                dplyr::mutate(col = create_cols_labels(col))
1327
            },
1328!
            env = list(hashing_function = hashing_function)
1329
          )
1330
        )
1331
1332!
      qenv <- teal.code::eval_code(
1333!
        qenv,
1334!
        substitute(
1335!
          expr = {
1336!
            by_subject_plot <- ggplot2::ggplot(summary_plot_patients, ggplot2::aes(
1337!
              x = factor(id, levels = order_subjects),
1338!
              y = factor(col, levels = ordered_columns[["column"]]),
1339!
              fill = isna
1340
            )) +
1341!
              ggplot2::geom_raster() +
1342!
              ggplot2::annotate(
1343!
                "text",
1344!
                x = length(order_subjects),
1345!
                y = seq_len(nrow(ordered_columns)),
1346!
                hjust = 1,
1347!
                label = sprintf("%d [%.02f%%]", ordered_columns[["na_count"]], ordered_columns[["na_percent"]])
1348
              ) +
1349!
              ggplot2::scale_fill_manual(
1350!
                name = "",
1351!
                values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]),
1352!
                labels = c("Present", "Missing (at least one)")
1353
              ) +
1354!
              labs +
1355!
              ggthemes +
1356!
              themes
1357
          },
1358!
          env = list(
1359!
            labs = parsed_ggplot2_args$labs,
1360!
            themes = parsed_ggplot2_args$theme,
1361!
            ggthemes = parsed_ggplot2_args$ggtheme
1362
          )
1363
        )
1364
      )
1365
    })
1366
1367
    # Decorated outputs
1368
1369
    # Summary_plot_q
1370!
    decorated_summary_plot_q <- srv_decorate_teal_data(
1371!
      id = "dec_summary_plot",
1372!
      data = summary_plot_q,
1373!
      decorators = select_decorators(decorators, "summary_plot"),
1374!
      expr = quote({
1375!
        summary_plot
1376
      })
1377
    )
1378
1379!
    decorated_combination_plot_q <- srv_decorate_teal_data(
1380!
      id = "dec_combination_plot",
1381!
      data = combination_plot_q,
1382!
      decorators = select_decorators(decorators, "combination_plot"),
1383!
      expr = quote({
1384!
        grid::grid.newpage()
1385!
        grid::grid.draw(combination_plot)
1386
      })
1387
    )
1388
1389!
    decorated_by_variable_plot_q <- srv_decorate_teal_data(
1390!
      id = "dec_by_variable_plot",
1391!
      data = by_variable_plot_q,
1392!
      decorators = select_decorators(decorators, "by_variable_plot"),
1393!
      expr = quote(by_variable_plot)
1394
    )
1395
1396!
    decorated_by_subject_plot_q <- srv_decorate_teal_data(
1397!
      id = "dec_by_subject_plot",
1398!
      data = by_subject_plot_q,
1399!
      decorators = select_decorators(decorators, "by_subject_plot"),
1400!
      expr = quote(by_subject_plot)
1401
    )
1402
1403
    # Plots & tables reactives
1404
1405!
    summary_plot_r <- reactive({
1406!
      req(decorated_summary_plot_q())[["summary_plot"]]
1407
    })
1408
1409!
    combination_plot_r <- reactive({
1410!
      req(decorated_combination_plot_q())[["combination_plot"]]
1411
    })
1412
1413!
    by_variable_plot_r <- reactive({
1414!
      req(decorated_by_variable_plot_q())[["by_variable_plot"]]
1415
    })
1416
1417!
    by_subject_plot_r <- reactive({
1418!
      req(decorated_by_subject_plot_q()[["by_subject_plot"]])
1419
    })
1420
1421
    # Generate output
1422!
    pws1 <- teal.widgets::plot_with_settings_srv(
1423!
      id = "summary_plot",
1424!
      plot_r = summary_plot_r,
1425!
      height = plot_height,
1426!
      width = plot_width
1427
    )
1428
1429!
    pws2 <- teal.widgets::plot_with_settings_srv(
1430!
      id = "combination_plot",
1431!
      plot_r = combination_plot_r,
1432!
      height = plot_height,
1433!
      width = plot_width
1434
    )
1435
1436!
    pws3 <- teal.widgets::plot_with_settings_srv(
1437!
      id = "by_variable_plot",
1438!
      plot_r = by_variable_plot_r,
1439!
      height = plot_height,
1440!
      width = plot_width
1441
    )
1442
1443!
    pws4 <- teal.widgets::plot_with_settings_srv(
1444!
      id = "by_subject_plot",
1445!
      plot_r = by_subject_plot_r,
1446!
      height = plot_height,
1447!
      width = plot_width
1448
    )
1449
1450!
    decorated_summary_plot_dims_q <- set_chunk_dims(pws1, decorated_summary_plot_q)
1451
1452!
    decorated_combination_plot_dims_q <- # nolint: object_length_linter.
1453!
      set_chunk_dims(pws2, decorated_combination_plot_q)
1454
1455!
    decorated_by_variable_plot_dims_q <- # nolint: object_length_linter.
1456!
      set_chunk_dims(pws3, decorated_by_variable_plot_q)
1457
1458!
    decorated_by_subject_plot_dims_q <- # nolint: object_length_linter.
1459!
      set_chunk_dims(pws4, decorated_by_subject_plot_q)
1460
1461!
    decorated_final_q <- reactive({
1462!
      sum_type <- req(input$summary_type)
1463!
      if (sum_type == "Summary") {
1464!
        decorated_summary_plot_dims_q()
1465!
      } else if (sum_type == "Combinations") {
1466!
        decorated_combination_plot_dims_q()
1467!
      } else if (sum_type == "By Variable Levels") {
1468!
        decorated_by_variable_plot_dims_q()
1469!
      } else if (sum_type == "Grouped by Subject") {
1470!
        decorated_by_subject_plot_dims_q()
1471
      }
1472
    })
1473
1474!
    decorated_final_q
1475
  })
1476
}
1
#' `teal` module: R Markdown render
2
#'
3
#' @description
4
#' `r lifecycle::badge("experimental")`
5
#'
6
#' Module to render R Markdown files using the data provided in the
7
#' `teal_data` object.
8
#'
9
#' The R Markdown file should be designed to accept variables available
10
#' in the data names of the module.
11
#'
12
#' @details
13
#' For example, if the `teal_data` object contains datasets named `mtcars`
14
#'  and `iris`, the R Markdown file can use these as variables as they
15
#' will be available in the R Markdown environment.
16
#'
17
#' The libraries used in the R Markdown file must be available in the
18
#' deployed shiny app environment.
19
#'
20
#' When developing the R Markdown file, the working data can be simulated
21
#' on a code chunk, which in turn can look for the presence of `.raw_data`
22
#' object to determine if it is being run inside the `teal` module or not.
23
#'
24
#' Example R markdown file:
25
#'
26
#' ``````md
27
#' ---
28
#' title: "R Markdown Report"
29
#' output: html_document
30
#' ---
31
#'
32
#' ```{r eval=!exists(".raw_data")}
33
#' mtcars <- datasets::mtcars
34
#' iris <- datasets::iris
35
#' ```
36
#'
37
#' ```{r}
38
#' summary(mtcars) |> print()
39
#' summary(iris) |> print()
40
#' ```
41
#' ``````
42
#'
43
#' @inheritParams teal::module
44
#' @inheritParams shared_params
45
#'
46
#' @param rmd_content (`character`) Content of the R Markdown file to be rendered.
47
#' This can be the value of `readLines("path/to/file.Rmd")`.
48
#' @param allow_download (`logical`) whether to allow downloading of the R Markdown file.
49
#' Defaults to `TRUE`.
50
#' @param extra_transform (`list`) of [teal::teal_transform_module()] that will be added in the module's UI.
51
#' This can be used to create interactive inputs that modify the parameters in R Markdown rendering.
52
#'
53
#' @inherit shared_params return
54
#'
55
#' @inheritSection teal::example_module Reporting
56
#'
57
#' @examplesShinylive
58
#' library(teal.modules.general)
59
#' interactive <- function() TRUE
60
#' {{ next_example }}
61
#' @examples
62
#'
63
#' # general data example
64
#' data <- teal_data()
65
#' data <- within(data, {
66
#'   CO2 <- CO2
67
#' })
68
#'
69
#' app <- init(
70
#'   data = data,
71
#'   modules = modules(
72
#'     tm_rmarkdown(
73
#'       label = "RMarkdown Module",
74
#'       rmd_content = c(
75
#'         "---",
76
#'         "title: \"R Markdown Report\"",
77
#'         "output: html_document",
78
#'         "---",
79
#'         "",
80
#'         "```{r}",
81
#'         "summary(CO2) |> print()",
82
#'         "```"
83
#'       )
84
#'     )
85
#'   )
86
#' )
87
#' if (interactive()) {
88
#'   shinyApp(app$ui, app$server)
89
#' }
90
#'
91
#' @examples
92
#' nrow_transform <- teal_transform_module(
93
#'   label = "N Rows selector",
94
#'   ui = function(id) {
95
#'     ns <- NS(id)
96
#'     tags$div(
97
#'       numericInput(ns("n_rows"), "Show n rows", value = 40, min = 0, max = 200, step = 5)
98
#'     )
99
#'   },
100
#'   server = function(id, data) {
101
#'     moduleServer(id, function(input, output, session) {
102
#'       reactive({
103
#'         req(data())
104
#'         within(data(),
105
#'           {
106
#'             n_rows <- n_rows_value
107
#'           },
108
#'           n_rows_value = input$n_rows
109
#'         )
110
#'       })
111
#'     })
112
#'   }
113
#' )
114
#'
115
#' app <- init(
116
#'   data = data,
117
#'   modules = modules(
118
#'     tm_rmarkdown(
119
#'       label = "RMarkdown Module",
120
#'       rmd_content = readLines(
121
#'         system.file(
122
#'           file.path("sample_files", "co2_example.Rmd"),
123
#'           package = "teal.modules.general"
124
#'         )
125
#'       ),
126
#'       allow_download = FALSE,
127
#'       extra_transform = list(nrow_transform)
128
#'     )
129
#'   )
130
#' )
131
#'
132
#' if (interactive()) {
133
#'   shinyApp(app$ui, app$server)
134
#' }
135
#' @export
136
tm_rmarkdown <- function(label = "RMarkdown Module",
137
                         rmd_content,
138
                         datanames = "all",
139
                         allow_download = TRUE,
140
                         pre_output = NULL,
141
                         post_output = NULL,
142
                         transformators = list(),
143
                         extra_transform = list()) {
144!
  message("Initializing tm_rmarkdown")
145
146
  # Start of assertions
147
148!
  checkmate::assert_string(label)
149!
  checkmate::assert_character(rmd_content)
150!
  checkmate::assert_flag(allow_download)
151
152!
  checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
153!
  checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
154
155
  # End of assertions
156
157
  # Make UI args
158!
  args <- as.list(environment())
159
160!
  ans <- module(
161!
    label = label,
162!
    server = srv_rmarkdown,
163!
    server_args = list(rmd_content = rmd_content, allow_download = allow_download, extra_transform = extra_transform),
164!
    ui = ui_rmarkdown,
165!
    ui_args = args,
166!
    transformators = transformators,
167!
    datanames = datanames
168
  )
169!
  disable_src(ans)
170
}
171
172
# UI function for the rmarkdown module
173
ui_rmarkdown <- function(id, rmd_content, allow_download, extra_transform, ...) {
174!
  args <- list(...)
175!
  ns <- NS(id)
176
177!
  teal.widgets::standard_layout(
178!
    output = teal.widgets::white_small_well(
179!
      tags$div(
180!
        tags$h4("Rendered report from Rmd"),
181!
        if (allow_download) {
182!
          downloadButton(
183!
            ns("download_rmd"),
184!
            sprintf("Download R Markdown file"),
185!
            class = "btn-primary btn-sm"
186
          )
187
        },
188!
        ui_transform_teal_data(ns("extra_transform"), transformators = extra_transform)
189
      ),
190!
      tags$hr(),
191!
      uiOutput(ns("rmd_output"))
192
    ),
193!
    encoding = NULL,
194!
    pre_output = args$pre_output,
195!
    post_output = args$post_output
196
  )
197
}
198
199
# Server function for the rmarkdown module
200
srv_rmarkdown <- function(id, data, rmd_content, allow_download, extra_transform) {
2013x
  checkmate::assert_class(data, "reactive")
2023x
  checkmate::assert_class(isolate(data()), "teal_data")
2033x
  moduleServer(id, function(input, output, session) {
2043x
    pre_decorated_q_r <- reactive({
2053x
      data_q <- req(data())
2063x
      teal.reporter::teal_card(data_q) <- c(
2073x
        teal.reporter::teal_card(data_q),
2083x
        teal.reporter::teal_card("## Module's output(s)")
209
      )
2103x
      data_q
211
    })
212
2133x
    q_r <- data_with_output_decorated <- teal::srv_transform_teal_data(
2143x
      "extra_transform",
2153x
      data = pre_decorated_q_r,
2163x
      transformators = extra_transform
217
    )
218
2193x
    if (allow_download) {
2203x
      output$download_rmd <- downloadHandler(
2213x
        filename = function() sprintf("from_teal_module-%s.Rmd", format(Sys.time(), "%Y%m%d_%H%M")),
2223x
        content = function(file) {
223
          # find the end of the YAML header or start of the file
224
          # and insert the contents of teal.code::get_code(q_r())
225!
          yaml_end <- which(rmd_content == "---")[2]
226!
          insert_pos <- if (!is.na(yaml_end)) yaml_end else 0
227!
          note_lines <- c(
228
            "",
229!
            "### Pre-processing data",
230
            "",
231!
            "The following code chunk was automatically added by the teal markdown module.",
232!
            "It shows how to generate the data used in this report.",
233
            "",
234!
            "```{r}",
235!
            teal.code::get_code(q_r()),
236
            "```",
237
            ""
238
          )
239!
          rmd_content <- append(rmd_content, note_lines, after = insert_pos)
240!
          writeLines(rmd_content, con = file)
241
        },
2423x
        contentType = "text/plain"
243
      )
244
    }
245
2463x
    clean_up_r <- shiny::reactiveVal(list())
247
    # Can only clean on sessionEnded as temporary files are needed for the reporter
248
    # during session
2493x
    onSessionEnded(function() {
2503x
      logger::log_debug("srv_rmarkdown: cleaning up temporary folders.")
2513x
      lapply(shiny::isolate(clean_up_r()), function(f) f())
2523x
    }, session)
253
2543x
    rendered_path_r <- reactive({
2553x
      datasets <- rlang::env_clone(
2563x
        as.environment(req(q_r())),
2573x
        parent = new.env() # Ensuring a clean parent environment that can load libraries
2583x
      ) # Clone to use unlocked environment
2593x
      temp_dir <- tempfile(pattern = "rmd_")
2603x
      dir.create(temp_dir, showWarnings = FALSE, recursive = TRUE)
2613x
      temp_rmd <- tempfile(pattern = "rmarkdown_module-", tmpdir = temp_dir, fileext = ".Rmd")
262
      # Schedule cleanup of temp files when reactive is re-executed
2633x
      shiny::isolate({
2643x
        old_clean_up <- clean_up_r()
2653x
        clean_up_r(c(old_clean_up, function() unlink(temp_dir, recursive = TRUE)))
266
      })
2673x
      writeLines(rmd_content, con = temp_rmd)
268
2693x
      tryCatch(
270
        {
2713x
          rmarkdown::render(
2723x
            temp_rmd,
2733x
            output_format = rmarkdown::md_document(
2743x
              variant = "markdown",
2753x
              standalone = TRUE,
2763x
              dev = "png"
277
            ),
2783x
            envir = datasets,
2793x
            quiet = TRUE,
2803x
            runtime = "static"
281
          )
282
        },
2833x
        error = function(e) {
284!
          warning("Error rendering RMD file: ", e$message) # verbose error in logs
285!
          e
286
        }
287
      )
288
    })
289
2903x
    rendered_html_r <- reactive({
2912x
      output_path <- req(rendered_path_r())
2922x
      validate(
2932x
        need(inherits(output_path, "character"), "Error rendering RMD file. Please contact the app developer.")
294
      )
2952x
      shiny::includeMarkdown(output_path)
296
    })
297
2983x
    output$rmd_output <- renderUI(rendered_html_r())
299
3003x
    result <- reactive({
3011x
      out_data <- q_r()
3021x
      report_doc <- .markdown_internal(rendered_path_r(), rendered_html_r())
3031x
      teal.reporter::teal_card(out_data) <- c(
3041x
        teal.reporter::teal_card(out_data), report_doc
305
      )
3061x
      out_data
307
    })
3083x
    result
309
  })
310
}
311
312
#' Create internal markdown object for use in reporter
313
#'
314
#' Creates an object of class `markdown_internal` that contains the
315
#' content of a markdown file.
316
#'
317
#' This package registers S3 methods for `toHTML` and `to_rmd` for this class to
318
#' facilitate rendering in `teal.reporter`.
319
#'
320
#' @param markdown_file (`character(1)`) path to markdown file.
321
#' @param rendered_html (`shiny.tag`) rendered HTML content.
322
#'
323
#' @return `markdown_internal` object
324
#'
325
#' @keywords internal
326
.markdown_internal <- function(markdown_file, rendered_html) {
3271x
  base_file <- basename(markdown_file)
328
329
  # Create new custom structure with contents and images in base64 as attribute
3301x
  structure(
3311x
    readLines(markdown_file),
3321x
    class = c("markdown_internal", "character"),
3331x
    parent_path = dirname(markdown_file),
3341x
    old_base_path = sprintf("%s_files/", tools::file_path_sans_ext(base_file)),
3351x
    cached_html = rendered_html
336
  )
337
}
338
339
#' @describeIn dot-markdown_internal Custom [tools::toHTML()] method for markdown_internal class that
340
#' uses a cached rendering of the module.
341
#' @inheritParams tools::toHTML
342
#' @param ... Arguments that will be passed to the next method.
343
#' @exportS3Method tools::toHTML
344
toHTML.markdown_internal <- function(x, ...) {
345!
  cached_html <- attr(x, "cached_html", exact = TRUE)
346!
  if (!is.null(cached_html)) {
347!
    return(cached_html)
348
  }
349!
  NextMethod(unclass(x), ...)
350
}
351
352
#' @describeIn dot-markdown_internal Custom [teal.reporter::to_rmd()] method for `markdown_internal`
353
#' object will be used to render the report.
354
#' @inheritParams teal.reporter::to_rmd
355
#' @param figures_dir (`character(1)`) directory where the R markdown auxiliary files will be saved.
356
#' @exportS3Method teal.reporter::to_rmd
357
to_rmd.markdown_internal <- function(block, figures_dir = "figures", ...) {
358!
  old_base_path <- attr(block, "old_base_path", exact = TRUE)
359!
  parent_path <- attr(block, "parent_path", exact = TRUE)
360!
  new_base_path <- file.path(figures_dir, old_base_path)
361
362
  # Copy figures from old path to new location
363!
  dir.create(figures_dir, showWarnings = FALSE, recursive = TRUE)
364!
  file.copy(file.path(parent_path, old_base_path), figures_dir, recursive = TRUE)
365
366
  # Change the image paths in the markdown content
367!
  block <- gsub(pattern = old_base_path, replacement = new_base_path, x = block, fixed = TRUE)
368!
  NextMethod(unclass(block), ...)
369
}
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
#' @inheritSection teal::example_module Reporting
71
#'
72
#' @examplesShinylive
73
#' library(teal.modules.general)
74
#' interactive <- function() TRUE
75
#' {{ next_example }}
76
#' @examples
77
#'
78
#' # general data example
79
#' data <- teal_data()
80
#' data <- within(data, {
81
#'   require(nestcolor)
82
#'   CO2 <- CO2
83
#' })
84
#'
85
#' app <- init(
86
#'   data = data,
87
#'   modules = modules(
88
#'     tm_a_regression(
89
#'       label = "Regression",
90
#'       response = data_extract_spec(
91
#'         dataname = "CO2",
92
#'         select = select_spec(
93
#'           label = "Select variable:",
94
#'           choices = "uptake",
95
#'           selected = "uptake",
96
#'           multiple = FALSE,
97
#'           fixed = TRUE
98
#'         )
99
#'       ),
100
#'       regressor = data_extract_spec(
101
#'         dataname = "CO2",
102
#'         select = select_spec(
103
#'           label = "Select variables:",
104
#'           choices = variable_choices(data[["CO2"]], c("conc", "Treatment")),
105
#'           selected = "conc",
106
#'           multiple = TRUE,
107
#'           fixed = FALSE
108
#'         )
109
#'       )
110
#'     )
111
#'   )
112
#' )
113
#' if (interactive()) {
114
#'   shinyApp(app$ui, app$server)
115
#' }
116
#'
117
#' @examplesShinylive
118
#' library(teal.modules.general)
119
#' interactive <- function() TRUE
120
#' {{ next_example }}
121
#' @examples
122
#' # CDISC data example
123
#' data <- teal_data()
124
#' data <- within(data, {
125
#'   require(nestcolor)
126
#'   ADSL <- teal.data::rADSL
127
#' })
128
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
129
#'
130
#' app <- init(
131
#'   data = data,
132
#'   modules = modules(
133
#'     tm_a_regression(
134
#'       label = "Regression",
135
#'       response = data_extract_spec(
136
#'         dataname = "ADSL",
137
#'         select = select_spec(
138
#'           label = "Select variable:",
139
#'           choices = "BMRKR1",
140
#'           selected = "BMRKR1",
141
#'           multiple = FALSE,
142
#'           fixed = TRUE
143
#'         )
144
#'       ),
145
#'       regressor = data_extract_spec(
146
#'         dataname = "ADSL",
147
#'         select = select_spec(
148
#'           label = "Select variables:",
149
#'           choices = variable_choices(data[["ADSL"]], c("AGE", "SEX", "RACE")),
150
#'           selected = "AGE",
151
#'           multiple = TRUE,
152
#'           fixed = FALSE
153
#'         )
154
#'       )
155
#'     )
156
#'   )
157
#' )
158
#' if (interactive()) {
159
#'   shinyApp(app$ui, app$server)
160
#' }
161
#'
162
#' @export
163
#'
164
tm_a_regression <- function(label = "Regression Analysis",
165
                            regressor,
166
                            response,
167
                            plot_height = c(600, 200, 2000),
168
                            plot_width = NULL,
169
                            alpha = c(1, 0, 1),
170
                            size = c(2, 1, 8),
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
                            default_plot_type = 1,
176
                            default_outlier_label = "USUBJID",
177
                            label_segment_threshold = c(0.5, 0, 10),
178
                            transformators = list(),
179
                            decorators = list()) {
180!
  message("Initializing tm_a_regression")
181
182
  # Normalize the parameters
183!
  if (inherits(regressor, "data_extract_spec")) regressor <- list(regressor)
184!
  if (inherits(response, "data_extract_spec")) response <- list(response)
185!
  if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args)
186
187
  # Start of assertions
188!
  checkmate::assert_string(label)
189!
  checkmate::assert_list(regressor, types = "data_extract_spec")
190
191!
  checkmate::assert_list(response, types = "data_extract_spec")
192!
  assert_single_selection(response)
193
194!
  checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE)
195!
  checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height")
196
197!
  checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE)
198!
  checkmate::assert_numeric(
199!
    plot_width[1],
200!
    lower = plot_width[2],
201!
    upper = plot_width[3],
202!
    null.ok = TRUE,
203!
    .var.name = "plot_width"
204
  )
205
206!
  if (length(alpha) == 1) {
207!
    checkmate::assert_numeric(alpha, any.missing = FALSE, finite = TRUE)
208
  } else {
209!
    checkmate::assert_numeric(alpha, len = 3, any.missing = FALSE, finite = TRUE)
210!
    checkmate::assert_numeric(alpha[1], lower = alpha[2], upper = alpha[3], .var.name = "alpha")
211
  }
212
213!
  if (length(size) == 1) {
214!
    checkmate::assert_numeric(size, any.missing = FALSE, finite = TRUE)
215
  } else {
216!
    checkmate::assert_numeric(size, len = 3, any.missing = FALSE, finite = TRUE)
217!
    checkmate::assert_numeric(size[1], lower = size[2], upper = size[3], .var.name = "size")
218
  }
219
220!
  ggtheme <- match.arg(ggtheme)
221
222!
  plot_choices <- c(
223!
    "Response vs Regressor", "Residuals vs Fitted", "Normal Q-Q", "Scale-Location",
224!
    "Cook's distance", "Residuals vs Leverage", "Cook's dist vs Leverage"
225
  )
226!
  checkmate::assert_list(ggplot2_args, types = "ggplot2_args")
227!
  checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices))
228
229!
  checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
230!
  checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
231!
  checkmate::assert_choice(default_plot_type, seq.int(1L, length(plot_choices)))
232!
  checkmate::assert_string(default_outlier_label)
233!
  checkmate::assert_list(decorators, "teal_transform_module")
234
235!
  if (length(label_segment_threshold) == 1) {
236!
    checkmate::assert_numeric(label_segment_threshold, any.missing = FALSE, finite = TRUE)
237
  } else {
238!
    checkmate::assert_numeric(label_segment_threshold, len = 3, any.missing = FALSE, finite = TRUE)
239!
    checkmate::assert_numeric(
240!
      label_segment_threshold[1],
241!
      lower = label_segment_threshold[2],
242!
      upper = label_segment_threshold[3],
243!
      .var.name = "label_segment_threshold"
244
    )
245
  }
246!
  assert_decorators(decorators, "plot")
247
  # End of assertions
248
249
  # Make UI args
250!
  args <- as.list(environment())
251!
  args[["plot_choices"]] <- plot_choices
252!
  data_extract_list <- list(
253!
    regressor = regressor,
254!
    response = response
255
  )
256
257!
  ans <- module(
258!
    label = label,
259!
    server = srv_a_regression,
260!
    ui = ui_a_regression,
261!
    ui_args = args,
262!
    server_args = c(
263!
      data_extract_list,
264!
      list(
265!
        plot_height = plot_height,
266!
        plot_width = plot_width,
267!
        default_outlier_label = default_outlier_label,
268!
        ggplot2_args = ggplot2_args,
269!
        decorators = decorators
270
      )
271
    ),
272!
    transformators = transformators,
273!
    datanames = teal.transform::get_extract_datanames(data_extract_list)
274
  )
275!
  attr(ans, "teal_bookmarkable") <- FALSE
276!
  ans
277
}
278
279
# UI function for the regression module
280
ui_a_regression <- function(id, ...) {
281!
  ns <- NS(id)
282!
  args <- list(...)
283!
  is_single_dataset_value <- teal.transform::is_single_dataset(args$regressor, args$response)
284!
  teal.widgets::standard_layout(
285!
    output = teal.widgets::white_small_well(tags$div(
286!
      teal.widgets::plot_with_settings_ui(id = ns("myplot")),
287!
      tags$div(verbatimTextOutput(ns("text")))
288
    )),
289!
    encoding = tags$div(
290!
      tags$label("Encodings", class = "text-primary"), tags$br(),
291!
      teal.transform::datanames_input(args[c("response", "regressor")]),
292!
      teal.transform::data_extract_ui(
293!
        id = ns("response"),
294!
        label = "Response variable",
295!
        data_extract_spec = args$response,
296!
        is_single_dataset = is_single_dataset_value
297
      ),
298!
      teal.transform::data_extract_ui(
299!
        id = ns("regressor"),
300!
        label = "Regressor variables",
301!
        data_extract_spec = args$regressor,
302!
        is_single_dataset = is_single_dataset_value
303
      ),
304!
      radioButtons(
305!
        ns("plot_type"),
306!
        label = "Plot type:",
307!
        choices = args$plot_choices,
308!
        selected = args$plot_choices[args$default_plot_type]
309
      ),
310!
      checkboxInput(ns("show_outlier"), label = "Display outlier labels", value = TRUE),
311!
      conditionalPanel(
312!
        condition = "input['show_outlier']",
313!
        ns = ns,
314!
        teal.widgets::optionalSliderInput(
315!
          ns("outlier"),
316!
          tags$div(
317!
            tagList(
318!
              "Outlier definition:",
319!
              bslib::tooltip(
320!
                icon("fas fa-circle-info"),
321!
                paste(
322!
                  "Use the slider to choose the cut-off value to define outliers.",
323!
                  "Points with a Cook's distance greater than",
324!
                  "the value on the slider times the mean of the Cook's distance of the dataset will have labels."
325
                )
326
              )
327
            )
328
          ),
329!
          min = 1, max = 10, value = 9, ticks = FALSE, step = .1
330
        ),
331!
        teal.widgets::optionalSelectInput(
332!
          ns("label_var"),
333!
          multiple = FALSE,
334!
          label = "Outlier label"
335
        )
336
      ),
337!
      ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")),
338!
      bslib::accordion(
339!
        open = TRUE,
340!
        bslib::accordion_panel(
341!
          title = "Plot settings",
342!
          teal.widgets::optionalSliderInputValMinMax(ns("alpha"), "Opacity:", args$alpha, ticks = FALSE),
343!
          teal.widgets::optionalSliderInputValMinMax(ns("size"), "Points size:", args$size, ticks = FALSE),
344!
          teal.widgets::optionalSliderInputValMinMax(
345!
            inputId = ns("label_min_segment"),
346!
            label = tags$div(
347!
              tagList(
348!
                "Label min. segment:",
349!
                bslib::tooltip(
350!
                  icon("circle-info"),
351!
                  tags$span(
352!
                    paste(
353!
                      "Use the slider to choose the cut-off value to define minimum distance between label and point",
354!
                      "that generates a line segment.",
355!
                      "It's only valid when 'Display outlier labels' is checked."
356
                    )
357
                  )
358
                )
359
              )
360
            ),
361!
            value_min_max = args$label_segment_threshold,
362
            # Extra parameters to sliderInput
363!
            ticks = FALSE,
364!
            step = .1,
365!
            round = FALSE
366
          ),
367!
          selectInput(
368!
            inputId = ns("ggtheme"),
369!
            label = "Theme (by ggplot):",
370!
            choices = ggplot_themes,
371!
            selected = args$ggtheme,
372!
            multiple = FALSE
373
          )
374
        )
375
      )
376
    ),
377!
    pre_output = args$pre_output,
378!
    post_output = args$post_output
379
  )
380
}
381
382
# Server function for the regression module
383
srv_a_regression <- function(id,
384
                             data,
385
                             response,
386
                             regressor,
387
                             plot_height,
388
                             plot_width,
389
                             ggplot2_args,
390
                             default_outlier_label,
391
                             decorators) {
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!
    rule_rvr1 <- function(value) {
400!
      if (isTRUE(input$plot_type == "Response vs Regressor")) {
401!
        if (length(value) > 1L) {
402!
          "This plot can only have one regressor."
403
        }
404
      }
405
    }
406!
    rule_rvr2 <- function(other) {
407!
      function(value) {
408!
        if (isTRUE(input$plot_type == "Response vs Regressor")) {
409!
          otherval <- selector_list()[[other]]()$select
410!
          if (isTRUE(value == otherval)) {
411!
            "Response and Regressor must be different."
412
          }
413
        }
414
      }
415
    }
416
417!
    selector_list <- teal.transform::data_extract_multiple_srv(
418!
      data_extract = list(response = response, regressor = regressor),
419!
      datasets = data,
420!
      select_validation_rule = list(
421!
        regressor = shinyvalidate::compose_rules(
422!
          shinyvalidate::sv_required("At least one regressor should be selected."),
423!
          rule_rvr1,
424!
          rule_rvr2("response")
425
        ),
426!
        response = shinyvalidate::compose_rules(
427!
          shinyvalidate::sv_required("At least one response should be selected."),
428!
          rule_rvr2("regressor")
429
        )
430
      )
431
    )
432
433!
    iv_r <- reactive({
434!
      iv <- shinyvalidate::InputValidator$new()
435!
      teal.transform::compose_and_enable_validators(iv, selector_list)
436
    })
437
438!
    iv_out <- shinyvalidate::InputValidator$new()
439!
    iv_out$condition(~ isTRUE(input$show_outlier))
440!
    iv_out$add_rule("label_var", shinyvalidate::sv_required("Please provide an `Outlier label` variable"))
441!
    iv_out$enable()
442
443!
    anl_merged_input <- teal.transform::merge_expression_srv(
444!
      selector_list = selector_list,
445!
      datasets = data
446
    )
447
448!
    regression_var <- reactive({
449!
      teal::validate_inputs(iv_r())
450
451!
      list(
452!
        response = as.vector(anl_merged_input()$columns_source$response),
453!
        regressor = as.vector(anl_merged_input()$columns_source$regressor)
454
      )
455
    })
456
457!
    qenv <- reactive({
458!
      obj <- data()
459!
      teal.reporter::teal_card(obj) <-
460!
        c(
461!
          teal.reporter::teal_card(obj),
462!
          teal.reporter::teal_card("## Module's output(s)")
463
        )
464!
      teal.code::eval_code(obj, "library(ggplot2);library(dplyr)")
465
    })
466
467!
    anl_merged_q <- reactive({
468!
      req(anl_merged_input())
469!
      qenv() %>%
470!
        teal.code::eval_code(as.expression(anl_merged_input()$expr))
471
    })
472
473
    # sets qenv object and populates it with data merge call and fit expression
474!
    fit_r <- reactive({
475!
      ANL <- anl_merged_q()[["ANL"]]
476!
      teal::validate_has_data(ANL, 10)
477
478!
      validate(need(is.numeric(ANL[regression_var()$response][[1]]), "Response variable should be numeric."))
479
480!
      teal::validate_has_data(
481!
        ANL[, c(regression_var()$response, regression_var()$regressor)], 10,
482!
        complete = TRUE, allow_inf = FALSE
483
      )
484
485!
      form <- stats::as.formula(
486!
        paste(
487!
          regression_var()$response,
488!
          paste(
489!
            regression_var()$regressor,
490!
            collapse = " + "
491
          ),
492!
          sep = " ~ "
493
        )
494
      )
495
496!
      if (input$show_outlier) {
497!
        opts <- teal.transform::variable_choices(ANL)
498!
        selected <- if (!is.null(isolate(input$label_var)) && isolate(input$label_var) %in% as.character(opts)) {
499!
          isolate(input$label_var)
500
        } else {
501!
          if (length(opts[as.character(opts) == default_outlier_label]) == 0) {
502!
            opts[[1]]
503
          } else {
504!
            opts[as.character(opts) == default_outlier_label]
505
          }
506
        }
507!
        teal.widgets::updateOptionalSelectInput(
508!
          session = session,
509!
          inputId = "label_var",
510!
          choices = opts,
511!
          selected = restoreInput(ns("label_var"), selected)
512
        )
513
514!
        data <- ggplot2::fortify(stats::lm(form, data = ANL))
515!
        cooksd <- data$.cooksd[!is.nan(data$.cooksd)]
516!
        max_outlier <- max(ceiling(max(cooksd) / mean(cooksd)), 2)
517!
        cur_outlier <- isolate(input$outlier)
518!
        updateSliderInput(
519!
          session = session,
520!
          inputId = "outlier",
521!
          min = 1,
522!
          max = max_outlier,
523!
          value = restoreInput(ns("outlier"), if (cur_outlier < max_outlier) cur_outlier else max_outlier * .9)
524
        )
525
      }
526
527!
      anl_fit <- anl_merged_q() %>%
528!
        teal.code::eval_code(substitute(fit <- stats::lm(form, data = ANL), env = list(form = form))) %>%
529!
        teal.code::eval_code(quote({
530!
          for (regressor in names(fit$contrasts)) {
531!
            alts <- paste0(levels(ANL[[regressor]]), collapse = "|")
532!
            names(fit$coefficients) <- gsub(
533!
              paste0("^(", regressor, ")(", alts, ")$"), paste0("\\1", ": ", "\\2"), names(fit$coefficients)
534
            )
535
          }
536
        })) %>%
537!
        teal.code::eval_code(quote({
538!
          fit_summary <- summary(fit)
539!
          fit_summary
540
        }))
541!
      teal.reporter::teal_card(anl_fit) <- c(teal.reporter::teal_card(anl_fit), "### Plot")
542!
      anl_fit
543
    })
544
545!
    label_col <- reactive({
546!
      teal::validate_inputs(iv_out)
547
548!
      substitute(
549!
        expr = dplyr::if_else(
550!
          data$.cooksd > outliers * mean(data$.cooksd, na.rm = TRUE),
551!
          as.character(stats::na.omit(ANL)[[label_var]]),
552
          ""
553
        ) %>%
554!
          dplyr::if_else(is.na(.), "cooksd == NaN", .),
555!
        env = list(outliers = input$outlier, label_var = input$label_var)
556
      )
557
    })
558
559!
    label_min_segment <- reactive({
560!
      input$label_min_segment
561
    })
562
563!
    outlier_label <- reactive({
564!
      substitute(
565!
        expr = ggrepel::geom_text_repel(
566!
          label = label_col,
567!
          color = "red",
568!
          hjust = 0,
569!
          vjust = 1,
570!
          max.overlaps = Inf,
571!
          min.segment.length = label_min_segment,
572!
          segment.alpha = 0.5,
573!
          seed = 123
574
        ),
575!
        env = list(label_col = label_col(), label_min_segment = label_min_segment())
576
      )
577
    })
578
579!
    output_plot_base <- reactive({
580!
      base_fit <- fit_r()
581!
      teal.code::eval_code(
582!
        base_fit,
583!
        quote({
584!
          class(fit$residuals) <- NULL
585
586!
          data <- ggplot2::fortify(fit)
587
588!
          smooth <- function(x, y) {
589!
            as.data.frame(stats::lowess(x, y, f = 2 / 3, iter = 3))
590
          }
591
592!
          smoothy_aes <- ggplot2::aes_string(x = "x", y = "y")
593
594!
          reg_form <- deparse(fit$call[[2]])
595
        })
596
      )
597
    })
598
599!
    output_plot_0 <- reactive({
600!
      fit <- fit_r()[["fit"]]
601!
      ANL <- anl_merged_q()[["ANL"]]
602
603!
      stopifnot(ncol(fit$model) == 2)
604
605!
      if (!is.factor(ANL[[regression_var()$regressor]])) {
606!
        shinyjs::show("size")
607!
        shinyjs::show("alpha")
608!
        plot <- substitute(
609!
          expr = ggplot2::ggplot(fit$model[, 2:1], ggplot2::aes_string(regressor, response)) +
610!
            ggplot2::geom_point(size = size, alpha = alpha) +
611!
            ggplot2::stat_smooth(method = "lm", formula = y ~ x, se = FALSE),
612!
          env = list(
613!
            regressor = regression_var()$regressor,
614!
            response = regression_var()$response,
615!
            size = input$size,
616!
            alpha = input$alpha
617
          )
618
        )
619!
        if (input$show_outlier) {
620!
          plot <- substitute(
621!
            expr = plot + outlier_label,
622!
            env = list(plot = plot, outlier_label = outlier_label())
623
          )
624
        }
625
      } else {
626!
        shinyjs::hide("size")
627!
        shinyjs::hide("alpha")
628!
        plot <- substitute(
629!
          expr = ggplot2::ggplot(fit$model[, 2:1], ggplot2::aes_string(regressor, response)) +
630!
            ggplot2::geom_boxplot(),
631!
          env = list(regressor = regression_var()$regressor, response = regression_var()$response)
632
        )
633!
        if (input$show_outlier) {
634!
          plot <- substitute(expr = plot + outlier_label, env = list(plot = plot, outlier_label = outlier_label()))
635
        }
636
      }
637
638!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
639!
        teal.widgets::resolve_ggplot2_args(
640!
          user_plot = ggplot2_args[["Response vs Regressor"]],
641!
          user_default = ggplot2_args$default,
642!
          module_plot = teal.widgets::ggplot2_args(
643!
            labs = list(
644!
              title = "Response vs Regressor",
645!
              x = varname_w_label(regression_var()$regressor, ANL),
646!
              y = varname_w_label(regression_var()$response, ANL)
647
            ),
648!
            theme = list()
649
          )
650
        ),
651!
        ggtheme = input$ggtheme
652
      )
653
654!
      teal.code::eval_code(
655!
        fit_r(),
656!
        substitute(
657!
          expr = {
658!
            class(fit$residuals) <- NULL
659!
            data <- ggplot2::fortify(fit)
660!
            plot <- graph
661
          },
662!
          env = list(
663!
            graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args))
664
          )
665
        )
666
      )
667
    })
668
669!
    output_plot_1 <- reactive({
670!
      plot_base <- output_plot_base()
671!
      shinyjs::show("size")
672!
      shinyjs::show("alpha")
673!
      plot <- substitute(
674!
        expr = ggplot2::ggplot(data = data, ggplot2::aes(.fitted, .resid)) +
675!
          ggplot2::geom_point(size = size, alpha = alpha) +
676!
          ggplot2::geom_hline(yintercept = 0, linetype = "dashed", size = 1) +
677!
          ggplot2::geom_line(data = smoothy, mapping = smoothy_aes),
678!
        env = list(size = input$size, alpha = input$alpha)
679
      )
680!
      if (input$show_outlier) {
681!
        plot <- substitute(expr = plot + outlier_label, env = list(plot = plot, outlier_label = outlier_label()))
682
      }
683
684!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
685!
        teal.widgets::resolve_ggplot2_args(
686!
          user_plot = ggplot2_args[["Residuals vs Fitted"]],
687!
          user_default = ggplot2_args$default,
688!
          module_plot = teal.widgets::ggplot2_args(
689!
            labs = list(
690!
              x = quote(paste0("Fitted values\nlm(", reg_form, ")")),
691!
              y = "Residuals",
692!
              title = "Residuals vs Fitted"
693
            )
694
          )
695
        ),
696!
        ggtheme = input$ggtheme
697
      )
698
699!
      teal.code::eval_code(
700!
        plot_base,
701!
        substitute(
702!
          expr = {
703!
            smoothy <- smooth(data$.fitted, data$.resid)
704!
            plot <- graph
705
          },
706!
          env = list(
707!
            graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args))
708
          )
709
        )
710
      )
711
    })
712
713!
    output_plot_2 <- reactive({
714!
      shinyjs::show("size")
715!
      shinyjs::show("alpha")
716!
      plot_base <- output_plot_base()
717!
      plot <- substitute(
718!
        expr = ggplot2::ggplot(data = data, ggplot2::aes(sample = .stdresid)) +
719!
          ggplot2::stat_qq(size = size, alpha = alpha) +
720!
          ggplot2::geom_abline(linetype = "dashed"),
721!
        env = list(size = input$size, alpha = input$alpha)
722
      )
723!
      if (input$show_outlier) {
724!
        plot <- substitute(
725!
          expr = plot +
726!
            ggplot2::stat_qq(
727!
              geom = ggrepel::GeomTextRepel,
728!
              label = label_col %>%
729!
                data.frame(label = .) %>%
730!
                dplyr::filter(label != "cooksd == NaN") %>%
731!
                unlist(),
732!
              color = "red",
733!
              hjust = 0,
734!
              vjust = 0,
735!
              max.overlaps = Inf,
736!
              min.segment.length = label_min_segment,
737!
              segment.alpha = .5,
738!
              seed = 123
739
            ),
740!
          env = list(plot = plot, label_col = label_col(), label_min_segment = label_min_segment())
741
        )
742
      }
743
744!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
745!
        teal.widgets::resolve_ggplot2_args(
746!
          user_plot = ggplot2_args[["Normal Q-Q"]],
747!
          user_default = ggplot2_args$default,
748!
          module_plot = teal.widgets::ggplot2_args(
749!
            labs = list(
750!
              x = quote(paste0("Theoretical Quantiles\nlm(", reg_form, ")")),
751!
              y = "Standardized residuals",
752!
              title = "Normal Q-Q"
753
            )
754
          )
755
        ),
756!
        ggtheme = input$ggtheme
757
      )
758
759!
      teal.code::eval_code(
760!
        plot_base,
761!
        substitute(
762!
          expr = {
763!
            plot <- graph
764
          },
765!
          env = list(
766!
            graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args))
767
          )
768
        )
769
      )
770
    })
771
772!
    output_plot_3 <- reactive({
773!
      shinyjs::show("size")
774!
      shinyjs::show("alpha")
775!
      plot_base <- output_plot_base()
776!
      plot <- substitute(
777!
        expr = ggplot2::ggplot(data = data, ggplot2::aes(.fitted, sqrt(abs(.stdresid)))) +
778!
          ggplot2::geom_point(size = size, alpha = alpha) +
779!
          ggplot2::geom_line(data = smoothy, mapping = smoothy_aes),
780!
        env = list(size = input$size, alpha = input$alpha)
781
      )
782!
      if (input$show_outlier) {
783!
        plot <- substitute(expr = plot + outlier_label, env = list(plot = plot, outlier_label = outlier_label()))
784
      }
785
786!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
787!
        teal.widgets::resolve_ggplot2_args(
788!
          user_plot = ggplot2_args[["Scale-Location"]],
789!
          user_default = ggplot2_args$default,
790!
          module_plot = teal.widgets::ggplot2_args(
791!
            labs = list(
792!
              x = quote(paste0("Fitted values\nlm(", reg_form, ")")),
793!
              y = quote(expression(sqrt(abs(`Standardized residuals`)))),
794!
              title = "Scale-Location"
795
            )
796
          )
797
        ),
798!
        ggtheme = input$ggtheme
799
      )
800
801!
      teal.code::eval_code(
802!
        plot_base,
803!
        substitute(
804!
          expr = {
805!
            smoothy <- smooth(data$.fitted, sqrt(abs(data$.stdresid)))
806!
            plot <- graph
807
          },
808!
          env = list(
809!
            graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args))
810
          )
811
        )
812
      )
813
    })
814
815!
    output_plot_4 <- reactive({
816!
      shinyjs::hide("size")
817!
      shinyjs::show("alpha")
818!
      plot_base <- output_plot_base()
819!
      plot <- substitute(
820!
        expr = ggplot2::ggplot(data = data, ggplot2::aes(seq_along(.cooksd), .cooksd)) +
821!
          ggplot2::geom_col(alpha = alpha),
822!
        env = list(alpha = input$alpha)
823
      )
824!
      if (input$show_outlier) {
825!
        plot <- substitute(
826!
          expr = plot +
827!
            ggplot2::geom_hline(
828!
              yintercept = c(
829!
                outlier * mean(data$.cooksd, na.rm = TRUE),
830!
                mean(data$.cooksd, na.rm = TRUE)
831
              ),
832!
              color = "red",
833!
              linetype = "dashed"
834
            ) +
835!
            ggplot2::annotate(
836!
              geom = "text",
837!
              x = 0,
838!
              y = mean(data$.cooksd, na.rm = TRUE),
839!
              label = paste("mu", "=", round(mean(data$.cooksd, na.rm = TRUE), 4)),
840!
              vjust = -1,
841!
              hjust = 0,
842!
              color = "red",
843!
              angle = 90
844
            ) +
845!
            outlier_label,
846!
          env = list(plot = plot, outlier = input$outlier, outlier_label = outlier_label())
847
        )
848
      }
849
850!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
851!
        teal.widgets::resolve_ggplot2_args(
852!
          user_plot = ggplot2_args[["Cook's distance"]],
853!
          user_default = ggplot2_args$default,
854!
          module_plot = teal.widgets::ggplot2_args(
855!
            labs = list(
856!
              x = quote(paste0("Obs. number\nlm(", reg_form, ")")),
857!
              y = "Cook's distance",
858!
              title = "Cook's distance"
859
            )
860
          )
861
        ),
862!
        ggtheme = input$ggtheme
863
      )
864
865!
      teal.code::eval_code(
866!
        plot_base,
867!
        substitute(
868!
          expr = {
869!
            plot <- graph
870
          },
871!
          env = list(
872!
            graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args))
873
          )
874
        )
875
      )
876
    })
877
878!
    output_plot_5 <- reactive({
879!
      shinyjs::show("size")
880!
      shinyjs::show("alpha")
881!
      plot_base <- output_plot_base()
882!
      plot <- substitute(
883!
        expr = ggplot2::ggplot(data = data, ggplot2::aes(.hat, .stdresid)) +
884!
          ggplot2::geom_vline(
885!
            size = 1,
886!
            colour = "black",
887!
            linetype = "dashed",
888!
            xintercept = 0
889
          ) +
890!
          ggplot2::geom_hline(
891!
            size = 1,
892!
            colour = "black",
893!
            linetype = "dashed",
894!
            yintercept = 0
895
          ) +
896!
          ggplot2::geom_point(size = size, alpha = alpha) +
897!
          ggplot2::geom_line(data = smoothy, mapping = smoothy_aes),
898!
        env = list(size = input$size, alpha = input$alpha)
899
      )
900!
      if (input$show_outlier) {
901!
        plot <- substitute(expr = plot + outlier_label, env = list(plot = plot, outlier_label = outlier_label()))
902
      }
903
904!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
905!
        teal.widgets::resolve_ggplot2_args(
906!
          user_plot = ggplot2_args[["Residuals vs Leverage"]],
907!
          user_default = ggplot2_args$default,
908!
          module_plot = teal.widgets::ggplot2_args(
909!
            labs = list(
910!
              x = quote(paste0("Standardized residuals\nlm(", reg_form, ")")),
911!
              y = "Leverage",
912!
              title = "Residuals vs Leverage"
913
            )
914
          )
915
        ),
916!
        ggtheme = input$ggtheme
917
      )
918
919!
      teal.code::eval_code(
920!
        plot_base,
921!
        substitute(
922!
          expr = {
923!
            smoothy <- smooth(data$.hat, data$.stdresid)
924!
            plot <- graph
925
          },
926!
          env = list(
927!
            graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args))
928
          )
929
        )
930
      )
931
    })
932
933!
    output_plot_6 <- reactive({
934!
      shinyjs::show("size")
935!
      shinyjs::show("alpha")
936!
      plot_base <- output_plot_base()
937!
      plot <- substitute(
938!
        expr = ggplot2::ggplot(data = data, ggplot2::aes(.hat, .cooksd)) +
939!
          ggplot2::geom_vline(xintercept = 0, colour = NA) +
940!
          ggplot2::geom_abline(
941!
            slope = seq(0, 3, by = 0.5),
942!
            colour = "black",
943!
            linetype = "dashed",
944!
            size = 1
945
          ) +
946!
          ggplot2::geom_line(data = smoothy, mapping = smoothy_aes) +
947!
          ggplot2::geom_point(size = size, alpha = alpha),
948!
        env = list(size = input$size, alpha = input$alpha)
949
      )
950!
      if (input$show_outlier) {
951!
        plot <- substitute(expr = plot + outlier_label, env = list(plot = plot, outlier_label = outlier_label()))
952
      }
953
954!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
955!
        teal.widgets::resolve_ggplot2_args(
956!
          user_plot = ggplot2_args[["Cook's dist vs Leverage"]],
957!
          user_default = ggplot2_args$default,
958!
          module_plot = teal.widgets::ggplot2_args(
959!
            labs = list(
960!
              x = quote(paste0("Leverage\nlm(", reg_form, ")")),
961!
              y = "Cooks's distance",
962!
              title = "Cook's dist vs Leverage"
963
            )
964
          )
965
        ),
966!
        ggtheme = input$ggtheme
967
      )
968
969!
      teal.code::eval_code(
970!
        plot_base,
971!
        substitute(
972!
          expr = {
973!
            smoothy <- smooth(data$.hat, data$.cooksd)
974!
            plot <- graph
975
          },
976!
          env = list(
977!
            graph = Reduce(function(x, y) call("+", x, y), c(plot, parsed_ggplot2_args))
978
          )
979
        )
980
      )
981
    })
982
983!
    output_q <- reactive({
984!
      teal::validate_inputs(iv_r())
985!
      switch(input$plot_type,
986!
        "Response vs Regressor" = output_plot_0(),
987!
        "Residuals vs Fitted" = output_plot_1(),
988!
        "Normal Q-Q" = output_plot_2(),
989!
        "Scale-Location" = output_plot_3(),
990!
        "Cook's distance" = output_plot_4(),
991!
        "Residuals vs Leverage" = output_plot_5(),
992!
        "Cook's dist vs Leverage" = output_plot_6()
993
      )
994
    })
995
996!
    decorated_output_q <- srv_decorate_teal_data(
997!
      "decorator",
998!
      data = output_q,
999!
      decorators = select_decorators(decorators, "plot"),
1000!
      expr = quote(plot)
1001
    )
1002
1003!
    fitted <- reactive({
1004!
      req(decorated_output_q())
1005!
      decorated_output_q()[["fit"]]
1006
    })
1007!
    plot_r <- reactive({
1008!
      req(decorated_output_q())
1009!
      decorated_output_q()[["plot"]]
1010
    })
1011
1012
    # Insert the plot into a plot_with_settings module from teal.widgets
1013!
    pws <- teal.widgets::plot_with_settings_srv(
1014!
      id = "myplot",
1015!
      plot_r = plot_r,
1016!
      height = plot_height,
1017!
      width = plot_width
1018
    )
1019
1020!
    output$text <- renderText({
1021!
      req(iv_r()$is_valid())
1022!
      req(iv_out$is_valid())
1023!
      paste(utils::capture.output(summary(fitted()))[-1], collapse = "\n")
1024
    })
1025
1026!
    set_chunk_dims(pws, decorated_output_q)
1027
  })
1028
}
1
# minimal implementation of ggplot2 mosaic after ggmosaic was archived in CRAN
2
#
3
# This was heavily inspired by github.com/haleyjeppson/ggmosaic package but
4
# simplified to only support 2 categorical variables
5
6
#' Mosaic Rectangles Layer for ggplot2
7
#'
8
#' Adds a mosaic-style rectangles layer to a ggplot, visualizing the
9
#' joint distribution of categorical variables.
10
#' Each rectangle's size reflects the proportion of observations for
11
#' combinations of `x` and `fill`.
12
#'
13
#' @param mapping Set of aesthetic mappings created by `aes()`. Must specify `x` and `fill`.
14
#' @param data The data to be displayed in this layer.
15
#' @param stat The statistical transformation to use on the data. Defaults to `"rects"`.
16
#' @param position Position adjustment. Defaults to `"identity"`.
17
#' @param ... Other arguments passed to `layer()`.
18
#' @param na.rm Logical. Should missing values be removed?
19
#' @param show.legend Logical. Should this layer be included in the legends?
20
#' @param inherit.aes Logical. If `FALSE`, overrides default aesthetics.
21
#'
22
#' @return A ggplot2 layer that adds mosaic rectangles to the plot.
23
#'
24
#' @examples
25
#' df <- data.frame(RACE = c("Black", "White", "Black", "Asian"), SEX = c("M", "M", "F", "F"))
26
#' library(ggplot2)
27
#' ggplot(df) +
28
#'   geom_mosaic(aes(x = RACE, fill = SEX))
29
#' @export
30
geom_mosaic <- function(mapping = NULL, data = NULL,
31
                        stat = "mosaic", position = "identity",
32
                        ...,
33
                        na.rm = FALSE, # nolint: object_name_linter.
34
                        show.legend = TRUE, # nolint: object_name_linter.
35
                        inherit.aes = TRUE) { # nolint: object_name_linter.
36
37!
  aes_x <- mapping$x
38!
  if (!is.null(aes_x)) {
39!
    aes_x <- list(rlang::quo_get_expr(mapping$x))
40!
    var_x <- paste0("x__", as.character(aes_x))
41!
    mapping[[var_x]] <- mapping$x
42
  }
43
44!
  aes_fill <- mapping$fill
45!
  if (!is.null(aes_fill)) {
46!
    aes_fill <- rlang::quo_text(mapping$fill)
47
  }
48
49!
  mapping$x <- structure(1L)
50
51!
  layer <- ggplot2::layer(
52!
    geom = GeomMosaic,
53!
    stat = "mosaic",
54!
    data = data,
55!
    mapping = mapping,
56!
    position = position,
57!
    show.legend = show.legend,
58!
    inherit.aes = inherit.aes,
59!
    check.aes = FALSE,
60!
    params = list(na.rm = na.rm, ...)
61
  )
62!
  list(layer, .scale_x_mosaic())
63
}
64
65
#' @keywords internal
66
GeomMosaic <- ggplot2::ggproto( # nolint: object_name_linter.
67
  "GeomMosaic", ggplot2::GeomRect,
68
  default_aes = ggplot2::aes(
69
    colour = NA, linewidth = 0.5, linetype = 1, alpha = 1, fill = "grey30"
70
  ),
71
  draw_panel = function(data, panel_params, coord) {
72
    if (all(is.na(data$colour))) data$colour <- scales::alpha(data$fill, data$alpha)
73
    ggplot2::GeomRect$draw_panel(data, panel_params, coord)
74
  },
75
  required_aes = c("xmin", "xmax", "ymin", "ymax")
76
)
77
78
#' @keywords internal
79
StatMosaic <- ggplot2::ggproto( # nolint: object_name_linter.
80
  "StatMosaic", ggplot2::Stat,
81
  required_aes = c("x", "fill"),
82
  compute_group = function(data, scales) data,
83
  compute_panel = function(data, scales) {
84
    data$x <- data[, grepl("x__", colnames(data))]
85
    result <- .calculate_coordinates(data)
86
87
    results_non_zero <- result[result$.n != 0, ]
88
    breaks <- unique(with(results_non_zero, (xmin + xmax) / 2))
89
    labels <- unique(results_non_zero$x)
90
    result$x <- list(list2env(list(breaks = breaks[breaks != 0], labels = labels[breaks != 0])))
91
92
    result$group <- 1
93
    result$PANEL <- unique(data$PANEL)
94
    result
95
  }
96
)
97
98
#' Determining scales for mosaics
99
#'
100
#' @param breaks,labels,minor_breaks One of:
101
#' - `NULL` for no breaks / labels.
102
#' - [ggplot2::waiver()] for the default breaks / labels computed by the scale.
103
#' - A numeric / character vector giving the positions of the breaks / labels.
104
#' - A function.
105
#' See [ggplot2::scale_x_continuous()] for more details.
106
#' @param na.value The value to be used for `NA` values.
107
#' @param position For position scales, The position of the axis.
108
#' left or right for y axes, top or bottom for x axes.
109
#' @param ... other arguments passed to `continuous_scale()`.
110
#' @keywords internal
111
.scale_x_mosaic <- function(breaks = unique,
112
                            minor_breaks = NULL,
113
                            labels = unique,
114
                            na.value = NA_real_, # nolint: object_name_linter.
115
                            position = "bottom",
116
                            ...) {
117!
  ggplot2::continuous_scale(
118!
    aesthetics = c(
119!
      "x", "xmin", "xmax", "xend", "xintercept", "xmin_final", "xmax_final",
120!
      "xlower", "xmiddle", "xupper"
121
    ),
122!
    palette = identity,
123!
    breaks = breaks,
124!
    minor_breaks = minor_breaks,
125!
    labels = labels,
126!
    na.value = na.value,
127!
    position = position,
128!
    super = ScaleContinuousMosaic, ,
129!
    guide = ggplot2::waiver(),
130
    ...
131
  )
132
}
133
134
#' @keywords internal
135
ScaleContinuousMosaic <- ggplot2::ggproto( # nolint: object_name_linter.
136
  "ScaleContinuousMosaic", ggplot2::ScaleContinuousPosition,
137
  train = function(self, x) {
138
    if (length(x) == 0) {
139
      return()
140
    }
141
    if (is.list(x)) {
142
      scale_x <- x[[1]]
143
      # re-assign the scale values now that we have the information - but only if necessary
144
      if (is.function(self$breaks)) self$breaks <- scale_x$breaks
145
      if (is.function(self$labels)) self$labels <- as.vector(scale_x$labels)
146
      return(NULL)
147
    }
148
    if (is_discrete(x)) {
149
      self$range$train(x = c(0, 1))
150
      return(NULL)
151
    }
152
    self$range$train(x, call = self$call)
153
  },
154
  map = function(self, x, limits = self$get_limits()) {
155
    if (is_discrete(x)) {
156
      return(x)
157
    }
158
    if (is.list(x)) {
159
      return(0)
160
    } # need a number
161
    scaled <- as.numeric(self$oob(x, limits))
162
    ifelse(!is.na(scaled), scaled, self$na.value)
163
  },
164
  dimension = function(self, expand = c(0, 0)) c(-0.05, 1.05)
165
)
166
167
#' @noRd
168!
is_discrete <- function(x) is.factor(x) || is.character(x) || is.logical(x)
169
170
#' @describeIn geom_mosaic
171
#' Computes the coordinates for rectangles in a mosaic plot based
172
#' on combinations of `x` and `fill` variables.
173
#' For each unique `x` and `fill`, calculates the proportional
174
#' widths and heights, stacking rectangles within each `x` group.
175
#'
176
#' ### Value
177
#'
178
#' A data frame with columns: `x`, `fill`, `xmin`, `xmax`, `ymin`, `ymax`,
179
#' representing the position and size of each rectangle.
180
#'
181
#' @keywords internal
182
.calculate_coordinates <- function(data) {
183
  # Example: compute rectangles from x and y
184!
  result <- data |>
185
    # Count combinations of X and Y
186!
    dplyr::count(.data$x, .data$fill, .drop = FALSE) |>
187
    # Compute total for each X group
188!
    dplyr::mutate(
189!
      .by = .data$x,
190!
      x_total = sum(.data$n),
191!
      prop = .data$n / .data$x_total,
192!
      prop = dplyr::if_else(is.nan(.data$prop), 0, .data$prop)
193
    ) |>
194!
    dplyr::arrange(dplyr::desc(.data$x_total), .data$x, .data$fill) |>
195
    # Compute total sample size to turn counts into widths
196!
    dplyr::mutate(
197!
      N_total = dplyr::n(),
198!
      x_width = .data$x_total / .data$N_total
199
    ) |>
200
    # Convert counts to x widths
201!
    dplyr::mutate(
202!
      .by = .data$x,
203!
      x_width_last = dplyr::if_else(dplyr::row_number() == dplyr::n(), .data$x_width, 0)
204
    ) |>
205
    # Compute x-min/x-max for each group
206!
    dplyr::mutate(
207!
      xmin = cumsum(dplyr::lag(.data$x_width_last, default = 0)),
208!
      xmax = .data$xmin + .data$x_width
209
    ) |>
210
    # Compute y-min/y-max for stacked proportions
211!
    dplyr::mutate(
212!
      .by = .data$x,
213!
      ymin = c(0, utils::head(cumsum(.data$prop), -1)),
214!
      ymax = cumsum(.data$prop)
215
    ) |>
216!
    dplyr::mutate(
217!
      xmin = .data$xmin / max(.data$xmax),
218!
      xmax = .data$xmax / max(.data$xmax),
219!
      xmin = dplyr::if_else(.data$n == 0, 0, .data$xmin + 0.005),
220!
      xmax = dplyr::if_else(.data$n == 0, 0, .data$xmax - 0.005),
221!
      ymin = dplyr::if_else(.data$n == 0, 0, .data$ymin + 0.005),
222!
      ymax = dplyr::if_else(.data$n == 0, 0, .data$ymax - 0.005)
223
    ) |>
224!
    dplyr::select(.data$x, .data$fill, .data$xmin, .data$xmax, .data$ymin, .data$ymax, .n = .data$n)
225!
  result
226
}
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_stop(
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!
    tags$div(
123!
      id = "front_page_content",
124!
      style = "margin-left: 2rem;",
125!
      tags$div(
126!
        id = "front_page_headers",
127!
        get_header_tags(args$header_text)
128
      ),
129!
      tags$div(
130!
        id = "front_page_tables",
131!
        style = "margin-left: 2rem;",
132!
        get_table_tags(args$tables, ns)
133
      ),
134!
      tags$div(
135!
        id = "front_page_custom_html",
136!
        style = "margin-left: 2rem;",
137!
        args$additional_tags
138
      ),
139!
      if (length(args$datanames) > 0L) {
140!
        tags$div(
141!
          id = "front_page_metabutton",
142!
          style = "margin: 1rem;",
143!
          actionButton(ns("metadata_button"), "Show metadata")
144
        )
145
      },
146!
      tags$footer(
147!
        class = "small",
148!
        get_footer_tags(args$footnotes)
149
      )
150
    )
151
  )
152
}
153
154
# Server function for the front page module
155
srv_front_page <- function(id, data, tables) {
156!
  checkmate::assert_class(data, "reactive")
157!
  checkmate::assert_class(isolate(data()), "teal_data")
158!
  moduleServer(id, function(input, output, session) {
159!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
160
161!
    ns <- session$ns
162
163!
    setBookmarkExclude("metadata_button")
164
165!
    lapply(seq_along(tables), function(idx) {
166!
      output[[paste0("table_", idx)]] <- renderTable(
167!
        tables[[idx]],
168!
        bordered = TRUE,
169!
        caption = names(tables)[idx],
170!
        caption.placement = "top"
171
      )
172
    })
173!
    if (length(isolate(names(data()))) > 0L) {
174!
      observeEvent(
175!
        input$metadata_button, showModal(
176!
          modalDialog(
177!
            title = "Metadata",
178!
            dataTableOutput(ns("metadata_table")),
179!
            size = "l",
180!
            easyClose = TRUE
181
          )
182
        )
183
      )
184
185!
      metadata_data_frame <- reactive({
186!
        datanames <- names(data())
187!
        convert_metadata_to_dataframe(
188!
          lapply(datanames, function(dataname) attr(data()[[dataname]], "metadata")),
189!
          datanames
190
        )
191
      })
192
193!
      output$metadata_table <- DT::renderDT({
194!
        validate(need(nrow(metadata_data_frame()) > 0, "The data has no associated metadata"))
195!
        metadata_data_frame()
196
      })
197
    }
198
  })
199
}
200
201
## utils functions
202
203
get_header_tags <- function(header_text) {
204!
  if (length(header_text) == 0) {
205!
    return(list())
206
  }
207
208!
  get_single_header_tags <- function(header_text, p_text, header_tag = tags$h4) {
209!
    tagList(
210!
      tags$div(
211!
        if (!is.null(header_text) && nchar(header_text) > 0) header_tag(header_text),
212!
        tags$p(p_text)
213
      )
214
    )
215
  }
216
217!
  header_tags <- get_single_header_tags(names(header_text[1]), header_text[1], header_tag = tags$h3)
218!
  c(header_tags, mapply(get_single_header_tags, utils::tail(names(header_text), -1), utils::tail(header_text, -1)))
219
}
220
221
get_table_tags <- function(tables, ns) {
222!
  if (length(tables) == 0) {
223!
    return(list())
224
  }
225!
  table_tags <- c(lapply(seq_along(tables), function(idx) {
226!
    list(
227!
      tableOutput(ns(paste0("table_", idx)))
228
    )
229
  }))
230!
  table_tags
231
}
232
233
get_footer_tags <- function(footnotes) {
234!
  if (length(footnotes) == 0) {
235!
    return(list())
236
  }
237!
  bold_texts <- if (is.null(names(footnotes))) rep("", length(footnotes)) else names(footnotes)
238!
  footnote_tags <- mapply(function(bold_text, value) {
239!
    list(
240!
      tags$div(
241!
        tags$b(bold_text),
242!
        value,
243!
        tags$br()
244
      )
245
    )
246!
  }, bold_text = bold_texts, value = footnotes)
247
}
248
249
# take a list of metadata, one item per dataset (raw_metadata each element from datasets$get_metadata())
250
# and the corresponding datanames and output a data.frame with columns {Dataset, Name, Value}.
251
# which are, the Dataset the metadata came from, the metadata's name and value
252
convert_metadata_to_dataframe <- function(raw_metadata, datanames) {
2534x
  output <- mapply(function(metadata, dataname) {
2546x
    if (is.null(metadata)) {
2552x
      return(data.frame(Dataset = character(0), Name = character(0), Value = character(0)))
256
    }
2574x
    data.frame(
2584x
      Dataset = dataname,
2594x
      Name = names(metadata),
2604x
      Value = unname(unlist(lapply(metadata, as.character)))
261
    )
2624x
  }, raw_metadata, datanames, SIMPLIFY = FALSE)
2634x
  do.call(rbind, output)
264
}
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_stop(
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!
  tags$div(
145!
    shinyjs::useShinyjs(),
146!
    teal.widgets::standard_layout(
147!
      output = tags$div(
148!
        htmlwidgets::getDependency("sparkline"), # needed for sparklines to work
149!
        bslib::layout_column_wrap(
150!
          width = 0.5,
151!
          teal.widgets::white_small_well(
152!
            uiOutput(ns("ui_variable_browser")),
153!
            shinyjs::hidden({
154!
              checkboxInput(ns("show_parent_vars"), "Show parent dataset variables", value = FALSE)
155
            })
156
          ),
157!
          teal.widgets::white_small_well(
158!
            uiOutput(ns("ui_histogram_display")),
159!
            uiOutput(ns("ui_numeric_display")),
160!
            teal.widgets::plot_with_settings_ui(ns("variable_plot")),
161!
            tags$br(),
162!
            bslib::accordion(
163!
              open = FALSE,
164!
              bslib::accordion_panel(
165!
                title = "Plot settings",
166!
                selectInput(
167!
                  inputId = ns("ggplot_theme"), label = "ggplot2 theme",
168!
                  choices = ggplot_themes,
169!
                  selected = "grey"
170
                ),
171!
                bslib::layout_columns(
172!
                  col_widths = c(6, 6),
173!
                  sliderInput(
174!
                    inputId = ns("font_size"), label = "font size",
175!
                    min = 5L, max = 30L, value = 15L, step = 1L, ticks = FALSE
176
                  ),
177!
                  sliderInput(
178!
                    inputId = ns("label_rotation"), label = "rotate x labels",
179!
                    min = 0L, max = 90L, value = 45L, step = 1, ticks = FALSE
180
                  )
181
                )
182
              )
183
            ),
184!
            tags$br(),
185!
            teal.widgets::get_dt_rows(ns("variable_summary_table"), ns("variable_summary_table_rows")),
186!
            DT::dataTableOutput(ns("variable_summary_table"))
187
          )
188
        )
189
      ),
190!
      pre_output = pre_output,
191!
      post_output = post_output
192
    )
193
  )
194
}
195
196
# Server function for the variable browser module
197
srv_variable_browser <- function(id,
198
                                 data,
199
                                 datanames, parent_dataname, ggplot2_args) {
200!
  checkmate::assert_class(data, "reactive")
201!
  checkmate::assert_class(isolate(data()), "teal_data")
202!
  moduleServer(id, function(input, output, session) {
203!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
204
205
    # if there are < this number of unique records then a numeric
206
    # variable can be treated as a factor and all factors with < this groups
207
    # have their values plotted
208!
    .unique_records_for_factor <- 30
209
    # if there are < this number of unique records then a numeric
210
    # variable is by default treated as a factor
211!
    .unique_records_default_as_factor <- 6 # nolint: object_length.
212
213!
    varname_numeric_as_factor <- reactiveValues()
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!
    output$ui_variable_browser <- renderUI({
220!
      ns <- session$ns
221!
      do.call(
222!
        tabsetPanel,
223!
        c(
224!
          id = ns("tabset_panel"),
225!
          do.call(
226!
            tagList,
227!
            lapply(datanames, function(dataname) {
228!
              tabPanel(
229!
                dataname,
230!
                tags$div(
231!
                  style = "margin-top: 1rem;",
232!
                  textOutput(ns(paste0("dataset_summary_", dataname)))
233
                ),
234!
                tags$div(
235!
                  style = "margin-top: 1rem;",
236!
                  teal.widgets::get_dt_rows(
237!
                    ns(paste0("variable_browser_", dataname)),
238!
                    ns(paste0("variable_browser_", dataname, "_rows"))
239
                  ),
240!
                  DT::dataTableOutput(ns(paste0("variable_browser_", dataname)), width = "100%")
241
                )
242
              )
243
            })
244
          )
245
        )
246
      )
247
    })
248
249
    # conditionally display checkbox
250!
    shinyjs::toggle(
251!
      id = "show_parent_vars",
252!
      condition = length(parent_dataname) > 0 && parent_dataname %in% datanames
253
    )
254
255!
    columns_names <- new.env()
256
257
    # plot_var$data holds the name of the currently selected dataset
258
    # plot_var$variable[[<dataset_name>]] holds the name of the currently selected
259
    # variable for dataset <dataset_name>
260!
    plot_var <- reactiveValues(data = NULL, variable = list())
261
262!
    establish_updating_selection(datanames, input, plot_var, columns_names)
263
264
    # validations
265!
    validation_checks <- validate_input(req(input), req(plot_var), data)
266
267
    # data_for_analysis is a list with two elements: a column from a dataset and the column label
268!
    plotted_data <- reactive({
269!
      req(input, plot_var, data())
270!
      validation_checks()
271!
      get_plotted_data(input, plot_var, data)
272
    })
273
274!
    treat_numeric_as_factor <- reactive({
275!
      if (is_num_var_short(.unique_records_for_factor, input, plotted_data)) {
276!
        input$numeric_as_factor
277
      } else {
278!
        FALSE
279
      }
280
    })
281
282!
    render_tabset_panel_content(
283!
      input = input,
284!
      output = output,
285!
      data = data,
286!
      datanames = datanames,
287!
      parent_dataname = parent_dataname,
288!
      columns_names = columns_names,
289!
      plot_var = plot_var
290
    )
291
    # add used-defined text size to ggplot arguments passed from caller frame
292!
    all_ggplot2_args <- reactive({
293!
      user_text <- teal.widgets::ggplot2_args(
294!
        theme = list(
295!
          "text" = ggplot2::element_text(size = input[["font_size"]]),
296!
          "axis.text.x" = ggplot2::element_text(angle = input[["label_rotation"]], hjust = 1)
297
        )
298
      )
299!
      user_theme <- utils::getFromNamespace(sprintf("theme_%s", input[["ggplot_theme"]]), ns = "ggplot2")
300!
      user_theme <- user_theme()
301
      # temporary fix to circumvent assertion issue with resolve_ggplot2_args
302
      # drop problematic elements
303!
      user_theme <- user_theme[grep("strip.text.y.left", names(user_theme), fixed = TRUE, invert = TRUE)]
304
305!
      teal.widgets::resolve_ggplot2_args(
306!
        user_plot = user_text,
307!
        user_default = teal.widgets::ggplot2_args(theme = user_theme),
308!
        module_plot = ggplot2_args
309
      )
310
    })
311
312!
    output$ui_numeric_display <- renderUI({
313!
      dataname <- req(input$tabset_panel)
314!
      varname <- req(plot_var$variable)[[dataname]]
315!
      df <- req(data())[[dataname]]
316!
      validation_checks()
317
318!
      numeric_ui <- bslib::page_fluid(
319!
        bslib::layout_columns(
320!
          col_widths = c(8, 4),
321!
          bslib::layout_columns(
322!
            col_widths = c(6, 6, 12),
323!
            style = bslib::css(grid_row_gap = 0),
324!
            bslib::input_switch(
325!
              id = session$ns("display_density"),
326!
              label = tags$div(
327!
                "Show density:",
328!
                bslib::tooltip(
329!
                  trigger = icon("circle-info"),
330!
                  tags$span(
331!
                    "Show kernel density estimation with gaussian kernel and bandwidth function bw.nrd0 (R default)"
332
                  )
333
                )
334
              ),
335!
              value = `if`(is.null(isolate(input$display_density)), TRUE, isolate(input$display_density)),
336!
              width = "100%"
337
            ),
338!
            bslib::input_switch(
339!
              id = session$ns("remove_outliers"),
340!
              label = "Remove outliers",
341!
              value = `if`(is.null(isolate(input$remove_outliers)), FALSE, isolate(input$remove_outliers)),
342!
              width = "100%"
343
            ),
344!
            uiOutput(session$ns("ui_outlier_help"))
345
          ),
346!
          uiOutput(session$ns("outlier_definition_slider_ui"))
347
        )
348
      )
349
350!
      observeEvent(input$numeric_as_factor, ignoreInit = TRUE, {
351!
        varname_numeric_as_factor[[plot_var$variable[[dataname]]]] <- input$numeric_as_factor
352
      })
353
354!
      if (is.numeric(df[[varname]])) {
355!
        unique_entries <- length(unique(df[[varname]]))
356!
        if (unique_entries < .unique_records_for_factor && unique_entries > 0) {
357!
          list(
358!
            checkboxInput(
359!
              session$ns("numeric_as_factor"),
360!
              "Treat variable as factor",
361!
              value = `if`(
362!
                is.null(varname_numeric_as_factor[[varname]]),
363!
                unique_entries < .unique_records_default_as_factor,
364!
                varname_numeric_as_factor[[varname]]
365
              )
366
            ),
367!
            conditionalPanel("!input.numeric_as_factor", ns = session$ns, numeric_ui)
368
          )
369!
        } else if (unique_entries > 0) {
370!
          numeric_ui
371
        }
372
      } else {
373!
        NULL
374
      }
375
    })
376
377!
    output$ui_histogram_display <- renderUI({
378!
      validation_checks()
379!
      dataname <- req(input$tabset_panel)
380!
      varname <- req(plot_var$variable)[[dataname]]
381!
      df <- req(data())[[dataname]]
382
383!
      numeric_ui <- bslib::input_switch(
384!
        id = session$ns("remove_NA_hist"),
385!
        label = "Remove NA values",
386!
        value = FALSE,
387!
        width = "100%"
388
      )
389
390!
      var <- df[[varname]]
391!
      if (anyNA(var) && (is.factor(var) || is.character(var) || is.logical(var))) {
392!
        groups <- unique(as.character(var))
393!
        len_groups <- length(groups)
394!
        if (len_groups >= .unique_records_for_factor) {
395!
          NULL
396
        } else {
397!
          numeric_ui
398
        }
399
      } else {
400!
        NULL
401
      }
402
    })
403
404!
    output$outlier_definition_slider_ui <- renderUI({
405!
      req(input$remove_outliers)
406!
      sliderInput(
407!
        inputId = session$ns("outlier_definition_slider"),
408!
        tags$div(
409!
          tagList(
410!
            "Outlier definition:",
411!
            bslib::tooltip(
412!
              icon("circle-info"),
413!
              tags$span(
414!
                paste(
415!
                  "Use the slider to choose the cut-off value to define outliers; the larger the value the",
416!
                  "further below Q1/above Q3 points have to be in order to be classed as outliers"
417
                )
418
              )
419
            )
420
          )
421
        ),
422!
        min = 1,
423!
        max = 5,
424!
        value = 3,
425!
        step = 0.5
426
      )
427
    })
428
429!
    output$ui_outlier_help <- renderUI({
430!
      req(is.logical(input$remove_outliers), input$outlier_definition_slider)
431!
      if (input$remove_outliers) {
432!
        tags$small(
433!
          helpText(
434!
            withMathJax(paste0(
435!
              "Outlier data points (\\( X \\lt Q1 - ", input$outlier_definition_slider, "\\times IQR \\) or
436!
            \\(Q3 + ", input$outlier_definition_slider, "\\times IQR \\lt X\\))
437!
            have not been displayed on the graph and will not be used for any kernel density estimations, ",
438!
              "although their values remain in the statisics table below."
439
            ))
440
          )
441
        )
442
      } else {
443!
        NULL
444
      }
445
    })
446
447!
    output$variable_summary_table <- DT::renderDataTable({
448!
      var_summary_table(
449!
        plotted_data()$ANL[, 1, drop = TRUE],
450!
        treat_numeric_as_factor(),
451!
        input$variable_summary_table_rows,
452!
        if (!is.null(input$remove_outliers) && input$remove_outliers) {
453!
          req(input$outlier_definition_slider)
454!
          as.numeric(input$outlier_definition_slider)
455
        } else {
456!
          0
457
        }
458
      )
459
    })
460
461!
    variable_plot_r <- reactive({
462!
      display_density <- `if`(is.null(input$display_density), FALSE, input$display_density)
463!
      remove_outliers <- `if`(is.null(input$remove_outliers), FALSE, input$remove_outliers)
464
465!
      if (remove_outliers) {
466!
        req(input$outlier_definition_slider)
467!
        outlier_definition <- as.numeric(input$outlier_definition_slider)
468
      } else {
469!
        outlier_definition <- 0
470
      }
471
472!
      plot_var_summary(
473!
        qenv = req(plotted_data()),
474!
        wrap_character = 15,
475!
        numeric_as_factor = treat_numeric_as_factor(),
476!
        remove_NA_hist = input$remove_NA_hist,
477!
        display_density = display_density,
478!
        outlier_definition = outlier_definition,
479!
        records_for_factor = .unique_records_for_factor,
480!
        ggplot2_args = all_ggplot2_args()
481
      )
482
    })
483
484!
    plot_r <- reactive({
485!
      validation_checks()
486!
      req(variable_plot_r())[["plot"]]
487
    })
488
489!
    pws <- teal.widgets::plot_with_settings_srv(
490!
      id = "variable_plot",
491!
      plot_r = plot_r,
492!
      height = c(500, 200, 2000)
493
    )
494
495!
    set_chunk_dims(pws, variable_plot_r)
496
  })
497
}
498
499
#' Summarize NAs.
500
#'
501
#' Summarizes occurrence of missing values in vector.
502
#' @param x vector of any type and length
503
#' @return Character string describing `NA` occurrence.
504
#' @keywords internal
505
var_missings_info <- function(x) {
506!
  sprintf("%s [%s%%]", sum(is.na(x)), round(mean(is.na(x) * 100), 2))
507
}
508
509
#' Summarizes variable
510
#'
511
#' Creates html summary with statistics relevant to data type. For numeric values it returns central
512
#' tendency measures, for factor returns level counts, for Date  date range, for other just
513
#' number of levels.
514
#'
515
#' @param x vector of any type
516
#' @param numeric_as_factor `logical` should the numeric variable be treated as a factor
517
#' @param dt_rows `numeric` current/latest `DT` page length
518
#' @param outlier_definition If 0 no outliers are removed, otherwise
519
#'   outliers (those more than `outlier_definition*IQR below/above Q1/Q3` be removed)
520
#' @return text with simple statistics.
521
#' @keywords internal
522
var_summary_table <- function(x, numeric_as_factor, dt_rows, outlier_definition) {
523!
  if (is.null(dt_rows)) {
524!
    dt_rows <- 10
525
  }
526!
  if (is.numeric(x) && !numeric_as_factor) {
527!
    req(!any(is.infinite(x)))
528
529!
    x <- remove_outliers_from(x, outlier_definition)
530
531!
    qvals <- round(stats::quantile(x, na.rm = TRUE, probs = c(0.25, 0.5, 0.75), type = 2), 2)
532
    # classical central tendency measures
533
534!
    summary <-
535!
      data.frame(
536!
        Statistic = c("min", "Q1", "median", "mean", "Q3", "max", "sd", "n"),
537!
        Value = c(
538!
          round(min(x, na.rm = TRUE), 2),
539!
          qvals[1],
540!
          qvals[2],
541!
          round(mean(x, na.rm = TRUE), 2),
542!
          qvals[3],
543!
          round(max(x, na.rm = TRUE), 2),
544!
          round(stats::sd(x, na.rm = TRUE), 2),
545!
          length(x[!is.na(x)])
546
        )
547
      )
548
549!
    DT::datatable(summary, rownames = FALSE, options = list(dom = "<t>", pageLength = dt_rows))
550!
  } else if (is.factor(x) || is.character(x) || (is.numeric(x) && numeric_as_factor) || is.logical(x)) {
551
    # make sure factor is ordered numeric
552!
    if (is.numeric(x)) {
553!
      x <- factor(x, levels = sort(unique(x)))
554
    }
555
556!
    level_counts <- table(x)
557!
    max_levels_signif <- nchar(level_counts)
558
559!
    if (!all(is.na(x))) {
560!
      levels <- names(level_counts)
561!
      counts <- sprintf(
562!
        "%s [%.2f%%]",
563!
        format(level_counts, width = max_levels_signif), prop.table(level_counts) * 100
564
      )
565
    } else {
566!
      levels <- character(0)
567!
      counts <- numeric(0)
568
    }
569
570!
    summary <- data.frame(
571!
      Level = levels,
572!
      Count = counts,
573!
      stringsAsFactors = FALSE
574
    )
575
576
    # sort the dataset in decreasing order of counts (needed as character variables default to alphabetical)
577!
    summary <- summary[order(summary$Count, decreasing = TRUE), ]
578
579!
    dom_opts <- if (nrow(summary) <= 10) {
580!
      "<t>"
581
    } else {
582!
      "<lf<t>ip>"
583
    }
584!
    DT::datatable(summary, rownames = FALSE, options = list(dom = dom_opts, pageLength = dt_rows))
585!
  } else if (inherits(x, "Date") || inherits(x, "POSIXct") || inherits(x, "POSIXlt")) {
586!
    summary <-
587!
      data.frame(
588!
        Statistic = c("min", "median", "max"),
589!
        Value = c(
590!
          min(x, na.rm = TRUE),
591!
          stats::median(x, na.rm = TRUE),
592!
          max(x, na.rm = TRUE)
593
        )
594
      )
595!
    DT::datatable(summary, rownames = FALSE, options = list(dom = "<t>", pageLength = dt_rows))
596
  } else {
597!
    NULL
598
  }
599
}
600
601
#' Plot variable
602
#'
603
#' Creates summary plot with statistics relevant to data type.
604
#'
605
#' @inheritParams shared_params
606
#' @param qenv teal_data object where code should be evaluated.
607
#' @param wrap_character (`numeric`) number of characters at which to wrap text values of `var`
608
#' @param numeric_as_factor (`logical`) should the numeric variable be treated as a factor
609
#' @param display_density (`logical`) should density estimation be displayed for numeric values
610
#' @param remove_NA_hist (`logical`) should `NA` values be removed for histogram of factor like variables
611
#' @param outlier_definition if 0 no outliers are removed, otherwise
612
#'   outliers (those more than outlier_definition*IQR below/above Q1/Q3 be removed)
613
#' @param records_for_factor (`numeric`) if the number of factor levels is >= than this value then
614
#'   a graph of the factors isn't shown, only a list of values
615
#'
616
#' @return plot
617
#' @keywords internal
618
plot_var_summary <- function(qenv,
619
                             wrap_character = NULL,
620
                             numeric_as_factor,
621
                             display_density = FALSE,
622
                             remove_NA_hist = FALSE, # nolint: object_name.
623
                             outlier_definition,
624
                             records_for_factor,
625
                             ggplot2_args) {
626!
  checkmate::assert_numeric(wrap_character, null.ok = TRUE)
627!
  checkmate::assert_flag(numeric_as_factor)
628!
  checkmate::assert_flag(display_density)
629!
  checkmate::assert_logical(remove_NA_hist, null.ok = TRUE)
630!
  checkmate::assert_number(outlier_definition, lower = 0, finite = TRUE)
631!
  checkmate::assert_integerish(records_for_factor, lower = 0, len = 1, any.missing = FALSE)
632!
  checkmate::assert_class(ggplot2_args, "ggplot2_args")
633
634!
  var_name <- names(qenv$ANL)
635
636!
  teal.reporter::teal_card(qenv) <- c(
637!
    teal.reporter::teal_card(qenv),
638!
    teal.reporter::teal_card("### Histogram plot")
639
  )
640
641!
  var <- qenv$ANL[[var_name]]
642!
  qenv_plot <- if (is.factor(var) || is.character(var) || is.logical(var)) {
643!
    groups <- unique(as.character(var))
644!
    len_groups <- length(groups)
645!
    if (len_groups >= records_for_factor) {
646!
      qenv_plot <- within(qenv,
647
        {
648!
          groups <- unique(as.character(ANL[[var]]))
649!
          len_groups <- length(groups)
650!
          text <- sprintf(
651!
            "%s unique values\n%s:\n %s\n ...\n %s",
652!
            len_groups,
653!
            teal.data::col_labels(ANL),
654!
            paste(utils::head(groups), collapse = ",\n "),
655!
            paste(utils::tail(groups), collapse = ",\n ")
656
          )
657!
          plot <- gridExtra::arrangeGrob(
658!
            grid::textGrob(
659!
              text,
660!
              x = grid::unit(1, "line"),
661!
              y = grid::unit(1, "npc") - grid::unit(1, "line"),
662!
              just = c("left", "top")
663
            ),
664!
            ncol = 1
665
          )
666
        },
667!
        var = var_name
668
      )
669
    } else {
670!
      if (!is.null(wrap_character)) {
671!
        qenv <- within(qenv,
672
          {
673!
            col_label <- attr(ANL[[var]], "label")
674!
            ANL[[var]] <- stringr::str_wrap(ANL[[var]], width = wrap_character)
675!
            attr(ANL[[var]], "label") <- col_label
676
          },
677!
          var = var_name,
678!
          wrap_character = wrap_character
679
        )
680
      }
681
682!
      if (isTRUE(remove_NA_hist)) {
683!
        qenv <- within(qenv,
684
          {
685!
            ANL <- filter(ANL, !is.na(var))
686
          },
687!
          var = as.name(var_name)
688
        )
689
      }
690!
      qenv_plot <- within(qenv,
691
        {
692!
          plot <- ANL %>%
693!
            ggplot2::ggplot(ggplot2::aes(x = forcats::fct_infreq(var_name))) +
694!
            ggplot2::geom_bar(
695!
              stat = "count", ggplot2::aes(fill = ifelse(is.na(var_name), "withcolor", "")), show.legend = FALSE
696
            ) +
697!
            ggplot2::scale_fill_manual(values = c("gray50", "tan"))
698
        },
699!
        var = var_name,
700!
        var_name = as.name(var_name)
701
      )
702
    }
703!
  } else if (is.numeric(var)) {
704
    # Validate input
705!
    validate(need(any(!is.na(var)), "No data left to visualize."))
706!
    var <- var[which(!is.na(var))] # Filter out NA
707!
    validate(need(!any(is.infinite(var)), "Cannot display graph when data includes infinite values"))
708
709!
    if (numeric_as_factor) {
710!
      var <- factor(var)
711!
      qenv_plot <- within(qenv,
712
        {
713!
          col_label <- attr(ANL[[var]], "label")
714!
          ANL[[var]] <- as.factor(ANL[[var]])
715!
          attr(ANL[[var]], "label") <- col_label
716!
          p <- ANL %>%
717!
            ggplot2::ggplot(ggplot2::aes(x = var_name)) +
718!
            ggplot2::geom_histogram(stat = "count")
719
        },
720!
        var = var_name,
721!
        var_name = as.name(var_name)
722
      )
723
    } else {
724
      # remove outliers
725!
      if (outlier_definition != 0) {
726!
        number_records <- length(var)
727!
        var <- remove_outliers_from(var, outlier_definition)
728!
        number_outliers <- number_records - length(var)
729!
        outlier_text <- paste0(
730!
          number_outliers, " outliers (",
731!
          round(number_outliers / number_records * 100, 2),
732!
          "% of non-missing records) not shown"
733
        )
734!
        validate(need(
735!
          length(var) > 1,
736!
          "At least two data points must remain after removing outliers for this graph to be displayed"
737
        ))
738!
        qenv <- within(qenv,
739
          {
740!
            filter_outliers <- filter_outliers
741!
            ANL <- filter(ANL, filter_outliers(var_name, outlier_definition))
742
          },
743!
          filter_outliers = filter_outliers,
744!
          var_name = as.name(var_name),
745!
          outlier_definition = outlier_definition
746
        )
747
      }
748
749
      ## histogram
750!
      binwidth <- get_bin_width(var)
751!
      qenv_plot <- within(qenv,
752
        {
753!
          plot <- ggplot2::ggplot(data = ANL, ggplot2::aes(x = var_name, y = ggplot2::after_stat(count))) +
754!
            ggplot2::geom_histogram(binwidth = binwidth) +
755!
            ggplot2::scale_y_continuous(
756!
              sec.axis = ggplot2::sec_axis(
757!
                trans = ~ . / nrow(ANL),
758!
                labels = scales::percent,
759!
                name = "proportion (in %)"
760
              )
761
            )
762
        },
763!
        var_name = as.name(var_name),
764!
        binwidth = binwidth
765
      )
766
767!
      if (display_density) {
768!
        qenv_plot <- within(qenv_plot,
769
          {
770!
            plot <- plot + ggplot2::geom_density(ggplot2::aes(y = ggplot2::after_stat(count * binwidth)))
771
          },
772!
          binwidth = binwidth
773
        )
774
      }
775!
      if (outlier_definition != 0) {
776!
        qenv_plot <- within(qenv_plot,
777
          {
778!
            plot <- plot + ggplot2::annotate(
779!
              geom = "text",
780!
              label = outlier_text,
781!
              x = Inf, y = Inf,
782!
              hjust = 1.02, vjust = 1.2,
783!
              color = "black",
784
              # explicitly modify geom text size according
785!
              size = size
786
            )
787
          },
788!
          outlier_text = outlier_text,
789!
          size = ggplot2_args[["theme"]][["text"]][["size"]] / 3.5
790
        )
791
      }
792!
      qenv_plot
793
    }
794!
    qenv_plot
795!
  } else if (inherits(var, "Date") || inherits(var, "POSIXct") || inherits(var, "POSIXlt")) {
796!
    var_num <- as.numeric(var)
797!
    binwidth <- get_bin_width(var_num, 1)
798!
    qenv_plot <- within(qenv,
799
      {
800!
        col_label <- attr(ANL[[var]], "label")
801!
        ANL[[var]] <- as.numeric(ANL[[var]])
802!
        attr(ANL[[var]], "label") <- col_label
803!
        plot <- ANL %>%
804!
          ggplot2::ggplot(ggplot2::aes(x = var_name, y = ggplot2::after_stat(count))) +
805!
          ggplot2::geom_histogram(binwidth = binwidth)
806
      },
807!
      binwidth = binwidth,
808!
      var = var_name,
809!
      var_name = as.name(var_name)
810
    )
811
  } else {
812!
    qenv_plot <- within(qenv,
813
      {
814!
        plot <- gridExtra::arrangeGrob(
815!
          grid::textGrob(
816!
            paste(strwrap(
817!
              utils::capture.output(utils::str(ANL[[var]])),
818!
              width = .9 * grid::convertWidth(grid::unit(1, "npc"), "char", TRUE)
819!
            ), collapse = "\n"),
820!
            x = grid::unit(1, "line"), y = grid::unit(1, "npc") - grid::unit(1, "line"), just = c("left", "top")
821
          ),
822!
          ncol = 1
823
        )
824
      },
825!
      var = var_name
826
    )
827
  }
828
829!
  dev_ggplot2_args <- teal.widgets::ggplot2_args(
830!
    labs = list(x = teal.data::col_labels(qenv$ANL))
831
  )
832
833!
  all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
834!
    ggplot2_args,
835!
    module_plot = dev_ggplot2_args
836
  )
837
838!
  if (is.ggplot(qenv_plot$plot)) {
839!
    qenv_plot <- within(qenv_plot,
840
      {
841!
        plot <- plot +
842!
          theme_light() +
843!
          labs
844
      },
845!
      labs = do.call("labs", all_ggplot2_args$labs)
846
    )
847
  }
848!
  qenv_plot <- within(qenv_plot, {
849!
    plot
850
  })
851
}
852
853
is_num_var_short <- function(.unique_records_for_factor, input, data_for_analysis) {
854!
  length(unique(data_for_analysis()$data)) < .unique_records_for_factor && !is.null(input$numeric_as_factor)
855
}
856
857
#' Validates the variable browser inputs
858
#'
859
#' @param input (`session$input`) the `shiny` session input
860
#' @param plot_var (`list`) list of a data frame and an array of variable names
861
#' @param data (`teal_data`) the datasets passed to the module
862
#'
863
#' @returns `logical` TRUE if validations pass; a `shiny` validation error otherwise
864
#' @keywords internal
865
validate_input <- function(input, plot_var, data) {
866!
  reactive({
867!
    dataset_name <- req(input$tabset_panel)
868!
    varname <- plot_var$variable[[dataset_name]]
869
870!
    validate(need(dataset_name, "No data selected"))
871!
    validate(need(varname, "No variable selected"))
872!
    df <- data()[[dataset_name]]
873!
    teal::validate_has_data(df, 1)
874!
    teal::validate_has_variable(varname = varname, data = df, "Variable not available")
875
876!
    TRUE
877
  })
878
}
879
880
get_plotted_data <- function(input, plot_var, data) {
881!
  dataset_name <- req(input$tabset_panel)
882!
  varname <- plot_var$variable[[dataset_name]]
883!
  obj <- data()
884!
  teal.reporter::teal_card(obj) <-
885!
    c(
886!
      teal.reporter::teal_card(obj),
887!
      teal.reporter::teal_card("## Module's output(s)")
888
    )
889!
  teal.code::eval_code(obj, "library(ggplot2)\nlibrary(dplyr)") |>
890!
    within(
891
      {
892!
        ANL <- dplyr::select(dataset_name, varname)
893
      },
894!
      dataset_name = as.name(dataset_name),
895!
      varname = as.name(varname)
896
    )
897
}
898
899
#' Renders the left-hand side `tabset` panel of the module
900
#'
901
#' @param datanames (`character`) the name of the dataset
902
#' @param parent_dataname (`character`) the name of a parent `dataname` to filter out variables from
903
#' @param data (`teal_data`) the object containing all datasets
904
#' @param input (`session$input`) the `shiny` session input
905
#' @param output (`session$output`) the `shiny` session output
906
#' @param columns_names (`environment`) the environment containing bindings for each dataset
907
#' @param plot_var (`list`) the list containing the currently selected dataset (tab) and its column names
908
#' @keywords internal
909
render_tabset_panel_content <- function(datanames, parent_dataname, output, data, input, columns_names, plot_var) {
910!
  lapply(datanames, render_single_tab,
911!
    input = input,
912!
    output = output,
913!
    data = data,
914!
    parent_dataname = parent_dataname,
915!
    columns_names = columns_names,
916!
    plot_var = plot_var
917
  )
918
}
919
920
#' Renders a single tab in the left-hand side tabset panel
921
#'
922
#' Renders a single tab in the left-hand side tabset panel. The rendered tab contains
923
#' information about one dataset out of many presented in the module.
924
#'
925
#' @param dataset_name (`character`) the name of the dataset contained in the rendered tab
926
#' @param parent_dataname (`character`) the name of a parent `dataname` to filter out variables from
927
#' @inheritParams render_tabset_panel_content
928
#' @keywords internal
929
render_single_tab <- function(dataset_name, parent_dataname, output, data, input, columns_names, plot_var) {
930!
  render_tab_header(dataset_name, output, data)
931
932!
  render_tab_table(
933!
    dataset_name = dataset_name,
934!
    parent_dataname = parent_dataname,
935!
    output = output,
936!
    data = data,
937!
    input = input,
938!
    columns_names = columns_names,
939!
    plot_var = plot_var
940
  )
941
}
942
943
#' Renders the text headlining a single tab in the left-hand side tabset panel
944
#'
945
#' @param dataset_name (`character`) the name of the dataset of the tab
946
#' @inheritParams render_tabset_panel_content
947
#' @keywords internal
948
render_tab_header <- function(dataset_name, output, data) {
949!
  dataset_ui_id <- paste0("dataset_summary_", dataset_name)
950!
  output[[dataset_ui_id]] <- renderText({
951!
    df <- data()[[dataset_name]]
952!
    join_keys <- teal.data::join_keys(data())
953!
    if (!is.null(join_keys)) {
954!
      key <- teal.data::join_keys(data())[dataset_name, dataset_name]
955
    } else {
956!
      key <- NULL
957
    }
958!
    sprintf(
959!
      "Dataset with %s unique key rows and %s variables",
960!
      nrow(unique(`if`(length(key) > 0, df[, key, drop = FALSE], df))),
961!
      ncol(df)
962
    )
963
  })
964
}
965
966
#' Renders the table for a single dataset in the left-hand side tabset panel
967
#'
968
#' The table contains column names, column labels,
969
#' small summary about NA values and `sparkline` (if appropriate).
970
#'
971
#' @param dataset_name (`character`) the name of the dataset
972
#' @param parent_dataname (`character`) the name of a parent `dataname` to filter out variables from
973
#' @inheritParams render_tabset_panel_content
974
#' @keywords internal
975
render_tab_table <- function(dataset_name, parent_dataname, output, data, input, columns_names, plot_var) {
976!
  table_ui_id <- paste0("variable_browser_", dataset_name)
977
978!
  output[[table_ui_id]] <- DT::renderDataTable({
979!
    df <- data()[[dataset_name]]
980
981!
    get_vars_df <- function(input, dataset_name, parent_name, data) {
982!
      data_cols <- colnames(df)
983!
      if (isTRUE(input$show_parent_vars)) {
984!
        data_cols
985!
      } else if (dataset_name != parent_name && parent_name %in% names(data)) {
986!
        setdiff(data_cols, colnames(data()[[parent_name]]))
987
      } else {
988!
        data_cols
989
      }
990
    }
991
992!
    if (length(parent_dataname) > 0) {
993!
      df_vars <- get_vars_df(input, dataset_name, parent_dataname, data)
994!
      df <- df[df_vars]
995
    }
996
997!
    if (is.null(df) || ncol(df) == 0) {
998!
      columns_names[[dataset_name]] <- character(0)
999!
      df_output <- data.frame(
1000!
        Type = character(0),
1001!
        Variable = character(0),
1002!
        Label = character(0),
1003!
        Missings = character(0),
1004!
        Sparklines = character(0),
1005!
        stringsAsFactors = FALSE
1006
      )
1007
    } else {
1008
      # extract data variable labels
1009!
      labels <- teal.data::col_labels(df)
1010
1011!
      columns_names[[dataset_name]] <- names(labels)
1012
1013
      # calculate number of missing values
1014!
      missings <- vapply(
1015!
        df,
1016!
        var_missings_info,
1017!
        FUN.VALUE = character(1),
1018!
        USE.NAMES = FALSE
1019
      )
1020
1021
      # get icons proper for the data types
1022!
      icons <- vapply(df, function(x) class(x)[1L], character(1L))
1023
1024!
      join_keys <- teal.data::join_keys(data())
1025!
      if (!is.null(join_keys)) {
1026!
        icons[intersect(join_keys[dataset_name, dataset_name], colnames(df))] <- "primary_key"
1027
      }
1028!
      icons <- variable_type_icons(icons)
1029
1030
      # generate sparklines
1031!
      sparklines_html <- vapply(
1032!
        df,
1033!
        create_sparklines,
1034!
        FUN.VALUE = character(1),
1035!
        USE.NAMES = FALSE
1036
      )
1037
1038!
      df_output <- data.frame(
1039!
        Type = icons,
1040!
        Variable = names(labels),
1041!
        Label = labels,
1042!
        Missings = missings,
1043!
        Sparklines = sparklines_html,
1044!
        stringsAsFactors = FALSE
1045
      )
1046
    }
1047
1048
    # Select row 1 as default / fallback
1049!
    selected_ix <- 1
1050
    # Define starting page index (base-0 index of the first item on page
1051
    #  note: in many cases it's not the item itself
1052!
    selected_page_ix <- 0
1053
1054
    # Retrieve current selected variable if any
1055!
    isolated_variable <- isolate(plot_var$variable[[dataset_name]])
1056
1057!
    if (!is.null(isolated_variable)) {
1058!
      index <- which(columns_names[[dataset_name]] == isolated_variable)[1]
1059!
      if (!is.null(index) && !is.na(index) && length(index) > 0) selected_ix <- index
1060
    }
1061
1062
    # Retrieve the index of the first item of the current page
1063
    #  it works with varying number of entries on the page (10, 25, ...)
1064!
    table_id_sel <- paste0("variable_browser_", dataset_name, "_state")
1065!
    dt_state <- isolate(input[[table_id_sel]])
1066!
    if (selected_ix != 1 && !is.null(dt_state)) {
1067!
      selected_page_ix <- floor(selected_ix / dt_state$length) * dt_state$length
1068
    }
1069
1070!
    DT::datatable(
1071!
      df_output,
1072!
      escape = FALSE,
1073!
      rownames = FALSE,
1074!
      selection = list(mode = "single", target = "row", selected = selected_ix),
1075!
      options = list(
1076!
        fnDrawCallback = htmlwidgets::JS("function() { HTMLWidgets.staticRender(); }"),
1077!
        pageLength = input[[paste0(table_ui_id, "_rows")]],
1078!
        displayStart = selected_page_ix
1079
      )
1080
    )
1081
  })
1082
}
1083
1084
#' Creates observers updating the currently selected column
1085
#'
1086
#' The created observers update the column currently selected in the left-hand side
1087
#' tabset panel.
1088
#'
1089
#' @note
1090
#' Creates an observer for each dataset (each tab in the tabset panel).
1091
#'
1092
#' @inheritParams render_tabset_panel_content
1093
#' @keywords internal
1094
establish_updating_selection <- function(datanames, input, plot_var, columns_names) {
1095!
  lapply(datanames, function(dataset_name) {
1096!
    table_ui_id <- paste0("variable_browser_", dataset_name)
1097!
    table_id_sel <- paste0(table_ui_id, "_rows_selected")
1098!
    observeEvent(input[[table_id_sel]], {
1099!
      plot_var$data <- dataset_name
1100!
      plot_var$variable[[dataset_name]] <- columns_names[[dataset_name]][input[[table_id_sel]]]
1101
    })
1102
  })
1103
}
1104
1105
get_bin_width <- function(x_vec, scaling_factor = 2) {
1106!
  x_vec <- x_vec[!is.na(x_vec)]
1107!
  qntls <- stats::quantile(x_vec, probs = c(0.1, 0.25, 0.75, 0.9), type = 2)
1108!
  iqr <- qntls[3] - qntls[2]
1109!
  binwidth <- max(scaling_factor * iqr / length(x_vec) ^ (1 / 3), sqrt(qntls[4] - qntls[1])) # styler: off
1110!
  binwidth <- ifelse(binwidth == 0, 1, binwidth)
1111
  # to ensure at least two bins when variable span is very small
1112!
  x_span <- diff(range(x_vec))
1113!
  if (isTRUE(x_span / binwidth >= 2)) binwidth else x_span / 2
1114
}
1115
1116
#' Removes the outlier observation from an array
1117
#'
1118
#' @param var (`numeric`) a numeric vector
1119
#' @param outlier_definition (`numeric`) if `0` then no outliers are removed, otherwise
1120
#'   outliers (those more than `outlier_definition*IQR below/above Q1/Q3`) are removed
1121
#' @returns (`numeric`) vector without the outlier values
1122
#' @keywords internal
1123
remove_outliers_from <- function(var, outlier_definition) {
11243x
  var[filter_outliers(var, outlier_definition)]
1125
}
1126
1127
1128
#' Logical vector
1129
#'
1130
#' Returns a logical vector.
1131
#' Suitable for `dplyr::filter()` and data.frames.
1132
#'
1133
#' @inheritParams remove_outliers_from
1134
#'
1135
#' @keywords internal
1136
filter_outliers <- function(var, outlier_definition) {
11373x
  if (outlier_definition == 0) {
11381x
    return(rep(TRUE, length.out = length(var)))
1139
  }
11402x
  q1_q3 <- stats::quantile(var, probs = c(0.25, 0.75), type = 2, na.rm = TRUE)
11412x
  iqr <- q1_q3[2] - q1_q3[1]
11422x
  var >= q1_q3[1] - outlier_definition * iqr & var <= q1_q3[2] + outlier_definition * iqr
1143
}
1144
1145
1146
# sparklines ----
1147
1148
#' S3 generic for `sparkline` widget HTML
1149
#'
1150
#' Generates the `sparkline` HTML code corresponding to the input array.
1151
#' For numeric variables creates a box plot, for character and factors - bar plot.
1152
#' Produces an empty string for variables of other types.
1153
#'
1154
#' @param arr vector of any type and length
1155
#' @param width `numeric` the width of the `sparkline` widget (pixels)
1156
#' @param bar_spacing `numeric` the spacing between the bars (in pixels)
1157
#' @param bar_width `numeric` the width of the bars (in pixels)
1158
#' @param ... `list` additional options passed to bar plots of `jquery.sparkline`;
1159
#'                   see [`jquery.sparkline docs`](https://omnipotent.net/jquery.sparkline/#common)
1160
#'
1161
#' @return Character string containing HTML code of the `sparkline` HTML widget.
1162
#' @keywords internal
1163
create_sparklines <- function(arr, width = 150, ...) {
1164!
  if (all(is.null(arr))) {
1165!
    return("")
1166
  }
1167!
  UseMethod("create_sparklines")
1168
}
1169
1170
#' @rdname create_sparklines
1171
#' @keywords internal
1172
#' @export
1173
create_sparklines.logical <- function(arr, ...) {
1174!
  create_sparklines(as.factor(arr))
1175
}
1176
1177
#' @rdname create_sparklines
1178
#' @keywords internal
1179
#' @export
1180
create_sparklines.numeric <- function(arr, width = 150, ...) {
1181!
  if (any(is.infinite(arr))) {
1182!
    return(as.character(tags$code("infinite values", class = "text-blue")))
1183
  }
1184!
  if (length(arr) > 100000) {
1185!
    return(as.character(tags$code("Too many rows (>100000)", class = "text-blue")))
1186
  }
1187
1188!
  arr <- arr[!is.na(arr)]
1189!
  sparkline::spk_chr(unname(arr), type = "box", width = width, ...)
1190
}
1191
1192
#' @rdname create_sparklines
1193
#' @keywords internal
1194
#' @export
1195
create_sparklines.character <- function(arr, ...) {
1196!
  create_sparklines(as.factor(arr))
1197
}
1198
1199
1200
#' @rdname create_sparklines
1201
#' @keywords internal
1202
#' @export
1203
create_sparklines.factor <- function(arr, width = 150, bar_spacing = 5, bar_width = 20, ...) {
1204!
  decreasing_order <- TRUE
1205
1206!
  counts <- table(arr)
1207!
  if (length(counts) >= 100) {
1208!
    return(as.character(tags$code("> 99 levels", class = "text-blue")))
1209!
  } else if (length(counts) == 0) {
1210!
    return(as.character(tags$code("no levels", class = "text-blue")))
1211!
  } else if (length(counts) == 1) {
1212!
    return(as.character(tags$code("one level", class = "text-blue")))
1213
  }
1214
1215
  # Summarize the occurences of different levels
1216
  # and get the maximum and minimum number of occurences
1217
  # This is needed for the sparkline to correctly display the bar plots
1218
  # Otherwise they are cropped
1219!
  counts <- sort(counts, decreasing = decreasing_order, method = "radix")
1220!
  max_value <- if (decreasing_order) counts[1] else counts[length[counts]]
1221!
  max_value <- unname(max_value)
1222
1223!
  sparkline::spk_chr(
1224!
    unname(counts),
1225!
    type = "bar",
1226!
    chartRangeMin = 0,
1227!
    chartRangeMax = max_value,
1228!
    width = width,
1229!
    barWidth = bar_width,
1230!
    barSpacing = bar_spacing,
1231!
    tooltipFormatter = custom_sparkline_formatter(names(counts), as.vector(counts))
1232
  )
1233
}
1234
1235
#' @rdname create_sparklines
1236
#' @keywords internal
1237
#' @export
1238
create_sparklines.Date <- function(arr, width = 150, bar_spacing = 5, bar_width = 20, ...) {
1239!
  arr_num <- as.numeric(arr)
1240!
  arr_num <- sort(arr_num, decreasing = FALSE, method = "radix")
1241!
  binwidth <- get_bin_width(arr_num, 1)
1242!
  bins <- floor(diff(range(arr_num)) / binwidth) + 1
1243!
  if (all(is.na(bins))) {
1244!
    return(as.character(tags$code("only NA", class = "text-blue")))
1245!
  } else if (bins == 1) {
1246!
    return(as.character(tags$code("one date", class = "text-blue")))
1247
  }
1248!
  counts <- as.vector(unname(base::table(cut(arr_num, breaks = bins))))
1249!
  max_value <- max(counts)
1250
1251!
  start_bins <- as.integer(seq(1, length(arr_num), length.out = bins))
1252!
  labels_start <- as.character(as.Date(arr_num[start_bins], origin = as.Date("1970-01-01")))
1253!
  labels <- paste("Start:", labels_start)
1254
1255!
  sparkline::spk_chr(
1256!
    unname(counts),
1257!
    type = "bar",
1258!
    chartRangeMin = 0,
1259!
    chartRangeMax = max_value,
1260!
    width = width,
1261!
    barWidth = bar_width,
1262!
    barSpacing = bar_spacing,
1263!
    tooltipFormatter = custom_sparkline_formatter(labels, counts)
1264
  )
1265
}
1266
1267
#' @rdname create_sparklines
1268
#' @keywords internal
1269
#' @export
1270
create_sparklines.POSIXct <- function(arr, width = 150, bar_spacing = 5, bar_width = 20, ...) {
1271!
  arr_num <- as.numeric(arr)
1272!
  arr_num <- sort(arr_num, decreasing = FALSE, method = "radix")
1273!
  binwidth <- get_bin_width(arr_num, 1)
1274!
  bins <- floor(diff(range(arr_num)) / binwidth) + 1
1275!
  if (all(is.na(bins))) {
1276!
    return(as.character(tags$code("only NA", class = "text-blue")))
1277!
  } else if (bins == 1) {
1278!
    return(as.character(tags$code("one date-time", class = "text-blue")))
1279
  }
1280!
  counts <- as.vector(unname(base::table(cut(arr_num, breaks = bins))))
1281!
  max_value <- max(counts)
1282
1283!
  start_bins <- as.integer(seq(1, length(arr_num), length.out = bins))
1284!
  labels_start <- as.character(format(as.POSIXct(arr_num[start_bins], origin = as.Date("1970-01-01")), "%Y-%m-%d"))
1285!
  labels <- paste("Start:", labels_start)
1286
1287!
  sparkline::spk_chr(
1288!
    unname(counts),
1289!
    type = "bar",
1290!
    chartRangeMin = 0,
1291!
    chartRangeMax = max_value,
1292!
    width = width,
1293!
    barWidth = bar_width,
1294!
    barSpacing = bar_spacing,
1295!
    tooltipFormatter = custom_sparkline_formatter(labels, counts)
1296
  )
1297
}
1298
1299
#' @rdname create_sparklines
1300
#' @keywords internal
1301
#' @export
1302
create_sparklines.POSIXlt <- function(arr, width = 150, bar_spacing = 5, bar_width = 20, ...) {
1303!
  arr_num <- as.numeric(arr)
1304!
  arr_num <- sort(arr_num, decreasing = FALSE, method = "radix")
1305!
  binwidth <- get_bin_width(arr_num, 1)
1306!
  bins <- floor(diff(range(arr_num)) / binwidth) + 1
1307!
  if (all(is.na(bins))) {
1308!
    return(as.character(tags$code("only NA", class = "text-blue")))
1309!
  } else if (bins == 1) {
1310!
    return(as.character(tags$code("one date-time", class = "text-blue")))
1311
  }
1312!
  counts <- as.vector(unname(base::table(cut(arr_num, breaks = bins))))
1313!
  max_value <- max(counts)
1314
1315!
  start_bins <- as.integer(seq(1, length(arr_num), length.out = bins))
1316!
  labels_start <- as.character(format(as.POSIXct(arr_num[start_bins], origin = as.Date("1970-01-01")), "%Y-%m-%d"))
1317!
  labels <- paste("Start:", labels_start)
1318
1319!
  sparkline::spk_chr(
1320!
    unname(counts),
1321!
    type = "bar",
1322!
    chartRangeMin = 0,
1323!
    chartRangeMax = max_value,
1324!
    width = width,
1325!
    barWidth = bar_width,
1326!
    barSpacing = bar_spacing,
1327!
    tooltipFormatter = custom_sparkline_formatter(labels, counts)
1328
  )
1329
}
1330
1331
#' @rdname create_sparklines
1332
#' @keywords internal
1333
#' @export
1334
create_sparklines.default <- function(arr, width = 150, ...) {
1335!
  as.character(tags$code("unsupported variable type", class = "text-blue"))
1336
}
1337
1338
custom_sparkline_formatter <- function(labels, counts) {
1339!
  htmlwidgets::JS(
1340!
    sprintf(
1341!
      "function(sparkline, options, field) {
1342!
        return 'ID: ' + %s[field[0].offset] + '<br>' + 'Count: ' + %s[field[0].offset];
1343
        }",
1344!
      jsonlite::toJSON(labels),
1345!
      jsonlite::toJSON(counts)
1346
    )
1347
  )
1348
}
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
#' @inheritSection teal::example_module Reporting
51
#'
52
#' @examplesShinylive
53
#' library(teal.modules.general)
54
#' interactive <- function() TRUE
55
#' {{ next_example }}
56
#' @examples
57
#'
58
#' # general data example
59
#' data <- teal_data()
60
#' data <- within(data, {
61
#'   require(nestcolor)
62
#'   USArrests <- USArrests
63
#' })
64
#'
65
#' app <- init(
66
#'   data = data,
67
#'   modules = modules(
68
#'     tm_a_pca(
69
#'       "PCA",
70
#'       dat = data_extract_spec(
71
#'         dataname = "USArrests",
72
#'         select = select_spec(
73
#'           choices = variable_choices(
74
#'             data = data[["USArrests"]], c("Murder", "Assault", "UrbanPop", "Rape")
75
#'           ),
76
#'           selected = c("Murder", "Assault"),
77
#'           multiple = TRUE
78
#'         ),
79
#'         filter = NULL
80
#'       )
81
#'     )
82
#'   )
83
#' )
84
#' if (interactive()) {
85
#'   shinyApp(app$ui, app$server)
86
#' }
87
#'
88
#' @examplesShinylive
89
#' library(teal.modules.general)
90
#' interactive <- function() TRUE
91
#' {{ next_example }}
92
#' @examples
93
#'
94
#' # CDISC data example
95
#' data <- teal_data()
96
#' data <- within(data, {
97
#'   require(nestcolor)
98
#'   ADSL <- teal.data::rADSL
99
#' })
100
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
101
#'
102
#' app <- init(
103
#'   data = data,
104
#'   modules = modules(
105
#'     tm_a_pca(
106
#'       "PCA",
107
#'       dat = data_extract_spec(
108
#'         dataname = "ADSL",
109
#'         select = select_spec(
110
#'           choices = variable_choices(
111
#'             data = data[["ADSL"]], c("BMRKR1", "AGE", "EOSDY")
112
#'           ),
113
#'           selected = c("BMRKR1", "AGE"),
114
#'           multiple = TRUE
115
#'         ),
116
#'         filter = NULL
117
#'       )
118
#'     )
119
#'   )
120
#' )
121
#' if (interactive()) {
122
#'   shinyApp(app$ui, app$server)
123
#' }
124
#'
125
#' @export
126
#'
127
tm_a_pca <- function(label = "Principal Component Analysis",
128
                     dat,
129
                     plot_height = c(600, 200, 2000),
130
                     plot_width = NULL,
131
                     ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"),
132
                     ggplot2_args = teal.widgets::ggplot2_args(),
133
                     rotate_xaxis_labels = FALSE,
134
                     font_size = c(12, 8, 20),
135
                     alpha = c(1, 0, 1),
136
                     size = c(2, 1, 8),
137
                     pre_output = NULL,
138
                     post_output = NULL,
139
                     transformators = list(),
140
                     decorators = list()) {
141!
  message("Initializing tm_a_pca")
142
143
  # Normalize the parameters
144!
  if (inherits(dat, "data_extract_spec")) dat <- list(dat)
145!
  if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args)
146
147
  # Start of assertions
148!
  checkmate::assert_string(label)
149!
  checkmate::assert_list(dat, types = "data_extract_spec")
150
151!
  checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE)
152!
  checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height")
153!
  checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE)
154!
  checkmate::assert_numeric(
155!
    plot_width[1],
156!
    lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width"
157
  )
158
159!
  ggtheme <- match.arg(ggtheme)
160
161!
  plot_choices <- c("Elbow plot", "Circle plot", "Biplot", "Eigenvector plot")
162!
  checkmate::assert_list(ggplot2_args, types = "ggplot2_args")
163!
  checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices))
164
165!
  checkmate::assert_flag(rotate_xaxis_labels)
166
167!
  if (length(font_size) == 1) {
168!
    checkmate::assert_numeric(font_size, any.missing = FALSE, finite = TRUE, lower = 8, upper = 20)
169
  } else {
170!
    checkmate::assert_numeric(font_size, len = 3, any.missing = FALSE, finite = TRUE, lower = 8, upper = 20)
171!
    checkmate::assert_numeric(font_size[1], lower = font_size[2], upper = font_size[3], .var.name = "font_size")
172
  }
173
174!
  if (length(alpha) == 1) {
175!
    checkmate::assert_numeric(alpha, any.missing = FALSE, finite = TRUE, lower = 0, upper = 1)
176
  } else {
177!
    checkmate::assert_numeric(alpha, len = 3, any.missing = FALSE, finite = TRUE, lower = 0, upper = 1)
178!
    checkmate::assert_numeric(alpha[1], lower = alpha[2], upper = alpha[3], .var.name = "alpha")
179
  }
180
181!
  if (length(size) == 1) {
182!
    checkmate::assert_numeric(size, any.missing = FALSE, finite = TRUE, lower = 1, upper = 8)
183
  } else {
184!
    checkmate::assert_numeric(size, len = 3, any.missing = FALSE, finite = TRUE, lower = 1, upper = 8)
185!
    checkmate::assert_numeric(size[1], lower = size[2], upper = size[3], .var.name = "size")
186
  }
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!
  available_decorators <- c("elbow_plot", "circle_plot", "biplot", "eigenvector_plot")
192!
  assert_decorators(decorators, available_decorators)
193
194
  # Make UI args
195!
  args <- as.list(environment())
196
197!
  data_extract_list <- list(dat = dat)
198
199!
  ans <- module(
200!
    label = label,
201!
    server = srv_a_pca,
202!
    ui = ui_a_pca,
203!
    ui_args = args,
204!
    server_args = c(
205!
      data_extract_list,
206!
      list(
207!
        plot_height = plot_height,
208!
        plot_width = plot_width,
209!
        ggplot2_args = ggplot2_args,
210!
        decorators = decorators
211
      )
212
    ),
213!
    transformators = transformators,
214!
    datanames = teal.transform::get_extract_datanames(data_extract_list)
215
  )
216!
  attr(ans, "teal_bookmarkable") <- FALSE
217!
  ans
218
}
219
220
# UI function for the PCA module
221
ui_a_pca <- function(id, ...) {
222!
  ns <- NS(id)
223!
  args <- list(...)
224!
  is_single_dataset_value <- teal.transform::is_single_dataset(args$dat)
225
226!
  color_selector <- args$dat
227!
  for (i in seq_along(color_selector)) {
228!
    color_selector[[i]]$select$multiple <- FALSE
229!
    color_selector[[i]]$select$always_selected <- NULL
230!
    color_selector[[i]]$select$selected <- NULL
231
  }
232
233!
  tagList(
234!
    teal.widgets::standard_layout(
235!
      output = teal.widgets::white_small_well(
236!
        uiOutput(ns("all_plots"))
237
      ),
238!
      encoding = tags$div(
239!
        tags$label("Encodings", class = "text-primary"),
240!
        teal.transform::datanames_input(args["dat"]),
241!
        teal.transform::data_extract_ui(
242!
          id = ns("dat"),
243!
          label = "Data selection",
244!
          data_extract_spec = args$dat,
245!
          is_single_dataset = is_single_dataset_value
246
        ),
247!
        bslib::accordion(
248!
          open = c("Display", "Pre-processing", "Selected plot specific settings"),
249!
          bslib::accordion_panel(
250!
            title = "Display",
251!
            checkboxGroupInput(
252!
              ns("tables_display"),
253!
              "Tables display",
254!
              choices = c("PC importance" = "importance", "Eigenvectors" = "eigenvector"),
255!
              selected = c("importance", "eigenvector")
256
            ),
257!
            radioButtons(
258!
              ns("plot_type"),
259!
              label = "Plot type",
260!
              choices = args$plot_choices,
261!
              selected = args$plot_choices[1]
262
            ),
263!
            conditionalPanel(
264!
              condition = sprintf("input['%s'] == 'Elbow plot'", ns("plot_type")),
265!
              ui_decorate_teal_data(
266!
                ns("d_elbow_plot"),
267!
                decorators = select_decorators(args$decorators, "elbow_plot")
268
              )
269
            ),
270!
            conditionalPanel(
271!
              condition = sprintf("input['%s'] == 'Circle plot'", ns("plot_type")),
272!
              ui_decorate_teal_data(
273!
                ns("d_circle_plot"),
274!
                decorators = select_decorators(args$decorators, "circle_plot")
275
              )
276
            ),
277!
            conditionalPanel(
278!
              condition = sprintf("input['%s'] == 'Biplot'", ns("plot_type")),
279!
              ui_decorate_teal_data(
280!
                ns("d_biplot"),
281!
                decorators = select_decorators(args$decorators, "biplot")
282
              )
283
            ),
284!
            conditionalPanel(
285!
              condition = sprintf("input['%s'] == 'Eigenvector plot'", ns("plot_type")),
286!
              ui_decorate_teal_data(
287!
                ns("d_eigenvector_plot"),
288!
                decorators = select_decorators(args$decorators, "eigenvector_plot")
289
              )
290
            )
291
          ),
292!
          bslib::accordion_panel(
293!
            title = "Pre-processing",
294!
            radioButtons(
295!
              ns("standardization"), "Standardization",
296!
              choices = c("None" = "none", "Center" = "center", "Center & Scale" = "center_scale"),
297!
              selected = "center_scale"
298
            ),
299!
            radioButtons(
300!
              ns("na_action"), "NA action",
301!
              choices = c("None" = "none", "Drop" = "drop"),
302!
              selected = "none"
303
            )
304
          ),
305!
          bslib::accordion_panel(
306!
            title = "Selected plot specific settings",
307!
            uiOutput(ns("plot_settings")),
308!
            conditionalPanel(
309!
              condition = sprintf("input['%s'] == 'Biplot'", ns("plot_type")),
310!
              list(
311!
                teal.transform::data_extract_ui(
312!
                  id = ns("response"),
313!
                  label = "Color by",
314!
                  data_extract_spec = color_selector,
315!
                  is_single_dataset = is_single_dataset_value
316
                ),
317!
                teal.widgets::optionalSliderInputValMinMax(ns("alpha"), "Opacity:", args$alpha, ticks = FALSE),
318!
                teal.widgets::optionalSliderInputValMinMax(ns("size"), "Points size:", args$size, ticks = FALSE)
319
              )
320
            )
321
          ),
322!
          bslib::accordion_panel(
323!
            title = "Plot settings",
324!
            conditionalPanel(
325!
              condition = sprintf(
326!
                "input['%s'] == 'Elbow plot' || input['%s'] == 'Eigenvector plot'",
327!
                ns("plot_type"),
328!
                ns("plot_type")
329
              ),
330!
              list(checkboxInput(ns("rotate_xaxis_labels"), "Rotate X axis labels", value = args$rotate_xaxis_labels))
331
            ),
332!
            selectInput(
333!
              inputId = ns("ggtheme"),
334!
              label = "Theme (by ggplot):",
335!
              choices = ggplot_themes,
336!
              selected = args$ggtheme,
337!
              multiple = FALSE
338
            ),
339!
            teal.widgets::optionalSliderInputValMinMax(ns("font_size"), "Font Size", args$font_size, ticks = FALSE)
340
          )
341
        )
342
      ),
343!
      pre_output = args$pre_output,
344!
      post_output = args$post_output
345
    )
346
  )
347
}
348
349
# Server function for the PCA module
350
srv_a_pca <- function(id, data, dat, plot_height, plot_width, ggplot2_args, decorators) {
351!
  checkmate::assert_class(data, "reactive")
352!
  checkmate::assert_class(isolate(data()), "teal_data")
353!
  moduleServer(id, function(input, output, session) {
354!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
355
356!
    response <- dat
357
358!
    for (i in seq_along(response)) {
359!
      response[[i]]$select$multiple <- FALSE
360!
      response[[i]]$select$always_selected <- NULL
361!
      response[[i]]$select$selected <- NULL
362!
      all_cols <- teal.data::col_labels(isolate(data())[[response[[i]]$dataname]])
363!
      ignore_cols <- unlist(teal.data::join_keys(isolate(data()))[[response[[i]]$dataname]])
364!
      color_cols <- all_cols[!names(all_cols) %in% ignore_cols]
365!
      response[[i]]$select$choices <- teal.transform::choices_labeled(names(color_cols), color_cols)
366
    }
367
368!
    selector_list <- teal.transform::data_extract_multiple_srv(
369!
      data_extract = list(dat = dat, response = response),
370!
      datasets = data,
371!
      select_validation_rule = list(
372!
        dat = ~ if (length(.) < 2L) "Please select more than 1 variable to perform PCA.",
373!
        response = shinyvalidate::compose_rules(
374!
          shinyvalidate::sv_optional(),
375!
          ~ if (isTRUE(is.element(., selector_list()$dat()$select))) {
376!
            "Response must not have been used for PCA."
377
          }
378
        )
379
      )
380
    )
381
382!
    iv_r <- reactive({
383!
      iv <- shinyvalidate::InputValidator$new()
384!
      teal.transform::compose_and_enable_validators(iv, selector_list)
385
    })
386
387!
    iv_extra <- shinyvalidate::InputValidator$new()
388!
    iv_extra$add_rule("x_axis", function(value) {
389!
      if (isTRUE(input$plot_type %in% c("Circle plot", "Biplot"))) {
390!
        if (!shinyvalidate::input_provided(value)) {
391!
          "Need X axis"
392
        }
393
      }
394
    })
395!
    iv_extra$add_rule("y_axis", function(value) {
396!
      if (isTRUE(input$plot_type %in% c("Circle plot", "Biplot"))) {
397!
        if (!shinyvalidate::input_provided(value)) {
398!
          "Need Y axis"
399
        }
400
      }
401
    })
402!
    rule_dupl <- function(...) {
403!
      if (isTRUE(input$plot_type %in% c("Circle plot", "Biplot"))) {
404!
        if (isTRUE(input$x_axis == input$y_axis)) {
405!
          "Please choose different X and Y axes."
406
        }
407
      }
408
    }
409!
    iv_extra$add_rule("x_axis", rule_dupl)
410!
    iv_extra$add_rule("y_axis", rule_dupl)
411!
    iv_extra$add_rule("variables", function(value) {
412!
      if (identical(input$plot_type, "Circle plot")) {
413!
        if (!shinyvalidate::input_provided(value)) {
414!
          "Need Original Coordinates"
415
        }
416
      }
417
    })
418!
    iv_extra$add_rule("pc", function(value) {
419!
      if (identical(input$plot_type, "Eigenvector plot")) {
420!
        if (!shinyvalidate::input_provided(value)) {
421!
          "Need PC"
422
        }
423
      }
424
    })
425!
    iv_extra$enable()
426
427!
    anl_merged_input <- teal.transform::merge_expression_srv(
428!
      selector_list = selector_list,
429!
      datasets = data
430
    )
431!
    qenv <- reactive({
432!
      obj <- data()
433!
      teal.reporter::teal_card(obj) <-
434!
        c(
435!
          teal.reporter::teal_card(obj),
436!
          teal.reporter::teal_card("## Module's output(s)")
437
        )
438!
      teal.code::eval_code(obj, "library(ggplot2);library(dplyr);library(tidyr)")
439
    })
440!
    anl_merged_q <- reactive({
441!
      req(anl_merged_input())
442!
      qenv() %>%
443!
        teal.code::eval_code(as.expression(anl_merged_input()$expr))
444
    })
445
446!
    merged <- list(
447!
      anl_input_r = anl_merged_input,
448!
      anl_q_r = anl_merged_q
449
    )
450
451!
    validation <- reactive({
452!
      req(merged$anl_q_r())
453
      # inputs
454!
      keep_cols <- as.character(merged$anl_input_r()$columns_source$dat)
455!
      na_action <- input$na_action
456!
      standardization <- input$standardization
457!
      center <- standardization %in% c("center", "center_scale")
458!
      scale <- standardization == "center_scale"
459!
      ANL <- merged$anl_q_r()[["ANL"]]
460
461!
      teal::validate_has_data(ANL, 10)
462!
      validate(need(
463!
        na_action != "none" | !anyNA(ANL[keep_cols]),
464!
        paste(
465!
          "There are NAs in the dataset. Please deal with them in preprocessing",
466!
          "or select \"Drop\" in the NA actions inside the encodings panel (left)."
467
        )
468
      ))
469!
      if (scale) {
470!
        not_single <- vapply(ANL[keep_cols], function(column) length(unique(column)) != 1, FUN.VALUE = logical(1))
471
472!
        msg <- paste0(
473!
          "You have selected `Center & Scale` under `Standardization` in the `Pre-processing` panel, ",
474!
          "but one or more of your columns has/have a variance value of zero, indicating all values are identical"
475
        )
476!
        validate(need(all(not_single), msg))
477
      }
478
    })
479
480
    # computation ----
481!
    computation <- reactive({
482!
      validation()
483
484
      # inputs
485!
      keep_cols <- as.character(merged$anl_input_r()$columns_source$dat)
486!
      na_action <- input$na_action
487!
      standardization <- input$standardization
488!
      center <- standardization %in% c("center", "center_scale")
489!
      scale <- standardization == "center_scale"
490!
      ANL <- merged$anl_q_r()[["ANL"]]
491
492!
      qenv <- teal.code::eval_code(
493!
        merged$anl_q_r(),
494!
        substitute(
495!
          expr = keep_columns <- keep_cols,
496!
          env = list(keep_cols = keep_cols)
497
        )
498
      )
499
500!
      if (na_action == "drop") {
501!
        qenv <- teal.code::eval_code(
502!
          qenv,
503!
          quote(ANL <- tidyr::drop_na(ANL, keep_columns))
504
        )
505
      }
506
507!
      qenv <- teal.code::eval_code(
508!
        qenv,
509!
        substitute(
510!
          expr = pca <- summary(stats::prcomp(ANL[keep_columns], center = center, scale. = scale, retx = TRUE)),
511!
          env = list(center = center, scale = scale)
512
        )
513
      )
514
515!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Principal Components Table")
516
517!
      qenv <- teal.code::eval_code(
518!
        qenv,
519!
        quote({
520!
          tbl_importance <- dplyr::as_tibble(pca$importance, rownames = "Metric")
521!
          tbl_importance
522
        })
523
      )
524
525!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Eigenvectors Table")
526
527!
      teal.code::eval_code(
528!
        qenv,
529!
        quote({
530!
          tbl_eigenvector <- dplyr::as_tibble(pca$rotation, rownames = "Variable")
531!
          tbl_eigenvector
532
        })
533
      )
534
    })
535
536
    # plot args ----
537!
    output$plot_settings <- renderUI({
538
      # reactivity triggers
539!
      req(iv_r()$is_valid())
540!
      req(computation())
541!
      qenv <- computation()
542
543!
      ns <- session$ns
544
545!
      pca <- qenv[["pca"]]
546!
      chcs_pcs <- colnames(pca$rotation)
547!
      chcs_vars <- qenv[["keep_columns"]]
548
549!
      tagList(
550!
        conditionalPanel(
551!
          condition = sprintf(
552!
            "input['%s'] == 'Biplot' || input['%s'] == 'Circle plot'",
553!
            ns("plot_type"), ns("plot_type")
554
          ),
555!
          list(
556!
            teal.widgets::optionalSelectInput(ns("x_axis"), "X axis", choices = chcs_pcs, selected = chcs_pcs[1]),
557!
            teal.widgets::optionalSelectInput(ns("y_axis"), "Y axis", choices = chcs_pcs, selected = chcs_pcs[2]),
558!
            teal.widgets::optionalSelectInput(
559!
              ns("variables"), "Original coordinates",
560!
              choices = chcs_vars, selected = chcs_vars,
561!
              multiple = TRUE
562
            )
563
          )
564
        ),
565!
        conditionalPanel(
566!
          condition = sprintf("input['%s'] == 'Elbow plot'", ns("plot_type")),
567!
          helpText("No plot specific settings available.")
568
        ),
569!
        conditionalPanel(
570!
          condition = paste0("input['", ns("plot_type"), "'] == 'Eigenvector plot'"),
571!
          teal.widgets::optionalSelectInput(ns("pc"), "PC", choices = chcs_pcs, selected = chcs_pcs[1])
572
        )
573
      )
574
    })
575
576
    # plot elbow ----
577!
    plot_elbow <- function(base_q) {
578!
      ggtheme <- input$ggtheme
579!
      rotate_xaxis_labels <- input$rotate_xaxis_labels
580!
      font_size <- input$font_size
581
582!
      angle_value <- ifelse(isTRUE(rotate_xaxis_labels), 45, 0)
583!
      hjust_value <- ifelse(isTRUE(rotate_xaxis_labels), 1, 0.5)
584
585!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
586!
        labs = list(x = "Principal component", y = "Proportion of variance explained", color = "", fill = "Legend"),
587!
        theme = list(
588!
          legend.position = "right",
589!
          legend.spacing.y = quote(grid::unit(-5, "pt")),
590!
          legend.title = quote(ggplot2::element_text(vjust = 25)),
591!
          axis.text.x = substitute(
592!
            ggplot2::element_text(angle = angle_value, hjust = hjust_value),
593!
            list(angle_value = angle_value, hjust_value = hjust_value)
594
          ),
595!
          text = substitute(ggplot2::element_text(size = font_size), list(font_size = font_size))
596
        )
597
      )
598
599!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
600!
        teal.widgets::resolve_ggplot2_args(
601!
          user_plot = ggplot2_args[["Elbow plot"]],
602!
          user_default = ggplot2_args$default,
603!
          module_plot = dev_ggplot2_args
604
        ),
605!
        ggtheme = ggtheme
606
      )
607!
      teal.reporter::teal_card(base_q) <- c(teal.reporter::teal_card(base_q), "### Elbow plot")
608!
      teal.code::eval_code(
609!
        base_q,
610!
        substitute(
611!
          expr = {
612!
            elb_dat <- pca$importance[c("Proportion of Variance", "Cumulative Proportion"), ] %>%
613!
              dplyr::as_tibble(rownames = "metric") %>%
614!
              tidyr::gather("component", "value", -metric) %>%
615!
              dplyr::mutate(
616!
                component = factor(component, levels = unique(stringr::str_sort(component, numeric = TRUE)))
617
              )
618
619!
            cols <- c(getOption("ggplot2.discrete.colour"), c("lightblue", "darkred", "black"))[1:3]
620!
            elbow_plot <- ggplot2::ggplot(mapping = ggplot2::aes_string(x = "component", y = "value")) +
621!
              ggplot2::geom_bar(
622!
                ggplot2::aes(fill = "Single variance"),
623!
                data = dplyr::filter(elb_dat, metric == "Proportion of Variance"),
624!
                color = "black",
625!
                stat = "identity"
626
              ) +
627!
              ggplot2::geom_point(
628!
                ggplot2::aes(color = "Cumulative variance"),
629!
                data = dplyr::filter(elb_dat, metric == "Cumulative Proportion")
630
              ) +
631!
              ggplot2::geom_line(
632!
                ggplot2::aes(group = 1, color = "Cumulative variance"),
633!
                data = dplyr::filter(elb_dat, metric == "Cumulative Proportion")
634
              ) +
635!
              labs +
636!
              ggplot2::scale_color_manual(values = c("Cumulative variance" = cols[2], "Single variance" = cols[3])) +
637!
              ggplot2::scale_fill_manual(values = c("Cumulative variance" = cols[2], "Single variance" = cols[1])) +
638!
              ggthemes +
639!
              themes
640
          },
641!
          env = list(
642!
            ggthemes = parsed_ggplot2_args$ggtheme,
643!
            labs = parsed_ggplot2_args$labs,
644!
            themes = parsed_ggplot2_args$theme
645
          )
646
        )
647
      )
648
    }
649
650
    # plot circle ----
651!
    plot_circle <- function(base_q) {
652!
      x_axis <- input$x_axis
653!
      y_axis <- input$y_axis
654!
      variables <- input$variables
655!
      ggtheme <- input$ggtheme
656
657!
      rotate_xaxis_labels <- input$rotate_xaxis_labels
658!
      font_size <- input$font_size
659
660!
      angle <- ifelse(isTRUE(rotate_xaxis_labels), 45, 0)
661!
      hjust <- ifelse(isTRUE(rotate_xaxis_labels), 1, 0.5)
662
663!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
664!
        theme = list(
665!
          text = substitute(ggplot2::element_text(size = font_size), list(font_size = font_size)),
666!
          axis.text.x = substitute(
667!
            ggplot2::element_text(angle = angle_val, hjust = hjust_val),
668!
            list(angle_val = angle, hjust_val = hjust)
669
          )
670
        )
671
      )
672
673!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
674!
        user_plot = ggplot2_args[["Circle plot"]],
675!
        user_default = ggplot2_args$default,
676!
        module_plot = dev_ggplot2_args
677
      )
678
679!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
680!
        all_ggplot2_args,
681!
        ggtheme = ggtheme
682
      )
683
684!
      teal.reporter::teal_card(base_q) <- c(teal.reporter::teal_card(base_q), "### Circle plot")
685!
      teal.code::eval_code(
686!
        base_q,
687!
        substitute(
688!
          expr = {
689!
            pca_rot <- pca$rotation[, c(x_axis, y_axis)] %>%
690!
              dplyr::as_tibble(rownames = "label") %>%
691!
              dplyr::filter(label %in% variables)
692
693!
            circle_data <- data.frame(
694!
              x = cos(seq(0, 2 * pi, length.out = 100)),
695!
              y = sin(seq(0, 2 * pi, length.out = 100))
696
            )
697
698!
            circle_plot <- ggplot2::ggplot(pca_rot) +
699!
              ggplot2::geom_point(ggplot2::aes_string(x = x_axis, y = y_axis)) +
700!
              ggplot2::geom_label(
701!
                ggplot2::aes_string(x = x_axis, y = y_axis, label = "label"),
702!
                nudge_x = 0.1, nudge_y = 0.05,
703!
                fontface = "bold"
704
              ) +
705!
              ggplot2::geom_path(ggplot2::aes(x, y, group = 1), data = circle_data) +
706!
              ggplot2::geom_point(ggplot2::aes(x = x, y = y), data = data.frame(x = 0, y = 0), shape = "x", size = 5) +
707!
              labs +
708!
              ggthemes +
709!
              themes
710
          },
711!
          env = list(
712!
            x_axis = x_axis,
713!
            y_axis = y_axis,
714!
            variables = variables,
715!
            ggthemes = parsed_ggplot2_args$ggtheme,
716!
            labs = `if`(is.null(parsed_ggplot2_args$labs), quote(labs()), parsed_ggplot2_args$labs),
717!
            themes = parsed_ggplot2_args$theme
718
          )
719
        )
720
      )
721
    }
722
723
    # plot biplot ----
724!
    plot_biplot <- function(base_q) {
725!
      qenv <- base_q
726
727!
      ANL <- qenv[["ANL"]]
728
729!
      resp_col <- as.character(merged$anl_input_r()$columns_source$response)
730!
      dat_cols <- as.character(merged$anl_input_r()$columns_source$dat)
731!
      x_axis <- input$x_axis
732!
      y_axis <- input$y_axis
733!
      variables <- input$variables
734!
      pca <- qenv[["pca"]]
735
736!
      ggtheme <- input$ggtheme
737
738!
      rotate_xaxis_labels <- input$rotate_xaxis_labels
739!
      alpha <- input$alpha
740!
      size <- input$size
741!
      font_size <- input$font_size
742
743!
      teal.reporter::teal_card(base_q) <- c(teal.reporter::teal_card(base_q), "### Biplot")
744!
      qenv <- teal.code::eval_code(
745!
        qenv,
746!
        substitute(
747!
          expr = pca_rot <- dplyr::as_tibble(pca$x[, c(x_axis, y_axis)]),
748!
          env = list(x_axis = x_axis, y_axis = y_axis)
749
        )
750
      )
751
752
      # rot_vars = data frame that displays arrows in the plot, need to be scaled to data
753!
      if (!is.null(input$variables)) {
754!
        qenv <- teal.code::eval_code(
755!
          qenv,
756!
          substitute(
757!
            expr = {
758!
              r <- sqrt(qchisq(0.69, df = 2)) * prod(colMeans(pca_rot ^ 2)) ^ (1 / 4) # styler: off
759!
              v_scale <- rowSums(pca$rotation ^ 2) # styler: off
760
761!
              rot_vars <- pca$rotation[, c(x_axis, y_axis)] %>%
762!
                dplyr::as_tibble(rownames = "label") %>%
763!
                dplyr::mutate_at(vars(c(x_axis, y_axis)), function(x) r * x / sqrt(max(v_scale)))
764
            },
765!
            env = list(x_axis = x_axis, y_axis = y_axis)
766
          )
767
        ) %>%
768!
          teal.code::eval_code(
769!
            if (is.logical(pca$center) && !pca$center) {
770!
              substitute(
771!
                expr = {
772!
                  rot_vars <- rot_vars %>%
773!
                    tibble::column_to_rownames("label") %>%
774!
                    sweep(1, apply(ANL[keep_columns], 2, mean, na.rm = TRUE)) %>%
775!
                    tibble::rownames_to_column("label") %>%
776!
                    dplyr::mutate(
777!
                      xstart = mean(pca$x[, x_axis], na.rm = TRUE),
778!
                      ystart = mean(pca$x[, y_axis], na.rm = TRUE)
779
                    )
780
                },
781!
                env = list(x_axis = x_axis, y_axis = y_axis)
782
              )
783
            } else {
784!
              quote(rot_vars <- rot_vars %>% dplyr::mutate(xstart = 0, ystart = 0))
785
            }
786
          ) %>%
787!
          teal.code::eval_code(
788!
            substitute(
789!
              expr = rot_vars <- rot_vars %>% dplyr::filter(label %in% variables),
790!
              env = list(variables = variables)
791
            )
792
          )
793
      }
794
795!
      pca_plot_biplot_expr <- list(quote(ggplot()))
796
797!
      if (length(resp_col) == 0) {
798!
        pca_plot_biplot_expr <- c(
799!
          pca_plot_biplot_expr,
800!
          substitute(
801!
            ggplot2::geom_point(ggplot2::aes_string(x = x_axis, y = y_axis),
802!
              data = pca_rot, alpha = alpha, size = size
803
            ),
804!
            list(x_axis = input$x_axis, y_axis = input$y_axis, alpha = input$alpha, size = input$size)
805
          )
806
        )
807!
        dev_labs <- list()
808
      } else {
809!
        rp_keys <- setdiff(colnames(ANL), as.character(unlist(merged$anl_input_r()$columns_source)))
810
811!
        response <- ANL[[resp_col]]
812
813!
        aes_biplot <- substitute(
814!
          ggplot2::aes_string(x = x_axis, y = y_axis, color = "response"),
815!
          env = list(x_axis = x_axis, y_axis = y_axis)
816
        )
817
818!
        qenv <- teal.code::eval_code(
819!
          qenv,
820!
          substitute(response <- ANL[[resp_col]], env = list(resp_col = resp_col))
821
        )
822
823!
        dev_labs <- list(color = varname_w_label(resp_col, ANL))
824
825!
        scales_biplot <-
826!
          if (
827!
            is.character(response) ||
828!
              is.factor(response) ||
829!
              (is.numeric(response) && length(unique(response)) <= 6)
830
          ) {
831!
            qenv <- teal.code::eval_code(
832!
              qenv,
833!
              quote(pca_rot$response <- as.factor(response))
834
            )
835!
            quote(ggplot2::scale_color_brewer(palette = "Dark2"))
836!
          } else if (inherits(response, "Date")) {
837!
            qenv <- teal.code::eval_code(
838!
              qenv,
839!
              quote(pca_rot$response <- numeric(response))
840
            )
841
842!
            quote(
843!
              ggplot2::scale_color_gradient(
844!
                low = c(getOption("ggplot2.discrete.colour")[2], "darkred")[1],
845!
                high = c(getOption("ggplot2.discrete.colour"), "lightblue")[1],
846!
                labels = function(x) as.Date(x, origin = "1970-01-01")
847
              )
848
            )
849
          } else {
850!
            qenv <- teal.code::eval_code(
851!
              qenv,
852!
              quote(pca_rot$response <- response)
853
            )
854!
            quote(ggplot2::scale_color_gradient(
855!
              low = c(getOption("ggplot2.discrete.colour")[2], "darkred")[1],
856!
              high = c(getOption("ggplot2.discrete.colour"), "lightblue")[1]
857
            ))
858
          }
859
860!
        pca_plot_biplot_expr <- c(
861!
          pca_plot_biplot_expr,
862!
          substitute(
863!
            ggplot2::geom_point(aes_biplot, data = pca_rot, alpha = alpha, size = size),
864!
            env = list(aes_biplot = aes_biplot, alpha = alpha, size = size)
865
          ),
866!
          scales_biplot
867
        )
868
      }
869
870!
      if (!is.null(input$variables)) {
871!
        pca_plot_biplot_expr <- c(
872!
          pca_plot_biplot_expr,
873!
          substitute(
874!
            ggplot2::geom_segment(
875!
              ggplot2::aes_string(x = "xstart", y = "ystart", xend = x_axis, yend = y_axis),
876!
              data = rot_vars,
877!
              lineend = "round", linejoin = "round",
878!
              arrow = grid::arrow(length = grid::unit(0.5, "cm"))
879
            ),
880!
            env = list(x_axis = x_axis, y_axis = y_axis)
881
          ),
882!
          substitute(
883!
            ggplot2::geom_label(
884!
              ggplot2::aes_string(
885!
                x = x_axis,
886!
                y = y_axis,
887!
                label = "label"
888
              ),
889!
              data = rot_vars,
890!
              nudge_y = 0.1,
891!
              fontface = "bold"
892
            ),
893!
            env = list(x_axis = x_axis, y_axis = y_axis)
894
          ),
895!
          quote(ggplot2::geom_point(ggplot2::aes(x = xstart, y = ystart), data = rot_vars, shape = "x", size = 5))
896
        )
897
      }
898
899!
      angle <- ifelse(isTRUE(rotate_xaxis_labels), 45, 0)
900!
      hjust <- ifelse(isTRUE(rotate_xaxis_labels), 1, 0.5)
901
902!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
903!
        labs = dev_labs,
904!
        theme = list(
905!
          text = substitute(ggplot2::element_text(size = font_size), list(font_size = font_size)),
906!
          axis.text.x = substitute(
907!
            ggplot2::element_text(angle = angle_val, hjust = hjust_val),
908!
            list(angle_val = angle, hjust_val = hjust)
909
          )
910
        )
911
      )
912
913!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
914!
        user_plot = ggplot2_args[["Biplot"]],
915!
        user_default = ggplot2_args$default,
916!
        module_plot = dev_ggplot2_args
917
      )
918
919!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
920!
        all_ggplot2_args,
921!
        ggtheme = ggtheme
922
      )
923
924!
      pca_plot_biplot_expr <- c(
925!
        pca_plot_biplot_expr,
926!
        parsed_ggplot2_args
927
      )
928
929!
      teal.code::eval_code(
930!
        qenv,
931!
        substitute(
932!
          expr = {
933!
            biplot <- plot_call
934
          },
935!
          env = list(
936!
            plot_call = Reduce(function(x, y) call("+", x, y), pca_plot_biplot_expr)
937
          )
938
        )
939
      )
940
    }
941
942
    # plot eigenvector_plot ----
943!
    plot_eigenvector <- function(base_q) {
944!
      req(input$pc)
945!
      pc <- input$pc
946!
      ggtheme <- input$ggtheme
947
948!
      rotate_xaxis_labels <- input$rotate_xaxis_labels
949!
      font_size <- input$font_size
950
951!
      angle <- ifelse(rotate_xaxis_labels, 45, 0)
952!
      hjust <- ifelse(rotate_xaxis_labels, 1, 0.5)
953
954!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
955!
        theme = list(
956!
          text = substitute(ggplot2::element_text(size = font_size), list(font_size = font_size)),
957!
          axis.text.x = substitute(
958!
            ggplot2::element_text(angle = angle_val, hjust = hjust_val),
959!
            list(angle_val = angle, hjust_val = hjust)
960
          )
961
        )
962
      )
963
964!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
965!
        user_plot = ggplot2_args[["Eigenvector plot"]],
966!
        user_default = ggplot2_args$default,
967!
        module_plot = dev_ggplot2_args
968
      )
969
970!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
971!
        all_ggplot2_args,
972!
        ggtheme = ggtheme
973
      )
974
975!
      ggplot_exprs <- c(
976!
        list(
977!
          quote(ggplot(pca_rot)),
978!
          substitute(
979!
            ggplot2::geom_bar(
980!
              ggplot2::aes_string(x = "Variable", y = pc),
981!
              stat = "identity",
982!
              color = "black",
983!
              fill = c(getOption("ggplot2.discrete.colour"), "lightblue")[1]
984
            ),
985!
            env = list(pc = pc)
986
          ),
987!
          substitute(
988!
            ggplot2::geom_text(
989!
              ggplot2::aes(
990!
                x = Variable,
991!
                y = pc_name,
992!
                label = round(pc_name, 3),
993!
                vjust = ifelse(pc_name > 0, -0.5, 1.3)
994
              )
995
            ),
996!
            env = list(pc_name = as.name(pc))
997
          )
998
        ),
999!
        parsed_ggplot2_args$labs,
1000!
        parsed_ggplot2_args$ggtheme,
1001!
        parsed_ggplot2_args$theme
1002
      )
1003
1004!
      teal.reporter::teal_card(base_q) <- c(teal.reporter::teal_card(base_q), "### Eigenvector plot")
1005!
      teal.code::eval_code(
1006!
        base_q,
1007!
        substitute(
1008!
          expr = {
1009!
            pca_rot <- pca$rotation[, pc, drop = FALSE] %>%
1010!
              dplyr::as_tibble(rownames = "Variable")
1011!
            eigenvector_plot <- plot_call
1012
          },
1013!
          env = list(
1014!
            pc = pc,
1015!
            plot_call = Reduce(function(x, y) call("+", x, y), ggplot_exprs)
1016
          )
1017
        )
1018
      )
1019
    }
1020
1021
    # qenvs ---
1022!
    output_q <- lapply(
1023!
      list(
1024!
        elbow_plot = plot_elbow,
1025!
        circle_plot = plot_circle,
1026!
        biplot = plot_biplot,
1027!
        eigenvector_plot = plot_eigenvector
1028
      ),
1029!
      function(fun) {
1030!
        reactive({
1031!
          req(computation())
1032!
          teal::validate_inputs(iv_r())
1033!
          teal::validate_inputs(iv_extra, header = "Plot settings are required")
1034!
          fun(computation())
1035
        })
1036
      }
1037
    )
1038
1039!
    decorated_q <- mapply(
1040!
      function(obj_name, q) {
1041!
        srv_decorate_teal_data(
1042!
          id = sprintf("d_%s", obj_name),
1043!
          data = q,
1044!
          decorators = select_decorators(decorators, obj_name),
1045!
          expr = reactive({
1046!
            substitute(.plot, env = list(.plot = as.name(obj_name)))
1047
          })
1048
        )
1049
      },
1050!
      names(output_q),
1051!
      output_q
1052
    )
1053
1054
    # plot final ----
1055!
    decorated_output_q <- reactive({
1056!
      switch(req(input$plot_type),
1057!
        "Elbow plot" = decorated_q$elbow_plot(),
1058!
        "Circle plot" = decorated_q$circle_plot(),
1059!
        "Biplot" = decorated_q$biplot(),
1060!
        "Eigenvector plot" = decorated_q$eigenvector_plot(),
1061!
        stop("Unknown plot")
1062
      )
1063
    })
1064
1065!
    plot_r <- reactive({
1066!
      plot_name <- gsub(" ", "_", tolower(req(input$plot_type)))
1067!
      req(decorated_output_q())[[plot_name]]
1068
    })
1069
1070!
    pws <- teal.widgets::plot_with_settings_srv(
1071!
      id = "pca_plot",
1072!
      plot_r = plot_r,
1073!
      height = plot_height,
1074!
      width = plot_width,
1075!
      graph_align = "center"
1076
    )
1077
1078
    # tables ----
1079!
    output$tbl_importance <- renderTable(
1080!
      expr = {
1081!
        req("importance" %in% input$tables_display, computation())
1082!
        computation()[["tbl_importance"]]
1083
      },
1084!
      bordered = TRUE,
1085!
      align = "c",
1086!
      digits = 3
1087
    )
1088
1089!
    output$tbl_importance_ui <- renderUI({
1090!
      req("importance" %in% input$tables_display)
1091!
      tags$div(
1092!
        align = "center",
1093!
        tags$h4("Principal components importance"),
1094!
        tableOutput(session$ns("tbl_importance")),
1095!
        tags$hr()
1096
      )
1097
    })
1098
1099!
    output$tbl_eigenvector <- renderTable(
1100!
      expr = {
1101!
        req("eigenvector" %in% input$tables_display, req(computation()))
1102!
        computation()[["tbl_eigenvector"]]
1103
      },
1104!
      bordered = TRUE,
1105!
      align = "c",
1106!
      digits = 3
1107
    )
1108
1109!
    output$tbl_eigenvector_ui <- renderUI({
1110!
      req("eigenvector" %in% input$tables_display)
1111!
      tags$div(
1112!
        align = "center",
1113!
        tags$h4("Eigenvectors"),
1114!
        tableOutput(session$ns("tbl_eigenvector")),
1115!
        tags$hr()
1116
      )
1117
    })
1118
1119!
    output$all_plots <- renderUI({
1120!
      teal::validate_inputs(iv_r())
1121!
      teal::validate_inputs(iv_extra, header = "Plot settings are required")
1122
1123!
      validation()
1124!
      tags$div(
1125!
        uiOutput(session$ns("tbl_importance_ui")),
1126!
        uiOutput(session$ns("tbl_eigenvector_ui")),
1127!
        teal.widgets::plot_with_settings_ui(id = session$ns("pca_plot"))
1128
      )
1129
    })
1130
1131!
    set_chunk_dims(pws, decorated_output_q)
1132
  })
1133
}
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
#' JavaScript expression to check if a tab is active
238
#'
239
#' @param id (`character(1)`) the id of the tab panel with tabs.
240
#' @param name (`character(1)`) the name of the tab.
241
#' @return JavaScript expression to be used in `shiny::conditionalPanel()` to determine
242
#' if the specified tab is active.
243
#' @keywords internal
244
#'
245
is_tab_active_js <- function(id, name) {
246
  # supporting the bs3 and higher version at the same time
247!
  sprintf(
248!
    "$(\"#%1$s > li.active\").text().trim() == '%2$s' || $(\"#%1$s > li a.active\").text().trim() == '%2$s'",
249!
    id, name
250
  )
251
}
252
253
#' Assert single selection on `data_extract_spec` object
254
#' Helper to reduce code in assertions
255
#' @noRd
256
#'
257
assert_single_selection <- function(x,
258
                                    .var.name = checkmate::vname(x)) { # nolint: object_name.
259104x
  if (any(vapply(x, function(.x) .x$select$multiple, logical(1)))) {
2604x
    stop("'", .var.name, "' should not allow multiple selection")
261
  }
262100x
  invisible(TRUE)
263
}
264
265
#' Wrappers around `srv_transform_teal_data` that allows to decorate the data
266
#' @inheritParams teal::srv_transform_teal_data
267
#' @inheritParams teal.reporter::`eval_code,teal_report-method`
268
#' @param expr (`reactive`) with expression to evaluate on the output of the
269
#' decoration. It must be compatible with `code` argument of [teal.code::eval_code()].
270
#' Default is `NULL` which won't evaluate any appending code.
271
#' @details
272
#' `srv_decorate_teal_data` is a wrapper around `srv_transform_teal_data` that
273
#' allows to decorate the data with additional expressions.
274
#' When original `teal_data` object is in error state, it will show that error
275
#' first.
276
#'
277
#' @keywords internal
278
srv_decorate_teal_data <- function(id, data, decorators, expr) {
279!
  checkmate::assert_class(data, classes = "reactive")
280!
  checkmate::assert_list(decorators, "teal_transform_module")
281
282!
  no_expr <- missing(expr)
283
284!
  moduleServer(id, function(input, output, session) {
285!
    decorated_output <- srv_transform_teal_data("inner", data = data, transformators = decorators)
286
287!
    expr_r <- if (is.reactive(expr)) expr else reactive(expr)
288
289!
    reactive({
290!
      req(decorated_output())
291!
      if (no_expr) {
292!
        decorated_output()
293
      } else {
294!
        teal.code::eval_code(decorated_output(), expr_r())
295
      }
296
    })
297
  })
298
}
299
300
#' @rdname srv_decorate_teal_data
301
#' @details
302
#' `ui_decorate_teal_data` is a wrapper around `ui_transform_teal_data`.
303
#' @keywords internal
304
ui_decorate_teal_data <- function(id, decorators, ...) {
305!
  teal::ui_transform_teal_data(NS(id, "inner"), transformators = decorators, ...)
306
}
307
308
#' Internal function to check if decorators is a valid object
309
#' @noRd
310
check_decorators <- function(x, names = NULL) { # nolint: object_name.
311
3125x
  check_message <- checkmate::check_list(x, names = "named")
313
3145x
  if (!is.null(names)) {
3155x
    if (isTRUE(check_message)) {
3165x
      if (length(names(x)) != length(unique(names(x)))) {
317!
        check_message <- sprintf(
318!
          "The `decorators` must contain unique names from these names: %s.",
319!
          paste(names, collapse = ", ")
320
        )
321
      }
322
    } else {
323!
      check_message <- sprintf(
324!
        "The `decorators` must be a named list from these names: %s.",
325!
        paste(names, collapse = ", ")
326
      )
327
    }
328
  }
329
3305x
  if (!isTRUE(check_message)) {
331!
    return(check_message)
332
  }
333
3345x
  valid_elements <- vapply(
3355x
    x,
3365x
    checkmate::test_class,
3375x
    classes = "teal_transform_module",
3385x
    FUN.VALUE = logical(1L)
339
  )
340
3415x
  if (all(valid_elements)) {
3425x
    return(TRUE)
343
  }
344
345!
  "Make sure that the named list contains 'teal_transform_module' objects created using `teal_transform_module()`."
346
}
347
#' Internal assertion on decorators
348
#' @noRd
349
assert_decorators <- checkmate::makeAssertionFunction(check_decorators)
350
351
#' Subset decorators based on the scope
352
#'
353
#' @param scope (`character`) a character vector of decorator names to include.
354
#' @param decorators (named `list`) of list decorators to subset.
355
#'
356
#' @return Subsetted list with all decorators to include.
357
#' It can be an empty list if none of the scope exists in `decorators` argument.
358
#' @keywords internal
359
select_decorators <- function(decorators, scope) {
360!
  checkmate::assert_character(scope, null.ok = TRUE)
361!
  if (scope %in% names(decorators)) {
362!
    decorators[scope]
363
  } else {
364!
    list()
365
  }
366
}
367
368
#' Set the attributes of the last chunk outputs
369
#' @param teal_card (`teal_card`) object to modify.
370
#' @param attributes (`list`) of attributes to set on the last chunk outputs.
371
#' @param n (`integer(1)`) number of the last element of `teal_card` to modify.
372
#' it will only change `chunk_output` objects.
373
#' @param inner_classes (`character`) classes within `chunk_output` that should be modified.
374
#' This can be used to only change `recordedplot`, `ggplot2` or other type of objects.
375
#' @keywords internal
376
set_chunk_attrs <- function(teal_card,
377
                            attributes,
378
                            n = 1,
379
                            inner_classes = NULL,
380
                            quiet = FALSE) {
3817x
  checkmate::assert_class(teal_card, "teal_card")
3827x
  checkmate::assert_list(attributes, names = "unique")
3837x
  checkmate::assert_int(n, lower = 1)
3847x
  checkmate::assert_character(inner_classes, null.ok = TRUE)
3857x
  checkmate::assert_flag(quiet)
386
3877x
  if (!inherits(teal_card[[length(teal_card)]], "chunk_output")) {
3881x
    if (!quiet) {
3891x
      warning("The last element of the `teal_card` is not a `chunk_output` object. No attributes were modified.")
390
    }
3911x
    return(teal_card)
392
  }
393
3946x
  for (ix in seq_len(length(teal_card))) {
39514x
    if (ix > n) {
3964x
      break
397
    }
39810x
    current_ix <- length(teal_card) + 1 - ix
39910x
    if (!inherits(teal_card[[current_ix]], "chunk_output")) {
4002x
      if (!quiet) {
4011x
        warning(
4021x
          "The ", ix,
4031x
          " to last element of the `teal_card` is not a `chunk_output` object. Skipping any further modifications."
404
        )
405
      }
4062x
      return(teal_card)
407
    }
408
409
    if (
4108x
      length(inner_classes) > 0 &&
4118x
        length(teal_card[[current_ix]]) >= 1 &&
4128x
        !checkmate::test_multi_class(teal_card[[current_ix]][[1]], inner_classes)
413
    ) {
4141x
      next
415
    }
416
4177x
    attributes(teal_card[[current_ix]]) <- utils::modifyList(
4187x
      attributes(teal_card[[current_ix]]),
4197x
      attributes
420
    )
421
  }
422
4234x
  teal_card
424
}
425
426
#' Create a reactive that sets plot dimensions on a `teal_card`
427
#'
428
#' This is a convenience function that creates a reactive expression that
429
#' automatically sets the `dev.width` and `dev.height` attributes on the last
430
#' chunk outputs of a `teal_card` based on plot dimensions from a plot widget.
431
#'
432
#' @param pws (`plot_widget`) plot widget that provides dimensions via `dim()` method
433
#' @param q_r (`reactive`) reactive expression that returns a `teal_reporter`
434
#' @param inner_classes (`character`) classes within `chunk_output` that should be modified.
435
#' This can be used to only change `recordedplot`, `ggplot2` or other type of objects.
436
#'
437
#' @return A reactive expression that returns the `teal_card` with updated dimensions
438
#'
439
#' @keywords internal
440
set_chunk_dims <- function(pws, q_r, inner_classes = NULL) {
4411x
  checkmate::assert_list(pws)
4421x
  checkmate::assert_names(names(pws), must.include = "dim")
4431x
  checkmate::assert_class(pws$dim, "reactive")
4441x
  checkmate::assert_class(q_r, "reactive")
4451x
  checkmate::assert_character(inner_classes, null.ok = TRUE)
446
4471x
  reactive({
4481x
    pws_dim <- stats::setNames(as.list(req(pws$dim())), c("width", "height"))
4491x
    if (identical(pws_dim$width, "auto")) { # ignore non-numeric values (such as "auto")
4501x
      pws_dim$width <- NULL
451
    }
4521x
    if (identical(pws_dim$height, "auto")) { # ignore non-numeric values (such as "auto")
453!
      pws_dim$height <- NULL
454
    }
4551x
    q <- req(q_r())
4561x
    teal.reporter::teal_card(q) <- set_chunk_attrs(
4571x
      teal.reporter::teal_card(q),
4581x
      list(dev.width = pws_dim$width, dev.height = pws_dim$height),
4591x
      inner_classes = inner_classes
460
    )
4611x
    q
462
  })
463
}
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
#' @inheritSection teal::example_module Reporting
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
#'   require(nestcolor)
70
#'   CO2 <- CO2
71
#' })
72
#'
73
#' app <- init(
74
#'   data = data,
75
#'   modules = modules(
76
#'     tm_g_scatterplot(
77
#'       label = "Scatterplot Choices",
78
#'       x = data_extract_spec(
79
#'         dataname = "CO2",
80
#'         select = select_spec(
81
#'           label = "Select variable:",
82
#'           choices = variable_choices(data[["CO2"]], c("conc", "uptake")),
83
#'           selected = "conc",
84
#'           multiple = FALSE,
85
#'           fixed = FALSE
86
#'         )
87
#'       ),
88
#'       y = data_extract_spec(
89
#'         dataname = "CO2",
90
#'         select = select_spec(
91
#'           label = "Select variable:",
92
#'           choices = variable_choices(data[["CO2"]], c("conc", "uptake")),
93
#'           selected = "uptake",
94
#'           multiple = FALSE,
95
#'           fixed = FALSE
96
#'         )
97
#'       ),
98
#'       color_by = data_extract_spec(
99
#'         dataname = "CO2",
100
#'         select = select_spec(
101
#'           label = "Select variable:",
102
#'           choices = variable_choices(
103
#'             data[["CO2"]],
104
#'             c("Plant", "Type", "Treatment", "conc", "uptake")
105
#'           ),
106
#'           selected = NULL,
107
#'           multiple = FALSE,
108
#'           fixed = FALSE
109
#'         )
110
#'       ),
111
#'       size_by = data_extract_spec(
112
#'         dataname = "CO2",
113
#'         select = select_spec(
114
#'           label = "Select variable:",
115
#'           choices = variable_choices(data[["CO2"]], c("conc", "uptake")),
116
#'           selected = "uptake",
117
#'           multiple = FALSE,
118
#'           fixed = FALSE
119
#'         )
120
#'       ),
121
#'       row_facet = data_extract_spec(
122
#'         dataname = "CO2",
123
#'         select = select_spec(
124
#'           label = "Select variable:",
125
#'           choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
126
#'           selected = NULL,
127
#'           multiple = FALSE,
128
#'           fixed = FALSE
129
#'         )
130
#'       ),
131
#'       col_facet = data_extract_spec(
132
#'         dataname = "CO2",
133
#'         select = select_spec(
134
#'           label = "Select variable:",
135
#'           choices = variable_choices(data[["CO2"]], c("Plant", "Type", "Treatment")),
136
#'           selected = NULL,
137
#'           multiple = FALSE,
138
#'           fixed = FALSE
139
#'         )
140
#'       )
141
#'     )
142
#'   )
143
#' )
144
#' if (interactive()) {
145
#'   shinyApp(app$ui, app$server)
146
#' }
147
#'
148
#' @examplesShinylive
149
#' library(teal.modules.general)
150
#' interactive <- function() TRUE
151
#' {{ next_example }}
152
# nolint start: line_length_linter.
153
#' @examples
154
# nolint end: line_length_linter.
155
#' # CDISC data example
156
#' data <- teal_data()
157
#' data <- within(data, {
158
#'   require(nestcolor)
159
#'   ADSL <- teal.data::rADSL
160
#' })
161
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
162
#'
163
#' app <- init(
164
#'   data = data,
165
#'   modules = modules(
166
#'     tm_g_scatterplot(
167
#'       label = "Scatterplot Choices",
168
#'       x = data_extract_spec(
169
#'         dataname = "ADSL",
170
#'         select = select_spec(
171
#'           label = "Select variable:",
172
#'           choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1", "BMRKR2")),
173
#'           selected = "AGE",
174
#'           multiple = FALSE,
175
#'           fixed = FALSE
176
#'         )
177
#'       ),
178
#'       y = data_extract_spec(
179
#'         dataname = "ADSL",
180
#'         select = select_spec(
181
#'           label = "Select variable:",
182
#'           choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1", "BMRKR2")),
183
#'           selected = "BMRKR1",
184
#'           multiple = FALSE,
185
#'           fixed = FALSE
186
#'         )
187
#'       ),
188
#'       color_by = data_extract_spec(
189
#'         dataname = "ADSL",
190
#'         select = select_spec(
191
#'           label = "Select variable:",
192
#'           choices = variable_choices(
193
#'             data[["ADSL"]],
194
#'             c("AGE", "BMRKR1", "BMRKR2", "RACE", "REGION1")
195
#'           ),
196
#'           selected = NULL,
197
#'           multiple = FALSE,
198
#'           fixed = FALSE
199
#'         )
200
#'       ),
201
#'       size_by = data_extract_spec(
202
#'         dataname = "ADSL",
203
#'         select = select_spec(
204
#'           label = "Select variable:",
205
#'           choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1")),
206
#'           selected = "AGE",
207
#'           multiple = FALSE,
208
#'           fixed = FALSE
209
#'         )
210
#'       ),
211
#'       row_facet = data_extract_spec(
212
#'         dataname = "ADSL",
213
#'         select = select_spec(
214
#'           label = "Select variable:",
215
#'           choices = variable_choices(data[["ADSL"]], c("BMRKR2", "RACE", "REGION1")),
216
#'           selected = NULL,
217
#'           multiple = FALSE,
218
#'           fixed = FALSE
219
#'         )
220
#'       ),
221
#'       col_facet = data_extract_spec(
222
#'         dataname = "ADSL",
223
#'         select = select_spec(
224
#'           label = "Select variable:",
225
#'           choices = variable_choices(data[["ADSL"]], c("BMRKR2", "RACE", "REGION1")),
226
#'           selected = NULL,
227
#'           multiple = FALSE,
228
#'           fixed = FALSE
229
#'         )
230
#'       )
231
#'     )
232
#'   )
233
#' )
234
#' if (interactive()) {
235
#'   shinyApp(app$ui, app$server)
236
#' }
237
#'
238
#' @export
239
#'
240
tm_g_scatterplot <- function(label = "Scatterplot",
241
                             x,
242
                             y,
243
                             color_by = NULL,
244
                             size_by = NULL,
245
                             row_facet = NULL,
246
                             col_facet = NULL,
247
                             plot_height = c(600, 200, 2000),
248
                             plot_width = NULL,
249
                             alpha = c(1, 0, 1),
250
                             shape = shape_names,
251
                             size = c(5, 1, 15),
252
                             max_deg = 5L,
253
                             rotate_xaxis_labels = FALSE,
254
                             ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"),
255
                             pre_output = NULL,
256
                             post_output = NULL,
257
                             table_dec = 4,
258
                             ggplot2_args = teal.widgets::ggplot2_args(),
259
                             transformators = list(),
260
                             decorators = list()) {
261!
  message("Initializing tm_g_scatterplot")
262
263
  # Normalize the parameters
264!
  if (inherits(x, "data_extract_spec")) x <- list(x)
265!
  if (inherits(y, "data_extract_spec")) y <- list(y)
266!
  if (inherits(color_by, "data_extract_spec")) color_by <- list(color_by)
267!
  if (inherits(size_by, "data_extract_spec")) size_by <- list(size_by)
268!
  if (inherits(row_facet, "data_extract_spec")) row_facet <- list(row_facet)
269!
  if (inherits(col_facet, "data_extract_spec")) col_facet <- list(col_facet)
270!
  if (is.double(max_deg)) max_deg <- as.integer(max_deg)
271
272
  # Start of assertions
273!
  checkmate::assert_string(label)
274!
  checkmate::assert_list(x, types = "data_extract_spec")
275!
  checkmate::assert_list(y, types = "data_extract_spec")
276!
  checkmate::assert_list(color_by, types = "data_extract_spec", null.ok = TRUE)
277!
  checkmate::assert_list(size_by, types = "data_extract_spec", null.ok = TRUE)
278
279!
  checkmate::assert_list(row_facet, types = "data_extract_spec", null.ok = TRUE)
280!
  assert_single_selection(row_facet)
281
282!
  checkmate::assert_list(col_facet, types = "data_extract_spec", null.ok = TRUE)
283!
  assert_single_selection(col_facet)
284
285!
  checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE)
286!
  checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height")
287!
  checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE)
288!
  checkmate::assert_numeric(
289!
    plot_width[1],
290!
    lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width"
291
  )
292
293!
  if (length(alpha) == 1) {
294!
    checkmate::assert_numeric(alpha, any.missing = FALSE, finite = TRUE)
295
  } else {
296!
    checkmate::assert_numeric(alpha, len = 3, any.missing = FALSE, finite = TRUE)
297!
    checkmate::assert_numeric(alpha[1], lower = alpha[2], upper = alpha[3], .var.name = "alpha")
298
  }
299
300!
  checkmate::assert_character(shape)
301
302!
  if (length(size) == 1) {
303!
    checkmate::assert_numeric(size, any.missing = FALSE, finite = TRUE)
304
  } else {
305!
    checkmate::assert_numeric(size, len = 3, any.missing = FALSE, finite = TRUE)
306!
    checkmate::assert_numeric(size[1], lower = size[2], upper = size[3], .var.name = "size")
307
  }
308
309!
  checkmate::assert_int(max_deg, lower = 1L)
310!
  checkmate::assert_flag(rotate_xaxis_labels)
311!
  ggtheme <- match.arg(ggtheme)
312
313!
  checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
314!
  checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
315
316!
  checkmate::assert_scalar(table_dec)
317!
  checkmate::assert_class(ggplot2_args, "ggplot2_args")
318
319!
  assert_decorators(decorators, "plot")
320
321
  # End of assertions
322
323
  # Make UI args
324!
  args <- as.list(environment())
325
326!
  data_extract_list <- list(
327!
    x = x,
328!
    y = y,
329!
    color_by = color_by,
330!
    size_by = size_by,
331!
    row_facet = row_facet,
332!
    col_facet = col_facet
333
  )
334
335!
  ans <- module(
336!
    label = label,
337!
    server = srv_g_scatterplot,
338!
    ui = ui_g_scatterplot,
339!
    ui_args = args,
340!
    server_args = c(
341!
      data_extract_list,
342!
      list(
343!
        plot_height = plot_height,
344!
        plot_width = plot_width,
345!
        table_dec = table_dec,
346!
        ggplot2_args = ggplot2_args,
347!
        decorators = decorators
348
      )
349
    ),
350!
    transformators = transformators,
351!
    datanames = teal.transform::get_extract_datanames(data_extract_list)
352
  )
353!
  attr(ans, "teal_bookmarkable") <- TRUE
354!
  ans
355
}
356
357
# UI function for the scatterplot module
358
ui_g_scatterplot <- function(id, ...) {
359!
  args <- list(...)
360!
  ns <- NS(id)
361!
  is_single_dataset_value <- teal.transform::is_single_dataset(
362!
    args$x, args$y, args$color_by, args$size_by, args$row_facet, args$col_facet
363
  )
364
365!
  tagList(
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$br(),
370!
        tags$h1(tags$strong("Selected points:"), style = "font-size: 150%;"),
371!
        teal.widgets::get_dt_rows(ns("data_table"), ns("data_table_rows")),
372!
        DT::dataTableOutput(ns("data_table"), width = "100%")
373
      ),
374!
      encoding = tags$div(
375!
        tags$label("Encodings", class = "text-primary"),
376!
        teal.transform::datanames_input(args[c("x", "y", "color_by", "size_by", "row_facet", "col_facet")]),
377!
        teal.transform::data_extract_ui(
378!
          id = ns("x"),
379!
          label = "X variable",
380!
          data_extract_spec = args$x,
381!
          is_single_dataset = is_single_dataset_value
382
        ),
383!
        checkboxInput(ns("log_x"), "Use log transformation", value = FALSE),
384!
        conditionalPanel(
385!
          condition = paste0("input['", ns("log_x"), "'] == true"),
386!
          radioButtons(
387!
            ns("log_x_base"),
388!
            label = NULL,
389!
            inline = TRUE,
390!
            choices = c("Natural" = "log", "Base 10" = "log10", "Base 2" = "log2")
391
          )
392
        ),
393!
        teal.transform::data_extract_ui(
394!
          id = ns("y"),
395!
          label = "Y variable",
396!
          data_extract_spec = args$y,
397!
          is_single_dataset = is_single_dataset_value
398
        ),
399!
        checkboxInput(ns("log_y"), "Use log transformation", value = FALSE),
400!
        conditionalPanel(
401!
          condition = paste0("input['", ns("log_y"), "'] == true"),
402!
          radioButtons(
403!
            ns("log_y_base"),
404!
            label = NULL,
405!
            inline = TRUE,
406!
            choices = c("Natural" = "log", "Base 10" = "log10", "Base 2" = "log2")
407
          )
408
        ),
409!
        if (!is.null(args$color_by)) {
410!
          teal.transform::data_extract_ui(
411!
            id = ns("color_by"),
412!
            label = "Color by variable",
413!
            data_extract_spec = args$color_by,
414!
            is_single_dataset = is_single_dataset_value
415
          )
416
        },
417!
        if (!is.null(args$size_by)) {
418!
          teal.transform::data_extract_ui(
419!
            id = ns("size_by"),
420!
            label = "Size by variable",
421!
            data_extract_spec = args$size_by,
422!
            is_single_dataset = is_single_dataset_value
423
          )
424
        },
425!
        if (!is.null(args$row_facet)) {
426!
          teal.transform::data_extract_ui(
427!
            id = ns("row_facet"),
428!
            label = "Row facetting",
429!
            data_extract_spec = args$row_facet,
430!
            is_single_dataset = is_single_dataset_value
431
          )
432
        },
433!
        if (!is.null(args$col_facet)) {
434!
          teal.transform::data_extract_ui(
435!
            id = ns("col_facet"),
436!
            label = "Column facetting",
437!
            data_extract_spec = args$col_facet,
438!
            is_single_dataset = is_single_dataset_value
439
          )
440
        },
441!
        ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")),
442!
        bslib::accordion(
443!
          open = TRUE,
444!
          bslib::accordion_panel(
445!
            title = "Plot settings",
446!
            teal.widgets::optionalSliderInputValMinMax(ns("alpha"), "Opacity:", args$alpha, ticks = FALSE),
447!
            teal.widgets::optionalSelectInput(
448!
              inputId = ns("shape"),
449!
              label = "Points shape:",
450!
              choices = args$shape,
451!
              selected = args$shape[1],
452!
              multiple = FALSE
453
            ),
454!
            colourpicker::colourInput(ns("color"), "Points color:", "black"),
455!
            teal.widgets::optionalSliderInputValMinMax(ns("size"), "Points size:", args$size, ticks = FALSE, step = .1),
456!
            checkboxInput(ns("rotate_xaxis_labels"), "Rotate X axis labels", value = args$rotate_xaxis_labels),
457!
            checkboxInput(ns("add_density"), "Add marginal density", value = FALSE),
458!
            checkboxInput(ns("rug_plot"), "Include rug plot", value = FALSE),
459!
            checkboxInput(ns("show_count"), "Show N (number of observations)", value = FALSE),
460!
            shinyjs::hidden(helpText(id = ns("line_msg"), "Trendline needs numeric X and Y variables")),
461!
            teal.widgets::optionalSelectInput(ns("smoothing_degree"), "Smoothing degree", seq_len(args$max_deg)),
462!
            shinyjs::hidden(teal.widgets::optionalSelectInput(ns("color_sub"), label = "", multiple = TRUE)),
463!
            teal.widgets::optionalSliderInputValMinMax(ns("ci"), "Confidence", c(.95, .8, .99), ticks = FALSE),
464!
            shinyjs::hidden(checkboxInput(ns("show_form"), "Show formula", value = TRUE)),
465!
            shinyjs::hidden(checkboxInput(ns("show_r2"), "Show adj-R Squared", value = TRUE)),
466!
            uiOutput(ns("num_na_removed")),
467!
            tags$div(
468!
              id = ns("label_pos"),
469!
              tags$div(tags$strong("Stats position")),
470!
              tags$div(style = "display: inline-block; width: 70%;", helpText("Left")),
471!
              tags$div(
472!
                style = "display: inline-block; width: 70%;",
473!
                teal.widgets::optionalSliderInput(
474!
                  ns("pos"),
475!
                  label = NULL,
476!
                  min = 0, max = 1, value = .99, ticks = FALSE, step = .01
477
                )
478
              ),
479!
              tags$div(style = "display: inline-block; width: 10%;", helpText("Right"))
480
            ),
481!
            teal.widgets::optionalSliderInput(
482!
              ns("label_size"), "Stats font size",
483!
              min = 3, max = 10, value = 5, ticks = FALSE, step = .1
484
            ),
485!
            if (!is.null(args$row_facet) || !is.null(args$col_facet)) {
486!
              checkboxInput(ns("free_scales"), "Free scales", value = FALSE)
487
            },
488!
            selectInput(
489!
              inputId = ns("ggtheme"),
490!
              label = "Theme (by ggplot):",
491!
              choices = ggplot_themes,
492!
              selected = args$ggtheme,
493!
              multiple = FALSE
494
            )
495
          )
496
        )
497
      ),
498!
      pre_output = args$pre_output,
499!
      post_output = args$post_output
500
    )
501
  )
502
}
503
504
# Server function for the scatterplot module
505
srv_g_scatterplot <- function(id,
506
                              data,
507
                              x,
508
                              y,
509
                              color_by,
510
                              size_by,
511
                              row_facet,
512
                              col_facet,
513
                              plot_height,
514
                              plot_width,
515
                              table_dec,
516
                              ggplot2_args,
517
                              decorators) {
518!
  checkmate::assert_class(data, "reactive")
519!
  checkmate::assert_class(isolate(data()), "teal_data")
520!
  moduleServer(id, function(input, output, session) {
521!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
522
523!
    data_extract <- list(
524!
      x = x,
525!
      y = y,
526!
      color_by = color_by,
527!
      size_by = size_by,
528!
      row_facet = row_facet,
529!
      col_facet = col_facet
530
    )
531
532!
    rule_diff <- function(other) {
533!
      function(value) {
534!
        othervalue <- selector_list()[[other]]()[["select"]]
535!
        if (!is.null(othervalue)) {
536!
          if (identical(value, othervalue)) {
537!
            "Row and column facetting variables must be different."
538
          }
539
        }
540
      }
541
    }
542
543!
    selector_list <- teal.transform::data_extract_multiple_srv(
544!
      data_extract = data_extract,
545!
      datasets = data,
546!
      select_validation_rule = list(
547!
        x = ~ if (length(.) != 1) "Please select exactly one x var.",
548!
        y = ~ if (length(.) != 1) "Please select exactly one y var.",
549!
        color_by = ~ if (length(.) > 1) "There cannot be more than 1 color variable.",
550!
        size_by = ~ if (length(.) > 1) "There cannot be more than 1 size variable.",
551!
        row_facet = shinyvalidate::compose_rules(
552!
          shinyvalidate::sv_optional(),
553!
          rule_diff("col_facet")
554
        ),
555!
        col_facet = shinyvalidate::compose_rules(
556!
          shinyvalidate::sv_optional(),
557!
          rule_diff("row_facet")
558
        )
559
      )
560
    )
561
562!
    iv_r <- reactive({
563!
      iv_facet <- shinyvalidate::InputValidator$new()
564!
      iv <- shinyvalidate::InputValidator$new()
565!
      teal.transform::compose_and_enable_validators(iv, selector_list)
566
    })
567!
    iv_facet <- shinyvalidate::InputValidator$new()
568!
    iv_facet$add_rule("add_density", ~ if (
569!
      isTRUE(.) &&
570
        (
571!
          length(selector_list()$row_facet()$select) > 0L ||
572!
            length(selector_list()$col_facet()$select) > 0L
573
        )
574
    ) {
575!
      "Cannot add marginal density when Row or Column facetting has been selected"
576
    })
577!
    iv_facet$enable()
578
579!
    anl_merged_input <- teal.transform::merge_expression_srv(
580!
      selector_list = selector_list,
581!
      datasets = data,
582!
      merge_function = "dplyr::inner_join"
583
    )
584!
    qenv <- reactive({
585!
      obj <- data()
586!
      teal.reporter::teal_card(obj) <- c(
587!
        teal.reporter::teal_card(obj),
588!
        teal.reporter::teal_card("## Module's output(s)")
589
      )
590!
      teal.code::eval_code(data(), "library(ggplot2);library(dplyr)")
591
    })
592
593!
    anl_merged_q <- reactive({
594!
      req(anl_merged_input())
595!
      qenv() %>%
596!
        teal.code::eval_code(as.expression(anl_merged_input()$expr))
597
    })
598
599!
    merged <- list(
600!
      anl_input_r = anl_merged_input,
601!
      anl_q_r = anl_merged_q
602
    )
603
604!
    trend_line_is_applicable <- reactive({
605!
      ANL <- merged$anl_q_r()[["ANL"]]
606!
      x_var <- as.vector(merged$anl_input_r()$columns_source$x)
607!
      y_var <- as.vector(merged$anl_input_r()$columns_source$y)
608!
      length(x_var) > 0 && length(y_var) > 0 && is.numeric(ANL[[x_var]]) && is.numeric(ANL[[y_var]])
609
    })
610
611!
    add_trend_line <- reactive({
612!
      smoothing_degree <- as.integer(input$smoothing_degree)
613!
      trend_line_is_applicable() && length(smoothing_degree) > 0
614
    })
615
616!
    if (!is.null(color_by)) {
617!
      observeEvent(
618!
        eventExpr = merged$anl_input_r()$columns_source$color_by,
619!
        handlerExpr = {
620!
          color_by_var <- as.vector(merged$anl_input_r()$columns_source$color_by)
621!
          if (length(color_by_var) > 0) {
622!
            shinyjs::hide("color")
623
          } else {
624!
            shinyjs::show("color")
625
          }
626
        }
627
      )
628
    }
629
630!
    output$num_na_removed <- renderUI({
631!
      if (add_trend_line()) {
632!
        ANL <- merged$anl_q_r()[["ANL"]]
633!
        x_var <- as.vector(merged$anl_input_r()$columns_source$x)
634!
        y_var <- as.vector(merged$anl_input_r()$columns_source$y)
635!
        if ((num_total_na <- nrow(ANL) - nrow(stats::na.omit(ANL[, c(x_var, y_var)]))) > 0) {
636!
          tags$div(paste(num_total_na, "row(s) with missing values were removed"), tags$hr())
637
        }
638
      }
639
    })
640
641!
    observeEvent(
642!
      eventExpr = merged$anl_input_r()$columns_source[c("col_facet", "row_facet")],
643!
      handlerExpr = {
644!
        if (
645!
          length(merged$anl_input_r()$columns_source$col_facet) == 0 &&
646!
            length(merged$anl_input_r()$columns_source$row_facet) == 0
647
        ) {
648!
          shinyjs::hide("free_scales")
649
        } else {
650!
          shinyjs::show("free_scales")
651
        }
652
      }
653
    )
654
655!
    output_q <- reactive({
656!
      teal::validate_inputs(iv_r(), iv_facet)
657
658!
      ANL <- merged$anl_q_r()[["ANL"]]
659
660!
      x_var <- as.vector(merged$anl_input_r()$columns_source$x)
661!
      y_var <- as.vector(merged$anl_input_r()$columns_source$y)
662!
      color_by_var <- as.vector(merged$anl_input_r()$columns_source$color_by)
663!
      size_by_var <- as.vector(merged$anl_input_r()$columns_source$size_by)
664!
      row_facet_name <- if (length(merged$anl_input_r()$columns_source$row_facet) == 0) {
665!
        character(0)
666
      } else {
667!
        as.vector(merged$anl_input_r()$columns_source$row_facet)
668
      }
669!
      col_facet_name <- if (length(merged$anl_input_r()$columns_source$col_facet) == 0) {
670!
        character(0)
671
      } else {
672!
        as.vector(merged$anl_input_r()$columns_source$col_facet)
673
      }
674!
      alpha <- input$alpha
675!
      size <- input$size
676!
      rotate_xaxis_labels <- input$rotate_xaxis_labels
677!
      add_density <- input$add_density
678!
      ggtheme <- input$ggtheme
679!
      rug_plot <- input$rug_plot
680!
      color <- input$color
681!
      shape <- `if`(is.null(input$shape) || identical(input$shape, ""), "circle", input$shape)
682!
      smoothing_degree <- as.integer(input$smoothing_degree)
683!
      ci <- input$ci
684
685!
      log_x <- input$log_x
686!
      log_y <- input$log_y
687
688!
      validate(need(
689!
        length(row_facet_name) == 0 || inherits(ANL[[row_facet_name]], c("character", "factor", "Date", "integer")),
690!
        "`Row facetting` variable must be of class `character`, `factor`, `Date`, or `integer`"
691
      ))
692!
      validate(need(
693!
        length(col_facet_name) == 0 || inherits(ANL[[col_facet_name]], c("character", "factor", "Date", "integer")),
694!
        "`Column facetting` variable must be of class `character`, `factor`, `Date`, or `integer`"
695
      ))
696
697!
      if (add_density && length(color_by_var) > 0) {
698!
        validate(need(
699!
          !is.numeric(ANL[[color_by_var]]),
700!
          "Marginal plots cannot be produced when the points are colored by numeric variables.
701!
        \n Uncheck the 'Add marginal density' checkbox to display the plot."
702
        ))
703!
        validate(need(
704
          !(
705!
            inherits(ANL[[color_by_var]], "Date") ||
706!
              inherits(ANL[[color_by_var]], "POSIXct") ||
707!
              inherits(ANL[[color_by_var]], "POSIXlt")
708
          ),
709!
          "Marginal plots cannot be produced when the points are colored by Date or POSIX variables.
710!
        \n Uncheck the 'Add marginal density' checkbox to display the plot."
711
        ))
712
      }
713
714!
      teal::validate_has_data(ANL[, c(x_var, y_var)], 1, complete = TRUE, allow_inf = FALSE)
715
716!
      if (log_x) {
717!
        validate(
718!
          need(
719!
            is.numeric(ANL[[x_var]]) && all(
720!
              ANL[[x_var]] > 0 | is.na(ANL[[x_var]])
721
            ),
722!
            "X variable can only be log transformed if variable is numeric and all values are positive."
723
          )
724
        )
725
      }
726!
      if (log_y) {
727!
        validate(
728!
          need(
729!
            is.numeric(ANL[[y_var]]) && all(
730!
              ANL[[y_var]] > 0 | is.na(ANL[[y_var]])
731
            ),
732!
            "Y variable can only be log transformed if variable is numeric and all values are positive."
733
          )
734
        )
735
      }
736
737!
      facet_cl <- facet_ggplot_call(
738!
        row_facet_name,
739!
        col_facet_name,
740!
        free_x_scales = isTRUE(input$free_scales),
741!
        free_y_scales = isTRUE(input$free_scales)
742
      )
743
744!
      point_sizes <- if (length(size_by_var) > 0) {
745!
        validate(need(is.numeric(ANL[[size_by_var]]), "Variable to size by must be numeric"))
746!
        substitute(
747!
          expr = size * ANL[[size_by_var]] / max(ANL[[size_by_var]], na.rm = TRUE),
748!
          env = list(size = size, size_by_var = size_by_var)
749
        )
750
      } else {
751!
        size
752
      }
753
754!
      plot_q <- merged$anl_q_r()
755
756!
      if (log_x) {
757!
        log_x_fn <- input$log_x_base
758!
        plot_q <- teal.code::eval_code(
759!
          object = plot_q,
760!
          code = substitute(
761!
            expr = ANL[, log_x_var] <- log_x_fn(ANL[, x_var]),
762!
            env = list(
763!
              x_var = x_var,
764!
              log_x_fn = as.name(log_x_fn),
765!
              log_x_var = paste0(log_x_fn, "_", x_var)
766
            )
767
          )
768
        )
769
      }
770
771!
      if (log_y) {
772!
        log_y_fn <- input$log_y_base
773!
        plot_q <- teal.code::eval_code(
774!
          object = plot_q,
775!
          code = substitute(
776!
            expr = ANL[, log_y_var] <- log_y_fn(ANL[, y_var]),
777!
            env = list(
778!
              y_var = y_var,
779!
              log_y_fn = as.name(log_y_fn),
780!
              log_y_var = paste0(log_y_fn, "_", y_var)
781
            )
782
          )
783
        )
784
      }
785
786!
      pre_pro_anl <- if (input$show_count) {
787!
        paste0(
788!
          "ANL %>% dplyr::group_by(",
789!
          paste(
790!
            c(
791!
              if (length(color_by_var) > 0 && inherits(ANL[[color_by_var]], c("factor", "character"))) color_by_var,
792!
              row_facet_name,
793!
              col_facet_name
794
            ),
795!
            collapse = ", "
796
          ),
797!
          ") %>% dplyr::mutate(n = dplyr::n()) %>% dplyr::ungroup()"
798
        )
799
      } else {
800!
        "ANL"
801
      }
802
803!
      plot_call <- substitute(expr = pre_pro_anl %>% ggplot2::ggplot(), env = list(pre_pro_anl = str2lang(pre_pro_anl)))
804
805!
      plot_call <- if (length(color_by_var) == 0) {
806!
        substitute(
807!
          expr = plot_call +
808!
            ggplot2::aes(x = x_name, y = y_name) +
809!
            ggplot2::geom_point(alpha = alpha_value, size = point_sizes, shape = shape_value, color = color_value),
810!
          env = list(
811!
            plot_call = plot_call,
812!
            x_name = if (log_x) as.name(paste0(log_x_fn, "_", x_var)) else as.name(x_var),
813!
            y_name = if (log_y) as.name(paste0(log_y_fn, "_", y_var)) else as.name(y_var),
814!
            alpha_value = alpha,
815!
            point_sizes = point_sizes,
816!
            shape_value = shape,
817!
            color_value = color
818
          )
819
        )
820
      } else {
821!
        substitute(
822!
          expr = plot_call +
823!
            ggplot2::aes(x = x_name, y = y_name, color = color_by_var_name) +
824!
            ggplot2::geom_point(alpha = alpha_value, size = point_sizes, shape = shape_value),
825!
          env = list(
826!
            plot_call = plot_call,
827!
            x_name = if (log_x) as.name(paste0(log_x_fn, "_", x_var)) else as.name(x_var),
828!
            y_name = if (log_y) as.name(paste0(log_y_fn, "_", y_var)) else as.name(y_var),
829!
            color_by_var_name = as.name(color_by_var),
830!
            alpha_value = alpha,
831!
            point_sizes = point_sizes,
832!
            shape_value = shape
833
          )
834
        )
835
      }
836
837!
      if (rug_plot) plot_call <- substitute(expr = plot_call + geom_rug(), env = list(plot_call = plot_call))
838
839!
      plot_label_generator <- function(rhs_formula = quote(y ~ 1),
840!
                                       show_form = input$show_form,
841!
                                       show_r2 = input$show_r2,
842!
                                       show_count = input$show_count,
843!
                                       pos = input$pos,
844!
                                       label_size = input$label_size) {
845!
        stopifnot(sum(show_form, show_r2, show_count) >= 1)
846!
        aes_label <- paste0(
847!
          "aes(",
848!
          if (show_count) "n = n, ",
849!
          "label = ",
850!
          if (sum(show_form, show_r2, show_count) > 1) "paste(",
851!
          paste(
852!
            c(
853!
              if (show_form) "stat(eq.label)",
854!
              if (show_r2) "stat(adj.rr.label)",
855!
              if (show_count) "paste('N ~`=`~', n)"
856
            ),
857!
            collapse = ", "
858
          ),
859!
          if (sum(show_form, show_r2, show_count) > 1) ", sep = '*\", \"*'))" else ")"
860
        )
861!
        label_geom <- substitute(
862!
          expr = ggpmisc::stat_poly_eq(
863!
            mapping = aes_label,
864!
            formula = rhs_formula,
865!
            parse = TRUE,
866!
            label.x = pos,
867!
            size = label_size
868
          ),
869!
          env = list(
870!
            rhs_formula = rhs_formula,
871!
            pos = pos,
872!
            aes_label = str2lang(aes_label),
873!
            label_size = label_size
874
          )
875
        )
876!
        substitute(
877!
          expr = plot_call + label_geom,
878!
          env = list(
879!
            plot_call = plot_call,
880!
            label_geom = label_geom
881
          )
882
        )
883
      }
884
885!
      if (trend_line_is_applicable()) {
886!
        shinyjs::hide("line_msg")
887!
        shinyjs::show("smoothing_degree")
888!
        if (!add_trend_line()) {
889!
          shinyjs::hide("ci")
890!
          shinyjs::hide("color_sub")
891!
          shinyjs::hide("show_form")
892!
          shinyjs::hide("show_r2")
893!
          if (input$show_count) {
894!
            plot_call <- plot_label_generator(show_form = FALSE, show_r2 = FALSE)
895!
            shinyjs::show("label_pos")
896!
            shinyjs::show("label_size")
897
          } else {
898!
            shinyjs::hide("label_pos")
899!
            shinyjs::hide("label_size")
900
          }
901
        } else {
902!
          shinyjs::show("ci")
903!
          shinyjs::show("show_form")
904!
          shinyjs::show("show_r2")
905!
          if (nrow(ANL) - nrow(stats::na.omit(ANL[, c(x_var, y_var)])) > 0) {
906!
            plot_q <- teal.code::eval_code(
907!
              plot_q,
908!
              substitute(
909!
                expr = ANL <- dplyr::filter(ANL, !is.na(x_var) & !is.na(y_var)),
910!
                env = list(x_var = as.name(x_var), y_var = as.name(y_var))
911
              )
912
            )
913
          }
914!
          rhs_formula <- substitute(
915!
            expr = y ~ poly(x, smoothing_degree, raw = TRUE),
916!
            env = list(smoothing_degree = smoothing_degree)
917
          )
918!
          if (input$show_form || input$show_r2 || input$show_count) {
919!
            plot_call <- plot_label_generator(rhs_formula = rhs_formula)
920!
            shinyjs::show("label_pos")
921!
            shinyjs::show("label_size")
922
          } else {
923!
            shinyjs::hide("label_pos")
924!
            shinyjs::hide("label_size")
925
          }
926!
          plot_call <- substitute(
927!
            expr = plot_call + ggplot2::geom_smooth(formula = rhs_formula, se = TRUE, level = ci, method = "lm"),
928!
            env = list(plot_call = plot_call, rhs_formula = rhs_formula, ci = ci)
929
          )
930
        }
931
      } else {
932!
        shinyjs::hide("smoothing_degree")
933!
        shinyjs::hide("ci")
934!
        shinyjs::hide("color_sub")
935!
        shinyjs::hide("show_form")
936!
        shinyjs::hide("show_r2")
937!
        if (input$show_count) {
938!
          plot_call <- plot_label_generator(show_form = FALSE, show_r2 = FALSE)
939!
          shinyjs::show("label_pos")
940!
          shinyjs::show("label_size")
941
        } else {
942!
          shinyjs::hide("label_pos")
943!
          shinyjs::hide("label_size")
944
        }
945!
        shinyjs::show("line_msg")
946
      }
947
948!
      if (!is.null(facet_cl)) {
949!
        plot_call <- substitute(expr = plot_call + facet_cl, env = list(plot_call = plot_call, facet_cl = facet_cl))
950
      }
951
952!
      y_label <- varname_w_label(
953!
        y_var,
954!
        ANL,
955!
        prefix = if (log_y) paste(log_y_fn, "(") else NULL,
956!
        suffix = if (log_y) ")" else NULL
957
      )
958!
      x_label <- varname_w_label(
959!
        x_var,
960!
        ANL,
961!
        prefix = if (log_x) paste(log_x_fn, "(") else NULL,
962!
        suffix = if (log_x) ")" else NULL
963
      )
964
965!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
966!
        labs = list(y = y_label, x = x_label),
967!
        theme = list(legend.position = "bottom")
968
      )
969
970!
      if (rotate_xaxis_labels) {
971!
        dev_ggplot2_args$theme[["axis.text.x"]] <- quote(ggplot2::element_text(angle = 45, hjust = 1))
972
      }
973
974!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
975!
        user_plot = ggplot2_args,
976!
        module_plot = dev_ggplot2_args
977
      )
978
979!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(all_ggplot2_args, ggtheme = ggtheme)
980
981
982!
      if (add_density) {
983!
        plot_call <- substitute(
984!
          expr = ggExtra::ggMarginal(
985!
            plot_call + labs + ggthemes + themes,
986!
            type = "density",
987!
            groupColour = group_colour
988
          ),
989!
          env = list(
990!
            plot_call = plot_call,
991!
            group_colour = if (length(color_by_var) > 0) TRUE else FALSE,
992!
            labs = parsed_ggplot2_args$labs,
993!
            ggthemes = parsed_ggplot2_args$ggtheme,
994!
            themes = parsed_ggplot2_args$theme
995
          )
996
        )
997
      } else {
998!
        plot_call <- substitute(
999!
          expr = plot_call +
1000!
            labs +
1001!
            ggthemes +
1002!
            themes,
1003!
          env = list(
1004!
            plot_call = plot_call,
1005!
            labs = parsed_ggplot2_args$labs,
1006!
            ggthemes = parsed_ggplot2_args$ggtheme,
1007!
            themes = parsed_ggplot2_args$theme
1008
          )
1009
        )
1010
      }
1011
1012!
      plot_call <- substitute(expr = plot <- plot_call, env = list(plot_call = plot_call))
1013
1014!
      teal.reporter::teal_card(plot_q) <- c(teal.reporter::teal_card(plot_q), "### Plot")
1015!
      teal.code::eval_code(plot_q, plot_call)
1016
    })
1017
1018!
    decorated_output_plot_q <- srv_decorate_teal_data(
1019!
      id = "decorator",
1020!
      data = output_q,
1021!
      decorators = select_decorators(decorators, "plot"),
1022!
      expr = quote(plot)
1023
    )
1024
1025!
    plot_r <- reactive(req(decorated_output_plot_q())[["plot"]])
1026
1027
    # Insert the plot into a plot_with_settings module from teal.widgets
1028!
    pws <- teal.widgets::plot_with_settings_srv(
1029!
      id = "scatter_plot",
1030!
      plot_r = plot_r,
1031!
      height = plot_height,
1032!
      width = plot_width,
1033!
      brushing = TRUE
1034
    )
1035
1036!
    output$data_table <- DT::renderDataTable({
1037!
      plot_brush <- pws$brush()
1038
1039!
      if (!is.null(plot_brush)) {
1040!
        validate(need(!input$add_density, "Brushing feature is currently not supported when plot has marginal density"))
1041
      }
1042
1043!
      merged_data <- isolate(output_q()[["ANL"]])
1044
1045!
      brushed_df <- teal.widgets::clean_brushedPoints(merged_data, plot_brush)
1046!
      numeric_cols <- names(brushed_df)[
1047!
        vapply(brushed_df, function(x) is.numeric(x) && !is.integer(x), FUN.VALUE = logical(1))
1048
      ]
1049
1050!
      if (length(numeric_cols) > 0) {
1051!
        DT::formatRound(
1052!
          DT::datatable(brushed_df,
1053!
            rownames = FALSE,
1054!
            options = list(scrollX = TRUE, pageLength = input$data_table_rows)
1055
          ),
1056!
          numeric_cols,
1057!
          table_dec
1058
        )
1059
      } else {
1060!
        DT::datatable(brushed_df, rownames = FALSE, options = list(scrollX = TRUE, pageLength = input$data_table_rows))
1061
      }
1062
    })
1063
1064!
    set_chunk_dims(pws, decorated_output_plot_q)
1065
  })
1066
}
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 = file.path(
35
#'           "https://www.fda.gov/files/drugs/published",
36
#'           "Portable-Document-Format-Specifications.pdf"
37
#'         )
38
#'       )
39
#'     )
40
#'   )
41
#' )
42
#' if (interactive()) {
43
#'   shinyApp(app$ui, app$server)
44
#' }
45
#'
46
#' @export
47
#'
48
tm_file_viewer <- function(label = "File Viewer Module",
49
                           input_path = list("Current Working Directory" = ".")) {
50!
  message("Initializing tm_file_viewer")
51
52
  # Normalize the parameters
53!
  if (length(label) == 0 || identical(label, "")) label <- " "
54!
  if (length(input_path) == 0 || identical(input_path, "")) input_path <- list()
55
56
  # Start of assertions
57!
  checkmate::assert_string(label)
58
59!
  checkmate::assert(
60!
    checkmate::check_list(input_path, types = "character", min.len = 0),
61!
    checkmate::check_character(input_path, min.len = 1)
62
  )
63!
  if (length(input_path) > 0) {
64!
    valid_url <- function(url_input, timeout = 2) {
65!
      con <- try(url(url_input), silent = TRUE)
66!
      check <- suppressWarnings(try(open.connection(con, open = "rt", timeout = timeout), silent = TRUE)[1])
67!
      try(close.connection(con), silent = TRUE)
68!
      is.null(check)
69
    }
70!
    idx <- vapply(input_path, function(x) file.exists(x) || valid_url(x), logical(1))
71
72!
    if (!all(idx)) {
73!
      warning(
74!
        paste0(
75!
          "Non-existent file or url path. Please provide valid paths for:\n",
76!
          paste0(input_path[!idx], collapse = "\n")
77
        )
78
      )
79
    }
80!
    input_path <- input_path[idx]
81
  } else {
82!
    warning(
83!
      "No file or url paths were provided."
84
    )
85
  }
86
  # End of assertions
87
88
  # Make UI args
89!
  args <- as.list(environment())
90
91!
  ans <- module(
92!
    label = label,
93!
    server = srv_viewer,
94!
    server_args = list(input_path = input_path),
95!
    ui = ui_viewer,
96!
    ui_args = args,
97!
    datanames = NULL
98
  )
99!
  attr(ans, "teal_bookmarkable") <- FALSE
100!
  ans
101
}
102
103
# UI function for the file viewer module
104
ui_viewer <- function(id, ...) {
105!
  args <- list(...)
106!
  ns <- NS(id)
107
108!
  tagList(
109!
    teal.widgets::standard_layout(
110!
      output = tags$div(
111!
        uiOutput(ns("output"))
112
      ),
113!
      encoding = tags$div(
114!
        style = "overflow-y: hidden; overflow-x: auto;",
115!
        tags$label("Encodings", class = "text-primary"),
116!
        shinyTree::shinyTree(
117!
          ns("tree"),
118!
          dragAndDrop = FALSE,
119!
          sort = FALSE,
120!
          theme = "proton",
121!
          multiple = FALSE,
122!
          search = TRUE
123
        )
124
      )
125
    )
126
  )
127
}
128
129
# Server function for the file viewer module
130
srv_viewer <- function(id, input_path) {
131!
  moduleServer(id, function(input, output, session) {
132!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
133
134!
    temp_dir <- tempfile()
135!
    if (!dir.exists(temp_dir)) {
136!
      dir.create(temp_dir, recursive = TRUE)
137
    }
138!
    addResourcePath(basename(temp_dir), temp_dir)
139
140!
    test_path_text <- function(selected_path, type) {
141!
      out <- tryCatch(
142!
        expr = {
143!
          if (type != "url") {
144!
            selected_path <- normalizePath(selected_path, winslash = "/")
145
          }
146!
          readLines(con = selected_path)
147
        },
148!
        error = function(cond) FALSE,
149!
        warning = function(cond) {
150!
          `if`(grepl("^incomplete final line found on", cond[[1]]), suppressWarnings(eval(cond[[2]])), FALSE)
151
        }
152
      )
153
    }
154
155!
    handle_connection_type <- function(selected_path) {
156!
      file_extension <- tools::file_ext(selected_path)
157!
      file_class <- suppressWarnings(file(selected_path))
158!
      close(file_class)
159
160!
      output_text <- test_path_text(selected_path, type = class(file_class)[1])
161
162!
      if (class(file_class)[1] == "url") {
163!
        list(selected_path = selected_path, output_text = output_text)
164
      } else {
165!
        file.copy(normalizePath(selected_path, winslash = "/"), temp_dir)
166!
        selected_path <- file.path(basename(temp_dir), basename(selected_path))
167!
        list(selected_path = selected_path, output_text = output_text)
168
      }
169
    }
170
171!
    display_file <- function(selected_path) {
172!
      con_type <- handle_connection_type(selected_path)
173!
      file_extension <- tools::file_ext(selected_path)
174!
      if (file_extension %in% c("png", "apng", "jpg", "jpeg", "svg", "gif", "webp", "bmp")) {
175!
        tags$img(src = con_type$selected_path, alt = "file does not exist")
176!
      } else if (file_extension == "pdf") {
177!
        tags$embed(
178!
          style = "height: 600px; width: 100%;",
179!
          src = con_type$selected_path
180
        )
181!
      } else if (!isFALSE(con_type$output_text[1])) {
182!
        tags$pre(paste0(con_type$output_text, collapse = "\n"))
183
      } else {
184!
        tags$p("Please select a supported format.")
185
      }
186
    }
187
188!
    tree_list <- function(file_or_dir) {
189!
      nested_list <- lapply(file_or_dir, function(path) {
190!
        file_class <- suppressWarnings(file(path))
191!
        close(file_class)
192!
        if (class(file_class)[[1]] != "url") {
193!
          isdir <- file.info(path)$isdir
194!
          if (!isdir) {
195!
            structure(path, ancestry = path, sticon = "file")
196
          } else {
197!
            files <- list.files(path, full.names = TRUE, include.dirs = TRUE)
198!
            out <- lapply(files, function(x) tree_list(x))
199!
            out <- unlist(out, recursive = FALSE)
200!
            if (length(files) > 0) names(out) <- basename(files)
201!
            out
202
          }
203
        } else {
204!
          structure(path, ancestry = path, sticon = "file")
205
        }
206
      })
207
208!
      missing_labels <- if (is.null(names(nested_list))) seq_along(nested_list) else which(names(nested_list) == "")
209!
      names(nested_list)[missing_labels] <- file_or_dir[missing_labels]
210!
      nested_list
211
    }
212
213!
    output$tree <- shinyTree::renderTree({
214!
      if (length(input_path) > 0) {
215!
        tree_list(input_path)
216
      } else {
217!
        list("Empty Path" = NULL)
218
      }
219
    })
220
221!
    output$output <- renderUI({
222!
      validate(
223!
        need(
224!
          length(shinyTree::get_selected(input$tree)) > 0,
225!
          "Please select a file."
226
        )
227
      )
228
229!
      obj <- shinyTree::get_selected(input$tree, format = "names")[[1]]
230!
      repo <- attr(obj, "ancestry")
231!
      repo_collapsed <- if (length(repo) > 1) paste0(repo, collapse = "/") else repo
232!
      is_not_named <- file.exists(file.path(c(repo_collapsed, obj[1])))[1]
233
234!
      if (is_not_named) {
235!
        selected_path <- do.call("file.path", as.list(c(repo, obj[1])))
236
      } else {
237!
        if (length(repo) == 0) {
238!
          selected_path <- do.call("file.path", as.list(attr(input$tree[[obj[1]]], "ancestry")))
239
        } else {
240!
          selected_path <- do.call("file.path", as.list(attr(input$tree[[repo]][[obj[1]]], "ancestry")))
241
        }
242
      }
243
244!
      validate(
245!
        need(
246!
          !isTRUE(file.info(selected_path)$isdir) && length(selected_path) > 0,
247!
          "Please select a single file."
248
        )
249
      )
250!
      display_file(selected_path)
251
    })
252
253!
    onStop(function() {
254!
      removeResourcePath(basename(temp_dir))
255!
      unlink(temp_dir)
256
    })
257
  })
258
}
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
#'
25
#' A Decorator is applied to the specific output using a named list of `teal_transform_module` objects.
26
#' The name of this list corresponds to the name of the output to which the decorator is applied.
27
#' See code snippet below:
28
#'
29
#' ```
30
#' tm_outliers(
31
#'    ..., # arguments for module
32
#'    decorators = list(
33
#'      box_plot = teal_transform_module(...), # applied only to `box_plot` output
34
#'      density_plot = teal_transform_module(...), # applied only to `density_plot` output
35
#'      cumulative_plot = teal_transform_module(...) # applied only to `cumulative_plot` output
36
#'    )
37
#' )
38
#' ```
39
#'
40
#' For additional details and examples of decorators, refer to the vignette
41
#' `vignette("decorate-module-output", package = "teal.modules.general")`.
42
#'
43
#' To learn more please refer to the vignette
44
#' `vignette("transform-module-output", package = "teal")` or the [`teal::teal_transform_module()`] documentation.
45
#'
46
#' @inheritSection teal::example_module Reporting
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
#' app <- init(
116
#'   data = data,
117
#'   modules = modules(
118
#'     tm_outliers(
119
#'       outlier_var = list(
120
#'         data_extract_spec(
121
#'           dataname = "ADSL",
122
#'           select = select_spec(
123
#'             label = "Select variable:",
124
#'             choices = variable_choices(data[["ADSL"]], c("AGE", "BMRKR1")),
125
#'             selected = "AGE",
126
#'             multiple = FALSE,
127
#'             fixed = FALSE
128
#'           )
129
#'         )
130
#'       ),
131
#'       categorical_var = list(
132
#'         data_extract_spec(
133
#'           dataname = "ADSL",
134
#'           filter = filter_spec(
135
#'             vars = vars,
136
#'             choices = value_choices(data[["ADSL"]], vars$selected),
137
#'             selected = value_choices(data[["ADSL"]], vars$selected),
138
#'             multiple = TRUE
139
#'           )
140
#'         )
141
#'       )
142
#'     )
143
#'   )
144
#' )
145
#' if (interactive()) {
146
#'   shinyApp(app$ui, app$server)
147
#' }
148
#'
149
#' @export
150
#'
151
tm_outliers <- function(label = "Outliers Module",
152
                        outlier_var,
153
                        categorical_var = NULL,
154
                        ggtheme = c("gray", "bw", "linedraw", "light", "dark", "minimal", "classic", "void"),
155
                        ggplot2_args = teal.widgets::ggplot2_args(),
156
                        plot_height = c(600, 200, 2000),
157
                        plot_width = NULL,
158
                        pre_output = NULL,
159
                        post_output = NULL,
160
                        transformators = list(),
161
                        decorators = list()) {
162!
  message("Initializing tm_outliers")
163
164
  # Normalize the parameters
165!
  if (inherits(outlier_var, "data_extract_spec")) outlier_var <- list(outlier_var)
166!
  if (inherits(categorical_var, "data_extract_spec")) categorical_var <- list(categorical_var)
167!
  if (inherits(ggplot2_args, "ggplot2_args")) ggplot2_args <- list(default = ggplot2_args)
168
169
  # Start of assertions
170!
  checkmate::assert_string(label)
171!
  checkmate::assert_list(outlier_var, types = "data_extract_spec")
172
173!
  checkmate::assert_list(categorical_var, types = "data_extract_spec", null.ok = TRUE)
174!
  if (is.list(categorical_var)) {
175!
    lapply(categorical_var, function(x) {
176!
      if (length(x$filter) > 1L) {
177!
        stop("tm_outliers: categorical_var data_extract_specs may only specify one filter_spec", call. = FALSE)
178
      }
179
    })
180
  }
181
182!
  ggtheme <- match.arg(ggtheme)
183
184!
  plot_choices <- c("Boxplot", "Density Plot", "Cumulative Distribution Plot")
185!
  checkmate::assert_list(ggplot2_args, types = "ggplot2_args")
186!
  checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices))
187
188!
  checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE)
189!
  checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height")
190!
  checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE)
191!
  checkmate::assert_numeric(
192!
    plot_width[1],
193!
    lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width"
194
  )
195
196!
  checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
197!
  checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
198
199!
  assert_decorators(decorators, names = c("box_plot", "density_plot", "cumulative_plot"))
200
  # End of assertions
201
202
  # Make UI args
203!
  args <- as.list(environment())
204
205!
  data_extract_list <- list(
206!
    outlier_var = outlier_var,
207!
    categorical_var = categorical_var
208
  )
209
210
211!
  ans <- module(
212!
    label = label,
213!
    server = srv_outliers,
214!
    server_args = c(
215!
      data_extract_list,
216!
      list(
217!
        plot_height = plot_height, plot_width = plot_width, ggplot2_args = ggplot2_args,
218!
        decorators = decorators
219
      )
220
    ),
221!
    ui = ui_outliers,
222!
    ui_args = args,
223!
    transformators = transformators,
224!
    datanames = teal.transform::get_extract_datanames(data_extract_list)
225
  )
226!
  attr(ans, "teal_bookmarkable") <- TRUE
227!
  ans
228
}
229
230
# UI function for the outliers module
231
ui_outliers <- function(id, ...) {
232!
  args <- list(...)
233!
  ns <- NS(id)
234!
  is_single_dataset_value <- teal.transform::is_single_dataset(args$outlier_var, args$categorical_var)
235
236!
  teal.widgets::standard_layout(
237!
    output = teal.widgets::white_small_well(
238!
      uiOutput(ns("total_outliers")),
239!
      tags$div(
240!
        style = "overflow: auto;",
241!
        DT::dataTableOutput(ns("summary_table"))
242
      ),
243!
      uiOutput(ns("total_missing")),
244!
      tags$br(), tags$hr(),
245!
      tabsetPanel(
246!
        id = ns("tabs"),
247!
        tabPanel(
248!
          "Boxplot",
249!
          teal.widgets::plot_with_settings_ui(id = ns("box_plot"))
250
        ),
251!
        tabPanel(
252!
          "Density Plot",
253!
          teal.widgets::plot_with_settings_ui(id = ns("density_plot"))
254
        ),
255!
        tabPanel(
256!
          "Cumulative Distribution Plot",
257!
          teal.widgets::plot_with_settings_ui(id = ns("cum_density_plot"))
258
        )
259
      ),
260!
      tags$br(), tags$hr(),
261!
      uiOutput(ns("table_ui_wrap")),
262!
      DT::dataTableOutput(ns("table_ui"))
263
    ),
264!
    encoding = tags$div(
265!
      tags$label("Encodings", class = "text-primary"),
266!
      teal.transform::datanames_input(args[c("outlier_var", "categorical_var")]),
267!
      teal.transform::data_extract_ui(
268!
        id = ns("outlier_var"),
269!
        label = "Variable",
270!
        data_extract_spec = args$outlier_var,
271!
        is_single_dataset = is_single_dataset_value
272
      ),
273!
      if (!is.null(args$categorical_var)) {
274!
        teal.transform::data_extract_ui(
275!
          id = ns("categorical_var"),
276!
          label = "Categorical factor",
277!
          data_extract_spec = args$categorical_var,
278!
          is_single_dataset = is_single_dataset_value
279
        )
280
      },
281!
      conditionalPanel(
282!
        condition = paste0("input['", ns("tabs"), "'] == 'Boxplot'"),
283!
        teal.widgets::optionalSelectInput(
284!
          inputId = ns("boxplot_alts"),
285!
          label = "Plot type",
286!
          choices = c("Box plot", "Violin plot"),
287!
          selected = "Box plot",
288!
          multiple = FALSE
289
        )
290
      ),
291!
      shinyjs::hidden(checkboxInput(ns("split_outliers"), "Define outliers based on group splitting", value = FALSE)),
292!
      shinyjs::hidden(checkboxInput(ns("order_by_outlier"), "Re-order categories by outliers [by %]", value = FALSE)),
293!
      bslib::accordion(
294!
        open = TRUE,
295!
        bslib::accordion_panel(
296!
          title = "Method parameters",
297!
          teal.widgets::optionalSelectInput(
298!
            inputId = ns("method"),
299!
            label = "Method",
300!
            choices = c("IQR", "Z-score", "Percentile"),
301!
            selected = "IQR",
302!
            multiple = FALSE
303
          ),
304!
          conditionalPanel(
305!
            condition =
306!
              paste0("input['", ns("method"), "'] == 'IQR'"),
307!
            sliderInput(
308!
              ns("iqr_slider"),
309!
              "Outlier range:",
310!
              min = 1,
311!
              max = 5,
312!
              value = 3,
313!
              step = 0.5
314
            )
315
          ),
316!
          conditionalPanel(
317!
            condition =
318!
              paste0("input['", ns("method"), "'] == 'Z-score'"),
319!
            sliderInput(
320!
              ns("zscore_slider"),
321!
              "Outlier range:",
322!
              min = 1,
323!
              max = 5,
324!
              value = 3,
325!
              step = 0.5
326
            )
327
          ),
328!
          conditionalPanel(
329!
            condition =
330!
              paste0("input['", ns("method"), "'] == 'Percentile'"),
331!
            sliderInput(
332!
              ns("percentile_slider"),
333!
              "Outlier range:",
334!
              min = 0.001,
335!
              max = 0.5,
336!
              value = 0.01,
337!
              step = 0.001
338
            )
339
          ),
340!
          uiOutput(ns("ui_outlier_help"))
341
        )
342
      ),
343!
      conditionalPanel(
344!
        condition = paste0("input['", ns("tabs"), "'] == 'Boxplot'"),
345!
        ui_decorate_teal_data(
346!
          ns("d_box_plot"),
347!
          decorators = select_decorators(args$decorators, "box_plot")
348
        )
349
      ),
350!
      conditionalPanel(
351!
        condition = paste0("input['", ns("tabs"), "'] == 'Density Plot'"),
352!
        ui_decorate_teal_data(
353!
          ns("d_density_plot"),
354!
          decorators = select_decorators(args$decorators, "density_plot")
355
        )
356
      ),
357!
      conditionalPanel(
358!
        condition = paste0("input['", ns("tabs"), "'] == 'Cumulative Distribution Plot'"),
359!
        ui_decorate_teal_data(
360!
          ns("d_cumulative_plot"),
361!
          decorators = select_decorators(args$decorators, "cumulative_plot")
362
        )
363
      ),
364!
      bslib::accordion(
365!
        open = TRUE,
366!
        bslib::accordion_panel(
367!
          title = "Plot settings",
368!
          selectInput(
369!
            inputId = ns("ggtheme"),
370!
            label = "Theme (by ggplot):",
371!
            choices = ggplot_themes,
372!
            selected = args$ggtheme,
373!
            multiple = FALSE
374
          )
375
        )
376
      )
377
    ),
378!
    pre_output = args$pre_output,
379!
    post_output = args$post_output
380
  )
381
}
382
383
# Server function for the outliers module
384
# Server function for the outliers module
385
srv_outliers <- function(id, data, outlier_var,
386
                         categorical_var, plot_height, plot_width, ggplot2_args, decorators) {
387!
  checkmate::assert_class(data, "reactive")
388!
  checkmate::assert_class(isolate(data()), "teal_data")
389!
  moduleServer(id, function(input, output, session) {
390!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
391
392!
    ns <- session$ns
393
394!
    vars <- list(outlier_var = outlier_var, categorical_var = categorical_var)
395
396!
    rule_diff <- function(other) {
397!
      function(value) {
398!
        othervalue <- tryCatch(selector_list()[[other]]()[["select"]], error = function(e) NULL)
399!
        if (!is.null(othervalue) && identical(othervalue, value)) {
400!
          "`Variable` and `Categorical factor` cannot be the same"
401
        }
402
      }
403
    }
404
405!
    selector_list <- teal.transform::data_extract_multiple_srv(
406!
      data_extract = vars,
407!
      datasets = data,
408!
      select_validation_rule = list(
409!
        outlier_var = shinyvalidate::compose_rules(
410!
          shinyvalidate::sv_required("Please select a variable"),
411!
          rule_diff("categorical_var")
412
        ),
413!
        categorical_var = rule_diff("outlier_var")
414
      )
415
    )
416
417!
    iv_r <- reactive({
418!
      iv <- shinyvalidate::InputValidator$new()
419!
      iv$add_rule("method", shinyvalidate::sv_required("Please select a method"))
420!
      iv$add_rule("boxplot_alts", shinyvalidate::sv_required("Please select Plot Type"))
421!
      teal.transform::compose_and_enable_validators(iv, selector_list)
422
    })
423
424!
    reactive_select_input <- reactive({
425!
      if (is.null(selector_list()$categorical_var) || length(selector_list()$categorical_var()$select) == 0) {
426!
        selector_list()[names(selector_list()) != "categorical_var"]
427
      } else {
428!
        selector_list()
429
      }
430
    })
431
432
    # Used to create outlier table and the dropdown with additional columns
433!
    dataname_first <- isolate(names(data())[[1]])
434
435!
    data_obj <- reactive({
436!
      obj <- data()
437!
      if (length(teal.data::join_keys(obj)) == 0) {
438!
        if (!".row_id" %in% names(obj[[dataname_first]])) {
439!
          obj[[dataname_first]]$.row_id <- seq_len(nrow(obj[[dataname_first]]))
440
        }
441!
        teal.data::join_keys(obj) <-
442!
          teal.data::join_keys(teal.data::join_key(dataname_first, dataname_first, ".row_id"))
443
      }
444!
      obj
445
    })
446
447!
    anl_merged_input <- teal.transform::merge_expression_srv(
448!
      selector_list = reactive_select_input,
449!
      datasets = data_obj,
450!
      merge_function = "dplyr::inner_join"
451
    )
452
453!
    anl_merged_q <- reactive({
454!
      req(anl_merged_input())
455!
      teal.code::eval_code(
456!
        data_obj(),
457!
        "library(dplyr);library(tidyr);library(tibble);library(ggplot2)"
458
      ) %>%
459!
        teal.code::eval_code(as.expression(anl_merged_input()$expr))
460
    })
461
462!
    merged <- list(
463!
      anl_input_r = anl_merged_input,
464!
      anl_q_r = anl_merged_q
465
    )
466
467!
    n_outlier_missing <- reactive({
468!
      req(iv_r()$is_valid())
469!
      outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var)
470!
      ANL <- merged$anl_q_r()[["ANL"]]
471!
      sum(is.na(ANL[[outlier_var]]))
472
    })
473
474!
    common_code_q <- reactive({
475!
      req(iv_r()$is_valid())
476
477!
      ANL <- merged$anl_q_r()[["ANL"]]
478!
      qenv <- merged$anl_q_r()
479!
      teal.reporter::teal_card(qenv) <-
480!
        c(
481!
          teal.reporter::teal_card(qenv),
482!
          teal.reporter::teal_card("## Module's output(s)")
483
        )
484
485!
      outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var)
486!
      categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var)
487!
      order_by_outlier <- input$order_by_outlier
488!
      method <- input$method
489!
      split_outliers <- input$split_outliers
490!
      teal::validate_has_data(
491
        # missing values in the categorical variable may be used to form a category of its own
492!
        `if`(
493!
          length(categorical_var) == 0,
494!
          ANL,
495!
          ANL[, names(ANL) != categorical_var, drop = FALSE]
496
        ),
497!
        min_nrow = 10,
498!
        complete = TRUE,
499!
        allow_inf = FALSE
500
      )
501!
      validate(need(is.numeric(ANL[[outlier_var]]), "`Variable` is not numeric"))
502!
      validate(need(length(unique(ANL[[outlier_var]])) > 1, "Variable has no variation, i.e. only one unique value"))
503
504
      # show/hide split_outliers
505!
      if (length(categorical_var) == 0) {
506!
        shinyjs::hide("split_outliers")
507!
        if (n_outlier_missing() > 0) {
508!
          qenv <- teal.code::eval_code(
509!
            qenv,
510!
            substitute(
511!
              expr = ANL <- ANL %>% dplyr::filter(!is.na(outlier_var_name)),
512!
              env = list(outlier_var_name = as.name(outlier_var))
513
            )
514
          )
515
        }
516
      } else {
517!
        validate(need(
518!
          is.factor(ANL[[categorical_var]]) ||
519!
            is.character(ANL[[categorical_var]]) ||
520!
            is.integer(ANL[[categorical_var]]),
521!
          "`Categorical factor` must be `factor`, `character`, or `integer`"
522
        ))
523
524!
        if (n_outlier_missing() > 0) {
525!
          qenv <- teal.code::eval_code(
526!
            qenv,
527!
            substitute(
528!
              expr = ANL <- ANL %>% dplyr::filter(!is.na(outlier_var_name)),
529!
              env = list(outlier_var_name = as.name(outlier_var))
530
            )
531
          )
532
        }
533!
        shinyjs::show("split_outliers")
534
      }
535
536
      # slider
537!
      outlier_definition_param <- if (method == "IQR") {
538!
        input$iqr_slider
539!
      } else if (method == "Z-score") {
540!
        input$zscore_slider
541!
      } else if (method == "Percentile") {
542!
        input$percentile_slider
543
      }
544
545
      # this is utils function that converts a %>% NULL %>% b into a %>% b
546!
      remove_pipe_null <- function(x) {
547!
        if (length(x) == 1) {
548!
          x
549!
        } else if (identical(x[[1]], as.name("%>%")) && is.null(x[[3]])) {
550!
          remove_pipe_null(x[[2]])
551
        } else {
552!
          as.call(c(x[[1]], lapply(x[-1], remove_pipe_null)))
553
        }
554
      }
555
556!
      qenv <- teal.code::eval_code(
557!
        qenv,
558!
        substitute(
559!
          expr = {
560!
            ANL_OUTLIER <- ANL %>%
561!
              group_expr %>% # styler: off
562!
              dplyr::mutate(is_outlier = {
563!
                q1_q3 <- stats::quantile(outlier_var_name, probs = c(0.25, 0.75))
564!
                iqr <- q1_q3[2] - q1_q3[1]
565!
                !(outlier_var_name >= q1_q3[1] - 1.5 * iqr & outlier_var_name <= q1_q3[2] + 1.5 * iqr)
566
              }) %>%
567!
              calculate_outliers %>% # styler: off
568!
              ungroup_expr %>% # styler: off
569!
              dplyr::filter(is_outlier | is_outlier_selected) %>%
570!
              dplyr::select(-is_outlier)
571
          },
572!
          env = list(
573!
            calculate_outliers = if (method == "IQR") {
574!
              substitute(
575!
                expr = dplyr::mutate(is_outlier_selected = {
576!
                  q1_q3 <- stats::quantile(outlier_var_name, probs = c(0.25, 0.75))
577!
                  iqr <- q1_q3[2] - q1_q3[1]
578
                  !(
579!
                    outlier_var_name >= q1_q3[1] - outlier_definition_param * iqr &
580!
                      outlier_var_name <= q1_q3[2] + outlier_definition_param * iqr
581
                  )
582
                }),
583!
                env = list(
584!
                  outlier_var_name = as.name(outlier_var),
585!
                  outlier_definition_param = outlier_definition_param
586
                )
587
              )
588!
            } else if (method == "Z-score") {
589!
              substitute(
590!
                expr = dplyr::mutate(
591!
                  is_outlier_selected = abs(outlier_var_name - mean(outlier_var_name)) /
592!
                    stats::sd(outlier_var_name) > outlier_definition_param
593
                ),
594!
                env = list(
595!
                  outlier_var_name = as.name(outlier_var),
596!
                  outlier_definition_param = outlier_definition_param
597
                )
598
              )
599!
            } else if (method == "Percentile") {
600!
              substitute(
601!
                expr = dplyr::mutate(
602!
                  is_outlier_selected = outlier_var_name < stats::quantile(outlier_var_name, outlier_definition_param) |
603!
                    outlier_var_name > stats::quantile(outlier_var_name, 1 - outlier_definition_param)
604
                ),
605!
                env = list(
606!
                  outlier_var_name = as.name(outlier_var),
607!
                  outlier_definition_param = outlier_definition_param
608
                )
609
              )
610
            },
611!
            outlier_var_name = as.name(outlier_var),
612!
            group_expr = if (isTRUE(split_outliers) && length(categorical_var) != 0) {
613!
              substitute(dplyr::group_by(x), list(x = as.name(categorical_var)))
614
            },
615!
            ungroup_expr = if (isTRUE(split_outliers) && length(categorical_var) != 0) {
616!
              substitute(dplyr::ungroup())
617
            }
618
          )
619
        ) %>%
620!
          remove_pipe_null()
621
      )
622
623
      # ANL_OUTLIER_EXTENDED is the base table
624!
      join_keys <- as.character(teal.data::join_keys(data_obj())[dataname_first, dataname_first])
625
626!
      if (length(join_keys) == 1 && join_keys == ".row_id") {
627
        # Dummy join key - single dataset, no join needed
628!
        qenv <- teal.code::eval_code(qenv, quote(ANL_OUTLIER_EXTENDED <- ANL_OUTLIER))
629
      } else {
630
        # Join keys exist - perform left join
631!
        qenv <- teal.code::eval_code(
632!
          qenv,
633!
          substitute(
634!
            expr = {
635!
              ANL_OUTLIER_EXTENDED <- dplyr::left_join(
636!
                ANL_OUTLIER,
637!
                dplyr::select(
638!
                  dataname,
639!
                  dplyr::setdiff(names(dataname), dplyr::setdiff(names(ANL_OUTLIER), join_keys))
640
                ),
641!
                by = join_keys
642
              )
643
            },
644!
            env = list(
645!
              dataname = as.name(dataname_first),
646!
              join_keys = join_keys
647
            )
648
          )
649
        )
650
      }
651
652!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Summary Table")
653!
      qenv <- if (length(categorical_var) > 0) {
654!
        qenv <- teal.code::eval_code(
655!
          qenv,
656!
          substitute(
657!
            expr = summary_data_pre <- ANL_OUTLIER %>%
658!
              dplyr::filter(is_outlier_selected) %>%
659!
              dplyr::select(outlier_var_name, categorical_var_name) %>%
660!
              dplyr::group_by(categorical_var_name) %>%
661!
              dplyr::summarise(n_outliers = dplyr::n()) %>%
662!
              dplyr::right_join(
663!
                ANL %>%
664!
                  dplyr::select(outlier_var_name, categorical_var_name) %>%
665!
                  dplyr::group_by(categorical_var_name) %>%
666!
                  dplyr::summarise(
667!
                    total_in_cat = dplyr::n(),
668!
                    n_na = sum(is.na(outlier_var_name) | is.na(categorical_var_name))
669
                  ),
670!
                by = categorical_var
671
              ) %>%
672
              # This is important as there may be categorical variables with natural orderings, e.g. AGE.
673
              # The plots should be displayed by default in increasing order in these situations.
674
              # dplyr::arrange will sort integer, factor, and character data types in the expected way.
675!
              dplyr::arrange(categorical_var_name) %>%
676!
              dplyr::mutate(
677!
                n_outliers = dplyr::if_else(is.na(n_outliers), 0, as.numeric(n_outliers)),
678!
                display_str = dplyr::if_else(
679!
                  n_outliers > 0,
680!
                  sprintf("%d [%.02f%%]", n_outliers, 100 * n_outliers / total_in_cat),
681!
                  "0"
682
                ),
683!
                display_str_na = dplyr::if_else(
684!
                  n_na > 0,
685!
                  sprintf("%d [%.02f%%]", n_na, 100 * n_na / total_in_cat),
686!
                  "0"
687
                ),
688!
                order = seq_along(n_outliers)
689
              ),
690!
            env = list(
691!
              categorical_var = categorical_var,
692!
              categorical_var_name = as.name(categorical_var),
693!
              outlier_var_name = as.name(outlier_var)
694
            )
695
          )
696
        )
697
        # now to handle when user chooses to order based on amount of outliers
698!
        if (order_by_outlier) {
699!
          qenv <- teal.code::eval_code(
700!
            qenv,
701!
            quote(
702!
              summary_data_pre <- summary_data_pre %>%
703!
                dplyr::arrange(desc(n_outliers / total_in_cat)) %>%
704!
                dplyr::mutate(order = seq_len(nrow(summary_data_pre)))
705
            )
706
          )
707
        }
708
709!
        teal.code::eval_code(
710!
          qenv,
711!
          substitute(
712!
            expr = {
713
              # In order for geom_rug to work properly when reordering takes place inside facet_grid,
714
              # all tables must have the column used for reording.
715
              # In this case, the column used for reordering is `order`.
716!
              ANL_OUTLIER <- dplyr::left_join(
717!
                ANL_OUTLIER,
718!
                summary_data_pre[, c("order", categorical_var)],
719!
                by = categorical_var
720
              )
721
              # so that x axis of plot aligns with columns of summary table, from most outliers to least by percentage
722!
              ANL <- ANL %>%
723!
                dplyr::left_join(
724!
                  dplyr::select(summary_data_pre, categorical_var_name, order),
725!
                  by = categorical_var
726
                ) %>%
727!
                dplyr::arrange(order)
728!
              summary_data <- summary_data_pre %>%
729!
                dplyr::select(
730!
                  categorical_var_name,
731!
                  Outliers = display_str, Missings = display_str_na, Total = total_in_cat
732
                ) %>%
733!
                dplyr::mutate_all(as.character) %>%
734!
                tidyr::pivot_longer(-categorical_var_name) %>%
735!
                tidyr::pivot_wider(names_from = categorical_var, values_from = value) %>%
736!
                tibble::column_to_rownames("name")
737
            },
738!
            env = list(
739!
              categorical_var = categorical_var,
740!
              categorical_var_name = as.name(categorical_var)
741
            )
742
          )
743
        ) |>
744!
          within({
745!
            table <- rtables::df_to_tt(summary_data)
746!
            table
747
          })
748
      } else {
749!
        msg <- "No categorical variable selected, summary table cannot be created."
750!
        showNotification(msg,
751!
          closeButton = FALSE, type = "warning",
752!
          id = session$ns("no_summary_table")
753
        )
754!
        within(qenv, cat(msg), msg = msg)
755
      }
756
757
758!
      if (length(categorical_var) > 0 && nrow(qenv[["ANL_OUTLIER"]]) > 0) {
759!
        shinyjs::show("order_by_outlier")
760
      } else {
761!
        shinyjs::hide("order_by_outlier")
762
      }
763
764!
      qenv
765
    })
766
767
    # boxplot/violinplot # nolint commented_code
768!
    box_plot_q <- reactive({
769!
      req(common_code_q())
770!
      qenv <- common_code_q()
771!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Box Plot")
772
773!
      ANL <- qenv[["ANL"]]
774!
      ANL_OUTLIER <- qenv[["ANL_OUTLIER"]]
775
776!
      outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var)
777!
      categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var)
778
779
      # validation
780!
      teal::validate_has_data(ANL, 1)
781
782
      # boxplot
783!
      plot_call <- quote(ANL %>% ggplot())
784
785!
      plot_call <- if (input$boxplot_alts == "Box plot") {
786!
        substitute(expr = plot_call + ggplot2::geom_boxplot(outlier.shape = NA), env = list(plot_call = plot_call))
787!
      } else if (input$boxplot_alts == "Violin plot") {
788!
        substitute(expr = plot_call + ggplot2::geom_violin(), env = list(plot_call = plot_call))
789
      } else {
790!
        NULL
791
      }
792
793!
      plot_call <- if (identical(categorical_var, character(0)) || is.null(categorical_var)) {
794!
        inner_call <- substitute(
795!
          expr = plot_call +
796!
            ggplot2::aes(x = "Entire dataset", y = outlier_var_name) +
797!
            ggplot2::scale_x_discrete(),
798!
          env = list(plot_call = plot_call, outlier_var_name = as.name(outlier_var))
799
        )
800!
        if (nrow(ANL_OUTLIER) > 0) {
801!
          substitute(
802!
            expr = inner_call + ggplot2::geom_point(
803!
              data = ANL_OUTLIER,
804!
              ggplot2::aes(x = "Entire dataset", y = outlier_var_name, color = is_outlier_selected)
805
            ),
806!
            env = list(inner_call = inner_call, outlier_var_name = as.name(outlier_var))
807
          )
808
        } else {
809!
          inner_call
810
        }
811
      } else {
812!
        substitute(
813!
          expr = plot_call +
814!
            ggplot2::aes(y = outlier_var_name, x = reorder(categorical_var_name, order)) +
815!
            ggplot2::xlab(categorical_var) +
816!
            ggplot2::scale_x_discrete() +
817!
            ggplot2::geom_point(
818!
              data = ANL_OUTLIER,
819!
              ggplot2::aes(x = as.factor(categorical_var_name), y = outlier_var_name, color = is_outlier_selected)
820
            ),
821!
          env = list(
822!
            plot_call = plot_call,
823!
            outlier_var_name = as.name(outlier_var),
824!
            categorical_var_name = as.name(categorical_var),
825!
            categorical_var = categorical_var
826
          )
827
        )
828
      }
829
830!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
831!
        labs = list(color = "Is outlier?"),
832!
        theme = list(legend.position = "top")
833
      )
834
835!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
836!
        user_plot = ggplot2_args[["Boxplot"]],
837!
        user_default = ggplot2_args$default,
838!
        module_plot = dev_ggplot2_args
839
      )
840
841!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
842!
        all_ggplot2_args,
843!
        ggtheme = input$ggtheme
844
      )
845
846!
      teal.code::eval_code(
847!
        qenv,
848!
        substitute(
849!
          expr = box_plot <- plot_call +
850!
            ggplot2::scale_color_manual(values = c("TRUE" = "red", "FALSE" = "black")) +
851!
            labs + ggthemes + themes,
852!
          env = list(
853!
            plot_call = plot_call,
854!
            labs = parsed_ggplot2_args$labs,
855!
            ggthemes = parsed_ggplot2_args$ggtheme,
856!
            themes = parsed_ggplot2_args$theme
857
          )
858
        )
859
      )
860
    })
861
862
    # density plot
863!
    density_plot_q <- reactive({
864!
      qenv <- common_code_q()
865!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Density Plot")
866
867!
      ANL <- qenv[["ANL"]]
868!
      ANL_OUTLIER <- qenv[["ANL_OUTLIER"]]
869
870!
      outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var)
871!
      categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var)
872
873
      # validation
874!
      teal::validate_has_data(ANL, 1)
875
      # plot
876!
      plot_call <- substitute(
877!
        expr = ANL %>%
878!
          ggplot2::ggplot(ggplot2::aes(x = outlier_var_name)) +
879!
          ggplot2::geom_density() +
880!
          ggplot2::geom_rug(data = ANL_OUTLIER, ggplot2::aes(x = outlier_var_name, color = is_outlier_selected)) +
881!
          ggplot2::scale_color_manual(values = c("TRUE" = "red", "FALSE" = "black")),
882!
        env = list(outlier_var_name = as.name(outlier_var))
883
      )
884
885!
      plot_call <- if (identical(categorical_var, character(0)) || is.null(categorical_var)) {
886!
        substitute(expr = plot_call, env = list(plot_call = plot_call))
887
      } else {
888!
        substitute(
889!
          expr = plot_call + ggplot2::facet_grid(~ reorder(categorical_var_name, order)),
890!
          env = list(plot_call = plot_call, categorical_var_name = as.name(categorical_var))
891
        )
892
      }
893
894!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
895!
        labs = list(color = "Is outlier?"),
896!
        theme = list(legend.position = "top")
897
      )
898
899!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
900!
        user_plot = ggplot2_args[["Density Plot"]],
901!
        user_default = ggplot2_args$default,
902!
        module_plot = dev_ggplot2_args
903
      )
904
905!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
906!
        all_ggplot2_args,
907!
        ggtheme = input$ggtheme
908
      )
909
910!
      teal.code::eval_code(
911!
        qenv,
912!
        substitute(
913!
          expr = density_plot <- plot_call + labs + ggthemes + themes,
914!
          env = list(
915!
            plot_call = plot_call,
916!
            labs = parsed_ggplot2_args$labs,
917!
            themes = parsed_ggplot2_args$theme,
918!
            ggthemes = parsed_ggplot2_args$ggtheme
919
          )
920
        )
921
      )
922
    })
923
924
    # Cumulative distribution plot
925!
    cumulative_plot_q <- reactive({
926!
      qenv <- common_code_q()
927!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Cumulative Distribution Plot")
928
929!
      ANL <- qenv[["ANL"]]
930!
      ANL_OUTLIER <- qenv[["ANL_OUTLIER"]]
931
932!
      outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var)
933!
      categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var)
934
935
      # validation
936!
      teal::validate_has_data(ANL, 1)
937
938
      # plot
939!
      plot_call <- substitute(
940!
        expr = ANL %>% ggplot2::ggplot(ggplot2::aes(x = outlier_var_name)) +
941!
          ggplot2::stat_ecdf(),
942!
        env = list(outlier_var_name = as.name(outlier_var))
943
      )
944!
      if (length(categorical_var) == 0) {
945!
        qenv <- teal.code::eval_code(
946!
          qenv,
947!
          substitute(
948!
            expr = {
949!
              ecdf_df <- ANL %>%
950!
                dplyr::mutate(
951!
                  y = stats::ecdf(ANL[[outlier_var]])(ANL[[outlier_var]])
952
                )
953
954!
              outlier_points <- dplyr::left_join(
955!
                ecdf_df,
956!
                ANL_OUTLIER,
957!
                by = dplyr::setdiff(names(ecdf_df), "y")
958
              ) %>%
959!
                dplyr::filter(!is.na(is_outlier_selected))
960
            },
961!
            env = list(outlier_var = outlier_var)
962
          )
963
        )
964
      } else {
965!
        qenv <- teal.code::eval_code(
966!
          qenv,
967!
          substitute(
968!
            expr = {
969!
              all_categories <- lapply(
970!
                unique(ANL[[categorical_var]]),
971!
                function(x) {
972!
                  ANL <- ANL %>% dplyr::filter(get(categorical_var) == x)
973!
                  anl_outlier2 <- ANL_OUTLIER %>% dplyr::filter(get(categorical_var) == x)
974!
                  ecdf_df <- ANL %>%
975!
                    dplyr::mutate(y = stats::ecdf(ANL[[outlier_var]])(ANL[[outlier_var]]))
976
977!
                  dplyr::left_join(
978!
                    ecdf_df,
979!
                    anl_outlier2,
980!
                    by = dplyr::setdiff(names(ecdf_df), "y")
981
                  ) %>%
982!
                    dplyr::filter(!is.na(is_outlier_selected))
983
                }
984
              )
985!
              outlier_points <- do.call(rbind, all_categories)
986
            },
987!
            env = list(categorical_var = categorical_var, outlier_var = outlier_var)
988
          )
989
        )
990!
        plot_call <- substitute(
991!
          expr = plot_call + ggplot2::facet_grid(~ reorder(categorical_var_name, order)),
992!
          env = list(plot_call = plot_call, categorical_var_name = as.name(categorical_var))
993
        )
994
      }
995
996!
      dev_ggplot2_args <- teal.widgets::ggplot2_args(
997!
        labs = list(color = "Is outlier?"),
998!
        theme = list(legend.position = "top")
999
      )
1000
1001!
      all_ggplot2_args <- teal.widgets::resolve_ggplot2_args(
1002!
        user_plot = ggplot2_args[["Cumulative Distribution Plot"]],
1003!
        user_default = ggplot2_args$default,
1004!
        module_plot = dev_ggplot2_args
1005
      )
1006
1007!
      parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(
1008!
        all_ggplot2_args,
1009!
        ggtheme = input$ggtheme
1010
      )
1011
1012!
      teal.code::eval_code(
1013!
        qenv,
1014!
        substitute(
1015!
          expr = cumulative_plot <- plot_call +
1016!
            ggplot2::geom_point(
1017!
              data = outlier_points,
1018!
              ggplot2::aes(x = outlier_var_name, y = y, color = is_outlier_selected)
1019
            ) +
1020!
            ggplot2::scale_color_manual(values = c("TRUE" = "red", "FALSE" = "black")) +
1021!
            labs + ggthemes + themes,
1022!
          env = list(
1023!
            plot_call = plot_call,
1024!
            outlier_var_name = as.name(outlier_var),
1025!
            labs = parsed_ggplot2_args$labs,
1026!
            themes = parsed_ggplot2_args$theme,
1027!
            ggthemes = parsed_ggplot2_args$ggtheme
1028
          )
1029
        )
1030
      )
1031
    })
1032
1033!
    current_tab_r <- reactive({
1034!
      switch(req(input$tabs),
1035!
        "Boxplot" = "box_plot",
1036!
        "Density Plot" = "density_plot",
1037!
        "Cumulative Distribution Plot" = "cumulative_plot"
1038
      )
1039
    })
1040
1041!
    decorated_q <- mapply(
1042!
      function(obj_name, q) {
1043!
        srv_decorate_teal_data(
1044!
          id = sprintf("d_%s", obj_name),
1045!
          data = q,
1046!
          decorators = select_decorators(decorators, obj_name),
1047!
          expr = reactive({
1048!
            substitute(
1049!
              expr = {
1050!
                columns_index <- union(
1051!
                  setdiff(names(ANL_OUTLIER), c("is_outlier_selected", "order")),
1052!
                  table_columns
1053
                )
1054!
                ANL_OUTLIER_EXTENDED[ANL_OUTLIER_EXTENDED$is_outlier_selected, columns_index]
1055!
                print(.plot)
1056
              },
1057!
              env = list(table_columns = input$table_ui_columns, .plot = as.name(obj_name))
1058
            )
1059
          })
1060
        )
1061
      },
1062!
      stats::setNames(nm = c("box_plot", "density_plot", "cumulative_plot")),
1063!
      c(box_plot_q, density_plot_q, cumulative_plot_q)
1064
    )
1065
1066!
    box_plot_r <- reactive({
1067!
      teal::validate_inputs(iv_r())
1068!
      req(decorated_q$box_plot())[["box_plot"]]
1069
    })
1070!
    density_plot_r <- reactive({
1071!
      teal::validate_inputs(iv_r())
1072!
      req(decorated_q$density_plot())[["density_plot"]]
1073
    })
1074!
    cumulative_plot_r <- reactive({
1075!
      teal::validate_inputs(iv_r())
1076!
      req(decorated_q$cumulative_plot())[["cumulative_plot"]]
1077
    })
1078
1079!
    box_pws <- teal.widgets::plot_with_settings_srv(
1080!
      id = "box_plot",
1081!
      plot_r = box_plot_r,
1082!
      height = plot_height,
1083!
      width = plot_width,
1084!
      brushing = TRUE
1085
    )
1086
1087!
    density_pws <- teal.widgets::plot_with_settings_srv(
1088!
      id = "density_plot",
1089!
      plot_r = density_plot_r,
1090!
      height = plot_height,
1091!
      width = plot_width,
1092!
      brushing = TRUE
1093
    )
1094
1095!
    cum_density_pws <- teal.widgets::plot_with_settings_srv(
1096!
      id = "cum_density_plot",
1097!
      plot_r = cumulative_plot_r,
1098!
      height = plot_height,
1099!
      width = plot_width,
1100!
      brushing = TRUE
1101
    )
1102
1103!
    pws_list <- list(box_plot = box_pws, density_plot = density_pws, cumulative_plot = cum_density_pws)
1104!
    decorated_final_q <- reactive({
1105!
      pws <- pws_list[[req(current_tab_r())]]
1106!
      req(pws$dim())
1107!
      req(decorated_q[[current_tab_r()]]())
1108!
      set_chunk_dims(pws, decorated_q[[current_tab_r()]])()
1109
    })
1110
1111!
    summary_table_r <- reactive({
1112!
      q <- req(decorated_final_q())
1113
1114!
      DT::datatable(
1115!
        data = if (iv_r()$is_valid()) {
1116!
          categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var)
1117!
          if (!is.null(categorical_var)) q[["summary_data"]]
1118
        },
1119!
        options = list(
1120!
          dom = "t",
1121!
          autoWidth = TRUE,
1122!
          columnDefs = list(list(width = "200px", targets = "_all"))
1123
        )
1124
      )
1125
    })
1126
1127!
    output$summary_table <- DT::renderDataTable(summary_table_r())
1128
1129
    # slider text
1130!
    output$ui_outlier_help <- renderUI({
1131!
      req(input$method)
1132!
      if (input$method == "IQR") {
1133!
        req(input$iqr_slider)
1134!
        tags$small(
1135!
          withMathJax(
1136!
            helpText(
1137!
              "Outlier data points (\\(x \\lt Q1 - ", input$iqr_slider, "\\times IQR\\) or \\(
1138!
            Q3 + ", input$iqr_slider, "\\times IQR \\lt x\\))
1139!
            are displayed in red on the plot and can be visualized in the table below."
1140
            ),
1141!
            if (input$split_outliers) {
1142!
              withMathJax(helpText("Note: Quantiles are calculated per group."))
1143
            }
1144
          )
1145
        )
1146!
      } else if (input$method == "Z-score") {
1147!
        req(input$zscore_slider)
1148!
        tags$small(
1149!
          withMathJax(
1150!
            helpText(
1151!
              "Outlier data points (\\(Zscore(x) < -", input$zscore_slider,
1152!
              "\\) or \\(", input$zscore_slider, "< Zscore(x) \\))
1153!
              are displayed in red on the plot and can be visualized in the table below."
1154
            ),
1155!
            if (input$split_outliers) {
1156!
              withMathJax(helpText(" Note: Z-scores are calculated per group."))
1157
            }
1158
          )
1159
        )
1160!
      } else if (input$method == "Percentile") {
1161!
        req(input$percentile_slider)
1162!
        tags$small(
1163!
          withMathJax(
1164!
            helpText(
1165!
              "Outlier/extreme data points (\\( Percentile(x) <", input$percentile_slider,
1166!
              "\\) or \\(", 1 - input$percentile_slider, " < Percentile(x) \\))
1167!
            are displayed in red on the plot and can be visualized in the table below."
1168
            ),
1169!
            if (input$split_outliers) {
1170!
              withMathJax(helpText("Note: Percentiles are calculated per group."))
1171
            }
1172
          )
1173
        )
1174
      }
1175
    })
1176
1177!
    choices <- reactive(teal.transform::variable_choices(data_obj()[[dataname_first]]))
1178
1179!
    observeEvent(common_code_q(), {
1180!
      ANL_OUTLIER <- common_code_q()[["ANL_OUTLIER"]]
1181!
      teal.widgets::updateOptionalSelectInput(
1182!
        session,
1183!
        inputId = "table_ui_columns",
1184!
        choices = dplyr::setdiff(choices(), names(ANL_OUTLIER)),
1185!
        selected = restoreInput(ns("table_ui_columns"), isolate(input$table_ui_columns))
1186
      )
1187
    })
1188
1189!
    output$table_ui <- DT::renderDataTable(
1190!
      expr = {
1191!
        tab <- input$tabs
1192!
        req(tab) # tab is NULL upon app launch, hence will crash without this statement
1193!
        req(iv_r()$is_valid()) # Same validation as output$table_ui_wrap
1194!
        outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var)
1195!
        categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var)
1196
1197!
        ANL_OUTLIER <- common_code_q()[["ANL_OUTLIER"]]
1198!
        ANL_OUTLIER_EXTENDED <- common_code_q()[["ANL_OUTLIER_EXTENDED"]]
1199!
        ANL <- common_code_q()[["ANL"]]
1200
1201!
        plot_brush <- switch(current_tab_r(),
1202!
          box_plot = {
1203!
            box_plot_r()
1204!
            box_pws$brush()
1205
          },
1206!
          density_plot = {
1207!
            density_plot_r()
1208!
            density_pws$brush()
1209
          },
1210!
          cumulative_plot = {
1211!
            cumulative_plot_r()
1212!
            cum_density_pws$brush()
1213
          }
1214
        )
1215
1216
        # removing unused column ASAP
1217!
        ANL_OUTLIER$order <- ANL$order <- NULL
1218
1219!
        display_table <- if (!is.null(plot_brush)) {
1220!
          if (length(categorical_var) > 0) {
1221
            # due to reordering, the x-axis label may be changed to something like "reorder(categorical_var, order)"
1222!
            if (tab == "Boxplot") {
1223!
              plot_brush$mapping$x <- categorical_var
1224
            } else {
1225
              # the other plots use facetting
1226
              # so it is panelvar1 that gets relabelled to "reorder(categorical_var, order)"
1227!
              plot_brush$mapping$panelvar1 <- categorical_var
1228
            }
1229
          } else {
1230!
            if (tab == "Boxplot") {
1231
              # in boxplot with no categorical variable, there is no column in ANL that would correspond to x-axis
1232
              # so a column needs to be inserted with the value "Entire dataset" because that's the label used in plot
1233!
              ANL[[plot_brush$mapping$x]] <- "Entire dataset"
1234
            }
1235
          }
1236
1237
          # in density and cumulative plots, ANL does not have a column corresponding to y-axis.
1238
          # so they need to be computed and attached to ANL
1239!
          if (tab == "Density Plot") {
1240!
            plot_brush$mapping$y <- "density"
1241!
            ANL$density <- plot_brush$ymin
1242
            # either ymin or ymax will work
1243!
          } else if (tab == "Cumulative Distribution Plot") {
1244!
            plot_brush$mapping$y <- "cdf"
1245!
            if (length(categorical_var) > 0) {
1246!
              ANL <- ANL %>%
1247!
                dplyr::group_by(!!as.name(plot_brush$mapping$panelvar1)) %>%
1248!
                dplyr::mutate(cdf = stats::ecdf(!!as.name(outlier_var))(!!as.name(outlier_var)))
1249
            } else {
1250!
              ANL$cdf <- stats::ecdf(ANL[[outlier_var]])(ANL[[outlier_var]])
1251
            }
1252
          }
1253
1254!
          brushed_rows <- brushedPoints(ANL, plot_brush)
1255!
          if (nrow(brushed_rows) > 0) {
1256
            # now we need to remove extra column from ANL so that it will have the same columns as ANL_OUTLIER
1257
            # so that dplyr::intersect will work
1258!
            if (tab == "Density Plot") {
1259!
              brushed_rows$density <- NULL
1260!
            } else if (tab == "Cumulative Distribution Plot") {
1261!
              brushed_rows$cdf <- NULL
1262!
            } else if (tab == "Boxplot" && length(categorical_var) == 0) {
1263!
              brushed_rows[[plot_brush$mapping$x]] <- NULL
1264
            }
1265
            # is_outlier_selected is part of ANL_OUTLIER so needed here
1266!
            brushed_rows$is_outlier_selected <- TRUE
1267!
            dplyr::intersect(ANL_OUTLIER, brushed_rows)
1268
          } else {
1269!
            ANL_OUTLIER[0, ]
1270
          }
1271
        } else {
1272!
          ANL_OUTLIER[ANL_OUTLIER$is_outlier_selected, ]
1273
        }
1274
1275!
        display_table$is_outlier_selected <- NULL
1276
1277
        # Extend the brushed ANL_OUTLIER with additional columns
1278!
        dplyr::left_join(
1279!
          display_table,
1280!
          dplyr::select(ANL_OUTLIER_EXTENDED, -"is_outlier_selected"),
1281!
          by = names(display_table)
1282
        ) %>%
1283!
          dplyr::select(union(names(display_table), input$table_ui_columns))
1284
      },
1285!
      options = list(
1286!
        searching = FALSE, language = list(
1287!
          zeroRecords = "The brushed area does not contain outlier observations for the currently defined threshold"
1288
        ),
1289!
        pageLength = input$table_ui_rows
1290
      )
1291
    )
1292
1293!
    output$total_outliers <- renderUI({
1294!
      req(iv_r()$is_valid())
1295!
      ANL <- merged$anl_q_r()[["ANL"]]
1296!
      ANL_OUTLIER <- common_code_q()[["ANL_OUTLIER"]]
1297!
      teal::validate_has_data(ANL, 1)
1298!
      ANL_OUTLIER_SELECTED <- ANL_OUTLIER[ANL_OUTLIER$is_outlier_selected, ]
1299!
      tags$h5(
1300!
        sprintf(
1301!
          "%s %d / %d [%.02f%%]",
1302!
          "Total number of outlier(s):",
1303!
          nrow(ANL_OUTLIER_SELECTED),
1304!
          nrow(ANL),
1305!
          100 * nrow(ANL_OUTLIER_SELECTED) / nrow(ANL)
1306
        )
1307
      )
1308
    })
1309
1310!
    output$total_missing <- renderUI({
1311!
      if (n_outlier_missing() > 0) {
1312!
        ANL <- merged$anl_q_r()[["ANL"]]
1313!
        helpText(
1314!
          sprintf(
1315!
            "%s %d / %d [%.02f%%]",
1316!
            "Total number of row(s) with missing values:",
1317!
            n_outlier_missing(),
1318!
            nrow(ANL),
1319!
            100 * (n_outlier_missing()) / nrow(ANL)
1320
          )
1321
        )
1322
      }
1323
    })
1324
1325!
    output$table_ui_wrap <- renderUI({
1326!
      req(iv_r()$is_valid())
1327!
      tagList(
1328!
        teal.widgets::optionalSelectInput(
1329!
          inputId = ns("table_ui_columns"),
1330!
          label = "Choose additional columns",
1331!
          choices = NULL,
1332!
          selected = NULL,
1333!
          multiple = TRUE
1334
        ),
1335!
        tags$h4("Outlier Table"),
1336!
        teal.widgets::get_dt_rows(ns("table_ui"), ns("table_ui_rows"))
1337
      )
1338
    })
1339
1340!
    decorated_final_q
1341
  })
1342
}
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
#' @inheritSection teal::example_module Reporting
46
#'
47
#' @examplesShinylive
48
#' library(teal.modules.general)
49
#' interactive <- function() TRUE
50
#' {{ next_example }}
51
#' @examples
52
#' # general data example
53
#' data <- teal_data()
54
#' data <- within(data, {
55
#'   countries <- data.frame(
56
#'     id = c("DE", "FR", "IT", "ES", "PT", "GR", "NL", "BE", "LU", "AT"),
57
#'     government = factor(
58
#'       c(2, 2, 2, 1, 2, 2, 1, 1, 1, 2),
59
#'       labels = c("Monarchy", "Republic")
60
#'     ),
61
#'     language_family = factor(
62
#'       c(1, 3, 3, 3, 3, 2, 1, 1, 3, 1),
63
#'       labels = c("Germanic", "Hellenic", "Romance")
64
#'     ),
65
#'     population = c(83, 67, 60, 47, 10, 11, 17, 11, 0.6, 9),
66
#'     area = c(357, 551, 301, 505, 92, 132, 41, 30, 2.6, 83),
67
#'     gdp = c(3.4, 2.7, 2.1, 1.4, 0.3, 0.2, 0.7, 0.5, 0.1, 0.4),
68
#'     debt = c(2.1, 2.3, 2.4, 2.6, 2.3, 2.4, 2.3, 2.4, 2.3, 2.4)
69
#'   )
70
#'   sales <- data.frame(
71
#'     id = 1:50,
72
#'     country_id = sample(
73
#'       c("DE", "FR", "IT", "ES", "PT", "GR", "NL", "BE", "LU", "AT"),
74
#'       size = 50,
75
#'       replace = TRUE
76
#'     ),
77
#'     year = sort(sample(2010:2020, 50, replace = TRUE)),
78
#'     venue = sample(c("small", "medium", "large", "online"), 50, replace = TRUE),
79
#'     cancelled = sample(c(TRUE, FALSE), 50, replace = TRUE),
80
#'     quantity = rnorm(50, 100, 20),
81
#'     costs = rnorm(50, 80, 20),
82
#'     profit = rnorm(50, 20, 10)
83
#'   )
84
#' })
85
#' join_keys(data) <- join_keys(
86
#'   join_key("countries", "countries", "id"),
87
#'   join_key("sales", "sales", "id"),
88
#'   join_key("countries", "sales", c("id" = "country_id"))
89
#' )
90
#'
91
#' app <- init(
92
#'   data = data,
93
#'   modules = modules(
94
#'     tm_g_scatterplotmatrix(
95
#'       label = "Scatterplot matrix",
96
#'       variables = list(
97
#'         data_extract_spec(
98
#'           dataname = "countries",
99
#'           select = select_spec(
100
#'             label = "Select variables:",
101
#'             choices = variable_choices(data[["countries"]]),
102
#'             selected = c("area", "gdp", "debt"),
103
#'             multiple = TRUE,
104
#'             ordered = TRUE,
105
#'             fixed = FALSE
106
#'           )
107
#'         ),
108
#'         data_extract_spec(
109
#'           dataname = "sales",
110
#'           filter = filter_spec(
111
#'             label = "Select variable:",
112
#'             vars = "country_id",
113
#'             choices = value_choices(data[["sales"]], "country_id"),
114
#'             selected = c("DE", "FR", "IT", "PT", "GR", "NL", "BE", "LU", "AT"),
115
#'             multiple = TRUE
116
#'           ),
117
#'           select = select_spec(
118
#'             label = "Select variables:",
119
#'             choices = variable_choices(data[["sales"]], c("quantity", "costs", "profit")),
120
#'             selected = c("quantity", "costs", "profit"),
121
#'             multiple = TRUE,
122
#'             ordered = TRUE,
123
#'             fixed = FALSE
124
#'           )
125
#'         )
126
#'       )
127
#'     )
128
#'   )
129
#' )
130
#' if (interactive()) {
131
#'   shinyApp(app$ui, app$server)
132
#' }
133
#'
134
#' @examplesShinylive
135
#' library(teal.modules.general)
136
#' interactive <- function() TRUE
137
#' {{ next_example }}
138
#' @examples
139
#' # CDISC data example
140
#' data <- teal_data()
141
#' data <- within(data, {
142
#'   ADSL <- teal.data::rADSL
143
#'   ADRS <- teal.data::rADRS
144
#' })
145
#' join_keys(data) <- default_cdisc_join_keys[names(data)]
146
#'
147
#' app <- init(
148
#'   data = data,
149
#'   modules = modules(
150
#'     tm_g_scatterplotmatrix(
151
#'       label = "Scatterplot matrix",
152
#'       variables = list(
153
#'         data_extract_spec(
154
#'           dataname = "ADSL",
155
#'           select = select_spec(
156
#'             label = "Select variables:",
157
#'             choices = variable_choices(data[["ADSL"]]),
158
#'             selected = c("AGE", "RACE", "SEX"),
159
#'             multiple = TRUE,
160
#'             ordered = TRUE,
161
#'             fixed = FALSE
162
#'           )
163
#'         ),
164
#'         data_extract_spec(
165
#'           dataname = "ADRS",
166
#'           filter = filter_spec(
167
#'             label = "Select endpoints:",
168
#'             vars = c("PARAMCD", "AVISIT"),
169
#'             choices = value_choices(data[["ADRS"]], c("PARAMCD", "AVISIT"), c("PARAM", "AVISIT")),
170
#'             selected = "INVET - END OF INDUCTION",
171
#'             multiple = TRUE
172
#'           ),
173
#'           select = select_spec(
174
#'             label = "Select variables:",
175
#'             choices = variable_choices(data[["ADRS"]]),
176
#'             selected = c("AGE", "AVAL", "ADY"),
177
#'             multiple = TRUE,
178
#'             ordered = TRUE,
179
#'             fixed = FALSE
180
#'           )
181
#'         )
182
#'       )
183
#'     )
184
#'   )
185
#' )
186
#' if (interactive()) {
187
#'   shinyApp(app$ui, app$server)
188
#' }
189
#'
190
#' @export
191
#'
192
tm_g_scatterplotmatrix <- function(label = "Scatterplot Matrix",
193
                                   variables,
194
                                   plot_height = c(600, 200, 2000),
195
                                   plot_width = NULL,
196
                                   pre_output = NULL,
197
                                   post_output = NULL,
198
                                   transformators = list(),
199
                                   decorators = list()) {
200!
  message("Initializing tm_g_scatterplotmatrix")
201
202
  # Normalize the parameters
203!
  if (inherits(variables, "data_extract_spec")) variables <- list(variables)
204
205
  # Start of assertions
206!
  checkmate::assert_string(label)
207!
  checkmate::assert_list(variables, types = "data_extract_spec")
208
209!
  checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE)
210!
  checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height")
211!
  checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE)
212!
  checkmate::assert_numeric(
213!
    plot_width[1],
214!
    lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width"
215
  )
216
217!
  checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
218!
  checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
219
220!
  assert_decorators(decorators, "plot")
221
  # End of assertions
222
223
  # Make UI args
224!
  args <- as.list(environment())
225
226!
  ans <- module(
227!
    label = label,
228!
    server = srv_g_scatterplotmatrix,
229!
    ui = ui_g_scatterplotmatrix,
230!
    ui_args = args,
231!
    server_args = list(
232!
      variables = variables,
233!
      plot_height = plot_height,
234!
      plot_width = plot_width,
235!
      decorators = decorators
236
    ),
237!
    transformators = transformators,
238!
    datanames = teal.transform::get_extract_datanames(variables)
239
  )
240!
  attr(ans, "teal_bookmarkable") <- TRUE
241!
  ans
242
}
243
244
# UI function for the scatterplot matrix module
245
ui_g_scatterplotmatrix <- function(id, ...) {
246!
  args <- list(...)
247!
  is_single_dataset_value <- teal.transform::is_single_dataset(args$variables)
248!
  ns <- NS(id)
249!
  teal.widgets::standard_layout(
250!
    output = teal.widgets::white_small_well(
251!
      textOutput(ns("message")),
252!
      tags$br(),
253!
      teal.widgets::plot_with_settings_ui(id = ns("myplot"))
254
    ),
255!
    encoding = tags$div(
256!
      tags$label("Encodings", class = "text-primary"),
257!
      teal.transform::datanames_input(args$variables),
258!
      teal.transform::data_extract_ui(
259!
        id = ns("variables"),
260!
        label = "Variables",
261!
        data_extract_spec = args$variables,
262!
        is_single_dataset = is_single_dataset_value
263
      ),
264!
      tags$hr(),
265!
      ui_decorate_teal_data(ns("decorator"), decorators = select_decorators(args$decorators, "plot")),
266!
      bslib::accordion(
267!
        open = TRUE,
268!
        bslib::accordion_panel(
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!
    pre_output = args$pre_output,
292!
    post_output = args$post_output
293
  )
294
}
295
296
# Server function for the scatterplot matrix module
297
srv_g_scatterplotmatrix <- function(id,
298
                                    data,
299
                                    variables,
300
                                    plot_height,
301
                                    plot_width,
302
                                    decorators) {
303!
  checkmate::assert_class(data, "reactive")
304!
  checkmate::assert_class(isolate(data()), "teal_data")
305!
  moduleServer(id, function(input, output, session) {
306!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
307
308!
    selector_list <- teal.transform::data_extract_multiple_srv(
309!
      data_extract = list(variables = variables),
310!
      datasets = data,
311!
      select_validation_rule = list(
312!
        variables = ~ if (length(.) <= 1) "Please select at least 2 columns."
313
      )
314
    )
315
316!
    iv_r <- reactive({
317!
      iv <- shinyvalidate::InputValidator$new()
318!
      teal.transform::compose_and_enable_validators(iv, selector_list)
319
    })
320
321!
    anl_merged_input <- teal.transform::merge_expression_srv(
322!
      datasets = data,
323!
      selector_list = selector_list
324
    )
325
326!
    anl_merged_q <- reactive({
327!
      req(anl_merged_input())
328!
      obj <- data()
329!
      teal.reporter::teal_card(obj) <- c(
330!
        teal.reporter::teal_card(obj),
331!
        teal.reporter::teal_card("## Module's output(s)")
332
      )
333!
      qenv <- teal.code::eval_code(obj, "library(dplyr);library(lattice)")
334!
      teal.code::eval_code(qenv, as.expression(anl_merged_input()$expr))
335
    })
336
337!
    merged <- list(
338!
      anl_input_r = anl_merged_input,
339!
      anl_q_r = anl_merged_q
340
    )
341
342
    # plot
343!
    output_q <- reactive({
344!
      teal::validate_inputs(iv_r())
345
346!
      qenv <- merged$anl_q_r()
347!
      ANL <- qenv[["ANL"]]
348
349!
      cols_names <- merged$anl_input_r()$columns_source$variables
350!
      alpha <- input$alpha
351!
      cex <- input$cex
352!
      add_cor <- input$cor
353!
      cor_method <- input$cor_method
354!
      cor_na_omit <- input$cor_na_omit
355
356!
      cor_na_action <- if (isTruthy(cor_na_omit)) {
357!
        "na.omit"
358
      } else {
359!
        "na.fail"
360
      }
361
362!
      teal::validate_has_data(ANL, 10)
363!
      teal::validate_has_data(ANL[, cols_names, drop = FALSE], 10, complete = TRUE, allow_inf = FALSE)
364
365
      # get labels and proper variable names
366!
      varnames <- varname_w_label(cols_names, ANL, wrap_width = 20)
367
368
      # check character columns. If any, then those are converted to factors
369!
      check_char <- vapply(ANL[, cols_names], is.character, logical(1))
370!
      if (any(check_char)) {
371!
        qenv <- teal.code::eval_code(
372!
          qenv,
373!
          substitute(
374!
            expr = ANL <- ANL[, cols_names] %>%
375!
              dplyr::mutate_if(is.character, as.factor) %>%
376!
              droplevels(),
377!
            env = list(cols_names = cols_names)
378
          )
379
        )
380
      } else {
381!
        qenv <- teal.code::eval_code(
382!
          qenv,
383!
          substitute(
384!
            expr = ANL <- ANL[, cols_names] %>%
385!
              droplevels(),
386!
            env = list(cols_names = cols_names)
387
          )
388
        )
389
      }
390
391
392
      # create plot
393!
      teal.reporter::teal_card(qenv) <- c(teal.reporter::teal_card(qenv), "### Plot")
394
395!
      if (add_cor) {
396!
        shinyjs::show("cor_method")
397!
        shinyjs::show("cor_use")
398!
        shinyjs::show("cor_na_omit")
399
400!
        qenv <- teal.code::eval_code(
401!
          qenv,
402!
          substitute(
403!
            expr = {
404!
              plot <- lattice::splom(
405!
                ANL,
406!
                varnames = varnames_value,
407!
                panel = function(x, y, ...) {
408!
                  lattice::panel.splom(x = x, y = y, ...)
409!
                  cpl <- lattice::current.panel.limits()
410!
                  lattice::panel.text(
411!
                    mean(cpl$xlim),
412!
                    mean(cpl$ylim),
413!
                    get_scatterplotmatrix_stats(
414!
                      x,
415!
                      y,
416!
                      .f = stats::cor.test,
417!
                      .f_args = list(method = cor_method, na.action = cor_na_action)
418
                    ),
419!
                    alpha = 0.6,
420!
                    fontsize = 18,
421!
                    fontface = "bold"
422
                  )
423
                },
424!
                pch = 16,
425!
                alpha = alpha_value,
426!
                cex = cex_value
427
              )
428
            },
429!
            env = list(
430!
              varnames_value = varnames,
431!
              cor_method = cor_method,
432!
              cor_na_action = cor_na_action,
433!
              alpha_value = alpha,
434!
              cex_value = cex
435
            )
436
          )
437
        )
438
      } else {
439!
        shinyjs::hide("cor_method")
440!
        shinyjs::hide("cor_use")
441!
        shinyjs::hide("cor_na_omit")
442!
        qenv <- teal.code::eval_code(
443!
          qenv,
444!
          substitute(
445!
            expr = {
446!
              plot <- lattice::splom(
447!
                ANL,
448!
                varnames = varnames_value,
449!
                pch = 16,
450!
                alpha = alpha_value,
451!
                cex = cex_value
452
              )
453
            },
454!
            env = list(varnames_value = varnames, alpha_value = alpha, cex_value = cex)
455
          )
456
        )
457
      }
458!
      qenv
459
    })
460
461!
    decorated_output_q <- srv_decorate_teal_data(
462!
      id = "decorator",
463!
      data = output_q,
464!
      decorators = select_decorators(decorators, "plot"),
465!
      expr = quote(plot)
466
    )
467
468!
    plot_r <- reactive(req(decorated_output_q())[["plot"]])
469
470
    # Insert the plot into a plot_with_settings module
471!
    pws <- teal.widgets::plot_with_settings_srv(
472!
      id = "myplot",
473!
      plot_r = plot_r,
474!
      height = plot_height,
475!
      width = plot_width
476
    )
477
478
    # show a message if conversion to factors took place
479!
    output$message <- renderText({
480!
      req(iv_r()$is_valid())
481!
      req(selector_list()$variables())
482!
      ANL <- merged$anl_q_r()[["ANL"]]
483!
      cols_names <- unique(unname(do.call(c, merged$anl_input_r()$columns_source)))
484!
      check_char <- vapply(ANL[, cols_names], is.character, logical(1))
485!
      if (any(check_char)) {
486!
        is_single <- sum(check_char) == 1
487!
        paste(
488!
          "Character",
489!
          ifelse(is_single, "variable", "variables"),
490!
          paste0("(", paste(cols_names[check_char], collapse = ", "), ")"),
491!
          ifelse(is_single, "was", "were"),
492!
          "converted to",
493!
          ifelse(is_single, "factor.", "factors.")
494
        )
495
      } else {
496
        ""
497
      }
498
    })
499
500!
    set_chunk_dims(pws, decorated_output_q)
501
  })
502
}
503
504
#' Get stats for x-y pairs in scatterplot matrix
505
#'
506
#' Uses [stats::cor.test()] per default for all numerical input variables and converts results
507
#' to character vector.
508
#' Could be extended if different stats for different variable types are needed.
509
#' Meant to be called from [lattice::panel.text()].
510
#'
511
#' Presently we need to use a formula input for `stats::cor.test` because
512
#' `na.fail` only gets evaluated when a formula is passed (see below).
513
#' ```
514
#' x = c(1,3,5,7,NA)
515
#' y = c(3,6,7,8,1)
516
#' stats::cor.test(x, y, na.action = "na.fail")
517
#' stats::cor.test(~ x + y,  na.action = "na.fail")
518
#' ```
519
#'
520
#' @param x,y (`numeric`) vectors of data values. `x` and `y` must have the same length.
521
#' @param .f (`function`) function that accepts x and y as formula input `~ x + y`.
522
#' Default `stats::cor.test`.
523
#' @param .f_args (`list`) of arguments to be passed to `.f`.
524
#' @param round_stat (`integer(1)`) optional, number of decimal places to use when rounding the estimate.
525
#' @param round_pval (`integer(1)`) optional, number of decimal places to use when rounding the p-value.
526
#'
527
#' @return Character with stats. For [stats::cor.test()] correlation coefficient and p-value.
528
#'
529
#' @examples
530
#' set.seed(1)
531
#' x <- runif(25, 0, 1)
532
#' y <- runif(25, 0, 1)
533
#' x[c(3, 10, 18)] <- NA
534
#'
535
#' get_scatterplotmatrix_stats(x, y, .f = stats::cor.test, .f_args = list(method = "pearson"))
536
#' get_scatterplotmatrix_stats(x, y, .f = stats::cor.test, .f_args = list(
537
#'   method = "pearson",
538
#'   na.action = na.fail
539
#' ))
540
#'
541
#' @export
542
#'
543
get_scatterplotmatrix_stats <- function(x, y,
544
                                        .f = stats::cor.test,
545
                                        .f_args = list(),
546
                                        round_stat = 2,
547
                                        round_pval = 4) {
5486x
  if (is.numeric(x) && is.numeric(y)) {
5493x
    stat <- tryCatch(do.call(.f, c(list(~ x + y), .f_args)), error = function(e) NA)
550
5513x
    if (anyNA(stat)) {
5521x
      return("NA")
5532x
    } else if (all(c("estimate", "p.value") %in% names(stat))) {
5542x
      return(paste(
5552x
        c(
5562x
          paste0(names(stat$estimate), ":", round(stat$estimate, round_stat)),
5572x
          paste0("P:", round(stat$p.value, round_pval))
558
        ),
5592x
        collapse = "\n"
560
      ))
561
    } else {
562!
      stop("function not supported")
563
    }
564
  } else {
5653x
    if ("method" %in% names(.f_args)) {
5663x
      if (.f_args$method == "pearson") {
5671x
        return("cor:-")
568
      }
5692x
      if (.f_args$method == "kendall") {
5701x
        return("tau:-")
571
      }
5721x
      if (.f_args$method == "spearman") {
5731x
        return("rho:-")
574
      }
575
    }
576!
    return("-")
577
  }
578
}
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_stop(
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!
    teal.widgets::standard_layout(
162!
      output = teal.widgets::white_small_well(
163!
        bslib::page_fluid(
164!
          checkboxInput(
165!
            ns("if_distinct"),
166!
            "Show only distinct rows:",
167!
            value = FALSE
168
          )
169
        ),
170!
        bslib::page_fluid(
171!
          uiOutput(ns("dataset_table"))
172
        )
173
      ),
174!
      pre_output = pre_output,
175!
      post_output = post_output
176
    )
177
  )
178
}
179
180
# Server page module
181
srv_page_data_table <- function(id,
182
                                data,
183
                                datanames,
184
                                variables_selected,
185
                                dt_args,
186
                                dt_options,
187
                                server_rendering) {
188!
  checkmate::assert_class(data, "reactive")
189!
  checkmate::assert_class(isolate(data()), "teal_data")
190!
  moduleServer(id, function(input, output, session) {
191!
    teal.logger::log_shiny_input_changes(input, namespace = "teal.modules.general")
192
193!
    if_filtered <- reactive(as.logical(input$if_filtered))
194!
    if_distinct <- reactive(as.logical(input$if_distinct))
195
196!
    datanames <- Filter(function(name) {
197!
      is.data.frame(isolate(data())[[name]])
198!
    }, if (identical(datanames, "all")) names(isolate(data())) else datanames)
199
200
201!
    output$dataset_table <- renderUI({
202!
      do.call(
203!
        tabsetPanel,
204!
        c(
205!
          list(id = session$ns("dataname_tab")),
206!
          lapply(
207!
            datanames,
208!
            function(x) {
209!
              dataset <- isolate(data()[[x]])
210!
              choices <- names(dataset)
211!
              labels <- vapply(
212!
                dataset,
213!
                function(x) ifelse(is.null(attr(x, "label")), "", attr(x, "label")),
214!
                character(1)
215
              )
216!
              names(choices) <- ifelse(
217!
                is.na(labels) | labels == "",
218!
                choices,
219!
                paste(choices, labels, sep = ": ")
220
              )
221!
              variables_selected <- if (!is.null(variables_selected[[x]])) {
222!
                variables_selected[[x]]
223
              } else {
224!
                utils::head(choices)
225
              }
226!
              tabPanel(
227!
                title = x,
228!
                bslib::layout_columns(
229!
                  col_widths = 12,
230!
                  ui_data_table(
231!
                    id = session$ns(x),
232!
                    choices = choices,
233!
                    selected = variables_selected
234
                  )
235
                )
236
              )
237
            }
238
          )
239
        )
240
      )
241
    })
242
243!
    lapply(
244!
      datanames,
245!
      function(x) {
246!
        srv_data_table(
247!
          id = x,
248!
          data = data,
249!
          dataname = x,
250!
          if_filtered = if_filtered,
251!
          if_distinct = if_distinct,
252!
          dt_args = dt_args,
253!
          dt_options = dt_options,
254!
          server_rendering = server_rendering
255
        )
256
      }
257
    )
258
  })
259
}
260
261
# UI function for the data_table module
262
ui_data_table <- function(id, choices, selected) {
263!
  ns <- NS(id)
264
265!
  if (!is.null(selected)) {
266!
    all_choices <- choices
267!
    choices <- c(selected, setdiff(choices, selected))
268!
    names(choices) <- names(all_choices)[match(choices, all_choices)]
269
  }
270
271!
  tagList(
272!
    teal.widgets::get_dt_rows(ns("data_table"), ns("dt_rows")),
273!
    bslib::page_fluid(
274!
      teal.widgets::optionalSelectInput(
275!
        ns("variables"),
276!
        "Select variables:",
277!
        choices = choices,
278!
        selected = selected,
279!
        multiple = TRUE,
280!
        width = "100%"
281
      )
282
    ),
283!
    bslib::page_fluid(
284!
      DT::dataTableOutput(ns("data_table"), width = "100%")
285
    )
286
  )
287
}
288
289
# Server function for the data_table module
290
srv_data_table <- function(id,
291
                           data,
292
                           dataname,
293
                           if_filtered,
294
                           if_distinct,
295
                           dt_args,
296
                           dt_options,
297
                           server_rendering) {
298!
  moduleServer(id, function(input, output, session) {
299!
    iv <- shinyvalidate::InputValidator$new()
300!
    iv$add_rule("variables", shinyvalidate::sv_required("Please select valid variable names"))
301!
    iv$add_rule("variables", shinyvalidate::sv_in_set(
302!
      set = names(isolate(data())[[dataname]]), message_fmt = "Not all selected variables exist in the data"
303
    ))
304!
    iv$enable()
305
306!
    data_table_data <- reactive({
307!
      df <- data()[[dataname]]
308
309!
      teal::validate_has_data(df, min_nrow = 1L, msg = paste("data", dataname, "is empty"))
310!
      qenv <- teal.code::eval_code(
311!
        data(),
312!
        "library(dplyr);library(DT)"
313
      )
314!
      teal.code::eval_code(
315!
        qenv,
316!
        substitute(
317!
          expr = {
318!
            variables <- vars
319!
            dataframe_selected <- if (if_distinct) {
320!
              dplyr::count(dataname, dplyr::across(dplyr::all_of(variables)))
321
            } else {
322!
              dataname[variables]
323
            }
324!
            dt_args <- args
325!
            dt_args$options <- dt_options
326!
            if (!is.null(dt_rows)) {
327!
              dt_args$options$pageLength <- dt_rows
328
            }
329!
            dt_args$data <- dataframe_selected
330!
            table <- do.call(DT::datatable, dt_args)
331
          },
332!
          env = list(
333!
            dataname = as.name(dataname),
334!
            if_distinct = if_distinct(),
335!
            vars = input$variables,
336!
            args = dt_args,
337!
            dt_options = dt_options,
338!
            dt_rows = input$dt_rows
339
          )
340
        )
341
      )
342
    })
343
344!
    output$data_table <- DT::renderDataTable(server = server_rendering, {
345!
      teal::validate_inputs(iv)
346!
      req(data_table_data())[["table"]]
347
    })
348
  })
349
}
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

[8]ページ先頭

©2009-2025 Movatter.jp