1 |
#' Creates `ggplot2_args` object
|
|
2 |
#'
|
|
3 |
#' @description `r lifecycle::badge("experimental")`
|
|
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 `r lifecycle::badge("experimental")`
|
|
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 `r lifecycle::badge("experimental")`
|
|
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 |
#' Wrapper for `pickerInput`
|
|
2 |
#'
|
|
3 |
#' @description `r lifecycle::badge("stable")`
|
|
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 |
#' # Create a minimal example data frame
|
|
38 |
#' data <- data.frame(
|
|
39 |
#' AGE = c(25, 30, 40, 35, 28),
|
|
40 |
#' SEX = c("Male", "Female", "Male", "Female", "Male"),
|
|
41 |
#' PARAMCD = c("Val1", "Val2", "Val3", "Val4", "Val5"),
|
|
42 |
#' PARAM = c("Param1", "Param2", "Param3", "Param4", "Param5"),
|
|
43 |
#' AVISIT = c("Visit1", "Visit2", "Visit3", "Visit4", "Visit5"),
|
|
44 |
#' stringsAsFactors = TRUE
|
|
45 |
#' )
|
|
46 |
#'
|
|
47 |
#' ui_grid <- function(...) {
|
|
48 |
#' bslib::page_fluid(
|
|
49 |
#' bslib::layout_columns(
|
|
50 |
#' col_widths = c(4, 4, 4),
|
|
51 |
#' ...
|
|
52 |
#' )
|
|
53 |
#' )
|
|
54 |
#' }
|
|
55 |
#'
|
|
56 |
#' ui <- ui_grid(
|
|
57 |
#' wellPanel(
|
|
58 |
#' optionalSelectInput(
|
|
59 |
#' inputId = "c1",
|
|
60 |
#' label = "Fixed choices",
|
|
61 |
#' choices = LETTERS[1:5],
|
|
62 |
#' selected = c("A", "B"),
|
|
63 |
#' fixed = TRUE
|
|
64 |
#' ),
|
|
65 |
#' verbatimTextOutput(outputId = "c1_out")
|
|
66 |
#' ),
|
|
67 |
#' wellPanel(
|
|
68 |
#' optionalSelectInput(
|
|
69 |
#' inputId = "c2",
|
|
70 |
#' label = "Single choice",
|
|
71 |
#' choices = "A",
|
|
72 |
#' selected = "A"
|
|
73 |
#' ),
|
|
74 |
#' verbatimTextOutput(outputId = "c2_out")
|
|
75 |
#' ),
|
|
76 |
#' wellPanel(
|
|
77 |
#' optionalSelectInput(
|
|
78 |
#' inputId = "c3",
|
|
79 |
#' label = "NULL choices",
|
|
80 |
#' choices = NULL
|
|
81 |
#' ),
|
|
82 |
#' verbatimTextOutput(outputId = "c3_out")
|
|
83 |
#' ),
|
|
84 |
#' wellPanel(
|
|
85 |
#' optionalSelectInput(
|
|
86 |
#' inputId = "c4",
|
|
87 |
#' label = "Default",
|
|
88 |
#' choices = LETTERS[1:5],
|
|
89 |
#' selected = "A"
|
|
90 |
#' ),
|
|
91 |
#' verbatimTextOutput(outputId = "c4_out")
|
|
92 |
#' ),
|
|
93 |
#' wellPanel(
|
|
94 |
#' optionalSelectInput(
|
|
95 |
#' inputId = "c5",
|
|
96 |
#' label = "Named vector",
|
|
97 |
#' choices = c(`A - value A` = "A", `B - value B` = "B", `C - value C` = "C"),
|
|
98 |
#' selected = "A"
|
|
99 |
#' ),
|
|
100 |
#' verbatimTextOutput(outputId = "c5_out")
|
|
101 |
#' ),
|
|
102 |
#' wellPanel(
|
|
103 |
#' selectInput(
|
|
104 |
#' inputId = "c6_choices", label = "Update choices", choices = letters, multiple = TRUE
|
|
105 |
#' ),
|
|
106 |
#' optionalSelectInput(
|
|
107 |
#' inputId = "c6",
|
|
108 |
#' label = "Updated choices",
|
|
109 |
#' choices = NULL,
|
|
110 |
#' multiple = TRUE,
|
|
111 |
#' fixed_on_single = TRUE
|
|
112 |
#' ),
|
|
113 |
#' verbatimTextOutput(outputId = "c6_out")
|
|
114 |
#' )
|
|
115 |
#' )
|
|
116 |
#'
|
|
117 |
#' server <- function(input, output, session) {
|
|
118 |
#' observeEvent(input$c6_choices, ignoreNULL = FALSE, {
|
|
119 |
#' updateOptionalSelectInput(
|
|
120 |
#' session = session,
|
|
121 |
#' inputId = "c6",
|
|
122 |
#' choices = input$c6_choices,
|
|
123 |
#' selected = input$c6_choices
|
|
124 |
#' )
|
|
125 |
#' })
|
|
126 |
#'
|
|
127 |
#' output$c1_out <- renderPrint(input$c1)
|
|
128 |
#' output$c2_out <- renderPrint(input$c2)
|
|
129 |
#' output$c3_out <- renderPrint(input$c3)
|
|
130 |
#' output$c4_out <- renderPrint(input$c4)
|
|
131 |
#' output$c5_out <- renderPrint(input$c5)
|
|
132 |
#' output$c6_out <- renderPrint(input$c6)
|
|
133 |
#' }
|
|
134 |
#'
|
|
135 |
#' if (interactive()) {
|
|
136 |
#' shinyApp(ui, server)
|
|
137 |
#' }
|
|
138 |
#'
|
|
139 |
optionalSelectInput <- function(inputId, # nolint |
|
140 |
label = NULL, |
|
141 |
choices = NULL, |
|
142 |
selected = NULL, |
|
143 |
multiple = FALSE, |
|
144 |
sep = NULL, |
|
145 |
options = list(), |
|
146 |
label_help = NULL, |
|
147 |
fixed = FALSE, |
|
148 |
fixed_on_single = FALSE, |
|
149 |
width = NULL) { |
|
150 | ! |
checkmate::assert_string(inputId) |
151 | ! |
checkmate::assert( |
152 | ! |
checkmate::check_string(label, null.ok = TRUE), |
153 | ! |
checkmate::check_class(label, "shiny.tag"), |
154 | ! |
checkmate::check_class(label, "shiny.tag.list"), |
155 | ! |
checkmate::check_class(label, "html") |
156 |
)
|
|
157 | ! |
stopifnot(is.null(choices) || length(choices) >= 1) |
158 | ! |
stopifnot( |
159 | ! |
is.null(selected) || |
160 | ! |
length(selected) == 0 || |
161 | ! |
all(selected %in% choices) || |
162 | ! |
all(selected %in% unlist(choices, recursive = FALSE)) |
163 |
)
|
|
164 | ! |
checkmate::assert_flag(multiple) |
165 | ! |
checkmate::assert_string(sep, null.ok = TRUE) |
166 | ! |
checkmate::assert_list(options) |
167 | ! |
checkmate::assert( |
168 | ! |
checkmate::check_string(label_help, null.ok = TRUE), |
169 | ! |
checkmate::check_class(label_help, "shiny.tag"), |
170 | ! |
checkmate::check_class(label_help, "shiny.tag.list"), |
171 | ! |
checkmate::check_class(label_help, "html") |
172 |
)
|
|
173 | ! |
checkmate::assert_flag(fixed) |
174 | ! |
checkmate::assert_flag(fixed_on_single) |
175 | ||
176 | ! |
if (!is.null(width)) { |
177 | ! |
validateCssUnit(width) |
178 |
}
|
|
179 | ||
180 | ! |
default_options <- list( |
181 | ! |
"actions-box" = multiple, |
182 | ! |
"none-selected-text" = "- Nothing selected -", |
183 | ! |
"max-options" = ifelse(multiple, Inf, 1), |
184 | ! |
"show-subtext" = TRUE, |
185 | ! |
"live-search" = ifelse(length(choices) > 10, TRUE, FALSE) |
186 |
)
|
|
187 | ||
188 | ! |
options <- if (!identical(options, list())) { |
189 | ! |
c(options, default_options[setdiff(names(default_options), names(options))]) |
190 |
} else { |
|
191 | ! |
default_options
|
192 |
}
|
|
193 | ||
194 | ! |
if (is.null(choices)) { |
195 | ! |
choices <- "" |
196 | ! |
selected <- NULL |
197 |
}
|
|
198 | ||
199 | ! |
if (length(choices) <= 1 && fixed_on_single) fixed <- TRUE |
200 | ||
201 | ! |
raw_choices <- extract_raw_choices(choices, attr(choices, "sep")) |
202 | ! |
raw_selected <- extract_raw_choices(selected, attr(choices, "sep")) |
203 | ||
204 |
# Making sure the default dropdown popup can be displayed in the whole body, even outside the sidebars.
|
|
205 | ! |
if (is.null(options$container)) { |
206 | ! |
options$container <- "body" |
207 |
}
|
|
208 | ||
209 | ! |
ui_picker <- tags$div( |
210 | ! |
id = paste0(inputId, "_input"), |
211 |
# visibility feature marked with display: none/block instead of shinyjs::hide/show
|
|
212 |
# as mechanism to hide/show is handled by javascript code
|
|
213 | ! |
style = if (fixed) "display: none;" else "display: block;", |
214 | ! |
shinyWidgets::pickerInput( |
215 | ! |
inputId = inputId, |
216 | ! |
label = label, |
217 | ! |
choices = raw_choices, |
218 | ! |
selected = raw_selected, |
219 | ! |
multiple = TRUE, |
220 | ! |
width = width, |
221 | ! |
options = options, |
222 | ! |
choicesOpt = picker_options(choices) |
223 |
)
|
|
224 |
)
|
|
225 | ||
226 | ! |
if (!is.null(label_help)) { |
227 | ! |
ui_picker[[3]] <- append(ui_picker[[3]], list(tags$div(class = "label-help", label_help)), after = 1) |
228 |
}
|
|
229 | ||
230 | ! |
ui_fixed <- tags$div( |
231 | ! |
id = paste0(inputId, "_fixed"), |
232 |
# visibility feature marked with display: none/block instead of shinyjs::hide/show
|
|
233 |
# as mechanism to hide/show is handled by javascript code
|
|
234 | ! |
style = if (fixed) "display: block;" else "display: none;", |
235 | ! |
tags$label(class = "control-label", label), |
236 |
# selected values as verbatim text
|
|
237 | ! |
tags$code( |
238 | ! |
id = paste0(inputId, "_selected_text"), |
239 | ! |
if (length(selected) > 0) { |
240 | ! |
toString(selected) |
241 |
} else { |
|
242 | ! |
"NULL"
|
243 |
}
|
|
244 |
),
|
|
245 | ! |
label_help
|
246 |
)
|
|
247 | ||
248 | ! |
tags$div( |
249 |
# when selected values in ui_picker change
|
|
250 |
# then update ui_fixed - specifically, update '{id}_selected_text' element
|
|
251 | ! |
tags$script( |
252 | ! |
sprintf( |
253 |
" |
|
254 | ! |
$(function() { |
255 | ! |
$('#%1$s').on('change', function(e) { |
256 | ! |
var select_concat = $(this).val().length ? $(this).val().join(', ') : 'NULL'; |
257 | ! |
$('#%1$s_selected_text').html(select_concat); |
258 |
}) |
|
259 |
})", |
|
260 | ! |
inputId
|
261 |
)
|
|
262 |
),
|
|
263 | ||
264 |
# if ui_picker has only one or less option or is fixed then hide {id}_input and show {id}_fixed
|
|
265 | ! |
if (fixed_on_single) { |
266 | ! |
js <- sprintf( |
267 | ! |
"$(function() { |
268 | ! |
$('#%1$s').on('change', function(e) { |
269 | ! |
var options = $('#%1$s').find('option'); |
270 | ! |
if (options.length == 1) { |
271 | ! |
$('#%1$s_input').hide(); |
272 | ! |
$('#%1$s_fixed').show(); |
273 |
} else { |
|
274 | ! |
$('#%1$s_input').show(); |
275 | ! |
$('#%1$s_fixed').hide(); |
276 |
}
|
|
277 |
}) |
|
278 |
})", |
|
279 | ! |
inputId
|
280 |
)
|
|
281 | ! |
tags$script(js) |
282 |
},
|
|
283 | ! |
tags$div(ui_picker, ui_fixed) |
284 |
)
|
|
285 |
}
|
|
286 | ||
287 |
#' @rdname optionalSelectInput
|
|
288 |
#' @param session (`shiny.session`)\cr
|
|
289 |
#' @export
|
|
290 |
updateOptionalSelectInput <- function(session, # nolint |
|
291 |
inputId, # nolint |
|
292 |
label = NULL, |
|
293 |
selected = NULL, |
|
294 |
choices = NULL) { |
|
295 | ! |
raw_choices <- extract_raw_choices(choices, attr(choices, "sep")) |
296 | ! |
raw_selected <- extract_raw_choices(selected, attr(choices, "sep")) |
297 | ||
298 |
# update picker input
|
|
299 | ! |
shinyWidgets::updatePickerInput( |
300 | ! |
session = session, |
301 | ! |
inputId = inputId, |
302 | ! |
label = label, |
303 | ! |
selected = as.character(raw_selected), |
304 | ! |
choices = raw_choices, |
305 | ! |
choicesOpt = picker_options(choices) |
306 |
)
|
|
307 | ||
308 | ! |
invisible(NULL) |
309 |
}
|
|
310 | ||
311 |
#' Get icons to represent variable types in dataset
|
|
312 |
#'
|
|
313 |
#' @param var_type (`character`)\cr
|
|
314 |
#' of R internal types (classes).
|
|
315 |
#'
|
|
316 |
#' @return (`character`)\cr
|
|
317 |
#' vector of HTML icons corresponding to data type in each column.
|
|
318 |
#' @keywords internal
|
|
319 |
#'
|
|
320 |
variable_type_icons <- function(var_type) { |
|
321 | ! |
checkmate::assert_character(var_type, any.missing = FALSE) |
322 | ||
323 | ! |
class_to_icon <- list( |
324 | ! |
numeric = "arrow-up-1-9", |
325 | ! |
integer = "arrow-up-1-9", |
326 | ! |
logical = "pause", |
327 | ! |
Date = "calendar", |
328 | ! |
POSIXct = "calendar", |
329 | ! |
POSIXlt = "calendar", |
330 | ! |
factor = "chart-bar", |
331 | ! |
character = "keyboard", |
332 | ! |
primary_key = "key", |
333 | ! |
unknown = "circle-question" |
334 |
)
|
|
335 | ! |
class_to_icon <- lapply(class_to_icon, function(icon_name) toString(icon(icon_name, lib = "font-awesome"))) |
336 | ||
337 | ! |
res <- unname(vapply( |
338 | ! |
var_type,
|
339 | ! |
FUN.VALUE = character(1), |
340 | ! |
FUN = function(class) { |
341 | ! |
if (class == "") { |
342 | ! |
class
|
343 | ! |
} else if (is.null(class_to_icon[[class]])) { |
344 | ! |
class_to_icon[["unknown"]] |
345 |
} else { |
|
346 | ! |
class_to_icon[[class]] |
347 |
}
|
|
348 |
}
|
|
349 |
)) |
|
350 | ||
351 | ! |
res
|
352 |
}
|
|
353 | ||
354 |
#' Optional content for `optionalSelectInput`
|
|
355 |
#'
|
|
356 |
#' Prepares content to be displayed in `optionalSelectInput` with icons and labels
|
|
357 |
#'
|
|
358 |
#' @param var_name (`character`)\cr
|
|
359 |
#' variable name
|
|
360 |
#' @param var_label (`character`)\cr
|
|
361 |
#' variable alternative name - for example variable label
|
|
362 |
#' @param var_type (`character`)
|
|
363 |
#' class of the variable.
|
|
364 |
#'
|
|
365 |
#' @return (`character`) HTML contents with all elements combined
|
|
366 |
#' @keywords internal
|
|
367 |
#'
|
|
368 |
picker_options_content <- function(var_name, var_label, var_type) { |
|
369 | ! |
if (length(var_name) == 0) { |
370 | ! |
res <- character(0) |
371 | ! |
} else if (length(var_type) == 0 && length(var_label) == 0) { |
372 | ! |
res <- var_name |
373 |
} else { |
|
374 | ! |
checkmate::assert_character(var_name, min.len = 1, any.missing = FALSE) |
375 | ! |
stopifnot( |
376 | ! |
identical(var_type, character(0)) || length(var_type) == length(var_name), |
377 | ! |
identical(var_label, character(0)) || length(var_label) == length(var_name) |
378 |
)
|
|
379 | ||
380 | ! |
var_icon <- variable_type_icons(var_type) |
381 | ||
382 | ! |
res <- trimws(paste( |
383 | ! |
var_icon,
|
384 | ! |
var_name,
|
385 | ! |
vapply( |
386 | ! |
var_label,
|
387 | ! |
function(x) { |
388 | ! |
ifelse(x == "", "", toString(tags$small(x, class = "text-muted"))) |
389 |
},
|
|
390 | ! |
character(1) |
391 |
)
|
|
392 |
)) |
|
393 |
}
|
|
394 | ||
395 | ! |
res
|
396 |
}
|
|
397 | ||
398 |
#' Create `choicesOpt` for `pickerInput`
|
|
399 |
#'
|
|
400 |
#' @param choices (`choices_labeled` or `character`)\cr
|
|
401 |
#' choices vector
|
|
402 |
#'
|
|
403 |
#' @return (`list`)\cr
|
|
404 |
#' to be passed as `choicesOpt` argument.
|
|
405 |
#' @keywords internal
|
|
406 |
picker_options <- function(choices) { |
|
407 | ! |
if (inherits(choices, "choices_labeled")) { |
408 | ! |
raw_choices <- extract_raw_choices(choices, sep = attr(choices, "sep")) |
409 | ! |
res <- list( |
410 | ! |
content = picker_options_content( |
411 | ! |
var_name = raw_choices, |
412 | ! |
var_label = extract_choices_labels(choices), |
413 | ! |
var_type = if (is.null(attr(choices, "types"))) character(0) else attr(choices, "types") |
414 |
)
|
|
415 |
)
|
|
416 | ! |
} else if (all(vapply(choices, inherits, logical(1), "choices_labeled"))) { |
417 | ! |
choices <- unlist(unname(choices)) |
418 | ! |
res <- list(content = picker_options_content( |
419 | ! |
var_name = choices, |
420 | ! |
var_label = extract_choices_labels(choices), |
421 | ! |
var_type = if (is.null(attr(choices, "types"))) character(0) else attr(choices, "types") |
422 |
)) |
|
423 |
} else { |
|
424 | ! |
res <- NULL |
425 |
}
|
|
426 | ! |
res
|
427 |
}
|
|
428 | ||
429 |
#' Extract raw values from choices
|
|
430 |
#'
|
|
431 |
#' @param choices (`choices_labeled`, `list` or `character`)\cr
|
|
432 |
#' object containing choices
|
|
433 |
#' @param sep (`character(1)`)\cr
|
|
434 |
#' A separator string to split the `choices` or `selected` inputs into the values of
|
|
435 |
#' the different columns.
|
|
436 |
#' @return choices simplified
|
|
437 |
#' @keywords internal
|
|
438 |
extract_raw_choices <- function(choices, sep) { |
|
439 | ! |
if (!is.null(sep)) { |
440 | ! |
vapply(choices, paste, collapse = sep, character(1)) |
441 | ! |
} else if (inherits(choices, "choices_labeled")) { |
442 | ! |
unname(unlist(choices)) |
443 |
} else { |
|
444 | ! |
choices
|
445 |
}
|
|
446 |
}
|
|
447 | ||
448 |
#' if min or max are `NA` then the slider widget will be hidden
|
|
449 |
#'
|
|
450 |
#' @description `r lifecycle::badge("stable")`\cr
|
|
451 |
#' Hidden input widgets are useful to have the `input[[inputId]]` variable
|
|
452 |
#' on available in the server function but no corresponding visual clutter from
|
|
453 |
#' input widgets that provide only a single choice.
|
|
454 |
#'
|
|
455 |
#' @inheritParams shiny::sliderInput
|
|
456 |
#' @param label_help (`shiny.tag`) optional\cr
|
|
457 |
#' object of class `shiny.tag`, e.g. an object returned by [shiny::helpText()]
|
|
458 |
#' @param ... optional arguments to `sliderInput`
|
|
459 |
#'
|
|
460 |
#' @return (`shiny.tag`) HTML tag with `sliderInput` widget.
|
|
461 |
#'
|
|
462 |
#' @export
|
|
463 |
#'
|
|
464 |
#' @examples
|
|
465 |
#' ui <- bslib::page_fluid(
|
|
466 |
#' shinyjs::useShinyjs(),
|
|
467 |
#' optionalSliderInput("s", "shown", 0, 1, 0.2),
|
|
468 |
#' optionalSliderInput("h", "hidden", 0, NA, 1),
|
|
469 |
#' )
|
|
470 |
#' if (interactive()) {
|
|
471 |
#' shiny::shinyApp(ui, function(input, output) {})
|
|
472 |
#' }
|
|
473 |
optionalSliderInput <- function(inputId, label, min, max, value, label_help = NULL, ...) { # nolint |
|
474 | 25x |
checkmate::assert_number(min, na.ok = TRUE) |
475 | 25x |
checkmate::assert_number(max, na.ok = TRUE) |
476 | 25x |
checkmate::assert_numeric(value, min.len = 1, max.len = 2, any.missing = FALSE) |
477 | ||
478 | 25x |
is_na_min <- is.na(min) |
479 | 25x |
is_na_max <- is.na(max) |
480 | ||
481 | 25x |
hide <- is_na_min || is_na_max |
482 | ||
483 | 25x |
if (length(value) == 2) { |
484 | 2x |
value1 <- value[1] |
485 | 2x |
value2 <- value[2] |
486 |
} else { |
|
487 | 23x |
value1 <- value |
488 | 23x |
value2 <- value |
489 |
}
|
|
490 | ||
491 | 25x |
if (is_na_min) { |
492 | 2x |
min <- value1 - 1 |
493 |
}
|
|
494 | 25x |
if (is_na_max) { |
495 | 1x |
max <- value2 + 1 |
496 |
}
|
|
497 | ||
498 | 25x |
if (min > value1 || max < value2) { |
499 | 2x |
stop("arguments inconsistent: min <= value <= max expected") |
500 |
}
|
|
501 | ||
502 | 23x |
slider <- sliderInput(inputId, label, min, max, value, ...) |
503 | ||
504 | 23x |
if (!is.null(label_help)) { |
505 | ! |
slider[[3]] <- append(slider[[3]], list(tags$div(class = "label-help", label_help)), after = 1) |
506 |
}
|
|
507 | ||
508 | 23x |
if (hide) { |
509 | 2x |
shinyjs::hidden(slider) |
510 |
} else { |
|
511 | 21x |
slider
|
512 |
}
|
|
513 |
}
|
|
514 | ||
515 |
#' For `teal` modules we parameterize an `optionalSliderInput` with one argument
|
|
516 |
#' `value_min_max`
|
|
517 |
#'
|
|
518 |
#' @description `r lifecycle::badge("stable")`
|
|
519 |
#' The [optionalSliderInput()] function needs three arguments to determine
|
|
520 |
#' whether to hide the `sliderInput` widget or not. For `teal` modules we specify an
|
|
521 |
#' optional slider input with one argument here called `value_min_max`.
|
|
522 |
#'
|
|
523 |
#' @inheritParams optionalSliderInput
|
|
524 |
#'
|
|
525 |
#' @param value_min_max (`numeric(1)` or `numeric(3)`)\cr
|
|
526 |
#' If of length 1 then the value gets set to that number and the `sliderInput` will be hidden.
|
|
527 |
#' Otherwise, if it is of length three the three elements will map to `value`, `min` and `max` of
|
|
528 |
#' the [optionalSliderInput()] function.
|
|
529 |
#'
|
|
530 |
#' @return (`shiny.tag`) HTML tag with range `sliderInput` widget.
|
|
531 |
#'
|
|
532 |
#' @export
|
|
533 |
#'
|
|
534 |
#' @examples
|
|
535 |
#'
|
|
536 |
#' ui <- bslib::page_fluid(
|
|
537 |
#' shinyjs::useShinyjs(),
|
|
538 |
#' optionalSliderInputValMinMax("a1", "b1", 1),
|
|
539 |
#' optionalSliderInputValMinMax("a2", "b2", c(3, 1, 5))
|
|
540 |
#' )
|
|
541 |
#' if (interactive()) {
|
|
542 |
#' shiny::shinyApp(ui, function(input, output) {})
|
|
543 |
#' }
|
|
544 |
optionalSliderInputValMinMax <- function(inputId, label, value_min_max, label_help = NULL, ...) { # nolint |
|
545 | 18x |
checkmate::assert( |
546 | 18x |
checkmate::check_numeric( |
547 | 18x |
value_min_max,
|
548 | 18x |
finite = TRUE, |
549 | 18x |
len = 3 |
550 |
),
|
|
551 | 18x |
checkmate::check_numeric( |
552 | 18x |
value_min_max,
|
553 | 18x |
finite = TRUE, |
554 | 18x |
len = 1 |
555 |
)
|
|
556 |
)
|
|
557 | ||
558 | 18x |
x <- value_min_max |
559 | ||
560 | 18x |
vals <- if (length(x) == 3) { |
561 | 18x |
checkmate::assert_number(x[1], lower = x[2], upper = x[3], .var.name = "value_min_max") |
562 | 18x |
list(value = x[1], min = x[2], max = x[3]) |
563 | 18x |
} else if (length(x) == 1) { |
564 | ! |
list(value = x, min = NA_real_, max = NA_real_) |
565 |
}
|
|
566 | ||
567 | 18x |
slider <- optionalSliderInput(inputId, label, vals$min, vals$max, vals$value, ...) |
568 | ||
569 | 18x |
if (!is.null(label_help)) { |
570 | ! |
slider[[3]] <- append(slider[[3]], list(tags$div(class = "label-help", label_help)), after = 1) |
571 |
}
|
|
572 | 18x |
slider
|
573 |
}
|
|
574 | ||
575 |
#' Extract labels from choices basing on attributes and names
|
|
576 |
#'
|
|
577 |
#' @param choices (`list` or `vector`)\cr
|
|
578 |
#' select choices
|
|
579 |
#' @param values optional\cr
|
|
580 |
#' choices subset for which labels should be extracted, `NULL` for all choices.
|
|
581 |
#'
|
|
582 |
#' @return (`character`) vector with labels
|
|
583 |
#' @keywords internal
|
|
584 |
extract_choices_labels <- function(choices, values = NULL) { |
|
585 | ! |
res <- if (inherits(choices, "choices_labeled")) { |
586 | ! |
attr(choices, "raw_labels") |
587 | ! |
} else if (!is.null(names(choices)) && !setequal(names(choices), unlist(unname(choices)))) { |
588 | ! |
names(choices) |
589 |
} else { |
|
590 | ! |
NULL
|
591 |
}
|
|
592 | ||
593 | ! |
if (!is.null(values) && !is.null(res)) { |
594 | ! |
stopifnot(all(values %in% choices)) |
595 | ! |
res <- res[vapply(values, function(val) which(val == choices), numeric(1))] |
596 |
}
|
|
597 | ||
598 | ! |
res
|
599 |
}
|
1 |
#' @keywords internal
|
|
2 |
#' @noRd
|
|
3 |
verbatim_popup_deps <- function() { |
|
4 | 4x |
htmltools::htmlDependency( |
5 | 4x |
name = "teal-widgets-verbatim-popup", |
6 | 4x |
version = utils::packageVersion("teal.widgets"), |
7 | 4x |
package = "teal.widgets", |
8 | 4x |
src = "verbatim-popup", |
9 | 4x |
script = "verbatim-popup.js" |
10 |
)
|
|
11 |
}
|
|
12 | ||
13 |
#' A `shiny` module that pops up verbatim text.
|
|
14 |
#' @name verbatim_popup
|
|
15 |
#' @description `r lifecycle::badge("experimental")`
|
|
16 |
#' This module consists of a button that once clicked pops up a
|
|
17 |
#' modal window with verbatim-styled text.
|
|
18 |
#'
|
|
19 |
#' @param id (`character(1)`) the `shiny` id
|
|
20 |
#' @param button_label (`character(1)`) the text printed on the button
|
|
21 |
#' @param type (`character(1)`) specifying whether to use `[shiny::actionButton()]` or `[shiny::actionLink()]`.
|
|
22 |
#' @param ... additional arguments to `[shiny::actionButton()]`(or `[shiny::actionLink()]`).
|
|
23 |
#'
|
|
24 |
#' @return the UI function returns a `shiny.tag.list` object
|
|
25 |
#' @export
|
|
26 |
#'
|
|
27 |
#' @examples
|
|
28 |
#' library(shiny)
|
|
29 |
#'
|
|
30 |
#' ui <- bslib::page_fluid(verbatim_popup_ui("my_id", button_label = "Open popup"))
|
|
31 |
#' srv <- function(input, output) {
|
|
32 |
#' verbatim_popup_srv(
|
|
33 |
#' "my_id",
|
|
34 |
#' "if (TRUE) { print('Popups are the best') }",
|
|
35 |
#' title = "My custom title",
|
|
36 |
#' style = TRUE
|
|
37 |
#' )
|
|
38 |
#' }
|
|
39 |
#' if (interactive()) shinyApp(ui, srv)
|
|
40 |
#'
|
|
41 |
verbatim_popup_ui <- function(id, button_label, type = c("button", "link"), ...) { |
|
42 | 5x |
checkmate::assert_string(id) |
43 | 5x |
checkmate::assert_string(button_label) |
44 | ||
45 | 5x |
ui_function <- switch(match.arg(type), |
46 | 5x |
"button" = shiny::actionButton, |
47 | 5x |
"link" = shiny::actionLink |
48 |
)
|
|
49 | ||
50 | 4x |
ns <- shiny::NS(id) |
51 | 4x |
ui_args <- list( |
52 | 4x |
inputId = ns("button"), |
53 | 4x |
label = button_label |
54 |
)
|
|
55 | ||
56 | 4x |
shiny::tagList( |
57 | 4x |
verbatim_popup_deps(), |
58 | 4x |
shinyjs::useShinyjs(), |
59 | 4x |
do.call(ui_function, c(ui_args, list(...))) |
60 |
)
|
|
61 |
}
|
|
62 | ||
63 |
#' @name verbatim_popup
|
|
64 |
#' @export
|
|
65 |
#'
|
|
66 |
#' @param verbatim_content (`character`, `expression`, `condition` or `reactive(1)`
|
|
67 |
#' holding any of the above) the content to show in the popup modal window
|
|
68 |
#' @param title (`character(1)`) the title of the modal window
|
|
69 |
#' @param style (`logical(1)`) whether to style the `verbatim_content` using `styler::style_text`.
|
|
70 |
#' If `verbatim_content` is a `condition` or `reactive` holding `condition` then this argument is ignored
|
|
71 |
#' @param disabled (`reactive(1)`) the `shiny` reactive value holding a `logical`. The popup button is disabled
|
|
72 |
#' when the flag is `TRUE` and enabled otherwise.
|
|
73 |
#'
|
|
74 |
verbatim_popup_srv <- function(id, verbatim_content, title, style = FALSE, disabled = shiny::reactiveVal(FALSE)) { |
|
75 | ! |
checkmate::assert_string(id) |
76 | ! |
checkmate::assert_string(title) |
77 | ! |
checkmate::assert_flag(style) |
78 | ! |
checkmate::assert_class(disabled, classes = "reactive") |
79 | ! |
moduleServer(id, function(input, output, session) { |
80 | ! |
ns <- session$ns |
81 | ! |
modal_content <- format_content(verbatim_content, style) |
82 | ! |
button_click_observer( |
83 | ! |
click_event = shiny::reactive(input$button), |
84 | ! |
copy_button_id = ns("copy_button"), |
85 | ! |
copied_area_id = ns("verbatim_content"), |
86 | ! |
modal_title = title, |
87 | ! |
modal_content = modal_content, |
88 | ! |
disabled = disabled |
89 |
)
|
|
90 |
}) |
|
91 |
}
|
|
92 | ||
93 |
#' Creates a `shiny` observer handling button clicks.
|
|
94 |
#'
|
|
95 |
#' @description
|
|
96 |
#' When the button is clicked it pop up a modal window with the text.
|
|
97 |
#'
|
|
98 |
#' @keywords internal
|
|
99 |
#' @param click_event `reactive` the click event
|
|
100 |
#' @param copy_button_id (`character(1)`) the id of the button to copy the modal content.
|
|
101 |
#' Automatically appended with a 1 and 2 suffix for top and bottom buttons respectively.
|
|
102 |
#' @param copied_area_id (`character(1)`) the id of the element which contents are copied
|
|
103 |
#' @param modal_title (`character(1)`) the title of the modal window
|
|
104 |
#' @param modal_content (`reactive`) the content of the modal window
|
|
105 |
#' @param disabled (`reactive(1)`) the `shiny` reactive value holding a `logical`. The popup button is disabled
|
|
106 |
#' when the flag is `TRUE` and enabled otherwise.
|
|
107 |
button_click_observer <- function(click_event, |
|
108 |
copy_button_id,
|
|
109 |
copied_area_id,
|
|
110 |
modal_title,
|
|
111 |
modal_content,
|
|
112 |
disabled) { |
|
113 | 1x |
shiny::observeEvent( |
114 | 1x |
disabled(), |
115 | 1x |
handlerExpr = { |
116 | ! |
if (disabled()) { |
117 | ! |
shinyjs::disable("button") |
118 |
} else { |
|
119 | ! |
shinyjs::enable("button") |
120 |
}
|
|
121 |
}
|
|
122 |
)
|
|
123 | ||
124 | 1x |
shiny::observeEvent( |
125 | 1x |
click_event(), |
126 | 1x |
handlerExpr = { |
127 | ! |
req(modal_content()) |
128 | ! |
shiny::showModal( |
129 | ! |
div( |
130 | ! |
class = "teal-widgets button-click-observer", |
131 | ! |
shiny::modalDialog( |
132 | ! |
tags$div( |
133 | ! |
tags$div( |
134 | ! |
shiny::actionButton( |
135 | ! |
paste0(copy_button_id, 1), |
136 | ! |
"Copy to Clipboard",
|
137 | ! |
onclick = paste0("copyToClipboard('", copied_area_id, "')") |
138 |
),
|
|
139 | ! |
shiny::modalButton("Dismiss") |
140 |
),
|
|
141 | ! |
tags$pre(id = copied_area_id, modal_content()), |
142 |
),
|
|
143 | ! |
title = modal_title, |
144 | ! |
footer = shiny::tagList( |
145 | ! |
shiny::actionButton( |
146 | ! |
paste0(copy_button_id, 2), |
147 | ! |
"Copy to Clipboard",
|
148 | ! |
onclick = paste0("copyToClipboard('", copied_area_id, "')") |
149 |
),
|
|
150 | ! |
shiny::modalButton("Dismiss") |
151 |
),
|
|
152 | ! |
size = "l", |
153 | ! |
easyClose = TRUE |
154 |
)
|
|
155 |
)
|
|
156 |
)
|
|
157 |
}
|
|
158 |
)
|
|
159 |
}
|
|
160 | ||
161 |
#' Formats the content of the modal popup window.
|
|
162 |
#'
|
|
163 |
#' @details
|
|
164 |
#' Formats the content:
|
|
165 |
#' * concatenates if needed
|
|
166 |
#' * styles if `style` is TRUE
|
|
167 |
#'
|
|
168 |
#' @keywords internal
|
|
169 |
#' @inheritParams verbatim_popup
|
|
170 |
#' @return `reactive` with the formatted content
|
|
171 |
format_content <- function(verbatim_content, style = FALSE) { |
|
172 | 11x |
shiny::reactive({ |
173 | 4x |
content <- if (inherits(verbatim_content, "reactive")) { |
174 | 2x |
tryCatch( |
175 | 2x |
verbatim_content(), |
176 | 2x |
error = function(e) { |
177 | ! |
e
|
178 |
}
|
|
179 |
)
|
|
180 |
} else { |
|
181 | 2x |
verbatim_content
|
182 |
}
|
|
183 | 4x |
shiny::validate(shiny::need( |
184 | 4x |
checkmate::test_multi_class(content, classes = c("expression", "character", "condition")), |
185 | 4x |
"verbatim_content should be an expression, character or condition"
|
186 |
)) |
|
187 | ||
188 | 4x |
content <- paste(as.character(content), collapse = "\n") |
189 | ||
190 | 4x |
if (style && !checkmate::test_class(content, "condition")) { |
191 | 3x |
content <- paste(styler::style_text(content), collapse = "\n") |
192 |
}
|
|
193 | 4x |
content
|
194 |
}) |
|
195 |
}
|
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 `r lifecycle::badge("stable")`\cr
|
|
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 |
tags$div( |
37 | 1x |
class = "teal-widgets settings-buttons", |
38 | 1x |
bslib::tooltip( |
39 | 1x |
trigger = tags$div(type_download_ui_table(ns("downbutton"))), |
40 | 1x |
options = list(trigger = "hover"), |
41 | 1x |
class = "download-button", |
42 | 1x |
"Download"
|
43 |
),
|
|
44 | 1x |
bslib::tooltip( |
45 | 1x |
trigger = tags$div( |
46 | 1x |
actionLink( |
47 | 1x |
ns("expand"), |
48 | 1x |
label = character(0), |
49 | 1x |
icon = icon("up-right-and-down-left-from-center"), |
50 | 1x |
class = "btn-sm", |
51 | 1x |
style = "color: #000;" |
52 |
)
|
|
53 |
),
|
|
54 | 1x |
options = list(trigger = "hover"), |
55 | 1x |
class = "expand-button", |
56 | 1x |
"Expand"
|
57 |
)
|
|
58 |
),
|
|
59 | 1x |
tags$div( |
60 | 1x |
class = "teal-widgets table-content", |
61 | 1x |
uiOutput(ns("table_out_main"), width = "100%", ...) |
62 |
)
|
|
63 |
)
|
|
64 |
)
|
|
65 |
}
|
|
66 | ||
67 |
#' @inheritParams shiny::moduleServer
|
|
68 |
#' @param table_r (`reactive`)\cr
|
|
69 |
#' reactive expression that yields an `rtable` object (`ElementaryTable` or `TableTree`)
|
|
70 |
#' @param show_hide_signal (`reactive logical`) optional\cr
|
|
71 |
#' mechanism to allow modules which call this module to show/hide the table_with_settings UI.
|
|
72 |
#'
|
|
73 |
#' @rdname table_with_settings
|
|
74 |
#'
|
|
75 |
#' @return A `shiny` module.
|
|
76 |
#'
|
|
77 |
#' @export
|
|
78 |
#'
|
|
79 |
#' @examples
|
|
80 |
#' library(shiny)
|
|
81 |
#' library(rtables)
|
|
82 |
#' library(magrittr)
|
|
83 |
#'
|
|
84 |
#' ui <- bslib::page_fluid(
|
|
85 |
#' table_with_settings_ui(
|
|
86 |
#' id = "table_with_settings"
|
|
87 |
#' )
|
|
88 |
#' )
|
|
89 |
#'
|
|
90 |
#' server <- function(input, output, session) {
|
|
91 |
#' table_r <- reactive({
|
|
92 |
#' l <- basic_table() %>%
|
|
93 |
#' split_cols_by("ARM") %>%
|
|
94 |
#' analyze(c("SEX", "AGE"))
|
|
95 |
#'
|
|
96 |
#' tbl <- build_table(l, DM)
|
|
97 |
#'
|
|
98 |
#' tbl
|
|
99 |
#' })
|
|
100 |
#'
|
|
101 |
#' table_with_settings_srv(id = "table_with_settings", table_r = table_r)
|
|
102 |
#' }
|
|
103 |
#'
|
|
104 |
#' if (interactive()) {
|
|
105 |
#' shinyApp(ui, server)
|
|
106 |
#' }
|
|
107 |
#'
|
|
108 |
table_with_settings_srv <- function(id, table_r, show_hide_signal = reactive(TRUE)) { |
|
109 | 5x |
checkmate::assert_class(table_r, c("reactive", "function")) |
110 | 4x |
checkmate::assert_class(show_hide_signal, c("reactive", "function")) |
111 | ||
112 | 3x |
if (!requireNamespace("rtables", quietly = TRUE)) { |
113 | ! |
stop("package rtables is required, please install") |
114 |
}
|
|
115 | ||
116 | 3x |
moduleServer(id, function(input, output, session) { |
117 | 3x |
ns <- session$ns |
118 |
# Turn on and off the UI
|
|
119 | 3x |
observeEvent(show_hide_signal(), { |
120 | 3x |
if (show_hide_signal()) { |
121 | 2x |
shinyjs::show("table-with-settings") |
122 |
} else { |
|
123 | 1x |
shinyjs::hide("table-with-settings") |
124 |
}
|
|
125 |
}) |
|
126 | ||
127 | 3x |
output$table_out_main <- output$table_out_modal <- renderUI({ |
128 | 6x |
rtables::as_html(table_r()) |
129 |
}) |
|
130 | ||
131 | 3x |
type_download_srv_table( |
132 | 3x |
id = "downbutton", |
133 | 3x |
table_reactive = table_r |
134 |
)
|
|
135 | ||
136 | 3x |
observeEvent(input$expand, { |
137 | 1x |
showModal( |
138 | 1x |
tags$div( |
139 | 1x |
class = "teal-widgets output-modal", |
140 | 1x |
modalDialog( |
141 | 1x |
easyClose = TRUE, |
142 | 1x |
tags$div( |
143 | 1x |
class = "float-right", |
144 | 1x |
bslib::tooltip( |
145 | 1x |
trigger = tags$div(type_download_ui_table(ns("modal_downbutton"))), |
146 | 1x |
options = list(trigger = "hover"), |
147 | 1x |
"Download"
|
148 |
)
|
|
149 |
),
|
|
150 | 1x |
tags$div( |
151 | 1x |
class = "teal-widgets table-modal-content", |
152 | 1x |
uiOutput(ns("table_out_modal")) |
153 |
)
|
|
154 |
)
|
|
155 |
)
|
|
156 |
)
|
|
157 |
}) |
|
158 | ||
159 | 3x |
type_download_srv_table( |
160 | 3x |
id = "modal_downbutton", |
161 | 3x |
table_reactive = table_r |
162 |
)
|
|
163 |
}) |
|
164 |
}
|
|
165 | ||
166 |
type_download_ui_table <- function(id) { |
|
167 | 2x |
ns <- NS(id) |
168 | 2x |
bslib::popover( |
169 | 2x |
icon("download"), |
170 | 2x |
tags$div( |
171 | 2x |
radioButtons(ns("file_format"), |
172 | 2x |
label = "File type", |
173 | 2x |
choices = c("formatted txt" = ".txt", "csv" = ".csv", "pdf" = ".pdf"), |
174 |
),
|
|
175 | 2x |
textInput(ns("file_name"), |
176 | 2x |
label = "File name (without extension)", |
177 | 2x |
value = paste0("table_", strftime(Sys.time(), format = "%Y%m%d_%H%M%S")) |
178 |
),
|
|
179 | 2x |
conditionalPanel( |
180 | 2x |
condition = paste0("input['", ns("file_format"), "'] != '.csv'"), |
181 | 2x |
tags$div( |
182 | 2x |
class = "lock-btn", |
183 | 2x |
title = "on / off", |
184 | 2x |
shinyWidgets::prettyToggle( |
185 | 2x |
ns("pagination_switch"), |
186 | 2x |
value = FALSE, |
187 | 2x |
label_on = NULL, |
188 | 2x |
label_off = NULL, |
189 | 2x |
status_on = "default", |
190 | 2x |
status_off = "default", |
191 | 2x |
outline = FALSE, |
192 | 2x |
plain = TRUE, |
193 | 2x |
icon_on = icon("fas fa-toggle-off"), |
194 | 2x |
icon_off = icon("fas fa-toggle-on"), |
195 | 2x |
animation = "pulse" |
196 |
)
|
|
197 |
),
|
|
198 | 2x |
tags$div( |
199 | 2x |
class = "paginate-ui", |
200 | 2x |
shinyWidgets::numericInputIcon( |
201 | 2x |
inputId = ns("lpp"), |
202 | 2x |
label = "Paginate table:", |
203 | 2x |
value = 70, |
204 | 2x |
icon = list("lines / page") |
205 |
),
|
|
206 | 2x |
uiOutput(ns("lpp_warning")) |
207 |
)
|
|
208 |
),
|
|
209 | 2x |
conditionalPanel( |
210 | 2x |
condition = paste0("input['", ns("file_name"), "'] != ''"), |
211 | 2x |
downloadButton(ns("data_download"), label = character(0), class = "btn-sm w-full") |
212 |
)
|
|
213 |
)
|
|
214 |
)
|
|
215 |
}
|
|
216 | ||
217 |
type_download_srv_table <- function(id, table_reactive) { |
|
218 | 12x |
moduleServer( |
219 | 12x |
id,
|
220 | 12x |
function(input, output, session) { |
221 | 12x |
observeEvent(input$pagination_switch, { |
222 | 13x |
if (input$pagination_switch) { |
223 | 7x |
shinyjs::enable("lpp") |
224 |
} else { |
|
225 | 6x |
shinyjs::disable("lpp") |
226 |
}
|
|
227 |
}) |
|
228 | ||
229 | 12x |
output$lpp_warning <- renderUI({ |
230 | 31x |
catch_warning <- if (input$file_format != ".csv" && input$pagination_switch) { |
231 | 9x |
try(rtables::paginate_table( |
232 | 9x |
tt = table_reactive(), |
233 | 9x |
lpp = as.numeric(input$lpp) |
234 | 9x |
), silent = TRUE) |
235 |
}
|
|
236 | ||
237 | 21x |
if (inherits(catch_warning, "try-error")) { |
238 | 4x |
helpText( |
239 | 4x |
class = "error", |
240 | 4x |
icon("triangle-exclamation"), |
241 | 4x |
"Maximum lines per page includes the reprinted header. Please enter a numeric value or increase the value."
|
242 |
)
|
|
243 |
}
|
|
244 |
}) |
|
245 | ||
246 | 12x |
output$data_download <- downloadHandler( |
247 | 12x |
filename = function() { |
248 | 22x |
paste0(input$file_name, input$file_format) |
249 |
},
|
|
250 | 12x |
content = function(file) { |
251 | 22x |
if (input$file_format == ".txt") { |
252 | 8x |
rtables::export_as_txt( |
253 | 8x |
x = table_reactive(), |
254 | 8x |
file = file, |
255 | 8x |
paginate = input$pagination_switch, |
256 | 8x |
lpp = if (input$pagination_switch) as.numeric(input$lpp) |
257 |
)
|
|
258 | 14x |
} else if (input$file_format == ".csv") { |
259 | 7x |
result <- rtables::matrix_form(table_reactive())$strings |
260 | 7x |
utils::write.table( |
261 | 7x |
x = result, |
262 | 7x |
file = file, |
263 | 7x |
sep = ",", |
264 | 7x |
col.names = FALSE, |
265 | 7x |
row.names = TRUE, |
266 | 7x |
append = FALSE |
267 |
)
|
|
268 |
} else { |
|
269 | 7x |
rtables::export_as_pdf( |
270 | 7x |
x = table_reactive(), |
271 | 7x |
file = file, |
272 | 7x |
paginate = input$pagination_switch, |
273 | 7x |
lpp = if (input$pagination_switch) as.numeric(input$lpp) |
274 |
)
|
|
275 |
}
|
|
276 |
}
|
|
277 |
)
|
|
278 |
}
|
|
279 |
)
|
|
280 |
}
|
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 `r lifecycle::badge("experimental")`
|
|
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 |
#' @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 |
tags$div( |
28 | 1x |
tags$div( |
29 | 1x |
class = "teal-widgets settings-buttons", |
30 | 1x |
bslib::tooltip( |
31 | 1x |
trigger = tags$div(type_download_ui(ns("downbutton"))), |
32 | 1x |
options = list(trigger = "hover"), |
33 | 1x |
class = "download-button", |
34 | 1x |
"Download"
|
35 |
),
|
|
36 | 1x |
bslib::tooltip( |
37 | 1x |
trigger = tags$div( |
38 | 1x |
bslib::popover( |
39 | 1x |
id = ns("expbut"), |
40 | 1x |
trigger = icon("maximize"), |
41 | 1x |
uiOutput(ns("slider_ui")), |
42 | 1x |
uiOutput(ns("width_warning")) |
43 |
)
|
|
44 |
),
|
|
45 | 1x |
options = list(trigger = "hover"), |
46 | 1x |
class = "resize-button", |
47 | 1x |
"Resize"
|
48 |
),
|
|
49 | 1x |
bslib::tooltip( |
50 | 1x |
trigger = tags$div( |
51 | 1x |
actionLink( |
52 | 1x |
ns("expand"), |
53 | 1x |
label = character(0), |
54 | 1x |
icon = icon("up-right-and-down-left-from-center"), |
55 | 1x |
class = "btn-sm", |
56 | 1x |
style = "color: #000;" |
57 |
)
|
|
58 |
),
|
|
59 | 1x |
options = list(trigger = "hover"), |
60 | 1x |
class = "expand-button", |
61 | 1x |
"Expand"
|
62 |
),
|
|
63 |
),
|
|
64 | 1x |
tags$br(), |
65 | 1x |
tags$div( |
66 | 1x |
class = "teal-widgets plot-content", |
67 | 1x |
uiOutput(ns("plot_out_main")) |
68 |
)
|
|
69 |
)
|
|
70 |
)
|
|
71 |
)
|
|
72 |
}
|
|
73 | ||
74 |
#' Plot-with-settings module
|
|
75 |
#'
|
|
76 |
#' @rdname plot_with_settings
|
|
77 |
#' @description `r lifecycle::badge("stable")`\cr
|
|
78 |
#' Universal module for plots with settings for height, width, and download.
|
|
79 |
#'
|
|
80 |
#' @export
|
|
81 |
#'
|
|
82 |
#' @param id (`character(1)`) `shiny` module id.
|
|
83 |
#'
|
|
84 |
#' @param plot_r (`reactive` or `function`)\cr
|
|
85 |
#' `reactive` expression or a simple `function` to draw a plot.
|
|
86 |
#' A simple `function` is needed e.g. for base plots like `plot(1)` as the output can not be caught when downloading.
|
|
87 |
#' Take into account that simple functions are less efficient than reactive, as not catching the result.
|
|
88 |
#' @param height (`numeric`) optional\cr
|
|
89 |
#' vector with three elements c(VAL, MIN, MAX), where VAL is the starting value of the slider in
|
|
90 |
#' the main and modal plot display. The value in the modal display is taken from the value of the
|
|
91 |
#' slider in the main plot display.
|
|
92 |
#' @param width (`numeric`) optional\cr
|
|
93 |
#' vector with three elements `c(VAL, MIN, MAX)`, where VAL is the starting value of the slider in
|
|
94 |
#' the main and modal plot display; `NULL` for default display. The value in the modal
|
|
95 |
#' display is taken from the value of the slider in the main plot display.
|
|
96 |
#' @param show_hide_signal optional, (`reactive logical` a mechanism to allow modules which call this
|
|
97 |
#' module to show/hide the plot_with_settings UI)
|
|
98 |
#' @param brushing (`logical`) optional\cr
|
|
99 |
#' mechanism to enable / disable brushing on the main plot (in particular: not the one displayed
|
|
100 |
#' in modal). All the brushing data is stored as a reactive object in the `"brush"` element of
|
|
101 |
#' returned list. See the example for details.
|
|
102 |
#' @param clicking (`logical`)\cr
|
|
103 |
#' a mechanism to enable / disable clicking on data points on the main plot (in particular: not the
|
|
104 |
#' one displayed in modal). All the clicking data is stored as a reactive object in the `"click"`
|
|
105 |
#' element of returned list. See the example for details.
|
|
106 |
#' @param dblclicking (`logical`) optional\cr
|
|
107 |
#' mechanism to enable / disable double-clicking on data points on the main plot (in particular:
|
|
108 |
#' not the one displayed in modal). All the double clicking data is stored as a reactive object in
|
|
109 |
#' the `"dblclick"` element of returned list. See the example for details.
|
|
110 |
#' @param hovering (`logical(1)`) optional\cr
|
|
111 |
#' mechanism to enable / disable hovering over data points on the main plot (in particular: not
|
|
112 |
#' the one displayed in modal). All the hovering data is stored as a reactive object in the
|
|
113 |
#' `"hover"` element of returned list. See the example for details.
|
|
114 |
#' @param graph_align (`character(1)`) optional,\cr
|
|
115 |
#' one of `"left"` (default), `"center"`, `"right"` or `"justify"`. The alignment of the graph on
|
|
116 |
#' the main page.
|
|
117 |
#'
|
|
118 |
#' @details By default the plot is rendered with `72 dpi`. In order to change this, to for example 96 set
|
|
119 |
#' `options(teal.plot_dpi = 96)`. The minimum allowed `dpi` value is `24` and it must be a whole number.
|
|
120 |
#' If an invalid value is set then the default value is used and a warning is outputted to the console.
|
|
121 |
#'
|
|
122 |
#' @return A `shiny` module.
|
|
123 |
#'
|
|
124 |
#' @examples
|
|
125 |
#' # Example using a reactive as input to plot_r
|
|
126 |
#' library(shiny)
|
|
127 |
#' library(ggplot2)
|
|
128 |
#'
|
|
129 |
#' ui <- bslib::page_fluid(
|
|
130 |
#' plot_with_settings_ui(
|
|
131 |
#' id = "plot_with_settings"
|
|
132 |
#' )
|
|
133 |
#' )
|
|
134 |
#'
|
|
135 |
#' server <- function(input, output, session) {
|
|
136 |
#' plot_r <- reactive({
|
|
137 |
#' ggplot(faithful, aes(x = .data$waiting, y = .data$eruptions)) +
|
|
138 |
#' geom_point()
|
|
139 |
#' })
|
|
140 |
#'
|
|
141 |
#' plot_with_settings_srv(
|
|
142 |
#' id = "plot_with_settings",
|
|
143 |
#' plot_r = plot_r,
|
|
144 |
#' height = c(400, 100, 1200),
|
|
145 |
#' width = c(500, 250, 750)
|
|
146 |
#' )
|
|
147 |
#' }
|
|
148 |
#'
|
|
149 |
#' if (interactive()) {
|
|
150 |
#' shinyApp(ui, server)
|
|
151 |
#' }
|
|
152 |
#'
|
|
153 |
#' # Example using a function as input to plot_r
|
|
154 |
#' library(lattice)
|
|
155 |
#'
|
|
156 |
#' ui <- bslib::page_fluid(
|
|
157 |
#' radioButtons("download_option", "Select the Option", list("ggplot", "trellis", "grob", "base")),
|
|
158 |
#' plot_with_settings_ui(
|
|
159 |
#' id = "plot_with_settings"
|
|
160 |
#' ),
|
|
161 |
#' sliderInput("nums", "Value", 1, 10, 1)
|
|
162 |
#' )
|
|
163 |
#'
|
|
164 |
#' server <- function(input, output, session) {
|
|
165 |
#' plot_r <- function() {
|
|
166 |
#' numbers <- seq_len(input$nums)
|
|
167 |
#' if (input$download_option == "ggplot") {
|
|
168 |
#' ggplot(data.frame(n = numbers), aes(.data$n)) +
|
|
169 |
#' geom_bar()
|
|
170 |
#' } else if (input$download_option == "trellis") {
|
|
171 |
#' densityplot(numbers)
|
|
172 |
#' } else if (input$download_option == "grob") {
|
|
173 |
#' tr_plot <- densityplot(numbers)
|
|
174 |
#' ggplotGrob(
|
|
175 |
#' ggplot(data.frame(n = numbers), aes(.data$n)) +
|
|
176 |
#' geom_bar()
|
|
177 |
#' )
|
|
178 |
#' } else if (input$download_option == "base") {
|
|
179 |
#' plot(numbers)
|
|
180 |
#' }
|
|
181 |
#' }
|
|
182 |
#'
|
|
183 |
#' plot_with_settings_srv(
|
|
184 |
#' id = "plot_with_settings",
|
|
185 |
#' plot_r = plot_r,
|
|
186 |
#' height = c(400, 100, 1200),
|
|
187 |
#' width = c(500, 250, 750)
|
|
188 |
#' )
|
|
189 |
#' }
|
|
190 |
#'
|
|
191 |
#' if (interactive()) {
|
|
192 |
#' shinyApp(ui, server)
|
|
193 |
#' }
|
|
194 |
#'
|
|
195 |
#' # Example with brushing/hovering/clicking/double-clicking
|
|
196 |
#' ui <- bslib::page_fluid(
|
|
197 |
#' plot_with_settings_ui(
|
|
198 |
#' id = "plot_with_settings"
|
|
199 |
#' ),
|
|
200 |
#' fluidRow(
|
|
201 |
#' column(4, tags$h3("Brush"), verbatimTextOutput("brushing_data")),
|
|
202 |
#' column(4, tags$h3("Click"), verbatimTextOutput("clicking_data")),
|
|
203 |
#' column(4, tags$h3("DblClick"), verbatimTextOutput("dblclicking_data")),
|
|
204 |
#' column(4, tags$h3("Hover"), verbatimTextOutput("hovering_data"))
|
|
205 |
#' )
|
|
206 |
#' )
|
|
207 |
#'
|
|
208 |
#' server <- function(input, output, session) {
|
|
209 |
#' plot_r <- reactive({
|
|
210 |
#' ggplot(faithful, aes(x = .data$waiting, y = .data$eruptions)) +
|
|
211 |
#' geom_point()
|
|
212 |
#' })
|
|
213 |
#'
|
|
214 |
#' plot_data <- plot_with_settings_srv(
|
|
215 |
#' id = "plot_with_settings",
|
|
216 |
#' plot_r = plot_r,
|
|
217 |
#' height = c(400, 100, 1200),
|
|
218 |
#' brushing = TRUE,
|
|
219 |
#' clicking = TRUE,
|
|
220 |
#' dblclicking = TRUE,
|
|
221 |
#' hovering = TRUE
|
|
222 |
#' )
|
|
223 |
#'
|
|
224 |
#' output$brushing_data <- renderPrint(plot_data$brush())
|
|
225 |
#' output$clicking_data <- renderPrint(plot_data$click())
|
|
226 |
#' output$dblclicking_data <- renderPrint(plot_data$dblclick())
|
|
227 |
#' output$hovering_data <- renderPrint(plot_data$hover())
|
|
228 |
#' }
|
|
229 |
#'
|
|
230 |
#' if (interactive()) {
|
|
231 |
#' shinyApp(ui, server)
|
|
232 |
#' }
|
|
233 |
#'
|
|
234 |
#' # Example which allows module to be hidden/shown
|
|
235 |
#' library("shinyjs")
|
|
236 |
#'
|
|
237 |
#' ui <- bslib::page_fluid(
|
|
238 |
#' useShinyjs(),
|
|
239 |
#' actionButton("button", "Show/Hide"),
|
|
240 |
#' plot_with_settings_ui(
|
|
241 |
#' id = "plot_with_settings"
|
|
242 |
#' )
|
|
243 |
#' )
|
|
244 |
#'
|
|
245 |
#' server <- function(input, output, session) {
|
|
246 |
#' plot_r <- plot_r <- reactive(
|
|
247 |
#' ggplot(faithful, aes(x = .data$waiting, y = .data$eruptions)) +
|
|
248 |
#' geom_point()
|
|
249 |
#' )
|
|
250 |
#'
|
|
251 |
#' show_hide_signal_rv <- reactiveVal(TRUE)
|
|
252 |
#'
|
|
253 |
#' observeEvent(input$button, show_hide_signal_rv(!show_hide_signal_rv()))
|
|
254 |
#'
|
|
255 |
#' plot_with_settings_srv(
|
|
256 |
#' id = "plot_with_settings",
|
|
257 |
#' plot_r = plot_r,
|
|
258 |
#' height = c(400, 100, 1200),
|
|
259 |
#' width = c(500, 250, 750),
|
|
260 |
#' show_hide_signal = reactive(show_hide_signal_rv())
|
|
261 |
#' )
|
|
262 |
#' }
|
|
263 |
#'
|
|
264 |
#' if (interactive()) {
|
|
265 |
#' shinyApp(ui, server)
|
|
266 |
#' }
|
|
267 |
#'
|
|
268 |
plot_with_settings_srv <- function(id, |
|
269 |
plot_r,
|
|
270 |
height = c(600, 200, 2000), |
|
271 |
width = NULL, |
|
272 |
show_hide_signal = reactive(TRUE), |
|
273 |
brushing = FALSE, |
|
274 |
clicking = FALSE, |
|
275 |
dblclicking = FALSE, |
|
276 |
hovering = FALSE, |
|
277 |
graph_align = "left") { |
|
278 | 23x |
checkmate::assert_string(id) |
279 | 23x |
checkmate::assert( |
280 | 23x |
checkmate::check_class(plot_r, "function"), |
281 | 23x |
checkmate::check_class(plot_r, "reactive") |
282 |
)
|
|
283 | 22x |
checkmate::assert_numeric(height, min.len = 1, any.missing = FALSE) |
284 | 21x |
checkmate::assert_numeric(height, len = 3, any.missing = FALSE, finite = TRUE) |
285 | 21x |
checkmate::assert_numeric(height[1], lower = height[2], upper = height[3], .var.name = "height") |
286 | 21x |
checkmate::assert_numeric(width, len = 3, any.missing = FALSE, null.ok = TRUE, finite = TRUE) |
287 | 21x |
checkmate::assert_numeric(width[1], lower = width[2], upper = width[3], null.ok = TRUE, .var.name = "width") |
288 | ||
289 | 20x |
checkmate::assert_class(show_hide_signal, c("reactive", "function")) |
290 | 19x |
checkmate::assert_flag(brushing) |
291 | 18x |
checkmate::assert_flag(clicking) |
292 | 17x |
checkmate::assert_flag(dblclicking) |
293 | 16x |
checkmate::assert_flag(hovering) |
294 | 15x |
checkmate::assert_string(graph_align) |
295 | 15x |
checkmate::assert_subset(graph_align, c("left", "right", "center", "justify")) |
296 | ||
297 | 14x |
moduleServer(id, function(input, output, session) { |
298 | 14x |
ns <- session$ns |
299 | 14x |
shinyjs::runjs( |
300 | 14x |
sprintf( |
301 | 14x |
'establishPlotResizing("%s", "%s", "%s");',
|
302 | 14x |
ns("plot_main"), # graph parent id |
303 | 14x |
ns("flex_width"), # session input$ variable name |
304 | 14x |
ns("plot_modal_width") # session input$ variable name |
305 |
)
|
|
306 |
)
|
|
307 | 14x |
default_w <- function() session$clientData[[paste0("output_", ns("plot_main_width"))]] |
308 | 14x |
default_h <- function() session$clientData[[paste0("output_", ns("plot_main_height"))]] |
309 | ||
310 | 14x |
default_slider_width <- reactiveVal(width) |
311 | 14x |
delayed_flex_width <- debounce(reactive(input$flex_width), millis = 100) |
312 | ||
313 | 14x |
if (is.null(width)) { |
314 |
# if width = NULL then set default_slider_width to be the value of the plot width on load
|
|
315 | ! |
observeEvent(session$clientData[[paste0("output_", ns("plot_main_width"))]], |
316 | ! |
handlerExpr = { |
317 | ! |
default_slider_width(default_w() * c(1, 0.5, 2.8)) |
318 |
},
|
|
319 | ! |
once = TRUE, |
320 | ! |
ignoreNULL = TRUE |
321 |
)
|
|
322 | ||
323 | ! |
observeEvent(delayed_flex_width(), { |
324 | ! |
if (delayed_flex_width() > 0 && !isFALSE(input$width_resize_switch)) { |
325 | ! |
default_slider_width(delayed_flex_width() * c(1, 0.5, 2.8)) |
326 | ! |
updateSliderInput(session, inputId = "width", value = delayed_flex_width()) |
327 |
}
|
|
328 |
}) |
|
329 |
}
|
|
330 | ||
331 | 14x |
plot_type <- reactive({ |
332 | 14x |
if (inherits(plot_r(), "ggplot")) { |
333 | 2x |
"gg"
|
334 | 12x |
} else if (inherits(plot_r(), "trellis")) { |
335 | 2x |
"trel"
|
336 | 10x |
} else if (inherits(plot_r(), "grob")) { |
337 | 2x |
"grob"
|
338 | 8x |
} else if (inherits(plot_r(), c("NULL", "histogram", "list")) && !inherits(plot_r, "reactive")) { |
339 | 6x |
"base"
|
340 |
} else { |
|
341 | 2x |
"other"
|
342 |
}
|
|
343 |
}) |
|
344 | ||
345 |
# allow modules which use this module to turn on and off the UI
|
|
346 | 14x |
observeEvent(show_hide_signal(), { |
347 | 8x |
if (show_hide_signal()) { |
348 | 8x |
shinyjs::show("plot-with-settings") |
349 |
} else { |
|
350 | ! |
shinyjs::hide("plot-with-settings") |
351 |
}
|
|
352 |
}) |
|
353 | ||
354 | 14x |
output$slider_ui <- renderUI({ |
355 | 8x |
req(default_slider_width()) |
356 | 8x |
tags$div( |
357 | 8x |
optionalSliderInputValMinMax( |
358 | 8x |
inputId = ns("height"), |
359 | 8x |
label = "Plot height", |
360 | 8x |
value_min_max = round(height), |
361 | 8x |
ticks = FALSE, |
362 | 8x |
step = 1L, |
363 | 8x |
round = TRUE |
364 |
),
|
|
365 | 8x |
tags$b("Plot width"), |
366 | 8x |
shinyWidgets::switchInput( |
367 | 8x |
inputId = ns("width_resize_switch"), |
368 | 8x |
onLabel = "ON", |
369 | 8x |
offLabel = "OFF", |
370 | 8x |
label = "Auto width", |
371 | 8x |
value = `if`(is.null(width), TRUE, FALSE), |
372 | 8x |
size = "mini", |
373 | 8x |
labelWidth = "80px" |
374 |
),
|
|
375 | 8x |
optionalSliderInputValMinMax( |
376 | 8x |
inputId = ns("width"), |
377 | 8x |
label = NULL, |
378 | 8x |
value_min_max = round(isolate(default_slider_width())), |
379 | 8x |
ticks = FALSE, |
380 | 8x |
step = 1L, |
381 | 8x |
round = TRUE |
382 |
)
|
|
383 |
)
|
|
384 |
}) |
|
385 | ||
386 | 14x |
observeEvent(input$width_resize_switch | delayed_flex_width(), { |
387 | 8x |
if (length(input$width_resize_switch) && input$width_resize_switch) { |
388 | ! |
shinyjs::disable("width") |
389 | ! |
updateSliderInput(session, inputId = "width", value = delayed_flex_width()) |
390 |
} else { |
|
391 | 8x |
shinyjs::enable("width") |
392 |
}
|
|
393 |
}) |
|
394 | ||
395 | 14x |
ranges <- reactiveValues(x = NULL, y = NULL) |
396 | ||
397 | 14x |
observeEvent(input$plot_dblclick, { |
398 | 1x |
brush <- input$plot_brush |
399 | 1x |
if (!is.null(brush)) { |
400 | ! |
ranges$x <- c(brush$xmin, brush$xmax) |
401 | ! |
ranges$y <- c(brush$ymin, brush$ymax) |
402 |
} else { |
|
403 | 1x |
ranges$x <- NULL |
404 | 1x |
ranges$y <- NULL |
405 |
}
|
|
406 |
}) |
|
407 | ||
408 | 14x |
p_height <- reactive(if (!is.null(input$height)) input$height else height[1]) |
409 | 14x |
p_width <- reactive( |
410 | 14x |
if (!is.null(input$width)) { |
411 | 5x |
input$width |
412 |
} else { |
|
413 | 3x |
if (!is.null(default_slider_width()[1])) { |
414 | 3x |
default_slider_width()[1] |
415 |
} else { |
|
416 |
# Fallback to "auto"
|
|
417 | ! |
"auto"
|
418 |
}
|
|
419 |
}
|
|
420 |
)
|
|
421 | 14x |
output$plot_main <- renderPlot( |
422 | 14x |
apply_plot_modifications( |
423 | 14x |
plot_obj = plot_suppress(plot_r()), |
424 | 14x |
plot_type = plot_suppress(plot_type()), |
425 | 14x |
dblclicking = dblclicking, |
426 | 14x |
ranges = ranges |
427 |
),
|
|
428 | 14x |
res = get_plot_dpi(), |
429 | 14x |
height = p_height, |
430 | 14x |
width = p_width |
431 |
)
|
|
432 | ||
433 | 14x |
output$plot_modal <- renderPlot( |
434 | 14x |
apply_plot_modifications( |
435 | 14x |
plot_obj = plot_suppress(plot_r()), |
436 | 14x |
plot_type = plot_suppress(plot_type()), |
437 | 14x |
dblclicking = dblclicking, |
438 | 14x |
ranges = ranges |
439 |
),
|
|
440 | 14x |
res = get_plot_dpi(), |
441 | 14x |
height = reactive(input$height_in_modal), |
442 | 14x |
width = reactive(input$width_in_modal) |
443 |
)
|
|
444 | ||
445 | 14x |
output$plot_out_main <- renderUI({ |
446 | 8x |
req(plot_suppress(plot_r())) |
447 | 4x |
tags$div( |
448 | 4x |
align = graph_align, |
449 | 4x |
plotOutput( |
450 | 4x |
ns("plot_main"), |
451 | 4x |
height = "100%", |
452 | 4x |
brush = `if`(brushing, brushOpts(ns("plot_brush"), resetOnNew = FALSE), NULL), |
453 | 4x |
click = `if`(clicking, clickOpts(ns("plot_click")), NULL), |
454 | 4x |
dblclick = `if`(dblclicking, dblclickOpts(ns("plot_dblclick")), NULL), |
455 | 4x |
hover = `if`(hovering, hoverOpts(ns("plot_hover")), NULL) |
456 |
)
|
|
457 |
)
|
|
458 |
}) |
|
459 | ||
460 | 14x |
output$width_warning <- renderUI({ |
461 | 8x |
grDevices::pdf(nullfile()) # reset Rplots.pdf for shiny server |
462 | 8x |
w <- grDevices::dev.size("px")[1] |
463 | 8x |
grDevices::dev.off() |
464 | 8x |
if (p_width() < w) { |
465 | 8x |
helpText( |
466 | 8x |
icon("triangle-exclamation"), |
467 | 8x |
"Plot might be cut off for small widths."
|
468 |
)
|
|
469 |
}
|
|
470 |
}) |
|
471 | ||
472 | 14x |
type_download_srv( |
473 | 14x |
id = "downbutton", |
474 | 14x |
plot_reactive = plot_r, |
475 | 14x |
plot_type = plot_type, |
476 | 14x |
plot_w = p_width, |
477 | 14x |
default_w = default_w, |
478 | 14x |
plot_h = p_height, |
479 | 14x |
default_h = default_h |
480 |
)
|
|
481 | ||
482 | 14x |
output$plot_out_modal <- renderUI({ |
483 | 9x |
plotOutput(ns("plot_modal"), height = input$height_in_modal, width = input$width_in_modal) |
484 |
}) |
|
485 | ||
486 | 14x |
observeEvent(input$expand, { |
487 | 1x |
showModal( |
488 | 1x |
tags$div( |
489 | 1x |
class = "teal-widgets plot-modal", |
490 | 1x |
modalDialog( |
491 | 1x |
easyClose = TRUE, |
492 | 1x |
tags$div( |
493 | 1x |
class = "plot-modal-sliders", |
494 | 1x |
optionalSliderInputValMinMax( |
495 | 1x |
inputId = ns("height_in_modal"), |
496 | 1x |
label = "Plot height", |
497 | 1x |
value_min_max = round(c(p_height(), height[2:3])), |
498 | 1x |
ticks = FALSE, |
499 | 1x |
step = 1L, |
500 | 1x |
round = TRUE, |
501 | 1x |
width = "30vw" |
502 |
),
|
|
503 | 1x |
optionalSliderInputValMinMax( |
504 | 1x |
inputId = ns("width_in_modal"), |
505 | 1x |
label = "Plot width", |
506 | 1x |
value_min_max = round(c( |
507 | 1x |
ifelse( |
508 | 1x |
is.null(input$width) || !isFALSE(input$width_resize_switch), |
509 | 1x |
ifelse( |
510 | 1x |
is.null(input$plot_modal_width) || input$plot_modal_width > default_slider_width()[3], |
511 | 1x |
default_slider_width()[1], |
512 | 1x |
input$plot_modal_width |
513 |
),
|
|
514 | 1x |
input$width |
515 |
),
|
|
516 | 1x |
default_slider_width()[2:3] |
517 |
)), |
|
518 | 1x |
ticks = FALSE, |
519 | 1x |
step = 1L, |
520 | 1x |
round = TRUE, |
521 | 1x |
width = "30vw" |
522 |
),
|
|
523 | 1x |
bslib::tooltip( |
524 | 1x |
trigger = tags$div(type_download_ui(ns("modal_downbutton"))), |
525 | 1x |
options = list(trigger = "hover"), |
526 | 1x |
"Download"
|
527 |
)
|
|
528 |
),
|
|
529 | 1x |
tags$div( |
530 | 1x |
class = "teal-widgets plot-modal-content", |
531 | 1x |
uiOutput(ns("plot_out_modal")) |
532 |
)
|
|
533 |
)
|
|
534 |
)
|
|
535 |
)
|
|
536 |
}) |
|
537 | ||
538 | 14x |
type_download_srv( |
539 | 14x |
id = "modal_downbutton", |
540 | 14x |
plot_reactive = plot_r, |
541 | 14x |
plot_type = plot_type, |
542 | 14x |
plot_w = reactive(input$width_in_modal), |
543 | 14x |
default_w = default_w, |
544 | 14x |
plot_h = reactive(input$height_in_modal), |
545 | 14x |
default_h = default_h |
546 |
)
|
|
547 | ||
548 | 14x |
list( |
549 | 14x |
brush = reactive({ |
550 |
# refresh brush data on the main plot size change
|
|
551 | 1x |
input$height |
552 | 1x |
input$width |
553 | 1x |
input$plot_brush |
554 |
}), |
|
555 | 14x |
click = reactive({ |
556 |
# refresh click data on the main plot size change
|
|
557 | 1x |
input$height |
558 | 1x |
input$width |
559 | 1x |
input$plot_click |
560 |
}), |
|
561 | 14x |
dblclick = reactive({ |
562 |
# refresh double click data on the main plot size change
|
|
563 | 1x |
input$height |
564 | 1x |
input$width |
565 | 1x |
input$plot_dblclick |
566 |
}), |
|
567 | 14x |
hover = reactive({ |
568 |
# refresh hover data on the main plot size change
|
|
569 | 1x |
input$height |
570 | 1x |
input$width |
571 | 1x |
input$plot_hover |
572 |
}), |
|
573 | 14x |
dim = reactive(c(p_width(), p_height())) |
574 |
)
|
|
575 |
}) |
|
576 |
}
|
|
577 | ||
578 |
#' @keywords internal
|
|
579 |
type_download_ui <- function(id) { |
|
580 | 3x |
ns <- NS(id) |
581 | 3x |
bslib::popover( |
582 | 3x |
icon("download"), |
583 | 3x |
tags$div( |
584 | 3x |
radioButtons(ns("file_format"), |
585 | 3x |
label = "File type", |
586 | 3x |
choices = c("png" = "png", "pdf" = "pdf", "svg" = "svg"), |
587 |
),
|
|
588 | 3x |
textInput(ns("file_name"), |
589 | 3x |
label = "File name (without extension)", |
590 | 3x |
value = paste0("plot_", strftime(Sys.time(), format = "%Y%m%d_%H%M%S")) |
591 |
),
|
|
592 | 3x |
conditionalPanel( |
593 | 3x |
condition = paste0("input['", ns("file_name"), "'] != ''"), |
594 | 3x |
downloadButton(ns("data_download"), label = character(0), class = "btn-sm w-full") |
595 |
)
|
|
596 |
)
|
|
597 |
)
|
|
598 |
}
|
|
599 | ||
600 |
#' @keywords internal
|
|
601 |
type_download_srv <- function(id, plot_reactive, plot_type, plot_w, default_w, plot_h, default_h) { |
|
602 | 32x |
moduleServer( |
603 | 32x |
id,
|
604 | 32x |
function(input, output, session) { |
605 | 32x |
output$data_download <- downloadHandler( |
606 | 32x |
filename = function() { |
607 | 20x |
paste(input$file_name, input$file_format, sep = ".") |
608 |
},
|
|
609 | 32x |
content = function(file) { |
610 | 20x |
width <- `if`(!is.null(plot_w()), plot_w(), default_w()) |
611 | 20x |
height <- `if`(!is.null(plot_h()), plot_h(), default_h()) |
612 | ||
613 |
# svg and pdf have width in inches and 1 inch = get_plot_dpi() pixels
|
|
614 | 20x |
switch(input$file_format, |
615 | 12x |
png = grDevices::png(file, width, height), |
616 | 4x |
pdf = grDevices::pdf(file, width / get_plot_dpi(), height / get_plot_dpi()), |
617 | 4x |
svg = grDevices::svg(file, width / get_plot_dpi(), height / get_plot_dpi()) |
618 |
)
|
|
619 | ||
620 | 20x |
print_plot(plot_reactive, plot_type) |
621 | ||
622 | 20x |
grDevices::dev.off() |
623 |
}
|
|
624 |
)
|
|
625 |
}
|
|
626 |
)
|
|
627 |
}
|
|
628 | ||
629 |
#' Clean brushed points
|
|
630 |
#'
|
|
631 |
#' @description `r lifecycle::badge("stable")`\cr
|
|
632 |
#' Cleans and organizes output to account for NAs and remove empty rows. Wrapper around `shiny::brushedPoints`.
|
|
633 |
#' @param data (`data.frame`)\cr
|
|
634 |
#' A data.frame from which to select rows.
|
|
635 |
#' @param brush (`list`)\cr
|
|
636 |
#' The data from a brush e.g. `input$plot_brush`.
|
|
637 |
#'
|
|
638 |
#' @return A `data.frame` of selected rows.
|
|
639 |
#'
|
|
640 |
#' @examples
|
|
641 |
#'
|
|
642 |
#' brush <- list(
|
|
643 |
#' mapping = list(
|
|
644 |
#' x = "AGE",
|
|
645 |
#' y = "BMRKR1"
|
|
646 |
#' ),
|
|
647 |
#' xmin = 30, xmax = 40,
|
|
648 |
#' ymin = 0.7, ymax = 10,
|
|
649 |
#' direction = "xy"
|
|
650 |
#' )
|
|
651 |
#'
|
|
652 |
#' data <- data.frame(
|
|
653 |
#' STUDYID = letters[1:20],
|
|
654 |
#' USUBJID = LETTERS[1:20],
|
|
655 |
#' AGE = sample(25:40, size = 20, replace = TRUE),
|
|
656 |
#' BMRKR1 = runif(20, min = 0, max = 12)
|
|
657 |
#' )
|
|
658 |
#' nrow(clean_brushedPoints(data, brush))
|
|
659 |
#' data$AGE[1:10] <- NA
|
|
660 |
#' nrow(clean_brushedPoints(data, brush))
|
|
661 |
#'
|
|
662 |
#' @export
|
|
663 |
#'
|
|
664 |
clean_brushedPoints <- function(data, brush) { # nolint object_name_linter. |
|
665 | 6x |
checkmate::assert_data_frame(data) |
666 | 4x |
checkmate::assert_list(brush, null.ok = TRUE) |
667 | ||
668 |
# define original panelvar1 and panelvar2 before getting overwritten
|
|
669 | 4x |
original_panelvar1 <- brush$mapping$panelvar1 |
670 | 4x |
original_panelvar2 <- brush$mapping$panelvar2 |
671 | ||
672 |
# Assign NULL to `mapping$panelvar1` and `mapping$panelvar1` if `brush$panelvar1` and `brush$panelvar1` are NULL
|
|
673 |
# This will not evaluate the `panelMatch` step in `brushedPoints` and thus will return a non empty dataframe
|
|
674 | 4x |
if (is.null(brush$panelvar1)) brush$mapping$panelvar1 <- NULL |
675 | 4x |
if (is.null(brush$panelvar2)) brush$mapping$panelvar2 <- NULL |
676 | ||
677 | 4x |
bp_df <- brushedPoints(data, brush) |
678 | ||
679 |
# Keep required rows only based on the value of `brush$panelvar1`
|
|
680 | 3x |
df <- if (is.null(brush$panelvar1) && is.character(original_panelvar1) && |
681 | 3x |
is.null(brush$panelvar2) && is.character(original_panelvar2)) { |
682 | ! |
df_var1 <- bp_df[is.na(bp_df[[original_panelvar1]]), ] |
683 | ! |
df_var1[is.na(df_var1[[original_panelvar2]]), ] |
684 | 3x |
} else if (is.null(brush$panelvar1) && is.character(original_panelvar1)) { |
685 | ! |
bp_df[is.na(bp_df[[original_panelvar1]]), ] |
686 | 3x |
} else if (is.null(brush$panelvar2) && is.character(original_panelvar2)) { |
687 | ! |
bp_df[is.na(bp_df[[original_panelvar2]]), ] |
688 |
} else { |
|
689 | 3x |
bp_df
|
690 |
}
|
|
691 | ||
692 |
# filter out rows that are only NAs
|
|
693 | 3x |
df <- df[rowSums(is.na(df)) != ncol(df), ] |
694 | 3x |
df
|
695 |
}
|
|
696 | ||
697 |
#' @keywords internal
|
|
698 |
#'
|
|
699 |
get_plot_dpi <- function() { |
|
700 | 53x |
default_dpi <- 72 |
701 | 53x |
dpi <- getOption("teal.plot_dpi", default_dpi) |
702 | 53x |
if (!checkmate::test_integerish(dpi, lower = 24, any.missing = FALSE, len = 1)) { |
703 | 4x |
warning(paste("Invalid value for option 'teal.plot_dpi', therefore defaulting to", default_dpi, "dpi")) |
704 | 4x |
dpi <- default_dpi |
705 |
}
|
|
706 | 53x |
dpi
|
707 |
}
|
|
708 | ||
709 |
#' Print plot for download functionality
|
|
710 |
#'
|
|
711 |
#' @param plot (`reactive`)\cr
|
|
712 |
#' reactive expression to draw a plot
|
|
713 |
#' @param plot_type (`reactive`)\cr
|
|
714 |
#' reactive plot type (`gg`, `trel`, `grob`, `other`)
|
|
715 |
#'
|
|
716 |
#' @return Nothing returned, the plot is printed.
|
|
717 |
#' @keywords internal
|
|
718 |
#'
|
|
719 |
print_plot <- function(plot, plot_type) { |
|
720 | 26x |
switch(plot_type(), |
721 | 2x |
"grob" = grid::grid.draw(plot()), |
722 |
"other" = { |
|
723 | 2x |
graphics::plot.new() |
724 | 2x |
graphics::text( |
725 | 2x |
x = graphics::grconvertX(0.5, from = "npc"), |
726 | 2x |
y = graphics::grconvertY(0.5, from = "npc"), |
727 | 2x |
labels = "This plot graphic type is not yet supported to download" |
728 |
)
|
|
729 |
},
|
|
730 | 18x |
"base" = plot(), |
731 | 4x |
print(plot()) |
732 |
)
|
|
733 |
}
|
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 `r lifecycle::badge("stable")`\cr
|
|
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 |
#' 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 | 13x |
if (plot_type == "gg" && dblclicking) { |
13 | 1x |
plot_obj + |
14 | 1x |
ggplot2::coord_cartesian(xlim = ranges$x, ylim = ranges$y, expand = FALSE) |
15 | 12x |
} else if (plot_type == "grob") { |
16 | 2x |
grid::grid.newpage() |
17 | 2x |
grid::grid.draw(plot_obj) |
18 |
} else { |
|
19 | 10x |
plot_obj
|
20 |
}
|
|
21 |
}
|
|
22 | ||
23 |
#' This function opens a PDF graphics device using [grDevices::pdf()] to suppress
|
|
24 |
#' the plot display in the IDE. The purpose of this function is to avoid opening graphic devices
|
|
25 |
#' directly in the IDE.
|
|
26 |
#'
|
|
27 |
#' @param x lazy binding which generates the plot(s)
|
|
28 |
#'
|
|
29 |
#' @keywords internal
|
|
30 |
plot_suppress <- function(x) { |
|
31 | 32x |
grDevices::pdf(nullfile()) |
32 | 32x |
on.exit(grDevices::dev.off()) |
33 | 32x |
force(x) |
34 |
}
|
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 |
#' Small well class for HTML
|
|
2 |
#'
|
|
3 |
#' @description `r lifecycle::badge("stable")`\cr
|
|
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 |
}
|
1 |
#' Nested Closeable Modal Popup
|
|
2 |
#'
|
|
3 |
#' @description `r lifecycle::badge("experimental")`
|
|
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 | ! |
modal_args <- append(list(...), modal_args) |
86 | ! |
tagList( |
87 | ! |
htmltools::tagQuery(do.call(modalDialog, modal_args))$ |
88 | ! |
removeAttrs("id")$ |
89 | ! |
addAttrs(id = id, `aria-hidden` = "true", class = "custom-modal", `data-backdrop` = "false")$ |
90 | ! |
children("div")$ |
91 | ! |
children("div")$ |
92 | ! |
children("div")$ |
93 | ! |
siblings(".modal-footer")$ |
94 | ! |
find("button")$ |
95 | ! |
removeAttrs(c("data-dismiss", "data-bs-dismiss"))$ |
96 | ! |
addAttrs(onclick = paste0("$('#", id, "').modal('hide');"))$ |
97 | ! |
allTags() |
98 |
)
|
|
99 |
}
|
1 |
#' Builds a `basic_table_args` object
|
|
2 |
#'
|
|
3 |
#' @description `r lifecycle::badge("experimental")`
|
|
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 `r lifecycle::badge("experimental")`
|
|
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 `r lifecycle::badge("experimental")`
|
|
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 `r lifecycle::badge("stable")`\cr
|
|
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 |
#' DTOutput(ns("data_table")),
|
|
20 |
#' get_dt_rows(ns("data_table"), ns("dt_rows"))
|
|
21 |
#' )
|
|
22 |
#' }
|
|
23 |
#'
|
|
24 |
#' # use the input$dt_rows in the Shiny Server function
|
|
25 |
#' server <- function(id) {
|
|
26 |
#' moduleServer(id, function(input, output, session) {
|
|
27 |
#' output$data_table <- renderDataTable(
|
|
28 |
#' {
|
|
29 |
#' iris
|
|
30 |
#' },
|
|
31 |
#' options = list(pageLength = input$dt_rows)
|
|
32 |
#' )
|
|
33 |
#' })
|
|
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.onInputChange('%s', len); |
50 |
}); |
|
51 |
});", |
|
52 | ! |
paste0("#", dt_name), |
53 | ! |
dt_rows
|
54 |
)
|
|
55 |
)
|
|
56 |
)
|
|
57 |
}
|