1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
|
#' @import htmltools
init <- function() {
htmltools::attachDependencies(
list(),
crosstalkLibs()
)
}
#' Crosstalk dependencies
#'
#' List of \code{\link[htmltools]{htmlDependency}} objects necessary for
#' Crosstalk to function. Intended for widget authors.
#' @importFrom stats na.omit setNames
#' @importFrom utils packageVersion
#' @export
crosstalkLibs <- function() {
list(
jqueryLib(),
htmltools::htmlDependency("crosstalk", packageVersion("crosstalk"),
src = system.file("www", package = "crosstalk"),
script = "js/crosstalk.min.js",
stylesheet = "css/crosstalk.css"
)
)
}
#' ClientValue object
#'
#' An object that can be used in a \href{http://shiny.rstudio.com}{Shiny} server
#' function to get or set a crosstalk variable that exists on the client. The
#' client copy of the variable is the canonical copy, so there is no direct
#' "set" method that immediately changes the value; instead, there is a
#' \code{sendUpdate} method that sends a request to the browser to change the
#' value, which will then cause the new value to be relayed back to the server.
#'
#' @section Methods:
#' \describe{
#' \item{\code{initialize(name, group = "default", session = shiny::getDefaultReactiveDomain())}}{
#' Create a new ClientValue object to reflect the crosstalk variable
#' specified by \code{group} and \code{name}. The \code{session} indicates
#' which Shiny session to connect to, and defaults to the current session.
#' }
#' \item{\code{get()}}{
#' Read the value. This is a reactive operation akin to reading a reactive
#' value, and so can only be done in a reactive context (e.g. in a
#' \code{\link[shiny]{reactive}}, \code{\link[shiny]{observe}}, or
#' \code{\link[shiny]{isolate}} block).
#' }
#' \item{\code{sendUpdate(value)}}{
#' Send a message to the browser asking it to update the crosstalk var to
#' the given value. This update does not happen synchronously, that is, a
#' call to \code{get()} immediately following \code{sendUpdate(value)} will
#' not reflect the new value. The value must be serializable as JSON using
#' jsonlite.
#' }
#' }
#'
#' @examples
#' library(shiny)
#'
#' server <- function(input, output, session) {
#' cv <- ClientValue$new("var1", "group1")
#'
#' r <- reactive({
#' # Don't proceed unless cv$get() is a non-NULL value
#' validate(need(cv$get(), message = FALSE))
#'
#' runif(cv$get())
#' })
#'
#' observeEvent(input$click, {
#' cv$sendUpdate(NULL)
#' })
#' }
#'
#' @docType class
#' @import R6
#' @format An \code{\link{R6Class}} generator object
#' @export
ClientValue <- R6Class(
"ClientValue",
private = list(
.session = "ANY",
.name = "ANY",
.group = "ANY",
.qualifiedName = "ANY",
.rv = "ANY"
),
public = list(
initialize = function(name, group = "default", session = shiny::getDefaultReactiveDomain()) {
private$.session <- session
private$.name <- name
private$.group <- group
private$.qualifiedName <- paste0(".clientValue-", group, "-", name)
},
get = function() {
private$.session$input[[private$.qualifiedName]]
},
sendUpdate = function(value) {
private$.session$sendCustomMessage("update-client-value", list(
name = private$.name,
group = private$.group,
value = value
))
}
)
)
createUniqueId <- function (bytes, prefix = "", suffix = "") {
paste(prefix, paste(format(as.hexmode(sample(256, bytes,
replace = TRUE) - 1), width = 2), collapse = ""),
suffix, sep = "")
}
#' An R6 class that represents a shared data frame
#'
#' ...or sufficiently data frame-like object. The primary use for
#' \code{SharedData} is to be passed to Crosstalk-compatible widgets in place
#' of a data frame. Each \code{SharedData$new(...)} call makes a new "group"
#' of widgets that link to each other, but not to widgets in other groups.
#' You can also use a \code{SharedData} object from Shiny code in order to
#' react to filtering and brushing from non-widget visualizations (like ggplot2
#' plots).
#'
#' @section Constructor:
#'
#' \code{SharedData$new(data, key = NULL, group = createUniqueId(4, prefix = "SharedData"))}
#'
#' \describe{
#' \item{\code{data}}{
#' A data frame-like object, or a Shiny \link[=reactive]{reactive
#' expression} that returns a data frame-like object.
#' }
#' \item{\code{key}}{
#' Character vector or one-sided formula that indicates the name of the
#' column that represents the key or ID of the data frame. These \emph{must}
#' be unique, and ideally will be something intrinsic to the data (a proper
#' ID) rather than a transient property like row index.
#'
#' If \code{NULL}, then \code{row.names(data)} will be used.
#' }
#' \item{\code{group}}{
#' The "identity" of the Crosstalk group that widgets will join when you
#' pass them this \code{SharedData} object. In some cases, you will want to
#' have multiple independent \code{SharedData} objects link up to form a
#' single web of widgets that all share selection and filtering state; in
#' those cases, you'll give those \code{SharedData} objects the same group
#' name. (One example: in Shiny, ui.R and server.R might each need their own
#' \code{SharedData} instance, even though they're intended to represent a
#' single group.)
#' }
#' }
#'
#' @section Methods:
#'
#' \describe{
#' \item{\code{data(withSelection = FALSE, withFilter = TRUE, withKey = FALSE)}}{
#' Return the data (or read and return the data if the data is a Shiny
#' reactive expression). If \code{withSelection}, add a \code{selection_}
#' column with logical values indicating which rows are in the current
#' selection, or \code{NA} if no selection is currently active. If
#' \code{withFilter} (the default), only return rows that are part of the
#' current filter settings, if any. If \code{withKey}, add a \code{key_}
#' column with the key values of each row (normally not needed since the
#' key is either one of the other columns or else just the row names).
#'
#' When running in Shiny, calling \code{data()} is a reactive operation
#' that will invalidate if the selection or filter change (assuming that
#' information was requested), or if the original data is a reactive
#' expression that has invalidated.
#' }
#' \item{\code{origData()}}{
#' Return the data frame that was used to create this \code{SharedData}
#' instance. If a reactive expression, evaluate the reactive expression.
#' Equivalent to \code{data(FALSE, FALSE, FALSE)}.
#' }
#' \item{\code{groupName()}}{
#' Returns the value of \code{group} that was used to create this instance.
#' }
#' \item{\code{key()}}{
#' Returns the vector of key values. Filtering is not applied.
#' }
#' \item{\code{selection(value, ownerId = "")}}{
#' If called without arguments, returns a logical vector of rows that are
#' currently selected (brushed), or \code{NULL} if no selection exists.
#' Intended to be called from a Shiny reactive context, and invalidates
#' whenever the selection changes.
#'
#' If called with one or two arguments, expects \code{value} to be a logical
#' vector of \code{nrow(origData())} length, indicating which rows are
#' currently selected (brushed). This value is propagated to the web browser
#' (assumes an active Shiny app or Shiny R Markdown document).
#'
#' Set the \code{ownerId} argument to the \code{outputId} of a widget if
#' conceptually that widget "initiated" the selection (prevents that widget
#' from clearing its visual selection box, which is normally cleared when
#' the selection changes). For example, if setting the selection based on a
#' \code{\link[shiny]{plotOutput}} brush, then \code{ownerId} should be the
#' \code{outputId} of the \code{plotOutput}.
#' }
#' \item{\code{clearSelection(ownerId = "")}}{
#' Clears the selection. For the meaning of \code{ownerId}, see the
#' \code{selection} method.
#' }
#' }
#'
#' @import R6 shiny
#' @export
SharedData <- R6Class(
"SharedData",
private = list(
.data = "ANY",
.key = "ANY",
.filterCV = "ANY",
.selectionCV = "ANY",
.rv = "ANY",
.group = "ANY"
),
public = list(
initialize = function(data, key = NULL, group = createUniqueId(4, prefix = "SharedData")) {
private$.data <- data
private$.filterCV <- ClientValue$new("filter", group)
private$.selectionCV <- ClientValue$new("selection", group)
private$.rv <- shiny::reactiveValues()
private$.group <- group
if (inherits(key, "formula")) {
private$.key <- key
} else if (is.character(key)) {
private$.key <- key
} else if (is.function(key)) {
private$.key <- key
} else if (is.null(key)) {
private$.key <- key
} else {
stop("Unknown key type")
}
if (shiny::is.reactive(private$.data)) {
observeEvent(private$.data(), {
self$clearSelection()
})
}
domain <- shiny::getDefaultReactiveDomain()
if (!is.null(domain)) {
observe({
selection <- private$.selectionCV$get()
if (!is.null(selection) && length(selection) > 0) {
self$.updateSelection(self$key() %in% selection)
} else {
self$.updateSelection(NULL)
}
})
}
},
origData = function() {
if (shiny::is.reactive(private$.data)) {
private$.data()
} else {
private$.data
}
},
groupName = function() {
private$.group
},
key = function() {
df <- if (shiny::is.reactive(private$.data)) {
private$.data()
} else {
private$.data
}
key <- private$.key
if (inherits(key, "formula"))
lazyeval::f_eval(key, df)
else if (is.character(key))
key
else if (is.function(key))
key(df)
else if (!is.null(row.names(df)))
row.names(df)
else if (nrow(df) > 0)
as.character(1:nrow(df))
else
character()
},
data = function(withSelection = FALSE, withFilter = TRUE, withKey = FALSE) {
df <- if (shiny::is.reactive(private$.data)) {
private$.data()
} else {
private$.data
}
op <- options(shiny.suppressMissingContextError = TRUE)
on.exit(options(op), add = TRUE)
if (withSelection) {
if (is.null(private$.rv$selected) || length(private$.rv$selected) == 0) {
df$selected_ = NA
} else {
# TODO: Warn if the length of _selected is different?
df$selected_ <- private$.rv$selected
}
}
if (withKey) {
df$key_ <- self$key()
}
if (withFilter) {
if (!is.null(private$.filterCV$get())) {
df <- df[self$key() %in% private$.filterCV$get(),]
}
}
df
},
# Public API for selection getting/setting. Setting a selection will
# cause an event to be propagated to the client.
selection = function(value, ownerId = "") {
if (missing(value)) {
return(private$.rv$selected)
} else {
# TODO: Should we even update the server at this time? Or do we
# force all such events to originate in the client (much like
# updateXXXInput)?
# .updateSelection needs logical array of length nrow(data)
# .selectionCV$sendUpdate needs character array of keys
isolate({
if (is.null(value)) {
self$.updateSelection(NULL)
private$.selectionCV$sendUpdate(NULL)
} else {
key <- self$key()
if (is.character(value)) {
self$.updateSelection(key %in% value)
private$.selectionCV$sendUpdate(value)
} else if (is.logical(value)) {
self$.updateSelection(value)
private$.selectionCV$sendUpdate(key[value])
} else if (is.numeric(value)) {
self$selection(1:nrow(self$data(FALSE)) %in% value)
}
}
})
}
},
clearSelection = function(ownerId = "") {
self$selection(list(), ownerId = "")
},
# Update selection without sending event
.updateSelection = function(value) {
force(value)
`$<-`(private$.rv, "selected", value)
}
)
)
#' Check if an object is \code{SharedData}
#'
#' Check if an object is an instance of \code{\link{SharedData}} or not.
#'
#' @param x The object that may or may not be an instance of \code{SharedData}
#' @return logical
#'
#' @export
is.SharedData <- function(x) {
inherits(x, "SharedData")
}
|