diff --git a/DESCRIPTION b/DESCRIPTION index 8fc6d1c..7c28853 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,23 +17,25 @@ Depends: R (>= 4.1.0), shiny (>= 1.7.0) Imports: - lpSolveAPI (>= 5.5.2), + bslib (>= 0.5.1), + cli (>= 3.6.0), + ggplot2 (>= 3.3.0), haven (>= 2.5.0), + htmltools, + lpSolveAPI (>= 5.5.2), markdown, productivity (>= 1.1.0), - readxl, - writexl, - bslib (>= 0.5.1), - ggplot2 (>= 3.3.0), reactable (>= 0.4.0), - cli (>= 3.6.0) + readxl, + rlang (>= 1.1.0), + scales, + writexl Suggests: Benchmarking (>= 0.30), data.table, rmarkdown, knitr, - scales, - rlang, + magick, testthat (>= 3.0.0), tibble, withr diff --git a/NAMESPACE b/NAMESPACE index a0a84f4..0a3f884 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -19,3 +19,9 @@ import(reactable) import(readxl) import(shiny) import(writexl) +importFrom(stats,complete.cases) +importFrom(stats,median) +importFrom(stats,quantile) +importFrom(stats,rnorm) +importFrom(stats,sd) +importFrom(stats,var) diff --git a/NEWS.md b/NEWS.md index d26c1d2..dd4f00d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,7 @@ - Updated documentation for `run_pioneer()` - Added custom functions for DEA calculations in order to remove `{Benchmarking}` as a dependency (#80) +- All R functions - including `server()` and `ui()` - have now been moved to the `R` directory with benefits to how internal functions are accessed. In additions, the global environment should be untouched when the app is run (#87) ## Bug fixes diff --git a/inst/app/R/fct_dea-bootstrap.R b/R/fct_dea-bootstrap.R similarity index 80% rename from inst/app/R/fct_dea-bootstrap.R rename to R/fct_dea-bootstrap.R index 81b110a..e2d3a72 100644 --- a/inst/app/R/fct_dea-bootstrap.R +++ b/R/fct_dea-bootstrap.R @@ -1,3 +1,6 @@ +#' @importFrom stats complete.cases median quantile rnorm sd var +NULL + bw_rule <- function(delta, rule = 'ucv') { # Values must be in range 1, Inf. Take inverse if values are in range 0, 1 if (min(delta) < 1) { @@ -8,10 +11,10 @@ bw_rule <- function(delta, rule = 'ucv') { delta_2m <- c(delta_m, 2 - delta_m) # See Daraio & Wilson (2007) for a discussion on methods for determining bandwidth h <- switch(rule, - silverman = bw.nrd0(delta_2m), - scott = bw.nrd(delta_2m), - ucv = suppressWarnings({ h <- bw.ucv(delta_2m) }), - suppressWarnings({ h <- bw.ucv(delta_2m) }) + silverman = stats::bw.nrd0(delta_2m), + scott = stats::bw.nrd(delta_2m), + ucv = suppressWarnings({ h <- stats::bw.ucv(delta_2m) }), + suppressWarnings({ h <- stats::bw.ucv(delta_2m) }) ) # See Daraio & Wilson (2007), p. 61, eq. 3.26 h <- h * 2^(1/5) * (length(delta_m)/length(delta))^(1/5) * (sd(delta)/sd(delta_2m)) @@ -38,11 +41,11 @@ perform_boot <- function(x, y, rts, orientation, i, h, theta, boot) { beta <- bootstrap_sample(theta, h = h) if (orientation == 'in') { x_ref <- (theta / beta) * x - boot[, i] <- pioneeR:::compute_efficiency(x, y, rts = rts, orientation = orientation, xref = x_ref, yref = y, values_only = TRUE)$values + boot[, i] <- compute_efficiency(x, y, rts = rts, orientation = orientation, xref = x_ref, yref = y, values_only = TRUE)$values } else if (orientation == 'out') { beta <- 1 / beta y_ref <- (theta / beta) * y - boot[, i] <- pioneeR:::compute_efficiency(x, y, rts = rts, orientation = orientation, xref = x, yref = y_ref, values_only = TRUE)$values + boot[, i] <- compute_efficiency(x, y, rts = rts, orientation = orientation, xref = x, yref = y_ref, values_only = TRUE)$values } return(invisible(boot)) } diff --git a/inst/app/R/fct_file-functions.R b/R/fct_file-functions.R similarity index 90% rename from inst/app/R/fct_file-functions.R rename to R/fct_file-functions.R index fcc2e4c..3df439b 100644 --- a/inst/app/R/fct_file-functions.R +++ b/R/fct_file-functions.R @@ -1,3 +1,5 @@ +#' Check user provided file +#' @noRd check_file <- function(file) { check_missing <- sapply(1:nrow(file), function(i) all(is.na(file[i,]))) @@ -28,6 +30,8 @@ check_file <- function(file) { } +#' Check if a cell in Excel has a value +#' @noRd has_value <- function(x) { if (length(x) > 1L) return(sapply(x, has_value, USE.NAMES = FALSE)) if (is.null(x) || is.na(x)) return(FALSE) diff --git a/inst/app/R/fct_ui-functions.R b/R/fct_ui-functions.R similarity index 84% rename from inst/app/R/fct_ui-functions.R rename to R/fct_ui-functions.R index 5f0016f..bf5a764 100644 --- a/inst/app/R/fct_ui-functions.R +++ b/R/fct_ui-functions.R @@ -1,3 +1,5 @@ +#' Create a bootstrap styled alert box +#' @noRd alert <- function(..., color = 'primary', icon = NULL, dismissable = FALSE) { cls <- sprintf('alert alert-%s', color) @@ -14,6 +16,8 @@ alert <- function(..., color = 'primary', icon = NULL, dismissable = FALSE) { } +#' Create a bootstrap styled modal button +#' @noRd bs_modal_button <- function( label = 'Close', color = 'default', size = c('normal', 'sm', 'lg')) { @@ -23,6 +27,8 @@ bs_modal_button <- function( tags$button(type = 'button', class = cls, `data-bs-dismiss` = 'modal', label) } +#' Create a content element with proper Bootstrap styling +#' @noRd content_div <- function(...) { tags$div( class = 'container mt-1', diff --git a/inst/app/R/mod_file-upload.R b/R/mod_file-upload.R similarity index 97% rename from inst/app/R/mod_file-upload.R rename to R/mod_file-upload.R index 03c559a..583e249 100644 --- a/inst/app/R/mod_file-upload.R +++ b/R/mod_file-upload.R @@ -1,5 +1,5 @@ -library(reactable) - +#' UI module for file upload function +#' @noRd file_upload_ui <- function(id, wrap = FALSE, ...) { ns <- NS(id) if (wrap) { @@ -9,6 +9,8 @@ file_upload_ui <- function(id, wrap = FALSE, ...) { } } +#' Server module for file upload function +#' @noRd file_upload_srv <- function(id) { moduleServer( id, function(input, output, session) { @@ -76,7 +78,7 @@ file_upload_srv <- function(id) { if (ext %in% c('csv', 'tsv', 'dsv', 'txt')) { req(input$file_sep, input$file_dec) header <- if (is.null(input$file_header)) TRUE else as.logical(input$file_header) - clean_doc <- read.table( + clean_doc <- utils::read.table( textConnection(raw_file()), sep = input$file_sep, dec = input$file_dec, diff --git a/R/pkg-utils.R b/R/pkg-utils.R index 7caed29..2356097 100644 --- a/R/pkg-utils.R +++ b/R/pkg-utils.R @@ -1,3 +1,5 @@ +#' Create temporary data that can be used by pioneeR when run locally +#' @noRd set_local_data <- function(x) { if (nzchar(Sys.getenv('PIONEER_DATA'))) { @@ -28,12 +30,16 @@ set_local_data <- function(x) { } +#' Get list of unsafe ports that are used in Chrome +#' @noRd unsafe_ports <- function() { return( c(3659, 4045, 5060, 5061, 6000, 6566, 6665:6669, 6697) ) } +#' Checks if the specified port is inside the ranges that are considered safe +#' @noRd check_for_unsafe_port <- function(port) { if (is.null(port)) return() port <- as.numeric(port) diff --git a/inst/app/server.R b/R/server.R similarity index 81% rename from inst/app/server.R rename to R/server.R index 62782eb..fa9486c 100644 --- a/inst/app/server.R +++ b/R/server.R @@ -1,16 +1,10 @@ -# Load required packages -require(readxl) -require(ucminf) -require(productivity) -require(ggplot2) -require(scales) -require(haven) -require(writexl) -require(reactable) -require(rlang) - -# Define server logic -shinyServer(function(input, output, session) { +#' @import reactable +#' @import ggplot2 +NULL + +#' Define server logic for pioneeR +#' @noRd +server <- function(input, output, session) { reactable_opts <- list( compact = TRUE, sortable = TRUE, filterable = TRUE, striped = TRUE, @@ -23,6 +17,7 @@ shinyServer(function(input, output, session) { load_data <- list(file = NULL, cols = NULL, error = NULL) # If we are running locally, and we have data, load the data + is_local <- !nzchar(Sys.getenv('SHINY_PORT')) if (is_local && nzchar(Sys.getenv('PIONEER_DATA'))) { local_data <- NULL if (file.exists(Sys.getenv('PIONEER_DATA'))) { @@ -157,8 +152,6 @@ shinyServer(function(input, output, session) { }) - source('conditionalUI.R', local = TRUE, encoding = 'UTF-8') - output$preview <- renderReactable({ # Input file is required. If input is NULL, return NULL req(preview()) @@ -251,7 +244,7 @@ shinyServer(function(input, output, session) { req(data(), dea.in(), dea.out()) - d <- pioneeR:::compute_efficiency( + d <- compute_efficiency( dea.in(), dea.out(), rts = model_params$rts, orientation = model_params$orientation) @@ -261,7 +254,7 @@ shinyServer(function(input, output, session) { dea.slack <- reactive({ x <- tryCatch({ - pioneeR:::compute_slack(dea.in(), dea.out(), dea.prod()) + compute_slack(dea.in(), dea.out(), dea.prod()) }, warning = function(e) { NULL }, error = function(e) { @@ -274,7 +267,7 @@ shinyServer(function(input, output, session) { req(data(), dea.in(), dea.out()) - d <- pioneeR:::compute_super_efficiency( + d <- compute_super_efficiency( dea.in(), dea.out(), rts = model_params$rts, orientation = model_params$orientation ) @@ -306,8 +299,8 @@ shinyServer(function(input, output, session) { geom_point(color = '#084887') + xlab(paste('Inputs:\n', paste(params()$inputs, collapse = ', '))) + ylab(paste('Outputs:\n', paste(params()$outputs, collapse = ', '))) + - scale_y_continuous(labels = label_number(suffix = find_scale(d$x)[[1]], scale = find_scale(d$x)[[2]])) + - scale_x_continuous(labels = label_number(suffix = find_scale(d$y)[[1]], scale = find_scale(d$y)[[2]])) + + scale_y_continuous(labels = scales::label_number(suffix = find_scale(d$x)[[1]], scale = find_scale(d$x)[[2]])) + + scale_x_continuous(labels = scales::label_number(suffix = find_scale(d$y)[[1]], scale = find_scale(d$y)[[2]])) + theme_pioneer() # Add frontier line @@ -316,21 +309,21 @@ shinyServer(function(input, output, session) { p <- p + geom_abline(intercept = 0, slope = max(d$y/d$x), color = '#f9ab55', linewidth = 1) } else { if (model_params$rts == 'vrs') { - # Use chull to find the points which lie on the convex hull - hpts <- chull(d$x, d$y) - hpts <- hpts[c(which(d$x[hpts] == min(d$x[hpts])), which(head(d$x[hpts], -1) < tail(d$x[hpts], -1)) + 1)] + # Use chull from grDevices to find the points which lie on the convex hull + hpts <- grDevices::chull(d$x, d$y) + hpts <- hpts[c(which(d$x[hpts] == min(d$x[hpts])), which(utils::head(d$x[hpts], -1) < utils::tail(d$x[hpts], -1)) + 1)] y <- c(0, d$y[hpts], max(d$y)) x <- c(min(d$x), d$x[hpts], max(d$x)) } else if (model_params$rts == 'drs') { # If we have NIRS, the front starts at origo, so we add origo to our coordinates - hpts <- chull(c(0, d$x), c(0, d$y)) + hpts <- grDevices::chull(c(0, d$x), c(0, d$y)) hpts <- hpts[hpts != 1]-1 - hpts <- hpts[c(which(head(d$x[hpts], -1) < tail(d$x[hpts], -1)) + 1)] + hpts <- hpts[c(which(utils::head(d$x[hpts], -1) < utils::tail(d$x[hpts], -1)) + 1)] y <- c(0, d$y[hpts], max(d$y)) x <- c(0, d$x[hpts], max(d$x)) } # Remove observations where the value on the y-axis is reduced - rm <- c(TRUE, mapply(\(t1, t2) t2 < t1, t1 = tail(y, -1), t2 = head(y, -1))) + rm <- c(TRUE, mapply(\(t1, t2) t2 < t1, t1 = utils::tail(y, -1), t2 = utils::head(y, -1))) coords <- data.frame(y = y[rm], x = x[rm]) p <- p + geom_line(data = coords, aes(x = x, y = y), color = '#f9ab55', linewidth = 1) } @@ -417,12 +410,13 @@ shinyServer(function(input, output, session) { color <- sprintf('#%s', input$salter.color) # 'rgb(8,48,107)' - g <- ggplot(d, aes(x = wt, y = eff, width = inputs, fill = as.character(row.names(d)))) + + # To avoid R CMD notes about no visible bindings, we use the .data pronoun + g <- ggplot(d, aes(x = .data$wt, y = .data$eff, width = .data$inputs, fill = as.character(row.names(d)))) + geom_bar(stat = 'identity', position = 'identity', show.legend = FALSE) + scale_fill_manual(values = rep(c('#084887', '#f9ab55'), length.out = nrow(d))) + labs(x = input$salter.xtitle, y = input$salter.ytitle) + - scale_x_continuous(labels = label_number(suffix = find_scale(d$wt)[[1]], scale = find_scale(d$wt)[[2]])) + - scale_y_continuous(labels = label_number(suffix = find_scale(d$eff)[[1]], scale = find_scale(d$eff)[[2]])) + + scale_x_continuous(labels = scales::label_number(suffix = find_scale(d$wt)[[1]], scale = find_scale(d$wt)[[2]])) + + scale_y_continuous(labels = scales::label_number(suffix = find_scale(d$eff)[[1]], scale = find_scale(d$eff)[[2]])) + theme_pioneer() }) @@ -549,7 +543,7 @@ shinyServer(function(input, output, session) { renderPlot({ # Find to optimal number of bins using Freedman-Diaconis rule if N is less # than 200, and Sturge's rule if N is equal or greater than 200 - n_bins <- if (length(eff) < 200) nclass.FD(eff) else nclass.Sturges(eff) + n_bins <- if (length(eff) < 200) grDevices::nclass.FD(eff) else grDevices::nclass.Sturges(eff) bins <- pretty(range(eff), n = n_bins, min.n = 1) ggplot(data.frame(eff = eff), aes(x = eff)) + stat_bin(fill = '#ee2255', color = '#eeeeee', breaks = bins) + @@ -565,18 +559,6 @@ shinyServer(function(input, output, session) { }) - observeEvent(input$exportplot, { - - require(processx) - - p <- plot.dea() - - tmpFile <- tempfile(fileext = ".png") - export(p, file = tmpFile) - browseURL(tmpFile) - - }) - dea.tbl <- reactive({ deff <- matrix(dea.prod()$values, ncol = 1, dimnames = list(NULL, 'Efficiency')) @@ -617,7 +599,7 @@ shinyServer(function(input, output, session) { output$dea.table <- renderReactable({ df <- dea.tbl() - opts <- list2(!!!reactable_opts, data = df, columns = list( + opts <- rlang::list2(!!!reactable_opts, data = df, columns = list( DMU = colDef(sticky = 'left') )) do.call(reactable, opts) @@ -627,15 +609,15 @@ shinyServer(function(input, output, session) { df <- round(dea.slack()$data, input$dea_round) colnames(df)[ncol(df)] <- 'Total' - opts <- list2(!!!reactable_opts, data = df) + opts <- rlang::list2(!!!reactable_opts, data = df) do.call(reactable, opts) }) output$peers.table <- renderReactable({ - df <- pioneeR:::get_peers(dea.prod(), ids = selection()[, input$dea_id], threshold = 0) + df <- get_peers(dea.prod(), ids = selection()[, input$dea_id], threshold = 0) colnames(df)[1] <- 'DMU' - opts <- list2(!!!reactable_opts, data = df, columns = list( + opts <- rlang::list2(!!!reactable_opts, data = df, columns = list( DMU = colDef(sticky = 'left') )) do.call(reactable, opts) @@ -657,7 +639,7 @@ shinyServer(function(input, output, session) { } else if (input$exportfileformat == 'xlsx') { writexl::write_xlsx(df, file) } else if (input$exportfileformat == 'csv') { - write.csv2(df, file, fileEncoding = 'CP1252', row.names = FALSE) + utils::write.csv2(df, file, fileEncoding = 'CP1252', row.names = FALSE) } } ) @@ -667,7 +649,7 @@ shinyServer(function(input, output, session) { num_cols <- which(sapply(df, is.numeric, USE.NAMES = FALSE)) df[, num_cols] <- round(df[, num_cols], input$dea_round) - opts <- list2(!!!reactable_opts, data = df, columns = list( + opts <- rlang::list2(!!!reactable_opts, data = df, columns = list( DMU = colDef(sticky = 'left'), Scale.eff. = colDef(name = 'Scale eff.'), VRS.NIRS.ratio = colDef( @@ -901,7 +883,7 @@ shinyServer(function(input, output, session) { # Add DMU names and round inputs df <- cbind(data.frame(DMU = names(dea.prod()$values)), round(res$tbl, input$boot_round)) - opts <- list2(!!!reactable_opts, data = df, columns = list( + opts <- rlang::list2(!!!reactable_opts, data = df, columns = list( eff = colDef(show = input$boot_show_eff, name = 'Efficiency'), bias = colDef(show = input$boot_show_bias, name = 'Bias'), eff_bc = colDef(name = 'Bias corr. score'), @@ -947,7 +929,7 @@ shinyServer(function(input, output, session) { } else if (input$boot_fileformat == 'xlsx') { writexl::write_xlsx(df, file) } else if (input$boot_fileformat == 'csv') { - write.csv2(df, file, fileEncoding = 'CP1252', row.names = FALSE) + utils::write.csv2(df, file, fileEncoding = 'CP1252', row.names = FALSE) } } ) @@ -958,7 +940,7 @@ shinyServer(function(input, output, session) { req(data(), params()$year) - df <- checkBalance(selection(), params()$id, params()$year) + df <- check_balance(selection(), params()$id, params()$year) if (nrow(df$data) == 0) { out <- alert( color = 'danger', icon = 'danger', @@ -982,9 +964,9 @@ shinyServer(function(input, output, session) { req(selection()) - df <- checkBalance(selection(), params()$id, params()$year) + df <- check_balance(selection(), params()$id, params()$year) - malmquist <- malm( + malmquist <- productivity::malm( data = df$data, id.var = params()$id, time.var = params()$year, x.vars = params()$inputs, y.vars = params()$outputs, rts = input$malm_rts, orientation = input$malm_orientation, scaled = TRUE) @@ -1028,7 +1010,7 @@ shinyServer(function(input, output, session) { } else if (input$malm.fileformat == 'xlsx') { writexl::write_xlsx(df, file) } else if (input$malm.fileformat == 'csv') { - write.csv2(df, file, fileEncoding = 'CP1252', row.names = FALSE) + utils::write.csv2(df, file, fileEncoding = 'CP1252', row.names = FALSE) } } ) @@ -1048,8 +1030,9 @@ shinyServer(function(input, output, session) { sprintf('dea-model-%s-%s.pdf', model_params$rts, model_params$orientation) }, content = function(file) { + template <- system.file('files', 'dea_analysis.Rmd', package = 'pioneeR') tempReport <- file.path(tempdir(), 'dea_analysis.Rmd') - file.copy('dea_analysis.Rmd', tempReport, overwrite = TRUE) + file.copy(template, tempReport, overwrite = TRUE) params <- list( data = selection(), @@ -1082,8 +1065,140 @@ shinyServer(function(input, output, session) { stopApp(x) }) + # From conditionalUI.R + # Functions to render the UI based on other input + + output$ui_id <- renderUI({ + + req(data()) + + if (is.null(data()$file)) return(NULL) + + choices <- colnames(data()$file) + selectInput('dea_id', 'Firm ID', choices = choices, multiple = FALSE) + + }) + + output$ui_inputs <- renderUI({ + + req(data()) + + if (is.null(data()$file)) return(NULL) + + # Restore input if we are restoring previous state + selected <- if (!is.null(restoreVals$inputs)) restoreVals$inputs else NULL + choices <- data()$cols[sapply(data()$file, is.numeric, USE.NAMES = FALSE)] + + selectInput('dea_input', 'Inputs', choices = choices, selected = selected, multiple = TRUE) + + }) + + observeEvent(input$dea_input, { + selected_inputs <- input$dea_input + selected <- input$dea_output + choices <- data()$cols[sapply(data()$file, is.numeric, USE.NAMES = FALSE)] + if (length(selected_inputs) > 0) { + choices <- choices[!(choices %in% selected_inputs)] + } + if (!is.null(selected) && any(selected %in% selected_inputs)) { + selected <- selected[!(selected %in% selected_inputs)] + } + updateSelectInput(session, 'dea_output', choices = choices, selected = selected) + }) + + output$ui_outputs <- renderUI({ + + req(data()) + + if (is.null(data()$file)) return(NULL) + + # Restore input if we are restoring previous state + selected <- if (!is.null(restoreVals$outputs)) restoreVals$outputs else NULL + choices <- data()$cols[sapply(data()$file, is.numeric, USE.NAMES = FALSE)] + + selectInput('dea_output', 'Outputs', choices = choices, selected = selected, multiple = TRUE) + + }) + + observeEvent(input$dea_output, { + selected_outputs <- input$dea_output + selected <- input$dea_input + choices <- data()$cols[sapply(data()$file, is.numeric, USE.NAMES = FALSE)] + if (length(selected_outputs) > 0) { + choices <- choices[!(choices %in% selected_outputs)] + } + if (!is.null(selected) && any(selected %in% selected_outputs)) { + selected <- selected[!(selected %in% selected_outputs)] + } + updateSelectInput(session, 'dea_input', choices = choices, selected = selected) + }) + + output$ui_timeseries <- renderUI({ + + req(data()$file) + + checkboxInput('hasyear', 'Time series data', value = FALSE) + + }) + + output$ui_year <- renderUI({ + + req(data(), input$hasyear) + + # We only want to show the input if we have time series data + if (!input$hasyear) return(NULL) + + df <- data()$file + + identify_year_variable <- \(x) { + if (!is.atomic(x) || !is.numeric(x) || any(is.na(x))) { + return(FALSE) + } + return(all(abs(2000 - range(x)) < 100)) + } + + choices <- data()$cols + year_variable <- df[names(df[(which(sapply(df, identify_year_variable)))])] + selected <- ifelse(length(year_variable) >= 1, names(year_variable[1]), choices[[1]]) + selectInput('dea_year', 'Time series variable', choices = choices, selected = selected, multiple = FALSE) + + }) + + output$ui_subset <- renderUI({ + + req(data()$file) + + tagList( + checkboxInput('data.subset', 'Subset data', value = FALSE) + ) + + }) + + output$ui_subset_info <- renderUI({ + + req(data()$file) + + n_rows <- length(preview_selected()) + t_rows <- nrow(data()$file) + one <- n_rows == 1 + + tagList( + p( + class = 'small', helpText(sprintf( + 'Click to subset data. You can select the rows to include in the analysis by + clicking the rows in the table. Currently %s %s of %s %s selected', + n_rows, if (one) 'row' else 'rows', t_rows, if (one) 'is' else 'are' + )) + ), + actionButton('data.subset.select', 'Select all'), + actionButton('data.subset.deselect', 'Deselect all') + ) + + }) + # Stop from conditionalUI.R + onStop(function() { Sys.unsetenv('PIONEER_DATA') }) -}) +} diff --git a/R/shiny-functions.R b/R/shiny-functions.R index 2f158c7..0a366a6 100644 --- a/R/shiny-functions.R +++ b/R/shiny-functions.R @@ -9,6 +9,19 @@ #' @import writexl NULL +#' Function to add JS and CSS dependencies to the app +#' @noRd +pioneer_scripts <- function() { + htmltools::htmlDependency( + name = 'pioneer-assets', + version = utils::packageVersion('pioneeR'), + package = 'pioneeR', + src = 'www', + script = 'pioneer.js', + style = 'style.css' + ) +} + #' Run pioneeR #' #' Run the pioneeR app on your local machine. @@ -38,7 +51,17 @@ run_pioneer <- function(x = NULL, port = NULL, ...) { port <- check_for_unsafe_port(port) } - shiny::runApp(system.file('app', package = 'pioneeR'), port = port, ...) + pioneer_env <- new.env() + environment(ui) <- pioneer_env + environment(server) <- pioneer_env + + # shiny::runApp(system.file('app', package = 'pioneeR'), port = port, ...) + shiny::shinyApp( + ui, + server, + enableBookmarking = 'server', + ... + ) } @@ -52,3 +75,31 @@ runPioneeR <- run_pioneer #' #' @export unset_env_vars <- \() Sys.unsetenv('PIONEER_DATA') + +#' Check if time series data is balanced for use in Malmquist models +#' @noRd +check_balance <- function(data, id_var, time_var) { + + units <- unique(data[, id_var]) + time <- unique(data[, time_var]) + + miss <- sapply(units, function(u) { + unit_time <- unique(data[data[,id_var] == u, time_var]) + all(sapply(time, function(t) t %in% unit_time)) + }) + + r <- list() + + if (!all(miss)) { + r$data <- data[data[,id_var] %in% units[miss],] + r$listwise <- TRUE + r$message <- 'Data was not balanced, listwise deleting has been performed' + } else { + r$data <- data + r$listwise <- FALSE + r$message <- NULL + } + + r + +} diff --git a/inst/app/ui.R b/R/ui.R similarity index 96% rename from inst/app/ui.R rename to R/ui.R index 9bcb582..1aa5708 100644 --- a/inst/app/ui.R +++ b/R/ui.R @@ -1,9 +1,6 @@ # Load required packages -require(data.table) -require(reactable) -require(bslib) - ver <- utils::packageVersion('pioneeR') +bs_ver <- 5 sidebar_width <- 400 @@ -21,7 +18,8 @@ if (utils::packageVersion('bslib') > '0.5.1') { theme_args = list(version = bs_ver) } -# Define UI for application that draws a histogram +#' Define the user interface for pioneeR +#' @noRd ui <- function(request) { page_navbar( title = 'pioneeR', @@ -30,10 +28,7 @@ ui <- function(request) { page_navbar( fluid = TRUE, # Add custom CSS - header = tags$head( - tags$link(rel = 'stylesheet', type = 'text/css', href = 'style.css'), - tags$script(src = 'pioneer.js') # pioneer.min.js - ), + header = pioneer_scripts(), tabPanel( 'Data', value = 'pioneer_upload', @@ -293,7 +288,7 @@ ui <- function(request) { page_navbar( tabPanel( 'About', value = 'pioneeR_about', content_div( - includeMarkdown('about.md') + includeMarkdown(system.file('files', 'about.md', package = 'pioneeR')) ) ), @@ -303,7 +298,7 @@ ui <- function(request) { page_navbar( Norway](https://www.riksrevisjonen.no/en)**. © %s Office of the Auditor General of Norway — Version %s with bslib %s', - format(Sys.Date(), '%Y'), ver, packageVersion('bslib') + format(Sys.Date(), '%Y'), ver, utils::packageVersion('bslib') )) ) diff --git a/inst/app/R/utils.R b/R/utils.R similarity index 94% rename from inst/app/R/utils.R rename to R/utils.R index 7b69cf8..772de55 100644 --- a/inst/app/R/utils.R +++ b/R/utils.R @@ -1,4 +1,3 @@ - #' Create Bootstrap dropdown button with custom UI #' #' @param title The name of the dropdown button @@ -11,7 +10,7 @@ #' @param direction The direction of the dropdown Default value \code{'down'}. #' @param autoclose If \code{TRUE} the dropdown will close when the user clicks inside it #' -#' @export +#' @noRd dropdown_button <- function( title, ..., color = 'primary', outline = FALSE, size = c('normal', 'sm', 'lg'), width = 400, direction = c('down', 'right', 'up', 'left'), autoclose = TRUE) @@ -39,6 +38,8 @@ dropdown_button <- function( } +#' Automatically assign a scale for large numbers in ggplot2 +#' @noRd find_scale <- function(x) { m <- abs(max(x)) i <- c(0, 1e3, 1e6, 1e9, 1e12) @@ -47,7 +48,9 @@ find_scale <- function(x) { return(list(names(i)[findInterval(m, i)], x[findInterval(m, i)])) } -theme_pioneer <- function(){ +#' Theme for ggplot2 +#' @noRd +theme_pioneer <- function() { theme( axis.text.x = element_text( angle = 0, color = '#183271', vjust = 0.5, @@ -91,6 +94,8 @@ theme_pioneer <- function(){ panel.spacing = unit(2, "lines")) } +#' Save function for ggplot2 objects +#' @noRd ggsave_ <- function(filename, plot, format = 'png', size = c('A5', 'A4', 'A3')) { size <- match.arg(size) dims <- switch( @@ -106,6 +111,8 @@ ggsave_ <- function(filename, plot, format = 'png', size = c('A5', 'A4', 'A3')) )) } +#' Create a random ID +#' @noRd rand_id <- function() { # For true random IDs, we should use uuid or openssl libraries, but this will do hex_digits <- c(0:9, letters[1:6]) diff --git a/inst/app/.gitignore b/inst/app/.gitignore deleted file mode 100644 index eda8ee3..0000000 --- a/inst/app/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -shiny_bookmarks - -# VS code settings -.vscode/settings.json diff --git a/inst/app/conditionalUI.R b/inst/app/conditionalUI.R deleted file mode 100644 index 02db80f..0000000 --- a/inst/app/conditionalUI.R +++ /dev/null @@ -1,129 +0,0 @@ -# Functions to render the UI based on other input - -output$ui_id <- renderUI({ - - req(data()) - - if (is.null(data()$file)) return(NULL) - - choices <- colnames(data()$file) - selectInput('dea_id', 'Firm ID', choices = choices, multiple = FALSE) - -}) - -output$ui_inputs <- renderUI({ - - req(data()) - - if (is.null(data()$file)) return(NULL) - - # Restore input if we are restoring previous state - selected <- if (!is.null(restoreVals$inputs)) restoreVals$inputs else NULL - choices <- data()$cols[sapply(data()$file, is.numeric, USE.NAMES = FALSE)] - - selectInput('dea_input', 'Inputs', choices = choices, selected = selected, multiple = TRUE) - -}) - -observeEvent(input$dea_input, { - selected_inputs <- input$dea_input - selected <- input$dea_output - choices <- data()$cols[sapply(data()$file, is.numeric, USE.NAMES = FALSE)] - if (length(selected_inputs) > 0) { - choices <- choices[!(choices %in% selected_inputs)] - } - if (!is.null(selected) && any(selected %in% selected_inputs)) { - selected <- selected[!(selected %in% selected_inputs)] - } - updateSelectInput(session, 'dea_output', choices = choices, selected = selected) -}) - -output$ui_outputs <- renderUI({ - - req(data()) - - if (is.null(data()$file)) return(NULL) - - # Restore input if we are restoring previous state - selected <- if (!is.null(restoreVals$outputs)) restoreVals$outputs else NULL - choices <- data()$cols[sapply(data()$file, is.numeric, USE.NAMES = FALSE)] - - selectInput('dea_output', 'Outputs', choices = choices, selected = selected, multiple = TRUE) - -}) - -observeEvent(input$dea_output, { - selected_outputs <- input$dea_output - selected <- input$dea_input - choices <- data()$cols[sapply(data()$file, is.numeric, USE.NAMES = FALSE)] - if (length(selected_outputs) > 0) { - choices <- choices[!(choices %in% selected_outputs)] - } - if (!is.null(selected) && any(selected %in% selected_outputs)) { - selected <- selected[!(selected %in% selected_outputs)] - } - updateSelectInput(session, 'dea_input', choices = choices, selected = selected) -}) - -output$ui_timeseries <- renderUI({ - - req(data()$file) - - checkboxInput('hasyear', 'Time series data', value = FALSE) - -}) - -output$ui_year <- renderUI({ - - req(data(), input$hasyear) - - # We only want to show the input if we have time series data - if (!input$hasyear) return(NULL) - - df <- data()$file - - identify_year_variable <- \(x) { - if (!is.atomic(x) || !is.numeric(x) || any(is.na(x))) { - return(FALSE) - } - return(all(abs(2000 - range(x)) < 100)) - } - - choices <- data()$cols - year_variable <- df[names(df[(which(sapply(df, identify_year_variable)))])] - selected <- ifelse(length(year_variable) >= 1, names(year_variable[1]), choices[[1]]) - selectInput('dea_year', 'Time series variable', choices = choices, selected = selected, multiple = FALSE) - -}) - -output$ui_subset <- renderUI({ - - req(data()$file) - - tagList( - checkboxInput('data.subset', 'Subset data', value = FALSE) - ) - -}) - -output$ui_subset_info <- renderUI({ - - req(data()$file) - - n_rows <- length(preview_selected()) - t_rows <- nrow(data()$file) - one <- n_rows == 1 - - tagList( - p( - class = 'small', helpText(sprintf( - 'Click to subset data. You can select the rows to include in the analysis by - clicking the rows in the table. Currently %s %s of %s %s selected', - n_rows, if (one) 'row' else 'rows', t_rows, if (one) 'is' else 'are' - )) - ), - actionButton('data.subset.select', 'Select all'), - actionButton('data.subset.deselect', 'Deselect all') - ) - -}) diff --git a/inst/app/global.R b/inst/app/global.R deleted file mode 100644 index 6ab1034..0000000 --- a/inst/app/global.R +++ /dev/null @@ -1,39 +0,0 @@ -# Load packages -library(pioneeR) -library(shiny) -library(bslib) -source('R/utils.R') - -# Set Bootstrap version -bs_ver <- 5 - -enableBookmarking(store = 'server') - -# Check if we're running locally or on a server -is_local <- !nzchar(Sys.getenv('SHINY_PORT')) - -checkBalance <- function(df, id.var, time.var) { - - units <- unique(df[, id.var]) - time <- unique(df[, time.var]) - - miss <- sapply(units, function(u) { - unit.time <- unique(df[df[,id.var] == u, time.var]) - all(sapply(time, function(t) t %in% unit.time)) - }) - - r <- list() - - if (!all(miss)) { - r$data <- df[df[,id.var] %in% units[miss],] - r$listwise <- TRUE - r$message <- 'Data was not balanced, listwise deleting has been performed' - } else { - r$data <- df - r$listwise <- FALSE - r$message <- NULL - } - - return(r) - -} diff --git a/inst/app/about.md b/inst/files/about.md similarity index 100% rename from inst/app/about.md rename to inst/files/about.md diff --git a/inst/app/dea_analysis.Rmd b/inst/files/dea_analysis.Rmd similarity index 100% rename from inst/app/dea_analysis.Rmd rename to inst/files/dea_analysis.Rmd diff --git a/inst/app/www/pioneer.js b/inst/www/pioneer.js similarity index 100% rename from inst/app/www/pioneer.js rename to inst/www/pioneer.js diff --git a/inst/app/www/pioneer.min.js b/inst/www/pioneer.min.js similarity index 100% rename from inst/app/www/pioneer.min.js rename to inst/www/pioneer.min.js diff --git a/inst/app/www/style.css b/inst/www/style.css similarity index 100% rename from inst/app/www/style.css rename to inst/www/style.css diff --git a/inst/app/www/style.min.css b/inst/www/style.min.css similarity index 100% rename from inst/app/www/style.min.css rename to inst/www/style.min.css