| 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()) { | |
| 217 | 18x | message("Initializing tm_g_bivariate") |
| 218 | ||
| 219 | # Normalize the parameters | |
| 220 | 14x | if (inherits(x, "data_extract_spec")) x <- list(x) |
| 221 | 13x | if (inherits(y, "data_extract_spec")) y <- list(y) |
| 222 | 1x | if (inherits(row_facet, "data_extract_spec")) row_facet <- list(row_facet) |
| 223 | 1x | if (inherits(col_facet, "data_extract_spec")) col_facet <- list(col_facet) |
| 224 | 1x | if (inherits(color, "data_extract_spec")) color <- list(color) |
| 225 | 1x | if (inherits(fill, "data_extract_spec")) fill <- list(fill) |
| 226 | 1x | if (inherits(size, "data_extract_spec")) size <- list(size) |
| 227 | ||
| 228 | # Start of assertions | |
| 229 | 18x | checkmate::assert_string(label) |
| 230 | ||
| 231 | 18x | checkmate::assert_list(x, types = "data_extract_spec") |
| 232 | 18x | assert_single_selection(x) |
| 233 | ||
| 234 | 16x | checkmate::assert_list(y, types = "data_extract_spec") |
| 235 | 16x | assert_single_selection(y) |
| 236 | ||
| 237 | 14x | checkmate::assert_list(row_facet, types = "data_extract_spec", null.ok = TRUE) |
| 238 | 14x | assert_single_selection(row_facet) |
| 239 | ||
| 240 | 14x | checkmate::assert_list(col_facet, types = "data_extract_spec", null.ok = TRUE) |
| 241 | 14x | assert_single_selection(col_facet) |
| 242 | ||
| 243 | 14x | checkmate::assert_flag(facet) |
| 244 | ||
| 245 | 14x | checkmate::assert_list(color, types = "data_extract_spec", null.ok = TRUE) |
| 246 | 14x | assert_single_selection(color) |
| 247 | ||
| 248 | 14x | checkmate::assert_list(fill, types = "data_extract_spec", null.ok = TRUE) |
| 249 | 14x | assert_single_selection(fill) |
| 250 | ||
| 251 | 14x | checkmate::assert_list(size, types = "data_extract_spec", null.ok = TRUE) |
| 252 | 14x | assert_single_selection(size) |
| 253 | ||
| 254 | 14x | checkmate::assert_flag(use_density) |
| 255 | ||
| 256 | # Determines color, fill & size if they are not explicitly set | |
| 257 | 14x | checkmate::assert_flag(color_settings) |
| 258 | 14x | if (color_settings) { |
| 259 | 2x | if (is.null(color)) { |
| 260 | 2x | color <- x |
| 261 | 2x | color[[1]]$select <- teal.transform::select_spec(choices = color[[1]]$select$choices, selected = NULL) |
| 262 | } | |
| 263 | 2x | if (is.null(fill)) { |
| 264 | 2x | fill <- x |
| 265 | 2x | fill[[1]]$select <- teal.transform::select_spec(choices = fill[[1]]$select$choices, selected = NULL) |
| 266 | } | |
| 267 | 2x | if (is.null(size)) { |
| 268 | 2x | size <- x |
| 269 | 2x | size[[1]]$select <- teal.transform::select_spec(choices = size[[1]]$select$choices, selected = NULL) |
| 270 | } | |
| 271 | } else { | |
| 272 | 12x | if (!is.null(c(color, fill, size))) { |
| 273 | 3x | stop("'color_settings' argument needs to be set to TRUE if 'color', 'fill', and/or 'size' is/are supplied.") |
| 274 | } | |
| 275 | } | |
| 276 | ||
| 277 | 11x | checkmate::assert_flag(free_x_scales) |
| 278 | 11x | checkmate::assert_flag(free_y_scales) |
| 279 | ||
| 280 | 11x | checkmate::assert_numeric(plot_height, len = 3, any.missing = FALSE, finite = TRUE) |
| 281 | 10x | checkmate::assert_numeric(plot_height[1], lower = plot_height[2], upper = plot_height[3], .var.name = "plot_height") |
| 282 | 8x | checkmate::assert_numeric(plot_width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
| 283 | 7x | checkmate::assert_numeric( |
| 284 | 7x | plot_width[1], |
| 285 | 7x | lower = plot_width[2], upper = plot_width[3], null.ok = TRUE, .var.name = "plot_width" |
| 286 | ) | |
| 287 | ||
| 288 | 5x | checkmate::assert_flag(rotate_xaxis_labels) |
| 289 | 5x | checkmate::assert_flag(swap_axes) |
| 290 | ||
| 291 | 5x | ggtheme <- match.arg(ggtheme) |
| 292 | 5x | checkmate::assert_class(ggplot2_args, "ggplot2_args") |
| 293 | ||
| 294 | 5x | checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
| 295 | 5x | checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE) |
| 296 | ||
| 297 | 5x | assert_decorators(decorators, "plot") |
| 298 | # End of assertions | |
| 299 | ||
| 300 | # Make UI args | |
| 301 | 5x | args <- as.list(environment()) |
| 302 | ||
| 303 | 5x | data_extract_list <- list( |
| 304 | 5x | x = x, |
| 305 | 5x | y = y, |
| 306 | 5x | row_facet = row_facet, |
| 307 | 5x | col_facet = col_facet, |
| 308 | 5x | color_settings = color_settings, |
| 309 | 5x | color = color, |
| 310 | 5x | fill = fill, |
| 311 | 5x | size = size |
| 312 | ) | |
| 313 | ||
| 314 | 5x | ans <- module( |
| 315 | 5x | label = label, |
| 316 | 5x | server = srv_g_bivariate, |
| 317 | 5x | ui = ui_g_bivariate, |
| 318 | 5x | ui_args = args, |
| 319 | 5x | server_args = c( |
| 320 | 5x | data_extract_list, |
| 321 | 5x | list(plot_height = plot_height, plot_width = plot_width, ggplot2_args = ggplot2_args, decorators = decorators) |
| 322 | ), | |
| 323 | 5x | transformators = transformators, |
| 324 | 5x | datanames = teal.transform::get_extract_datanames(data_extract_list) |
| 325 | ) | |
| 326 | 5x | attr(ans, "teal_bookmarkable") <- TRUE |
| 327 | 5x | 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()) { | |
| 807 | 46x | x_class <- switch(x_class, |
| 808 | 46x | "character" = , |
| 809 | 46x | "ordered" = , |
| 810 | 46x | "logical" = , |
| 811 | 46x | "factor" = "factor", |
| 812 | 46x | "integer" = , |
| 813 | 46x | "numeric" = "numeric", |
| 814 | 46x | "NULL" = "NULL", |
| 815 | 46x | stop("unsupported x_class: ", x_class) |
| 816 | ) | |
| 817 | 46x | y_class <- switch(y_class, |
| 818 | 46x | "character" = , |
| 819 | 46x | "ordered" = , |
| 820 | 46x | "logical" = , |
| 821 | 46x | "factor" = "factor", |
| 822 | 46x | "integer" = , |
| 823 | 46x | "numeric" = "numeric", |
| 824 | 46x | "NULL" = "NULL", |
| 825 | 46x | stop("unsupported y_class: ", y_class) |
| 826 | ) | |
| 827 | ||
| 828 | 46x | if (all(c(x_class, y_class) == "NULL")) { |
| 829 | ! | stop("either x or y is required") |
| 830 | } | |
| 831 | ||
| 832 | 46x | reduce_plot_call <- function(...) { |
| 833 | 112x | args <- Filter(Negate(is.null), list(...)) |
| 834 | 112x | Reduce(function(x, y) call("+", x, y), args) |
| 835 | } | |
| 836 | ||
| 837 | 46x | plot_call <- substitute(ggplot2::ggplot(data_name), env = list(data_name = as.name(data_name))) |
| 838 | ||
| 839 | # Single data plots | |
| 840 | 46x | if (x_class == "numeric" && y_class == "NULL") { |
| 841 | 6x | plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = xval), env = list(xval = x))) |
| 842 | ||
| 843 | 6x | if (freq) { |
| 844 | 4x | plot_call <- reduce_plot_call( |
| 845 | 4x | plot_call, |
| 846 | 4x | quote(ggplot2::geom_histogram(bins = 30)), |
| 847 | 4x | quote(ggplot2::ylab("Frequency")) |
| 848 | ) | |
| 849 | } else { | |
| 850 | 2x | plot_call <- reduce_plot_call( |
| 851 | 2x | plot_call, |
| 852 | 2x | quote(ggplot2::geom_histogram(bins = 30, ggplot2::aes(y = ggplot2::after_stat(density)))), |
| 853 | 2x | quote(ggplot2::geom_density(ggplot2::aes(y = ggplot2::after_stat(density)))), |
| 854 | 2x | quote(ggplot2::ylab("Density")) |
| 855 | ) | |
| 856 | } | |
| 857 | 40x | } else if (x_class == "NULL" && y_class == "numeric") { |
| 858 | 6x | plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = yval), env = list(yval = y))) |
| 859 | ||
| 860 | 6x | if (freq) { |
| 861 | 4x | plot_call <- reduce_plot_call( |
| 862 | 4x | plot_call, |
| 863 | 4x | quote(ggplot2::geom_histogram(bins = 30)), |
| 864 | 4x | quote(ggplot2::ylab("Frequency")) |
| 865 | ) | |
| 866 | } else { | |
| 867 | 2x | plot_call <- reduce_plot_call( |
| 868 | 2x | plot_call, |
| 869 | 2x | quote(ggplot2::geom_histogram(bins = 30, ggplot2::aes(y = ggplot2::after_stat(density)))), |
| 870 | 2x | quote(ggplot2::geom_density(ggplot2::aes(y = ggplot2::after_stat(density)))), |
| 871 | 2x | quote(ggplot2::ylab("Density")) |
| 872 | ) | |
| 873 | } | |
| 874 | 34x | } else if (x_class == "factor" && y_class == "NULL") { |
| 875 | 4x | plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = xval), env = list(xval = x))) |
| 876 | ||
| 877 | 4x | if (freq) { |
| 878 | 2x | plot_call <- reduce_plot_call( |
| 879 | 2x | plot_call, |
| 880 | 2x | quote(ggplot2::geom_bar()), |
| 881 | 2x | quote(ggplot2::ylab("Frequency")) |
| 882 | ) | |
| 883 | } else { | |
| 884 | 2x | plot_call <- reduce_plot_call( |
| 885 | 2x | plot_call, |
| 886 | 2x | quote(ggplot2::geom_bar(ggplot2::aes(y = ggplot2::after_stat(prop), group = 1))), |
| 887 | 2x | quote(ggplot2::ylab("Fraction")) |
| 888 | ) | |
| 889 | } | |
| 890 | 30x | } else if (x_class == "NULL" && y_class == "factor") { |
| 891 | 4x | plot_call <- reduce_plot_call(plot_call, substitute(ggplot2::aes(x = yval), env = list(yval = y))) |
| 892 | ||
| 893 | 4x | if (freq) { |
| 894 | 2x | plot_call <- reduce_plot_call( |
| 895 | 2x | plot_call, |
| 896 | 2x | quote(ggplot2::geom_bar()), |
| 897 | 2x | quote(ggplot2::ylab("Frequency")) |
| 898 | ) | |
| 899 | } else { | |
| 900 | 2x | plot_call <- reduce_plot_call( |
| 901 | 2x | plot_call, |
| 902 | 2x | quote(ggplot2::geom_bar(ggplot2::aes(y = ggplot2::after_stat(prop), group = 1))), |
| 903 | 2x | quote(ggplot2::ylab("Fraction")) |
| 904 | ) | |
| 905 | } | |
| 906 | # Numeric Plots | |
| 907 | 26x | } else if (x_class == "numeric" && y_class == "numeric") { |
| 908 | 2x | plot_call <- reduce_plot_call( |
| 909 | 2x | plot_call, |
| 910 | 2x | 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) | |
| 912 | 2x | `if`( |
| 913 | 2x | !is.null(size), |
| 914 | 2x | substitute( |
| 915 | 2x | ggplot2::geom_point(alpha = alphaval, size = sizeval, pch = 21), |
| 916 | 2x | env = list(alphaval = alpha, sizeval = size) |
| 917 | ), | |
| 918 | 2x | substitute( |
| 919 | 2x | ggplot2::geom_point(alpha = alphaval, pch = 21), |
| 920 | 2x | env = list(alphaval = alpha) |
| 921 | ) | |
| 922 | ) | |
| 923 | ) | |
| 924 | 24x | } else if ((x_class == "numeric" && y_class == "factor") || (x_class == "factor" && y_class == "numeric")) { |
| 925 | 6x | plot_call <- reduce_plot_call( |
| 926 | 6x | plot_call, |
| 927 | 6x | substitute(ggplot2::aes(x = xval, y = yval), env = list(xval = x, yval = y)), |
| 928 | 6x | quote(ggplot2::geom_boxplot()) |
| 929 | ) | |
| 930 | # Factor and character plots | |
| 931 | 18x | } else if (x_class == "factor" && y_class == "factor") { |
| 932 | 18x | plot_call <- reduce_plot_call( |
| 933 | 18x | plot_call, |
| 934 | 18x | substitute( |
| 935 | 18x | teal.modules.general::geom_mosaic(ggplot2::aes(x = xval, fill = yval)), |
| 936 | 18x | env = list(xval = x, yval = y) |
| 937 | ) | |
| 938 | ) | |
| 939 | } else { | |
| 940 | ! | stop("x y type combination not allowed") |
| 941 | } | |
| 942 | ||
| 943 | 46x | labs_base <- if (x_class == "NULL") { |
| 944 | 10x | list(x = substitute(ylab, list(ylab = ylab))) |
| 945 | 46x | } else if (y_class == "NULL") { |
| 946 | 10x | list(x = substitute(xlab, list(xlab = xlab))) |
| 947 | } else { | |
| 948 | 26x | list( |
| 949 | 26x | x = substitute(xlab, list(xlab = xlab)), |
| 950 | 26x | y = substitute(ylab, list(ylab = ylab)) |
| 951 | ) | |
| 952 | } | |
| 953 | ||
| 954 | 46x | dev_ggplot2_args <- teal.widgets::ggplot2_args(labs = labs_base) |
| 955 | ||
| 956 | 46x | if (rotate_xaxis_labels) { |
| 957 | ! | dev_ggplot2_args$theme <- list(axis.text.x = quote(ggplot2::element_text(angle = 45, hjust = 1))) |
| 958 | } | |
| 959 | ||
| 960 | 46x | all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( |
| 961 | 46x | user_plot = ggplot2_args, |
| 962 | 46x | module_plot = dev_ggplot2_args |
| 963 | ) | |
| 964 | ||
| 965 | 46x | parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args(all_ggplot2_args, ggtheme = theme) |
| 966 | ||
| 967 | 46x | plot_call <- reduce_plot_call( |
| 968 | 46x | plot_call, |
| 969 | 46x | parsed_ggplot2_args$labs, |
| 970 | 46x | parsed_ggplot2_args$ggtheme, |
| 971 | 46x | parsed_ggplot2_args$theme |
| 972 | ) | |
| 973 | ||
| 974 | 46x | if (swap_axes) { |
| 975 | ! | plot_call <- reduce_plot_call(plot_call, quote(coord_flip())) |
| 976 | } | |
| 977 | ||
| 978 | 46x | 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 ( | |
| 1017 | 15x | !identical(colour, character(0)) && |
| 1018 | 15x | !identical(fill, character(0)) && |
| 1019 | 15x | is_point && |
| 1020 | 15x | !identical(size, character(0)) |
| 1021 | ) { | |
| 1022 | 1x | substitute( |
| 1023 | 1x | expr = ggplot2::aes(colour = colour_name, fill = fill_name, size = size_name), |
| 1024 | 1x | env = list(colour_name = as.name(colour), fill_name = as.name(fill), size_name = as.name(size)) |
| 1025 | ) | |
| 1026 | } else if ( | |
| 1027 | 14x | identical(colour, character(0)) && |
| 1028 | 14x | !identical(fill, character(0)) && |
| 1029 | 14x | is_point && |
| 1030 | 14x | identical(size, character(0)) |
| 1031 | ) { | |
| 1032 | 1x | substitute(expr = ggplot2::aes(fill = fill_name), env = list(fill_name = as.name(fill))) |
| 1033 | } else if ( | |
| 1034 | 13x | !identical(colour, character(0)) && |
| 1035 | 13x | !identical(fill, character(0)) && |
| 1036 | 13x | (!is_point || identical(size, character(0))) |
| 1037 | ) { | |
| 1038 | 3x | substitute( |
| 1039 | 3x | expr = ggplot2::aes(colour = colour_name, fill = fill_name), |
| 1040 | 3x | env = list(colour_name = as.name(colour), fill_name = as.name(fill)) |
| 1041 | ) | |
| 1042 | } else if ( | |
| 1043 | 10x | !identical(colour, character(0)) && |
| 1044 | 10x | identical(fill, character(0)) && |
| 1045 | 10x | (!is_point || identical(size, character(0))) |
| 1046 | ) { | |
| 1047 | 1x | substitute(expr = ggplot2::aes(colour = colour_name), env = list(colour_name = as.name(colour))) |
| 1048 | } else if ( | |
| 1049 | 9x | identical(colour, character(0)) && |
| 1050 | 9x | !identical(fill, character(0)) && |
| 1051 | 9x | (!is_point || identical(size, character(0))) |
| 1052 | ) { | |
| 1053 | 2x | substitute(expr = ggplot2::aes(fill = fill_name), env = list(fill_name = as.name(fill))) |
| 1054 | } else if ( | |
| 1055 | 7x | identical(colour, character(0)) && |
| 1056 | 7x | identical(fill, character(0)) && |
| 1057 | 7x | is_point && |
| 1058 | 7x | !identical(size, character(0)) |
| 1059 | ) { | |
| 1060 | 1x | substitute(expr = ggplot2::aes(size = size_name), env = list(size_name = as.name(size))) |
| 1061 | } else if ( | |
| 1062 | 6x | !identical(colour, character(0)) && |
| 1063 | 6x | identical(fill, character(0)) && |
| 1064 | 6x | is_point && |
| 1065 | 6x | !identical(size, character(0)) |
| 1066 | ) { | |
| 1067 | 1x | substitute( |
| 1068 | 1x | expr = ggplot2::aes(colour = colour_name, size = size_name), |
| 1069 | 1x | env = list(colour_name = as.name(colour), size_name = as.name(size)) |
| 1070 | ) | |
| 1071 | } else if ( | |
| 1072 | 5x | identical(colour, character(0)) && |
| 1073 | 5x | !identical(fill, character(0)) && |
| 1074 | 5x | is_point && |
| 1075 | 5x | !identical(size, character(0)) |
| 1076 | ) { | |
| 1077 | 1x | substitute( |
| 1078 | 1x | expr = ggplot2::aes(colour = colour_name, fill = fill_name, size = size_name), |
| 1079 | 1x | env = list(colour_name = as.name(fill), fill_name = as.name(fill), size_name = as.name(size)) |
| 1080 | ) | |
| 1081 | } else { | |
| 1082 | 4x | 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) { | |
| 201 | 3x | checkmate::assert_class(data, "reactive") |
| 202 | 3x | checkmate::assert_class(isolate(data()), "teal_data") |
| 203 | 3x | moduleServer(id, function(input, output, session) { |
| 204 | 3x | pre_decorated_q_r <- reactive({ |
| 205 | 3x | data_q <- req(data()) |
| 206 | 3x | teal.reporter::teal_card(data_q) <- c( |
| 207 | 3x | teal.reporter::teal_card(data_q), |
| 208 | 3x | teal.reporter::teal_card("## Module's output(s)") |
| 209 | ) | |
| 210 | 3x | data_q |
| 211 | }) | |
| 212 | ||
| 213 | 3x | q_r <- data_with_output_decorated <- teal::srv_transform_teal_data( |
| 214 | 3x | "extra_transform", |
| 215 | 3x | data = pre_decorated_q_r, |
| 216 | 3x | transformators = extra_transform |
| 217 | ) | |
| 218 | ||
| 219 | 3x | if (allow_download) { |
| 220 | 3x | output$download_rmd <- downloadHandler( |
| 221 | 3x | filename = function() sprintf("from_teal_module-%s.Rmd", format(Sys.time(), "%Y%m%d_%H%M")), |
| 222 | 3x | 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 | }, | |
| 242 | 3x | contentType = "text/plain" |
| 243 | ) | |
| 244 | } | |
| 245 | ||
| 246 | 3x | clean_up_r <- shiny::reactiveVal(list()) |
| 247 | # Can only clean on sessionEnded as temporary files are needed for the reporter | |
| 248 | # during session | |
| 249 | 3x | onSessionEnded(function() { |
| 250 | 3x | logger::log_debug("srv_rmarkdown: cleaning up temporary folders.") |
| 251 | 3x | lapply(shiny::isolate(clean_up_r()), function(f) f()) |
| 252 | 3x | }, session) |
| 253 | ||
| 254 | 3x | rendered_path_r <- reactive({ |
| 255 | 3x | datasets <- rlang::env_clone( |
| 256 | 3x | as.environment(req(q_r())), |
| 257 | 3x | parent = new.env() # Ensuring a clean parent environment that can load libraries |
| 258 | 3x | ) # Clone to use unlocked environment |
| 259 | 3x | temp_dir <- tempfile(pattern = "rmd_") |
| 260 | 3x | dir.create(temp_dir, showWarnings = FALSE, recursive = TRUE) |
| 261 | 3x | temp_rmd <- tempfile(pattern = "rmarkdown_module-", tmpdir = temp_dir, fileext = ".Rmd") |
| 262 | # Schedule cleanup of temp files when reactive is re-executed | |
| 263 | 3x | shiny::isolate({ |
| 264 | 3x | old_clean_up <- clean_up_r() |
| 265 | 3x | clean_up_r(c(old_clean_up, function() unlink(temp_dir, recursive = TRUE))) |
| 266 | }) | |
| 267 | 3x | writeLines(rmd_content, con = temp_rmd) |
| 268 | ||
| 269 | 3x | tryCatch( |
| 270 | { | |
| 271 | 3x | rmarkdown::render( |
| 272 | 3x | temp_rmd, |
| 273 | 3x | output_format = rmarkdown::md_document( |
| 274 | 3x | variant = "markdown", |
| 275 | 3x | standalone = TRUE, |
| 276 | 3x | dev = "png" |
| 277 | ), | |
| 278 | 3x | envir = datasets, |
| 279 | 3x | quiet = TRUE, |
| 280 | 3x | runtime = "static" |
| 281 | ) | |
| 282 | }, | |
| 283 | 3x | error = function(e) { |
| 284 | ! | warning("Error rendering RMD file: ", e$message) # verbose error in logs |
| 285 | ! | e |
| 286 | } | |
| 287 | ) | |
| 288 | }) | |
| 289 | ||
| 290 | 3x | rendered_html_r <- reactive({ |
| 291 | 2x | output_path <- req(rendered_path_r()) |
| 292 | 2x | validate( |
| 293 | 2x | need(inherits(output_path, "character"), "Error rendering RMD file. Please contact the app developer.") |
| 294 | ) | |
| 295 | 2x | shiny::includeMarkdown(output_path) |
| 296 | }) | |
| 297 | ||
| 298 | 3x | output$rmd_output <- renderUI(rendered_html_r()) |
| 299 | ||
| 300 | 3x | result <- reactive({ |
| 301 | 1x | out_data <- q_r() |
| 302 | 1x | report_doc <- .markdown_internal(rendered_path_r(), rendered_html_r()) |
| 303 | 1x | teal.reporter::teal_card(out_data) <- c( |
| 304 | 1x | teal.reporter::teal_card(out_data), report_doc |
| 305 | ) | |
| 306 | 1x | out_data |
| 307 | }) | |
| 308 | 3x | 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) { | |
| 327 | 1x | base_file <- basename(markdown_file) |
| 328 | ||
| 329 | # Create new custom structure with contents and images in base64 as attribute | |
| 330 | 1x | structure( |
| 331 | 1x | readLines(markdown_file), |
| 332 | 1x | class = c("markdown_internal", "character"), |
| 333 | 1x | parent_path = dirname(markdown_file), |
| 334 | 1x | old_base_path = sprintf("%s_files/", tools::file_path_sans_ext(base_file)), |
| 335 | 1x | 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) { | |
| 253 | 4x | output <- mapply(function(metadata, dataname) { |
| 254 | 6x | if (is.null(metadata)) { |
| 255 | 2x | return(data.frame(Dataset = character(0), Name = character(0), Value = character(0))) |
| 256 | } | |
| 257 | 4x | data.frame( |
| 258 | 4x | Dataset = dataname, |
| 259 | 4x | Name = names(metadata), |
| 260 | 4x | Value = unname(unlist(lapply(metadata, as.character))) |
| 261 | ) | |
| 262 | 4x | }, raw_metadata, datanames, SIMPLIFY = FALSE) |
| 263 | 4x | 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) { | |
| 1124 | 3x | 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) { | |
| 1137 | 3x | if (outlier_definition == 0) { |
| 1138 | 1x | return(rep(TRUE, length.out = length(var))) |
| 1139 | } | |
| 1140 | 2x | q1_q3 <- stats::quantile(var, probs = c(0.25, 0.75), type = 2, na.rm = TRUE) |
| 1141 | 2x | iqr <- q1_q3[2] - q1_q3[1] |
| 1142 | 2x | 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. | |
| 259 | 104x | if (any(vapply(x, function(.x) .x$select$multiple, logical(1)))) { |
| 260 | 4x | stop("'", .var.name, "' should not allow multiple selection") |
| 261 | } | |
| 262 | 100x | 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 | ||
| 312 | 5x | check_message <- checkmate::check_list(x, names = "named") |
| 313 | ||
| 314 | 5x | if (!is.null(names)) { |
| 315 | 5x | if (isTRUE(check_message)) { |
| 316 | 5x | 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 | ||
| 330 | 5x | if (!isTRUE(check_message)) { |
| 331 | ! | return(check_message) |
| 332 | } | |
| 333 | ||
| 334 | 5x | valid_elements <- vapply( |
| 335 | 5x | x, |
| 336 | 5x | checkmate::test_class, |
| 337 | 5x | classes = "teal_transform_module", |
| 338 | 5x | FUN.VALUE = logical(1L) |
| 339 | ) | |
| 340 | ||
| 341 | 5x | if (all(valid_elements)) { |
| 342 | 5x | 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) { | |
| 381 | 7x | checkmate::assert_class(teal_card, "teal_card") |
| 382 | 7x | checkmate::assert_list(attributes, names = "unique") |
| 383 | 7x | checkmate::assert_int(n, lower = 1) |
| 384 | 7x | checkmate::assert_character(inner_classes, null.ok = TRUE) |
| 385 | 7x | checkmate::assert_flag(quiet) |
| 386 | ||
| 387 | 7x | if (!inherits(teal_card[[length(teal_card)]], "chunk_output")) { |
| 388 | 1x | if (!quiet) { |
| 389 | 1x | warning("The last element of the `teal_card` is not a `chunk_output` object. No attributes were modified.") |
| 390 | } | |
| 391 | 1x | return(teal_card) |
| 392 | } | |
| 393 | ||
| 394 | 6x | for (ix in seq_len(length(teal_card))) { |
| 395 | 14x | if (ix > n) { |
| 396 | 4x | break |
| 397 | } | |
| 398 | 10x | current_ix <- length(teal_card) + 1 - ix |
| 399 | 10x | if (!inherits(teal_card[[current_ix]], "chunk_output")) { |
| 400 | 2x | if (!quiet) { |
| 401 | 1x | warning( |
| 402 | 1x | "The ", ix, |
| 403 | 1x | " to last element of the `teal_card` is not a `chunk_output` object. Skipping any further modifications." |
| 404 | ) | |
| 405 | } | |
| 406 | 2x | return(teal_card) |
| 407 | } | |
| 408 | ||
| 409 | if ( | |
| 410 | 8x | length(inner_classes) > 0 && |
| 411 | 8x | length(teal_card[[current_ix]]) >= 1 && |
| 412 | 8x | !checkmate::test_multi_class(teal_card[[current_ix]][[1]], inner_classes) |
| 413 | ) { | |
| 414 | 1x | next |
| 415 | } | |
| 416 | ||
| 417 | 7x | attributes(teal_card[[current_ix]]) <- utils::modifyList( |
| 418 | 7x | attributes(teal_card[[current_ix]]), |
| 419 | 7x | attributes |
| 420 | ) | |
| 421 | } | |
| 422 | ||
| 423 | 4x | 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) { | |
| 441 | 1x | checkmate::assert_list(pws) |
| 442 | 1x | checkmate::assert_names(names(pws), must.include = "dim") |
| 443 | 1x | checkmate::assert_class(pws$dim, "reactive") |
| 444 | 1x | checkmate::assert_class(q_r, "reactive") |
| 445 | 1x | checkmate::assert_character(inner_classes, null.ok = TRUE) |
| 446 | ||
| 447 | 1x | reactive({ |
| 448 | 1x | pws_dim <- stats::setNames(as.list(req(pws$dim())), c("width", "height")) |
| 449 | 1x | if (identical(pws_dim$width, "auto")) { # ignore non-numeric values (such as "auto") |
| 450 | 1x | pws_dim$width <- NULL |
| 451 | } | |
| 452 | 1x | if (identical(pws_dim$height, "auto")) { # ignore non-numeric values (such as "auto") |
| 453 | ! | pws_dim$height <- NULL |
| 454 | } | |
| 455 | 1x | q <- req(q_r()) |
| 456 | 1x | teal.reporter::teal_card(q) <- set_chunk_attrs( |
| 457 | 1x | teal.reporter::teal_card(q), |
| 458 | 1x | list(dev.width = pws_dim$width, dev.height = pws_dim$height), |
| 459 | 1x | inner_classes = inner_classes |
| 460 | ) | |
| 461 | 1x | 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) { | |
| 548 | 6x | if (is.numeric(x) && is.numeric(y)) { |
| 549 | 3x | stat <- tryCatch(do.call(.f, c(list(~ x + y), .f_args)), error = function(e) NA) |
| 550 | ||
| 551 | 3x | if (anyNA(stat)) { |
| 552 | 1x | return("NA") |
| 553 | 2x | } else if (all(c("estimate", "p.value") %in% names(stat))) { |
| 554 | 2x | return(paste( |
| 555 | 2x | c( |
| 556 | 2x | paste0(names(stat$estimate), ":", round(stat$estimate, round_stat)), |
| 557 | 2x | paste0("P:", round(stat$p.value, round_pval)) |
| 558 | ), | |
| 559 | 2x | collapse = "\n" |
| 560 | )) | |
| 561 | } else { | |
| 562 | ! | stop("function not supported") |
| 563 | } | |
| 564 | } else { | |
| 565 | 3x | if ("method" %in% names(.f_args)) { |
| 566 | 3x | if (.f_args$method == "pearson") { |
| 567 | 1x | return("cor:-") |
| 568 | } | |
| 569 | 2x | if (.f_args$method == "kendall") { |
| 570 | 1x | return("tau:-") |
| 571 | } | |
| 572 | 1x | if (.f_args$method == "spearman") { |
| 573 | 1x | 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 |