diff --git a/CITATION.cff b/CITATION.cff index 29039358..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.1 +version: 25.6.2 doi: 10.5281/zenodo.14527429 identifiers: - type: url diff --git a/DESCRIPTION b/DESCRIPTION index 5c9b2489..cebd5484 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: FreesearchR Title: A free and open-source browser based data analysis tool for researchers with publication ready output -Version: 25.6.1 +Version: 25.6.3 Authors@R: c( person("Andreas Gammelgaard", "Damsbo",email="agdamsbo@clin.au.dk", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-7559-1154")), @@ -64,7 +64,8 @@ Imports: RcppArmadillo, ggcorrplot, shinyjs, - emmeans + emmeans, + visdat Suggests: styler, devtools, diff --git a/NAMESPACE b/NAMESPACE index 8e8a7a23..5419ce5a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -30,6 +30,8 @@ export(cut_variable_ui) export(data_correlations_server) export(data_correlations_ui) export(data_description) +export(data_missings_server) +export(data_missings_ui) export(data_summary_server) export(data_summary_ui) export(data_type) diff --git a/NEWS.md b/NEWS.md index 05d0347f..2b9c1524 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,17 @@ +# FreesearchR 25.6.3 + +- *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 + +- *FIX* Added warning about only using REDCap with sensitive data running locally. THis applies to all data actually. Considering taking REDCap out in hosted version. Standalone app is in the works. + +- *FIX* Reworded the completeness filter to be on missingness, as this is a more commonly used concept. + +- *FIX* Improved layout around data filters to improve usage. + +- *FIX* Regression table in report respects inclusion of p-values or not. + # FreesearchR 25.6.1 - *FIX* big not allowing to browse data diff --git a/R/app_version.R b/R/app_version.R index e81f97f3..440e842f 100644 --- a/R/app_version.R +++ b/R/app_version.R @@ -1 +1 @@ -app_version <- function()'25.6.1' +app_version <- function()'25.6.3' diff --git a/R/helpers.R b/R/helpers.R index 73129194..cb2a1aa4 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -50,7 +50,7 @@ write_quarto <- function(data, ...) { ) } -write_rmd <- function(data, ...) { +write_rmd <- function(data, ..., params.args=NULL) { # Exports data to temporary location # # I assume this is more secure than putting it in the www folder and deleting @@ -65,7 +65,7 @@ write_rmd <- function(data, ...) { ## Ref: https://github.com/quarto-dev/quarto-cli/discussions/4041 ## Outputs to the same as the .qmd file rmarkdown::render( - params = list(data.file = "web_data.rds",version=app_version()), + params = modifyList(list(data.file = "web_data.rds",version=app_version()),params.args), # execute_params = list(data.file = temp), ... ) diff --git a/R/hosted_version.R b/R/hosted_version.R index 095d1d44..6d5e327f 100644 --- a/R/hosted_version.R +++ b/R/hosted_version.R @@ -1 +1 @@ -hosted_version <- function()'v25.6.1-250604' +hosted_version <- function()'v25.6.3-250620' diff --git a/R/missings-module.R b/R/missings-module.R new file mode 100644 index 00000000..451d4abc --- /dev/null +++ b/R/missings-module.R @@ -0,0 +1,113 @@ +#' Data correlations evaluation module +#' +#' @param id Module id +#' +#' @name data-missings +#' @returns Shiny ui module +#' @export +data_missings_ui <- function(id) { + ns <- shiny::NS(id) + + shiny::tagList( + gt::gt_output(outputId = ns("missings_table")), + shiny::plotOutput(outputId = ns("missings_plot")) + ) +} + + +#' +#' @param data data +#' @param output.format output format +#' +#' @name data-missings +#' @returns shiny server module +#' @export +data_missings_server <- function(id, + data, + ...) { + shiny::moduleServer( + id = id, + module = function(input, output, session) { + # ns <- session$ns + + rv <- shiny::reactiveValues( + data = NULL + ) + + rv$data <- if (is.reactive(data)) data else reactive(data) + + output$missings_plot <- shiny::renderPlot({ + visdat::vis_dat(rv$data(),palette = "cb_safe") + }) + } + ) +} + + +missing_demo_app <- function() { + ui <- shiny::fluidPage( + shiny::actionButton( + inputId = "modal_missings", + label = "Browse data", + width = "100%", + disabled = FALSE + )#, + # data_missings_ui("data") + ) + server <- function(input, output, session) { + data_demo <- mtcars + data_demo[2:4, "cyl"] <- NA + + observeEvent(input$modal_missings, { + tryCatch( + { + 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") + } + ) + }) + } + shiny::shinyApp(ui, server) +} + +missing_demo_app() + + +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( + shiny::renderPlot({ + visdat::vis_dat(datar())+ + ggplot2::guides(fill = ggplot2::guide_legend(title = "Data class")) + + # ggplot2::theme_void() + + ggplot2::theme( + # legend.position = "none", + panel.grid.major = ggplot2::element_blank(), + panel.grid.minor = ggplot2::element_blank(), + # axis.text.y = element_blank(), + # axis.title.y = element_blank(), + 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"), + # panel.border = ggplot2::element_blank() + plot.title = ggplot2::element_blank() + ) + }) + ), + easyClose = easyClose, + size = size, + footer = footer + )) +} diff --git a/R/sysdata.rda b/R/sysdata.rda index 66754e3c..e1714c39 100644 Binary files a/R/sysdata.rda and b/R/sysdata.rda differ diff --git a/SESSION.md b/SESSION.md index eeee39b9..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-04 | +|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.1.250604 | +|FreesearchR |25.6.2.250606 | -------------------------------------------------------------------------------- @@ -76,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) | @@ -84,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.1 |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) | @@ -160,6 +159,7 @@ |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) | diff --git a/inst/apps/FreesearchR/app.R b/inst/apps/FreesearchR/app.R index 235b1d45..51b30506 100644 --- a/inst/apps/FreesearchR/app.R +++ b/inst/apps/FreesearchR/app.R @@ -16,7 +16,7 @@ library(shiny) # library(readODS) # library(bslib) # library(assertthat) -# library(dplyr) +library(dplyr) # library(quarto) # library(here) # library(broom) @@ -31,7 +31,7 @@ library(datamods) library(shinyWidgets) # library(DT) # library(data.table) -# library(gtsummary) +library(gtsummary) library(bsicons) library(rlang) # library(datamods) @@ -49,7 +49,7 @@ library(rlang) #### Current file: /Users/au301842/FreesearchR/R//app_version.R ######## -app_version <- function()'25.6.1' +app_version <- function()'25.6.3' ######## @@ -3374,7 +3374,7 @@ write_quarto <- function(data, ...) { ) } -write_rmd <- function(data, ...) { +write_rmd <- function(data, ..., params.args=NULL) { # Exports data to temporary location # # I assume this is more secure than putting it in the www folder and deleting @@ -3389,7 +3389,7 @@ write_rmd <- function(data, ...) { ## Ref: https://github.com/quarto-dev/quarto-cli/discussions/4041 ## Outputs to the same as the .qmd file rmarkdown::render( - params = list(data.file = "web_data.rds",version=app_version()), + params = modifyList(list(data.file = "web_data.rds",version=app_version()),params.args), # execute_params = list(data.file = temp), ... ) @@ -3996,7 +3996,7 @@ simple_snake <- function(data){ #### Current file: /Users/au301842/FreesearchR/R//hosted_version.R ######## -hosted_version <- function()'v25.6.1-250604' +hosted_version <- function()'v25.6.3-250620' ######## @@ -4655,6 +4655,125 @@ launch_FreesearchR <- function(...){ +######## +#### Current file: /Users/au301842/FreesearchR/R//missings-module.R +######## + +#' Data correlations evaluation module +#' +#' @param id Module id +#' +#' @name data-missings +#' @returns Shiny ui module +#' @export +data_missings_ui <- function(id) { + ns <- shiny::NS(id) + + shiny::tagList( + gt::gt_output(outputId = ns("missings_table")), + shiny::plotOutput(outputId = ns("missings_plot")) + ) +} + + +#' +#' @param data data +#' @param output.format output format +#' +#' @name data-missings +#' @returns shiny server module +#' @export +data_missings_server <- function(id, + data, + ...) { + shiny::moduleServer( + id = id, + module = function(input, output, session) { + # ns <- session$ns + + rv <- shiny::reactiveValues( + data = NULL + ) + + rv$data <- if (is.reactive(data)) data else reactive(data) + + output$missings_plot <- shiny::renderPlot({ + visdat::vis_dat(rv$data(),palette = "cb_safe") + }) + } + ) +} + + +missing_demo_app <- function() { + ui <- shiny::fluidPage( + shiny::actionButton( + inputId = "modal_missings", + label = "Browse data", + width = "100%", + disabled = FALSE + )#, + # data_missings_ui("data") + ) + server <- function(input, output, session) { + data_demo <- mtcars + data_demo[2:4, "cyl"] <- NA + + observeEvent(input$modal_missings, { + tryCatch( + { + 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") + } + ) + }) + } + shiny::shinyApp(ui, server) +} + +missing_demo_app() + + +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( + shiny::renderPlot({ + visdat::vis_dat(datar())+ + ggplot2::guides(fill = ggplot2::guide_legend(title = "Data class")) + + # ggplot2::theme_void() + + ggplot2::theme( + # legend.position = "none", + panel.grid.major = ggplot2::element_blank(), + panel.grid.minor = ggplot2::element_blank(), + # axis.text.y = element_blank(), + # axis.title.y = element_blank(), + 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"), + # panel.border = ggplot2::element_blank() + plot.title = ggplot2::element_blank() + ) + }) + ), + easyClose = easyClose, + size = size, + footer = footer + )) +} + + ######## #### Current file: /Users/au301842/FreesearchR/R//plot_box.R ######## @@ -9404,6 +9523,7 @@ ui_elements <- list( width = 8, shiny::h4("Choose your data source"), shiny::br(), + # shiny::uiOutput(outputId = "source"), shinyWidgets::radioGroupButtons( inputId = "source", selected = "file", @@ -9429,6 +9549,13 @@ ui_elements <- list( ), shiny::conditionalPanel( condition = "input.source=='redcap'", + shinyWidgets::alert( + 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"),"."), + dismissible = TRUE + ), m_redcap_readUI( id = "redcap_import", title = "" @@ -9446,11 +9573,11 @@ ui_elements <- list( condition = "output.data_loaded == true", shiny::br(), shiny::br(), - shiny::h5("Specify variables to include"), + shiny::h5("Select variables for final import"), shiny::fluidRow( shiny::column( width = 6, - shiny::p("Filter by completeness threshold:"), + shiny::p("Exclude incomplete variables:"), shiny::br(), shinyWidgets::noUiSliderInput( inputId = "complete_cutoff", @@ -9459,16 +9586,16 @@ ui_elements <- list( min = 0, max = 100, step = 5, - value = 70, + value = 30, format = shinyWidgets::wNumbFormat(decimals = 0), color = datamods:::get_primary_color() ), - shiny::helpText("Exclude variables with completeness below the specified percentage."), + shiny::helpText("Only include variables with missingness below the specified percentage."), shiny::br() ), shiny::column( width = 6, - shiny::p("Specify manually:"), + shiny::p("Manual selection:"), shiny::br(), shiny::uiOutput(outputId = "import_var"), shiny::br() @@ -9528,34 +9655,45 @@ ui_elements <- list( ), 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::tags$br(), - shiny::tags$br(), - shiny::uiOutput(outputId = "column_filter"), - shiny::helpText("Variable ", tags$a( - "data type", + shiny::br(), + shiny::br(), + shiny::tags$h6("Filter data types"), + shiny::uiOutput( + outputId = "column_filter"), + shiny::helpText("Read more on how ", tags$a( + "data types", href = "https://agdamsbo.github.io/FreesearchR/articles/data-types.html", target = "_blank", rel = "noopener noreferrer" - ), " filtering."), - shiny::tags$br(), - shiny::tags$br(), + ), " are defined."), + shiny::br(), + shiny::br(), + shiny::tags$h6("Create data filters"), + shiny::tags$p("Filter on observation level"), IDEAFilter::IDEAFilter_ui("data_filter"), - shiny::helpText("Observations level filtering."), - shiny::tags$br(), - shiny::tags$br() + shiny::br(), + shiny::br() ) ), - shiny::tags$br(), - shiny::tags$br(), - shiny::tags$br(), - shiny::tags$br(), - shiny::tags$br() + shiny::br(), + shiny::br(), + # shiny::br(), + # shiny::br(), + shiny::br() ), bslib::nav_panel( title = "Modify", @@ -9953,9 +10091,15 @@ ui <- bslib::page_fixed( -data(starwars) + data(mtcars) -data(trial) + +# trial <- gtsummary::trial +# starwars <- dplyr::starwars +# +# mtcars_na <- rbind(mtcars,NA,NA) + +# thematic::thematic_shiny() load_data <- function() { Sys.sleep(1) @@ -9963,6 +10107,7 @@ load_data <- function() { shinyjs::show("main_content") } +# is_local = is.na(Sys.getenv('SHINY_SERVER_VERSION', NA)) server <- function(input, output, session) { ## Listing files in www in session start to keep when ending and removing @@ -10018,6 +10163,28 @@ server <- function(input, output, session) { ######### ############################################################################## + ## This does not render correctly apparently due to css and load order + # output$source <- shiny::renderUI({ + # + # choices <- c( + # "File upload" = "file", + # "REDCap server export" = "redcap", + # "Local or sample data" = "env" + # ) + # + # if (isTRUE(is_local)){ + # choices <- choices[c(1,3)] + # } + # + # shinyWidgets::radioGroupButtons( + # inputId = "source", + # selected = "file", + # choices = choices, + # size = "lg" + # ) + # }) + + data_file <- import_file_server( id = "file_import", show_data_in = "popup", @@ -10067,7 +10234,7 @@ server <- function(input, output, session) { output$import_var <- shiny::renderUI({ shiny::req(rv$data_temp) - preselect <- names(rv$data_temp)[sapply(rv$data_temp, missing_fraction) <= input$complete_cutoff / 100] + preselect <- names(rv$data_temp)[sapply(rv$data_temp, missing_fraction) <= (input$complete_cutoff / 100)] shinyWidgets::virtualSelectInput( inputId = "import_var", @@ -10136,10 +10303,12 @@ server <- function(input, output, session) { if (is.null(rv$data_original) | NROW(rv$data_original) == 0) { shiny::updateActionButton(inputId = "act_start", disabled = TRUE) shiny::updateActionButton(inputId = "modal_browse", disabled = TRUE) + shiny::updateActionButton(inputId = "modal_missings", disabled = TRUE) shiny::updateActionButton(inputId = "act_eval", disabled = TRUE) } else { shiny::updateActionButton(inputId = "act_start", disabled = FALSE) shiny::updateActionButton(inputId = "modal_browse", disabled = FALSE) + shiny::updateActionButton(inputId = "modal_missings", disabled = FALSE) shiny::updateActionButton(inputId = "act_eval", disabled = FALSE) } }) @@ -10279,7 +10448,7 @@ server <- function(input, output, session) { # c("dichotomous", "ordinal", "categorical", "datatime", "continuous") shinyWidgets::virtualSelectInput( inputId = "column_filter", - label = "Select variable types to include", + label = "Select data types to include", selected = unique(data_type(rv$data)), choices = unique(data_type(rv$data)), updateOn = "change", @@ -10378,6 +10547,19 @@ server <- function(input, output, session) { ) }) + observeEvent(input$modal_missings, { + tryCatch( + { + 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") + } + ) + }) + + output$original_str <- renderPrint({ str(rv$data_original) }) @@ -10660,6 +10842,9 @@ server <- function(input, output, session) { { rv$list |> write_rmd( + params.args = list( + regression.p=rv$list$regression$input$add_regression_p + ), output_format = format, input = file.path(getwd(), "www/report.rmd") ) diff --git a/inst/apps/FreesearchR/www/report.rmd b/inst/apps/FreesearchR/www/report.rmd index 7f7c61f6..fcc6d4d3 100644 --- a/inst/apps/FreesearchR/www/report.rmd +++ b/inst/apps/FreesearchR/www/report.rmd @@ -7,6 +7,7 @@ toc: false params: data.file: NA version: NA + regression.p: NA --- ```{r setup, echo = FALSE} @@ -65,7 +66,15 @@ if ("table1" %in% names(web_data)) { ```{r, results = 'asis'} if ("regression" %in% names(web_data) && length(web_data$regression) > 0) { reg_tbl <- web_data$regression$regression$tables - knitr::knit_print(tbl_merge(reg_tbl)) + + merged <- tbl_merge(reg_tbl) + + if (params$regression.p == "no") { + merged <- merged |> + gtsummary::modify_column_hide(column = dplyr::starts_with("p.value")) + } + + knitr::knit_print(merged) } ``` diff --git a/inst/apps/FreesearchR/www/umami-app.html b/inst/apps/FreesearchR/www/umami-app.html new file mode 100644 index 00000000..f4e9a526 --- /dev/null +++ b/inst/apps/FreesearchR/www/umami-app.html @@ -0,0 +1 @@ + diff --git a/inst/apps/FreesearchR/www/web_data.rds b/inst/apps/FreesearchR/www/web_data.rds index bddd5583..a3256d47 100644 Binary files a/inst/apps/FreesearchR/www/web_data.rds and b/inst/apps/FreesearchR/www/web_data.rds differ diff --git a/man/data-missings.Rd b/man/data-missings.Rd new file mode 100644 index 00000000..e79398fc --- /dev/null +++ b/man/data-missings.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/missings-module.R +\name{data-missings} +\alias{data-missings} +\alias{data_missings_ui} +\alias{data_missings_server} +\title{Data correlations evaluation module} +\usage{ +data_missings_ui(id) + +data_missings_server(id, data, ...) +} +\arguments{ +\item{id}{Module id} + +\item{data}{data} + +\item{output.format}{output format} +} +\value{ +Shiny ui module + +shiny server module +} +\description{ +Data correlations evaluation module +}