File: crosstalk.R

package info (click to toggle)
r-cran-crosstalk 1.0.0%2Bdfsg-3
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 360 kB
  • sloc: makefile: 2
file content (373 lines) | stat: -rw-r--r-- 12,786 bytes parent folder | download
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")
}