diff --git a/R/tm_missing_data.R b/R/tm_missing_data.R index 71e9cad50..027ba182b 100644 --- a/R/tm_missing_data.R +++ b/R/tm_missing_data.R @@ -447,332 +447,574 @@ encoding_missing_data <- function(id, summary_per_patient = FALSE, ggtheme, data ) } -srv_summary_plot <- function(id, - data_r, - data_keys, - common_code_q, - data_parent_keys, +# Server function for the missing data (single dataset) +srv_missing_data <- function(id, + data, + reporter, + filter_panel_api, + dataname, + parent_dataname, + plot_height, + plot_width, ggplot2_args, - # inputs - summary_type_r, - any_na_r, - ggtheme_r, - if_patients_plot_r) { + decorators) { + with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") + with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") + checkmate::assert_class(data, "reactive") + checkmate::assert_class(isolate(data()), "teal_data") moduleServer(id, function(input, output, session) { - summary_plot_q <- reactive({ - req(summary_type_r() == "Summary") # needed to trigger show r code update on tab change - teal::validate_has_data(data_r(), 1) + ns <- session$ns - qenv <- common_code_q() - if (any_na_r()) { - new_col_name <- "**anyna**" - qenv <- teal.code::eval_code( - qenv, - substitute( - expr = ANL[[new_col_name]] <- ifelse(rowSums(is.na(ANL)) > 0, NA, FALSE), - env = list(new_col_name = new_col_name) - ) - ) + prev_group_by_var <- reactiveVal("") + data_r <- reactive(data()[[dataname]]) + data_keys <- reactive(unlist(teal.data::join_keys(data())[[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 <- teal.data::join_keys(data())[[dataname]] + if (parent_dataname %in% names(keys)) { + keys[[parent_dataname]] + } else { + keys[[dataname]] + } + } else { + NULL } + }) - qenv <- teal.code::eval_code( - qenv, - substitute( - expr = analysis_vars <- setdiff(colnames(ANL), data_keys), - env = list(data_keys = data_keys()) - ) - ) %>% + common_code_q <- reactive({ + teal::validate_inputs(iv_r()) + + group_var <- input$group_by_var + anl <- data_r() + + qenv <- if (!is.null(selected_vars()) && length(selected_vars()) != ncol(anl)) { teal.code::eval_code( + data(), substitute( - expr = summary_plot_obs <- data_frame_call[, analysis_vars] %>% - dplyr::summarise_all(list(function(x) sum(is.na(x)))) %>% - tidyr::pivot_longer(dplyr::everything(), names_to = "col", values_to = "n_na") %>% - dplyr::mutate(n_not_na = nrow(ANL) - n_na) %>% - tidyr::pivot_longer(-col, names_to = "isna", values_to = "n") %>% - dplyr::mutate(isna = isna == "n_na", n_pct = n / nrow(ANL) * 100), - env = list(data_frame_call = if (!inherits(data_r(), "tbl_df")) { - quote(tibble::as_tibble(ANL)) - } else { - quote(ANL) - }) + expr = ANL <- anl_name[, selected_vars, drop = FALSE], + env = list(anl_name = as.name(dataname), selected_vars = selected_vars()) ) - ) %>% - # x axis ordering according to number of missing values and alphabet + ) + } else { teal.code::eval_code( - quote( - expr = x_levels <- dplyr::filter(summary_plot_obs, isna) %>% - dplyr::arrange(n_pct, dplyr::desc(col)) %>% - dplyr::pull(col) %>% - create_cols_labels() - ) + data(), + substitute(expr = ANL <- anl_name, env = list(anl_name = as.name(dataname))) ) + } - # always set "**anyna**" level as the last one - if (isolate(any_na_r())) { + if (input$summary_type == "By Variable Levels" && !is.null(group_var) && !(group_var %in% selected_vars())) { qenv <- teal.code::eval_code( qenv, - quote(x_levels <- c(setdiff(x_levels, "**anyna**"), "**anyna**")) + substitute( + expr = ANL[[group_var]] <- anl_name[[group_var]], + env = list(group_var = group_var, anl_name = as.name(dataname)) + ) ) } - dev_ggplot2_args <- teal.widgets::ggplot2_args( - labs = list(x = "Variable", y = "Missing observations"), - theme = list(legend.position = "bottom", axis.text.x = quote(element_text(angle = 45, hjust = 1))) - ) - - all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( - user_plot = ggplot2_args[["Summary Obs"]], - user_default = ggplot2_args$default, - module_plot = dev_ggplot2_args - ) - - parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( - all_ggplot2_args, - ggtheme = ggtheme_r() - ) + new_col_name <- "**anyna**" qenv <- teal.code::eval_code( qenv, substitute( - summary_plot_top <- summary_plot_obs %>% - ggplot() + - aes( - x = factor(create_cols_labels(col), levels = x_levels), - y = n_pct, - fill = isna - ) + - geom_bar(position = "fill", stat = "identity") + - scale_fill_manual( - name = "", - values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]), - labels = c("Present", "Missing") - ) + - scale_y_continuous( - labels = scales::percent_format(), - breaks = seq(0, 1, by = 0.1), - expand = c(0, 0) - ) + - geom_text( - aes(label = ifelse(isna == TRUE, sprintf("%d [%.02f%%]", n, n_pct), ""), y = 1), - hjust = 1, - color = "black" - ) + - labs + - ggthemes + - themes + - coord_flip(), + expr = + create_cols_labels <- function(cols, just_label = FALSE) { + column_labels <- column_labels_value + column_labels[is.na(column_labels) | length(column_labels) == 0] <- "" + if (just_label) { + labels <- column_labels[cols] + } else { + labels <- ifelse(cols == new_col_name | cols == "", cols, paste0(column_labels[cols], " [", cols, "]")) + } + labels + }, env = list( - labs = parsed_ggplot2_args$labs, - themes = parsed_ggplot2_args$theme, - ggthemes = parsed_ggplot2_args$ggtheme + new_col_name = new_col_name, + column_labels_value = c(teal.data::col_labels(data_r())[selected_vars()], + new_col_name = new_col_name + ) ) ) ) + qenv + }) - if (isTRUE(if_patients_plot_r())) { - qenv <- teal.code::eval_code( - qenv, - substitute( - expr = parent_keys <- keys, - env = list(keys = data_parent_keys()) - ) - ) %>% - teal.code::eval_code(quote(ndistinct_subjects <- dplyr::n_distinct(ANL[, parent_keys]))) %>% - teal.code::eval_code( - quote( - summary_plot_patients <- ANL[, c(parent_keys, analysis_vars)] %>% - dplyr::group_by_at(parent_keys) %>% - dplyr::summarise_all(anyNA) %>% - tidyr::pivot_longer(cols = !dplyr::all_of(parent_keys), names_to = "col", values_to = "anyna") %>% - dplyr::group_by_at(c("col")) %>% - dplyr::summarise(count_na = sum(anyna)) %>% - dplyr::mutate(count_not_na = ndistinct_subjects - count_na) %>% - tidyr::pivot_longer(-c(col), names_to = "isna", values_to = "n") %>% - dplyr::mutate(isna = isna == "count_na", n_pct = n / ndistinct_subjects * 100) %>% - dplyr::arrange_at(c("isna", "n"), .funs = dplyr::desc) - ) - ) + selected_vars <- reactive({ + req(input$variables_select) + keys <- data_keys() + vars <- unique(c(keys, input$variables_select)) + vars + }) - dev_ggplot2_args <- teal.widgets::ggplot2_args( - labs = list(x = "", y = "Missing patients"), - theme = list( - legend.position = "bottom", - axis.text.x = quote(element_text(angle = 45, hjust = 1)), - axis.text.y = quote(element_blank()) - ) - ) + vars_summary <- reactive({ + na_count <- data_r() %>% + sapply(function(x) mean(is.na(x)), USE.NAMES = TRUE) %>% + sort(decreasing = TRUE) - all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( - user_plot = ggplot2_args[["Summary Patients"]], - user_default = ggplot2_args$default, - module_plot = dev_ggplot2_args - ) + tibble::tibble( + key = names(na_count), + value = unname(na_count), + label = cut(na_count, breaks = seq(from = 0, to = 1, by = 0.1), include.lowest = TRUE) + ) + }) - parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( - all_ggplot2_args, - ggtheme = ggtheme_r() - ) + # Keep encoding panel up-to-date + output$variables <- renderUI({ + choices <- split(x = vars_summary()$key, f = vars_summary()$label, drop = TRUE) %>% rev() + selected <- choices <- unname(unlist(choices)) - qenv <- teal.code::eval_code( - qenv, - substitute( - summary_plot_bottom <- summary_plot_patients %>% - ggplot() + - aes_( - x = ~ factor(create_cols_labels(col), levels = x_levels), - y = ~n_pct, - fill = ~isna - ) + - geom_bar(alpha = 1, stat = "identity", position = "fill") + - scale_y_continuous( - labels = scales::percent_format(), - breaks = seq(0, 1, by = 0.1), - expand = c(0, 0) - ) + - scale_fill_manual( - name = "", - values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]), - labels = c("Present", "Missing") - ) + - geom_text( - aes(label = ifelse(isna == TRUE, sprintf("%d [%.02f%%]", n, n_pct), ""), y = 1), - hjust = 1, - color = "black" - ) + - labs + - ggthemes + - themes + - coord_flip(), - env = list( - labs = parsed_ggplot2_args$labs, - themes = parsed_ggplot2_args$theme, - ggthemes = parsed_ggplot2_args$ggtheme - ) - ) - ) - } + teal.widgets::optionalSelectInput( + ns("variables_select"), + label = "Select variables", + label_help = HTML(paste0("Dataset: ", tags$code(dataname))), + choices = teal.transform::variable_choices(data_r(), choices), + selected = selected, + multiple = TRUE + ) + }) - arrange_expr <- if (isTRUE(if_patients_plot_r())) { - quote({ - g1 <- ggplotGrob(summary_plot_top) - g2 <- ggplotGrob(summary_plot_bottom) - summary_plot <- gridExtra::gtable_cbind(g1, g2, size = "first") - summary_plot$heights <- grid::unit.pmax(g1$heights, g2$heights) - }) + observeEvent(input$filter_na, { + choices <- vars_summary() %>% + dplyr::select(!!as.name("key")) %>% + getElement(name = 1) + + selected <- vars_summary() %>% + dplyr::filter(!!as.name("value") > 0) %>% + dplyr::select(!!as.name("key")) %>% + getElement(name = 1) + + teal.widgets::updateOptionalSelectInput( + session = session, + inputId = "variables_select", + choices = teal.transform::variable_choices(data_r()), + selected = restoreInput(ns("variables_select"), selected) + ) + }) + + output$group_by_var_ui <- renderUI({ + all_choices <- teal.transform::variable_choices(data_r()) + cat_choices <- all_choices[!sapply(data_r(), function(x) is.numeric(x) || inherits(x, "POSIXct"))] + validate( + need(cat_choices, "Dataset does not have any non-numeric or non-datetime variables to use to group data with") + ) + teal.widgets::optionalSelectInput( + ns("group_by_var"), + label = "Group by variable", + choices = cat_choices, + selected = `if`( + is.null(isolate(input$group_by_var)), + cat_choices[1], + isolate(input$group_by_var) + ), + multiple = FALSE, + label_help = paste0("Dataset: ", dataname) + ) + }) + + output$group_by_vals_ui <- renderUI({ + req(input$group_by_var) + + choices <- teal.transform::value_choices(data_r(), input$group_by_var, input$group_by_var) + prev_choices <- isolate(input$group_by_vals) + + # determine selected value based on filtered data + # display those previously selected values that are still available + 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 + ) { + # if not any previously selected value is available and the grouping variable is the same, + # then display NULL + NULL } else { - quote({ - g1 <- ggplotGrob(summary_plot_top) - summary_plot <- g1 - }) + # if new grouping variable (i.e. not any previously selected value is available), + # then display all choices + choices } - teal.code::eval_code(qenv, arrange_expr) - }) - }) -} -srv_combination_plot <- function(id, - data_r, - common_code_q, - data_keys, - data_parent_keys, - combination_cutoff_q, - ggplot2_args, - # inputs - summary_type_r, - combination_cutoff_r, - ggtheme_r, - variables_select_r) { - moduleServer(id, function(input, output, session) { - combination_plot_q <- reactive({ - req(summary_type_r() == "Combinations", combination_cutoff_r(), combination_cutoff_q()) - teal::validate_has_data(data_r(), 1) + prev_group_by_var(input$group_by_var) # set current group_by_var + validate(need(length(choices) < 100, "Please select group-by variable with fewer than 100 unique values")) + teal.widgets::optionalSelectInput( + ns("group_by_vals"), + label = "Filter levels", + choices = choices, + selected = selected, + multiple = TRUE, + label_help = paste0("Dataset: ", dataname) + ) + }) - qenv <- teal.code::eval_code( - combination_cutoff_q(), - substitute( - expr = data_combination_plot_cutoff <- combination_cutoff %>% - dplyr::filter(n >= combination_cutoff_value) %>% - dplyr::mutate(id = rank(-n, ties.method = "first")) %>% - tidyr::pivot_longer(-c(n, id), names_to = "key", values_to = "value") %>% - dplyr::arrange(n), - env = list(combination_cutoff_value = combination_cutoff_r()) + combination_cutoff_q <- reactive({ + req(common_code_q()) + teal.code::eval_code( + common_code_q(), + quote( + combination_cutoff <- ANL %>% + dplyr::mutate_all(is.na) %>% + dplyr::group_by_all() %>% + dplyr::tally() %>% + dplyr::ungroup() ) ) + }) - # find keys in dataset not selected in the UI and remove them from dataset - keys_not_selected <- setdiff(data_keys(), variables_select_r()) - if (length(keys_not_selected) > 0) { + output$cutoff <- renderUI({ + x <- combination_cutoff_q()[["combination_cutoff"]]$n + + # select 10-th from the top + n <- length(x) + idx <- max(1, n - 10) + prev_value <- isolate(input$combination_cutoff) + value <- `if`( + is.null(prev_value) || prev_value > max(x) || prev_value < min(x), + sort(x, partial = idx)[idx], prev_value + ) + + teal.widgets::optionalSliderInputValMinMax( + ns("combination_cutoff"), + "Combination cut-off", + c(value, range(x)) + ) + }) + + # Prepare qenvs for output objects + + 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) + + qenv <- common_code_q() + if (input$any_na) { + new_col_name <- "**anyna**" qenv <- teal.code::eval_code( qenv, substitute( - expr = data_combination_plot_cutoff <- data_combination_plot_cutoff %>% - dplyr::filter(!key %in% keys_not_selected), - env = list(keys_not_selected = keys_not_selected) + expr = ANL[[new_col_name]] <- ifelse(rowSums(is.na(ANL)) > 0, NA, FALSE), + env = list(new_col_name = new_col_name) ) ) } qenv <- teal.code::eval_code( qenv, - quote( - labels <- data_combination_plot_cutoff %>% - dplyr::filter(key == key[[1]]) %>% - getElement(name = 1) + substitute( + expr = analysis_vars <- setdiff(colnames(ANL), data_keys), + env = list(data_keys = data_keys()) ) - ) - - dev_ggplot2_args1 <- teal.widgets::ggplot2_args( - labs = list(x = "", y = ""), - theme = list( - legend.position = "bottom", - axis.text.x = quote(element_blank()) + ) %>% + teal.code::eval_code( + substitute( + expr = summary_plot_obs <- data_frame_call[, analysis_vars] %>% + dplyr::summarise_all(list(function(x) sum(is.na(x)))) %>% + tidyr::pivot_longer(dplyr::everything(), names_to = "col", values_to = "n_na") %>% + dplyr::mutate(n_not_na = nrow(ANL) - n_na) %>% + tidyr::pivot_longer(-col, names_to = "isna", values_to = "n") %>% + dplyr::mutate(isna = isna == "n_na", n_pct = n / nrow(ANL) * 100), + env = list(data_frame_call = if (!inherits(data_r(), "tbl_df")) { + quote(tibble::as_tibble(ANL)) + } else { + quote(ANL) + }) + ) + ) %>% + # x axis ordering according to number of missing values and alphabet + teal.code::eval_code( + quote( + expr = x_levels <- dplyr::filter(summary_plot_obs, isna) %>% + dplyr::arrange(n_pct, dplyr::desc(col)) %>% + dplyr::pull(col) %>% + create_cols_labels() + ) ) - ) - - all_ggplot2_args1 <- teal.widgets::resolve_ggplot2_args( - user_plot = ggplot2_args[["Combinations Hist"]], - user_default = ggplot2_args$default, - module_plot = dev_ggplot2_args1 - ) - parsed_ggplot2_args1 <- teal.widgets::parse_ggplot2_args( - all_ggplot2_args1, - ggtheme = "void" - ) - - dev_ggplot2_args2 <- teal.widgets::ggplot2_args( - labs = list(x = "", y = ""), - theme = list( - legend.position = "bottom", - axis.text.x = quote(element_blank()), - axis.ticks = quote(element_blank()), - panel.grid.major = quote(element_blank()) + # always set "**anyna**" level as the last one + if (isolate(input$any_na)) { + qenv <- teal.code::eval_code( + qenv, + quote(x_levels <- c(setdiff(x_levels, "**anyna**"), "**anyna**")) ) + } + + dev_ggplot2_args <- teal.widgets::ggplot2_args( + labs = list(x = "Variable", y = "Missing observations"), + theme = list(legend.position = "bottom", axis.text.x = quote(element_text(angle = 45, hjust = 1))) ) - all_ggplot2_args2 <- teal.widgets::resolve_ggplot2_args( - user_plot = ggplot2_args[["Combinations Main"]], + all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( + user_plot = ggplot2_args[["Summary Obs"]], user_default = ggplot2_args$default, - module_plot = dev_ggplot2_args2 + module_plot = dev_ggplot2_args ) - parsed_ggplot2_args2 <- teal.widgets::parse_ggplot2_args( - all_ggplot2_args2, - ggtheme = ggtheme_r() + parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( + all_ggplot2_args, + ggtheme = input$ggtheme ) qenv <- teal.code::eval_code( qenv, substitute( - expr = { - combination_plot_top <- data_combination_plot_cutoff %>% - dplyr::select(id, n) %>% - dplyr::distinct() %>% - ggplot(aes(x = id, y = n)) + - geom_bar(stat = "identity", fill = c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]) + - geom_text( - aes(label = n), - position = position_dodge(width = 0.9), + summary_plot_top <- summary_plot_obs %>% + ggplot() + + aes( + x = factor(create_cols_labels(col), levels = x_levels), + y = n_pct, + fill = isna + ) + + geom_bar(position = "fill", stat = "identity") + + scale_fill_manual( + name = "", + values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]), + labels = c("Present", "Missing") + ) + + scale_y_continuous( + labels = scales::percent_format(), + breaks = seq(0, 1, by = 0.1), + expand = c(0, 0) + ) + + geom_text( + aes(label = ifelse(isna == TRUE, sprintf("%d [%.02f%%]", n, n_pct), ""), y = 1), + hjust = 1, + color = "black" + ) + + labs + + ggthemes + + themes + + coord_flip(), + env = list( + labs = parsed_ggplot2_args$labs, + themes = parsed_ggplot2_args$theme, + ggthemes = parsed_ggplot2_args$ggtheme + ) + ) + ) + + if (isTRUE(input$if_patients_plot)) { + qenv <- teal.code::eval_code( + qenv, + substitute( + expr = parent_keys <- keys, + env = list(keys = data_parent_keys()) + ) + ) %>% + teal.code::eval_code(quote(ndistinct_subjects <- dplyr::n_distinct(ANL[, parent_keys]))) %>% + teal.code::eval_code( + quote( + summary_plot_patients <- ANL[, c(parent_keys, analysis_vars)] %>% + dplyr::group_by_at(parent_keys) %>% + dplyr::summarise_all(anyNA) %>% + tidyr::pivot_longer(cols = !dplyr::all_of(parent_keys), names_to = "col", values_to = "anyna") %>% + dplyr::group_by_at(c("col")) %>% + dplyr::summarise(count_na = sum(anyna)) %>% + dplyr::mutate(count_not_na = ndistinct_subjects - count_na) %>% + tidyr::pivot_longer(-c(col), names_to = "isna", values_to = "n") %>% + dplyr::mutate(isna = isna == "count_na", n_pct = n / ndistinct_subjects * 100) %>% + dplyr::arrange_at(c("isna", "n"), .funs = dplyr::desc) + ) + ) + + dev_ggplot2_args <- teal.widgets::ggplot2_args( + labs = list(x = "", y = "Missing patients"), + theme = list( + legend.position = "bottom", + axis.text.x = quote(element_text(angle = 45, hjust = 1)), + axis.text.y = quote(element_blank()) + ) + ) + + all_ggplot2_args <- teal.widgets::resolve_ggplot2_args( + user_plot = ggplot2_args[["Summary Patients"]], + user_default = ggplot2_args$default, + module_plot = dev_ggplot2_args + ) + + parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( + all_ggplot2_args, + ggtheme = input$ggtheme + ) + + qenv <- teal.code::eval_code( + qenv, + substitute( + summary_plot_bottom <- summary_plot_patients %>% + ggplot() + + aes_( + x = ~ factor(create_cols_labels(col), levels = x_levels), + y = ~n_pct, + fill = ~isna + ) + + geom_bar(alpha = 1, stat = "identity", position = "fill") + + scale_y_continuous( + labels = scales::percent_format(), + breaks = seq(0, 1, by = 0.1), + expand = c(0, 0) + ) + + scale_fill_manual( + name = "", + values = c("grey90", c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]), + labels = c("Present", "Missing") + ) + + geom_text( + aes(label = ifelse(isna == TRUE, sprintf("%d [%.02f%%]", n, n_pct), ""), y = 1), + hjust = 1, + color = "black" + ) + + labs + + ggthemes + + themes + + coord_flip(), + env = list( + labs = parsed_ggplot2_args$labs, + themes = parsed_ggplot2_args$theme, + ggthemes = parsed_ggplot2_args$ggtheme + ) + ) + ) + } + expr <- if (isTRUE(input$if_patients_plot)) { + quote({ + g1 <- ggplotGrob(summary_plot_top) + g2 <- ggplotGrob(summary_plot_bottom) + summary_plot <- gridExtra::gtable_cbind(g1, g2, size = "first") + summary_plot$heights <- grid::unit.pmax(g1$heights, g2$heights) + }) + } else { + quote({ + g1 <- ggplotGrob(summary_plot_top) + summary_plot <- g1 + }) + } + teal.code::eval_code(qenv, expr) + }) + + combination_plot_q <- reactive({ + req(input$summary_type == "Combinations", input$combination_cutoff, combination_cutoff_q()) + teal::validate_has_data(data_r(), 1) + + qenv <- teal.code::eval_code( + combination_cutoff_q(), + substitute( + expr = data_combination_plot_cutoff <- combination_cutoff %>% + dplyr::filter(n >= combination_cutoff_value) %>% + dplyr::mutate(id = rank(-n, ties.method = "first")) %>% + tidyr::pivot_longer(-c(n, id), names_to = "key", values_to = "value") %>% + dplyr::arrange(n), + env = list(combination_cutoff_value = input$combination_cutoff) + ) + ) + + # find keys in dataset not selected in the UI and remove them from dataset + keys_not_selected <- setdiff(data_keys(), input$variables_select) + if (length(keys_not_selected) > 0) { + qenv <- teal.code::eval_code( + qenv, + substitute( + expr = data_combination_plot_cutoff <- data_combination_plot_cutoff %>% + dplyr::filter(!key %in% keys_not_selected), + env = list(keys_not_selected = keys_not_selected) + ) + ) + } + + qenv <- teal.code::eval_code( + qenv, + quote( + labels <- data_combination_plot_cutoff %>% + dplyr::filter(key == key[[1]]) %>% + getElement(name = 1) + ) + ) + + dev_ggplot2_args1 <- teal.widgets::ggplot2_args( + labs = list(x = "", y = ""), + theme = list( + legend.position = "bottom", + axis.text.x = quote(element_blank()) + ) + ) + + all_ggplot2_args1 <- teal.widgets::resolve_ggplot2_args( + user_plot = ggplot2_args[["Combinations Hist"]], + user_default = ggplot2_args$default, + module_plot = dev_ggplot2_args1 + ) + + parsed_ggplot2_args1 <- teal.widgets::parse_ggplot2_args( + all_ggplot2_args1, + ggtheme = "void" + ) + + dev_ggplot2_args2 <- teal.widgets::ggplot2_args( + labs = list(x = "", y = ""), + theme = list( + legend.position = "bottom", + axis.text.x = quote(element_blank()), + axis.ticks = quote(element_blank()), + panel.grid.major = quote(element_blank()) + ) + ) + + all_ggplot2_args2 <- teal.widgets::resolve_ggplot2_args( + user_plot = ggplot2_args[["Combinations Main"]], + user_default = ggplot2_args$default, + module_plot = dev_ggplot2_args2 + ) + + parsed_ggplot2_args2 <- teal.widgets::parse_ggplot2_args( + all_ggplot2_args2, + ggtheme = input$ggtheme + ) + + qenv <- teal.code::eval_code( + qenv, + substitute( + expr = { + combination_plot_top <- data_combination_plot_cutoff %>% + dplyr::select(id, n) %>% + dplyr::distinct() %>% + ggplot(aes(x = id, y = n)) + + geom_bar(stat = "identity", fill = c(getOption("ggplot2.discrete.colour")[2], "#ff2951ff")[1]) + + geom_text( + aes(label = n), + position = position_dodge(width = 0.9), vjust = -0.25 ) + ylim(c(0, max(data_combination_plot_cutoff$n) * 1.5)) + @@ -817,25 +1059,10 @@ srv_combination_plot <- function(id, combination_plot$heights[7] <- grid::unit(0.2, "null") # rescale to get the bar chart smaller }) }) - }) -} -srv_summary_table <- function(id, - data_r, - common_code_q, - data_keys, - data_parent_keys, - selected_vars, - # inputs - summary_type_r, - group_by_var_r, - group_by_vals_r, - variables_select_r, - count_type_r) { - moduleServer(id, function(input, output, session) { - summary_table_q_r <- reactive({ + summary_table_q <- reactive({ req( - summary_type_r() == "By Variable Levels", # needed to trigger show r code update on tab change + input$summary_type == "By Variable Levels", # needed to trigger show r code update on tab change common_code_q() ) teal::validate_has_data(data_r(), 1) @@ -843,7 +1070,7 @@ srv_summary_table <- function(id, # extract the ANL dataset for use in further validation anl <- common_code_q()[["ANL"]] - group_var <- group_by_var_r() + group_var <- input$group_by_var validate( need( is.null(group_var) || @@ -852,10 +1079,10 @@ srv_summary_table <- function(id, ) ) - group_vals <- group_by_vals_r() - variables_select <- variables_select_r() + group_vals <- input$group_by_vals + variables_select <- input$variables_select vars <- unique(variables_select, group_var) - count_type <- count_type_r() + count_type <- input$count_type if (!is.null(selected_vars()) && length(selected_vars()) != ncol(anl)) { variables <- selected_vars() @@ -863,7 +1090,7 @@ srv_summary_table <- function(id, variables <- colnames(anl) } - summ_fn <- if (count_type == "counts") { + summ_fn <- if (input$count_type == "counts") { function(x) sum(is.na(x)) } else { function(x) round(sum(is.na(x)) / length(x), 4) @@ -898,8 +1125,7 @@ srv_summary_table <- function(id, substitute( expr = summary_data <- ANL %>% dplyr::summarise_all(summ_fn) %>% - tidyr::pivot_longer( - dplyr::everything(), + tidyr::pivot_longer(dplyr::everything(), names_to = "Variable", values_to = paste0("Missing (N=", nrow(ANL), ")") ) %>% @@ -911,22 +1137,10 @@ srv_summary_table <- function(id, within(qenv, table <- DT::datatable(summary_data)) }) - }) -} -srv_by_subject_plot <- function(id, - data_r, - common_code_q, - data_keys, - data_parent_keys, - ggplot2_args, - # inputs - summary_type_r, - ggtheme_r) { - moduleServer(id, function(input, output, session) { by_subject_plot_q <- reactive({ # needed to trigger show r code update on tab change - req(summary_type_r() == "Grouped by Subject", common_code_q()) + req(input$summary_type == "Grouped by Subject", common_code_q()) teal::validate_has_data(data_r(), 1) @@ -943,7 +1157,7 @@ srv_by_subject_plot <- function(id, parsed_ggplot2_args <- teal.widgets::parse_ggplot2_args( all_ggplot2_args, - ggtheme = ggtheme_r() + ggtheme = input$ggtheme ) teal.code::eval_code( @@ -1029,333 +1243,6 @@ srv_by_subject_plot <- function(id, ) ) }) - }) -} - -# Server function for the missing data (single dataset) -srv_missing_data <- function(id, - data, - reporter, - filter_panel_api, - dataname, - parent_dataname, - plot_height, - plot_width, - ggplot2_args, - decorators) { - with_reporter <- !missing(reporter) && inherits(reporter, "Reporter") - with_filter <- !missing(filter_panel_api) && inherits(filter_panel_api, "FilterPanelAPI") - checkmate::assert_class(data, "reactive") - checkmate::assert_class(isolate(data()), "teal_data") - moduleServer(id, function(input, output, session) { - ns <- session$ns - - prev_group_by_var <- reactiveVal("") - data_r <- reactive(data()[[dataname]]) - data_keys <- reactive(unlist(teal.data::join_keys(data())[[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 <- teal.data::join_keys(data())[[dataname]] - if (parent_dataname %in% names(keys)) { - keys[[parent_dataname]] - } else { - keys[[dataname]] - } - } else { - NULL - } - }) - - common_code_q <- reactive({ - teal::validate_inputs(iv_r()) - - group_var <- input$group_by_var - anl <- data_r() - - qenv <- if (!is.null(selected_vars()) && length(selected_vars()) != ncol(anl)) { - teal.code::eval_code( - data(), - substitute( - expr = ANL <- anl_name[, selected_vars, drop = FALSE], - env = list(anl_name = as.name(dataname), selected_vars = selected_vars()) - ) - ) - } else { - teal.code::eval_code( - data(), - substitute(expr = ANL <- anl_name, env = list(anl_name = as.name(dataname))) - ) - } - - if (input$summary_type == "By Variable Levels" && !is.null(group_var) && !(group_var %in% selected_vars())) { - qenv <- teal.code::eval_code( - qenv, - substitute( - expr = ANL[[group_var]] <- anl_name[[group_var]], - env = list(group_var = group_var, anl_name = as.name(dataname)) - ) - ) - } - - new_col_name <- "**anyna**" - - qenv <- teal.code::eval_code( - qenv, - substitute( - expr = - create_cols_labels <- function(cols, just_label = FALSE) { - column_labels <- column_labels_value - column_labels[is.na(column_labels) | length(column_labels) == 0] <- "" - if (just_label) { - labels <- column_labels[cols] - } else { - labels <- ifelse(cols == new_col_name | cols == "", cols, paste0(column_labels[cols], " [", cols, "]")) - } - labels - }, - env = list( - new_col_name = new_col_name, - column_labels_value = c(teal.data::col_labels(data_r())[selected_vars()], - new_col_name = new_col_name - ) - ) - ) - ) - qenv - }) - - selected_vars <- reactive({ - req(input$variables_select) - keys <- data_keys() - vars <- unique(c(keys, input$variables_select)) - vars - }) - - vars_summary <- reactive({ - na_count <- data_r() %>% - sapply(function(x) mean(is.na(x)), USE.NAMES = TRUE) %>% - sort(decreasing = TRUE) - - tibble::tibble( - key = names(na_count), - value = unname(na_count), - label = cut(na_count, breaks = seq(from = 0, to = 1, by = 0.1), include.lowest = TRUE) - ) - }) - - # Keep encoding panel up-to-date - output$variables <- renderUI({ - choices <- split(x = vars_summary()$key, f = vars_summary()$label, drop = TRUE) %>% rev() - selected <- choices <- unname(unlist(choices)) - - teal.widgets::optionalSelectInput( - ns("variables_select"), - label = "Select variables", - label_help = HTML(paste0("Dataset: ", tags$code(dataname))), - choices = teal.transform::variable_choices(data_r(), choices), - selected = selected, - multiple = TRUE - ) - }) - - observeEvent(input$filter_na, { - choices <- vars_summary() %>% - dplyr::select(!!as.name("key")) %>% - getElement(name = 1) - - selected <- vars_summary() %>% - dplyr::filter(!!as.name("value") > 0) %>% - dplyr::select(!!as.name("key")) %>% - getElement(name = 1) - - teal.widgets::updateOptionalSelectInput( - session = session, - inputId = "variables_select", - choices = teal.transform::variable_choices(data_r()), - selected = restoreInput(ns("variables_select"), selected) - ) - }) - - output$group_by_var_ui <- renderUI({ - all_choices <- teal.transform::variable_choices(data_r()) - cat_choices <- all_choices[!sapply(data_r(), function(x) is.numeric(x) || inherits(x, "POSIXct"))] - validate( - need(cat_choices, "Dataset does not have any non-numeric or non-datetime variables to use to group data with") - ) - teal.widgets::optionalSelectInput( - ns("group_by_var"), - label = "Group by variable", - choices = cat_choices, - selected = `if`( - is.null(isolate(input$group_by_var)), - cat_choices[1], - isolate(input$group_by_var) - ), - multiple = FALSE, - label_help = paste0("Dataset: ", dataname) - ) - }) - - output$group_by_vals_ui <- renderUI({ - req(input$group_by_var) - - choices <- teal.transform::value_choices(data_r(), input$group_by_var, input$group_by_var) - prev_choices <- isolate(input$group_by_vals) - - # determine selected value based on filtered data - # display those previously selected values that are still available - 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 - ) { - # if not any previously selected value is available and the grouping variable is the same, - # then display NULL - NULL - } else { - # if new grouping variable (i.e. not any previously selected value is available), - # then display all choices - choices - } - - prev_group_by_var(input$group_by_var) # set current group_by_var - validate(need(length(choices) < 100, "Please select group-by variable with fewer than 100 unique values")) - teal.widgets::optionalSelectInput( - ns("group_by_vals"), - label = "Filter levels", - choices = choices, - selected = selected, - multiple = TRUE, - label_help = paste0("Dataset: ", dataname) - ) - }) - - combination_cutoff_q <- reactive({ - req(common_code_q()) - teal.code::eval_code( - common_code_q(), - quote( - combination_cutoff <- ANL %>% - dplyr::mutate_all(is.na) %>% - dplyr::group_by_all() %>% - dplyr::tally() %>% - dplyr::ungroup() - ) - ) - }) - - output$cutoff <- renderUI({ - x <- combination_cutoff_q()[["combination_cutoff"]]$n - - # select 10-th from the top - n <- length(x) - idx <- max(1, n - 10) - prev_value <- isolate(input$combination_cutoff) - value <- `if`( - is.null(prev_value) || prev_value > max(x) || prev_value < min(x), - sort(x, partial = idx)[idx], prev_value - ) - - teal.widgets::optionalSliderInputValMinMax( - ns("combination_cutoff"), - "Combination cut-off", - c(value, range(x)) - ) - }) - - # Common inputs in build functions - summary_type_r <- reactive(input$summary_type) - ggtheme_r <- reactive(input$ggtheme) - - # Building qenvs - summary_plot_q <- srv_summary_plot( - "summary_plot", - data_r, - data_keys, - common_code_q, - data_parent_keys, - ggplot2_args, - summary_type_r, - reactive(input$any_na), - ggtheme_r, - reactive(input$if_patients_plot) - ) - - combination_plot_q <- srv_combination_plot( - "combination_plot", - data_r, - common_code_q, - data_keys, - data_parent_keys, - combination_cutoff_q, - ggplot2_args, - summary_type_r, - reactive(input$combination_cutoff), - ggtheme_r, - reactive(input$variables_select) - ) - - summary_table_q <- srv_summary_table( - "summary_table", - data_r, - common_code_q, - data_keys, - data_parent_keys, - selected_vars, - summary_type_r, - reactive(input$group_by_var), - reactive(input$group_by_vals), - reactive(input$variables_select), - reactive(input$count_type) - ) - - by_subject_plot_q <- srv_by_subject_plot( - "by_subject_plot", - data_r, - common_code_q, - data_keys, - data_parent_keys, - ggplot2_args, - summary_type_r, - ggtheme_r - ) # Decorated outputs