Skip to content

Add more input events #1392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 61 commits into from
Feb 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
379e482
add restyle event, addresses #1321
cpsievert Nov 2, 2018
133e5ff
Merge branch 'master' into input-events
cpsievert Nov 6, 2018
06de984
add an example of mapping parcoord ranges back to the observations of…
cpsievert Nov 8, 2018
8d71818
Merge branch 'master' into input-events
cpsievert Nov 20, 2018
39553e6
New shiny input event 'plotly_brush' returns the x/y range of rectang…
cpsievert Nov 20, 2018
eb5a093
add plotly_selecting and plotly_brushing
cpsievert Nov 27, 2018
40f039c
Merge branch 'master' into input-events
cpsievert Nov 27, 2018
f949d7c
use Shiny.setInputValue() over Shiny.onInputChange()
cpsievert Nov 30, 2018
849f5f1
Relay full annotation info on 'plotly_clickannotation' event
cpsievert Nov 30, 2018
5514bfe
Relay 'plotly_afterplot' event
cpsievert Nov 30, 2018
5a54e41
Relay traces tied to legend events
cpsievert Nov 30, 2018
f23838f
Allow shiny input priority to be configurable at the plot-level, clos…
cpsievert Dec 1, 2018
5576397
fix legend event data and add example app
cpsievert Dec 1, 2018
fe1f075
add a 'proxy method' for modifying the plot config; add annotation ex…
cpsievert Dec 1, 2018
80a750d
add double-click, unhover, and deselect events
cpsievert Dec 2, 2018
1522f59
update list of supported events and use match.arg() to make sure a su…
cpsievert Dec 2, 2018
e7f718e
avoid fromJSON simplification for legend events
cpsievert Dec 3, 2018
1e10fc7
document
cpsievert Dec 3, 2018
b8789dd
tidy up documentation and function signature
cpsievert Dec 3, 2018
6e9e636
'plotly_unhover' emits data
cpsievert Dec 3, 2018
2abe932
attempt to dynamically register events
cpsievert Dec 4, 2018
65f473a
make sure events are registered once; clean up some other bugs
cpsievert Dec 5, 2018
f2b5426
always send message on flush and tidy up registration logic
cpsievert Dec 5, 2018
eb6cadb
consistent use of [].map
cpsievert Dec 5, 2018
d32b86f
silly off-by-one mistake
cpsievert Dec 5, 2018
8f00e01
tidy up R docs
cpsievert Dec 5, 2018
14d674d
resolve priority of 'null' events server-side (with a message)
cpsievert Dec 5, 2018
0eac8b5
plotly_brushed is more consistent naming than plotly_brush
cpsievert Dec 5, 2018
d2cf6a2
Merge branch 'master' into input-events
cpsievert Dec 6, 2018
dc91b70
Merge branch 'master' into input-events
cpsievert Dec 6, 2018
95b3121
improve parcoords example app
cpsievert Dec 6, 2018
c20fbf4
Treat Shiny.addCustomMessageHandler like the 'global' operation that …
cpsievert Dec 11, 2018
e4d240a
clean-up comments
cpsievert Dec 11, 2018
5b37957
update event priority example app
cpsievert Dec 11, 2018
d2e4042
only add message handler when in shinyMode
cpsievert Dec 11, 2018
8377bec
revert to 6e9e636
cpsievert Dec 12, 2018
f293486
force user to specify inputs/events to register as part of plot object
cpsievert Dec 13, 2018
c52909a
use modern jsonlite::parse_json for safer JSON parsing
cpsievert Dec 13, 2018
f62c50e
Track plot events in user's session data to throw informative errors
cpsievert Dec 14, 2018
1437d64
Merge branch 'master' into input-events
cpsievert Dec 17, 2018
ecbe79d
more clean-up
cpsievert Dec 17, 2018
f2937d7
events currently need their own dedicated input value
cpsievert Dec 17, 2018
d6ea8bc
merge with master
cpsievert Jan 22, 2019
a159ded
fix event_data shiny example
cpsievert Jan 22, 2019
d4c1d58
remove .clientValue prefix
cpsievert Jan 25, 2019
834498a
always set input values with event priority and leverage reactive cac…
cpsievert Jan 25, 2019
89296a5
fix test expectations
cpsievert Jan 25, 2019
d2ffbb0
maintain backwards-compatibility; document priority argument; update …
cpsievert Jan 25, 2019
a3b826d
more news
cpsievert Jan 25, 2019
e5dde36
clean-up
cpsievert Jan 25, 2019
9128db3
new bullet about event_register() and event_unregister()
cpsievert Jan 28, 2019
7671756
more news improvements
cpsievert Jan 28, 2019
781643d
add a shinytest test for event_data app
cpsievert Jan 28, 2019
bd56d2e
update event_data tests
cpsievert Jan 29, 2019
3519787
disable shinytest tests during visual test runs
cpsievert Jan 29, 2019
71d5d48
actually disable this time
cpsievert Jan 29, 2019
fe2684a
actually, no really, disable shinytest in vdiffr run
cpsievert Jan 29, 2019
39d9e92
load plotly in example shiny apps
cpsievert Jan 30, 2019
6155442
throw a warning (not an error) if the input value that event_data() i…
cpsievert Jan 31, 2019
8db07c3
Wait to check input values have been registered until after shiny's r…
cpsievert Feb 1, 2019
35a6887
warn instead of stop
cpsievert Feb 5, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ before_script:

# work around temporary travis + R 3.5 bug
r_packages: devtools

# temporary: needed for plotly input testing in shiny
r_github_packages: rstudio/shinytest
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Imports:
tools,
scales,
httr,
jsonlite,
jsonlite (>= 1.6),
magrittr,
digest,
viridisLite,
Expand All @@ -54,6 +54,7 @@ Suggests:
knitr,
devtools,
shiny (>= 1.1.0),
shinytest (> 1.3.0),
curl,
rmarkdown,
vdiffr,
Expand Down
5 changes: 4 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export(do)
export(do_)
export(embed_notebook)
export(event_data)
export(event_register)
export(event_unregister)
export(export)
export(filter)
export(filter_)
Expand Down Expand Up @@ -246,7 +248,8 @@ importFrom(httr,config)
importFrom(httr,content)
importFrom(httr,stop_for_status)
importFrom(httr,warn_for_status)
importFrom(jsonlite,fromJSON)
importFrom(jsonlite,parse_json)
importFrom(jsonlite,read_json)
importFrom(jsonlite,toJSON)
importFrom(lazyeval,all_dots)
importFrom(lazyeval,f_eval)
Expand Down
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

* The `orca_serve()` function was added for efficient exporting of many plotly graphs. For examples, see `help(orca_serve)`.
* The `orca()` function gains new arguments `more_args` and `...` for finer control over the underlying system commands.
* Improvements related to accessing plotly.js events in shiny:
* 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")`,
* 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`.
* 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")`.
* 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")`.

## IMPROVEMENTS

Expand Down
2 changes: 1 addition & 1 deletion R/imports.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#' @importFrom stats setNames complete.cases quantile is.leaf
#' @importFrom tidyr unnest
#' @importFrom viridisLite viridis
#' @importFrom jsonlite toJSON fromJSON
#' @importFrom jsonlite toJSON parse_json read_json
#' @importFrom httr GET POST PATCH content config add_headers stop_for_status warn_for_status
#' @importFrom htmlwidgets createWidget sizingPolicy saveWidget onRender prependContent
#' @importFrom lazyeval f_eval is_formula all_dots is_lang f_new
Expand Down
3 changes: 3 additions & 0 deletions R/plotly_build.R
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@ plotly_build.plotly <- function(p, registerFrames = TRUE) {
p <- registerFrames(p, frameMapping = frameMapping)
}

# set the default plotly.js events to register in shiny
p <- shiny_defaults_set(p)

p <- verify_guides(p)

# verify plot attributes are legal according to the plotly.js spec
Expand Down
3 changes: 2 additions & 1 deletion R/proxy.R
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ plotlyjs_methods <- function() {
c(
"restyle", "relayout", "update", "addTraces", "deleteTraces", "moveTraces",
"extendTraces", "prependTraces", "purge", "toImage", "downloadImage", "animate",
"newPlot", "react", "validate", "makeTemplate", "validateTemplate", "addFrames"
"newPlot", "react", "validate", "makeTemplate", "validateTemplate", "addFrames",
"reconfig"
)
}

Expand Down
175 changes: 161 additions & 14 deletions R/shiny.R
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ renderPlotly <- function(expr, env = parent.frame(), quoted = FALSE) {
shiny::snapshotPreprocessOutput(
renderFunc,
function(value) {
json <- from_JSON_safe(value)
json <- from_JSON(value)
json$x <- json$x[setdiff(names(json$x), c("visdat", "cur_data", "attrs"))]
to_JSON(json)
}
Expand All @@ -55,38 +55,185 @@ renderPlotly <- function(expr, env = parent.frame(), quoted = FALSE) {

# Converts a plot, OR a promise of a plot, to plotly
prepareWidget <- function(x) {
if (promises::is.promising(x)) {
promises::then(x, ggplotly)
p <- if (promises::is.promising(x)) {
promises::then(x, plotly_build)
} else {
ggplotly(x)
plotly_build(x)
}
register_plot_events(p)
p
}

register_plot_events <- function(p) {
session <- getDefaultReactiveDomain()
eventIDs <- paste(p$x$shinyEvents, p$x$source, sep = "-")
session$userData$plotlyShinyEventIDs <- unique(c(
session$userData$plotlyShinyEventIDs,
eventIDs
))
}


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

event_data <- function(event = c("plotly_hover", "plotly_click", "plotly_selected",
"plotly_relayout"), source = "A",
session = shiny::getDefaultReactiveDomain()) {
event_data <- function(
event = c(
"plotly_hover", "plotly_unhover", "plotly_click", "plotly_doubleclick",
"plotly_selected", "plotly_selecting", "plotly_brushed", "plotly_brushing",
"plotly_deselect", "plotly_relayout", "plotly_restyle", "plotly_legendclick",
"plotly_legenddoubleclick", "plotly_clickannotation", "plotly_afterplot"
),
source = "A",
session = shiny::getDefaultReactiveDomain(),
priority = c("input", "event")
) {
if (is.null(session)) {
stop("No reactive domain detected. This function can only be called \n",
"from within a reactive shiny context.")
}
src <- sprintf(".clientValue-%s-%s", event[1], source)
val <- session$rootScope()$input[[src]]
if (is.null(val)) val else jsonlite::fromJSON(val)

event <- match.arg(event)
eventID <- paste(event, source, sep = "-")

# It's possible for event_data() to execute before any
# relevant input values have been registered (i.e, before
# relevant plotly graphs have been executed). Therefore,
# we delay checking that a relevant input value has been
# registered until shiny flushes
session$onFlushed(
function() {
eventIDRegistered <- eventID %in% session$userData$plotlyShinyEventIDs
if (!eventIDRegistered) {
warning(
"The '", event, "' event tied a source ID of '", source, "' ",
"is not registered. In order to obtain this event data, ",
"please add `event_register(p, '", event, "')` to the plot (`p`) ",
"that you wish to obtain event data from.",
call. = FALSE
)
}
}
)

# legend clicking returns trace(s), which shouldn't be simplified...
parseJSON <- if (event %in% c("plotly_legendclick", "plotly_legenddoubleclick")) {
from_JSON
} else {
function(x) jsonlite::parse_json(x, simplifyVector = TRUE)
}

# Handle NULL sensibly
parseJSONVal <- function(x) {
if (is.null(x)) x else parseJSON(x)
}

parsedInputValue <- function() {
parseJSONVal(session$rootScope()$input[[eventID]])
}

priority <- match.arg(priority)
if (priority == "event") {
# Shiny.setInputValue() is always called with event priority
# so simply return the parse input value
return(parsedInputValue())

} else {

eventHasStorage <- eventID %in% names(session$userData$plotlyInputStore)

if (!eventHasStorage) {
# store input value as a reactive value to leverage caching
session$userData$plotlyInputStore <- session$userData$plotlyInputStore %||% shiny::reactiveValues()
session$userData$plotlyInputStore[[eventID]] <- shiny::isolate(parsedInputValue())
shiny::observe({
session$userData$plotlyInputStore[[eventID]] <- parsedInputValue()
}, priority = 10000, domain = session)
}

session$userData$plotlyInputStore[[eventID]]
}

}


#' Register a shiny input value
#'
#' @inheritParams event_data
#' @seealso [event_data]
#' @export
#' @author Carson Sievert
event_register <- function(p, event = NULL) {
event <- match.arg(event, event_data_events())
shiny_event_add(p, event)
}

#' Un-register a shiny input value
#'
#' @inheritParams event_data
#' @seealso [event_data]
#' @export
#' @author Carson Sievert
event_unregister <- function(p, event = NULL) {
event <- match.arg(event, event_data_events())
shiny_event_remove(p, event)
}


# helpers
shiny_event_add <- function(p, event) {
p <- shiny_defaults_set(p)
p$x$shinyEvents <- unique(c(p$x$shinyEvents, event))
p
}

shiny_event_remove <- function(p, event) {
p <- shiny_defaults_set(p)
p$x$shinyEvents <- setdiff(p$x$shinyEvents, event)
p
}

shiny_defaults_set <- function(p) {
p$x$shinyEvents <- p$x$shinyEvents %||% shiny_event_defaults()
p
}

shiny_event_defaults <- function() {
c(
"plotly_hover",
"plotly_click",
"plotly_selected",
"plotly_relayout",
"plotly_brushed",
"plotly_brushing",
"plotly_clickannotation",
"plotly_doubleclick",
"plotly_deselect",
"plotly_afterplot"
)
}

event_data_events <- function() {
eval(formals(event_data)$event)
}
11 changes: 2 additions & 9 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ grab <- function(what = "username") {

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

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

# preferred defaults for toJSON mapping
from_JSON <- function(x, ...) {
jsonlite::fromJSON(x, simplifyDataFrame = FALSE, simplifyMatrix = FALSE, ...)
}

from_JSON_safe <- function(txt, ...) {
if (!jsonlite::validate(txt)) {
stop("Expected a valid JSON string.")
}
from_JSON(txt, ...)
jsonlite::parse_json(x, simplifyVector = TRUE, simplifyDataFrame = FALSE, simplifyMatrix = FALSE, ...)
}

i <- function(x) {
Expand Down
33 changes: 26 additions & 7 deletions inst/examples/shiny/event_data/app.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@ ui <- fluidPage(
plotlyOutput("plot"),
verbatimTextOutput("hover"),
verbatimTextOutput("click"),
verbatimTextOutput("brush")
verbatimTextOutput("selected"),
verbatimTextOutput("selecting"),
verbatimTextOutput("brushed"),
verbatimTextOutput("brushing")
)

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

nms <- row.names(mtcars)

output$plot <- renderPlotly({
if (identical(input$plotType, "ggplotly")) {
p <- ggplot(mtcars, aes(x = mpg, y = wt, customdata = nms)) + geom_point()
ggplotly(p) %>% layout(dragmode = "select")
p <- if (identical(input$plotType, "ggplotly")) {
ggplotly(ggplot(mtcars, aes(x = mpg, y = wt, customdata = nms)) + geom_point())
} else {
plot_ly(mtcars, x = ~mpg, y = ~wt, customdata = nms) %>%
layout(dragmode = "select")
plot_ly(mtcars, x = ~mpg, y = ~wt, customdata = nms)
}
p %>%
layout(dragmode = "select") %>%
event_register("plotly_selecting")
})

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

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

output$selecting <- renderPrint({
d <- event_data("plotly_selecting")
if (is.null(d)) "Click and drag events (i.e., select/lasso) appear here (double-click to clear)" else d
})

output$brush <- renderPrint({
d <- event_data("plotly_brushed")
if (is.null(d)) "Extents of the selection brush will appear here." else d
})

output$brushing <- renderPrint({
d <- event_data("plotly_brushing")
if (is.null(d)) "Extents of the selection brush will appear here." else d
})

}

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