diff --git a/NAMESPACE b/NAMESPACE index 8ff01a8..e40a598 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,8 @@ export(dataOutputServer) export(dataOutputUI) export(dataSelectServer) export(dataSelectUI) +export(dataSyncServer) +export(dataSyncUI) export(data_edit) importFrom(htmltools,HTML) importFrom(htmltools,br) diff --git a/NEWS.md b/NEWS.md index 13cdacd..9221646 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,9 @@ * Display hints when user hovers cursor over buttons. * Make sure R objects containing `.` can be loaded into the data editor. * Turn on highlighting for columns and rows. +* Add `dataSync` module to handle synchronisation of an edited data subset with the complete dataset. +* Improved support for tibbles. +* Fix `dataFilter` to ensure the logic and levels are always updated when the column selection changes. # DataEditR 0.1.2 @@ -12,7 +15,7 @@ # DataEditR 0.1.1 -* Ensure `dataInput()` searches for data outside `data_edit()`. +* Ensure `dataInput` searches for data outside `data_edit()`. * Prevent loading of highlighted data object in RStudio when data has been supplied to `data_edit()` directly. * Allow saving by `write_fun` whenever `save_as` is supplied. diff --git a/R/dataFilter.R b/R/dataFilter.R index d1bb90f..7296f15 100644 --- a/R/dataFilter.R +++ b/R/dataFilter.R @@ -6,7 +6,7 @@ #' making multiple calls to this shiny module. #' @param data an array wrapped in \code{reactive()} containing the data to be #' filtered. -#' @param hide logical indicating whether the data sfiltering user interface +#' @param hide logical indicating whether the data filtering user interface #' should be hidden from the user, set to FALSE by default. #' @param hover_text text to display on download button when user hovers cursor #' over button, set to NULL by default to turn off hover text. @@ -95,7 +95,7 @@ dataFilterServer <- function(id, if(!is.null(hover_text)) { addTooltip(session = session, id = ns("filter"), - title = "filter rows") + title = hover_text) } } @@ -184,10 +184,11 @@ dataFilterServer <- function(id, # VALUES values$filters[[filter_name]]$column <- input[[paste0(filter_name, "-col")]] - - # LEVELS - if (!is.numeric(values$data[, input[[paste0(filter_name, "-col")]]])) { - + + # LEVELS - DROP REQUIRED FOR TIBBLES + if (!is.numeric( + values$data[, input[[paste0(filter_name, "-col")]], + drop = TRUE])) { # UPDATE LOGIC updateSelectInput( session, @@ -207,16 +208,45 @@ dataFilterServer <- function(id, paste0(filter_name, "-levels"), choices = as.vector( unique( - values$data[, input[[paste0(filter_name, "-col")]]] + values$data[, input[[paste0(filter_name, "-col")]], + drop = TRUE] ) - ) + ), + server = TRUE + ) + # NUMERIC + } else { + # UPDATE LOGIC + updateSelectInput( + session, + paste0(filter_name, "-logic"), + choices = c( + "equal", + "not equal", + "greater than", + "less than", + "greater than or equal", + "less than or equal", + "between", + "not between", + "contain", + "not contain" + ), + selected = "equal" + ) + # UPDATE SELECT INPUT + updateSelectizeInput( + session, + paste0(filter_name, "-levels"), + choices = NULL, + selected = NULL, + server = TRUE ) } }), # LOGIC logic = observeEvent(input[[paste0(filter_name, "-logic")]], { - # VALUES values$filters[[filter_name]]$logic <- input[[paste0(filter_name, "-logic")]] @@ -224,7 +254,6 @@ dataFilterServer <- function(id, # LEVELS levels = observeEvent(input[[paste0(filter_name, "-levels")]], { - # VALUES values$filters[[filter_name]]$levels <- input[[paste0(filter_name, "-levels")]] @@ -232,22 +261,37 @@ dataFilterServer <- function(id, # DELETE delete = observeEvent(input[[paste0(filter_name, "-remove")]], { - # COLUMN removeUI( - selector = paste0("div:has(>> #", ns(paste0(filter_name, "-col")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(filter_name, "-col")), + ")" + ) ) # LOGIC removeUI( - selector = paste0("div:has(>> #", ns(paste0(filter_name, "-logic")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(filter_name, "-logic")), + ")" + ) ) # LEVELS removeUI( - selector = paste0("div:has(>> #", ns(paste0(filter_name, "-levels")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(filter_name, "-levels")), + ")" + ) ) # DELETE removeUI( - selector = paste0("div:has(>> #", ns(paste0(filter_name, "-remove")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(filter_name, "-remove")), + ")" + ) ) # VALUES values$filters[[filter_name]] <- NULL @@ -303,7 +347,6 @@ dataFilterServer <- function(id, lapply(names(values$filters), function(z){ filter_id <- gsub("filter-", "", z) - filter_ns_name <- ns(z) # ARGUMENTS @@ -421,8 +464,10 @@ dataFilterServer <- function(id, values$filters[[filter_name]]$column <- input[[paste0(filter_name, "-col")]] - # VALIDATE NUMERIC - if (!is.numeric(values$data[, input[[paste0(filter_name, "-col")]]])) { + # CHARACTER/FACTOR + if (!is.numeric( + values$data[, input[[paste0(filter_name, "-col")]], + drop = TRUE])) { # UPDATE LOGIC updateSelectInput( @@ -443,16 +488,45 @@ dataFilterServer <- function(id, paste0(filter_name, "-levels"), choices = as.vector( unique( - values$data[, input[[paste0(filter_name, "-col")]]] + values$data[, input[[paste0(filter_name, "-col")]], + drop = TRUE] ) - ) + ), + server = TRUE + ) + # NUMERIC + } else { + # UPDATE LOGIC + updateSelectInput( + session, + paste0(filter_name, "-logic"), + choices = c( + "equal", + "not equal", + "greater than", + "less than", + "greater than or equal", + "less than or equal", + "between", + "not between", + "contain", + "not contain" + ), + selected = "equal" + ) + # UPDATE SELECT INPUT + updateSelectizeInput( + session, + paste0(filter_name, "-levels"), + choices = NULL, + selected = NULL, + server = TRUE ) } }), # LOGIC logic = observeEvent(input[[paste0(filter_name, "-logic")]], { - # VALUES values$filters[[filter_name]]$logic <- input[[paste0(filter_name, "-logic")]] @@ -460,7 +534,6 @@ dataFilterServer <- function(id, # LEVELS levels = observeEvent(input[[paste0(filter_name, "-levels")]], { - # VALUES values$filters[[filter_name]]$levels <- input[[paste0(filter_name, "-levels")]] @@ -468,22 +541,37 @@ dataFilterServer <- function(id, # DELETE delete = observeEvent(input[[paste0(filter_name, "-remove")]], { - # COLUMN removeUI( - selector = paste0("div:has(>> #", ns(paste0(filter_name, "-col")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(filter_name, "-col")), + ")" + ) ) # LOGIC removeUI( - selector = paste0("div:has(>> #", ns(paste0(filter_name, "-logic")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(filter_name, "-logic")), + ")" + ) ) # LEVELS removeUI( - selector = paste0("div:has(>> #", ns(paste0(filter_name, "-levels")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(filter_name, "-levels")), + ")" + ) ) # DELETE removeUI( - selector = paste0("div:has(>> #", ns(paste0(filter_name, "-remove")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(filter_name, "-remove")), + ")" + ) ) # VALUES values$filters[[filter_name]] <- NULL @@ -535,86 +623,91 @@ dataFilterServer <- function(id, # REMOVE FILTERS observeEvent(input$filter_reset, { - # REMOVE FILTER UI lapply(names(values$filters), function(z) { # COLUMN removeUI( - selector = paste0("div:has(>> #", ns(paste0(z, "-col")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(z, "-col")), + ")" + ) ) # LOGIC removeUI( - selector = paste0("div:has(>> #", ns(paste0(z, "-logic")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(z, "-logic")), + ")" + ) ) # LEVELS removeUI( - selector = paste0("div:has(>> #", ns(paste0(z, "-levels")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(z, "-levels")), + ")" + ) ) # DELETE removeUI( - selector = paste0("div:has(>> #", ns(paste0(z, "-remove")), ")") + selector = paste0( + "div:has(>> #", + ns(paste0(z, "-remove")), + ")" + ) ) }) - # FLUSH OBSERVERS filter_observers <- list() - # FLUSH FILTERS values$filters <- NULL - # FLUSH ROWS values$rows <- NULL - }) # UPDATE & FILTER observeEvent(input$close, { - # DATA TO FILTER subset <- values$data - # FILTER DATA if(length(values$filters) != 0) { - # FILTER INDICES - ENTIRE DATASET ind <- unlist( lapply(names(values$filters), function(z) { - col <- values$filters[[z]]$column logic <- values$filters[[z]]$logic levels <- values$filters[[z]]$levels vals <- subset[, col] - # NUMERIC LEVELS if (is.numeric(vals)) { levels <- as.numeric(levels) } - - # LEVELS REQUIRED + # LEVELS REQUIRED - DROP REQUIRED FOR TIBBLES if (!is.null(levels)) { # EQUAL if (logic == "equal") { - return(which(values$data[, col] %in% levels)) + return(which(values$data[, col, drop = TRUE] %in% levels)) # NOT EQUAL } else if (logic == "not equal") { - return(which(!values$data[, col] %in% levels)) + return(which(!values$data[, col, drop = TRUE] %in% levels)) # GREATER THAN } else if (logic == "greater than") { - return(which(values$data[, col] > levels)) + return(which(values$data[, col, drop = TRUE] > levels)) # LESS THAN } else if (logic == "less than") { - return(which(values$data[, col] < levels)) + return(which(values$data[, col, drop = TRUE] < levels)) # GREATER THAN OR EQUAL } else if (logic == "greater than or equal") { - return(which(values$data[, col] >= levels)) + return(which(values$data[, col, drop = TRUE] >= levels)) # LESS THAN OR EQUAL } else if (logic == "less than or equal") { - return(which(values$data[, col] <= levels)) + return(which(values$data[, col, drop = TRUE] <= levels)) # BETWEEN | NOT BETWEEN } else if (logic %in% c("between", "not between")) { ind <- which( - values$data[, col] > levels[1] & - values$data[, col] < levels[2] + values$data[, col, drop = TRUE] > levels[1] & + values$data[, col, drop = TRUE] < levels[2] ) # BETWEEN if (logic == "between") { @@ -628,7 +721,7 @@ dataFilterServer <- function(id, ind <- unique( unlist( lapply(levels, function(z) { - which(grepl(z, subset[, col])) + which(grepl(z, subset[, col, drop = TRUE])) }) ) ) @@ -657,7 +750,6 @@ dataFilterServer <- function(id, } values$subset <- values$data[values$rows, ] } - # CLOSE POPUP removeModal() }) diff --git a/R/dataOutput.R b/R/dataOutput.R index 9901c22..040c02a 100644 --- a/R/dataOutput.R +++ b/R/dataOutput.R @@ -120,6 +120,7 @@ dataOutputServer <- function(id, enable("save") # FORMAT if (!nzchar(trimws(colnames(values$data)[1]))) { + # WARNING - SETTING ROWNAMES ON TIBBLES rownames(values$data) <- values$data[, 1] values$data <- values$data[, -1] } diff --git a/R/dataSelect.R b/R/dataSelect.R index 4dccc9b..ea8d9bf 100644 --- a/R/dataSelect.R +++ b/R/dataSelect.R @@ -94,7 +94,7 @@ dataSelectServer <- function(id, if(!is.null(hover_text)) { addTooltip(session = session, id = ns("select"), - title = "select columns") + title = hover_text) } } diff --git a/R/dataSync.R b/R/dataSync.R new file mode 100644 index 0000000..bcc8b80 --- /dev/null +++ b/R/dataSync.R @@ -0,0 +1,163 @@ +## DATA SYNC MODULE ------------------------------------------------------------ + +#' A shiny module to synchronise datasets +#' +#' The purpose of this module is to merge changes made to a subset of the data +#' with the master copy of the data. +#' +#' @param id unique identifier for the module to prevent namespace clashes when +#' making multiple calls to this shiny module. +#' @param data master copy of the data. +#' @param data_subset subset of \code{data} with altered entries. +#' @param rows the row indices of \code{data_subset} within \code{data}. +#' @param columns the column indices of \code{data_subset} within \code{data}. +#' @param hide logical indicating whether the data synchronisation user +#' interface should be hidden from the user, set to FALSE by default. +#' @param hover_text text to display on download button when user hovers cursor +#' over button, set to NULL by default to turn off hover text. +#' +#' @importFrom shinyjs hidden show +#' @importFrom shinyBS addTooltip +#' @importFrom shiny actionButton icon moduleServer eventReactive is.reactive +#' reactive +#' +#' @author Dillon Hammill, \email{Dillon.Hammill@anu.edu.au} +#' +#' @examples +#' if(interactive()){ +#' library(shiny) +#' library(rhandsontable) +#' library(shinyjs) +#' +#' ui <- fluidPage( +#' useShinyjs(), +#' dataInputUI("input1"), +#' dataFilterUI("filter1"), +#' dataSyncUI("sync1"), +#' dataEditUI("edit1") +#' ) +#' +#' server <- function(input, +#' output, +#' session) { +#' +#' values <- reactiveValues( +#' data = NULL, +#' data_subset = NULL +#' ) +#' +#' data_input <- dataInputServer("input1") +#' +#' data_edit <- dataEditServer( +#' "edit1", +#' data = data_input +#' ) +#' +#' data_sync <- dataSyncServer( +#' "sync1", +#' data = data_input, +#' data_subset = data_edit, +#' rows = NULL, +#' columns = NULL +#' ) +#' +#' } +#' shinyApp(ui, server) +#' } +#' +#' @name dataSync +NULL + +#' @rdname dataSync +#' @export +dataSyncUI <- function(id) { + + hidden( + actionButton( + NS(id, "sync"), + label = NULL, + icon = icon("sync") + ) + ) + +} + +#' @rdname dataSync +#' @export +dataSyncServer <- function(id, + data = reactive(NULL), + data_subset = reactive(NULL), + rows = reactive(NULL), + columns = reactive(NULL), + hide = FALSE, + hover_text = NULL) { + + moduleServer(id, function(input, output, session){ + + # NAMESPACE + ns <- session$ns + + # HIDE USER INTERFACE + if (!hide) { + show("sync") + if(!is.null(hover_text)) { + addTooltip(session = session, + id = ns("sync"), + title = hover_text) + } + } + + # SYNCHRONISE + data_sync <- eventReactive(input$sync, { + data_old <- data() + data_new <- data_subset() + # ROW INDICES + if(is.reactive(rows)) { + row_ind <- rows() + } else { + row_ind <- rows + } + # COLUMN INDICES + if(is.reactive(columns)) { + col_ind <- columns() + } else { + col_ind <- columns + } + # ENTIRE DATA + if(length(row_ind) == 0 & length(col_ind) == 0) { + data_old <- data_new + # DATA SUBSET + } else { + # VALUES + if(length(row_ind) != 0 & length(col_ind) == 0) { + data_old[row_ind, ] <- data_new + } else if(length(row_ind) == 0 & length(col_ind) != 0) { + data_old[ , col_ind] <- data_new + } else if(length(row_ind) != 0 & length(col_ind) != 0) { + data_old[row_ind, col_ind] <- data_new + } + # ROW/COLUMN NAMES + if(!is.null(data_new)) { + # ROW NAMES + if(!all(rownames(data_new) == rownames(data_old)[row_ind])) { + rownames(data_old)[row_ind] <- rownames(data_new) + } + # COLUMN NAMES + if(!all(colnames(data_new) == colnames(data_old)[col_ind])) { + colnames(data_old)[col_ind] <- colnames(data_new) + } + } + } + return(data_old) + }) + + # RETURN SYNCHRONISED DATA + return( + reactive({ + data_sync() + }) + ) + + }) + +} diff --git a/R/data_edit.R b/R/data_edit.R index be52a0c..2bbd052 100644 --- a/R/data_edit.R +++ b/R/data_edit.R @@ -238,11 +238,7 @@ data_edit <- function(x = NULL, style = "padding-left: 5px; margin-top: 35px;", dataSelectUI("select1"), dataFilterUI("filter1"), - hidden( - actionButton("sync", - label = NULL, - icon = icon("sync")) - ), + dataSyncUI("sync1"), dataOutputUI("output-active"), dataOutputUI("output-update", icon = "file-download"), @@ -373,36 +369,15 @@ data_edit <- function(x = NULL, }) # SYNC - observeEvent(input$sync, { - # ENTIRE DATA - if(length(values$rows) == 0 & length(values$columns) == 0) { - values$data <- values$data_active - # DATA - } else { - # VALUES - if(length(values$rows) != 0 & length(values$columns) == 0) { - values$data[values$rows, ] <- values$data_active - } else if(length(values$rows) == 0 & length(values$columns) != 0) { - values$data[ , values$columns] <- values$data_active - } else if(length(values$rows) != 0 & length(values$columns) != 0) { - values$data[values$rows, values$columns] <- values$data_active - } - # ROW/COLUMN NAMES - if(!is.null(values$data_active)) { - # ROW NAMES - if(!all(rownames(values$data_active) == - rownames(values$data)[values$rows])) { - rownames(values$data)[values$rows] <- - rownames(values$data_active) - } - # COLUMN NAMES - if(!all(colnames(values$data_active) == - colnames(values$data)[values$columns])) { - colnames(values$data)[values$columns] <- - colnames(values$data_active) - } - } - } + data_sync <- dataSyncServer("sync1", + data = reactive(values$data), + data_subset = reactive(values$data_active), + rows = reactive(values$rows), + columns = reactive(values$cols), + hide = hide, + hover_text = "synchronise") + observe({ + values$data <- data_sync() }) # DATA OUTPUT - DATA ACTIVE diff --git a/docs/articles/DataEditR.html b/docs/articles/DataEditR.html index 7b43575..b4b4d38 100644 --- a/docs/articles/DataEditR.html +++ b/docs/articles/DataEditR.html @@ -77,7 +77,7 @@ -
.
can be loaded into the data editor.dataSync
module to handle synchronisation of an edited data subset with the complete dataset.dataFilter
to ensure the logic and levels are always updated when the column selection changes.dataInput()
searches for data outside data_edit()
.dataInput
searches for data outside data_edit()
.data_edit()
directly.write_fun
whenever save_as
is supplied.logical indicating whether the data sfiltering user interface +
logical indicating whether the data filtering user interface should be hidden from the user, set to FALSE by default.
The purpose of this module is to merge changes made to a subset of the data +with the master copy of the data.
+dataSyncUI(id, ...) + +dataSyncServer( + id, + data = reactive(NULL), + data_subset = reactive(NULL), + rows = reactive(NULL), + columns = reactive(NULL), + hide = FALSE, + hover_text = NULL +)+ +
id | +unique identifier for the module to prevent namespace clashes when +making multiple calls to this shiny module. |
+
---|---|
data | +master copy of the data. |
+
data_subset | +subset of |
+
rows | +the row indices of |
+
columns | +the column indices of |
+
hide | +logical indicating whether the data synchronisation user +interface should be hidden from the user, set to FALSE by default. |
+
hover_text | +text to display on download button when user hovers cursor +over button, set to NULL by default to turn off hover text. |
+
Dillon Hammill, Dillon.Hammill@anu.edu.au
+ ++if(interactive()){ + library(shiny) + library(rhandsontable) + library(shinyjs) + + ui <- fluidPage( + useShinyjs(), + dataInputUI("input1"), + dataFilterUI("filter1"), + dataSyncUI("sync1"), + dataEditUI("edit1") + ) + + server <- function(input, + output, + session) { + + values <- reactiveValues( + data = NULL, + data_subset = NULL + ) + + data_input <- dataInputServer("input1") + + data_edit <- dataEditServer( + "edit1", + data = data_input + ) + + data_sync <- dataSyncServer( + "sync1", + data = data_input, + data_subset = data_edit, + rows = NULL, + columns = NULL + ) + + } + shinyApp(ui, server) +} + +
dataSyncServer()
+
+ A shiny module to synchronise datasets