diff --git a/DESCRIPTION b/DESCRIPTION index e998e597d..c26c52474 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -32,6 +32,7 @@ Imports: magrittr, scales, shinyjs, + shinyvalidate, shinyWidgets, stats, stringr, diff --git a/NEWS.md b/NEWS.md index 4a5b0d4a4..2331c4136 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,7 @@ * Improved UI labels and plot panel title in `tm_g_association`. * Added inputs `tm_variable_browser` module for text size and plot theme. * Forced ggplot theme to be always selected in all modules. +* Updated encodings input checks to use `shinyvalidate::InputValidator` instead of `shiny::validate` for better UI experience. ### Bug fixes * Fixed a bug in `tm_g_scatterplot` when selected x and y facets were the same. @@ -20,6 +21,7 @@ ### Miscellaneous * Examples now use `scda.2022` rather than `scda.2021`. +* Replaced deprecated `ggplot2` functions `..count..`, `..density..` and `..prop..`. # teal.modules.general 0.2.15 diff --git a/R/tm_a_pca.R b/R/tm_a_pca.R index 97806cb37..cbc85bb09 100644 --- a/R/tm_a_pca.R +++ b/R/tm_a_pca.R @@ -150,14 +150,7 @@ ui_a_pca <- function(id, ...) { include_css_files("custom"), teal.widgets::standard_layout( output = teal.widgets::white_small_well( - tags$div( - class = "overflow-scroll", - uiOutput(ns("tbl_importance_ui")), - hr(), - uiOutput(ns("tbl_eigenvector_ui")), - hr(), - teal.widgets::plot_with_settings_ui(id = ns("pca_plot")) - ) + uiOutput(ns("all_plots")) ), encoding = div( ### Reporter @@ -269,10 +262,63 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl ) } - anl_merged_input <- teal.transform::merge_expression_module( + selector_list <- teal.transform::data_extract_multiple_srv( + data_extract = list(dat = dat, response = response), + datasets = data, + select_validation_rule = list( + dat = ~ if (length(.) < 2L) "Please select more than 1 variable to perform PCA.", + response = shinyvalidate::compose_rules( + shinyvalidate::sv_optional(), + ~ if (isTRUE(is.element(., selector_list()$dat()$select))) + "Response must not have been used for PCA." + ) + ) + ) + + iv_r <- reactive({ + iv <- shinyvalidate::InputValidator$new() + teal.transform::compose_and_enable_validators(iv, selector_list) + }) + + iv_extra <- shinyvalidate::InputValidator$new() + iv_extra$add_rule("x_axis", function(value) { + if (isTRUE(input$plot_type %in% c("Circle plot", "Biplot"))) { + if (!shinyvalidate::input_provided(value)) + "Need X axis" + } + }) + iv_extra$add_rule("y_axis", function(value) { + if (isTRUE(input$plot_type %in% c("Circle plot", "Biplot"))) { + if (!shinyvalidate::input_provided(value)) + "Need Y axis" + } + }) + rule_dupl <- function(...) { + if (isTRUE(input$plot_type %in% c("Circle plot", "Biplot"))) { + if (isTRUE(input$x_axis == input$y_axis)) + "Please choose different X and Y axes." + } + } + iv_extra$add_rule("x_axis", rule_dupl) + iv_extra$add_rule("y_axis", rule_dupl) + iv_extra$add_rule("variables", function(value) { + if (identical(input$plot_type, "Circle plot")) { + if (!shinyvalidate::input_provided(value)) + "Need Original Coordinates" + } + }) + iv_extra$add_rule("pc", function(value) { + if (identical(input$plot_type, "Eigenvector plot")) { + if (!shinyvalidate::input_provided(value)) + "Need PC" + } + }) + iv_extra$enable() + + anl_merged_input <- teal.transform::merge_expression_srv( + selector_list = selector_list, datasets = data, - join_keys = get_join_keys(data), - data_extract = list(dat = dat, response = response) + join_keys = get_join_keys(data) ) anl_merged_q <- reactive({ @@ -286,8 +332,7 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl anl_q_r = anl_merged_q ) - # computation ---- - computation <- reactive({ + validation <- reactive({ req(merged$anl_q_r()) # inputs keep_cols <- as.character(merged$anl_input_r()$columns_source$dat) @@ -297,21 +342,36 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl scale <- standardization == "center_scale" ANL <- merged$anl_q_r()[["ANL"]] # nolint - # inputs validation - validate(need(length(keep_cols) > 1, "Please select more than 1 variable to perform PCA.")) - teal::validate_has_elements(keep_cols, "Please select columns") - validate(need( - all(vapply(ANL[keep_cols], function(x) is.numeric(x) && all(!is.infinite(x)), logical(1))), - "PCA is only defined for (finite) numeric columns." - )) teal::validate_has_data(ANL, 10) validate(need( na_action != "none" | !anyNA(ANL[keep_cols]), paste( "There are NAs in the dataset. Please deal with them in preprocessing", - 'or select "Drop" in the NA actions inside the encodings panel (left).' + "or select \"Drop\" in the NA actions inside the encodings panel (left)." ) )) + if (scale) { + not_single <- vapply(ANL[keep_cols], function(column) length(unique(column)) != 1, FUN.VALUE = logical(1)) + + msg <- paste0( + "You have selected `Center & Scale` under `Standardization` in the `Pre-processing` panel, ", + "but one or more of your columns has/have a variance value of zero, indicating all values are identical" + ) + validate(need(all(not_single), msg)) + } + }) + + # computation ---- + computation <- reactive({ + validation() + + # inputs + keep_cols <- as.character(merged$anl_input_r()$columns_source$dat) + na_action <- input$na_action + standardization <- input$standardization + center <- standardization %in% c("center", "center_scale") # nolint + scale <- standardization == "center_scale" + ANL <- merged$anl_q_r()[["ANL"]] # nolint qenv <- teal.code::eval_code( merged$anl_q_r(), @@ -328,16 +388,6 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl ) } - if (scale) { - not_single <- vapply(ANL[keep_cols], function(column) length(unique(column)) != 1, FUN.VALUE = logical(1)) - - msg <- paste0( - "You have selected `Center & Scale` under `Standardization` in the `Pre-processing` panel, ", - "but one or more of your columns has/have a variance value of zero, indicating all values are identical" - ) - validate(need(all(not_single), msg)) - } - qenv <- teal.code::eval_code( qenv, substitute( @@ -366,6 +416,7 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl # plot args ---- output$plot_settings <- renderUI({ # reactivity triggers + req(iv_r()$is_valid()) req(computation()) qenv <- computation() @@ -405,7 +456,6 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl # plot elbow ---- plot_elbow <- function(base_q) { ggtheme <- input$ggtheme - validate(need(ggtheme, "Please select a theme.")) rotate_xaxis_labels <- input$rotate_xaxis_labels # nolint font_size <- input$font_size # nolint @@ -417,7 +467,7 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl theme = list( legend.position = "right", legend.spacing.y = quote(grid::unit(-5, "pt")), - legend.title = quote(element_text(vjust = 8)), + legend.title = quote(element_text(vjust = 25)), axis.text.x = substitute( element_text(angle = angle_value, hjust = hjust_value), list(angle_value = angle_value, hjust_value = hjust_value) @@ -481,19 +531,10 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl # plot circle ---- plot_circle <- function(base_q) { - validate( - need(input$x_axis, "Need additional plot settings - x axis"), - need(input$y_axis, "Need additional plot settings - y axis"), - need(input$variables, "Need additional plot settings - variables") - ) - validate(need(input$x_axis != input$y_axis, "Please choose different X and Y axes.")) - x_axis <- input$x_axis # nolint y_axis <- input$y_axis # nolint variables <- input$variables # nolint - ggtheme <- input$ggtheme - validate(need(ggtheme, "Please select a theme.")) rotate_xaxis_labels <- input$rotate_xaxis_labels # nolint font_size <- input$font_size # nolint @@ -563,12 +604,6 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl # plot biplot ---- plot_biplot <- function(base_q) { - validate( - need(input$x_axis, "Need additional plot settings - x axis"), - need(input$y_axis, "Need additional plot settings - y axis") - ) - validate(need(isTRUE(input$x_axis != input$y_axis), "Please choose different X and Y axes.")) - qenv <- base_q ANL <- qenv[["ANL"]] # nolint @@ -581,7 +616,6 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl pca <- qenv[["pca"]] ggtheme <- input$ggtheme - validate(need(ggtheme, "Please select a theme.")) rotate_xaxis_labels <- input$rotate_xaxis_labels # nolint alpha <- input$alpha # nolint @@ -602,8 +636,8 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl qenv, substitute( expr = { - r <- sqrt(qchisq(0.69, df = 2)) * prod(colMeans(pca_rot ^ 2)) ^ (1 / 4) # styler: off - v_scale <- rowSums(pca$rotation ^ 2) # styler: off + r <- sqrt(qchisq(0.69, df = 2)) * prod(colMeans(pca_rot ^ 2)) ^ (1 / 4) # styler: off + v_scale <- rowSums(pca$rotation ^ 2) # styler: off rot_vars <- pca$rotation[, c(x_axis, y_axis)] %>% dplyr::as_tibble(rownames = "label") %>% @@ -651,10 +685,6 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl ) dev_labs <- list() } else { - validate(need( - !resp_col %in% dat_cols, - "Response column must be different from the original variables (that were used for PCA)." - )) rp_keys <- setdiff( colnames(ANL), @@ -676,8 +706,8 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl dev_labs <- list(color = varname_w_label(resp_col, ANL)) scales_biplot <- if (is.character(response) || - is.factor(response) || - (is.numeric(response) && length(unique(response)) <= 6)) { + is.factor(response) || + (is.numeric(response) && length(unique(response)) <= 6)) { qenv <- teal.code::eval_code( qenv, quote(pca_rot$response <- as.factor(response)) @@ -792,12 +822,8 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl # plot pc_var ---- plot_pc_var <- function(base_q) { - validate(need(input$pc, "Need additional plot settings - PC")) - pc <- input$pc # nolint - ggtheme <- input$ggtheme - validate(need(ggtheme, "Please select a theme.")) rotate_xaxis_labels <- input$rotate_xaxis_labels # nolint font_size <- input$font_size # nolint @@ -831,9 +857,9 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl quote(ggplot(pca_rot)), substitute( geom_bar(aes_string(x = "Variable", y = pc), - stat = "identity", - color = "black", - fill = c(getOption("ggplot2.discrete.colour"), "lightblue")[1] + stat = "identity", + color = "black", + fill = c(getOption("ggplot2.discrete.colour"), "lightblue")[1] ), env = list(pc = pc) ), @@ -876,20 +902,22 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl # plot final ---- output_q <- reactive({ req(computation()) - if (input$plot_type == "Elbow plot") { - plot_elbow(computation()) - } else if (input$plot_type == "Circle plot") { - plot_circle(computation()) - } else if (input$plot_type == "Biplot") { - plot_biplot(computation()) - } else if (input$plot_type == "Eigenvector plot") { - plot_pc_var(computation()) - } else { - stop("Unknown plot") - } + # teal::validate_inputs_segregated(list("Some inputs require attention" = iv_r(), + # "Plot settings are required" = iv_extra)) + teal::validate_inputs(iv_r()) + teal::validate_inputs(iv_extra, header = "Plot settings are required") + + switch(input$plot_type, + "Elbow plot" = plot_elbow(computation()), + "Circle plot" = plot_circle(computation()), + "Biplot" = plot_biplot(computation()), + "Eigenvector plot" = plot_pc_var(computation()), + stop("Unknown plot")) }) - plot_r <- reactive(output_q()[["g"]]) + plot_r <- reactive({ + output_q()[["g"]] + }) pws <- teal.widgets::plot_with_settings_srv( id = "pca_plot", @@ -938,6 +966,23 @@ srv_a_pca <- function(id, data, reporter, filter_panel_api, dat, plot_height, pl ) }) + output$all_plots <- renderUI({ + # teal::validate_inputs_segregated(list("Some inputs require attention" = iv_r(), + # "Plot settings are required" = iv_extra)) + teal::validate_inputs(iv_r()) + teal::validate_inputs(iv_extra, header = "Plot settings are required") + + validation() + tags$div( + class = "overflow-scroll", + uiOutput(session$ns("tbl_importance_ui")), + hr(), + uiOutput(session$ns("tbl_eigenvector_ui")), + hr(), + teal.widgets::plot_with_settings_ui(id = session$ns("pca_plot")) + ) + }) + teal.widgets::verbatim_popup_srv( id = "warning", verbatim_content = reactive(teal.code::get_warnings(output_q())), diff --git a/R/tm_a_regression.R b/R/tm_a_regression.R index 8a9f30dff..4bd3e6ff7 100644 --- a/R/tm_a_regression.R +++ b/R/tm_a_regression.R @@ -178,30 +178,34 @@ ui_a_regression <- function(id, ...) { selected = args$plot_choices[args$default_plot_type] ), checkboxInput(ns("show_outlier"), label = "Display outlier labels", value = TRUE), - shinyjs::hidden(teal.widgets::optionalSliderInput( - ns("outlier"), - div( - class = "teal-tooltip", - tagList( - "Outlier definition:", - icon("circle-info"), - span( - class = "tooltiptext", - paste( - "Use the slider to choose the cut-off value to define outliers.", - "Points with a Cook's distance greater than", - "the value on the slider times the mean of the Cook's distance of the dataset will have labels." + conditionalPanel( + condition = "input['show_outlier']", + ns = ns, + teal.widgets::optionalSliderInput( + ns("outlier"), + div( + class = "teal-tooltip", + tagList( + "Outlier definition:", + icon("circle-info"), + span( + class = "tooltiptext", + paste( + "Use the slider to choose the cut-off value to define outliers.", + "Points with a Cook's distance greater than", + "the value on the slider times the mean of the Cook's distance of the dataset will have labels." + ) ) ) - ) + ), + min = 1, max = 10, value = 9, ticks = FALSE, step = .1 ), - min = 1, max = 10, value = 9, ticks = FALSE, step = .1 - )), - shinyjs::hidden(teal.widgets::optionalSelectInput( - ns("label_var"), - multiple = FALSE, - label = "Outlier label" - )), + teal.widgets::optionalSelectInput( + ns("label_var"), + multiple = FALSE, + label = "Outlier label" + ) + ), teal.widgets::panel_group( teal.widgets::panel_item( title = "Plot settings", @@ -241,20 +245,57 @@ srv_a_regression <- function(id, with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { - anl_merged_input <- teal.transform::merge_expression_module( + + rule_rvr1 <- function(value) { + if (isTRUE(input$plot_type == "Response vs Regressor")) { + if (length(value) > 1L) + "This plot can only have one regressor." + } + } + rule_rvr2 <- function(other) { + function(value) { + if (isTRUE(input$plot_type == "Response vs Regressor")) { + otherval <- selector_list()[[other]]()$select + if (isTRUE(value == otherval)) + "Response and Regressor must be different." + } + } + } + + selector_list <- teal.transform::data_extract_multiple_srv( + data_extract = list(response = response, regressor = regressor), datasets = data, - join_keys = get_join_keys(data), - data_extract = list(response = response, regressor = regressor) + select_validation_rule = list( + regressor = shinyvalidate::compose_rules( + shinyvalidate::sv_required("At least one regressor should be selected."), + rule_rvr1, + rule_rvr2("response") + ), + response = shinyvalidate::compose_rules( + shinyvalidate::sv_required("At least one response should be selected."), + rule_rvr2("regressor") + ) + ) + ) + + iv_r <- reactive({ + iv <- shinyvalidate::InputValidator$new() + teal.transform::compose_and_enable_validators(iv, selector_list) + }) + + iv_out <- shinyvalidate::InputValidator$new() + iv_out$condition(~ isTRUE(input$show_outlier)) + iv_out$add_rule("label_var", shinyvalidate::sv_required("Please provide an `Outlier label` variable")) + iv_out$enable() + + anl_merged_input <- teal.transform::merge_expression_srv( + selector_list = selector_list, + datasets = data, + join_keys = get_join_keys(data) ) regression_var <- reactive({ - validate( - need( - !is.null(anl_merged_input()$columns_source$response) && - !is.null(anl_merged_input()$columns_source$regressor), - "Please select regressor and response variables" - ) - ) + teal::validate_inputs(iv_r()) list( response = as.vector(anl_merged_input()$columns_source$response), @@ -273,35 +314,7 @@ srv_a_regression <- function(id, ANL <- anl_merged_q()[["ANL"]] # nolint teal::validate_has_data(ANL, 10) - # validation - validate( - need( - length(regression_var()$regressor) > 0, - "At least one regressor should be selected." - ) - ) - validate( - need( - length(regression_var()$response) == 1, - "Response variable should be of length one." - ) - ) validate(need(is.numeric(ANL[regression_var()$response][[1]]), "Response variable should be numeric.")) - validate( - need( - input$plot_type != "Response vs Regressor" || length(regression_var()$regressor) == 1, - paste0( - "Response vs Regressor plot is provided only for regressions with exactly one regressor.\n", - "Choose another plot or reduce number of regressors" - ) - ) - ) - validate( - need( - input$plot_type != "Response vs Regressor" || regression_var()$regressor != regression_var()$response, - "Response vs Regressor is only provided if regression and response variables are different" - ) - ) teal::validate_has_data( ANL[, c(regression_var()$response, regression_var()$regressor)], 10, @@ -364,7 +377,8 @@ srv_a_regression <- function(id, }) label_col <- reactive({ - validate(need(input$label_var, "`Display outlier labels` field is checked but `Outlier label` field is empty")) + teal::validate_inputs(iv_out) + substitute( expr = dplyr::if_else( data$.cooksd > outliers * mean(data$.cooksd, na.rm = TRUE), @@ -383,16 +397,6 @@ srv_a_regression <- function(id, ) }) - observeEvent(input$show_outlier, { - if (input$show_outlier) { - shinyjs::show("outlier") - shinyjs::show("label_var") - } else { - shinyjs::hide("outlier") - shinyjs::hide("label_var") - } - }) - output_q <- reactive({ alpha <- input$alpha # nolint size <- input$size # nolint @@ -400,7 +404,7 @@ srv_a_regression <- function(id, input_type <- input$plot_type show_outlier <- input$show_outlier - validate(need(!is.null(ggtheme), "Please select a theme.")) + teal::validate_inputs(iv_r()) plot_type_0 <- function() { fit <- fit_r()[["fit"]] @@ -819,12 +823,12 @@ srv_a_regression <- function(id, } else { plot_base_q <- plot_base() switch(input_type, - "Residuals vs Fitted" = plot_base_q %>% plot_type_1(), - "Normal Q-Q" = plot_base_q %>% plot_type_2(), - "Scale-Location" = plot_base_q %>% plot_type_3(), - "Cook's distance" = plot_base_q %>% plot_type_4(), - "Residuals vs Leverage" = plot_base_q %>% plot_type_5(), - "Cook's dist vs Leverage" = plot_base_q %>% plot_type_6() + "Residuals vs Fitted" = plot_base_q %>% plot_type_1(), + "Normal Q-Q" = plot_base_q %>% plot_type_2(), + "Scale-Location" = plot_base_q %>% plot_type_3(), + "Cook's distance" = plot_base_q %>% plot_type_4(), + "Residuals vs Leverage" = plot_base_q %>% plot_type_5(), + "Cook's dist vs Leverage" = plot_base_q %>% plot_type_6() ) } qenv @@ -843,6 +847,8 @@ srv_a_regression <- function(id, ) output$text <- renderText({ + req(iv_r()$is_valid()) + req(iv_out$is_valid()) paste(utils::capture.output(summary(fitted()))[-1], collapse = "\n") }) diff --git a/R/tm_data_table.R b/R/tm_data_table.R index a5914e683..1d1bdb550 100644 --- a/R/tm_data_table.R +++ b/R/tm_data_table.R @@ -250,16 +250,20 @@ srv_data_table <- function(id, dt_options, server_rendering) { moduleServer(id, function(input, output, session) { - output$data_table <- DT::renderDataTable(server = server_rendering, { - variables <- input$variables + iv <- shinyvalidate::InputValidator$new() + iv$add_rule("variables", shinyvalidate::sv_required("Please select valid variable names")) + iv$add_rule("variables", shinyvalidate::sv_in_set( + set = names(data[[dataname]]()), message_fmt = "Not all selected variables exist in the data" + )) + iv$enable() - validate(need(variables, "need valid variable names")) + output$data_table <- DT::renderDataTable(server = server_rendering, { + teal::validate_inputs(iv) df <- data[[dataname]]() + variables <- input$variables - validate(need(df, paste("data", dataname, "is empty"))) - - validate(need(all(variables %in% names(df)), "not all selected variables exist")) + teal::validate_has_data(df, min_nrow = 1L, msg = paste("data", dataname, "is empty")) dataframe_selected <- if (if_distinct()) { dplyr::count(df, dplyr::across(tidyselect::all_of(variables))) diff --git a/R/tm_g_association.R b/R/tm_g_association.R index 4f7e7273a..833679913 100644 --- a/R/tm_g_association.R +++ b/R/tm_g_association.R @@ -222,11 +222,29 @@ srv_tm_g_association <- function(id, with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { + selector_list <- teal.transform::data_extract_multiple_srv( data_extract = list(ref = ref, vars = vars), - datasets = data + datasets = data, + select_validation_rule = list( + ref = shinyvalidate::compose_rules( + shinyvalidate::sv_required("A reference variable needs to be selected."), + ~ if ((.) %in% selector_list()$vars()$select) + "Associated variables and reference variable cannot overlap" + ), + vars = shinyvalidate::compose_rules( + shinyvalidate::sv_required("An associated variable needs to be selected."), + ~ if (length(selector_list()$ref()$select) != 0 && selector_list()$ref()$select %in% (.)) + "Associated variables and reference variable cannot overlap" + ) + ) ) + iv_r <- reactive({ + iv <- shinyvalidate::InputValidator$new() + teal.transform::compose_and_enable_validators(iv, selector_list) + }) + anl_merged_input <- teal.transform::merge_expression_srv( datasets = data, selector_list = selector_list, @@ -245,12 +263,7 @@ srv_tm_g_association <- function(id, ) output_q <- reactive({ - validate({ - need( - !is.null(selector_list()$ref()) && !is.null(selector_list()$vars()), - "Please select reference and associated variables" - ) - }) + teal::validate_inputs(iv_r()) ANL <- merged$anl_q_r()[["ANL"]] # nolint teal::validate_has_data(ANL, 3) @@ -266,8 +279,6 @@ srv_tm_g_association <- function(id, distribution_theme <- input$distribution_theme association_theme <- input$association_theme - validate(need(ref_name, "need at least one variable selected")) - is_scatterplot <- is.numeric(ANL[[ref_name]]) && any(vapply(ANL[vars_names], is.numeric, logical(1))) if (is_scatterplot) { shinyjs::show("alpha") @@ -281,9 +292,6 @@ srv_tm_g_association <- function(id, size <- 2 } - validate(need(!(ref_name %in% vars_names), "associated variables and reference variable cannot overlap")) - validate(need(!is.null(distribution_theme) && !is.null(association_theme), "Please select a theme.")) - teal::validate_has_data(ANL[, c(ref_name, vars_names)], 3, complete = TRUE, allow_inf = FALSE) # reference @@ -370,26 +378,26 @@ srv_tm_g_association <- function(id, new_title <- if (association) { switch(as.character(length(vars_names)), - "0" = sprintf("Value distribution for %s", ref_cl_lbl), - "1" = sprintf( - "Association between %s and %s", - ref_cl_lbl, - format_varnames(vars_names) - ), - sprintf( - "Associations between %s and: %s", - ref_cl_lbl, - paste(lapply(vars_names, format_varnames), collapse = ", ") - ) + "0" = sprintf("Value distribution for %s", ref_cl_lbl), + "1" = sprintf( + "Association between %s and %s", + ref_cl_lbl, + format_varnames(vars_names) + ), + sprintf( + "Associations between %s and: %s", + ref_cl_lbl, + paste(lapply(vars_names, format_varnames), collapse = ", ") + ) ) } else { switch(as.character(length(vars_names)), - "0" = sprintf("Value distribution for %s", ref_cl_lbl), - sprintf( - "Value distributions for %s and %s", - ref_cl_lbl, - paste(lapply(vars_names, format_varnames), collapse = ", ") - ) + "0" = sprintf("Value distribution for %s", ref_cl_lbl), + sprintf( + "Value distributions for %s and %s", + ref_cl_lbl, + paste(lapply(vars_names, format_varnames), collapse = ", ") + ) ) } @@ -410,14 +418,17 @@ srv_tm_g_association <- function(id, }, env = list( plot_calls = do.call("call", c(list("list", ref_call), var_calls), - quote = TRUE + quote = TRUE ) ) ) ) }) - plot_r <- shiny::reactive(output_q()[["p"]]) + plot_r <- shiny::reactive({ + shiny::req(iv_r()$is_valid()) + output_q()[["p"]] + }) pws <- teal.widgets::plot_with_settings_srv( id = "myplot", diff --git a/R/tm_g_bivariate.R b/R/tm_g_bivariate.R index 1b7a8219f..b3c5086e4 100644 --- a/R/tm_g_bivariate.R +++ b/R/tm_g_bivariate.R @@ -74,6 +74,24 @@ #' fixed = FALSE #' ) #' ), +#' row_facet = data_extract_spec( +#' dataname = "ADSL", +#' select = select_spec( +#' label = "Select variable:", +#' choices = variable_choices(ADSL), +#' selected = "ARM", +#' fixed = FALSE +#' ) +#' ), +#' col_facet = data_extract_spec( +#' dataname = "ADSL", +#' select = select_spec( +#' label = "Select variable:", +#' choices = variable_choices(ADSL), +#' selected = "COUNTRY", +#' fixed = FALSE +#' ) +#' ), #' ggplot2_args = teal.widgets::ggplot2_args( #' labs = list(subtitle = "Plot generated by Bivariate Module") #' ) @@ -122,7 +140,13 @@ tm_g_bivariate <- function(label = "Bivariate Plots", stop("'y' should not allow multiple selection") } checkmate::assert_list(row_facet, types = "data_extract_spec", null.ok = TRUE) + if (!all(vapply(row_facet, function(x) !x$select$multiple, logical(1)))) { + stop("'row_facet' should not allow multiple selection") + } checkmate::assert_list(col_facet, types = "data_extract_spec", null.ok = TRUE) + if (!all(vapply(col_facet, function(x) !x$select$multiple, logical(1)))) { + stop("'col_facet' should not allow multiple selection") + } checkmate::assert_list(color, types = "data_extract_spec", null.ok = TRUE) if (!all(vapply(color, function(x) !x$select$multiple, logical(1)))) { stop("'color' should not allow multiple selection") @@ -363,7 +387,50 @@ srv_g_bivariate <- function(id, color = color, fill = fill, size = size ) - selector_list <- teal.transform::data_extract_multiple_srv(data_extract, data) + rule_var <- function(other) { + function(value) { + othervalue <- selector_list()[[other]]()$select + if (length(value) == 0L && length(othervalue) == 0L) + "Please select at least one of x-variable or y-variable" + } + } + rule_diff <- function(other) { + function(value) { + othervalue <- selector_list()[[other]]()[["select"]] + if (!is.null(othervalue)) { + if (identical(value, othervalue)) + "Row and column facetting variables must be different." + } + } + } + + selector_list <- teal.transform::data_extract_multiple_srv( + data_extract = data_extract, + datasets = data, + select_validation_rule = list( + x = rule_var("y"), + y = rule_var("x"), + row_facet = shinyvalidate::compose_rules( + shinyvalidate::sv_optional(), + rule_diff("col_facet") + ), + col_facet = shinyvalidate::compose_rules( + shinyvalidate::sv_optional(), + rule_diff("row_facet") + ) + ) + ) + + iv_r <- reactive({ + iv_facet <- shinyvalidate::InputValidator$new() + iv_child <- teal.transform::compose_and_enable_validators(iv_facet, selector_list, + validator_names = c("row_facet", "col_facet")) + iv_child$condition(~ isTRUE(input$facetting)) + + iv <- shinyvalidate::InputValidator$new() + iv$add_validator(iv_child) + teal.transform::compose_and_enable_validators(iv, selector_list, validator_names = c("x", "y")) + }) anl_merged_input <- teal.transform::merge_expression_srv( selector_list = selector_list, @@ -383,12 +450,7 @@ srv_g_bivariate <- function(id, ) output_q <- reactive({ - validate({ - need( - length(merged$anl_input_r()$columns_source$x) > 0 || length(merged$anl_input_r()$columns_source$y) > 0, - "x-variable and y-variable aren't correctly specified. At least one should be valid." - ) - }) + teal::validate_inputs(iv_r()) ANL <- merged$anl_q_r()[["ANL"]] # nolint teal::validate_has_data(ANL, 3) @@ -399,7 +461,7 @@ srv_g_bivariate <- function(id, y_name <- `if`(is.null(y_col_vec), character(0), y_col_vec) row_facet_name <- as.vector(merged$anl_input_r()$columns_source$row_facet) - col_facet_name <- as.vector(merged$anl_input_r()$col_facet) + col_facet_name <- as.vector(merged$anl_input_r()$columns_source$col_facet) color_name <- if ("color" %in% names(merged$anl_input_r()$columns_source)) { as.vector(merged$anl_input_r()$columns_source$color) } else { @@ -451,7 +513,6 @@ srv_g_bivariate <- function(id, teal::validate_has_data(ANL[, c(x_name, y_name)], 3, complete = TRUE, allow_inf = FALSE) - validate(need(!is.null(ggtheme), "Please select a theme.")) cl <- bivariate_plot_call( data_name = "ANL", @@ -717,8 +778,8 @@ bivariate_ggplot_call <- function(x_class = c("NULL", "numeric", "integer", "fac } else { plot_call <- reduce_plot_call( plot_call, - quote(geom_histogram(bins = 30, aes(y = ..density..))), - quote(geom_density(aes(y = ..density..))), + quote(geom_histogram(bins = 30, aes(y = after_stat(density)))), + quote(geom_density(aes(y = after_stat(density)))), quote(ylab("Density")) ) } @@ -734,8 +795,8 @@ bivariate_ggplot_call <- function(x_class = c("NULL", "numeric", "integer", "fac } else { plot_call <- reduce_plot_call( plot_call, - quote(geom_histogram(bins = 30, aes(y = ..density..))), - quote(geom_density(aes(y = ..density..))), + quote(geom_histogram(bins = 30, aes(y = after_stat(density)))), + quote(geom_density(aes(y = after_stat(density)))), quote(ylab("Density")) ) } @@ -751,7 +812,7 @@ bivariate_ggplot_call <- function(x_class = c("NULL", "numeric", "integer", "fac } else { plot_call <- reduce_plot_call( plot_call, - quote(geom_bar(aes(y = ..prop.., group = 1))), + quote(geom_bar(aes(y = after_stat(prop), group = 1))), quote(ylab("Fraction")) ) } @@ -767,7 +828,7 @@ bivariate_ggplot_call <- function(x_class = c("NULL", "numeric", "integer", "fac } else { plot_call <- reduce_plot_call( plot_call, - quote(geom_bar(aes(y = ..prop.., group = 1))), + quote(geom_bar(aes(y = after_stat(prop), group = 1))), quote(ylab("Fraction")) ) } @@ -891,37 +952,37 @@ coloring_ggplot_call <- function(colour, size, is_point = FALSE) { if (!identical(colour, character(0)) && !identical(fill, character(0)) && - is_point && !identical(size, character(0))) { + is_point && !identical(size, character(0))) { substitute( expr = aes(colour = colour_name, fill = fill_name, size = size_name), env = list(colour_name = as.name(colour), fill_name = as.name(fill), size_name = as.name(size)) ) } else if (identical(colour, character(0)) && !identical(fill, character(0)) && - is_point && identical(size, character(0))) { + is_point && identical(size, character(0))) { substitute(expr = aes(fill = fill_name), env = list(fill_name = as.name(fill))) } else if (!identical(colour, character(0)) && !identical(fill, character(0)) && - (!is_point || identical(size, character(0)))) { + (!is_point || identical(size, character(0)))) { substitute( expr = aes(colour = colour_name, fill = fill_name), env = list(colour_name = as.name(colour), fill_name = as.name(fill)) ) } else if (!identical(colour, character(0)) && identical(fill, character(0)) && - (!is_point || identical(size, character(0)))) { + (!is_point || identical(size, character(0)))) { substitute(expr = aes(colour = colour_name), env = list(colour_name = as.name(colour))) } else if (identical(colour, character(0)) && !identical(fill, character(0)) && - (!is_point || identical(size, character(0)))) { + (!is_point || identical(size, character(0)))) { substitute(expr = aes(fill = fill_name), env = list(fill_name = as.name(fill))) } else if (identical(colour, character(0)) && identical(fill, character(0)) && - is_point && !identical(size, character(0))) { + is_point && !identical(size, character(0))) { substitute(expr = aes(size = size_name), env = list(size_name = as.name(size))) } else if (!identical(colour, character(0)) && identical(fill, character(0)) && - is_point && !identical(size, character(0))) { + is_point && !identical(size, character(0))) { substitute( expr = aes(colour = colour_name, size = size_name), env = list(colour_name = as.name(colour), size_name = as.name(size)) ) } else if (identical(colour, character(0)) && !identical(fill, character(0)) && - is_point && !identical(size, character(0))) { + is_point && !identical(size, character(0))) { substitute( expr = aes(colour = colour_name, fill = fill_name, size = size_name), env = list(colour_name = as.name(fill), fill_name = as.name(fill), size_name = as.name(size)) diff --git a/R/tm_g_distribution.R b/R/tm_g_distribution.R index 8cd6f1580..7f7c981de 100644 --- a/R/tm_g_distribution.R +++ b/R/tm_g_distribution.R @@ -109,21 +109,14 @@ tm_g_distribution <- function(label = "Distribution Module", pre_output = NULL, post_output = NULL) { logger::log_info("Initializing tm_g_distribution") - if (!requireNamespace("ggpmisc", quietly = TRUE)) { - stop("Cannot load ggpmisc - please install the package or restart your session.") - } - if (!requireNamespace("ggpp", quietly = TRUE)) { - stop("Cannot load ggpp - please install the package or restart your session.") - } - if (!requireNamespace("goftest", quietly = TRUE)) { - stop("Cannot load goftest - please install the package or restart your session.") - } - if (!requireNamespace("MASS", quietly = TRUE)) { - stop("Cannot load MASS - please install the package or restart your session.") - } - if (!requireNamespace("broom", quietly = TRUE)) { - stop("Cannot load broom - please install the package or restart your session.") + + extra_packages <- c("ggpmisc", "ggpp", "goftest", "MASS", "broom") + missing_packages <- Filter(function(x) !requireNamespace(x, quietly = TRUE), extra_packages) + if (length(missing_packages) > 0L) { + stop(sprintf("Cannot load package(s): %s.\nInstall or restart your session.", + paste(missing_packages, sep = ", "))) } + if (inherits(dist_var, "data_extract_spec")) dist_var <- list(dist_var) if (inherits(strata_var, "data_extract_spec")) strata_var <- list(strata_var) if (inherits(group_var, "data_extract_spec")) group_var <- list(group_var) @@ -172,13 +165,6 @@ ui_distribution <- function(id, ...) { ns <- NS(id) is_single_dataset_value <- teal.transform::is_single_dataset(args$dist_var, args$strata_var, args$group_var) - scales_condition_base <- paste0( - "input['", - extract_input(ns("group_i"), args$group_var[[1]]$dataname, filter = TRUE), - "']" - ) - scales_condition <- paste0(scales_condition_base, " && ", paste0(scales_condition_base, ".length != 0")) - teal.widgets::standard_layout( output = tagList( tabsetPanel( @@ -211,17 +197,7 @@ ui_distribution <- function(id, ...) { data_extract_spec = args$group_var, is_single_dataset = is_single_dataset_value ), - conditionalPanel( - condition = scales_condition, - shinyWidgets::prettyRadioButtons( - ns("scales_type"), - label = "Scales:", - choices = c("Fixed", "Free"), - selected = "Fixed", - bigger = FALSE, - inline = TRUE - ) - ) + uiOutput(ns("scales_types_ui")) ) }, if (!is.null(args$strata_var)) { @@ -293,14 +269,14 @@ ui_distribution <- function(id, ...) { "Tests:", choices = c( "Shapiro-Wilk", - "t-test (two-samples, not paired)", - "one-way ANOVA", - "Fligner-Killeen", - "F-test", + if (!is.null(args$strata_var)) "t-test (two-samples, not paired)", + if (!is.null(args$strata_var)) "one-way ANOVA", + if (!is.null(args$strata_var)) "Fligner-Killeen", + if (!is.null(args$strata_var)) "F-test", "Kolmogorov-Smirnov (one-sample)", "Anderson-Darling (one-sample)", "Cramer-von Mises (one-sample)", - "Kolmogorov-Smirnov (two-samples)" + if (!is.null(args$strata_var)) "Kolmogorov-Smirnov (two-samples)" ), selected = NULL ) @@ -343,9 +319,101 @@ srv_distribution <- function(id, with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { - data_extract <- list(dist_i = dist_var, strata_i = strata_var, group_i = group_var) - selector_list <- teal.transform::data_extract_multiple_srv(data_extract, data) + rule_req <- function(value) { + if (isTRUE(input$dist_tests %in% c("Fligner-Killeen", + "t-test (two-samples, not paired)", + "F-test", + "Kolmogorov-Smirnov (two-samples)", + "one-way ANOVA"))) { + if (!shinyvalidate::input_provided(value)) + "Please select stratify variable." + } + } + rule_dupl <- function(...) { + if (identical(input$dist_tests, "Fligner-Killeen")) { + strata <- selector_list()$strata_i()$select + group <- selector_list()$group_i()$select + if (isTRUE(strata == group)) + "Please select different variables for strata and group." + } + } + + selector_list <- teal.transform::data_extract_multiple_srv( + data_extract = list( + dist_i = dist_var, + strata_i = strata_var, + group_i = group_var + ), + data, + select_validation_rule = list( + dist_i = shinyvalidate::sv_required("Please select a variable") + ), + filter_validation_rule = list( + strata_i = shinyvalidate::compose_rules( + rule_req, + rule_dupl + ), + group_i = rule_dupl + ) + ) + + iv_r <- reactive({ + iv <- shinyvalidate::InputValidator$new() + teal.transform::compose_and_enable_validators(iv, selector_list, validator_names = "dist_i") + }) + + iv_r_dist <- reactive({ + iv <- shinyvalidate::InputValidator$new() + teal.transform::compose_and_enable_validators( + iv, selector_list, validator_names = c("strata_i", "group_i")) + }) + rule_dist_1 <- function(value) { + if (!is.null(input$t_dist)) { + switch( + input$t_dist, + "normal" = if (!shinyvalidate::input_provided(value)) "mean is required", + "lognormal" = if (!shinyvalidate::input_provided(value)) "meanlog is required", + "gamma" = { + if (!shinyvalidate::input_provided(value)) "shape is required" else + if (value <= 0) "shape must be positive" + }, + "unif" = NULL) + } + } + rule_dist_2 <- function(value) { + if (!is.null(input$t_dist)) { + switch( + input$t_dist, + "normal" = { + if (!shinyvalidate::input_provided(value)) "sd is required" else + if (value < 0) "sd must be non-negative" + }, + "lognormal" = { + if (!shinyvalidate::input_provided(value)) "sdlog is required" else + if (value < 0) "sdlog must be non-negative" + }, + "gamma" = { + if (!shinyvalidate::input_provided(value)) "rate is required" else + if (value <= 0) "rate must be positive" + }, + "unif" = NULL) + } + } + rule_dist <- function(value) { + if (isTRUE(input$tabs == "QQplot" || + input$dist_tests %in% c("Kolmogorov-Smirnov (one-sample)", + "Anderson-Darling (one-sample)", + "Cramer-von Mises (one-sample)"))) { + if (!shinyvalidate::input_provided(value)) + "Please select the theoretical distribution." + } + } + iv_dist <- shinyvalidate::InputValidator$new() + iv_dist$add_rule("t_dist", rule_dist) + iv_dist$add_rule("dist_param1", rule_dist_1) + iv_dist$add_rule("dist_param2", rule_dist_2) + iv_dist$enable() anl_merged_input <- teal.transform::merge_expression_srv( selector_list = selector_list, @@ -364,11 +432,24 @@ srv_distribution <- function(id, anl_q_r = anl_merged_q ) + output$scales_types_ui <- renderUI({ + if ("group_i" %in% names(selector_list()) && length(selector_list()$group_i()$filters[[1]]$selected) > 0) { + shinyWidgets::prettyRadioButtons( + session$ns("scales_type"), + label = "Scales:", + choices = c("Fixed", "Free"), + selected = "Fixed", + bigger = FALSE, + inline = TRUE + ) + } + }) + observeEvent( eventExpr = list( input$t_dist, input$params_reset, - input[[extract_input("dist_i", dist_var[[1]]$dataname)]] + selector_list()$dist_i()$select ), handlerExpr = { if (length(input$t_dist) != 0) { @@ -402,6 +483,8 @@ srv_distribution <- function(id, ) merge_vars <- reactive({ + teal::validate_inputs(iv_r()) + dist_var <- as.vector(merged$anl_input_r()$columns_source$dist_i) s_var <- as.vector(merged$anl_input_r()$columns_source$strata_i) g_var <- as.vector(merged$anl_input_r()$columns_source$group_i) @@ -423,9 +506,6 @@ srv_distribution <- function(id, # common qenv common_q <- reactive({ # Create a private stack for this function only. - validate({ - need(length(merged$anl_input_r()$columns_source$dist_i) > 0, "Please select a variable") - }) ANL <- merged$anl_q_r()[["ANL"]] # nolint dist_var <- merge_vars()$dist_var @@ -480,17 +560,13 @@ srv_distribution <- function(id, teal::validate_has_data(ANL, 1, complete = TRUE) if (length(t_dist) != 0) { - map_distr_nams <- data.frame( - distr = c("normal", "lognormal", "gamma", "unif"), - namparam = I(list( - c("mean", "sd"), - c("meanlog", "sdlog"), - c("shape", "rate"), - c("min", "max") - )), - stringsAsFactors = FALSE + map_distr_nams <- list( + normal = c("mean", "sd"), + lognormal = c("meanlog", "sdlog"), + gamma = c("shape", "rate"), + unif = c("min", "max") ) - params_names_raw <- map_distr_nams$namparam[match(t_dist, map_distr_nams$distr)][[1]] + params_names_raw <- map_distr_nams[[t_dist]] qenv <- teal.code::eval_code( qenv, @@ -569,9 +645,6 @@ srv_distribution <- function(id, is.null(input$ggtheme) }, valueExpr = { - ANL <- common_q()[["ANL"]] # nolint - summary_table <- common_q()[["summary_table"]] # nolint - dist_var <- merge_vars()$dist_var s_var <- merge_vars()$s_var g_var <- merge_vars()$g_var @@ -583,52 +656,50 @@ srv_distribution <- function(id, dist_param2 <- input$dist_param2 scales_type <- input$scales_type + ndensity <- 512 main_type_var <- input$main_type bins_var <- input$bins add_dens_var <- input$add_dens ggtheme <- input$ggtheme - validate(need(ggtheme, "Please select a theme.")) + teal::validate_inputs(iv_dist) qenv <- common_q() - m_type <- if (main_type_var == "Density") "..density.." else "..count.." - m_type2 <- if (main_type_var == "Density") { - "..density.." - } else { - paste(diff(range(ANL[[dist_var]], na.rm = TRUE)) / bins_var, "* ..count..") - } + m_type <- if (main_type_var == "Density") "density" else "count" plot_call <- if (length(s_var) == 0 && length(g_var) == 0) { substitute( expr = ggplot(ANL, aes(dist_var_name)) + - geom_histogram(position = "identity", aes_string(y = m_type), bins = bins_var, alpha = 0.3), + geom_histogram( + position = "identity", aes(y = after_stat(m_type)), bins = bins_var, alpha = 0.3), env = list( - m_type = m_type, - bins_var = bins_var, - dist_var_name = as.name(dist_var) + m_type = as.name(m_type), bins_var = bins_var, dist_var_name = as.name(dist_var) ) ) } else if (length(s_var) != 0 && length(g_var) == 0) { substitute( expr = ggplot(ANL, aes(dist_var_name, col = s_var_name)) + - geom_histogram(position = "identity", aes_string(y = m_type, fill = s_var), bins = bins_var, alpha = 0.3), + geom_histogram( + position = "identity", aes(y = after_stat(m_type), fill = s_var), bins = bins_var, alpha = 0.3), env = list( - m_type = m_type, + m_type = as.name(m_type), bins_var = bins_var, dist_var_name = dist_var_name, - s_var = s_var, + s_var = as.name(s_var), s_var_name = s_var_name ) ) } else if (length(s_var) == 0 && length(g_var) != 0) { + req(scales_type) substitute( expr = ggplot(ANL[ANL[[g_var]] != "NA", ], aes(dist_var_name)) + - geom_histogram(position = "identity", aes_string(y = m_type), bins = bins_var, alpha = 0.3) + + geom_histogram( + position = "identity", aes(y = after_stat(m_type)), bins = bins_var, alpha = 0.3) + facet_wrap(~g_var_name, ncol = 1, scales = scales_raw), env = list( - m_type = m_type, + m_type = as.name(m_type), bins_var = bins_var, dist_var_name = dist_var_name, g_var = g_var, @@ -637,19 +708,20 @@ srv_distribution <- function(id, ) ) } else { + req(scales_type) substitute( expr = ggplot(ANL[ANL[[g_var]] != "NA", ], aes(dist_var_name, col = s_var_name)) + geom_histogram( position = "identity", - aes_string(y = m_type, fill = s_var), bins = bins_var, alpha = 0.3 + aes(y = after_stat(m_type), fill = s_var), bins = bins_var, alpha = 0.3 ) + facet_wrap(~g_var_name, ncol = 1, scales = scales_raw), env = list( - m_type = m_type, + m_type = as.name(m_type), bins_var = bins_var, dist_var_name = dist_var_name, g_var = g_var, - s_var = s_var, + s_var = as.name(s_var), g_var_name = g_var_name, s_var_name = s_var_name, scales_raw = tolower(scales_type) @@ -661,7 +733,7 @@ srv_distribution <- function(id, plot_call <- substitute( expr = plot_call + stat_density( - aes_string(y = m_type2), + aes(y = after_stat(const * m_type2)), geom = "line", position = "identity", alpha = 0.5, @@ -670,13 +742,15 @@ srv_distribution <- function(id, ), env = list( plot_call = plot_call, - m_type2 = m_type2, + const = if (main_type_var == "Density") 1 else + diff(range(qenv[["ANL"]][[dist_var]], na.rm = TRUE)) / bins_var, + m_type2 = if (main_type_var == "Density") as.name("density") else as.name("count"), ndensity = ndensity ) ) } - if (length(t_dist) != 0 && m_type == "..density.." && length(g_var) == 0 && length(s_var) == 0) { + if (length(t_dist) != 0 && main_type_var == "Density" && length(g_var) == 0 && length(s_var) == 0) { qenv <- teal.code::eval_code( qenv, substitute( @@ -697,8 +771,8 @@ srv_distribution <- function(id, ) } - if (length(s_var) == 0 && length(g_var) == 0 && m_type == "..density.." && - length(t_dist) != 0 && m_type == "..density..") { + if (length(s_var) == 0 && length(g_var) == 0 && main_type_var == "Density" && + length(t_dist) != 0 && main_type_var == "Density") { map_dist <- stats::setNames( c("dnorm", "dlnorm", "dgamma", "dunif"), c("normal", "lognormal", "gamma", "unif") @@ -755,9 +829,6 @@ srv_distribution <- function(id, is.null(input$ggtheme) }, valueExpr = { - ANL <- common_q()[["ANL"]] # nolint - summary_table <- common_q()[["summary_table"]] - dist_var <- merge_vars()$dist_var s_var <- merge_vars()$s_var g_var <- merge_vars()$g_var @@ -771,26 +842,19 @@ srv_distribution <- function(id, scales_type <- input$scales_type ggtheme <- input$ggtheme - validate(need(ggtheme, "Please select a theme.")) - validate(need(t_dist, "Please select the theoretical distribution.")) - validate_dist_parameters(t_dist, dist_param1, dist_param2) + teal::validate_inputs(iv_r_dist(), iv_dist) qenv <- common_q() plot_call <- if (length(s_var) == 0 && length(g_var) == 0) { substitute( expr = ggplot(ANL, aes_string(sample = dist_var)), - env = list( - dist_var = dist_var - ) + env = list(dist_var = dist_var) ) } else if (length(s_var) != 0 && length(g_var) == 0) { substitute( expr = ggplot(ANL, aes_string(sample = dist_var, color = s_var)), - env = list( - dist_var = dist_var, - s_var = s_var - ) + env = list(dist_var = dist_var, s_var = s_var) ) } else if (length(s_var) == 0 && length(g_var) != 0) { substitute( @@ -860,10 +924,7 @@ srv_distribution <- function(id, plot_call <- substitute( expr = plot_call + stat_qq_line(distribution = mapped_dist, dparams = params), - env = list( - plot_call = plot_call, - mapped_dist = as.name(unname(map_dist[t_dist])) - ) + env = list(plot_call = plot_call, mapped_dist = as.name(unname(map_dist[t_dist]))) ) } @@ -919,6 +980,8 @@ srv_distribution <- function(id, validate(need(dist_tests, "Please select a test")) + teal::validate_inputs(iv_dist) + if (length(s_var) > 0 || length(g_var) > 0) { counts <- ANL %>% dplyr::group_by_at(dplyr::vars(dplyr::any_of(c(s_var, g_var)))) %>% @@ -927,21 +990,12 @@ srv_distribution <- function(id, validate(need(all(counts$n > 5), "Please select strata*group with at least 5 observation each.")) } + if (dist_tests %in% c( - "Kolmogorov-Smirnov (one-sample)", - "Anderson-Darling (one-sample)", - "Cramer-von Mises (one-sample)" - )) { - validate(need(t_dist, "Please select the theoretical distribution.")) - } else if (dist_tests == "Fligner-Killeen") { - validate(need(s_var, "Please select stratify variable.")) - validate(need(!identical(s_var, g_var), "Please select different variables for strata and group.")) - } else if (dist_tests %in% c( "t-test (two-samples, not paired)", "F-test", "Kolmogorov-Smirnov (two-samples)" )) { - validate(need(s_var, "Please select stratify variable.")) if (length(g_var) == 0 && length(s_var) > 0) { validate(need( length(unique(ANL[[s_var]])) == 2, @@ -950,14 +1004,12 @@ srv_distribution <- function(id, } if (length(g_var) > 0 && length(s_var) > 0) { validate(need( - all(stats::na.omit(as.vector(tapply( - ANL[[s_var]], list(ANL[[g_var]]), function(x) length(unique(x)) - ) == 2))), + all(stats::na.omit(as.vector( + tapply(ANL[[s_var]], list(ANL[[g_var]]), function(x) length(unique(x))) == 2)) + ), "Please select stratify variable with 2 levels, per each group." )) } - } else if (dist_tests == "one-way ANOVA") { - validate(need(s_var, "Please select stratify variable.")) } map_dist <- stats::setNames( @@ -1011,15 +1063,15 @@ srv_distribution <- function(id, ) tests_base <- switch(dist_tests, - "Kolmogorov-Smirnov (one-sample)" = sks_args, - "Shapiro-Wilk" = ssw_args, - "Fligner-Killeen" = mfil_args, - "one-way ANOVA" = manov_args, - "t-test (two-samples, not paired)" = mt_args, - "F-test" = mv_args, - "Kolmogorov-Smirnov (two-samples)" = mks_args, - "Anderson-Darling (one-sample)" = sad_args, - "Cramer-von Mises (one-sample)" = scvm_args + "Kolmogorov-Smirnov (one-sample)" = sks_args, + "Shapiro-Wilk" = ssw_args, + "Fligner-Killeen" = mfil_args, + "one-way ANOVA" = manov_args, + "t-test (two-samples, not paired)" = mt_args, + "F-test" = mv_args, + "Kolmogorov-Smirnov (two-samples)" = mks_args, + "Anderson-Darling (one-sample)" = sad_args, + "Cramer-von Mises (one-sample)" = scvm_args ) env <- list( @@ -1093,15 +1145,12 @@ srv_distribution <- function(id, qenv_final }) - dist_r <- reactive(dist_q()[["g"]]) qq_r <- reactive(qq_q()[["g"]]) - tests_r <- reactive(test_q()[["test_stats"]]) - output$summary_table <- DT::renderDataTable( - expr = common_q()[["summary_table"]], + expr = if (iv_r()$is_valid()) common_q()[["summary_table"]] else NULL, options = list( autoWidth = TRUE, columnDefs = list(list(width = "200px", targets = "_all")) @@ -1109,6 +1158,12 @@ srv_distribution <- function(id, rownames = FALSE ) + tests_r <- reactive({ + req(iv_r()$is_valid()) + teal::validate_inputs(iv_r_dist()) + test_q()[["test_stats"]] + }) + pws1 <- teal.widgets::plot_with_settings_srv( id = "hist_plot", plot_r = dist_r, @@ -1178,33 +1233,3 @@ srv_distribution <- function(id, ### }) } - -#' @description -#' Validates the parameters of the given theoretical distribution. -#' -#' @note Returns a Shiny validation error if the parameters don't meet -#' the assumptions of the theoretical distribution. -#' -#' @param dist_type (`character(1)`) the family of a distribution -#' @param dist_param1 (`numeric(1)`) the first parameter of the distribution -#' @param dist_param2 (`numeric(1)`) the second parameter of the distribution -#' @return NULL -#' @noRd -validate_dist_parameters <- function(dist_type, dist_param1, dist_param2) { - switch(dist_type, - "normal" = { - validate(need(dist_param2 >= 0, "Variance of the normal distribution needs to be nonnegative")) - }, - "lognormal" = { - validate(need(dist_param2 >= 0, "Sigma parameter of the log-normal distribution needs to be nonnegative")) - }, - "gamma" = { - validate(need( - dist_param1 > 0 && dist_param2 > 0, - "k and theta parameters of the gamma distribution need to be positive" - )) - }, - "unif" = NULL - ) - NULL -} diff --git a/R/tm_g_response.R b/R/tm_g_response.R index 63a33e991..61cd3dc40 100644 --- a/R/tm_g_response.R +++ b/R/tm_g_response.R @@ -234,7 +234,40 @@ srv_g_response <- function(id, moduleServer(id, function(input, output, session) { data_extract <- list(response = response, x = x, row_facet = row_facet, col_facet = col_facet) - selector_list <- teal.transform::data_extract_multiple_srv(data_extract, data) + rule_diff <- function(other) { + function(value) { + othervalue <- selector_list()[[other]]()[["select"]] + if (!is.null(othervalue)) { + if (identical(value, othervalue)) + "Row and column facetting variables must be different." + } + } + } + + selector_list <- teal.transform::data_extract_multiple_srv( + data_extract = data_extract, + datasets = data, + select_validation_rule = list( + response = shinyvalidate::sv_required("Please define a column for the response variable"), + x = shinyvalidate::sv_required("Please define a column for X variable"), + row_facet = shinyvalidate::compose_rules( + shinyvalidate::sv_optional(), + ~ if (length(.) > 1) "There must be 1 or no row facetting variable.", + rule_diff("col_facet") + ), + col_facet = shinyvalidate::compose_rules( + shinyvalidate::sv_optional(), + ~ if (length(.) > 1) "There must be 1 or no column facetting variable.", + rule_diff("row_facet") + ) + ) + ) + + iv_r <- reactive({ + iv <- shinyvalidate::InputValidator$new() + iv$add_rule("ggtheme", shinyvalidate::sv_required("Please select a theme")) + teal.transform::compose_and_enable_validators(iv, selector_list) + }) anl_merged_input <- teal.transform::merge_expression_srv( selector_list = selector_list, @@ -254,15 +287,13 @@ srv_g_response <- function(id, ) output_q <- reactive({ + teal::validate_inputs(iv_r()) + qenv <- merged$anl_q_r() ANL <- qenv[["ANL"]] # nolint resp_var <- as.vector(merged$anl_input_r()$columns_source$response) x <- as.vector(merged$anl_input_r()$columns_source$x) - validate(need(!identical(resp_var, character(0)), "Please define a valid column for the response variable")) - validate(need(!identical(x, character(0)), "Please define a valid column for the X-variable")) - validate(need(length(resp_var) == 1, "Please define a column for Response variable")) - validate(need(length(x) == 1, "Please define a column for X variable")) validate(need(is.factor(ANL[[resp_var]]), "Please select a factor variable as the response.")) validate(need(is.factor(ANL[[x]]), "Please select a factor variable as the X-Variable.")) teal::validate_has_data(ANL, 10) @@ -285,8 +316,6 @@ srv_g_response <- function(id, rotate_xaxis_labels <- input$rotate_xaxis_labels ggtheme <- input$ggtheme - validate(need(!is.null(ggtheme), "Please select a theme.")) - arg_position <- if (freq) "stack" else "fill" # nolint rowf <- if (length(row_facet_name) == 0) NULL else as.name(row_facet_name) # nolint @@ -336,7 +365,7 @@ srv_g_response <- function(id, plot_call <- substitute( expr = ggplot(ANL2, aes(x = x_cl, y = ns)) + - geom_bar(aes(fill = resp_cl), stat = "identity", position = arg_position), + geom_bar(aes(fill = resp_cl), stat = "identity", position = arg_position), env = list( x_cl = x_cl, resp_cl = resp_cl, diff --git a/R/tm_g_scatterplot.R b/R/tm_g_scatterplot.R index f31f93238..8dbb5db98 100644 --- a/R/tm_g_scatterplot.R +++ b/R/tm_g_scatterplot.R @@ -93,7 +93,7 @@ #' dataname = "ADSL", #' select = select_spec( #' label = "Select variable:", -#' choices = variable_choices(ADSL, c("AGE", "BMRKR1", "BMRKR2", "RACE", "REGION1")), +#' choices = variable_choices(ADSL, c("BMRKR2", "RACE", "REGION1")), #' selected = NULL, #' multiple = FALSE, #' fixed = FALSE @@ -103,7 +103,7 @@ #' dataname = "ADSL", #' select = select_spec( #' label = "Select variable:", -#' choices = variable_choices(ADSL, c("AGE", "BMRKR1", "BMRKR2", "RACE", "REGION1")), +#' choices = variable_choices(ADSL, c("BMRKR2", "RACE", "REGION1")), #' selected = NULL, #' multiple = FALSE, #' fixed = FALSE @@ -141,15 +141,14 @@ tm_g_scatterplot <- function(label = "Scatterplot", table_dec = 4, ggplot2_args = teal.widgets::ggplot2_args()) { logger::log_info("Initializing tm_g_scatterplot") - if (!requireNamespace("ggpmisc", quietly = TRUE)) { - stop("Cannot load ggpmisc - please install the package or restart your session.") - } - if (!requireNamespace("ggExtra", quietly = TRUE)) { - stop("Cannot load ggExtra - please install the package or restart your session.") - } - if (!requireNamespace("colourpicker", quietly = TRUE)) { - stop("Cannot load colourpicker - please install the package or restart your session.") + + extra_packages <- c("ggpmisc", "ggExtra", "colourpicker") + missing_packages <- Filter(function(x) !requireNamespace(x, quietly = TRUE), extra_packages) + if (length(missing_packages) > 0L) { + stop(sprintf("Cannot load package(s): %s.\nInstall or restart your session.", + paste(missing_packages, sep = ", "))) } + if (inherits(x, "data_extract_spec")) x <- list(x) if (inherits(y, "data_extract_spec")) y <- list(y) if (inherits(color_by, "data_extract_spec")) color_by <- list(color_by) @@ -166,6 +165,13 @@ tm_g_scatterplot <- function(label = "Scatterplot", checkmate::assert_list(size_by, types = "data_extract_spec", null.ok = TRUE) checkmate::assert_list(row_facet, types = "data_extract_spec", null.ok = TRUE) checkmate::assert_list(col_facet, types = "data_extract_spec", null.ok = TRUE) + checkmate::assert_list(row_facet, types = "data_extract_spec", null.ok = TRUE) + if (!all(vapply(row_facet, function(x) !x$select$multiple, logical(1)))) { + stop("'row_facet' should not allow multiple selection") + } + if (!all(vapply(col_facet, function(x) !x$select$multiple, logical(1)))) { + stop("'col_facet' should not allow multiple selection") + } checkmate::assert_character(shape) checkmate::assert_int(max_deg, lower = 1L) @@ -389,9 +395,54 @@ srv_g_scatterplot <- function(id, checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { data_extract <- list( - x = x, y = y, color_by = color_by, size_by = size_by, row_facet = row_facet, col_facet = col_facet + x = x, + y = y, + color_by = color_by, + size_by = size_by, + row_facet = row_facet, + col_facet = col_facet ) - selector_list <- teal.transform::data_extract_multiple_srv(data_extract, data) + + rule_diff <- function(other) { + function(value) { + othervalue <- selector_list()[[other]]()[["select"]] + if (!is.null(othervalue)) { + if (identical(value, othervalue)) + "Row and column facetting variables must be different." + } + } + } + + selector_list <- teal.transform::data_extract_multiple_srv( + data_extract = data_extract, + datasets = data, + select_validation_rule = list( + x = ~ if (length(.) != 1) "Please select exactly one x var.", + y = ~ if (length(.) != 1) "Please select exactly one y var.", + color_by = ~ if (length(.) > 1) "There cannot be more than 1 color variable.", + size_by = ~ if (length(.) > 1) "There cannot be more than 1 size variable.", + row_facet = shinyvalidate::compose_rules( + shinyvalidate::sv_optional(), + rule_diff("col_facet") + ), + col_facet = shinyvalidate::compose_rules( + shinyvalidate::sv_optional(), + rule_diff("row_facet") + ) + ) + ) + + iv_r <- reactive({ + iv_facet <- shinyvalidate::InputValidator$new() + iv <- shinyvalidate::InputValidator$new() + teal.transform::compose_and_enable_validators(iv, selector_list) + }) + iv_facet <- shinyvalidate::InputValidator$new() + iv_facet$add_rule("add_density", ~ if (isTRUE(.) && + (length(selector_list()$row_facet()$select) > 0L || + length(selector_list()$col_facet()$select) > 0L)) + "Cannot add marginal density when Row or Column facetting has been selected") + iv_facet$enable() anl_merged_input <- teal.transform::merge_expression_srv( selector_list = selector_list, @@ -448,12 +499,11 @@ srv_g_scatterplot <- function(id, } }) - observeEvent( eventExpr = merged$anl_input_r()$columns_source[c("col_facet", "row_facet")], handlerExpr = { if (length(merged$anl_input_r()$columns_source$col_facet) == 0 && - length(merged$anl_input_r()$columns_source$row_facet) == 0) { + length(merged$anl_input_r()$columns_source$row_facet) == 0) { shinyjs::hide("free_scales") } else { shinyjs::show("free_scales") @@ -462,6 +512,8 @@ srv_g_scatterplot <- function(id, ) output_q <- reactive({ + teal::validate_inputs(iv_r(), iv_facet) + ANL <- merged$anl_q_r()[["ANL"]] # nolint x_var <- as.vector(merged$anl_input_r()$columns_source$x) @@ -492,16 +544,6 @@ srv_g_scatterplot <- function(id, log_x <- input$log_x log_y <- input$log_y - validate(need(!is.null(ggtheme), "Please select a theme.")) - validate(need(length(x_var) == 1, "There must be exactly one x var.")) - validate(need(length(y_var) == 1, "There must be exactly one y var.")) - validate(need(is.null(color_by_var) || length(color_by_var) <= 1, "There must be 1 or no color variable.")) - validate(need(is.null(size_by_var) || length(size_by_var) <= 1, "There must be 1 or no size variable.")) - validate(need(length(row_facet_name) <= 1, "There must be 1 or no row facetting variable.")) - validate(need(length(col_facet_name) <= 1, "There must be 1 or no column facetting variable.")) - if (length(row_facet_name) * length(col_facet_name) > 0) { - validate(need(row_facet_name != col_facet_name, "Row and column facetting variables must be different.")) - } validate(need( length(row_facet_name) == 0 || inherits(ANL[[row_facet_name]], c("character", "factor", "Date", "integer")), "`Row facetting` variable must be of class `character`, `factor`, `Date`, or `integer`" @@ -510,6 +552,7 @@ srv_g_scatterplot <- function(id, length(col_facet_name) == 0 || inherits(ANL[[col_facet_name]], c("character", "factor", "Date", "integer")), "`Column facetting` variable must be of class `character`, `factor`, `Date`, or `integer`" )) + if (add_density && length(color_by_var) > 0) { validate(need( !is.numeric(ANL[[color_by_var]]), @@ -518,8 +561,8 @@ srv_g_scatterplot <- function(id, )) validate(need( !(inherits(ANL[[color_by_var]], "Date") || - inherits(ANL[[color_by_var]], "POSIXct") || - inherits(ANL[[color_by_var]], "POSIXlt")), + inherits(ANL[[color_by_var]], "POSIXct") || + inherits(ANL[[color_by_var]], "POSIXlt")), "Marginal plots cannot be produced when the points are colored by Date or POSIX variables. \n Uncheck the 'Add marginal density' checkbox to display the plot." )) @@ -554,13 +597,6 @@ srv_g_scatterplot <- function(id, free_x_scales = isTRUE(input$free_scales), free_y_scales = isTRUE(input$free_scales) ) - if (!is.null(facet_cl)) { - validate(need( - !add_density, - "Marginal density is not supported when faceting is used. Please uncheck `Add marginal density` - or remove facetting." - )) - } point_sizes <- if (length(size_by_var) > 0) { validate(need(is.numeric(ANL[[size_by_var]]), "Variable to size by must be numeric")) @@ -864,8 +900,8 @@ srv_g_scatterplot <- function(id, if (length(numeric_cols) > 0) { DT::formatRound( DT::datatable(brushed_df, - rownames = FALSE, - options = list(scrollX = TRUE, pageLength = input$data_table_rows) + rownames = FALSE, + options = list(scrollX = TRUE, pageLength = input$data_table_rows) ), numeric_cols, table_dec diff --git a/R/tm_g_scatterplotmatrix.R b/R/tm_g_scatterplotmatrix.R index 5c9c1918d..71724e520 100644 --- a/R/tm_g_scatterplotmatrix.R +++ b/R/tm_g_scatterplotmatrix.R @@ -166,9 +166,17 @@ srv_g_scatterplotmatrix <- function(id, data, reporter, filter_panel_api, variab moduleServer(id, function(input, output, session) { selector_list <- teal.transform::data_extract_multiple_srv( data_extract = list(variables = variables), - datasets = data + datasets = data, + select_validation_rule = list( + variables = ~ if (length(.) <= 1) "Please select at least 2 columns." + ) ) + iv_r <- reactive({ + iv <- shinyvalidate::InputValidator$new() + teal.transform::compose_and_enable_validators(iv, selector_list) + }) + anl_merged_input <- teal.transform::merge_expression_srv( datasets = data, join_keys = get_join_keys(data), @@ -188,6 +196,8 @@ srv_g_scatterplotmatrix <- function(id, data, reporter, filter_panel_api, variab # plot output_q <- reactive({ + teal::validate_inputs(iv_r()) + qenv <- merged$anl_q_r() ANL <- qenv[["ANL"]] # nolint @@ -204,7 +214,6 @@ srv_g_scatterplotmatrix <- function(id, data, reporter, filter_panel_api, variab "na.fail" } - validate(need(length(cols_names) > 1, "Need at least 2 columns.")) teal::validate_has_data(ANL, 10) teal::validate_has_data(ANL[, cols_names], 10, complete = TRUE, allow_inf = FALSE) @@ -310,6 +319,7 @@ srv_g_scatterplotmatrix <- function(id, data, reporter, filter_panel_api, variab # show a message if conversion to factors took place output$message <- renderText({ + shiny::req(iv_r()$is_valid()) req(selector_list()$variables()) ANL <- merged$anl_q_r()[["ANL"]] # nolint cols_names <- unique(unname(do.call(c, merged$anl_input_r()$columns_source))) diff --git a/R/tm_missing_data.R b/R/tm_missing_data.R index e14094c24..e462da19f 100644 --- a/R/tm_missing_data.R +++ b/R/tm_missing_data.R @@ -348,11 +348,44 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { prev_group_by_var <- reactiveVal("") - data_r <- data[[dataname]] - data_keys <- reactive(get_join_keys(data)$get(dataname)[[dataname]]) + iv_r <- reactive({ + iv <- shinyvalidate::InputValidator$new() + iv$add_rule( + "variables_select", + shinyvalidate::sv_required("At least one reference variable needs to be selected.") + ) + iv$add_rule( + "variables_select", + ~ if (length(setdiff((.), data_keys())) < 1) "Please also select non-key columns." + ) + iv_summary_table <- shinyvalidate::InputValidator$new() + iv_summary_table$condition(~ isTRUE(input$summary_type == "By Variable Levels")) + iv_summary_table$add_rule("count_type", shinyvalidate::sv_required("Please select type of counts")) + iv_summary_table$add_rule( + "group_by_vals", + shinyvalidate::sv_required("Please select both group-by variable and values") + ) + iv_summary_table$add_rule( + "group_by_var", + ~ if (length(.) > 0 && length(input$variables_select) == 1 && (.) == input$variables_select) { + "If only one reference variable is selected it must not be the grouping variable." + } + ) + iv_summary_table$add_rule( + "variables_select", + ~ if (length(input$group_by_var) > 0 && length(.) == 1 && (.) == input$group_by_var) { + "If only one reference variable is selected it must not be the grouping variable." + } + ) + iv$add_validator(iv_summary_table) + iv$enable() + iv + }) + + data_parent_keys <- reactive({ if (length(parent_dataname) > 0 && parent_dataname %in% names(data)) { keys <- get_join_keys(data)$get(dataname) @@ -367,6 +400,8 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par }) common_code_q <- reactive({ + teal::validate_inputs(iv_r()) + group_var <- input$group_by_var anl <- data_r() qenv <- teal.code::new_qenv(tdata2env(data), code = get_code_tdata(data)) @@ -415,7 +450,7 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par env = list( new_col_name = new_col_name, column_labels_value = c(var_labels(data_r())[selected_vars()], - new_col_name = new_col_name + new_col_name = new_col_name ) ) ) @@ -427,7 +462,6 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par req(input$variables_select) keys <- data_keys() vars <- unique(c(keys, input$variables_select)) - validate(need(length(setdiff(vars, keys)) >= 1, "Please also select non-key columns.")) vars }) @@ -506,8 +540,8 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par selected <- if (!is.null(prev_choices) && any(prev_choices %in% choices)) { prev_choices[match(choices[choices %in% prev_choices], prev_choices)] } else if (!is.null(prev_choices) && - !any(prev_choices %in% choices) && - isolate(prev_group_by_var()) == input$group_by_var) { + !any(prev_choices %in% choices) && + isolate(prev_group_by_var()) == input$group_by_var) { # if not any previously selected value is available and the grouping variable is the same, # then display NULL NULL @@ -533,7 +567,6 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par summary_plot_q <- reactive({ req(input$summary_type == "Summary") # needed to trigger show r code update on tab change teal::validate_has_data(data_r(), 1) - validate(need(length(input$variables_select) > 0, "No variables selected")) qenv <- common_code_q() @@ -776,7 +809,6 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par }) combination_plot_q <- reactive({ - validate(need(length(input$variables_select) > 0, "No variables selected")) req(input$summary_type == "Combinations", input$combination_cutoff, combination_cutoff_q()) teal::validate_has_data(data_r(), 1) @@ -919,11 +951,6 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par # extract the ANL dataset for use in further validation anl <- common_code_q()[["ANL"]] - validate(need(input$count_type, "Please select type of counts")) - if (!is.null(input$group_by_var)) { - validate(need(!is.null(input$group_by_vals), "Please select both group-by variable and values")) - } - group_var <- input$group_by_var validate( need( @@ -953,11 +980,6 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par qenv <- common_code_q() if (!is.null(group_var)) { - validate(need( - length(variables_select) > 1 || variables_select != group_var, - "If only one variable is selected it must not be the grouping variable." - )) - qenv <- teal.code::eval_code( qenv, substitute( @@ -987,8 +1009,8 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par expr = summary_data <- ANL %>% dplyr::summarise_all(summ_fn) %>% tidyr::pivot_longer(tidyselect::everything(), - names_to = "Variable", - values_to = paste0("Missing (N=", nrow(ANL), ")") + names_to = "Variable", + values_to = paste0("Missing (N=", nrow(ANL), ")") ) %>% dplyr::mutate(`Variable label` = create_cols_labels(Variable), .after = Variable), env = list(summ_fn = summ_fn) @@ -1002,9 +1024,10 @@ srv_missing_data <- function(id, data, reporter, filter_panel_api, dataname, par summary_table_r <- reactive(summary_table_q()[["summary_data"]]) by_subject_plot_q <- reactive({ - req(input$summary_type == "Grouped by Subject") # needed to trigger show r code update on tab change + # needed to trigger show r code update on tab change + req(input$summary_type == "Grouped by Subject", common_code_q()) + teal::validate_has_data(data_r(), 1) - validate(need(length(input$variables_select) > 0, "No variables selected")) dev_ggplot2_args <- teal.widgets::ggplot2_args( labs = list(x = "", y = ""), diff --git a/R/tm_outliers.R b/R/tm_outliers.R index 9b22e24da..12e212e2e 100644 --- a/R/tm_outliers.R +++ b/R/tm_outliers.R @@ -79,6 +79,13 @@ tm_outliers <- function(label = "Outliers Module", checkmate::assert_string(label) checkmate::assert_list(outlier_var, types = "data_extract_spec") checkmate::assert_list(categorical_var, types = "data_extract_spec", null.ok = TRUE) + if (is.list(categorical_var)) { + lapply(categorical_var, function(x) { + if (length(x$filter) > 1L) { + stop("tm_outliers: categorical_var data_extract_specs may only specify one filter_spec", call. = FALSE) + } + }) + } plot_choices <- c("Boxplot", "Density Plot", "Cumulative Distribution Plot") checkmate::assert_list(ggplot2_args, types = "ggplot2_args") checkmate::assert_subset(names(ggplot2_args), c("default", plot_choices)) @@ -116,21 +123,18 @@ ui_outliers <- function(id, ...) { br(), hr(), tabsetPanel( id = ns("tabs"), - tabPanel("Boxplot", teal.widgets::plot_with_settings_ui(id = ns("box_plot"))), - tabPanel("Density Plot", teal.widgets::plot_with_settings_ui(id = ns("density_plot"))), - tabPanel("Cumulative Distribution Plot", teal.widgets::plot_with_settings_ui(id = ns("cum_density_plot"))) + tabPanel( + "Boxplot", + teal.widgets::plot_with_settings_ui(id = ns("box_plot"))), + tabPanel( + "Density Plot", + teal.widgets::plot_with_settings_ui(id = ns("density_plot"))), + tabPanel( + "Cumulative Distribution Plot", + teal.widgets::plot_with_settings_ui(id = ns("cum_density_plot"))) ), br(), hr(), - teal.widgets::optionalSelectInput( - inputId = ns("table_ui_columns"), - label = "Choose additional columns", - choices = NULL, - selected = NULL, - multiple = TRUE - ), - h4("Outlier Table"), - teal.widgets::get_dt_rows(ns("table_ui"), ns("table_ui_rows")), - DT::dataTableOutput(ns("table_ui")) + uiOutput(ns("table_ui_wrap")) ), encoding = div( ### Reporter @@ -241,7 +245,35 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { vars <- list(outlier_var = outlier_var, categorical_var = categorical_var) - selector_list <- teal.transform::data_extract_multiple_srv(vars, data) + + rule_diff <- function(other) { + function(value) { + othervalue <- selector_list()[[other]]()[["select"]] + if (!is.null(othervalue)) { + if (identical(othervalue, value)) + "`Variable` and `Categorical factor` cannot be the same" + } + } + } + + selector_list <- teal.transform::data_extract_multiple_srv( + data_extract = vars, + datasets = data, + select_validation_rule = list( + outlier_var = shinyvalidate::compose_rules( + shinyvalidate::sv_required("Please select a variable"), + rule_diff("categorical_var") + ), + categorical_var = rule_diff("outlier_var") + ) + ) + + iv_r <- reactive({ + iv <- shinyvalidate::InputValidator$new() + iv$add_rule("method", shinyvalidate::sv_required("Please select a method")) + iv$add_rule("boxplot_alts", shinyvalidate::sv_required("Please select Plot Type")) + teal.transform::compose_and_enable_validators(iv, selector_list) + }) reactive_select_input <- reactive({ if (is.null(selector_list()$categorical_var) || length(selector_list()$categorical_var()$select) == 0) { @@ -269,22 +301,15 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, anl_q_r = anl_merged_q ) - is_cat_filter_spec <- inherits(categorical_var[[1]]$filter[[1]], "filter_spec") - cat_dataname <- categorical_var[[1]]$dataname - n_outlier_missing <- reactive({ + shiny::req(iv_r()$is_valid()) outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var) - validate(need(outlier_var, "Please select a variable")) ANL <- merged$anl_q_r()[["ANL"]] # nolint sum(is.na(ANL[[outlier_var]])) }) common_code_q <- reactive({ - input_catvar <- input[[extract_input( - "categorical_var", - cat_dataname, - filter = is_cat_filter_spec - )]] + shiny::req(iv_r()$is_valid()) ANL <- merged$anl_q_r()[["ANL"]] # nolint qenv <- merged$anl_q_r() @@ -294,10 +319,6 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, order_by_outlier <- input$order_by_outlier # nolint method <- input$method split_outliers <- input$split_outliers - validate(need(outlier_var, "Please select a variable")) - validate(need(is.numeric(ANL[[outlier_var]]), "`Variable` is not numeric")) - validate(need(length(unique(ANL[[outlier_var]])) > 1, "Variable has no variation, i.e. only one unique value")) - validate(need(input$method, "Please select a method")) teal::validate_has_data( # missing values in the categorical variable may be used to form a category of its own `if`( @@ -309,6 +330,8 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, complete = TRUE, allow_inf = FALSE ) + validate(need(is.numeric(ANL[[outlier_var]]), "`Variable` is not numeric")) + validate(need(length(unique(ANL[[outlier_var]])) > 1, "Variable has no variation, i.e. only one unique value")) # show/hide split_outliers if (length(categorical_var) == 0) { @@ -323,54 +346,12 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, ) } } else { - validate(need(input_catvar, "Please select categories to include")) - validate(need( is.factor(ANL[[categorical_var]]) || is.character(ANL[[categorical_var]]) || is.integer(ANL[[categorical_var]]), "`Categorical factor` must be `factor`, `character`, or `integer`" )) - validate(need(outlier_var != categorical_var, "`Variable` and `Categorical factor` cannot be the same")) - - input_catlevels <- if (is_cat_filter_spec) { - input_catvar - } else { - NULL - } - - # If there are both string values "NA" and missing values NA, value_choices function should output a warning - if ("NA" %in% input_catlevels) { - qenv <- teal.code::eval_code( - qenv, - substitute( - expr = { - ANL[[categorical_var]] <- dplyr::if_else( # nolint - is.na(ANL[[categorical_var]]), - "NA", - as.character(ANL[[categorical_var]]) - ) - }, - env = list( - categorical_var = categorical_var, - categorical_var_name = as.name(categorical_var) - ) - ) - ) - } - - if (is_cat_filter_spec && !all(unique(ANL[[categorical_var]]) %in% input_catlevels)) { - qenv <- teal.code::eval_code( - qenv, - substitute( - expr = ANL <- ANL %>% dplyr::filter(categorical_var_name %in% categorical_var_levels), # nolint - env = list( - categorical_var_name = as.name(categorical_var), - categorical_var_levels = input_catlevels - ) - ) - ) - } if (n_outlier_missing() > 0) { qenv <- teal.code::eval_code( @@ -428,7 +409,7 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, q1_q3 <- stats::quantile(outlier_var_name, probs = c(0.25, 0.75)) iqr <- q1_q3[2] - q1_q3[1] !(outlier_var_name >= q1_q3[1] - outlier_definition_param * iqr & - outlier_var_name <= q1_q3[2] + outlier_definition_param * iqr) + outlier_var_name <= q1_q3[2] + outlier_definition_param * iqr) }), env = list( outlier_var_name = as.name(outlier_var), @@ -572,12 +553,11 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, qenv }) - validate(need(outlier_var, "Please select a variable")) output$summary_table <- DT::renderDataTable( expr = { - categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var) - if (length(categorical_var) > 0) { + if (iv_r()$is_valid()) { + categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var) DT::datatable( common_code_q()[["summary_table"]], options = list( @@ -594,6 +574,7 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, # boxplot/violinplot #nolint boxplot_q <- reactive({ + req(common_code_q()) ANL <- common_code_q()[["ANL"]] # nolint ANL_OUTLIER <- common_code_q()[["ANL_OUTLIER"]] # nolint @@ -602,7 +583,6 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, # validation teal::validate_has_data(ANL, 1) - validate(need(input$boxplot_alts, "Please select `Plot type`")) # boxplot plot_call <- quote(ANL %>% ggplot()) # nolint @@ -865,7 +845,7 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, # slider text output$ui_outlier_help <- renderUI({ - validate(need(input$method, "Please select a method")) + req(input$method) if (input$method == "IQR") { req(input$iqr_slider) tags$small( @@ -911,9 +891,18 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, } }) - boxplot_r <- reactive(boxplot_q()[["g"]]) - density_plot_r <- reactive(density_plot_q()[["g"]]) - cumulative_plot_r <- reactive(cumulative_plot_q()[["g"]]) + boxplot_r <- reactive({ + teal::validate_inputs(iv_r()) + boxplot_q()[["g"]] + }) + density_plot_r <- reactive({ + teal::validate_inputs(iv_r()) + density_plot_q()[["g"]] + }) + cumulative_plot_r <- reactive({ + teal::validate_inputs(iv_r()) + cumulative_plot_q()[["g"]] + }) box_pws <- teal.widgets::plot_with_settings_srv( id = "box_plot", @@ -957,7 +946,6 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, expr = { tab <- input$tabs req(tab) # tab is NULL upon app launch, hence will crash without this statement - validate(need(!is.null(reactive_select_input()$outlier()), "")) outlier_var <- as.vector(merged$anl_input_r()$columns_source$outlier_var) categorical_var <- as.vector(merged$anl_input_r()$columns_source$categorical_var) @@ -1044,13 +1032,14 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, }, options = list( searching = FALSE, language = list( - zeroRecords = "The highlighted area does not contain outlier points under the actual defined threshold" + zeroRecords = "The brushed area does not contain outlier observations for the currently defined threshold" ), pageLength = input$table_ui_rows ) ) output$total_outliers <- renderUI({ + shiny::req(iv_r()$is_valid()) ANL <- merged$anl_q_r()[["ANL"]] # nolint ANL_OUTLIER <- common_code_q()[["ANL_OUTLIER"]] # nolint teal::validate_has_data(ANL, 1) @@ -1081,6 +1070,22 @@ srv_outliers <- function(id, data, reporter, filter_panel_api, outlier_var, } }) + output$table_ui_wrap <- renderUI({ + shiny::req(iv_r()$is_valid()) + tagList( + teal.widgets::optionalSelectInput( + inputId = session$ns("table_ui_columns"), + label = "Choose additional columns", + choices = NULL, + selected = NULL, + multiple = TRUE + ), + h4("Outlier Table"), + teal.widgets::get_dt_rows(session$ns("table_ui"), session$ns("table_ui_rows")), + DT::dataTableOutput(session$ns("table_ui")) + ) + }) + teal.widgets::verbatim_popup_srv( id = "warning", verbatim_content = reactive(teal.code::get_warnings(final_q())), diff --git a/R/tm_t_crosstable.R b/R/tm_t_crosstable.R index c7a2782cf..da2911e28 100644 --- a/R/tm_t_crosstable.R +++ b/R/tm_t_crosstable.R @@ -169,7 +169,25 @@ srv_t_crosstable <- function(id, data, reporter, filter_panel_api, label, x, y, with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") checkmate::assert_class(data, "tdata") moduleServer(id, function(input, output, session) { - selector_list <- teal.transform::data_extract_multiple_srv(data_extract = list(x = x, y = y), datasets = data) + selector_list <- teal.transform::data_extract_multiple_srv( + data_extract = list(x = x, y = y), + datasets = data, + select_validation_rule = list( + x = shinyvalidate::sv_required("Please define column for row variable."), + y = shinyvalidate::sv_required("Please define column for column variable.") + ) + ) + + iv_r <- reactive({ + iv <- shinyvalidate::InputValidator$new() + iv$add_rule("join_fun", function(value) { + if (!identical(selector_list()$x()$dataname, selector_list()$y()$dataname)) { + if (!shinyvalidate::input_provided(value)) + "Please select a joining function." + } + }) + teal.transform::compose_and_enable_validators(iv, selector_list) + }) observeEvent( eventExpr = { @@ -212,16 +230,13 @@ srv_t_crosstable <- function(id, data, reporter, filter_panel_api, label, x, y, ) output_q <- reactive({ + teal::validate_inputs(iv_r()) ANL <- merged$anl_q_r()[["ANL"]] # nolint # As this is a summary - x_name <- as.vector(merged$anl_input_r()$columns_source$x) y_name <- as.vector(merged$anl_input_r()$columns_source$y) - validate(need(length(x_name) > 0, "Please define column for row variable that is not empty.")) - validate(need(length(y_name) > 0, "Please define column for column variable that is not empty.")) - teal::validate_has_data(ANL, 3) teal::validate_has_data(ANL[, c(x_name, y_name)], 3, complete = TRUE, allow_inf = FALSE) @@ -311,7 +326,10 @@ srv_t_crosstable <- function(id, data, reporter, filter_panel_api, label, x, y, output$title <- renderText(output_q()[["title"]]) - table_r <- reactive(output_q()[["tbl"]]) + table_r <- reactive({ + shiny::req(iv_r()$is_valid()) + output_q()[["tbl"]] + }) teal.widgets::table_with_settings_srv( id = "table", diff --git a/R/tm_variable_browser.R b/R/tm_variable_browser.R index 342e8ac83..acaed45bd 100644 --- a/R/tm_variable_browser.R +++ b/R/tm_variable_browser.R @@ -943,7 +943,7 @@ plot_var_summary <- function(var, } ## histogram binwidth <- get_bin_width(var) - p <- ggplot(data = data.frame(var = var), aes_string(x = "var", y = "..count..")) + + p <- ggplot(data = data.frame(var = var), aes(x = var, y = after_stat(count))) + geom_histogram(binwidth = binwidth) + scale_y_continuous( sec.axis = sec_axis( @@ -954,7 +954,7 @@ plot_var_summary <- function(var, ) if (display_density) { - p <- p + geom_density(aes_string(y = "..count.. * binwidth")) + p <- p + geom_density(aes(y = after_stat(count * binwidth))) } if (outlier_definition != 0) { @@ -973,7 +973,7 @@ plot_var_summary <- function(var, } else if (inherits(var, "Date") || inherits(var, "POSIXct") || inherits(var, "POSIXlt")) { var_num <- as.numeric(var) binwidth <- get_bin_width(var_num, 1) - p <- ggplot(data = data.frame(var = var), aes_string(x = "var", y = "..count..")) + + p <- ggplot(data = data.frame(var = var), aes(x = var, y = after_stat(count))) + geom_histogram(binwidth = binwidth) } else { grid::textGrob( diff --git a/man/tm_g_bivariate.Rd b/man/tm_g_bivariate.Rd index 87bce6583..97b2817d9 100644 --- a/man/tm_g_bivariate.Rd +++ b/man/tm_g_bivariate.Rd @@ -141,6 +141,24 @@ app <- init( fixed = FALSE ) ), + row_facet = data_extract_spec( + dataname = "ADSL", + select = select_spec( + label = "Select variable:", + choices = variable_choices(ADSL), + selected = "ARM", + fixed = FALSE + ) + ), + col_facet = data_extract_spec( + dataname = "ADSL", + select = select_spec( + label = "Select variable:", + choices = variable_choices(ADSL), + selected = "COUNTRY", + fixed = FALSE + ) + ), ggplot2_args = teal.widgets::ggplot2_args( labs = list(subtitle = "Plot generated by Bivariate Module") ) diff --git a/man/tm_g_scatterplot.Rd b/man/tm_g_scatterplot.Rd index dd07360c2..f7045b4e6 100644 --- a/man/tm_g_scatterplot.Rd +++ b/man/tm_g_scatterplot.Rd @@ -155,7 +155,7 @@ app <- init( dataname = "ADSL", select = select_spec( label = "Select variable:", - choices = variable_choices(ADSL, c("AGE", "BMRKR1", "BMRKR2", "RACE", "REGION1")), + choices = variable_choices(ADSL, c("BMRKR2", "RACE", "REGION1")), selected = NULL, multiple = FALSE, fixed = FALSE @@ -165,7 +165,7 @@ app <- init( dataname = "ADSL", select = select_spec( label = "Select variable:", - choices = variable_choices(ADSL, c("AGE", "BMRKR1", "BMRKR2", "RACE", "REGION1")), + choices = variable_choices(ADSL, c("BMRKR2", "RACE", "REGION1")), selected = NULL, multiple = FALSE, fixed = FALSE diff --git a/vignettes/using-bivariate-plot.Rmd b/vignettes/using-bivariate-plot.Rmd index 028f94d21..59b8e7f27 100644 --- a/vignettes/using-bivariate-plot.Rmd +++ b/vignettes/using-bivariate-plot.Rmd @@ -118,7 +118,7 @@ app <- init( label = "Select variables:", choices = variable_choices(ADSL), selected = NULL, - multiple = TRUE, + multiple = FALSE, fixed = FALSE ) ), @@ -128,7 +128,7 @@ app <- init( label = "Select variables:", choices = variable_choices(ADSL), selected = NULL, - multiple = TRUE, + multiple = FALSE, fixed = FALSE ) ) @@ -169,7 +169,7 @@ app <- init( label = "Select variables:", choices = variable_choices(ADSL2), selected = NULL, - multiple = TRUE, + multiple = FALSE, fixed = FALSE ) ) @@ -231,7 +231,7 @@ app <- init( label = "Select variables:", choices = variable_choices(ADSL, c("SEX", "RACE")), selected = NULL, - multiple = TRUE, + multiple = FALSE, fixed = FALSE ) ), @@ -459,7 +459,7 @@ app <- init( select = select_spec( choices = variable_choices(ADRS, c("AVISIT", "PARAMCD")), selected = "PARAMCD", - multiple = TRUE, + multiple = FALSE, label = "Select variables:" ) ), @@ -468,7 +468,7 @@ app <- init( select = select_spec( choices = variable_choices(ADRS, c("AVISIT", "PARAMCD")), selected = "AVISIT", - multiple = TRUE, + multiple = FALSE, label = "Select variables:" ) ) @@ -547,7 +547,7 @@ app <- init( select = select_spec( choices = variable_choices(ADLB, c("RACE", "SEX", "ARMCD", "ACTARMCD")), selected = NULL, - multiple = TRUE, + multiple = FALSE, fixed = FALSE, label = "Select variable:" ) @@ -573,7 +573,7 @@ app <- init( select = select_spec( choices = variable_choices(ADLB, c("RACE", "SEX", "ARMCD", "ACTARMCD")), selected = "ARMCD", - multiple = TRUE, + multiple = FALSE, fixed = FALSE, label = "Select variables:" )