| 1 |
#' Wrapper for `pickerInput` |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' Wrapper for [shinyWidgets::pickerInput()] with additional features. |
|
| 5 |
#' When `fixed = TRUE` or when the number of `choices` is less or equal to 1 (see `fixed_on_single`), |
|
| 6 |
#' the `pickerInput` widget is hidden and non-interactive widget will be displayed |
|
| 7 |
#' instead. Toggle of `HTML` elements is just the visual effect to avoid displaying |
|
| 8 |
#' `pickerInput` widget when there is only one choice. |
|
| 9 |
#' |
|
| 10 |
#' @inheritParams shinyWidgets::pickerInput |
|
| 11 |
#' |
|
| 12 |
#' @param sep (`character(1)`)\cr |
|
| 13 |
#' A separator string to split the `choices` or `selected` inputs into the values of the different |
|
| 14 |
#' columns. |
|
| 15 |
#' |
|
| 16 |
#' @param label_help (`shiny.tag`) optional,\cr |
|
| 17 |
#' e.g. an object returned by [shiny::helpText()]. |
|
| 18 |
#' |
|
| 19 |
#' @param fixed (`logical(1)`) optional,\cr |
|
| 20 |
#' whether to block user to select choices. |
|
| 21 |
#' |
|
| 22 |
#' @param fixed_on_single (`logical(1)`) optional,\cr |
|
| 23 |
#' whether to block user to select a choice when there is only one or less choice. |
|
| 24 |
#' When `FALSE`, user is still able to select or deselect the choice. |
|
| 25 |
#' |
|
| 26 |
#' @param width (`character(1)`)\cr |
|
| 27 |
#' The width of the input passed to `pickerInput` e.g. `'auto'`, `'fit'`, `'100px'` or `'75%'` |
|
| 28 |
#' |
|
| 29 |
#' @return (`shiny.tag`) HTML tag with `pickerInput` widget and |
|
| 30 |
#' non-interactive element listing selected values. |
|
| 31 |
#' |
|
| 32 |
#' @export |
|
| 33 |
#' |
|
| 34 |
#' @examples |
|
| 35 |
#' library(shiny) |
|
| 36 |
#' |
|
| 37 |
#' ui_grid <- function(...) {
|
|
| 38 |
#' bslib::page_fluid( |
|
| 39 |
#' bslib::layout_columns( |
|
| 40 |
#' col_widths = c(4, 4, 4), |
|
| 41 |
#' ... |
|
| 42 |
#' ) |
|
| 43 |
#' ) |
|
| 44 |
#' } |
|
| 45 |
#' |
|
| 46 |
#' ui <- ui_grid( |
|
| 47 |
#' wellPanel( |
|
| 48 |
#' optionalSelectInput( |
|
| 49 |
#' inputId = "c1", |
|
| 50 |
#' label = "Fixed choices", |
|
| 51 |
#' choices = LETTERS[1:5], |
|
| 52 |
#' selected = c("A", "B"),
|
|
| 53 |
#' fixed = TRUE |
|
| 54 |
#' ), |
|
| 55 |
#' verbatimTextOutput(outputId = "c1_out") |
|
| 56 |
#' ), |
|
| 57 |
#' wellPanel( |
|
| 58 |
#' optionalSelectInput( |
|
| 59 |
#' inputId = "c2", |
|
| 60 |
#' label = "Single choice", |
|
| 61 |
#' choices = "A", |
|
| 62 |
#' selected = "A" |
|
| 63 |
#' ), |
|
| 64 |
#' verbatimTextOutput(outputId = "c2_out") |
|
| 65 |
#' ), |
|
| 66 |
#' wellPanel( |
|
| 67 |
#' optionalSelectInput( |
|
| 68 |
#' inputId = "c3", |
|
| 69 |
#' label = "NULL choices", |
|
| 70 |
#' choices = NULL |
|
| 71 |
#' ), |
|
| 72 |
#' verbatimTextOutput(outputId = "c3_out") |
|
| 73 |
#' ), |
|
| 74 |
#' wellPanel( |
|
| 75 |
#' optionalSelectInput( |
|
| 76 |
#' inputId = "c4", |
|
| 77 |
#' label = "Default", |
|
| 78 |
#' choices = LETTERS[1:5], |
|
| 79 |
#' selected = "A" |
|
| 80 |
#' ), |
|
| 81 |
#' verbatimTextOutput(outputId = "c4_out") |
|
| 82 |
#' ), |
|
| 83 |
#' wellPanel( |
|
| 84 |
#' optionalSelectInput( |
|
| 85 |
#' inputId = "c5", |
|
| 86 |
#' label = "Named vector", |
|
| 87 |
#' choices = c(`A - value A` = "A", `B - value B` = "B", `C - value C` = "C"), |
|
| 88 |
#' selected = "A" |
|
| 89 |
#' ), |
|
| 90 |
#' verbatimTextOutput(outputId = "c5_out") |
|
| 91 |
#' ), |
|
| 92 |
#' wellPanel( |
|
| 93 |
#' selectInput( |
|
| 94 |
#' inputId = "c6_choices", label = "Update choices", choices = letters, multiple = TRUE |
|
| 95 |
#' ), |
|
| 96 |
#' optionalSelectInput( |
|
| 97 |
#' inputId = "c6", |
|
| 98 |
#' label = "Updated choices", |
|
| 99 |
#' choices = NULL, |
|
| 100 |
#' multiple = TRUE, |
|
| 101 |
#' fixed_on_single = TRUE |
|
| 102 |
#' ), |
|
| 103 |
#' verbatimTextOutput(outputId = "c6_out") |
|
| 104 |
#' ) |
|
| 105 |
#' ) |
|
| 106 |
#' |
|
| 107 |
#' server <- function(input, output, session) {
|
|
| 108 |
#' observeEvent(input$c6_choices, ignoreNULL = FALSE, {
|
|
| 109 |
#' updateOptionalSelectInput( |
|
| 110 |
#' session = session, |
|
| 111 |
#' inputId = "c6", |
|
| 112 |
#' choices = input$c6_choices, |
|
| 113 |
#' selected = input$c6_choices |
|
| 114 |
#' ) |
|
| 115 |
#' }) |
|
| 116 |
#' |
|
| 117 |
#' output$c1_out <- renderPrint(input$c1) |
|
| 118 |
#' output$c2_out <- renderPrint(input$c2) |
|
| 119 |
#' output$c3_out <- renderPrint(input$c3) |
|
| 120 |
#' output$c4_out <- renderPrint(input$c4) |
|
| 121 |
#' output$c5_out <- renderPrint(input$c5) |
|
| 122 |
#' output$c6_out <- renderPrint(input$c6) |
|
| 123 |
#' } |
|
| 124 |
#' |
|
| 125 |
#' if (interactive()) {
|
|
| 126 |
#' shinyApp(ui, server) |
|
| 127 |
#' } |
|
| 128 |
#' |
|
| 129 |
optionalSelectInput <- function(inputId, # nolint |
|
| 130 |
label = NULL, |
|
| 131 |
choices = NULL, |
|
| 132 |
selected = NULL, |
|
| 133 |
multiple = FALSE, |
|
| 134 |
sep = NULL, |
|
| 135 |
options = list(), |
|
| 136 |
label_help = NULL, |
|
| 137 |
fixed = FALSE, |
|
| 138 |
fixed_on_single = FALSE, |
|
| 139 |
width = NULL) {
|
|
| 140 | ! |
checkmate::assert_string(inputId) |
| 141 | ! |
checkmate::assert( |
| 142 | ! |
checkmate::check_string(label, null.ok = TRUE), |
| 143 | ! |
checkmate::check_class(label, "shiny.tag"), |
| 144 | ! |
checkmate::check_class(label, "shiny.tag.list"), |
| 145 | ! |
checkmate::check_class(label, "html") |
| 146 |
) |
|
| 147 | ! |
stopifnot(is.null(choices) || length(choices) >= 1) |
| 148 | ! |
stopifnot( |
| 149 | ! |
is.null(selected) || |
| 150 | ! |
length(selected) == 0 || |
| 151 | ! |
all(selected %in% choices) || |
| 152 | ! |
all(selected %in% unlist(choices, recursive = FALSE)) |
| 153 |
) |
|
| 154 | ! |
checkmate::assert_flag(multiple) |
| 155 | ! |
checkmate::assert_string(sep, null.ok = TRUE) |
| 156 | ! |
checkmate::assert_list(options) |
| 157 | ! |
checkmate::assert( |
| 158 | ! |
checkmate::check_string(label_help, null.ok = TRUE), |
| 159 | ! |
checkmate::check_class(label_help, "shiny.tag"), |
| 160 | ! |
checkmate::check_class(label_help, "shiny.tag.list"), |
| 161 | ! |
checkmate::check_class(label_help, "html") |
| 162 |
) |
|
| 163 | ! |
checkmate::assert_flag(fixed) |
| 164 | ! |
checkmate::assert_flag(fixed_on_single) |
| 165 | ||
| 166 | ! |
if (!is.null(width)) {
|
| 167 | ! |
validateCssUnit(width) |
| 168 |
} |
|
| 169 | ||
| 170 | ! |
default_options <- list( |
| 171 | ! |
"actions-box" = multiple, |
| 172 | ! |
"none-selected-text" = "- Nothing selected -", |
| 173 | ! |
"allow-clear" = !multiple, |
| 174 | ! |
"max-options" = ifelse(multiple, Inf, 1), |
| 175 | ! |
"show-subtext" = TRUE, |
| 176 | ! |
"live-search" = ifelse(length(choices) > 10, TRUE, FALSE) |
| 177 |
) |
|
| 178 | ||
| 179 | ! |
options <- if (!identical(options, list())) {
|
| 180 | ! |
c(options, default_options[setdiff(names(default_options), names(options))]) |
| 181 |
} else {
|
|
| 182 | ! |
default_options |
| 183 |
} |
|
| 184 | ||
| 185 | ! |
if (is.null(choices)) {
|
| 186 | ! |
choices <- "" |
| 187 | ! |
selected <- NULL |
| 188 |
} |
|
| 189 | ||
| 190 | ! |
if (length(choices) <= 1 && fixed_on_single) fixed <- TRUE |
| 191 | ||
| 192 | ! |
raw_choices <- extract_raw_choices(choices, attr(choices, "sep")) |
| 193 | ! |
raw_selected <- extract_raw_choices(selected, attr(choices, "sep")) |
| 194 | ||
| 195 |
# Making sure the default dropdown popup can be displayed in the whole body, even outside the sidebars. |
|
| 196 | ! |
if (is.null(options$container)) {
|
| 197 | ! |
options$container <- "body" |
| 198 |
} |
|
| 199 | ||
| 200 | ! |
ui_picker <- tags$div( |
| 201 | ! |
id = paste0(inputId, "_input"), |
| 202 |
# visibility feature marked with display: none/block instead of shinyjs::hide/show |
|
| 203 |
# as mechanism to hide/show is handled by javascript code |
|
| 204 | ! |
style = if (fixed) "display: none;" else "display: block;", |
| 205 | ! |
shinyWidgets::pickerInput( |
| 206 | ! |
inputId = inputId, |
| 207 | ! |
label = label, |
| 208 | ! |
choices = raw_choices, |
| 209 | ! |
selected = raw_selected, |
| 210 | ! |
multiple = TRUE, |
| 211 | ! |
width = width, |
| 212 | ! |
options = options, |
| 213 | ! |
choicesOpt = picker_options(choices) |
| 214 |
) |
|
| 215 |
) |
|
| 216 | ||
| 217 | ! |
if (!is.null(label_help)) {
|
| 218 | ! |
ui_picker[[3]] <- append(ui_picker[[3]], list(tags$div(class = "label-help", label_help)), after = 1) |
| 219 |
} |
|
| 220 | ||
| 221 | ! |
ui_fixed <- tags$div( |
| 222 | ! |
id = paste0(inputId, "_fixed"), |
| 223 |
# visibility feature marked with display: none/block instead of shinyjs::hide/show |
|
| 224 |
# as mechanism to hide/show is handled by javascript code |
|
| 225 | ! |
style = if (fixed) "display: block;" else "display: none;", |
| 226 | ! |
tags$label(class = "control-label", label), |
| 227 |
# selected values as verbatim text |
|
| 228 | ! |
tags$code( |
| 229 | ! |
id = paste0(inputId, "_selected_text"), |
| 230 | ! |
if (length(selected) > 0) {
|
| 231 | ! |
toString(selected) |
| 232 |
} else {
|
|
| 233 | ! |
"NULL" |
| 234 |
} |
|
| 235 |
), |
|
| 236 | ! |
label_help |
| 237 |
) |
|
| 238 | ||
| 239 | ! |
tags$div( |
| 240 |
# when selected values in ui_picker change |
|
| 241 |
# then update ui_fixed - specifically, update '{id}_selected_text' element
|
|
| 242 | ! |
tags$script( |
| 243 | ! |
sprintf( |
| 244 |
" |
|
| 245 | ! |
$(function() {
|
| 246 | ! |
$('#%1$s').on('change', function(e) {
|
| 247 | ! |
var select_concat = $(this).val().length ? $(this).val().join(', ') : 'NULL';
|
| 248 | ! |
$('#%1$s_selected_text').html(select_concat);
|
| 249 |
}) |
|
| 250 |
})", |
|
| 251 | ! |
inputId |
| 252 |
) |
|
| 253 |
), |
|
| 254 | ||
| 255 |
# if ui_picker has only one or less option or is fixed then hide {id}_input and show {id}_fixed
|
|
| 256 | ! |
if (fixed_on_single) {
|
| 257 | ! |
js <- sprintf( |
| 258 | ! |
"$(function() {
|
| 259 | ! |
$('#%1$s').on('change', function(e) {
|
| 260 | ! |
var options = $('#%1$s').find('option');
|
| 261 | ! |
if (options.length == 1) {
|
| 262 | ! |
$('#%1$s_input').hide();
|
| 263 | ! |
$('#%1$s_fixed').show();
|
| 264 |
} else {
|
|
| 265 | ! |
$('#%1$s_input').show();
|
| 266 | ! |
$('#%1$s_fixed').hide();
|
| 267 |
} |
|
| 268 |
}) |
|
| 269 |
})", |
|
| 270 | ! |
inputId |
| 271 |
) |
|
| 272 | ! |
tags$script(js) |
| 273 |
}, |
|
| 274 | ! |
tags$div(ui_picker, ui_fixed) |
| 275 |
) |
|
| 276 |
} |
|
| 277 | ||
| 278 |
#' @rdname optionalSelectInput |
|
| 279 |
#' @param session (`shiny.session`)\cr |
|
| 280 |
#' @export |
|
| 281 |
updateOptionalSelectInput <- function(session, # nolint |
|
| 282 |
inputId, # nolint |
|
| 283 |
label = NULL, |
|
| 284 |
selected = NULL, |
|
| 285 |
choices = NULL) {
|
|
| 286 | ! |
raw_choices <- extract_raw_choices(choices, attr(choices, "sep")) |
| 287 | ! |
raw_selected <- extract_raw_choices(selected, attr(choices, "sep")) |
| 288 | ||
| 289 |
# update picker input |
|
| 290 | ! |
shinyWidgets::updatePickerInput( |
| 291 | ! |
session = session, |
| 292 | ! |
inputId = inputId, |
| 293 | ! |
label = label, |
| 294 | ! |
selected = as.character(raw_selected), |
| 295 | ! |
choices = raw_choices, |
| 296 | ! |
choicesOpt = picker_options(choices), |
| 297 | ! |
options = list( |
| 298 | ! |
`live-search` = ifelse(length(raw_choices) > 10, TRUE, FALSE) |
| 299 |
) |
|
| 300 |
) |
|
| 301 | ||
| 302 | ! |
invisible(NULL) |
| 303 |
} |
|
| 304 | ||
| 305 |
#' Get icons to represent variable types in dataset |
|
| 306 |
#' |
|
| 307 |
#' @param var_type (`character`)\cr |
|
| 308 |
#' of R internal types (classes). |
|
| 309 |
#' |
|
| 310 |
#' @return (`character`)\cr |
|
| 311 |
#' vector of HTML icons corresponding to data type in each column. |
|
| 312 |
#' @keywords internal |
|
| 313 |
#' |
|
| 314 |
variable_type_icons <- function(var_type) {
|
|
| 315 | ! |
checkmate::assert_character(var_type, any.missing = FALSE) |
| 316 | ||
| 317 | ! |
class_to_icon <- list( |
| 318 | ! |
numeric = "arrow-up-1-9", |
| 319 | ! |
integer = "arrow-up-1-9", |
| 320 | ! |
logical = "pause", |
| 321 | ! |
Date = "calendar", |
| 322 | ! |
POSIXct = "calendar", |
| 323 | ! |
POSIXlt = "calendar", |
| 324 | ! |
factor = "chart-bar", |
| 325 | ! |
character = "keyboard", |
| 326 | ! |
primary_key = "key", |
| 327 | ! |
unknown = "circle-question" |
| 328 |
) |
|
| 329 | ! |
class_to_icon <- lapply(class_to_icon, function(icon_name) toString(icon(icon_name, lib = "font-awesome"))) |
| 330 | ||
| 331 | ! |
res <- unname(vapply( |
| 332 | ! |
var_type, |
| 333 | ! |
FUN.VALUE = character(1), |
| 334 | ! |
FUN = function(class) {
|
| 335 | ! |
if (class == "") {
|
| 336 | ! |
class |
| 337 | ! |
} else if (is.null(class_to_icon[[class]])) {
|
| 338 | ! |
class_to_icon[["unknown"]] |
| 339 |
} else {
|
|
| 340 | ! |
class_to_icon[[class]] |
| 341 |
} |
|
| 342 |
} |
|
| 343 |
)) |
|
| 344 | ||
| 345 | ! |
res |
| 346 |
} |
|
| 347 | ||
| 348 |
#' Optional content for `optionalSelectInput` |
|
| 349 |
#' |
|
| 350 |
#' Prepares content to be displayed in `optionalSelectInput` with icons and labels |
|
| 351 |
#' |
|
| 352 |
#' @param var_name (`character`)\cr |
|
| 353 |
#' variable name |
|
| 354 |
#' @param var_label (`character`)\cr |
|
| 355 |
#' variable alternative name - for example variable label |
|
| 356 |
#' @param var_type (`character`) |
|
| 357 |
#' class of the variable. |
|
| 358 |
#' |
|
| 359 |
#' @return (`character`) HTML contents with all elements combined |
|
| 360 |
#' @keywords internal |
|
| 361 |
#' |
|
| 362 |
picker_options_content <- function(var_name, var_label, var_type) {
|
|
| 363 | ! |
if (length(var_name) == 0) {
|
| 364 | ! |
res <- character(0) |
| 365 | ! |
} else if (length(var_type) == 0 && length(var_label) == 0) {
|
| 366 | ! |
res <- var_name |
| 367 |
} else {
|
|
| 368 | ! |
checkmate::assert_character(var_name, min.len = 1, any.missing = FALSE) |
| 369 | ! |
stopifnot( |
| 370 | ! |
identical(var_type, character(0)) || length(var_type) == length(var_name), |
| 371 | ! |
identical(var_label, character(0)) || length(var_label) == length(var_name) |
| 372 |
) |
|
| 373 | ||
| 374 | ! |
var_icon <- variable_type_icons(var_type) |
| 375 | ||
| 376 | ! |
res <- trimws(paste( |
| 377 | ! |
var_icon, |
| 378 | ! |
var_name, |
| 379 | ! |
vapply( |
| 380 | ! |
var_label, |
| 381 | ! |
function(x) {
|
| 382 | ! |
ifelse(x == "", "", toString(tags$small(x, class = "text-muted"))) |
| 383 |
}, |
|
| 384 | ! |
character(1) |
| 385 |
) |
|
| 386 |
)) |
|
| 387 |
} |
|
| 388 | ||
| 389 | ! |
res |
| 390 |
} |
|
| 391 | ||
| 392 |
#' Create `choicesOpt` for `pickerInput` |
|
| 393 |
#' |
|
| 394 |
#' @param choices (`choices_labeled` or `character`)\cr |
|
| 395 |
#' choices vector |
|
| 396 |
#' |
|
| 397 |
#' @return (`list`)\cr |
|
| 398 |
#' to be passed as `choicesOpt` argument. |
|
| 399 |
#' @keywords internal |
|
| 400 |
picker_options <- function(choices) {
|
|
| 401 | ! |
if (inherits(choices, "choices_labeled")) {
|
| 402 | ! |
raw_choices <- extract_raw_choices(choices, sep = attr(choices, "sep")) |
| 403 | ! |
res <- list( |
| 404 | ! |
content = picker_options_content( |
| 405 | ! |
var_name = raw_choices, |
| 406 | ! |
var_label = extract_choices_labels(choices), |
| 407 | ! |
var_type = if (is.null(attr(choices, "types"))) character(0) else attr(choices, "types") |
| 408 |
) |
|
| 409 |
) |
|
| 410 | ! |
} else if (all(vapply(choices, inherits, logical(1), "choices_labeled"))) {
|
| 411 | ! |
choices <- unlist(unname(choices)) |
| 412 | ! |
res <- list(content = picker_options_content( |
| 413 | ! |
var_name = choices, |
| 414 | ! |
var_label = extract_choices_labels(choices), |
| 415 | ! |
var_type = if (is.null(attr(choices, "types"))) character(0) else attr(choices, "types") |
| 416 |
)) |
|
| 417 |
} else {
|
|
| 418 | ! |
res <- NULL |
| 419 |
} |
|
| 420 | ! |
res |
| 421 |
} |
|
| 422 | ||
| 423 |
#' Extract raw values from choices |
|
| 424 |
#' |
|
| 425 |
#' @param choices (`choices_labeled`, `list` or `character`)\cr |
|
| 426 |
#' object containing choices |
|
| 427 |
#' @param sep (`character(1)`)\cr |
|
| 428 |
#' A separator string to split the `choices` or `selected` inputs into the values of |
|
| 429 |
#' the different columns. |
|
| 430 |
#' @return choices simplified |
|
| 431 |
#' @keywords internal |
|
| 432 |
extract_raw_choices <- function(choices, sep) {
|
|
| 433 | ! |
if (!is.null(sep)) {
|
| 434 | ! |
vapply(choices, paste, collapse = sep, character(1)) |
| 435 | ! |
} else if (inherits(choices, "choices_labeled")) {
|
| 436 | ! |
unname(unlist(choices)) |
| 437 |
} else {
|
|
| 438 | ! |
choices |
| 439 |
} |
|
| 440 |
} |
|
| 441 | ||
| 442 |
#' Optional Slider Input Widget |
|
| 443 |
#' |
|
| 444 |
#' if min or max are `NA` then the slider widget will be hidden |
|
| 445 |
#' |
|
| 446 |
#' @description |
|
| 447 |
#' Hidden input widgets are useful to have the `input[[inputId]]` variable |
|
| 448 |
#' on available in the server function but no corresponding visual clutter from |
|
| 449 |
#' input widgets that provide only a single choice. |
|
| 450 |
#' |
|
| 451 |
#' @inheritParams shiny::sliderInput |
|
| 452 |
#' @param label_help (`shiny.tag`) optional\cr |
|
| 453 |
#' object of class `shiny.tag`, e.g. an object returned by [shiny::helpText()] |
|
| 454 |
#' @param ... optional arguments to `sliderInput` |
|
| 455 |
#' |
|
| 456 |
#' @return (`shiny.tag`) HTML tag with `sliderInput` widget. |
|
| 457 |
#' |
|
| 458 |
#' @export |
|
| 459 |
#' |
|
| 460 |
#' @examples |
|
| 461 |
#' ui <- bslib::page_fluid( |
|
| 462 |
#' shinyjs::useShinyjs(), |
|
| 463 |
#' optionalSliderInput("s", "shown", 0, 1, 0.2),
|
|
| 464 |
#' optionalSliderInput("h", "hidden", 0, NA, 1),
|
|
| 465 |
#' ) |
|
| 466 |
#' if (interactive()) {
|
|
| 467 |
#' shiny::shinyApp(ui, function(input, output) {})
|
|
| 468 |
#' } |
|
| 469 |
optionalSliderInput <- function(inputId, label, min, max, value, label_help = NULL, ...) { # nolint
|
|
| 470 | 21x |
checkmate::assert_number(min, na.ok = TRUE) |
| 471 | 21x |
checkmate::assert_number(max, na.ok = TRUE) |
| 472 | 21x |
checkmate::assert_numeric(value, min.len = 1, max.len = 2, any.missing = FALSE) |
| 473 | ||
| 474 | 21x |
is_na_min <- is.na(min) |
| 475 | 21x |
is_na_max <- is.na(max) |
| 476 | ||
| 477 | 21x |
hide <- is_na_min || is_na_max |
| 478 | ||
| 479 | 21x |
if (length(value) == 2) {
|
| 480 | 2x |
value1 <- value[1] |
| 481 | 2x |
value2 <- value[2] |
| 482 |
} else {
|
|
| 483 | 19x |
value1 <- value |
| 484 | 19x |
value2 <- value |
| 485 |
} |
|
| 486 | ||
| 487 | 21x |
if (is_na_min) {
|
| 488 | 2x |
min <- value1 - 1 |
| 489 |
} |
|
| 490 | 21x |
if (is_na_max) {
|
| 491 | 1x |
max <- value2 + 1 |
| 492 |
} |
|
| 493 | ||
| 494 | 21x |
if (min > value1 || max < value2) {
|
| 495 | 2x |
stop("arguments inconsistent: min <= value <= max expected")
|
| 496 |
} |
|
| 497 | ||
| 498 | 19x |
slider <- sliderInput(inputId, label, min, max, value, ...) |
| 499 | ||
| 500 | 19x |
if (!is.null(label_help)) {
|
| 501 | ! |
slider[[3]] <- append(slider[[3]], list(tags$div(class = "label-help", label_help)), after = 1) |
| 502 |
} |
|
| 503 | ||
| 504 | 19x |
if (hide) {
|
| 505 | 2x |
shinyjs::hidden(slider) |
| 506 |
} else {
|
|
| 507 | 17x |
slider |
| 508 |
} |
|
| 509 |
} |
|
| 510 | ||
| 511 |
#' Optional Slider Input with minimal and maximal values |
|
| 512 |
#' |
|
| 513 |
#' For `teal` modules we parameterize an `optionalSliderInput` with one argument |
|
| 514 |
#' `value_min_max` |
|
| 515 |
#' |
|
| 516 |
#' @description |
|
| 517 |
#' The [optionalSliderInput()] function needs three arguments to determine |
|
| 518 |
#' whether to hide the `sliderInput` widget or not. For `teal` modules we specify an |
|
| 519 |
#' optional slider input with one argument here called `value_min_max`. |
|
| 520 |
#' |
|
| 521 |
#' @inheritParams optionalSliderInput |
|
| 522 |
#' |
|
| 523 |
#' @param value_min_max (`numeric(1)` or `numeric(3)`)\cr |
|
| 524 |
#' If of length 1 then the value gets set to that number and the `sliderInput` will be hidden. |
|
| 525 |
#' Otherwise, if it is of length three the three elements will map to `value`, `min` and `max` of |
|
| 526 |
#' the [optionalSliderInput()] function. |
|
| 527 |
#' |
|
| 528 |
#' @return (`shiny.tag`) HTML tag with range `sliderInput` widget. |
|
| 529 |
#' |
|
| 530 |
#' @export |
|
| 531 |
#' |
|
| 532 |
#' @examples |
|
| 533 |
#' |
|
| 534 |
#' ui <- bslib::page_fluid( |
|
| 535 |
#' shinyjs::useShinyjs(), |
|
| 536 |
#' optionalSliderInputValMinMax("a1", "b1", 1), # Hidden
|
|
| 537 |
#' optionalSliderInputValMinMax("a2", "b2", c(3, 1, 5)) # Shown
|
|
| 538 |
#' ) |
|
| 539 |
#' if (interactive()) {
|
|
| 540 |
#' shiny::shinyApp(ui, function(input, output) {})
|
|
| 541 |
#' } |
|
| 542 |
optionalSliderInputValMinMax <- function(inputId, label, value_min_max, label_help = NULL, ...) { # nolint
|
|
| 543 | 14x |
checkmate::assert( |
| 544 | 14x |
checkmate::check_numeric( |
| 545 | 14x |
value_min_max, |
| 546 | 14x |
finite = TRUE, |
| 547 | 14x |
len = 3 |
| 548 |
), |
|
| 549 | 14x |
checkmate::check_numeric( |
| 550 | 14x |
value_min_max, |
| 551 | 14x |
finite = TRUE, |
| 552 | 14x |
len = 1 |
| 553 |
) |
|
| 554 |
) |
|
| 555 | ||
| 556 | 14x |
x <- value_min_max |
| 557 | ||
| 558 | 14x |
vals <- if (length(x) == 3) {
|
| 559 | 14x |
checkmate::assert_number(x[1], lower = x[2], upper = x[3], .var.name = "value_min_max") |
| 560 | 14x |
list(value = x[1], min = x[2], max = x[3]) |
| 561 | 14x |
} else if (length(x) == 1) {
|
| 562 | ! |
list(value = x, min = NA_real_, max = NA_real_) |
| 563 |
} |
|
| 564 | ||
| 565 | 14x |
slider <- optionalSliderInput(inputId, label, vals$min, vals$max, vals$value, ...) |
| 566 | ||
| 567 | 14x |
if (!is.null(label_help)) {
|
| 568 | ! |
slider[[3]] <- append(slider[[3]], list(tags$div(class = "label-help", label_help)), after = 1) |
| 569 |
} |
|
| 570 | 14x |
slider |
| 571 |
} |
|
| 572 | ||
| 573 |
#' Extract labels from choices basing on attributes and names |
|
| 574 |
#' |
|
| 575 |
#' @param choices (`list` or `vector`)\cr |
|
| 576 |
#' select choices |
|
| 577 |
#' @param values optional\cr |
|
| 578 |
#' choices subset for which labels should be extracted, `NULL` for all choices. |
|
| 579 |
#' |
|
| 580 |
#' @return (`character`) vector with labels |
|
| 581 |
#' @keywords internal |
|
| 582 |
extract_choices_labels <- function(choices, values = NULL) {
|
|
| 583 | ! |
res <- if (inherits(choices, "choices_labeled")) {
|
| 584 | ! |
attr(choices, "raw_labels") |
| 585 | ! |
} else if (!is.null(names(choices)) && !setequal(names(choices), unlist(unname(choices)))) {
|
| 586 | ! |
names(choices) |
| 587 |
} else {
|
|
| 588 | ! |
NULL |
| 589 |
} |
|
| 590 | ||
| 591 | ! |
if (!is.null(values) && !is.null(res)) {
|
| 592 | ! |
stopifnot(all(values %in% choices)) |
| 593 | ! |
res <- res[vapply(values, function(val) which(val == choices), numeric(1))] |
| 594 |
} |
|
| 595 | ||
| 596 | ! |
res |
| 597 |
} |
| 1 |
#' Creates `ggplot2_args` object |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' Constructor of `ggplot2_args` class of objects. |
|
| 5 |
#' The `ggplot2_args` argument should be a part of every module which contains any `ggplot2` graphics. |
|
| 6 |
#' The function arguments are validated to match their `ggplot2` equivalents. |
|
| 7 |
#' |
|
| 8 |
#' For more details see the vignette: `vignette("custom-ggplot2-arguments", package = "teal.widgets")`.
|
|
| 9 |
#' |
|
| 10 |
#' @seealso |
|
| 11 |
#' * [resolve_ggplot2_args()] to resolve multiple objects into one using pre-defined priorities. |
|
| 12 |
#' * [parse_ggplot2_args()] to parse resolved list into list of calls. |
|
| 13 |
#' |
|
| 14 |
#' @param labs (named `list`)\cr |
|
| 15 |
#' where all fields have to match [ggplot2::labs()] arguments. |
|
| 16 |
#' @param theme (named `list`)\cr |
|
| 17 |
#' where all fields have to match [ggplot2::theme()] arguments. |
|
| 18 |
#' |
|
| 19 |
#' @return (`ggplot2_args`) object. |
|
| 20 |
#' @export |
|
| 21 |
#' @examples |
|
| 22 |
#' ggplot2_args( |
|
| 23 |
#' labs = list(title = "TITLE"), |
|
| 24 |
#' theme = list(title = ggplot2::element_text(size = 20)) |
|
| 25 |
#' ) |
|
| 26 |
ggplot2_args <- function(labs = list(), theme = list()) {
|
|
| 27 | 92x |
checkmate::assert_list(labs) |
| 28 | 92x |
checkmate::assert_list(theme) |
| 29 | 92x |
checkmate::assert_character(names(labs), unique = TRUE, null.ok = TRUE) |
| 30 | 92x |
checkmate::assert_character(names(theme), unique = TRUE, null.ok = TRUE) |
| 31 | ||
| 32 | 92x |
ggplot2_theme <- methods::formalArgs(ggplot2::theme) |
| 33 |
# utils::getFromNamespace is not recommended nevertheless needed as it is replacing `:::`. |
|
| 34 |
# usage of static values will be vulnerable to any changes in ggplot2 aesthetics. |
|
| 35 | 92x |
ggplot2_labs <- c( |
| 36 | 92x |
utils::getFromNamespace(".all_aesthetics", "ggplot2"),
|
| 37 | 92x |
methods::formalArgs(ggplot2::labs) |
| 38 |
) |
|
| 39 | 92x |
checkmate::assert_subset(names(labs), choices = ggplot2_labs, empty.ok = TRUE) |
| 40 | 91x |
checkmate::assert_subset(names(theme), choices = ggplot2_theme, empty.ok = TRUE) |
| 41 | ||
| 42 | 90x |
structure(list(labs = labs, theme = theme), class = "ggplot2_args") |
| 43 |
} |
|
| 44 | ||
| 45 |
#' Resolving and reducing multiple `ggplot2_args` objects |
|
| 46 |
#' |
|
| 47 |
#' @description |
|
| 48 |
#' Resolving and reducing multiple `ggplot2_args` objects. |
|
| 49 |
#' This function is intended to utilize user provided settings, defaults provided by the module creator and |
|
| 50 |
#' also `teal` option. See `Details`, below, to understand the logic. |
|
| 51 |
#' |
|
| 52 |
#' @seealso [parse_ggplot2_args()] to parse resolved list into list of calls. |
|
| 53 |
#' |
|
| 54 |
#' @param user_plot (`ggplot2_args`)\cr |
|
| 55 |
#' end user setup for theme and labs in the specific plot. |
|
| 56 |
#' Created with the [ggplot2_args()] function. The `NULL` value is supported. |
|
| 57 |
#' @param user_default (`ggplot2_args`)\cr |
|
| 58 |
#' end user setup for module default theme and labs. |
|
| 59 |
#' Created with the [ggplot2_args()] function. The `NULL` value is supported. |
|
| 60 |
#' @param module_plot (`ggplot2_args`)\cr |
|
| 61 |
#' module creator setup for theme and labs in the specific plot. |
|
| 62 |
#' Created with the [ggplot2_args()] function. The `NULL` value is supported. |
|
| 63 |
#' @param app_default (`ggplot2_args`)\cr |
|
| 64 |
#' Application level setting. Can be `NULL`. |
|
| 65 |
#' |
|
| 66 |
#' @return `ggplot2_args` object. |
|
| 67 |
#' |
|
| 68 |
#' @details |
|
| 69 |
#' The function picks the first non `NULL` value for each argument, checking in the following order: |
|
| 70 |
#' 1. `ggplot2_args` argument provided by the end user. |
|
| 71 |
#' Per plot (`user_plot`) and then default (`user_default`) setup. |
|
| 72 |
#' 2. `app_default` global R variable, `teal.ggplot2_args`. |
|
| 73 |
#' 3. `module_plot` which is a module creator setup. |
|
| 74 |
#' @export |
|
| 75 |
#' @examples |
|
| 76 |
#' resolve_ggplot2_args( |
|
| 77 |
#' user_plot = ggplot2_args( |
|
| 78 |
#' labs = list(title = "TITLE"), |
|
| 79 |
#' theme = list(title = ggplot2::element_text(size = 20)) |
|
| 80 |
#' ), |
|
| 81 |
#' user_default = ggplot2_args( |
|
| 82 |
#' labs = list(x = "XLAB") |
|
| 83 |
#' ) |
|
| 84 |
#' ) |
|
| 85 |
resolve_ggplot2_args <- function(user_plot = ggplot2_args(), |
|
| 86 |
user_default = ggplot2_args(), |
|
| 87 |
module_plot = ggplot2_args(), |
|
| 88 |
app_default = getOption("teal.ggplot2_args", ggplot2_args())) {
|
|
| 89 | 18x |
checkmate::assert_class(user_plot, "ggplot2_args", null.ok = TRUE) |
| 90 | 17x |
checkmate::assert_class(user_default, "ggplot2_args", null.ok = TRUE) |
| 91 | 17x |
checkmate::assert_class(module_plot, "ggplot2_args", null.ok = TRUE) |
| 92 | 17x |
checkmate::assert_class(app_default, "ggplot2_args", null.ok = TRUE) |
| 93 | ||
| 94 | 17x |
ggplot2_args_all <- list( |
| 95 | 17x |
"plot" = user_plot, |
| 96 | 17x |
"default" = user_default, |
| 97 | 17x |
"teal" = app_default, |
| 98 | 17x |
"module" = module_plot |
| 99 |
) |
|
| 100 | ||
| 101 | 17x |
labs_args <- Reduce(`c`, lapply(ggplot2_args_all, function(x) x$labs)) |
| 102 | 17x |
labs_args <- if (is.null(labs_args)) list() else labs_args[!duplicated(names(labs_args))] |
| 103 | ||
| 104 | 17x |
theme_args <- Reduce(`c`, lapply(ggplot2_args_all, function(x) x$theme)) |
| 105 | 17x |
theme_args <- if (is.null(theme_args)) list() else theme_args[!duplicated(names(theme_args))] |
| 106 | ||
| 107 | 17x |
ggplot2_args(labs = labs_args, theme = theme_args) |
| 108 |
} |
|
| 109 | ||
| 110 |
#' Parse `ggplot2_args` object into the `ggplot2` expression |
|
| 111 |
#' |
|
| 112 |
#' @description |
|
| 113 |
#' A function to parse expression from the `ggplot2_args` object. |
|
| 114 |
#' @param ggplot2_args (`ggplot2_args`)\cr |
|
| 115 |
#' This argument could be a result of the [resolve_ggplot2_args()]. |
|
| 116 |
#' @param ggtheme (`character(1)`)\cr |
|
| 117 |
#' name of the `ggplot2` theme to be applied, e.g. `"dark"`. |
|
| 118 |
#' |
|
| 119 |
#' @return (`list`) of up to three elements of class `languange`: `"labs"`, `"ggtheme"` and `"theme"`. |
|
| 120 |
#' @export |
|
| 121 |
#' @examples |
|
| 122 |
#' parse_ggplot2_args( |
|
| 123 |
#' resolve_ggplot2_args(ggplot2_args( |
|
| 124 |
#' labs = list(title = "TITLE"), |
|
| 125 |
#' theme = list(title = ggplot2::element_text(size = 20)) |
|
| 126 |
#' )) |
|
| 127 |
#' ) |
|
| 128 |
#' |
|
| 129 |
#' parse_ggplot2_args( |
|
| 130 |
#' resolve_ggplot2_args( |
|
| 131 |
#' ggplot2_args( |
|
| 132 |
#' labs = list(title = "TITLE"), |
|
| 133 |
#' theme = list(title = ggplot2::element_text(size = 20)) |
|
| 134 |
#' ) |
|
| 135 |
#' ), |
|
| 136 |
#' ggtheme = "gray" |
|
| 137 |
#' ) |
|
| 138 |
parse_ggplot2_args <- function(ggplot2_args = teal.widgets::ggplot2_args(), |
|
| 139 |
ggtheme = c( |
|
| 140 |
"default", |
|
| 141 |
"gray", |
|
| 142 |
"bw", |
|
| 143 |
"linedraw", |
|
| 144 |
"light", |
|
| 145 |
"dark", |
|
| 146 |
"minimal", |
|
| 147 |
"classic", |
|
| 148 |
"void", |
|
| 149 |
"test" |
|
| 150 |
)) {
|
|
| 151 | 10x |
checkmate::assert_class(ggplot2_args, "ggplot2_args") |
| 152 | 9x |
ggtheme <- match.arg(ggtheme) |
| 153 | ||
| 154 | 9x |
res_list <- list() |
| 155 | ||
| 156 | 9x |
labs_args <- ggplot2_args$labs |
| 157 | ||
| 158 | 9x |
labs_f <- if (length(labs_args)) {
|
| 159 | 5x |
as.call(c(list(quote(ggplot2::labs)), labs_args)) |
| 160 |
} else {
|
|
| 161 | 4x |
NULL |
| 162 |
} |
|
| 163 | ||
| 164 | 9x |
default_theme <- if (ggtheme != "default") {
|
| 165 | 1x |
as.call(list(str2lang(paste0("ggplot2::theme_", ggtheme))))
|
| 166 |
} else {
|
|
| 167 | 8x |
NULL |
| 168 |
} |
|
| 169 | ||
| 170 | 9x |
theme_args <- ggplot2_args$theme |
| 171 | ||
| 172 | 9x |
theme_f <- if (length(theme_args)) {
|
| 173 | 2x |
as.call(c(list(quote(ggplot2::theme)), theme_args)) |
| 174 |
} else {
|
|
| 175 | 7x |
NULL |
| 176 |
} |
|
| 177 | ||
| 178 | 9x |
final_list <- Filter(Negate(is.null), list(labs = labs_f, ggtheme = default_theme, theme = theme_f)) |
| 179 |
# For empty final_list we want to return empty list, not empty named list |
|
| 180 | 3x |
`if`(length(final_list) == 0, list(), final_list) |
| 181 |
} |
| 1 |
#' @keywords internal |
|
| 2 |
#' @noRd |
|
| 3 |
draggable_buckets_deps <- function() {
|
|
| 4 | ! |
htmltools::htmlDependency( |
| 5 | ! |
name = "teal-widgets-draggable-buckets", |
| 6 | ! |
version = utils::packageVersion("teal.widgets"),
|
| 7 | ! |
package = "teal.widgets", |
| 8 | ! |
src = "draggable-buckets", |
| 9 | ! |
script = "draggable-buckets.js", |
| 10 | ! |
stylesheet = "draggable-buckets.css" |
| 11 |
) |
|
| 12 |
} |
|
| 13 | ||
| 14 |
#' @title Draggable Buckets |
|
| 15 |
#' @description |
|
| 16 |
#' A custom widget with draggable elements that can be put into buckets. |
|
| 17 |
#' |
|
| 18 |
#' @param input_id (`character(1)`) the `HTML` id of this widget |
|
| 19 |
#' @param label (`character(1)` or `shiny.tag`) the header of this widget |
|
| 20 |
#' @param elements (`character`) the elements to drag into buckets |
|
| 21 |
#' @param buckets (`character` or `list`) the names of the buckets the elements can be put in or a list of key-pair |
|
| 22 |
#' values where key is a name of a bucket and value is a character vector of elements in a bucket |
|
| 23 |
#' |
|
| 24 |
#' @return the `HTML` code comprising an instance of this widget |
|
| 25 |
#' @export |
|
| 26 |
#' |
|
| 27 |
#' @details `shinyvalidate` validation can be used with this widget. See example below. |
|
| 28 |
#' |
|
| 29 |
#' @examples |
|
| 30 |
#' library(shiny) |
|
| 31 |
#' |
|
| 32 |
#' ui <- bslib::page_fluid( |
|
| 33 |
#' draggable_buckets("id", "Choices #1", c("a", "b"), c("bucket1", "bucket2")),
|
|
| 34 |
#' draggable_buckets("id2", "Choices #2", letters, c("vowels", "consonants")),
|
|
| 35 |
#' verbatimTextOutput("out"),
|
|
| 36 |
#' verbatimTextOutput("out2")
|
|
| 37 |
#' ) |
|
| 38 |
#' server <- function(input, output) {
|
|
| 39 |
#' iv <- shinyvalidate::InputValidator$new() |
|
| 40 |
#' iv$add_rule( |
|
| 41 |
#' "id", |
|
| 42 |
#' function(data) if (length(data[["bucket1"]]) == 0) "There should be stuff in bucket 1" |
|
| 43 |
#' ) |
|
| 44 |
#' iv$enable() |
|
| 45 |
#' |
|
| 46 |
#' observeEvent(list(input$id, input$id2), {
|
|
| 47 |
#' print(isolate(input$id)) |
|
| 48 |
#' print(isolate(input$id2)) |
|
| 49 |
#' }) |
|
| 50 |
#' output$out <- renderPrint({
|
|
| 51 |
#' iv$is_valid() |
|
| 52 |
#' input$id |
|
| 53 |
#' }) |
|
| 54 |
#' output$out2 <- renderPrint(input$id2) |
|
| 55 |
#' } |
|
| 56 |
#' if (interactive()) shinyApp(ui, server) |
|
| 57 |
#' |
|
| 58 |
#' # With default elements in the bucket |
|
| 59 |
#' ui <- bslib::page_fluid( |
|
| 60 |
#' draggable_buckets("id", "Choices #1", c("a", "b"), list(bucket1 = character(), bucket2 = c("c"))),
|
|
| 61 |
#' verbatimTextOutput("out")
|
|
| 62 |
#' ) |
|
| 63 |
#' server <- function(input, output) {
|
|
| 64 |
#' observeEvent(input$id, {
|
|
| 65 |
#' print(isolate(input$id)) |
|
| 66 |
#' }) |
|
| 67 |
#' output$out <- renderPrint(input$id) |
|
| 68 |
#' } |
|
| 69 |
#' if (interactive()) shinyApp(ui, server) |
|
| 70 |
draggable_buckets <- function(input_id, label, elements = character(), buckets) {
|
|
| 71 | ! |
checkmate::assert_string(input_id) |
| 72 | ! |
checkmate::assert_true(inherits(label, "character") || inherits(label, "shiny.tag")) |
| 73 | ! |
checkmate::assert_character(c(elements, unlist(buckets)), min.len = 0, null.ok = TRUE, unique = TRUE) |
| 74 | ! |
checkmate::assert( |
| 75 | ! |
checkmate::check_character(buckets, min.len = 1), |
| 76 | ! |
checkmate::check_list(buckets, types = "character", names = "unique") |
| 77 |
) |
|
| 78 | ||
| 79 | ! |
elements_iterator <- new.env(parent = emptyenv()) |
| 80 | ! |
elements_iterator$it <- 0 |
| 81 | ||
| 82 | ! |
shiny::tagList( |
| 83 | ! |
draggable_buckets_deps(), |
| 84 | ! |
shiny::div( |
| 85 | ! |
tags$span(label), |
| 86 | ! |
render_unbucketed_elements(elements = elements, elements_iterator = elements_iterator, widget_id = input_id), |
| 87 | ! |
render_buckets(buckets = buckets, elements_iterator = elements_iterator, widget_id = input_id), |
| 88 | ! |
class = "draggableBuckets", |
| 89 | ! |
id = input_id |
| 90 |
) |
|
| 91 |
) |
|
| 92 |
} |
|
| 93 | ||
| 94 |
render_unbucketed_elements <- function(elements, elements_iterator, widget_id) {
|
|
| 95 | ! |
tags$div( |
| 96 | ! |
lapply(elements, function(element) {
|
| 97 | ! |
elements_iterator$it <- elements_iterator$it + 1 |
| 98 | ! |
render_draggable_element( |
| 99 | ! |
value = element, |
| 100 | ! |
id = paste0(widget_id, "draggable", elements_iterator$it), |
| 101 | ! |
widget_id = widget_id |
| 102 |
) |
|
| 103 |
}), |
|
| 104 | ! |
id = paste0(widget_id, "elements"), |
| 105 | ! |
class = c("form-control", "elements"),
|
| 106 | ! |
ondragover = "allowDrop(event)", |
| 107 | ! |
ondrop = "drop(event)", |
| 108 | ! |
`data-widget` = widget_id |
| 109 |
) |
|
| 110 |
} |
|
| 111 | ||
| 112 |
render_buckets <- function(buckets, elements_iterator, widget_id) {
|
|
| 113 | ! |
buckets <- `if`( |
| 114 | ! |
is.list(buckets), |
| 115 | ! |
lapply(names(buckets), function(bucket_name) {
|
| 116 | ! |
render_bucket( |
| 117 | ! |
name = bucket_name, |
| 118 | ! |
elements = buckets[[bucket_name]], |
| 119 | ! |
elements_iterator = elements_iterator, |
| 120 | ! |
widget_id = widget_id |
| 121 |
) |
|
| 122 |
}), |
|
| 123 | ! |
lapply(buckets, render_bucket, widget_id = widget_id, elements_iterator = elements_iterator) |
| 124 |
) |
|
| 125 | ! |
shiny::tagList(buckets) |
| 126 |
} |
|
| 127 | ||
| 128 |
render_draggable_element <- function(value, id, widget_id) {
|
|
| 129 | ! |
tags$div( |
| 130 | ! |
value, |
| 131 | ! |
id = id, |
| 132 | ! |
class = "element", |
| 133 | ! |
draggable = "true", |
| 134 | ! |
ondragstart = "drag(event)", |
| 135 | ! |
ondragover = "allowDrop(event)", |
| 136 | ! |
ondrop = "dropReorder(event)", |
| 137 | ! |
`data-widget` = widget_id |
| 138 |
) |
|
| 139 |
} |
|
| 140 | ||
| 141 |
render_bucket <- function(name, elements = NULL, elements_iterator = NULL, widget_id = NULL) {
|
|
| 142 | ! |
tags$div( |
| 143 | ! |
tags$div( |
| 144 | ! |
paste0(name, ":"), |
| 145 | ! |
class = "bucket-name", |
| 146 | ! |
ondragover = "allowDrop(event)", |
| 147 | ! |
ondrop = "dropBucketName(event)", |
| 148 | ! |
`data-widget` = widget_id |
| 149 |
), |
|
| 150 | ! |
lapply(elements, function(element) {
|
| 151 | ! |
elements_iterator$it <- elements_iterator$it + 1 |
| 152 | ! |
render_draggable_element( |
| 153 | ! |
value = element, |
| 154 | ! |
id = paste0(widget_id, "draggable", elements_iterator$it), |
| 155 | ! |
widget_id = widget_id |
| 156 |
) |
|
| 157 |
}), |
|
| 158 | ! |
class = c("form-control", "bucket"),
|
| 159 | ! |
ondragover = "allowDrop(event)", |
| 160 | ! |
ondrop = "drop(event)", |
| 161 | ! |
`data-label` = name, |
| 162 | ! |
`data-widget` = widget_id |
| 163 |
) |
|
| 164 |
} |
| 1 |
#' Panel group widget |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge("deprecated")`\cr
|
|
| 4 |
#' Designed to group [`panel_item`] elements. Used to handle `shiny` inputs in the encoding panel. |
|
| 5 |
#' @param id optional, (`character`)\cr |
|
| 6 |
#' @param ... (`shiny.tag`)\cr |
|
| 7 |
#' panels created by [panel_group()] |
|
| 8 |
#' |
|
| 9 |
#' @return (`shiny.tag`) |
|
| 10 |
#' |
|
| 11 |
#' @examples |
|
| 12 |
#' |
|
| 13 |
#' library(shiny) |
|
| 14 |
#' panel_group( |
|
| 15 |
#' panel_item( |
|
| 16 |
#' title = "Display", |
|
| 17 |
#' collapsed = FALSE, |
|
| 18 |
#' checkboxGroupInput( |
|
| 19 |
#' "check", |
|
| 20 |
#' "Tables display", |
|
| 21 |
#' choices = LETTERS[1:3], |
|
| 22 |
#' selected = LETTERS[1] |
|
| 23 |
#' ), |
|
| 24 |
#' radioButtons( |
|
| 25 |
#' "radio", |
|
| 26 |
#' label = "Plot type", |
|
| 27 |
#' choices = letters[1:2], |
|
| 28 |
#' selected = letters[1] |
|
| 29 |
#' ) |
|
| 30 |
#' ), |
|
| 31 |
#' panel_item( |
|
| 32 |
#' title = "Pre-processing", |
|
| 33 |
#' radioButtons( |
|
| 34 |
#' "filtering", |
|
| 35 |
#' "What to filter", |
|
| 36 |
#' choices = LETTERS[1:4], |
|
| 37 |
#' selected = LETTERS[1] |
|
| 38 |
#' ), |
|
| 39 |
#' radioButtons( |
|
| 40 |
#' "na_action", |
|
| 41 |
#' "NA action", |
|
| 42 |
#' choices = letters[1:3], |
|
| 43 |
#' selected = letters[1] |
|
| 44 |
#' ) |
|
| 45 |
#' ) |
|
| 46 |
#' ) |
|
| 47 |
#' |
|
| 48 |
#' @export |
|
| 49 |
panel_group <- function(..., id = NULL) {
|
|
| 50 | ! |
lifecycle::deprecate_soft( |
| 51 | ! |
when = "0.4.3", |
| 52 | ! |
what = "panel_group()", |
| 53 | ! |
details = paste( |
| 54 | ! |
"The `panel_group()` and `panel_item()` view can be achieved by using the `bslib` package.", |
| 55 | ! |
"Please use the `bslib::accordion()` and `bslib::accordion_panel()` functions instead.", |
| 56 | ! |
"This function will be removed in the next release." |
| 57 |
) |
|
| 58 |
) |
|
| 59 | ! |
checkmate::assert_string(id, null.ok = TRUE) |
| 60 | ||
| 61 |
# panel-group |
|
| 62 |
# div |
|
| 63 | ||
| 64 | ! |
bslib::accordion( |
| 65 |
... |
|
| 66 |
) |
|
| 67 |
} |
|
| 68 | ||
| 69 |
#' @keywords internal |
|
| 70 |
#' @noRd |
|
| 71 |
panel_item_deps <- function() {
|
|
| 72 | ! |
htmltools::htmlDependency( |
| 73 | ! |
name = "teal-widgets-panel-item", |
| 74 | ! |
version = utils::packageVersion("teal.widgets"),
|
| 75 | ! |
package = "teal.widgets", |
| 76 | ! |
src = "panel-item", |
| 77 | ! |
script = "panel-item.js", |
| 78 | ! |
stylesheet = "panel-item.css" |
| 79 |
) |
|
| 80 |
} |
|
| 81 | ||
| 82 |
#' Panel item widget |
|
| 83 |
#' |
|
| 84 |
#' @description `r lifecycle::badge("deprecated")`\cr
|
|
| 85 |
#' Designed to be grouped using [`panel_group`] element. Used to handle `shiny` inputs in the encoding panel. |
|
| 86 |
#' @param title (`character`)\cr title of panel |
|
| 87 |
#' @param ... content of panel |
|
| 88 |
#' @param collapsed (`logical`) optional,\cr |
|
| 89 |
#' whether to initially collapse panel |
|
| 90 |
#' @param input_id (`character`) optional\cr |
|
| 91 |
#' name of the panel item element. If supplied, this will register a shiny input variable that |
|
| 92 |
#' indicates whether the panel item is open or collapsed and is accessed with `input$input_id`. |
|
| 93 |
#' |
|
| 94 |
#' @return (`shiny.tag`) |
|
| 95 |
#' |
|
| 96 |
#' @examples |
|
| 97 |
#' |
|
| 98 |
#' library(shiny) |
|
| 99 |
#' panel_item( |
|
| 100 |
#' title = "Display", |
|
| 101 |
#' collapsed = FALSE, |
|
| 102 |
#' checkboxGroupInput( |
|
| 103 |
#' "check", |
|
| 104 |
#' "Tables display", |
|
| 105 |
#' choices = LETTERS[1:3], |
|
| 106 |
#' selected = LETTERS[1] |
|
| 107 |
#' ), |
|
| 108 |
#' radioButtons( |
|
| 109 |
#' "radio", |
|
| 110 |
#' label = "Plot type", |
|
| 111 |
#' choices = letters[1:2], |
|
| 112 |
#' selected = letters[1] |
|
| 113 |
#' ) |
|
| 114 |
#' ) |
|
| 115 |
#' |
|
| 116 |
#' @export |
|
| 117 |
panel_item <- function(title, ..., collapsed = TRUE, input_id = NULL) {
|
|
| 118 | ! |
lifecycle::deprecate_soft( |
| 119 | ! |
when = "0.4.3", |
| 120 | ! |
what = "panel_item()", |
| 121 | ! |
details = paste( |
| 122 | ! |
"The `panel_group()` and `panel_item()` view can be achieved by using the `bslib` package.", |
| 123 | ! |
"Please use the `bslib::accordion()` and `bslib::accordion_panel()` functions instead.", |
| 124 | ! |
"This function will be removed in the next release." |
| 125 |
) |
|
| 126 |
) |
|
| 127 | ! |
stopifnot(checkmate::test_character(title, len = 1) || inherits(title, c("shiny.tag", "shiny.tag.list", "html")))
|
| 128 | ! |
checkmate::assert_flag(collapsed) |
| 129 | ! |
checkmate::assert_string(input_id, null.ok = TRUE) |
| 130 | ||
| 131 | ! |
bslib::accordion_panel( |
| 132 | ! |
id = input_id, |
| 133 | ! |
title = title, |
| 134 | ! |
open = !collapsed, |
| 135 |
... |
|
| 136 |
) |
|
| 137 |
} |
| 1 |
#' @keywords internal |
|
| 2 |
#' @noRd |
|
| 3 |
plot_with_settings_deps <- function() {
|
|
| 4 | 1x |
htmltools::htmlDependency( |
| 5 | 1x |
name = "teal-widgets-plot-with-settings", |
| 6 | 1x |
version = utils::packageVersion("teal.widgets"),
|
| 7 | 1x |
package = "teal.widgets", |
| 8 | 1x |
src = "plot-with-settings", |
| 9 | 1x |
stylesheet = "plot-with-settings.css", |
| 10 | 1x |
script = "plot-with-settings.js" |
| 11 |
) |
|
| 12 |
} |
|
| 13 | ||
| 14 |
#' @name plot_with_settings |
|
| 15 |
#' @rdname plot_with_settings |
|
| 16 |
#' @export |
|
| 17 |
plot_with_settings_ui <- function(id) {
|
|
| 18 | 1x |
checkmate::assert_string(id) |
| 19 | ||
| 20 | 1x |
ns <- NS(id) |
| 21 | ||
| 22 | 1x |
tags$div( |
| 23 | 1x |
plot_with_settings_deps(), |
| 24 | 1x |
shinyjs::useShinyjs(), |
| 25 | 1x |
bslib::card( |
| 26 | 1x |
id = ns("plot-with-settings"),
|
| 27 | 1x |
full_screen = TRUE, |
| 28 | 1x |
tags$div( |
| 29 | 1x |
tags$div( |
| 30 | 1x |
class = "teal-widgets settings-buttons", |
| 31 | 1x |
bslib::tooltip( |
| 32 | 1x |
trigger = tags$div( |
| 33 | 1x |
bslib::popover( |
| 34 | 1x |
id = ns("expbut"),
|
| 35 | 1x |
trigger = icon("maximize"),
|
| 36 | 1x |
uiOutput(ns("slider_ui")),
|
| 37 | 1x |
uiOutput(ns("width_warning"))
|
| 38 |
) |
|
| 39 |
), |
|
| 40 | 1x |
options = list(trigger = "hover"), |
| 41 | 1x |
class = "resize-button", |
| 42 | 1x |
"Resize" |
| 43 |
), |
|
| 44 | 1x |
bslib::tooltip( |
| 45 | 1x |
trigger = tags$div(type_download_ui(ns("downbutton"))),
|
| 46 | 1x |
options = list(trigger = "hover"), |
| 47 | 1x |
class = "download-button", |
| 48 | 1x |
"Download" |
| 49 |
) |
|
| 50 |
), |
|
| 51 | 1x |
tags$div( |
| 52 | 1x |
id = ns("plot-out-main"),
|
| 53 | 1x |
class = "teal-widgets plot-content", |
| 54 | 1x |
uiOutput(ns("plot_out_main"))
|
| 55 |
) |
|
| 56 |
) |
|
| 57 |
) |
|
| 58 |
) |
|
| 59 |
} |
|
| 60 | ||
| 61 |
#' Plot-with-settings module |
|
| 62 |
#' |
|
| 63 |
#' @rdname plot_with_settings |
|
| 64 |
#' @description |
|
| 65 |
#' Universal module for plots with settings for height, width, and download. |
|
| 66 |
#' |
|
| 67 |
#' @export |
|
| 68 |
#' |
|
| 69 |
#' @param id (`character(1)`) `shiny` module id. |
|
| 70 |
#' |
|
| 71 |
#' @param plot_r (`reactive` or `function`)\cr |
|
| 72 |
#' `reactive` expression or a simple `function` to draw a plot. |
|
| 73 |
#' A simple `function` is needed e.g. for base plots like `plot(1)` as the output can not be caught when downloading. |
|
| 74 |
#' Take into account that simple functions are less efficient than reactive, as not catching the result. |
|
| 75 |
#' @param height (`numeric`) optional\cr |
|
| 76 |
#' vector with three elements c(VAL, MIN, MAX), where VAL is the starting value of the slider in |
|
| 77 |
#' the main and expanded plot display. |
|
| 78 |
#' @param width (`numeric`) optional\cr |
|
| 79 |
#' vector with three elements `c(VAL, MIN, MAX)`, where VAL is the starting value of the slider in |
|
| 80 |
#' the main and expanded plot display; `NULL` for default display. |
|
| 81 |
#' @param show_hide_signal optional, (`reactive logical` a mechanism to allow modules which call this |
|
| 82 |
#' module to show/hide the plot_with_settings UI) |
|
| 83 |
#' @param brushing (`logical`) optional\cr |
|
| 84 |
#' mechanism to enable / disable brushing on the main plot. |
|
| 85 |
#' All the brushing data is stored as a reactive object in the `"brush"` element of |
|
| 86 |
#' returned list. See the example for details. |
|
| 87 |
#' @param clicking (`logical`)\cr |
|
| 88 |
#' a mechanism to enable / disable clicking on data points on the main plot. |
|
| 89 |
#' All the clicking data is stored as a reactive object in the `"click"` |
|
| 90 |
#' element of returned list. See the example for details. |
|
| 91 |
#' @param dblclicking (`logical`) optional\cr |
|
| 92 |
#' mechanism to enable / disable double-clicking on data points on the main plot. |
|
| 93 |
#' All the double clicking data is stored as a reactive object in the |
|
| 94 |
#' the `"dblclick"` element of returned list. See the example for details. |
|
| 95 |
#' @param hovering (`logical(1)`) optional\cr |
|
| 96 |
#' mechanism to enable / disable hovering over data points on the main plot. |
|
| 97 |
#' All the hovering data is stored as a reactive object in the |
|
| 98 |
#' `"hover"` element of returned list. See the example for details. |
|
| 99 |
#' @param graph_align (`character(1)`) optional,\cr |
|
| 100 |
#' one of `"left"` (default), `"center"`, `"right"` or `"justify"`. The alignment of the graph on |
|
| 101 |
#' the main page. |
|
| 102 |
#' |
|
| 103 |
#' @details By default the plot is rendered with `72 dpi`. In order to change this, to for example 96 set |
|
| 104 |
#' `options(teal.plot_dpi = 96)`. The minimum allowed `dpi` value is `24` and it must be a whole number. |
|
| 105 |
#' If an invalid value is set then the default value is used and a warning is outputted to the console. |
|
| 106 |
#' |
|
| 107 |
#' @return A `shiny` module. |
|
| 108 |
#' |
|
| 109 |
#' @examples |
|
| 110 |
#' # Example using a reactive as input to plot_r |
|
| 111 |
#' library(shiny) |
|
| 112 |
#' library(ggplot2) |
|
| 113 |
#' |
|
| 114 |
#' ui <- bslib::page_fluid( |
|
| 115 |
#' plot_with_settings_ui( |
|
| 116 |
#' id = "plot_with_settings" |
|
| 117 |
#' ) |
|
| 118 |
#' ) |
|
| 119 |
#' |
|
| 120 |
#' server <- function(input, output, session) {
|
|
| 121 |
#' plot_r <- reactive({
|
|
| 122 |
#' ggplot(faithful, aes(x = .data$waiting, y = .data$eruptions)) + |
|
| 123 |
#' geom_point() |
|
| 124 |
#' }) |
|
| 125 |
#' |
|
| 126 |
#' plot_with_settings_srv( |
|
| 127 |
#' id = "plot_with_settings", |
|
| 128 |
#' plot_r = plot_r, |
|
| 129 |
#' height = c(400, 100, 1200), |
|
| 130 |
#' width = c(500, 250, 750) |
|
| 131 |
#' ) |
|
| 132 |
#' } |
|
| 133 |
#' |
|
| 134 |
#' if (interactive()) {
|
|
| 135 |
#' shinyApp(ui, server) |
|
| 136 |
#' } |
|
| 137 |
#' |
|
| 138 |
#' # Example using a function as input to plot_r |
|
| 139 |
#' library(lattice) |
|
| 140 |
#' |
|
| 141 |
#' ui <- bslib::page_fluid( |
|
| 142 |
#' radioButtons("download_option", "Select the Option", list("ggplot", "trellis", "grob", "base")),
|
|
| 143 |
#' plot_with_settings_ui( |
|
| 144 |
#' id = "plot_with_settings" |
|
| 145 |
#' ), |
|
| 146 |
#' sliderInput("nums", "Value", 1, 10, 1)
|
|
| 147 |
#' ) |
|
| 148 |
#' |
|
| 149 |
#' server <- function(input, output, session) {
|
|
| 150 |
#' plot_r <- function() {
|
|
| 151 |
#' numbers <- seq_len(input$nums) |
|
| 152 |
#' if (input$download_option == "ggplot") {
|
|
| 153 |
#' ggplot(data.frame(n = numbers), aes(.data$n)) + |
|
| 154 |
#' geom_bar() |
|
| 155 |
#' } else if (input$download_option == "trellis") {
|
|
| 156 |
#' densityplot(numbers) |
|
| 157 |
#' } else if (input$download_option == "grob") {
|
|
| 158 |
#' tr_plot <- densityplot(numbers) |
|
| 159 |
#' ggplotGrob( |
|
| 160 |
#' ggplot(data.frame(n = numbers), aes(.data$n)) + |
|
| 161 |
#' geom_bar() |
|
| 162 |
#' ) |
|
| 163 |
#' } else if (input$download_option == "base") {
|
|
| 164 |
#' plot(numbers) |
|
| 165 |
#' } |
|
| 166 |
#' } |
|
| 167 |
#' |
|
| 168 |
#' plot_with_settings_srv( |
|
| 169 |
#' id = "plot_with_settings", |
|
| 170 |
#' plot_r = plot_r, |
|
| 171 |
#' height = c(400, 100, 1200), |
|
| 172 |
#' width = c(500, 250, 750) |
|
| 173 |
#' ) |
|
| 174 |
#' } |
|
| 175 |
#' |
|
| 176 |
#' if (interactive()) {
|
|
| 177 |
#' shinyApp(ui, server) |
|
| 178 |
#' } |
|
| 179 |
#' |
|
| 180 |
#' # Example with brushing/hovering/clicking/double-clicking |
|
| 181 |
#' ui <- bslib::page_fluid( |
|
| 182 |
#' plot_with_settings_ui( |
|
| 183 |
#' id = "plot_with_settings" |
|
| 184 |
#' ), |
|
| 185 |
#' fluidRow( |
|
| 186 |
#' column(4, tags$h3("Brush"), verbatimTextOutput("brushing_data")),
|
|
| 187 |
#' column(4, tags$h3("Click"), verbatimTextOutput("clicking_data")),
|
|
| 188 |
#' column(4, tags$h3("DblClick"), verbatimTextOutput("dblclicking_data")),
|
|
| 189 |
#' column(4, tags$h3("Hover"), verbatimTextOutput("hovering_data"))
|
|
| 190 |
#' ) |
|
| 191 |
#' ) |
|
| 192 |
#' |
|
| 193 |
#' server <- function(input, output, session) {
|
|
| 194 |
#' plot_r <- reactive({
|
|
| 195 |
#' ggplot(faithful, aes(x = .data$waiting, y = .data$eruptions)) + |
|
| 196 |
#' geom_point() |
|
| 197 |
#' }) |
|
| 198 |
#' |
|
| 199 |
#' plot_data <- plot_with_settings_srv( |
|
| 200 |
#' id = "plot_with_settings", |
|
| 201 |
#' plot_r = plot_r, |
|
| 202 |
#' height = c(400, 100, 1200), |
|
| 203 |
#' brushing = TRUE, |
|
| 204 |
#' clicking = TRUE, |
|
| 205 |
#' dblclicking = TRUE, |
|
| 206 |
#' hovering = TRUE |
|
| 207 |
#' ) |
|
| 208 |
#' |
|
| 209 |
#' output$brushing_data <- renderPrint(plot_data$brush()) |
|
| 210 |
#' output$clicking_data <- renderPrint(plot_data$click()) |
|
| 211 |
#' output$dblclicking_data <- renderPrint(plot_data$dblclick()) |
|
| 212 |
#' output$hovering_data <- renderPrint(plot_data$hover()) |
|
| 213 |
#' } |
|
| 214 |
#' |
|
| 215 |
#' if (interactive()) {
|
|
| 216 |
#' shinyApp(ui, server) |
|
| 217 |
#' } |
|
| 218 |
#' |
|
| 219 |
#' # Example which allows module to be hidden/shown |
|
| 220 |
#' library("shinyjs")
|
|
| 221 |
#' |
|
| 222 |
#' ui <- bslib::page_fluid( |
|
| 223 |
#' useShinyjs(), |
|
| 224 |
#' actionButton("button", "Show/Hide"),
|
|
| 225 |
#' plot_with_settings_ui( |
|
| 226 |
#' id = "plot_with_settings" |
|
| 227 |
#' ) |
|
| 228 |
#' ) |
|
| 229 |
#' |
|
| 230 |
#' server <- function(input, output, session) {
|
|
| 231 |
#' plot_r <- plot_r <- reactive( |
|
| 232 |
#' ggplot(faithful, aes(x = .data$waiting, y = .data$eruptions)) + |
|
| 233 |
#' geom_point() |
|
| 234 |
#' ) |
|
| 235 |
#' |
|
| 236 |
#' show_hide_signal_rv <- reactiveVal(TRUE) |
|
| 237 |
#' |
|
| 238 |
#' observeEvent(input$button, show_hide_signal_rv(!show_hide_signal_rv())) |
|
| 239 |
#' |
|
| 240 |
#' plot_with_settings_srv( |
|
| 241 |
#' id = "plot_with_settings", |
|
| 242 |
#' plot_r = plot_r, |
|
| 243 |
#' height = c(400, 100, 1200), |
|
| 244 |
#' width = c(500, 250, 750), |
|
| 245 |
#' show_hide_signal = reactive(show_hide_signal_rv()) |
|
| 246 |
#' ) |
|
| 247 |
#' } |
|
| 248 |
#' |
|
| 249 |
#' if (interactive()) {
|
|
| 250 |
#' shinyApp(ui, server) |
|
| 251 |
#' } |
|
| 252 |
#' |
|
| 253 |
plot_with_settings_srv <- function(id, |
|
| 254 |
plot_r, |
|
| 255 |
height = c(600, 200, 2000), |
|
| 256 |
width = NULL, |
|
| 257 |
show_hide_signal = reactive(TRUE), |
|
| 258 |
brushing = FALSE, |
|
| 259 |
clicking = FALSE, |
|
| 260 |
dblclicking = FALSE, |
|
| 261 |
hovering = FALSE, |
|
| 262 |
graph_align = "left") {
|
|
| 263 | 16x |
checkmate::assert_string(id) |
| 264 | 16x |
checkmate::assert( |
| 265 | 16x |
checkmate::check_class(plot_r, "function"), |
| 266 | 16x |
checkmate::check_class(plot_r, "reactive") |
| 267 |
) |
|
| 268 | 15x |
checkmate::assert_numeric(height, min.len = 1, any.missing = FALSE) |
| 269 | 14x |
checkmate::assert_numeric(height, len = 3, any.missing = FALSE, finite = TRUE) |
| 270 | 14x |
checkmate::assert_numeric(height[1], lower = height[2], upper = height[3], .var.name = "height") |
| 271 | 14x |
checkmate::assert_numeric(width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
| 272 | 14x |
checkmate::assert_numeric(width[1], lower = width[2], upper = width[3], null.ok = TRUE, .var.name = "width") |
| 273 | ||
| 274 | 13x |
checkmate::assert_class(show_hide_signal, c("reactive", "function"))
|
| 275 | 12x |
checkmate::assert_flag(brushing) |
| 276 | 11x |
checkmate::assert_flag(clicking) |
| 277 | 10x |
checkmate::assert_flag(dblclicking) |
| 278 | 9x |
checkmate::assert_flag(hovering) |
| 279 | 8x |
checkmate::assert_string(graph_align) |
| 280 | 8x |
checkmate::assert_subset(graph_align, c("left", "right", "center", "justify"))
|
| 281 | ||
| 282 | 7x |
moduleServer(id, function(input, output, session) {
|
| 283 | 7x |
ns <- session$ns |
| 284 | 7x |
shinyjs::runjs( |
| 285 | 7x |
sprintf( |
| 286 | 7x |
'establishPlotResizing("%s", "%s");',
|
| 287 | 7x |
ns("plot-out-main"), # graph parent id
|
| 288 | 7x |
ns("flex_width") # session input$ variable name
|
| 289 |
) |
|
| 290 |
) |
|
| 291 | 7x |
default_w <- function() session$clientData[[paste0("output_", ns("plot_main_width"))]]
|
| 292 | 7x |
default_h <- function() session$clientData[[paste0("output_", ns("plot_main_height"))]]
|
| 293 | ||
| 294 | 7x |
default_slider_width <- reactiveVal(width) |
| 295 | 7x |
delayed_flex_width <- debounce(reactive(input$flex_width), millis = 100) |
| 296 | ||
| 297 | 7x |
if (is.null(width)) {
|
| 298 |
# if width = NULL then set default_slider_width to be the value of the plot width on load |
|
| 299 | ! |
observeEvent(session$clientData[[paste0("output_", ns("plot_main_width"))]],
|
| 300 | ! |
handlerExpr = {
|
| 301 | ! |
default_slider_width(default_w() * c(1, 0.5, 2.8)) |
| 302 |
}, |
|
| 303 | ! |
once = TRUE, |
| 304 | ! |
ignoreNULL = TRUE |
| 305 |
) |
|
| 306 |
} |
|
| 307 | ||
| 308 | 7x |
plot_type <- reactive({
|
| 309 | 7x |
if (inherits(plot_r(), "ggplot")) {
|
| 310 | 1x |
"gg" |
| 311 | 6x |
} else if (inherits(plot_r(), "trellis")) {
|
| 312 | 1x |
"trel" |
| 313 | 5x |
} else if (inherits(plot_r(), "grob")) {
|
| 314 | 1x |
"grob" |
| 315 | 4x |
} else if (inherits(plot_r(), c("NULL", "histogram", "list")) && !inherits(plot_r, "reactive")) {
|
| 316 | 3x |
"base" |
| 317 |
} else {
|
|
| 318 | 1x |
"other" |
| 319 |
} |
|
| 320 |
}) |
|
| 321 | ||
| 322 |
# allow modules which use this module to turn on and off the UI |
|
| 323 | 7x |
observeEvent(show_hide_signal(), {
|
| 324 | 7x |
if (show_hide_signal()) {
|
| 325 | 7x |
shinyjs::show("plot-with-settings")
|
| 326 |
} else {
|
|
| 327 | ! |
shinyjs::hide("plot-with-settings")
|
| 328 |
} |
|
| 329 |
}) |
|
| 330 | ||
| 331 | 7x |
output$slider_ui <- renderUI({
|
| 332 | 7x |
req(default_slider_width()) |
| 333 | 7x |
tags$div( |
| 334 | 7x |
optionalSliderInputValMinMax( |
| 335 | 7x |
inputId = ns("height"),
|
| 336 | 7x |
label = "Plot height", |
| 337 | 7x |
value_min_max = round(height), |
| 338 | 7x |
ticks = FALSE, |
| 339 | 7x |
step = 1L, |
| 340 | 7x |
round = TRUE |
| 341 |
), |
|
| 342 | 7x |
tags$b("Plot width"),
|
| 343 | 7x |
bslib::input_switch( |
| 344 | 7x |
id = ns("width_resize_switch"),
|
| 345 | 7x |
label = "Automatic", |
| 346 | 7x |
value = `if`(is.null(width), TRUE, FALSE) |
| 347 |
), |
|
| 348 | 7x |
optionalSliderInputValMinMax( |
| 349 | 7x |
inputId = ns("width"),
|
| 350 | 7x |
label = NULL, |
| 351 | 7x |
value_min_max = round(isolate(default_slider_width())), |
| 352 | 7x |
ticks = FALSE, |
| 353 | 7x |
step = 1L, |
| 354 | 7x |
round = TRUE |
| 355 |
) |
|
| 356 |
) |
|
| 357 |
}) |
|
| 358 | ||
| 359 | 7x |
observeEvent(input$width_resize_switch | delayed_flex_width(), {
|
| 360 | 7x |
if (!isFALSE(input$width_resize_switch)) {
|
| 361 | 7x |
shinyjs::disable("width")
|
| 362 | 7x |
updateSliderInput(session, inputId = "width", value = delayed_flex_width()) |
| 363 |
} else {
|
|
| 364 | ! |
shinyjs::enable("width")
|
| 365 |
} |
|
| 366 |
}) |
|
| 367 | ||
| 368 | 7x |
ranges <- reactiveValues(x = NULL, y = NULL) |
| 369 | ||
| 370 | 7x |
observeEvent(input$plot_dblclick, {
|
| 371 | 1x |
brush <- input$plot_brush |
| 372 | 1x |
if (!is.null(brush)) {
|
| 373 | ! |
ranges$x <- c(brush$xmin, brush$xmax) |
| 374 | ! |
ranges$y <- c(brush$ymin, brush$ymax) |
| 375 |
} else {
|
|
| 376 | 1x |
ranges$x <- NULL |
| 377 | 1x |
ranges$y <- NULL |
| 378 |
} |
|
| 379 |
}) |
|
| 380 | ||
| 381 | 7x |
p_height <- reactive(if (!is.null(input$height)) input$height else height[1]) |
| 382 | 7x |
p_width <- reactive( |
| 383 | 7x |
if (!is.null(input$width)) {
|
| 384 | 5x |
input$width |
| 385 |
} else {
|
|
| 386 | 2x |
if (!is.null(default_slider_width()[1])) {
|
| 387 | 2x |
default_slider_width()[1] |
| 388 |
} else {
|
|
| 389 |
# Fallback to "auto" |
|
| 390 | ! |
"auto" |
| 391 |
} |
|
| 392 |
} |
|
| 393 |
) |
|
| 394 | 7x |
output$plot_main <- renderPlot( |
| 395 | 7x |
apply_plot_modifications( |
| 396 | 7x |
plot_obj = plot_r(), |
| 397 | 7x |
plot_type = plot_type(), |
| 398 | 7x |
dblclicking = dblclicking, |
| 399 | 7x |
ranges = ranges |
| 400 |
), |
|
| 401 | 7x |
res = get_plot_dpi(), |
| 402 | 7x |
height = p_height, |
| 403 | 7x |
width = p_width |
| 404 |
) |
|
| 405 | ||
| 406 | 7x |
output$plot_out_main <- renderUI({
|
| 407 | 7x |
req(plot_r()) |
| 408 | 4x |
tags$div( |
| 409 | 4x |
align = graph_align, |
| 410 | 4x |
plotOutput( |
| 411 | 4x |
ns("plot_main"),
|
| 412 | 4x |
height = "100%", |
| 413 | 4x |
width = p_width(), |
| 414 | 4x |
brush = `if`(brushing, brushOpts(ns("plot_brush"), resetOnNew = FALSE), NULL),
|
| 415 | 4x |
click = `if`(clicking, clickOpts(ns("plot_click")), NULL),
|
| 416 | 4x |
dblclick = `if`(dblclicking, dblclickOpts(ns("plot_dblclick")), NULL),
|
| 417 | 4x |
hover = `if`(hovering, hoverOpts(ns("plot_hover")), NULL)
|
| 418 |
) |
|
| 419 |
) |
|
| 420 |
}) |
|
| 421 | ||
| 422 | 7x |
output$width_warning <- renderUI({
|
| 423 | 7x |
grDevices::pdf(nullfile()) # reset Rplots.pdf for shiny server |
| 424 | 7x |
w <- grDevices::dev.size("px")[1]
|
| 425 | 7x |
grDevices::dev.off() |
| 426 | 7x |
if (p_width() < w) {
|
| 427 | 7x |
helpText( |
| 428 | 7x |
icon("triangle-exclamation"),
|
| 429 | 7x |
"Plot might be cut off for small widths." |
| 430 |
) |
|
| 431 |
} |
|
| 432 |
}) |
|
| 433 | ||
| 434 | 7x |
type_download_srv( |
| 435 | 7x |
id = "downbutton", |
| 436 | 7x |
plot_reactive = plot_r, |
| 437 | 7x |
plot_type = plot_type, |
| 438 | 7x |
plot_w = p_width, |
| 439 | 7x |
default_w = default_w, |
| 440 | 7x |
plot_h = p_height, |
| 441 | 7x |
default_h = default_h |
| 442 |
) |
|
| 443 | ||
| 444 | 7x |
list( |
| 445 | 7x |
brush = reactive({
|
| 446 |
# refresh brush data on the main plot size change |
|
| 447 | 1x |
input$height |
| 448 | 1x |
input$width |
| 449 | 1x |
input$plot_brush |
| 450 |
}), |
|
| 451 | 7x |
click = reactive({
|
| 452 |
# refresh click data on the main plot size change |
|
| 453 | 1x |
input$height |
| 454 | 1x |
input$width |
| 455 | 1x |
input$plot_click |
| 456 |
}), |
|
| 457 | 7x |
dblclick = reactive({
|
| 458 |
# refresh double click data on the main plot size change |
|
| 459 | 1x |
input$height |
| 460 | 1x |
input$width |
| 461 | 1x |
input$plot_dblclick |
| 462 |
}), |
|
| 463 | 7x |
hover = reactive({
|
| 464 |
# refresh hover data on the main plot size change |
|
| 465 | 1x |
input$height |
| 466 | 1x |
input$width |
| 467 | 1x |
input$plot_hover |
| 468 |
}), |
|
| 469 | 7x |
dim = reactive(c(p_width(), p_height())) |
| 470 |
) |
|
| 471 |
}) |
|
| 472 |
} |
|
| 473 | ||
| 474 |
#' @keywords internal |
|
| 475 |
type_download_ui <- function(id) {
|
|
| 476 | 2x |
ns <- NS(id) |
| 477 | 2x |
bslib::popover( |
| 478 | 2x |
icon("download"),
|
| 479 | 2x |
tags$div( |
| 480 | 2x |
radioButtons(ns("file_format"),
|
| 481 | 2x |
label = "File type", |
| 482 | 2x |
choices = c("png" = "png", "pdf" = "pdf", "svg" = "svg"),
|
| 483 |
), |
|
| 484 | 2x |
textInput(ns("file_name"),
|
| 485 | 2x |
label = "File name (without extension)", |
| 486 | 2x |
value = paste0("plot_", strftime(Sys.time(), format = "%Y%m%d_%H%M%S"))
|
| 487 |
), |
|
| 488 | 2x |
conditionalPanel( |
| 489 | 2x |
condition = paste0("input['", ns("file_name"), "'] != ''"),
|
| 490 | 2x |
downloadButton(ns("data_download"), label = character(0), class = "btn-sm w-full")
|
| 491 |
) |
|
| 492 |
) |
|
| 493 |
) |
|
| 494 |
} |
|
| 495 | ||
| 496 |
#' @keywords internal |
|
| 497 |
type_download_srv <- function(id, plot_reactive, plot_type, plot_w, default_w, plot_h, default_h) {
|
|
| 498 | 11x |
moduleServer( |
| 499 | 11x |
id, |
| 500 | 11x |
function(input, output, session) {
|
| 501 | 11x |
output$data_download <- downloadHandler( |
| 502 | 11x |
filename = function() {
|
| 503 | 19x |
paste(input$file_name, input$file_format, sep = ".") |
| 504 |
}, |
|
| 505 | 11x |
content = function(file) {
|
| 506 | 19x |
width <- `if`(!is.null(plot_w()), plot_w(), default_w()) |
| 507 | 19x |
height <- `if`(!is.null(plot_h()), plot_h(), default_h()) |
| 508 | ||
| 509 |
# svg and pdf have width in inches and 1 inch = get_plot_dpi() pixels |
|
| 510 | 19x |
switch(input$file_format, |
| 511 | 11x |
png = grDevices::png(file, width, height), |
| 512 | 4x |
pdf = grDevices::pdf(file, width / get_plot_dpi(), height / get_plot_dpi()), |
| 513 | 4x |
svg = grDevices::svg(file, width / get_plot_dpi(), height / get_plot_dpi()) |
| 514 |
) |
|
| 515 | ||
| 516 | 19x |
print_plot(plot_reactive, plot_type) |
| 517 | ||
| 518 | 19x |
grDevices::dev.off() |
| 519 |
} |
|
| 520 |
) |
|
| 521 |
} |
|
| 522 |
) |
|
| 523 |
} |
|
| 524 | ||
| 525 |
#' Clean brushed points |
|
| 526 |
#' |
|
| 527 |
#' @description |
|
| 528 |
#' Cleans and organizes output to account for NAs and remove empty rows. Wrapper around `shiny::brushedPoints`. |
|
| 529 |
#' @param data (`data.frame`)\cr |
|
| 530 |
#' A data.frame from which to select rows. |
|
| 531 |
#' @param brush (`list`)\cr |
|
| 532 |
#' The data from a brush e.g. `input$plot_brush`. |
|
| 533 |
#' |
|
| 534 |
#' @return A `data.frame` of selected rows. |
|
| 535 |
#' |
|
| 536 |
#' @examples |
|
| 537 |
#' |
|
| 538 |
#' brush <- list( |
|
| 539 |
#' mapping = list( |
|
| 540 |
#' x = "AGE", |
|
| 541 |
#' y = "BMRKR1" |
|
| 542 |
#' ), |
|
| 543 |
#' xmin = 30, xmax = 40, |
|
| 544 |
#' ymin = 0.7, ymax = 10, |
|
| 545 |
#' direction = "xy" |
|
| 546 |
#' ) |
|
| 547 |
#' |
|
| 548 |
#' data <- data.frame( |
|
| 549 |
#' STUDYID = letters[1:20], |
|
| 550 |
#' USUBJID = LETTERS[1:20], |
|
| 551 |
#' AGE = sample(25:40, size = 20, replace = TRUE), |
|
| 552 |
#' BMRKR1 = runif(20, min = 0, max = 12) |
|
| 553 |
#' ) |
|
| 554 |
#' nrow(clean_brushedPoints(data, brush)) |
|
| 555 |
#' data$AGE[1:10] <- NA |
|
| 556 |
#' nrow(clean_brushedPoints(data, brush)) |
|
| 557 |
#' |
|
| 558 |
#' @export |
|
| 559 |
#' |
|
| 560 |
clean_brushedPoints <- function(data, brush) { # nolint object_name_linter.
|
|
| 561 | 6x |
checkmate::assert_data_frame(data) |
| 562 | 4x |
checkmate::assert_list(brush, null.ok = TRUE) |
| 563 | ||
| 564 |
# define original panelvar1 and panelvar2 before getting overwritten |
|
| 565 | 4x |
original_panelvar1 <- brush$mapping$panelvar1 |
| 566 | 4x |
original_panelvar2 <- brush$mapping$panelvar2 |
| 567 | ||
| 568 |
# Assign NULL to `mapping$panelvar1` and `mapping$panelvar1` if `brush$panelvar1` and `brush$panelvar1` are NULL |
|
| 569 |
# This will not evaluate the `panelMatch` step in `brushedPoints` and thus will return a non empty dataframe |
|
| 570 | 4x |
if (is.null(brush$panelvar1)) brush$mapping$panelvar1 <- NULL |
| 571 | 4x |
if (is.null(brush$panelvar2)) brush$mapping$panelvar2 <- NULL |
| 572 | ||
| 573 | 4x |
bp_df <- brushedPoints(data, brush) |
| 574 | ||
| 575 |
# Keep required rows only based on the value of `brush$panelvar1` |
|
| 576 | 3x |
df <- if (is.null(brush$panelvar1) && is.character(original_panelvar1) && |
| 577 | 3x |
is.null(brush$panelvar2) && is.character(original_panelvar2)) {
|
| 578 | ! |
df_var1 <- bp_df[is.na(bp_df[[original_panelvar1]]), ] |
| 579 | ! |
df_var1[is.na(df_var1[[original_panelvar2]]), ] |
| 580 | 3x |
} else if (is.null(brush$panelvar1) && is.character(original_panelvar1)) {
|
| 581 | ! |
bp_df[is.na(bp_df[[original_panelvar1]]), ] |
| 582 | 3x |
} else if (is.null(brush$panelvar2) && is.character(original_panelvar2)) {
|
| 583 | ! |
bp_df[is.na(bp_df[[original_panelvar2]]), ] |
| 584 |
} else {
|
|
| 585 | 3x |
bp_df |
| 586 |
} |
|
| 587 | ||
| 588 |
# filter out rows that are only NAs |
|
| 589 | 3x |
df <- df[rowSums(is.na(df)) != ncol(df), ] |
| 590 | 3x |
df |
| 591 |
} |
|
| 592 | ||
| 593 |
#' @keywords internal |
|
| 594 |
#' |
|
| 595 |
get_plot_dpi <- function() {
|
|
| 596 | 32x |
default_dpi <- 72 |
| 597 | 32x |
dpi <- getOption("teal.plot_dpi", default_dpi)
|
| 598 | 32x |
if (!checkmate::test_integerish(dpi, lower = 24, any.missing = FALSE, len = 1)) {
|
| 599 | 4x |
warning(paste("Invalid value for option 'teal.plot_dpi', therefore defaulting to", default_dpi, "dpi"))
|
| 600 | 4x |
dpi <- default_dpi |
| 601 |
} |
|
| 602 | 32x |
dpi |
| 603 |
} |
|
| 604 | ||
| 605 |
#' Print plot for download functionality |
|
| 606 |
#' |
|
| 607 |
#' @param plot (`reactive`)\cr |
|
| 608 |
#' reactive expression to draw a plot |
|
| 609 |
#' @param plot_type (`reactive`)\cr |
|
| 610 |
#' reactive plot type (`gg`, `trel`, `grob`, `other`) |
|
| 611 |
#' |
|
| 612 |
#' @return Nothing returned, the plot is printed. |
|
| 613 |
#' @keywords internal |
|
| 614 |
#' |
|
| 615 |
print_plot <- function(plot, plot_type) {
|
|
| 616 | 25x |
switch(plot_type(), |
| 617 | 2x |
"grob" = grid::grid.draw(plot()), |
| 618 |
"other" = {
|
|
| 619 | 2x |
graphics::plot.new() |
| 620 | 2x |
graphics::text( |
| 621 | 2x |
x = graphics::grconvertX(0.5, from = "npc"), |
| 622 | 2x |
y = graphics::grconvertY(0.5, from = "npc"), |
| 623 | 2x |
labels = "This plot graphic type is not yet supported to download" |
| 624 |
) |
|
| 625 |
}, |
|
| 626 | 17x |
"base" = plot(), |
| 627 | 4x |
print(plot()) |
| 628 |
) |
|
| 629 |
} |
| 1 |
#' @keywords internal |
|
| 2 |
#' @noRd |
|
| 3 |
table_with_settings_deps <- function() {
|
|
| 4 | 1x |
htmltools::htmlDependency( |
| 5 | 1x |
name = "teal-widgets-table-with-settings", |
| 6 | 1x |
version = utils::packageVersion("teal.widgets"),
|
| 7 | 1x |
package = "teal.widgets", |
| 8 | 1x |
src = "table-with-settings", |
| 9 | 1x |
stylesheet = "table-with-settings.css" |
| 10 |
) |
|
| 11 |
} |
|
| 12 | ||
| 13 |
#' @name table_with_settings |
|
| 14 |
#' |
|
| 15 |
#' @title `table_with_settings` module |
|
| 16 |
#' |
|
| 17 |
#' @description |
|
| 18 |
#' Module designed to create a `shiny` table output based on `rtable` object (`ElementaryTable` or `TableTree`) input. |
|
| 19 |
#' @inheritParams shiny::moduleServer |
|
| 20 |
#' @param ... (`character`)\cr |
|
| 21 |
#' Useful for providing additional HTML classes for the output tag. |
|
| 22 |
#' |
|
| 23 |
#' @rdname table_with_settings |
|
| 24 |
#' @export |
|
| 25 |
#' |
|
| 26 |
table_with_settings_ui <- function(id, ...) {
|
|
| 27 | 1x |
checkmate::assert_string(id) |
| 28 | ||
| 29 | 1x |
ns <- NS(id) |
| 30 | ||
| 31 | 1x |
tags$div( |
| 32 | 1x |
table_with_settings_deps(), |
| 33 | 1x |
shinyjs::useShinyjs(), |
| 34 | 1x |
bslib::card( |
| 35 | 1x |
id = ns("table-with-settings"),
|
| 36 | 1x |
full_screen = TRUE, |
| 37 | 1x |
tags$div( |
| 38 | 1x |
class = "teal-widgets settings-buttons", |
| 39 | 1x |
bslib::tooltip( |
| 40 | 1x |
trigger = tags$div(type_download_ui_table(ns("downbutton"))),
|
| 41 | 1x |
options = list(trigger = "hover"), |
| 42 | 1x |
class = "download-button", |
| 43 | 1x |
"Download" |
| 44 |
) |
|
| 45 |
), |
|
| 46 | 1x |
tags$div( |
| 47 | 1x |
class = "teal-widgets table-content", |
| 48 | 1x |
uiOutput(ns("table_out_main"), width = "100%", ...)
|
| 49 |
) |
|
| 50 |
) |
|
| 51 |
) |
|
| 52 |
} |
|
| 53 | ||
| 54 |
#' @inheritParams shiny::moduleServer |
|
| 55 |
#' @param table_r (`reactive`)\cr |
|
| 56 |
#' reactive expression that yields an `rtable` object (`ElementaryTable` or `TableTree`) |
|
| 57 |
#' @param show_hide_signal (`reactive logical`) optional\cr |
|
| 58 |
#' mechanism to allow modules which call this module to show/hide the table_with_settings UI. |
|
| 59 |
#' |
|
| 60 |
#' @rdname table_with_settings |
|
| 61 |
#' |
|
| 62 |
#' @return A `shiny` module. |
|
| 63 |
#' |
|
| 64 |
#' @export |
|
| 65 |
#' |
|
| 66 |
#' @examples |
|
| 67 |
#' library(shiny) |
|
| 68 |
#' library(rtables) |
|
| 69 |
#' library(magrittr) |
|
| 70 |
#' |
|
| 71 |
#' ui <- bslib::page_fluid( |
|
| 72 |
#' table_with_settings_ui( |
|
| 73 |
#' id = "table_with_settings" |
|
| 74 |
#' ) |
|
| 75 |
#' ) |
|
| 76 |
#' |
|
| 77 |
#' server <- function(input, output, session) {
|
|
| 78 |
#' table_r <- reactive({
|
|
| 79 |
#' l <- basic_table() %>% |
|
| 80 |
#' split_cols_by("ARM") %>%
|
|
| 81 |
#' analyze(c("SEX", "AGE"))
|
|
| 82 |
#' |
|
| 83 |
#' tbl <- build_table(l, DM) |
|
| 84 |
#' |
|
| 85 |
#' tbl |
|
| 86 |
#' }) |
|
| 87 |
#' |
|
| 88 |
#' table_with_settings_srv(id = "table_with_settings", table_r = table_r) |
|
| 89 |
#' } |
|
| 90 |
#' |
|
| 91 |
#' if (interactive()) {
|
|
| 92 |
#' shinyApp(ui, server) |
|
| 93 |
#' } |
|
| 94 |
#' |
|
| 95 |
table_with_settings_srv <- function(id, table_r, show_hide_signal = reactive(TRUE)) {
|
|
| 96 | 5x |
checkmate::assert_class(table_r, c("reactive", "function"))
|
| 97 | 4x |
checkmate::assert_class(show_hide_signal, c("reactive", "function"))
|
| 98 | ||
| 99 | 3x |
if (!requireNamespace("rtables", quietly = TRUE)) {
|
| 100 | ! |
stop("package rtables is required, please install")
|
| 101 |
} |
|
| 102 | ||
| 103 | 3x |
moduleServer(id, function(input, output, session) {
|
| 104 | 3x |
ns <- session$ns |
| 105 |
# Turn on and off the UI |
|
| 106 | 3x |
observeEvent(show_hide_signal(), {
|
| 107 | 3x |
if (show_hide_signal()) {
|
| 108 | 2x |
shinyjs::show("table-with-settings")
|
| 109 |
} else {
|
|
| 110 | 1x |
shinyjs::hide("table-with-settings")
|
| 111 |
} |
|
| 112 |
}) |
|
| 113 | ||
| 114 | 3x |
output$table_out_main <- output$table_out_modal <- renderUI({
|
| 115 | 6x |
rtables::as_html(table_r()) |
| 116 |
}) |
|
| 117 | ||
| 118 | 3x |
type_download_srv_table( |
| 119 | 3x |
id = "downbutton", |
| 120 | 3x |
table_reactive = table_r |
| 121 |
) |
|
| 122 |
}) |
|
| 123 |
} |
|
| 124 | ||
| 125 |
type_download_ui_table <- function(id) {
|
|
| 126 | 1x |
ns <- NS(id) |
| 127 | 1x |
bslib::popover( |
| 128 | 1x |
icon("download"),
|
| 129 | 1x |
tags$div( |
| 130 | 1x |
radioButtons(ns("file_format"),
|
| 131 | 1x |
label = "File type", |
| 132 | 1x |
choices = c("formatted txt" = ".txt", "csv" = ".csv", "pdf" = ".pdf"),
|
| 133 |
), |
|
| 134 | 1x |
textInput(ns("file_name"),
|
| 135 | 1x |
label = "File name (without extension)", |
| 136 | 1x |
value = paste0("table_", strftime(Sys.time(), format = "%Y%m%d_%H%M%S"))
|
| 137 |
), |
|
| 138 | 1x |
conditionalPanel( |
| 139 | 1x |
condition = paste0("input['", ns("file_format"), "'] != '.csv'"),
|
| 140 | 1x |
tags$div( |
| 141 | 1x |
class = "lock-btn", |
| 142 | 1x |
title = "on / off", |
| 143 | 1x |
shinyWidgets::prettyToggle( |
| 144 | 1x |
ns("pagination_switch"),
|
| 145 | 1x |
value = FALSE, |
| 146 | 1x |
label_on = NULL, |
| 147 | 1x |
label_off = NULL, |
| 148 | 1x |
status_on = "default", |
| 149 | 1x |
status_off = "default", |
| 150 | 1x |
outline = FALSE, |
| 151 | 1x |
plain = TRUE, |
| 152 | 1x |
icon_on = icon("fas fa-toggle-off"),
|
| 153 | 1x |
icon_off = icon("fas fa-toggle-on"),
|
| 154 | 1x |
animation = "pulse" |
| 155 |
) |
|
| 156 |
), |
|
| 157 | 1x |
tags$div( |
| 158 | 1x |
class = "paginate-ui", |
| 159 | 1x |
shinyWidgets::numericInputIcon( |
| 160 | 1x |
inputId = ns("lpp"),
|
| 161 | 1x |
label = "Paginate table:", |
| 162 | 1x |
value = 70, |
| 163 | 1x |
icon = list("lines / page")
|
| 164 |
), |
|
| 165 | 1x |
uiOutput(ns("lpp_warning"))
|
| 166 |
) |
|
| 167 |
), |
|
| 168 | 1x |
conditionalPanel( |
| 169 | 1x |
condition = paste0("input['", ns("file_name"), "'] != ''"),
|
| 170 | 1x |
downloadButton(ns("data_download"), label = character(0), class = "btn-sm w-full")
|
| 171 |
) |
|
| 172 |
) |
|
| 173 |
) |
|
| 174 |
} |
|
| 175 | ||
| 176 |
type_download_srv_table <- function(id, table_reactive) {
|
|
| 177 | 9x |
moduleServer( |
| 178 | 9x |
id, |
| 179 | 9x |
function(input, output, session) {
|
| 180 | 9x |
observeEvent(input$pagination_switch, {
|
| 181 | 13x |
if (input$pagination_switch) {
|
| 182 | 7x |
shinyjs::enable("lpp")
|
| 183 |
} else {
|
|
| 184 | 6x |
shinyjs::disable("lpp")
|
| 185 |
} |
|
| 186 |
}) |
|
| 187 | ||
| 188 | 9x |
output$lpp_warning <- renderUI({
|
| 189 | 28x |
catch_warning <- if (input$file_format != ".csv" && input$pagination_switch) {
|
| 190 | 9x |
try(rtables::paginate_table( |
| 191 | 9x |
tt = table_reactive(), |
| 192 | 9x |
lpp = as.numeric(input$lpp) |
| 193 | 9x |
), silent = TRUE) |
| 194 |
} |
|
| 195 | ||
| 196 | 21x |
if (inherits(catch_warning, "try-error")) {
|
| 197 | 4x |
helpText( |
| 198 | 4x |
class = "error", |
| 199 | 4x |
icon("triangle-exclamation"),
|
| 200 | 4x |
"Maximum lines per page includes the reprinted header. Please enter a numeric value or increase the value." |
| 201 |
) |
|
| 202 |
} |
|
| 203 |
}) |
|
| 204 | ||
| 205 | 9x |
output$data_download <- downloadHandler( |
| 206 | 9x |
filename = function() {
|
| 207 | 22x |
paste0(input$file_name, input$file_format) |
| 208 |
}, |
|
| 209 | 9x |
content = function(file) {
|
| 210 | 22x |
if (input$file_format == ".txt") {
|
| 211 | 8x |
rtables::export_as_txt( |
| 212 | 8x |
x = table_reactive(), |
| 213 | 8x |
file = file, |
| 214 | 8x |
paginate = input$pagination_switch, |
| 215 | 8x |
lpp = if (input$pagination_switch) as.numeric(input$lpp) |
| 216 |
) |
|
| 217 | 14x |
} else if (input$file_format == ".csv") {
|
| 218 | 7x |
result <- rtables::matrix_form(table_reactive())$strings |
| 219 | 7x |
utils::write.table( |
| 220 | 7x |
x = result, |
| 221 | 7x |
file = file, |
| 222 | 7x |
sep = ",", |
| 223 | 7x |
col.names = FALSE, |
| 224 | 7x |
row.names = TRUE, |
| 225 | 7x |
append = FALSE |
| 226 |
) |
|
| 227 |
} else {
|
|
| 228 | 7x |
rtables::export_as_pdf( |
| 229 | 7x |
x = table_reactive(), |
| 230 | 7x |
file = file, |
| 231 | 7x |
paginate = input$pagination_switch, |
| 232 | 7x |
lpp = if (input$pagination_switch) as.numeric(input$lpp) |
| 233 |
) |
|
| 234 |
} |
|
| 235 |
} |
|
| 236 |
) |
|
| 237 |
} |
|
| 238 |
) |
|
| 239 |
} |
| 1 |
#' @keywords internal |
|
| 2 |
#' @noRd |
|
| 3 |
verbatim_popup_deps <- function() {
|
|
| 4 | 5x |
htmltools::htmlDependency( |
| 5 | 5x |
name = "teal-widgets-verbatim-popup", |
| 6 | 5x |
version = utils::packageVersion("teal.widgets"),
|
| 7 | 5x |
package = "teal.widgets", |
| 8 | 5x |
src = "verbatim-popup", |
| 9 | 5x |
stylesheet = "verbatim-popup.css", |
| 10 | 5x |
script = "verbatim-popup.js" |
| 11 |
) |
|
| 12 |
} |
|
| 13 | ||
| 14 |
#' A `shiny` module that pops up verbatim text. |
|
| 15 |
#' @name verbatim_popup |
|
| 16 |
#' @description |
|
| 17 |
#' This module consists of a button that once clicked pops up a |
|
| 18 |
#' modal window with verbatim-styled text. |
|
| 19 |
#' |
|
| 20 |
#' @param id (`character(1)`) the `shiny` id |
|
| 21 |
#' @param button_label (`character(1)`) the text printed on the button |
|
| 22 |
#' @param type (`character(1)`) specifying whether to use `[shiny::actionButton()]` or `[shiny::actionLink()]`. |
|
| 23 |
#' @param ... additional arguments to `[shiny::actionButton()]`(or `[shiny::actionLink()]`). |
|
| 24 |
#' |
|
| 25 |
#' @return the UI function returns a `shiny.tag.list` object |
|
| 26 |
#' @export |
|
| 27 |
#' |
|
| 28 |
#' @examples |
|
| 29 |
#' library(shiny) |
|
| 30 |
#' |
|
| 31 |
#' ui <- bslib::page_fluid(verbatim_popup_ui("my_id", button_label = "Open popup"))
|
|
| 32 |
#' srv <- function(input, output) {
|
|
| 33 |
#' verbatim_popup_srv( |
|
| 34 |
#' "my_id", |
|
| 35 |
#' "if (TRUE) { print('Popups are the best') }",
|
|
| 36 |
#' title = "My custom title", |
|
| 37 |
#' style = TRUE |
|
| 38 |
#' ) |
|
| 39 |
#' } |
|
| 40 |
#' if (interactive()) shinyApp(ui, srv) |
|
| 41 |
#' |
|
| 42 |
verbatim_popup_ui <- function(id, button_label, type = c("button", "link"), ...) {
|
|
| 43 | 5x |
checkmate::assert_string(id) |
| 44 | 5x |
checkmate::assert_string(button_label) |
| 45 | ||
| 46 | 5x |
ns <- shiny::NS(id) |
| 47 | ||
| 48 | 5x |
shiny::tagList( |
| 49 | 5x |
verbatim_popup_deps(), |
| 50 | 5x |
shinyjs::useShinyjs(), |
| 51 | 5x |
shiny::actionButton( |
| 52 | 5x |
inputId = ns("button"),
|
| 53 | 5x |
label = button_label, |
| 54 | 5x |
class = c("teal-widgets-busy-disable", match.arg(type)),
|
| 55 |
... |
|
| 56 |
) |
|
| 57 |
) |
|
| 58 |
} |
|
| 59 | ||
| 60 |
#' @name verbatim_popup |
|
| 61 |
#' @export |
|
| 62 |
#' |
|
| 63 |
#' @param verbatim_content (`character`, `expression`, `condition` or `reactive(1)` |
|
| 64 |
#' holding any of the above) the content to show in the popup modal window |
|
| 65 |
#' @param title (`character(1)`) the title of the modal window |
|
| 66 |
#' @param style (`logical(1)`) whether to style the `verbatim_content` using `styler::style_text`. |
|
| 67 |
#' If `verbatim_content` is a `condition` or `reactive` holding `condition` then this argument is ignored |
|
| 68 |
#' @param disabled (`reactive(1)`) the `shiny` reactive value holding a `logical`. The popup button is disabled |
|
| 69 |
#' when the flag is `TRUE` and enabled otherwise. |
|
| 70 |
#' |
|
| 71 |
verbatim_popup_srv <- function(id, verbatim_content, title, style = FALSE, disabled = shiny::reactiveVal(FALSE)) {
|
|
| 72 | ! |
checkmate::assert_string(id) |
| 73 | ! |
checkmate::assert_string(title) |
| 74 | ! |
checkmate::assert_flag(style) |
| 75 | ! |
checkmate::assert_class(disabled, classes = "reactive") |
| 76 | ! |
moduleServer(id, function(input, output, session) {
|
| 77 | ! |
ns <- session$ns |
| 78 | ! |
modal_content <- format_content(verbatim_content, style) |
| 79 | ! |
button_click_observer( |
| 80 | ! |
click_event = shiny::reactive(input$button), |
| 81 | ! |
copy_button_id = ns("copy_button"),
|
| 82 | ! |
copied_area_id = ns("verbatim_content"),
|
| 83 | ! |
modal_title = title, |
| 84 | ! |
modal_content = modal_content, |
| 85 | ! |
disabled = disabled |
| 86 |
) |
|
| 87 |
}) |
|
| 88 |
} |
|
| 89 | ||
| 90 |
#' Creates a `shiny` observer handling button clicks. |
|
| 91 |
#' |
|
| 92 |
#' @description |
|
| 93 |
#' When the button is clicked it pop up a modal window with the text. |
|
| 94 |
#' |
|
| 95 |
#' @keywords internal |
|
| 96 |
#' @param click_event `reactive` the click event |
|
| 97 |
#' @param copy_button_id (`character(1)`) the id of the button to copy the modal content. |
|
| 98 |
#' Automatically appended with a 1 and 2 suffix for top and bottom buttons respectively. |
|
| 99 |
#' @param copied_area_id (`character(1)`) the id of the element which contents are copied |
|
| 100 |
#' @param modal_title (`character(1)`) the title of the modal window |
|
| 101 |
#' @param modal_content (`reactive`) the content of the modal window |
|
| 102 |
#' @param disabled (`reactive(1)`) the `shiny` reactive value holding a `logical`. The popup button is disabled |
|
| 103 |
#' when the flag is `TRUE` and enabled otherwise. |
|
| 104 |
button_click_observer <- function(click_event, |
|
| 105 |
copy_button_id, |
|
| 106 |
copied_area_id, |
|
| 107 |
modal_title, |
|
| 108 |
modal_content, |
|
| 109 |
disabled) {
|
|
| 110 | 1x |
shiny::observeEvent( |
| 111 | 1x |
disabled(), |
| 112 | 1x |
handlerExpr = {
|
| 113 | ! |
if (disabled()) {
|
| 114 | ! |
shinyjs::disable("button")
|
| 115 |
} else {
|
|
| 116 | ! |
shinyjs::enable("button")
|
| 117 |
} |
|
| 118 |
} |
|
| 119 |
) |
|
| 120 | ||
| 121 | 1x |
shiny::observeEvent( |
| 122 | 1x |
click_event(), |
| 123 | 1x |
handlerExpr = {
|
| 124 | ! |
req(modal_content()) |
| 125 | ! |
shiny::showModal( |
| 126 | ! |
div( |
| 127 | ! |
class = "teal-widgets button-click-observer", |
| 128 | ! |
shiny::modalDialog( |
| 129 | ! |
tags$div( |
| 130 | ! |
tags$div( |
| 131 | ! |
shiny::modalButton("Dismiss"),
|
| 132 | ! |
shiny::actionButton( |
| 133 | ! |
paste0(copy_button_id, 1), |
| 134 | ! |
"Copy to Clipboard", |
| 135 | ! |
class = "btn-primary", |
| 136 | ! |
onclick = paste0("copyToClipboard('", copied_area_id, "')")
|
| 137 |
) |
|
| 138 |
), |
|
| 139 | ! |
tags$pre(id = copied_area_id, modal_content()), |
| 140 |
), |
|
| 141 | ! |
title = modal_title, |
| 142 | ! |
footer = shiny::tagList( |
| 143 | ! |
shiny::modalButton("Dismiss"),
|
| 144 | ! |
shiny::actionButton( |
| 145 | ! |
paste0(copy_button_id, 2), |
| 146 | ! |
"Copy to Clipboard", |
| 147 | ! |
class = "btn-primary", |
| 148 | ! |
onclick = paste0("copyToClipboard('", copied_area_id, "')")
|
| 149 |
) |
|
| 150 |
), |
|
| 151 | ! |
size = "l", |
| 152 | ! |
easyClose = TRUE |
| 153 |
) |
|
| 154 |
) |
|
| 155 |
) |
|
| 156 |
} |
|
| 157 |
) |
|
| 158 |
} |
|
| 159 | ||
| 160 |
#' Formats the content of the modal popup window. |
|
| 161 |
#' |
|
| 162 |
#' @details |
|
| 163 |
#' Formats the content: |
|
| 164 |
#' * concatenates if needed |
|
| 165 |
#' * styles if `style` is TRUE |
|
| 166 |
#' |
|
| 167 |
#' @keywords internal |
|
| 168 |
#' @inheritParams verbatim_popup |
|
| 169 |
#' @return `reactive` with the formatted content |
|
| 170 |
format_content <- function(verbatim_content, style = FALSE) {
|
|
| 171 | 11x |
shiny::reactive({
|
| 172 | 4x |
content <- if (inherits(verbatim_content, "reactive")) {
|
| 173 | 2x |
tryCatch( |
| 174 | 2x |
verbatim_content(), |
| 175 | 2x |
error = function(e) {
|
| 176 | ! |
e |
| 177 |
} |
|
| 178 |
) |
|
| 179 |
} else {
|
|
| 180 | 2x |
verbatim_content |
| 181 |
} |
|
| 182 | 4x |
shiny::validate(shiny::need( |
| 183 | 4x |
checkmate::test_multi_class(content, classes = c("expression", "character", "condition")),
|
| 184 | 4x |
"verbatim_content should be an expression, character or condition" |
| 185 |
)) |
|
| 186 | ||
| 187 | 4x |
content <- paste(as.character(content), collapse = "\n") |
| 188 | ||
| 189 | 4x |
if (style && !checkmate::test_class(content, "condition")) {
|
| 190 | 3x |
content <- paste(styler::style_text(content), collapse = "\n") |
| 191 |
} |
|
| 192 | 4x |
content |
| 193 |
}) |
|
| 194 |
} |
| 1 |
#' This function checks the plot type and applies specific modifications |
|
| 2 |
#' to the plot object based on the provided parameters. |
|
| 3 |
#' |
|
| 4 |
#' @param plot_obj The original plot object. |
|
| 5 |
#' @param plot_type The type of the plot, either `gg` (`ggplot2`) or `grob` (`grid`, `graphics`). |
|
| 6 |
#' @param dblclicking A logical value indicating whether double-clicking on data points on |
|
| 7 |
#' the main plot is enabled or disabled. |
|
| 8 |
#' @param ranges A list containing x and y values of ranges. |
|
| 9 |
#' |
|
| 10 |
#' @keywords internal |
|
| 11 |
apply_plot_modifications <- function(plot_obj, plot_type, dblclicking, ranges) {
|
|
| 12 | 11x |
if (plot_type == "gg" && dblclicking) {
|
| 13 | 1x |
plot_obj + |
| 14 | 1x |
ggplot2::coord_cartesian(xlim = ranges$x, ylim = ranges$y, expand = FALSE) |
| 15 | 10x |
} else if (plot_type == "grob") {
|
| 16 | 2x |
grid::grid.newpage() |
| 17 | 2x |
grid::grid.draw(plot_obj) |
| 18 |
} else {
|
|
| 19 | 8x |
plot_obj |
| 20 |
} |
|
| 21 |
} |
| 1 |
#' @keywords internal |
|
| 2 |
#' @noRd |
|
| 3 |
standard_layout_deps <- function() {
|
|
| 4 | 1x |
htmltools::htmlDependency( |
| 5 | 1x |
name = "teal-widgets-standard-layout", |
| 6 | 1x |
version = utils::packageVersion("teal.widgets"),
|
| 7 | 1x |
package = "teal.widgets", |
| 8 | 1x |
src = "standard-layout", |
| 9 | 1x |
stylesheet = "standard-layout.css" |
| 10 |
) |
|
| 11 |
} |
|
| 12 | ||
| 13 |
#' Standard UI layout |
|
| 14 |
#' |
|
| 15 |
#' @description |
|
| 16 |
#' Create a standard UI layout with output on the right and an encoding panel on |
|
| 17 |
#' the left. This is the layout used by the `teal` modules. |
|
| 18 |
#' |
|
| 19 |
#' @param output (`shiny.tag`)\cr |
|
| 20 |
#' object with the output element (table, plot, listing) such as for example returned |
|
| 21 |
#' by [shiny::plotOutput()]. |
|
| 22 |
#' @param encoding (`shiny.tag`)\cr |
|
| 23 |
#' object containing the encoding elements. If this element is `NULL` then no encoding side |
|
| 24 |
#' panel on the right is created. |
|
| 25 |
#' @param forms (`tagList`)\cr |
|
| 26 |
#' for example [shiny::actionButton()] that are placed below the encodings panel |
|
| 27 |
#' @param pre_output (`shiny.tag`) optional,\cr |
|
| 28 |
#' with text placed before the output to put the output into context. For example a title. |
|
| 29 |
#' @param post_output (`shiny.tag`) optional, with text placed after the output to put the output |
|
| 30 |
#' into context. For example the [shiny::helpText()] elements are useful. |
|
| 31 |
#' |
|
| 32 |
#' @return an object of class `shiny.tag` with the UI code. |
|
| 33 |
#' |
|
| 34 |
#' @examples |
|
| 35 |
#' library(shiny) |
|
| 36 |
#' standard_layout( |
|
| 37 |
#' output = white_small_well(tags$h3("Tests")),
|
|
| 38 |
#' encoding = tags$div( |
|
| 39 |
#' tags$label("Encodings", class = "text-primary"),
|
|
| 40 |
#' panel_item( |
|
| 41 |
#' "Tests", |
|
| 42 |
#' optionalSelectInput( |
|
| 43 |
#' "tests", |
|
| 44 |
#' "Tests:", |
|
| 45 |
#' choices = c( |
|
| 46 |
#' "Shapiro-Wilk", |
|
| 47 |
#' "Kolmogorov-Smirnov (one-sample)" |
|
| 48 |
#' ), |
|
| 49 |
#' selected = "Shapiro-Wilk" |
|
| 50 |
#' ) |
|
| 51 |
#' ) |
|
| 52 |
#' ), |
|
| 53 |
#' forms = tagList( |
|
| 54 |
#' verbatim_popup_ui("warning", "Show Warnings"),
|
|
| 55 |
#' verbatim_popup_ui("rcode", "Show R code")
|
|
| 56 |
#' ) |
|
| 57 |
#' ) |
|
| 58 |
#' |
|
| 59 |
#' @export |
|
| 60 |
standard_layout <- function(output, |
|
| 61 |
encoding = NULL, |
|
| 62 |
forms = NULL, |
|
| 63 |
pre_output = NULL, |
|
| 64 |
post_output = NULL) {
|
|
| 65 |
# checking arguments |
|
| 66 | 6x |
checkmate::assert_multi_class(output, c("shiny.tag", "shiny.tag.list", "html"))
|
| 67 | 4x |
checkmate::assert_multi_class(encoding, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
|
| 68 | 3x |
checkmate::assert_multi_class(pre_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
|
| 69 | 2x |
checkmate::assert_multi_class(post_output, c("shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE)
|
| 70 | ||
| 71 |
# if encoding=NULL then forms is placed below output |
|
| 72 | ||
| 73 | 1x |
tag_output <- tags$div( |
| 74 | 1x |
class = "teal-widgets standard-layout", |
| 75 | 1x |
tags$div(class = "standard-layout-pre-output", pre_output), |
| 76 | 1x |
tags$div(class = "standard-layout-output", output), |
| 77 | 1x |
tags$div(class = "standard-layout-post-output", post_output) |
| 78 |
) |
|
| 79 | ||
| 80 | 1x |
tag_enc_out <- if (!is.null(encoding)) {
|
| 81 | ! |
tags$div( |
| 82 | ! |
class = "teal-widgets standard-layout has-encodings", |
| 83 | ! |
bslib::layout_sidebar( |
| 84 | ! |
sidebar = bslib::sidebar( |
| 85 | ! |
tags$div( |
| 86 | ! |
encoding, |
| 87 | ! |
if (is.null(forms)) {
|
| 88 | ! |
NULL |
| 89 |
} else {
|
|
| 90 | ! |
tags$div( |
| 91 | ! |
tags$br(), |
| 92 | ! |
forms |
| 93 |
) |
|
| 94 |
} |
|
| 95 |
) |
|
| 96 |
), |
|
| 97 | ! |
tag_output |
| 98 |
) |
|
| 99 |
) |
|
| 100 |
} else {
|
|
| 101 | 1x |
tags$div( |
| 102 | 1x |
tag_output, |
| 103 | 1x |
if (is.null(forms)) {
|
| 104 | 1x |
NULL |
| 105 |
} else {
|
|
| 106 | ! |
tags$div(class = "form-group", forms) |
| 107 |
} |
|
| 108 |
) |
|
| 109 |
} |
|
| 110 | ||
| 111 | 1x |
bslib::page_fluid( |
| 112 | 1x |
class = "teal-widgets standard-layout-wrapper", |
| 113 | 1x |
standard_layout_deps(), |
| 114 | 1x |
tag_enc_out |
| 115 |
) |
|
| 116 |
} |
| 1 |
#' Nested Closeable Modal Popup |
|
| 2 |
#' |
|
| 3 |
#' @description `r lifecycle::badge("deprecated")`
|
|
| 4 |
#' Alternative to `shiny::modalDialog`. Create a nested modal popup that can be shown/hidden |
|
| 5 |
#' using `jQuery` and modal `id`, without disturbing the parent modal. |
|
| 6 |
#' |
|
| 7 |
#' @param id (`character(1)`) `shiny` module id for the component.\cr |
|
| 8 |
#' Note that this id can be used to show/hide this modal |
|
| 9 |
#' with the appended `jQuery` methods show/hide. |
|
| 10 |
#' @param ... (`shiny.tag`) `shiny` UI elements that will be displayed in the modal UI |
|
| 11 |
#' @param modal_args (`list`) optional list of arguments for the `shiny::modalDialog` function |
|
| 12 |
#' to customize the modal. Has `easyClose` set to `TRUE` as default |
|
| 13 |
#' |
|
| 14 |
#' @return (`shiny.tag`) returns `HTML` for `shiny` module UI which can be nested into a modal popup |
|
| 15 |
#' @export |
|
| 16 |
#' |
|
| 17 |
#' @examples |
|
| 18 |
#' library(shiny) |
|
| 19 |
#' library(shinyjs) |
|
| 20 |
#' |
|
| 21 |
#' ui <- bslib::page_fluid( |
|
| 22 |
#' useShinyjs(), |
|
| 23 |
#' actionButton("show_1", "$(\"#modal_1\").modal(\"show\")"),
|
|
| 24 |
#' nested_closeable_modal( |
|
| 25 |
#' "modal_1", |
|
| 26 |
#' modal_args = list( |
|
| 27 |
#' size = "l", |
|
| 28 |
#' title = "First Modal", |
|
| 29 |
#' easyClose = TRUE, |
|
| 30 |
#' footer = NULL |
|
| 31 |
#' ), |
|
| 32 |
#' tags$div( |
|
| 33 |
#' "This modal can be closed by running", tags$code("$(\"#modal_1\").modal(\"hide\")"),
|
|
| 34 |
#' "in the JS console!", |
|
| 35 |
#' tags$br(), |
|
| 36 |
#' "Note that the second modal is placed right within this modal", |
|
| 37 |
#' tags$br(), |
|
| 38 |
#' "Alternatively, calling the", tags$code("removeModal()"),
|
|
| 39 |
#' "will remove all the active modal popups", |
|
| 40 |
#' tags$br(), tags$br(), |
|
| 41 |
#' actionButton("show_2", "$(\"#modal_2\").modal(\"show\")"),
|
|
| 42 |
#' actionButton("hide_1", "$(\"#modal_1\").modal(\"hide\")"),
|
|
| 43 |
#' nested_closeable_modal( |
|
| 44 |
#' id = "modal_2", |
|
| 45 |
#' modal_args = list( |
|
| 46 |
#' size = "m", |
|
| 47 |
#' title = "Second Modal", |
|
| 48 |
#' footer = NULL, |
|
| 49 |
#' easyClose = TRUE |
|
| 50 |
#' ), |
|
| 51 |
#' tags$div( |
|
| 52 |
#' "This modal can be closed by running", tags$code("$(\"#modal_1\").modal(\"hide\")"),
|
|
| 53 |
#' "in the JS console!", |
|
| 54 |
#' "Note that removing the parent will remove the child. |
|
| 55 |
#' But, reopening will remember the open state of child", |
|
| 56 |
#' actionButton("hide_2", "$(\"#modal_2\").modal(\"hide\")"),
|
|
| 57 |
#' actionButton("hide_all", "$(\"#modal_1\").modal(\"hide\")")
|
|
| 58 |
#' ) |
|
| 59 |
#' ) |
|
| 60 |
#' ) |
|
| 61 |
#' ) |
|
| 62 |
#' ) |
|
| 63 |
#' |
|
| 64 |
#' server <- function(input, output) {
|
|
| 65 |
#' observeEvent(input$show_1, {
|
|
| 66 |
#' runjs("$(\"#modal_1\").modal(\"show\")")
|
|
| 67 |
#' }) |
|
| 68 |
#' observeEvent(input$show_2, {
|
|
| 69 |
#' runjs("$(\"#modal_2\").modal(\"show\")")
|
|
| 70 |
#' }) |
|
| 71 |
#' observeEvent(c(input$hide_1, input$hide_all), {
|
|
| 72 |
#' runjs("$(\"#modal_1\").modal(\"hide\")")
|
|
| 73 |
#' }) |
|
| 74 |
#' observeEvent(input$hide_2, {
|
|
| 75 |
#' runjs("$(\"#modal_2\").modal(\"hide\")")
|
|
| 76 |
#' }) |
|
| 77 |
#' } |
|
| 78 |
#' |
|
| 79 |
#' if (interactive()) {
|
|
| 80 |
#' shinyApp(ui, server) |
|
| 81 |
#' } |
|
| 82 |
nested_closeable_modal <- function(id, ..., modal_args = list(easyClose = TRUE)) {
|
|
| 83 | ! |
checkmate::assert_string(id) |
| 84 | ! |
checkmate::assert_list(modal_args) |
| 85 | ! |
lifecycle::deprecate_soft( |
| 86 | ! |
when = "0.5.0", |
| 87 | ! |
what = "nested_closeable_modal()" |
| 88 |
) |
|
| 89 | ||
| 90 | ! |
modal_args <- append(list(...), modal_args) |
| 91 | ! |
tagList( |
| 92 | ! |
htmltools::tagQuery(do.call(modalDialog, modal_args))$ |
| 93 | ! |
removeAttrs("id")$
|
| 94 | ! |
addAttrs(id = id, `aria-hidden` = "true", class = "custom-modal", `data-backdrop` = "false")$ |
| 95 | ! |
children("div")$
|
| 96 | ! |
children("div")$
|
| 97 | ! |
children("div")$
|
| 98 | ! |
siblings(".modal-footer")$
|
| 99 | ! |
find("button")$
|
| 100 | ! |
removeAttrs(c("data-dismiss", "data-bs-dismiss"))$
|
| 101 | ! |
addAttrs(onclick = paste0("$('#", id, "').modal('hide');"))$
|
| 102 | ! |
allTags() |
| 103 |
) |
|
| 104 |
} |
| 1 |
#' Builds a `basic_table_args` object |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' This function has to be used to build an input for a `basic_table_args` argument. |
|
| 5 |
#' The `basic_table_args` argument should be a part of every module which contains any `rtables` object. |
|
| 6 |
#' Arguments are validated to match their `rtables` equivalents. |
|
| 7 |
#' |
|
| 8 |
#' For more details see the vignette: `vignette("custom-basic-table-arguments", package = "teal.widgets")`.
|
|
| 9 |
#' |
|
| 10 |
#' @seealso |
|
| 11 |
#' * [resolve_basic_table_args()] to resolve multiple objects into one using pre-defined priorities. |
|
| 12 |
#' * [parse_basic_table_args()] to parse resolved list into list of calls. |
|
| 13 |
#' |
|
| 14 |
#' @param ... arguments compatible with [rtables::basic_table()]. |
|
| 15 |
#' |
|
| 16 |
#' @return (`basic_table_args`) object. |
|
| 17 |
#' @export |
|
| 18 |
#' @examples |
|
| 19 |
#' basic_table_args(subtitles = "SUBTITLE") |
|
| 20 |
basic_table_args <- function(...) {
|
|
| 21 | 118x |
table_args_l <- list(...) |
| 22 | 118x |
checkmate::assert_character(names(table_args_l), unique = TRUE, null.ok = TRUE) |
| 23 | ||
| 24 | 118x |
basic_table_formals <- methods::formalArgs(rtables::basic_table) |
| 25 | 118x |
checkmate::assert_subset(names(table_args_l), choices = basic_table_formals, empty.ok = TRUE) |
| 26 | ||
| 27 | 114x |
structure(table_args_l, class = "basic_table_args") |
| 28 |
} |
|
| 29 | ||
| 30 |
#' Resolves and reduces multiple `basic_table_args` objects |
|
| 31 |
#' |
|
| 32 |
#' @description |
|
| 33 |
#' Resolving and reducing multiple `basic_table_args` objects. |
|
| 34 |
#' This function is intended to utilize user provided settings, defaults provided by the module creator and |
|
| 35 |
#' also `teal` option. See `Details`, below, to understand the logic. |
|
| 36 |
#' |
|
| 37 |
#' @seealso [parse_basic_table_args()] to parse resolved list into list of calls. |
|
| 38 |
#' |
|
| 39 |
#' @param user_table (`basic_table_args`)\cr |
|
| 40 |
#' end user setup for [rtables::basic_table()] of a specific table. |
|
| 41 |
#' Created with the [basic_table_args()] function. The `NULL` value is supported. |
|
| 42 |
#' @param user_default (`basic_table_args`)\cr |
|
| 43 |
#' end user default setup for [rtables::basic_table()] |
|
| 44 |
#' of a specific table. Created with the [basic_table_args()] function. The `NULL` value is supported. |
|
| 45 |
#' @param module_table (`ggplot2_args`)\cr |
|
| 46 |
#' module creator setup for [rtables::basic_table()] of a specific table. |
|
| 47 |
#' Created with the [basic_table_args()] function. The `NULL` value is supported. |
|
| 48 |
#' @param app_default (`basic_table_args`)\cr |
|
| 49 |
#' Application level setting. Can be `NULL`. |
|
| 50 |
#' |
|
| 51 |
#' @return `basic_table_args` object. |
|
| 52 |
#' @details |
|
| 53 |
#' The function picks the first non `NULL` value for each argument, checking in the following order: |
|
| 54 |
#' 1. `basic_table_args` argument provided by the end user. |
|
| 55 |
#' Per table (`user_table`) and then default (`user_default`) setup. |
|
| 56 |
#' 2. `app_default` global R variable, `teal.basic_table_args`. |
|
| 57 |
#' 3. `module_table` which is a module creator setup. |
|
| 58 |
#' @export |
|
| 59 |
#' @examples |
|
| 60 |
#' resolve_basic_table_args( |
|
| 61 |
#' user_table = basic_table_args(title = "TITLE"), |
|
| 62 |
#' user_default = basic_table_args(title = "DEFAULT_TITLE", subtitles = "SUBTITLE") |
|
| 63 |
#' ) |
|
| 64 |
resolve_basic_table_args <- function(user_table = basic_table_args(), |
|
| 65 |
user_default = basic_table_args(), |
|
| 66 |
module_table = basic_table_args(), |
|
| 67 |
app_default = getOption("teal.basic_table_args", basic_table_args())) {
|
|
| 68 | 24x |
checkmate::assert_class(user_table, "basic_table_args", null.ok = TRUE) |
| 69 | 23x |
checkmate::assert_class(user_default, "basic_table_args", null.ok = TRUE) |
| 70 | 23x |
checkmate::assert_class(module_table, "basic_table_args", null.ok = TRUE) |
| 71 | 23x |
checkmate::assert_class(app_default, "basic_table_args", null.ok = TRUE) |
| 72 | ||
| 73 | 23x |
table_args_all <- list( |
| 74 | 23x |
"table" = user_table, |
| 75 | 23x |
"default" = user_default, |
| 76 | 23x |
"teal" = app_default, |
| 77 | 23x |
"module" = module_table |
| 78 |
) |
|
| 79 | ||
| 80 | 23x |
table_args_f <- Reduce(`c`, table_args_all) |
| 81 | ||
| 82 | 23x |
if (length(table_args_f) == 0) {
|
| 83 | 9x |
basic_table_args() |
| 84 |
} else {
|
|
| 85 | 14x |
do.call(basic_table_args, table_args_f[!duplicated(names(table_args_f))]) |
| 86 |
} |
|
| 87 |
} |
|
| 88 | ||
| 89 |
#' Parses `basic_table_args` object into the `basic_table` expression |
|
| 90 |
#' |
|
| 91 |
#' @description |
|
| 92 |
#' A function to parse expression from the `basic_table_args` object. |
|
| 93 |
#' @param basic_table_args (`basic_table_args`)\cr |
|
| 94 |
#' This argument could be a result of the [`resolve_basic_table_args()`]. |
|
| 95 |
#' |
|
| 96 |
#' @return (`language`) the `rtables::basic_table()` filled with additional arguments. |
|
| 97 |
#' @export |
|
| 98 |
#' @examples |
|
| 99 |
#' parse_basic_table_args( |
|
| 100 |
#' resolve_basic_table_args( |
|
| 101 |
#' user_table = basic_table_args(title = "TITLE"), |
|
| 102 |
#' user_default = basic_table_args(title = "DEFAULT_TITLE", subtitles = "SUBTITLE") |
|
| 103 |
#' ) |
|
| 104 |
#' ) |
|
| 105 |
parse_basic_table_args <- function(basic_table_args = teal.widgets::basic_table_args()) {
|
|
| 106 | 8x |
checkmate::assert_class(basic_table_args, "basic_table_args", null.ok = TRUE) |
| 107 | ||
| 108 | 7x |
if (length(basic_table_args) == 0) {
|
| 109 | 3x |
quote(rtables::basic_table()) |
| 110 |
} else {
|
|
| 111 | 4x |
as.call(c(list(quote(rtables::basic_table)), basic_table_args)) |
| 112 |
} |
|
| 113 |
} |
| 1 |
#' Map `lenghtMenu` property |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' Maps the `lengthMenu` selected value property of `DT::datatable` to a `shiny` variable. |
|
| 5 |
#' @param dt_name `ns()` of `inputId` of the `DT::datatable` |
|
| 6 |
#' @param dt_rows `ns()` of `inputId` of the variable that holds the current selected value of `lengthMenu` |
|
| 7 |
#' |
|
| 8 |
#' @name get_dt_rows |
|
| 9 |
#' |
|
| 10 |
#' @return (`shiny::tagList`) A `shiny tagList`. |
|
| 11 |
#' |
|
| 12 |
#' @examplesIf require("DT")
|
|
| 13 |
#' library(shiny) |
|
| 14 |
#' library(DT) |
|
| 15 |
#' |
|
| 16 |
#' ui <- function(id) {
|
|
| 17 |
#' ns <- NS(id) |
|
| 18 |
#' tagList( |
|
| 19 |
#' get_dt_rows(ns("data_table"), ns("dt_rows")),
|
|
| 20 |
#' textOutput(ns("rows")),
|
|
| 21 |
#' DT::DTOutput(ns("data_table"))
|
|
| 22 |
#' ) |
|
| 23 |
#' } |
|
| 24 |
#' |
|
| 25 |
#' # use the input$dt_rows in the Shiny Server function |
|
| 26 |
#' server <- function(id) {
|
|
| 27 |
#' moduleServer(id, function(input, output, session) {
|
|
| 28 |
#' output$data_table <- DT::renderDataTable(iris) |
|
| 29 |
#' # Change rows selected to see the first line on the UI change |
|
| 30 |
#' rows <- reactive({
|
|
| 31 |
#' paste0("Selected Rows ", input$dt_rows)
|
|
| 32 |
#' }) |
|
| 33 |
#' output$rows <- renderText(rows()) |
|
| 34 |
#' }) |
|
| 35 |
#' } |
|
| 36 |
#' if (interactive()) {
|
|
| 37 |
#' shinyApp( |
|
| 38 |
#' ui = ui("my_table_module"),
|
|
| 39 |
#' server = function(input, output, session) server("my_table_module")
|
|
| 40 |
#' ) |
|
| 41 |
#' } |
|
| 42 |
#' @export |
|
| 43 |
get_dt_rows <- function(dt_name, dt_rows) {
|
|
| 44 | ! |
tags$head( |
| 45 | ! |
tags$script( |
| 46 | ! |
sprintf( |
| 47 | ! |
"$(document).ready(function() {
|
| 48 | ! |
$('%s').on('length.dt', function(e, settings, len) {
|
| 49 | ! |
Shiny.setInputValue('%s', len);
|
| 50 |
}); |
|
| 51 |
});", |
|
| 52 | ! |
paste0("#", dt_name),
|
| 53 | ! |
dt_rows |
| 54 |
) |
|
| 55 |
) |
|
| 56 |
) |
|
| 57 |
} |
| 1 |
#' Small well class for HTML |
|
| 2 |
#' |
|
| 3 |
#' @description |
|
| 4 |
#' Adds Small Well class and overflow-x property to HTML output element. |
|
| 5 |
#' @param ... other arguments to pass to tag object's div attributes. |
|
| 6 |
#' |
|
| 7 |
#' @details `white_small_well` is intended to be used with [shiny::uiOutput()]. |
|
| 8 |
#' The overflow-x property is set to auto so that a scroll bar is added |
|
| 9 |
#' when the content overflows at the left and right edges of the output window. |
|
| 10 |
#' For example, this is useful for displaying wide tables. |
|
| 11 |
#' |
|
| 12 |
#' @return An HTML output element with class Small Well and overflow-x property |
|
| 13 |
#' @export |
|
| 14 |
#' |
|
| 15 |
#' @examples |
|
| 16 |
#' |
|
| 17 |
#' white_small_well(shiny::htmlOutput("summary"))
|
|
| 18 |
white_small_well <- function(...) {
|
|
| 19 | ! |
shiny::tagList( |
| 20 | ! |
tags$div( |
| 21 | ! |
class = "well well-sm", |
| 22 | ! |
style = "background-color: white;", |
| 23 |
... |
|
| 24 |
) |
|
| 25 |
) |
|
| 26 |
} |