diff --git a/CITATION.cff b/CITATION.cff index ed962eb8..419ad2b8 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -9,7 +9,7 @@ type: software license: AGPL-3.0-or-later title: 'FreesearchR: A free and open-source browser based data analysis tool for researchers with publication ready output' -version: 25.6.3 +version: 25.6.2 doi: 10.5281/zenodo.14527429 identifiers: - type: url @@ -1002,19 +1002,6 @@ references: email: russell-lenth@uiowa.edu year: '2025' doi: 10.32614/CRAN.package.emmeans -- type: software - title: visdat - abstract: 'visdat: Preliminary Visualisation of Data' - notes: Imports - url: https://docs.ropensci.org/visdat/ - repository: https://CRAN.R-project.org/package=visdat - authors: - - family-names: Tierney - given-names: Nicholas - email: nicholas.tierney@gmail.com - orcid: https://orcid.org/0000-0003-1460-8722 - year: '2025' - doi: 10.32614/CRAN.package.visdat - type: software title: styler abstract: 'styler: Non-Invasive Pretty Printing of R Code' diff --git a/NAMESPACE b/NAMESPACE index 2e380700..5419ce5a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -78,7 +78,6 @@ export(m_redcap_readUI) export(merge_expression) export(merge_long) export(missing_fraction) -export(missings_apex_plot) export(modal_create_column) export(modal_cut_variable) export(modal_update_factor) diff --git a/NEWS.md b/NEWS.md index 3ac90ee6..2b9c1524 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,10 +1,6 @@ # FreesearchR 25.6.3 -- *NEW* First go at introducing more options to evaluate missings. This has introduced a new dependency to use the visdat package and visualisation. The solution includes the option to visualise data classes and missingness as well as comparisons of variables by missing outcome variable or not to determine the nature of missingness. - -- *FIX* The REDCap import module has been updated visually and the PAI token is now hidden as a password. This module should still only be used when running locally if you are accessing sensitive data. - -- minor rewordings and updated UI. +- *NEW* First go at introducing more options to evaluate missings. Also reworded the text on the initial filter to only include variables missings less than the given threshold. # FreesearchR 25.6.2 diff --git a/R/hosted_version.R b/R/hosted_version.R index 9c90096e..6d5e327f 100644 --- a/R/hosted_version.R +++ b/R/hosted_version.R @@ -1 +1 @@ -hosted_version <- function()'v25.6.3-250625' +hosted_version <- function()'v25.6.3-250620' diff --git a/R/missings-module.R b/R/missings-module.R index 53ea9298..451d4abc 100644 --- a/R/missings-module.R +++ b/R/missings-module.R @@ -9,7 +9,8 @@ data_missings_ui <- function(id) { ns <- shiny::NS(id) shiny::tagList( - gt::gt_output(outputId = ns("missings_table")) + gt::gt_output(outputId = ns("missings_table")), + shiny::plotOutput(outputId = ns("missings_plot")) ) } @@ -23,56 +24,20 @@ data_missings_ui <- function(id) { #' @export data_missings_server <- function(id, data, - variable, ...) { shiny::moduleServer( id = id, module = function(input, output, session) { # ns <- session$ns - datar <- if (is.reactive(data)) data else reactive(data) - variabler <- if (is.reactive(variable)) variable else reactive(variable) - rv <- shiny::reactiveValues( data = NULL ) - rv$data <- shiny::reactive({ - df_tbl <- datar() - by_var <- variabler() + rv$data <- if (is.reactive(data)) data else reactive(data) - tryCatch( - { - if (!is.null(by_var) && by_var != "" && by_var %in% names(df_tbl)) { - df_tbl[[by_var]] <- ifelse(is.na(df_tbl[[by_var]]), "Missing", "Non-missing") - - out <- gtsummary::tbl_summary(df_tbl, by = by_var) |> - gtsummary::add_p() - } else { - out <- gtsummary::tbl_summary(df_tbl) - } - }, - error = function(err) { - showNotification(paste0("Error: ", err), type = "err") - } - ) - - out - }) - - output$missings_table <- gt::render_gt({ - shiny::req(datar) - shiny::req(variabler) - - if (is.null(variabler()) || variabler() == "" || !variabler() %in% names(datar())) { - title <- "No missing observations" - } else { - title <- paste("Missing vs non-missing observations in", variabler()) - } - - rv$data() |> - gtsummary::as_gt() |> - gt::tab_header(title = gt::md(title)) + output$missings_plot <- shiny::renderPlot({ + visdat::vis_dat(rv$data(),palette = "cb_safe") }) } ) @@ -86,24 +51,17 @@ missing_demo_app <- function() { label = "Browse data", width = "100%", disabled = FALSE - ), - shiny::selectInput( - inputId = "missings_var", - label = "Select variable to stratify analysis", choices = c("cyl", "vs") - ), - data_missings_ui("data") + )#, + # data_missings_ui("data") ) server <- function(input, output, session) { data_demo <- mtcars - data_demo[sample(1:32, 10), "cyl"] <- NA - data_demo[sample(1:32, 8), "vs"] <- NA - - data_missings_server(id = "data", data = data_demo, variable = shiny::reactive(input$missings_var)) + data_demo[2:4, "cyl"] <- NA observeEvent(input$modal_missings, { tryCatch( { - modal_visual_missings(data = data_demo, id = "modal_missings") + modal_data_missings(data = data_demo, id = "modal_missings") }, error = function(err) { showNotification(paste0("We encountered the following error browsing your data: ", err), type = "err") @@ -117,22 +75,20 @@ missing_demo_app <- function() { missing_demo_app() -modal_visual_missings <- function(data, - title = "Visual overview of data classes and missing observations", - easyClose = TRUE, - size = "xl", - footer = NULL, - ...) { +modal_data_missings <- function(data, + title = "Show missing pattern", + easyClose = TRUE, + size = "xl", + footer = NULL, + ...) { + datar <- if (is.reactive(data)) data else reactive(data) showModal(modalDialog( title = tagList(title, datamods:::button_close_modal()), tags$div( - # apexcharter::renderApexchart({ - # missings_apex_plot(datar(), ...) - # }) shiny::renderPlot({ - visdat::vis_dat(datar(),sort_type = FALSE) + + visdat::vis_dat(datar())+ ggplot2::guides(fill = ggplot2::guide_legend(title = "Data class")) + # ggplot2::theme_void() + ggplot2::theme( @@ -141,7 +97,7 @@ modal_visual_missings <- function(data, panel.grid.minor = ggplot2::element_blank(), # axis.text.y = element_blank(), # axis.title.y = element_blank(), - text = ggplot2::element_text(size = 18), + text = ggplot2::element_text(size = 15), # axis.text = ggplot2::element_blank(), # panel.background = ggplot2::element_rect(fill = "white"), # plot.background = ggplot2::element_rect(fill = "white"), @@ -155,99 +111,3 @@ modal_visual_missings <- function(data, footer = footer )) } - - -## Slow with many observations... - -#' Plot missings and class with apexcharter -#' -#' @param data data frame -#' -#' @returns An [apexchart()] `htmlwidget` object. -#' @export -#' -#' @examples -#' data_demo <- mtcars -#' data_demo[2:4, "cyl"] <- NA -#' rbind(data_demo, data_demo, data_demo, data_demo) |> missings_apex_plot() -#' data_demo |> missings_apex_plot() -#' mtcars |> missings_apex_plot(animation = TRUE) -#' # dplyr::storms |> missings_apex_plot() -#' visdat::vis_dat(dplyr::storms) -missings_apex_plot <- function(data, animation = FALSE, ...) { - browser() - - df_plot <- purrr::map_df(data, \(x){ - ifelse(is.na(x), - yes = NA, - no = glue::glue_collapse(class(x), - sep = "\n" - ) - ) - }) %>% - dplyr::mutate(rows = dplyr::row_number()) %>% - tidyr::pivot_longer( - cols = -rows, - names_to = "variable", values_to = "valueType", values_transform = list(valueType = as.character) - ) %>% - dplyr::arrange(rows, variable, valueType) - - - df_plot$valueType_num <- df_plot$valueType |> - forcats::as_factor() |> - as.numeric() - - - df_plot$valueType[is.na(df_plot$valueType)] <- "NA" - df_plot$valueType_num[is.na(df_plot$valueType_num)] <- max(df_plot$valueType_num, na.rm = TRUE) + 1 - - labels <- setNames(unique(df_plot$valueType_num), unique(df_plot$valueType)) - - if (any(df_plot$valueType == "NA")) { - colors <- setNames(c(viridisLite::viridis(n = length(labels) - 1), "#999999"), names(labels)) - } else { - colors <- setNames(viridisLite::viridis(n = length(labels)), names(labels)) - } - - - label_list <- labels |> - purrr::imap(\(.x, .i){ - list( - from = .x, - to = .x, - color = colors[[.i]], - name = .i - ) - }) |> - setNames(NULL) - - out <- apexcharter::apex( - data = df_plot, - type = "heatmap", - mapping = apexcharter::aes(x = variable, y = rows, fill = valueType_num), - ... - ) %>% - apexcharter::ax_stroke(width = NULL) |> - apexcharter::ax_plotOptions( - heatmap = apexcharter::heatmap_opts( - radius = 0, - enableShades = FALSE, - colorScale = list( - ranges = label_list - ), - useFillColorAsStroke = TRUE - ) - ) %>% - apexcharter::ax_dataLabels(enabled = FALSE) |> - apexcharter::ax_tooltip( - enabled = FALSE, - intersect = FALSE - ) - - if (!isTRUE(animation)) { - out <- out |> - apexcharter::ax_chart(animations = list(enabled = FALSE)) - } - - out -} diff --git a/R/redcap_read_shiny_module.R b/R/redcap_read_shiny_module.R index 19e1123e..9499e7d3 100644 --- a/R/redcap_read_shiny_module.R +++ b/R/redcap_read_shiny_module.R @@ -18,25 +18,18 @@ m_redcap_readUI <- function(id, title = TRUE, url = NULL) { } server_ui <- shiny::tagList( + # width = 6, shiny::tags$h4("REDCap server"), shiny::textInput( inputId = ns("uri"), label = "Web address", - value = if_not_missing(url, "https://redcap.your.institution/"), - width = "100%" + value = if_not_missing(url, "https://redcap.your.institution/") ), shiny::helpText("Format should be either 'https://redcap.your.institution/' or 'https://your.institution/redcap/'"), - # shiny::textInput( - # inputId = ns("api"), - # label = "API token", - # value = "", - # width = "100%" - # ), - shiny::passwordInput( + shiny::textInput( inputId = ns("api"), label = "API token", - value = "", - width = "100%" + value = "" ), shiny::helpText("The token is a string of 32 numbers and letters."), shiny::br(), @@ -74,34 +67,31 @@ m_redcap_readUI <- function(id, title = TRUE, url = NULL) { params_ui <- shiny::tagList( + # width = 6, shiny::tags$h4("Data import parameters"), - shiny::tags$div( - style = htmltools::css( - display = "grid", - gridTemplateColumns = "1fr 50px", - gridColumnGap = "10px" - ), - shiny::uiOutput(outputId = ns("fields")), - shiny::tags$div( - class = "shiny-input-container", - shiny::tags$label( - class = "control-label", - `for` = ns("dropdown_params"), - "...", - style = htmltools::css(visibility = "hidden") - ), - shinyWidgets::dropMenu( - shiny::actionButton( - inputId = ns("dropdown_params"), - label = shiny::icon("filter"), - width = "50px" - ), - filter_ui - ) - ) - ), - shiny::helpText("Select fields/variables to import and click the funnel to apply optional filters"), + shiny::helpText("Options here will show, when API and uri are typed"), shiny::tags$br(), + shiny::uiOutput(outputId = ns("fields")), + shiny::tags$div( + class = "shiny-input-container", + shiny::tags$label( + class = "control-label", + `for` = ns("dropdown_params"), + "...", + style = htmltools::css(visibility = "hidden") + ), + shinyWidgets::dropMenu( + shiny::actionButton( + inputId = ns("dropdown_params"), + label = "Add data filters", + icon = shiny::icon("filter"), + width = "100%", + class = "px-1" + ), + filter_ui + ), + shiny::helpText("Optionally filter project arms if logitudinal or apply server side data filters") + ), shiny::tags$br(), shiny::uiOutput(outputId = ns("data_type")), shiny::uiOutput(outputId = ns("fill")), @@ -122,14 +112,28 @@ m_redcap_readUI <- function(id, title = TRUE, url = NULL) { tags$p(phosphoricons::ph("info", weight = "bold"), "Please specify data to download, then press 'Import'.") ), dismissible = TRUE - ) + ) # , + ## TODO: Use busy indicator like on download to have button activate/deactivate + # bslib::input_task_button( + # id = ns("data_import"), + # label = "Import", + # icon = shiny::icon("download", lib = "glyphicon"), + # label_busy = "Just a minute...", + # icon_busy = fontawesome::fa_i("arrows-rotate", + # class = "fa-spin", + # "aria-hidden" = "true" + # ), + # type = "primary", + # auto_reset = TRUE#,state="busy" + # ), + # shiny::br(), + # shiny::helpText("Press 'Import' to get data from the REDCap server. Check the preview below before proceeding.") ) shiny::fluidPage( title = title, server_ui, - # shiny::uiOutput(ns("params_ui")), shiny::conditionalPanel( condition = "output.connect_success == true", params_ui, @@ -253,7 +257,6 @@ m_redcap_readServer <- function(id) { output$connect_success <- shiny::reactive(identical(data_rv$dd_status, "success")) shiny::outputOptions(output, "connect_success", suspendWhenHidden = FALSE) - shiny::observeEvent(input$see_dd, { show_data( purrr::pluck(data_rv$dd_list, "data"), @@ -289,7 +292,7 @@ m_redcap_readServer <- function(id) { shiny::req(data_rv$dd_list) shinyWidgets::virtualSelectInput( inputId = ns("fields"), - label = "Select fields/variables to import:", + label = "Select variables to import:", choices = purrr::pluck(data_rv$dd_list, "data") |> dplyr::select(field_name, form_name) |> (\(.x){ @@ -298,8 +301,7 @@ m_redcap_readServer <- function(id) { updateOn = "change", multiple = TRUE, search = TRUE, - showValueAsTags = TRUE, - width = "100%" + showValueAsTags = TRUE ) }) @@ -308,14 +310,13 @@ m_redcap_readServer <- function(id) { if (isTRUE(data_rv$info$has_repeating_instruments_or_events)) { vectorSelectInput( inputId = ns("data_type"), - label = "Specify the data format", + label = "Select the data format to import", choices = c( "Wide data (One row for each subject)" = "wide", "Long data for project with repeating instruments (default REDCap)" = "long" ), selected = "wide", - multiple = FALSE, - width = "100%" + multiple = FALSE ) } }) @@ -341,8 +342,7 @@ m_redcap_readServer <- function(id) { "No, leave the data as is" = "no" ), selected = "no", - multiple = FALSE, - width = "100%" + multiple = FALSE ) } }) @@ -362,8 +362,7 @@ m_redcap_readServer <- function(id) { selected = NULL, label = "Filter by events/arms", choices = stats::setNames(arms()[[3]], arms()[[1]]), - multiple = TRUE, - width = "100%" + multiple = TRUE ) } }) diff --git a/R/sysdata.rda b/R/sysdata.rda index 4350625e..e1714c39 100644 Binary files a/R/sysdata.rda and b/R/sysdata.rda differ diff --git a/SESSION.md b/SESSION.md index e63210ee..d7a6af1f 100644 --- a/SESSION.md +++ b/SESSION.md @@ -11,11 +11,11 @@ |collate |en_US.UTF-8 | |ctype |en_US.UTF-8 | |tz |Europe/Copenhagen | -|date |2025-06-25 | +|date |2025-06-06 | |rstudio |2025.05.0+496 Mariposa Orchid (desktop) | |pandoc |3.6.4 @ /opt/homebrew/bin/ (via rmarkdown) | |quarto |1.7.30 @ /usr/local/bin/quarto | -|FreesearchR |25.6.3.250625 | +|FreesearchR |25.6.2.250606 | -------------------------------------------------------------------------------- @@ -43,6 +43,7 @@ |cardx |0.2.4 |2025-04-12 |CRAN (R 4.4.1) | |caTools |1.18.3 |2024-09-04 |CRAN (R 4.4.1) | |cellranger |1.1.0 |2016-07-27 |CRAN (R 4.4.0) | +|cffr |1.2.0 |2025-01-25 |CRAN (R 4.4.1) | |checkmate |2.3.2 |2024-07-29 |CRAN (R 4.4.0) | |class |7.3-23 |2025-01-01 |CRAN (R 4.4.1) | |classInt |0.4-11 |2025-01-08 |CRAN (R 4.4.1) | @@ -75,7 +76,6 @@ |farver |2.1.2 |2024-05-13 |CRAN (R 4.4.1) | |fastmap |1.2.0 |2024-05-15 |CRAN (R 4.4.1) | |flextable |0.9.7 |2024-10-27 |CRAN (R 4.4.1) | -|fontawesome |0.5.3 |2024-11-16 |CRAN (R 4.4.1) | |fontBitstreamVera |0.1.1 |2017-02-01 |CRAN (R 4.4.1) | |fontLiberation |0.1.0 |2016-10-15 |CRAN (R 4.4.1) | |fontquiver |0.2.1 |2017-02-01 |CRAN (R 4.4.0) | @@ -83,7 +83,7 @@ |foreach |1.5.2 |2022-02-02 |CRAN (R 4.4.0) | |foreign |0.8-90 |2025-03-31 |CRAN (R 4.4.1) | |Formula |1.2-5 |2023-02-24 |CRAN (R 4.4.1) | -|FreesearchR |25.6.3 |NA |NA | +|FreesearchR |25.6.2 |NA |NA | |fs |1.6.6 |2025-04-12 |CRAN (R 4.4.1) | |gdtools |0.4.2 |2025-03-27 |CRAN (R 4.4.1) | |generics |0.1.3 |2022-07-05 |CRAN (R 4.4.1) | @@ -106,24 +106,21 @@ |htmltools |0.5.8.1 |2024-04-04 |CRAN (R 4.4.1) | |htmlwidgets |1.6.4 |2023-12-06 |CRAN (R 4.4.0) | |httpuv |1.6.16 |2025-04-16 |CRAN (R 4.4.1) | -|httr |1.4.7 |2023-08-15 |CRAN (R 4.4.0) | |IDEAFilter |0.2.0 |2024-04-15 |CRAN (R 4.4.0) | |insight |1.2.0 |2025-04-22 |CRAN (R 4.4.1) | |iterators |1.0.14 |2022-02-05 |CRAN (R 4.4.1) | |jquerylib |0.1.4 |2021-04-26 |CRAN (R 4.4.0) | |jsonlite |2.0.0 |2025-03-27 |CRAN (R 4.4.1) | +|jsonvalidate |1.5.0 |2025-02-07 |CRAN (R 4.4.1) | |KernSmooth |2.23-26 |2025-01-01 |CRAN (R 4.4.1) | |keyring |1.3.2 |2023-12-11 |CRAN (R 4.4.0) | |knitr |1.50 |2025-03-16 |CRAN (R 4.4.1) | -|labeling |0.4.3 |2023-08-29 |CRAN (R 4.4.1) | |later |1.4.2 |2025-04-08 |CRAN (R 4.4.1) | |lattice |0.22-7 |2025-04-02 |CRAN (R 4.4.1) | |lifecycle |1.0.4 |2023-11-07 |CRAN (R 4.4.1) | -|litedown |0.7 |2025-04-08 |CRAN (R 4.4.1) | |lme4 |1.1-37 |2025-03-26 |CRAN (R 4.4.1) | |lubridate |1.9.4 |2024-12-08 |CRAN (R 4.4.1) | |magrittr |2.0.3 |2022-03-30 |CRAN (R 4.4.1) | -|markdown |2.0 |2025-03-23 |CRAN (R 4.4.1) | |MASS |7.3-65 |2025-02-28 |CRAN (R 4.4.1) | |Matrix |1.7-3 |2025-03-11 |CRAN (R 4.4.1) | |memoise |2.0.1 |2021-11-26 |CRAN (R 4.4.0) | @@ -138,7 +135,6 @@ |opdisDownsampling |1.0.1 |2024-04-15 |CRAN (R 4.4.0) | |openssl |2.3.2 |2025-02-03 |CRAN (R 4.4.1) | |openxlsx2 |1.15 |2025-04-25 |CRAN (R 4.4.1) | -|pak |0.8.0.2 |2025-04-08 |CRAN (R 4.4.1) | |parameters |0.24.2 |2025-03-04 |CRAN (R 4.4.1) | |patchwork |1.3.0 |2024-09-16 |CRAN (R 4.4.1) | |pbmcapply |1.5.1 |2022-04-28 |CRAN (R 4.4.1) | @@ -160,13 +156,10 @@ |qqconf |1.3.2 |2023-04-14 |CRAN (R 4.4.0) | |qqplotr |0.0.6 |2023-01-25 |CRAN (R 4.4.0) | |quarto |1.4.4 |2024-07-20 |CRAN (R 4.4.0) | -|R.cache |0.16.0 |2022-07-21 |CRAN (R 4.4.0) | -|R.methodsS3 |1.8.2 |2022-06-13 |CRAN (R 4.4.1) | -|R.oo |1.27.0 |2024-11-01 |CRAN (R 4.4.1) | -|R.utils |2.13.0 |2025-02-24 |CRAN (R 4.4.1) | |R6 |2.6.1 |2025-02-15 |CRAN (R 4.4.1) | |ragg |1.4.0 |2025-04-10 |CRAN (R 4.4.1) | |rankinPlot |1.1.0 |2023-01-30 |CRAN (R 4.4.0) | +|rappdirs |0.3.3 |2021-01-31 |CRAN (R 4.4.1) | |rbibutils |2.3 |2024-10-04 |CRAN (R 4.4.1) | |RColorBrewer |1.1-3 |2022-04-03 |CRAN (R 4.4.1) | |Rcpp |1.0.14 |2025-01-12 |CRAN (R 4.4.1) | @@ -198,13 +191,11 @@ |sessioninfo |1.2.3 |2025-02-05 |CRAN (R 4.4.1) | |shiny |1.10.0 |2024-12-14 |CRAN (R 4.4.1) | |shinybusy |0.3.3 |2024-03-09 |CRAN (R 4.4.0) | -|shinydashboard |0.7.3 |NA |NA | |shinyjs |2.1.0 |2021-12-23 |CRAN (R 4.4.0) | |shinyTime |1.0.3 |2022-08-19 |CRAN (R 4.4.0) | |shinyWidgets |0.9.0 |2025-02-21 |CRAN (R 4.4.1) | |stringi |1.8.7 |2025-03-27 |CRAN (R 4.4.1) | |stringr |1.5.1 |2023-11-14 |CRAN (R 4.4.0) | -|styler |1.10.3 |2024-04-07 |CRAN (R 4.4.0) | |systemfonts |1.2.2 |2025-04-04 |CRAN (R 4.4.1) | |testthat |3.2.3 |2025-01-13 |CRAN (R 4.4.1) | |textshaping |1.0.0 |2025-01-20 |CRAN (R 4.4.1) | @@ -220,8 +211,8 @@ |urlchecker |1.0.1 |2021-11-30 |CRAN (R 4.4.1) | |usethis |3.1.0 |2024-11-26 |CRAN (R 4.4.1) | |uuid |1.2-1 |2024-07-29 |CRAN (R 4.4.1) | +|V8 |6.0.3 |2025-03-26 |CRAN (R 4.4.1) | |vctrs |0.6.5 |2023-12-01 |CRAN (R 4.4.0) | -|visdat |0.6.0 |2023-02-02 |CRAN (R 4.4.0) | |vroom |1.6.5 |2023-12-05 |CRAN (R 4.4.0) | |withr |3.0.2 |2024-10-28 |CRAN (R 4.4.1) | |writexl |1.5.4 |2025-04-15 |CRAN (R 4.4.1) | diff --git a/inst/apps/FreesearchR/app.R b/inst/apps/FreesearchR/app.R index 8fbf7426..51b30506 100644 --- a/inst/apps/FreesearchR/app.R +++ b/inst/apps/FreesearchR/app.R @@ -3996,7 +3996,7 @@ simple_snake <- function(data){ #### Current file: /Users/au301842/FreesearchR/R//hosted_version.R ######## -hosted_version <- function()'v25.6.3-250625' +hosted_version <- function()'v25.6.3-250620' ######## @@ -4670,7 +4670,8 @@ data_missings_ui <- function(id) { ns <- shiny::NS(id) shiny::tagList( - gt::gt_output(outputId = ns("missings_table")) + gt::gt_output(outputId = ns("missings_table")), + shiny::plotOutput(outputId = ns("missings_plot")) ) } @@ -4684,56 +4685,20 @@ data_missings_ui <- function(id) { #' @export data_missings_server <- function(id, data, - variable, ...) { shiny::moduleServer( id = id, module = function(input, output, session) { # ns <- session$ns - datar <- if (is.reactive(data)) data else reactive(data) - variabler <- if (is.reactive(variable)) variable else reactive(variable) - rv <- shiny::reactiveValues( data = NULL ) - rv$data <- shiny::reactive({ - df_tbl <- datar() - by_var <- variabler() + rv$data <- if (is.reactive(data)) data else reactive(data) - tryCatch( - { - if (!is.null(by_var) && by_var != "" && by_var %in% names(df_tbl)) { - df_tbl[[by_var]] <- ifelse(is.na(df_tbl[[by_var]]), "Missing", "Non-missing") - - out <- gtsummary::tbl_summary(df_tbl, by = by_var) |> - gtsummary::add_p() - } else { - out <- gtsummary::tbl_summary(df_tbl) - } - }, - error = function(err) { - showNotification(paste0("Error: ", err), type = "err") - } - ) - - out - }) - - output$missings_table <- gt::render_gt({ - shiny::req(datar) - shiny::req(variabler) - - if (is.null(variabler()) || variabler() == "" || !variabler() %in% names(datar())) { - title <- "No missing observations" - } else { - title <- paste("Missing vs non-missing observations in", variabler()) - } - - rv$data() |> - gtsummary::as_gt() |> - gt::tab_header(title = gt::md(title)) + output$missings_plot <- shiny::renderPlot({ + visdat::vis_dat(rv$data(),palette = "cb_safe") }) } ) @@ -4747,24 +4712,17 @@ missing_demo_app <- function() { label = "Browse data", width = "100%", disabled = FALSE - ), - shiny::selectInput( - inputId = "missings_var", - label = "Select variable to stratify analysis", choices = c("cyl", "vs") - ), - data_missings_ui("data") + )#, + # data_missings_ui("data") ) server <- function(input, output, session) { data_demo <- mtcars - data_demo[sample(1:32, 10), "cyl"] <- NA - data_demo[sample(1:32, 8), "vs"] <- NA - - data_missings_server(id = "data", data = data_demo, variable = shiny::reactive(input$missings_var)) + data_demo[2:4, "cyl"] <- NA observeEvent(input$modal_missings, { tryCatch( { - modal_visual_missings(data = data_demo, id = "modal_missings") + modal_data_missings(data = data_demo, id = "modal_missings") }, error = function(err) { showNotification(paste0("We encountered the following error browsing your data: ", err), type = "err") @@ -4778,22 +4736,20 @@ missing_demo_app <- function() { missing_demo_app() -modal_visual_missings <- function(data, - title = "Visual overview of data classes and missing observations", - easyClose = TRUE, - size = "xl", - footer = NULL, - ...) { +modal_data_missings <- function(data, + title = "Show missing pattern", + easyClose = TRUE, + size = "xl", + footer = NULL, + ...) { + datar <- if (is.reactive(data)) data else reactive(data) showModal(modalDialog( title = tagList(title, datamods:::button_close_modal()), tags$div( - # apexcharter::renderApexchart({ - # missings_apex_plot(datar(), ...) - # }) shiny::renderPlot({ - visdat::vis_dat(datar(),sort_type = FALSE) + + visdat::vis_dat(datar())+ ggplot2::guides(fill = ggplot2::guide_legend(title = "Data class")) + # ggplot2::theme_void() + ggplot2::theme( @@ -4802,7 +4758,7 @@ modal_visual_missings <- function(data, panel.grid.minor = ggplot2::element_blank(), # axis.text.y = element_blank(), # axis.title.y = element_blank(), - text = ggplot2::element_text(size = 18), + text = ggplot2::element_text(size = 15), # axis.text = ggplot2::element_blank(), # panel.background = ggplot2::element_rect(fill = "white"), # plot.background = ggplot2::element_rect(fill = "white"), @@ -4818,102 +4774,6 @@ modal_visual_missings <- function(data, } -## Slow with many observations... - -#' Plot missings and class with apexcharter -#' -#' @param data data frame -#' -#' @returns An [apexchart()] `htmlwidget` object. -#' @export -#' -#' @examples -#' data_demo <- mtcars -#' data_demo[2:4, "cyl"] <- NA -#' rbind(data_demo, data_demo, data_demo, data_demo) |> missings_apex_plot() -#' data_demo |> missings_apex_plot() -#' mtcars |> missings_apex_plot(animation = TRUE) -#' # dplyr::storms |> missings_apex_plot() -#' visdat::vis_dat(dplyr::storms) -missings_apex_plot <- function(data, animation = FALSE, ...) { - browser() - - df_plot <- purrr::map_df(data, \(x){ - ifelse(is.na(x), - yes = NA, - no = glue::glue_collapse(class(x), - sep = "\n" - ) - ) - }) %>% - dplyr::mutate(rows = dplyr::row_number()) %>% - tidyr::pivot_longer( - cols = -rows, - names_to = "variable", values_to = "valueType", values_transform = list(valueType = as.character) - ) %>% - dplyr::arrange(rows, variable, valueType) - - - df_plot$valueType_num <- df_plot$valueType |> - forcats::as_factor() |> - as.numeric() - - - df_plot$valueType[is.na(df_plot$valueType)] <- "NA" - df_plot$valueType_num[is.na(df_plot$valueType_num)] <- max(df_plot$valueType_num, na.rm = TRUE) + 1 - - labels <- setNames(unique(df_plot$valueType_num), unique(df_plot$valueType)) - - if (any(df_plot$valueType == "NA")) { - colors <- setNames(c(viridisLite::viridis(n = length(labels) - 1), "#999999"), names(labels)) - } else { - colors <- setNames(viridisLite::viridis(n = length(labels)), names(labels)) - } - - - label_list <- labels |> - purrr::imap(\(.x, .i){ - list( - from = .x, - to = .x, - color = colors[[.i]], - name = .i - ) - }) |> - setNames(NULL) - - out <- apexcharter::apex( - data = df_plot, - type = "heatmap", - mapping = apexcharter::aes(x = variable, y = rows, fill = valueType_num), - ... - ) %>% - apexcharter::ax_stroke(width = NULL) |> - apexcharter::ax_plotOptions( - heatmap = apexcharter::heatmap_opts( - radius = 0, - enableShades = FALSE, - colorScale = list( - ranges = label_list - ), - useFillColorAsStroke = TRUE - ) - ) %>% - apexcharter::ax_dataLabels(enabled = FALSE) |> - apexcharter::ax_tooltip( - enabled = FALSE, - intersect = FALSE - ) - - if (!isTRUE(animation)) { - out <- out |> - apexcharter::ax_chart(animations = list(enabled = FALSE)) - } - - out -} - - ######## #### Current file: /Users/au301842/FreesearchR/R//plot_box.R ######## @@ -5683,25 +5543,18 @@ m_redcap_readUI <- function(id, title = TRUE, url = NULL) { } server_ui <- shiny::tagList( + # width = 6, shiny::tags$h4("REDCap server"), shiny::textInput( inputId = ns("uri"), label = "Web address", - value = if_not_missing(url, "https://redcap.your.institution/"), - width = "100%" + value = if_not_missing(url, "https://redcap.your.institution/") ), shiny::helpText("Format should be either 'https://redcap.your.institution/' or 'https://your.institution/redcap/'"), - # shiny::textInput( - # inputId = ns("api"), - # label = "API token", - # value = "", - # width = "100%" - # ), - shiny::passwordInput( + shiny::textInput( inputId = ns("api"), label = "API token", - value = "", - width = "100%" + value = "" ), shiny::helpText("The token is a string of 32 numbers and letters."), shiny::br(), @@ -5739,34 +5592,31 @@ m_redcap_readUI <- function(id, title = TRUE, url = NULL) { params_ui <- shiny::tagList( + # width = 6, shiny::tags$h4("Data import parameters"), - shiny::tags$div( - style = htmltools::css( - display = "grid", - gridTemplateColumns = "1fr 50px", - gridColumnGap = "10px" - ), - shiny::uiOutput(outputId = ns("fields")), - shiny::tags$div( - class = "shiny-input-container", - shiny::tags$label( - class = "control-label", - `for` = ns("dropdown_params"), - "...", - style = htmltools::css(visibility = "hidden") - ), - shinyWidgets::dropMenu( - shiny::actionButton( - inputId = ns("dropdown_params"), - label = shiny::icon("filter"), - width = "50px" - ), - filter_ui - ) - ) - ), - shiny::helpText("Select fields/variables to import and click the funnel to apply optional filters"), + shiny::helpText("Options here will show, when API and uri are typed"), shiny::tags$br(), + shiny::uiOutput(outputId = ns("fields")), + shiny::tags$div( + class = "shiny-input-container", + shiny::tags$label( + class = "control-label", + `for` = ns("dropdown_params"), + "...", + style = htmltools::css(visibility = "hidden") + ), + shinyWidgets::dropMenu( + shiny::actionButton( + inputId = ns("dropdown_params"), + label = "Add data filters", + icon = shiny::icon("filter"), + width = "100%", + class = "px-1" + ), + filter_ui + ), + shiny::helpText("Optionally filter project arms if logitudinal or apply server side data filters") + ), shiny::tags$br(), shiny::uiOutput(outputId = ns("data_type")), shiny::uiOutput(outputId = ns("fill")), @@ -5787,14 +5637,28 @@ m_redcap_readUI <- function(id, title = TRUE, url = NULL) { tags$p(phosphoricons::ph("info", weight = "bold"), "Please specify data to download, then press 'Import'.") ), dismissible = TRUE - ) + ) # , + ## TODO: Use busy indicator like on download to have button activate/deactivate + # bslib::input_task_button( + # id = ns("data_import"), + # label = "Import", + # icon = shiny::icon("download", lib = "glyphicon"), + # label_busy = "Just a minute...", + # icon_busy = fontawesome::fa_i("arrows-rotate", + # class = "fa-spin", + # "aria-hidden" = "true" + # ), + # type = "primary", + # auto_reset = TRUE#,state="busy" + # ), + # shiny::br(), + # shiny::helpText("Press 'Import' to get data from the REDCap server. Check the preview below before proceeding.") ) shiny::fluidPage( title = title, server_ui, - # shiny::uiOutput(ns("params_ui")), shiny::conditionalPanel( condition = "output.connect_success == true", params_ui, @@ -5918,7 +5782,6 @@ m_redcap_readServer <- function(id) { output$connect_success <- shiny::reactive(identical(data_rv$dd_status, "success")) shiny::outputOptions(output, "connect_success", suspendWhenHidden = FALSE) - shiny::observeEvent(input$see_dd, { show_data( purrr::pluck(data_rv$dd_list, "data"), @@ -5954,7 +5817,7 @@ m_redcap_readServer <- function(id) { shiny::req(data_rv$dd_list) shinyWidgets::virtualSelectInput( inputId = ns("fields"), - label = "Select fields/variables to import:", + label = "Select variables to import:", choices = purrr::pluck(data_rv$dd_list, "data") |> dplyr::select(field_name, form_name) |> (\(.x){ @@ -5963,8 +5826,7 @@ m_redcap_readServer <- function(id) { updateOn = "change", multiple = TRUE, search = TRUE, - showValueAsTags = TRUE, - width = "100%" + showValueAsTags = TRUE ) }) @@ -5973,14 +5835,13 @@ m_redcap_readServer <- function(id) { if (isTRUE(data_rv$info$has_repeating_instruments_or_events)) { vectorSelectInput( inputId = ns("data_type"), - label = "Specify the data format", + label = "Select the data format to import", choices = c( "Wide data (One row for each subject)" = "wide", "Long data for project with repeating instruments (default REDCap)" = "long" ), selected = "wide", - multiple = FALSE, - width = "100%" + multiple = FALSE ) } }) @@ -6006,8 +5867,7 @@ m_redcap_readServer <- function(id) { "No, leave the data as is" = "no" ), selected = "no", - multiple = FALSE, - width = "100%" + multiple = FALSE ) } }) @@ -6027,8 +5887,7 @@ m_redcap_readServer <- function(id) { selected = NULL, label = "Filter by events/arms", choices = stats::setNames(arms()[[3]], arms()[[1]]), - multiple = TRUE, - width = "100%" + multiple = TRUE ) } }) @@ -9694,7 +9553,7 @@ ui_elements <- list( id = "redcap-warning", status = "info", shiny::tags$h2(shiny::markdown("Careful with sensitive data")), - shiny::tags$p("The", shiny::tags$i(shiny::tags$b("FreesearchR")), "app only stores data for analyses, but please only use with sensitive data when running locally.", "", shiny::tags$a("Read more here", href = "https://agdamsbo.github.io/FreesearchR/#run-locally-on-your-own-machine"), "."), + shiny::tags$p("The", shiny::tags$i(shiny::tags$b("FreesearchR")), "app only stores data for analyses, but please only use with sensitive data when running locally.", "", shiny::tags$a("Read more here", href = "https://agdamsbo.github.io/FreesearchR/#run-locally-on-your-own-machine"),"."), dismissible = TRUE ), m_redcap_readUI( @@ -9712,14 +9571,6 @@ ui_elements <- list( # ), shiny::conditionalPanel( condition = "output.data_loaded == true", - shiny::br(), - shiny::actionButton( - inputId = "modal_initial_view", - label = "Quick overview", - width = "100%", - icon = shiny::icon("binoculars"), - disabled = FALSE - ), shiny::br(), shiny::br(), shiny::h5("Select variables for final import"), @@ -9739,7 +9590,7 @@ ui_elements <- list( format = shinyWidgets::wNumbFormat(decimals = 0), color = datamods:::get_primary_color() ), - shiny::helpText("Only include variables missing less observations than the specified percentage."), + shiny::helpText("Only include variables with missingness below the specified percentage."), shiny::br() ), shiny::column( @@ -9750,24 +9601,22 @@ ui_elements <- list( shiny::br() ) ), - shiny::uiOutput(outputId = "data_info_import", inline = TRUE), - shiny::br(), - shiny::br(), - shiny::actionButton( - inputId = "act_start", - label = "Start", - width = "100%", - icon = shiny::icon("play"), - disabled = TRUE - ), - shiny::helpText('After importing, hit "Start" or navigate to the desired tab.'), - shiny::br(), - shiny::br() + shiny::uiOutput(outputId = "data_info_import", inline = TRUE) ), + shiny::br(), + shiny::br(), + shiny::actionButton( + inputId = "act_start", + label = "Start", + width = "100%", + icon = shiny::icon("play"), + disabled = TRUE + ), + shiny::helpText('After importing, hit "Start" or navigate to the desired tab.'), + shiny::br(), + shiny::br(), shiny::column(width = 2) - ), - shiny::br(), - shiny::br() + ) ) ), ############################################################################## @@ -9790,8 +9639,19 @@ ui_elements <- list( width = 9, shiny::uiOutput(outputId = "data_info", inline = TRUE), shiny::tags$p( - "Below is a short summary table, on the right you can click to visualise data classes or browse data and create different data filters." + "Below is a short summary table, on the right you can click to browse data and create data filters." ) + ) + ), + fluidRow( + shiny::column( + width = 9, + data_summary_ui(id = "data_summary"), + shiny::br(), + shiny::br(), + shiny::br(), + shiny::br(), + shiny::br() ), shiny::column( width = 3, @@ -9810,41 +9670,10 @@ ui_elements <- list( disabled = TRUE ), shiny::br(), - shiny::br() - ) - ), - fluidRow( - shiny::column( - width = 9, - data_summary_ui(id = "data_summary"), shiny::br(), - shiny::br(), - shiny::br(), - shiny::br(), - shiny::br() - ), - shiny::column( - width = 3, - # shiny::actionButton( - # inputId = "modal_missings", - # label = "Visual overview", - # width = "100%", - # disabled = TRUE - # ), - # shiny::br(), - # shiny::br(), - # shiny::actionButton( - # inputId = "modal_browse", - # label = "Browse data", - # width = "100%", - # disabled = TRUE - # ), - # shiny::br(), - # shiny::br(), shiny::tags$h6("Filter data types"), shiny::uiOutput( - outputId = "column_filter" - ), + outputId = "column_filter"), shiny::helpText("Read more on how ", tags$a( "data types", href = "https://agdamsbo.github.io/FreesearchR/articles/data-types.html", @@ -9853,7 +9682,7 @@ ui_elements <- list( ), " are defined."), shiny::br(), shiny::br(), - shiny::tags$h6("Filter observations"), + shiny::tags$h6("Create data filters"), shiny::tags$p("Filter on observation level"), IDEAFilter::IDEAFilter_ui("data_filter"), shiny::br(), @@ -10021,13 +9850,6 @@ ui_elements <- list( color = datamods:::get_primary_color() ), shiny::helpText("Set the cut-off for considered 'highly correlated'.") - ), - bslib::accordion_panel( - vlaue = "acc_mis", - title = "Missings", - icon = bsicons::bs_icon("x-circle"), - shiny::uiOutput("missings_var"), - shiny::helpText("To consider if daata is missing by random, choose the outcome/dependent variable, if it has any missings to evaluate if there is a significant difference across other variables depending on missing data or not.") ) ) ), @@ -10038,10 +9860,6 @@ ui_elements <- list( bslib::nav_panel( title = "Correlations", data_correlations_ui(id = "correlations", height = 600) - ), - bslib::nav_panel( - title = "Missings", - data_missings_ui(id = "missingness") ) ) ), @@ -10271,6 +10089,9 @@ ui <- bslib::page_fixed( #### Current file: /Users/au301842/FreesearchR/app/server.R ######## + + + data(mtcars) # trial <- gtsummary::trial @@ -10410,21 +10231,6 @@ server <- function(input, output, session) { rv$code <- modifyList(x = rv$code, list(import = from_env$name())) }) - observeEvent(input$modal_initial_view, { - tryCatch( - { - modal_visual_missings( - data = default_parsing(rv$data_temp), - footer = NULL, - size = "xl" - ) - }, - error = function(err) { - showNotification(paste0("We encountered the following error showing missingness: ", err), type = "err") - } - ) - }) - output$import_var <- shiny::renderUI({ shiny::req(rv$data_temp) @@ -10744,11 +10550,8 @@ server <- function(input, output, session) { observeEvent(input$modal_missings, { tryCatch( { - modal_visual_missings( - data = REDCapCAST::fct_drop(rv$data_filtered), - footer = "Here is an overview of how your data is interpreted, and where data is missing. Use this information to consider if data is missing at random or if some observations are missing systematically wich may be caused by an observation bias.", - size = "xl" - ) + modal_data_missings(data = REDCapCAST::fct_drop(rv$data_filtered), + footer = "This pop-up gives you an overview of how your data is interpreted, and where data is missing. Use this information to consider if data is missing at random or if some observations are missing systematically wich may be caused by an observation bias.") }, error = function(err) { showNotification(paste0("We encountered the following error showing missingness: ", err), type = "err") @@ -10908,16 +10711,6 @@ server <- function(input, output, session) { } ) - output$table1 <- gt::render_gt({ - if (!is.null(rv$list$table1)) { - rv$list$table1 |> - gtsummary::as_gt() |> - gt::tab_header(gt::md("**Table 1: Baseline Characteristics**")) - } else { - return(NULL) - } - }) - output$outcome_var_cor <- shiny::renderUI({ columnSelectInput( inputId = "outcome_var_cor", @@ -10932,6 +10725,16 @@ server <- function(input, output, session) { ) }) + output$table1 <- gt::render_gt({ + if (!is.null(rv$list$table1)) { + rv$list$table1 |> + gtsummary::as_gt() |> + gt::tab_header(gt::md("**Table 1: Baseline Characteristics**")) + } else { + return(NULL) + } + }) + data_correlations_server( id = "correlations", data = shiny::reactive({ @@ -10945,24 +10748,6 @@ server <- function(input, output, session) { cutoff = shiny::reactive(input$cor_cutoff) ) - output$missings_var <- shiny::renderUI({ - columnSelectInput( - inputId = "missings_var", - label = "Select variable to stratify analysis", - data = shiny::reactive({ - shiny::req(rv$data_filtered) - rv$data_filtered[apply(rv$data_filtered,2,anyNA)] - })() - ) - }) - - data_missings_server( - id = "missingness", - data = shiny::reactive(rv$data_filtered), - variable = shiny::reactive(input$missings_var) - ) - - ############################################################################## ######### ######### Data visuals @@ -11058,8 +10843,8 @@ server <- function(input, output, session) { rv$list |> write_rmd( params.args = list( - regression.p = rv$list$regression$input$add_regression_p - ), + regression.p=rv$list$regression$input$add_regression_p + ), output_format = format, input = file.path(getwd(), "www/report.rmd") ) diff --git a/man/data-missings.Rd b/man/data-missings.Rd index 510d78b0..e79398fc 100644 --- a/man/data-missings.Rd +++ b/man/data-missings.Rd @@ -8,7 +8,7 @@ \usage{ data_missings_ui(id) -data_missings_server(id, data, variable, ...) +data_missings_server(id, data, ...) } \arguments{ \item{id}{Module id} diff --git a/man/missings_apex_plot.Rd b/man/missings_apex_plot.Rd deleted file mode 100644 index c9474534..00000000 --- a/man/missings_apex_plot.Rd +++ /dev/null @@ -1,26 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/missings-module.R -\name{missings_apex_plot} -\alias{missings_apex_plot} -\title{Plot missings and class with apexcharter} -\usage{ -missings_apex_plot(data, animation = FALSE, ...) -} -\arguments{ -\item{data}{data frame} -} -\value{ -An \code{\link[=apexchart]{apexchart()}} \code{htmlwidget} object. -} -\description{ -Plot missings and class with apexcharter -} -\examples{ -data_demo <- mtcars -data_demo[2:4, "cyl"] <- NA -rbind(data_demo, data_demo, data_demo, data_demo) |> missings_apex_plot() -data_demo |> missings_apex_plot() -mtcars |> missings_apex_plot(animation = TRUE) -# dplyr::storms |> missings_apex_plot() -visdat::vis_dat(dplyr::storms) -} diff --git a/renv.lock b/renv.lock index 442e69c8..074b3f4f 100644 --- a/renv.lock +++ b/renv.lock @@ -8917,54 +8917,6 @@ "Author": "Simon Garnier [aut, cre], Noam Ross [ctb, cph], Bob Rudis [ctb, cph], Marco Sciaini [ctb, cph], Antônio Pedro Camargo [ctb, cph], Cédric Scherer [ctb, cph]", "Repository": "CRAN" }, - "visdat": { - "Package": "visdat", - "Version": "0.6.0", - "Source": "Repository", - "Title": "Preliminary Visualisation of Data", - "Authors@R": "c( person(\"Nicholas\", \"Tierney\", role = c(\"aut\", \"cre\"), email = \"nicholas.tierney@gmail.com\", comment = c(ORCID = \"https://orcid.org/0000-0003-1460-8722\")), person(\"Sean\", \"Hughes\", role = \"rev\", comment =c(ORCID = \"https://orcid.org/0000-0002-9409-9405\", \"Sean Hughes reviewed the package for rOpenSci, see https://github.com/ropensci/onboarding/issues/87\")), person(\"Mara\", \"Averick\", role = \"rev\", comment = \"Mara Averick reviewed the package for rOpenSci, see https://github.com/ropensci/onboarding/issues/87\"), person(\"Stuart\", \"Lee\", role = c(\"ctb\")), person(\"Earo\", \"Wang\", role = c(\"ctb\")), person(\"Nic\", \"Crane\", role = c(\"ctb\")), person(\"Christophe\", \"Regouby\", role=c(\"ctb\")) )", - "Description": "Create preliminary exploratory data visualisations of an entire dataset to identify problems or unexpected features using 'ggplot2'.", - "Depends": [ - "R (>= 3.2.2)" - ], - "License": "MIT + file LICENSE", - "LazyData": "true", - "RoxygenNote": "7.2.3", - "Imports": [ - "ggplot2", - "tidyr", - "dplyr", - "purrr", - "readr", - "magrittr", - "stats", - "tibble", - "glue", - "forcats", - "cli", - "scales" - ], - "URL": "https://docs.ropensci.org/visdat/, https://github.com/ropensci/visdat", - "BugReports": "https://github.com/ropensci/visdat/issues", - "Suggests": [ - "testthat (>= 3.0.0)", - "plotly (>= 4.5.6)", - "knitr", - "rmarkdown", - "vdiffr", - "spelling", - "covr", - "stringr" - ], - "VignetteBuilder": "knitr", - "Encoding": "UTF-8", - "Language": "en-US", - "Config/testthat/edition": "3", - "NeedsCompilation": "no", - "Author": "Nicholas Tierney [aut, cre] (), Sean Hughes [rev] (, Sean Hughes reviewed the package for rOpenSci, see https://github.com/ropensci/onboarding/issues/87), Mara Averick [rev] (Mara Averick reviewed the package for rOpenSci, see https://github.com/ropensci/onboarding/issues/87), Stuart Lee [ctb], Earo Wang [ctb], Nic Crane [ctb], Christophe Regouby [ctb]", - "Maintainer": "Nicholas Tierney ", - "Repository": "CRAN" - }, "vroom": { "Package": "vroom", "Version": "1.6.5",