File: shiny.R

package info (click to toggle)
r-cran-ggvis 0.4.4%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 1,716 kB
  • sloc: sh: 25; makefile: 2
file content (217 lines) | stat: -rw-r--r-- 6,509 bytes parent folder | download | duplicates (3)
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
#' Connect a ggvis graphic to a shiny app.
#'
#' Embedding ggvis in a shiny app is easy. You need to make a place for it in
#' your \code{ui.r} with \code{ggvisOutput}, and tell your \code{server.r}
#' where to draw it with \code{bind_shiny}. It's easiest to learn by example:
#' there are many shiny apps in \code{demo/apps/} that you can learn from.
#'
#' @section Client-side:
#' In your UI, use \code{ggvisOutput()} in \code{ui.r} to insert an html
#' placeholder for the plot.
#'
#' If you're going to be using interactive controls generated by ggvis,
#' use \code{\link[shiny]{renderUI}()} to add a place holder. By convention,
#' if the id of plot placehold is called "plot", call the controls placeholder
#' "plot_ui".
#'
#' @section Server-side:
#' When you run ggvis plot interactively, it is automatically plotted because
#' it triggers the default print method. In shiny apps, you need to
#' explicitly render the plot to a specific placeholder with
#' \code{bind_shiny}:
#'
#' \code{p \%>\% bind_shiny("plot")}
#'
#' If the plot has controls, and you've reserved space for them in the UI,
#' supply the name of the placeholder as the third argument:
#'
#' \code{p \%>\% bind_shiny("plot", "plot_ui")}
#' @examples
#' ## Run these examples only in interactive R sessions
#' if (interactive()) {
#'
#' # Simplest possible app:
#' library(shiny)
#' runApp(list(
#'   ui = bootstrapPage(
#'     ggvisOutput("p"),
#'     uiOutput("p_ui")
#'   ),
#'   server = function(..., session) {
#'     mtcars %>%
#'       ggvis(~wt, ~mpg) %>%
#'       layer_points() %>%
#'       layer_smooths(span = input_slider(0, 1)) %>%
#'       bind_shiny("p", "p_ui")
#'   }
#' ))
#'
#' }
#' @name shiny-ggvis
NULL

#' @rdname shiny-ggvis
#' @param vis A ggvis object, or a reactive expression that returns a ggvis
#'   object.
#' @param session A Shiny session object.
#' @param ... Other arguments passed to \code{as.vega}.
#' @export
bind_shiny <- function(vis, plot_id, controls_id = NULL, ...,
                       session = shiny::getDefaultReactiveDomain()) {

  validate_plot_id(plot_id)

  if (is.null(session)) {
    stop("bind_shiny() must be run inside a shiny app.", call. = FALSE)
  }

  if (shiny::is.reactive(vis)) {
    visf <- vis
  } else if (is.ggvis(vis)) {
    visf <- function() vis
  } else {
    stop("bind_shiny requires a ggvis object or a reactive expression that returns a ggvis object",
      call. = FALSE)
  }

  r_spec <- shiny::reactive({
    as.vega(visf(), session = session, dynamic = TRUE, ...)
  })

  observe_spec(r_spec, plot_id, session)
  observe_data(r_spec, plot_id, session)
  exec_connectors(r_spec, plot_id, session)

  if (!is.null(controls_id)) {
    bind_shiny_ui(vis, controls_id, session = session)
  }

  vis
}

#' @param controls_id Unique identifier for controls div.
#' @rdname shiny-ggvis
#' @export
bind_shiny_ui <- function(vis, controls_id,
  session = shiny::getDefaultReactiveDomain()) {
  if (is.null(session)) {
    stop("bind_shiny_ui() must be run inside a shiny app.", call. = FALSE)
  }

  if (shiny::is.reactive(vis)) {
    visf <- vis
  } else if (is.ggvis(vis)) {
    visf <- function() vis
  } else {
    stop("bind_shiny_ui requires a ggvis object or a reactive expression that returns a ggvis object",
      call. = FALSE)
  }

  shiny::observe({
    controls <- visf()$controls
    if (empty(controls)) return()

    # Wrap each control in a div, for layout purposes
    divs <- lapply(controls, htmltools::div,  class = "ggvis-input-container")
    session$output[[controls_id]] <- shiny::renderUI(htmltools::tagList(divs))
  })

  vis
}

# Tell an observer to suspend or resume, depending on the hidden state of an
# output object on the client.
sync_with_hidden_state <- function(obs, id, session) {
  force(obs)

  shiny::observe({
    isHidden <- session$clientData[[paste0('output_', id, '_hidden')]]

    if (identical(isHidden, FALSE)) {
      obs$resume()
    } else {
      obs$suspend()
    }
  })
}

# Create an observer for a reactive vega spec
observe_spec <- function(r_spec, id, session) {
  obs <- shiny::observe(suspended = TRUE, {
    session$sendCustomMessage("ggvis_vega_spec", list(
      plotId = id,
      spec = r_spec()
    ))
  })

  sync_with_hidden_state(obs, id, session)
}

# Create observers for the data objects attached to a reactive vega spec
observe_data <- function(r_spec, id, session) {
  # A list for keeping track of each data observer
  data_observers <- list()

  outer_obs <- shiny::observe(suspended = TRUE, {
    # If data_observers list is nonempty, that means there are old observers
    # which need to be suspended before we create new ones. This can happen when
    # the reactive containing the ggvis() call is invalidated.
    for (obs in data_observers) obs$suspend()

    data_table <- c(attr(r_spec(), "data_table", TRUE),
                    attr(r_spec(), "scale_data_table", TRUE))

    # Create observers for each of the data objects
    data_observers <<- lapply(names(data_table), function(data_name) {
      # The data_table list contains named objects. The names are synthetic IDs
      # that are present in the vega spec.

      force(data_name)
      obs <- shiny::observe(suspended = TRUE, {
        data_reactive <- data_table[[data_name]]

        session$sendCustomMessage("ggvis_data", list(
          plotId = id,
          name = data_name,
          value = as.vega(data_reactive(), data_name)
        ))
      })
      sync_with_hidden_state(obs, id, session)

      obs
    })

    # Tell the plot to update _after_ all the data has been sent
    data_observers[[length(data_observers) + 1]] <<- shiny::observe(suspended = TRUE, {
      # Take dependency on all data objects
      for (name in names(data_table)) {
        data_table[[name]]()
      }

      session$sendCustomMessage("ggvis_command", list(
        plotId = id,
        command = "update"
      ))
    }, priority = -1)
    sync_with_hidden_state(data_observers[[length(data_observers)]], id, session)
  })

  sync_with_hidden_state(outer_obs, id, session)
}

# Run the connector functions
exec_connectors <- function(r_spec, plot_id, session) {
  connectors <- shiny::isolate(attr(r_spec(), "connectors", TRUE))

  lapply(connectors, function(connect) {
    if (!is.null(connect)) {
      connect(session, plot_id)
    }
  })
}

validate_plot_id <- function(id) {
  if (grepl(".", id, fixed = TRUE)) {
    stop("Plot ID '", id, "' is not valid. The ID must not contain a dot (.) character.")
  }
}