Skip to content

Commit b73f190

Browse files
authored
Merge pull request #1392 from ropensci/input-events
Add more input events
2 parents 1e0477b + 35a6887 commit b73f190

File tree

30 files changed

+2465
-80
lines changed

30 files changed

+2465
-80
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ before_script:
3333

3434
# work around temporary travis + R 3.5 bug
3535
r_packages: devtools
36+
37+
# temporary: needed for plotly input testing in shiny
38+
r_github_packages: rstudio/shinytest

DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Imports:
2727
tools,
2828
scales,
2929
httr,
30-
jsonlite,
30+
jsonlite (>= 1.6),
3131
magrittr,
3232
digest,
3333
viridisLite,
@@ -54,6 +54,7 @@ Suggests:
5454
knitr,
5555
devtools,
5656
shiny (>= 1.1.0),
57+
shinytest (> 1.3.0),
5758
curl,
5859
rmarkdown,
5960
vdiffr,

NAMESPACE

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ export(do)
128128
export(do_)
129129
export(embed_notebook)
130130
export(event_data)
131+
export(event_register)
132+
export(event_unregister)
131133
export(export)
132134
export(filter)
133135
export(filter_)
@@ -246,7 +248,8 @@ importFrom(httr,config)
246248
importFrom(httr,content)
247249
importFrom(httr,stop_for_status)
248250
importFrom(httr,warn_for_status)
249-
importFrom(jsonlite,fromJSON)
251+
importFrom(jsonlite,parse_json)
252+
importFrom(jsonlite,read_json)
250253
importFrom(jsonlite,toJSON)
251254
importFrom(lazyeval,all_dots)
252255
importFrom(lazyeval,f_eval)

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
* The `orca_serve()` function was added for efficient exporting of many plotly graphs. For examples, see `help(orca_serve)`.
66
* The `orca()` function gains new arguments `more_args` and `...` for finer control over the underlying system commands.
7+
* Improvements related to accessing plotly.js events in shiny:
8+
* The `event` argument of the `event_data()` function now supports the following events: `plotly_selecting`, `plotly_brushed`, `plotly_brushing`, `plotly_restyle`, `plotly_legendclick`, `plotly_legenddoubleclick`, `plotly_clickannotation`, `plotly_afterplot`, `plotly_doubleclick`, `plotly_deselect`, `plotly_unhover`. For examples, see `plotly_example("shiny", "event_data")`, `plotly_example("shiny", "event_data_legends")`, and `plotly_example("shiny", "event_data_annotation")`,
9+
* New `event_register()` and `event_unregister()` functions for declaring which events to transmit over the wire (i.e., from the browser to the shiny server). Events that are likely to have large overhead are not registered by default, so you'll need to register these: `plotly_selecting`, `plotly_unhover`, `plotly_restyle`, `plotly_legendclick`, and `plotly_legenddoubleclick`.
10+
* A new `priority` argument. By setting `priority='event'`, the `event` is treated like a true event: any reactive expression using the `event` becomes invalidated (regardless of whether the input values has changed). For an example, see `plotly_example("shiny", "event_priority")`.
11+
* The `method` argument of `plotlyProxyInvoke()` gains support for a `"reconfig"` method. This makes it possible to change just the configuration of a plot. For an example use, see `plotly_example("shiny", "event_data_annotation")`.
712

813
## IMPROVEMENTS
914

R/imports.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#' @importFrom stats setNames complete.cases quantile is.leaf
66
#' @importFrom tidyr unnest
77
#' @importFrom viridisLite viridis
8-
#' @importFrom jsonlite toJSON fromJSON
8+
#' @importFrom jsonlite toJSON parse_json read_json
99
#' @importFrom httr GET POST PATCH content config add_headers stop_for_status warn_for_status
1010
#' @importFrom htmlwidgets createWidget sizingPolicy saveWidget onRender prependContent
1111
#' @importFrom lazyeval f_eval is_formula all_dots is_lang f_new

R/plotly_build.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,9 @@ plotly_build.plotly <- function(p, registerFrames = TRUE) {
368368
p <- registerFrames(p, frameMapping = frameMapping)
369369
}
370370

371+
# set the default plotly.js events to register in shiny
372+
p <- shiny_defaults_set(p)
373+
371374
p <- verify_guides(p)
372375

373376
# verify plot attributes are legal according to the plotly.js spec

R/proxy.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ plotlyjs_methods <- function() {
101101
c(
102102
"restyle", "relayout", "update", "addTraces", "deleteTraces", "moveTraces",
103103
"extendTraces", "prependTraces", "purge", "toImage", "downloadImage", "animate",
104-
"newPlot", "react", "validate", "makeTemplate", "validateTemplate", "addFrames"
104+
"newPlot", "react", "validate", "makeTemplate", "validateTemplate", "addFrames",
105+
"reconfig"
105106
)
106107
}
107108

R/shiny.R

Lines changed: 161 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ renderPlotly <- function(expr, env = parent.frame(), quoted = FALSE) {
4646
shiny::snapshotPreprocessOutput(
4747
renderFunc,
4848
function(value) {
49-
json <- from_JSON_safe(value)
49+
json <- from_JSON(value)
5050
json$x <- json$x[setdiff(names(json$x), c("visdat", "cur_data", "attrs"))]
5151
to_JSON(json)
5252
}
@@ -55,38 +55,185 @@ renderPlotly <- function(expr, env = parent.frame(), quoted = FALSE) {
5555

5656
# Converts a plot, OR a promise of a plot, to plotly
5757
prepareWidget <- function(x) {
58-
if (promises::is.promising(x)) {
59-
promises::then(x, ggplotly)
58+
p <- if (promises::is.promising(x)) {
59+
promises::then(x, plotly_build)
6060
} else {
61-
ggplotly(x)
61+
plotly_build(x)
6262
}
63+
register_plot_events(p)
64+
p
65+
}
66+
67+
register_plot_events <- function(p) {
68+
session <- getDefaultReactiveDomain()
69+
eventIDs <- paste(p$x$shinyEvents, p$x$source, sep = "-")
70+
session$userData$plotlyShinyEventIDs <- unique(c(
71+
session$userData$plotlyShinyEventIDs,
72+
eventIDs
73+
))
6374
}
6475

6576

6677
#' Access plotly user input event data in shiny
6778
#'
6879
#' This function must be called within a reactive shiny context.
6980
#'
70-
#' @param event The type of plotly event. Currently 'plotly_hover',
71-
#' 'plotly_click', 'plotly_selected', and 'plotly_relayout' are supported.
81+
#' @param event The type of plotly event. All supported events are listed in the
82+
#' function signature above (i.e., the usage section).
7283
#' @param source a character string of length 1. Match the value of this string
73-
#' with the source argument in [plot_ly()] to retrieve the
74-
#' event data corresponding to a specific plot (shiny apps can have multiple plots).
84+
#' with the `source` argument in [plot_ly()] (or [ggplotly()]) to respond to
85+
#' events emitted from that specific plot.
7586
#' @param session a shiny session object (the default should almost always be used).
87+
#' @param priority the priority of the corresponding shiny input value.
88+
#' If equal to `"event"`, then [event_data()] always triggers re-execution,
89+
#' instead of re-executing only when the relevant shiny input value changes
90+
#' (the default).
7691
#' @export
92+
#' @seealso [event_register], [event_unregister]
93+
#' @references
94+
#' * <https://plotly-book.cpsievert.me/shiny-plotly-inputs.html>
95+
#' * <https://plot.ly/javascript/plotlyjs-function-reference/>
7796
#' @author Carson Sievert
7897
#' @examples \dontrun{
7998
#' plotly_example("shiny", "event_data")
8099
#' }
81100

82-
event_data <- function(event = c("plotly_hover", "plotly_click", "plotly_selected",
83-
"plotly_relayout"), source = "A",
84-
session = shiny::getDefaultReactiveDomain()) {
101+
event_data <- function(
102+
event = c(
103+
"plotly_hover", "plotly_unhover", "plotly_click", "plotly_doubleclick",
104+
"plotly_selected", "plotly_selecting", "plotly_brushed", "plotly_brushing",
105+
"plotly_deselect", "plotly_relayout", "plotly_restyle", "plotly_legendclick",
106+
"plotly_legenddoubleclick", "plotly_clickannotation", "plotly_afterplot"
107+
),
108+
source = "A",
109+
session = shiny::getDefaultReactiveDomain(),
110+
priority = c("input", "event")
111+
) {
85112
if (is.null(session)) {
86113
stop("No reactive domain detected. This function can only be called \n",
87114
"from within a reactive shiny context.")
88115
}
89-
src <- sprintf(".clientValue-%s-%s", event[1], source)
90-
val <- session$rootScope()$input[[src]]
91-
if (is.null(val)) val else jsonlite::fromJSON(val)
116+
117+
event <- match.arg(event)
118+
eventID <- paste(event, source, sep = "-")
119+
120+
# It's possible for event_data() to execute before any
121+
# relevant input values have been registered (i.e, before
122+
# relevant plotly graphs have been executed). Therefore,
123+
# we delay checking that a relevant input value has been
124+
# registered until shiny flushes
125+
session$onFlushed(
126+
function() {
127+
eventIDRegistered <- eventID %in% session$userData$plotlyShinyEventIDs
128+
if (!eventIDRegistered) {
129+
warning(
130+
"The '", event, "' event tied a source ID of '", source, "' ",
131+
"is not registered. In order to obtain this event data, ",
132+
"please add `event_register(p, '", event, "')` to the plot (`p`) ",
133+
"that you wish to obtain event data from.",
134+
call. = FALSE
135+
)
136+
}
137+
}
138+
)
139+
140+
# legend clicking returns trace(s), which shouldn't be simplified...
141+
parseJSON <- if (event %in% c("plotly_legendclick", "plotly_legenddoubleclick")) {
142+
from_JSON
143+
} else {
144+
function(x) jsonlite::parse_json(x, simplifyVector = TRUE)
145+
}
146+
147+
# Handle NULL sensibly
148+
parseJSONVal <- function(x) {
149+
if (is.null(x)) x else parseJSON(x)
150+
}
151+
152+
parsedInputValue <- function() {
153+
parseJSONVal(session$rootScope()$input[[eventID]])
154+
}
155+
156+
priority <- match.arg(priority)
157+
if (priority == "event") {
158+
# Shiny.setInputValue() is always called with event priority
159+
# so simply return the parse input value
160+
return(parsedInputValue())
161+
162+
} else {
163+
164+
eventHasStorage <- eventID %in% names(session$userData$plotlyInputStore)
165+
166+
if (!eventHasStorage) {
167+
# store input value as a reactive value to leverage caching
168+
session$userData$plotlyInputStore <- session$userData$plotlyInputStore %||% shiny::reactiveValues()
169+
session$userData$plotlyInputStore[[eventID]] <- shiny::isolate(parsedInputValue())
170+
shiny::observe({
171+
session$userData$plotlyInputStore[[eventID]] <- parsedInputValue()
172+
}, priority = 10000, domain = session)
173+
}
174+
175+
session$userData$plotlyInputStore[[eventID]]
176+
}
177+
178+
}
179+
180+
181+
#' Register a shiny input value
182+
#'
183+
#' @inheritParams event_data
184+
#' @seealso [event_data]
185+
#' @export
186+
#' @author Carson Sievert
187+
event_register <- function(p, event = NULL) {
188+
event <- match.arg(event, event_data_events())
189+
shiny_event_add(p, event)
190+
}
191+
192+
#' Un-register a shiny input value
193+
#'
194+
#' @inheritParams event_data
195+
#' @seealso [event_data]
196+
#' @export
197+
#' @author Carson Sievert
198+
event_unregister <- function(p, event = NULL) {
199+
event <- match.arg(event, event_data_events())
200+
shiny_event_remove(p, event)
201+
}
202+
203+
204+
# helpers
205+
shiny_event_add <- function(p, event) {
206+
p <- shiny_defaults_set(p)
207+
p$x$shinyEvents <- unique(c(p$x$shinyEvents, event))
208+
p
209+
}
210+
211+
shiny_event_remove <- function(p, event) {
212+
p <- shiny_defaults_set(p)
213+
p$x$shinyEvents <- setdiff(p$x$shinyEvents, event)
214+
p
215+
}
216+
217+
shiny_defaults_set <- function(p) {
218+
p$x$shinyEvents <- p$x$shinyEvents %||% shiny_event_defaults()
219+
p
220+
}
221+
222+
shiny_event_defaults <- function() {
223+
c(
224+
"plotly_hover",
225+
"plotly_click",
226+
"plotly_selected",
227+
"plotly_relayout",
228+
"plotly_brushed",
229+
"plotly_brushing",
230+
"plotly_clickannotation",
231+
"plotly_doubleclick",
232+
"plotly_deselect",
233+
"plotly_afterplot"
234+
)
235+
}
236+
237+
event_data_events <- function() {
238+
eval(formals(event_data)$event)
92239
}

R/utils.R

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,7 @@ grab <- function(what = "username") {
949949

950950
# try to grab an object key from a JSON file (returns empty string on error)
951951
try_file <- function(f, what) {
952-
tryCatch(jsonlite::fromJSON(f)[[what]], error = function(e) NULL)
952+
tryCatch(jsonlite::read_json(f)[[what]], error = function(e) NULL)
953953
}
954954

955955
# preferred defaults for toJSON mapping
@@ -960,14 +960,7 @@ to_JSON <- function(x, ...) {
960960

961961
# preferred defaults for toJSON mapping
962962
from_JSON <- function(x, ...) {
963-
jsonlite::fromJSON(x, simplifyDataFrame = FALSE, simplifyMatrix = FALSE, ...)
964-
}
965-
966-
from_JSON_safe <- function(txt, ...) {
967-
if (!jsonlite::validate(txt)) {
968-
stop("Expected a valid JSON string.")
969-
}
970-
from_JSON(txt, ...)
963+
jsonlite::parse_json(x, simplifyVector = TRUE, simplifyDataFrame = FALSE, simplifyMatrix = FALSE, ...)
971964
}
972965

973966
i <- function(x) {

inst/examples/shiny/event_data/app.R

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@ ui <- fluidPage(
66
plotlyOutput("plot"),
77
verbatimTextOutput("hover"),
88
verbatimTextOutput("click"),
9-
verbatimTextOutput("brush")
9+
verbatimTextOutput("selected"),
10+
verbatimTextOutput("selecting"),
11+
verbatimTextOutput("brushed"),
12+
verbatimTextOutput("brushing")
1013
)
1114

1215
server <- function(input, output, session) {
1316

1417
nms <- row.names(mtcars)
1518

1619
output$plot <- renderPlotly({
17-
if (identical(input$plotType, "ggplotly")) {
18-
p <- ggplot(mtcars, aes(x = mpg, y = wt, customdata = nms)) + geom_point()
19-
ggplotly(p) %>% layout(dragmode = "select")
20+
p <- if (identical(input$plotType, "ggplotly")) {
21+
ggplotly(ggplot(mtcars, aes(x = mpg, y = wt, customdata = nms)) + geom_point())
2022
} else {
21-
plot_ly(mtcars, x = ~mpg, y = ~wt, customdata = nms) %>%
22-
layout(dragmode = "select")
23+
plot_ly(mtcars, x = ~mpg, y = ~wt, customdata = nms)
2324
}
25+
p %>%
26+
layout(dragmode = "select") %>%
27+
event_register("plotly_selecting")
2428
})
2529

2630
output$hover <- renderPrint({
@@ -33,11 +37,26 @@ server <- function(input, output, session) {
3337
if (is.null(d)) "Click events appear here (double-click to clear)" else d
3438
})
3539

36-
output$brush <- renderPrint({
40+
output$selected <- renderPrint({
3741
d <- event_data("plotly_selected")
3842
if (is.null(d)) "Click and drag events (i.e., select/lasso) appear here (double-click to clear)" else d
3943
})
4044

45+
output$selecting <- renderPrint({
46+
d <- event_data("plotly_selecting")
47+
if (is.null(d)) "Click and drag events (i.e., select/lasso) appear here (double-click to clear)" else d
48+
})
49+
50+
output$brush <- renderPrint({
51+
d <- event_data("plotly_brushed")
52+
if (is.null(d)) "Extents of the selection brush will appear here." else d
53+
})
54+
55+
output$brushing <- renderPrint({
56+
d <- event_data("plotly_brushing")
57+
if (is.null(d)) "Extents of the selection brush will appear here." else d
58+
})
59+
4160
}
4261

4362
shinyApp(ui, server, options = list(display.mode = "showcase"))

0 commit comments

Comments
 (0)