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 28 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
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@ Suggests:
plotlyGeoAssets,
forcats
LazyData: true
RoxygenNote: 6.1.0
RoxygenNote: 6.1.1
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
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
59 changes: 50 additions & 9 deletions R/shiny.R
Original file line number Diff line number Diff line change
Expand Up @@ -56,26 +56,67 @@ prepareWidget <- function(x) {
#'
#' 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 relevant shiny input. 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
#' @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)

# make sure the input event is sensible
event <- match.arg(event)
priority <- match.arg(priority)

isNullEvent <- event %in% c("plotly_doubleclick", "plotly_deselect", "plotly_afterplot")
if (isNullEvent && priority != "event") {
message(
"The input value tied to a '", event, "' event never changes. ",
"Setting `priority = 'event'` so that the event actually triggers re-execution."
)
priority <- "event"
}

# register event on client-side
session$onFlushed(function() {
session$sendCustomMessage(
type = "plotlyEventData",
message = list(event = event, source = source, priority = priority)
)
}, once = FALSE)


src <- sprintf(".clientValue-%s-%s-%s", event, source, priority)
val <- session$rootScope()$input[[src]]
if (is.null(val)) val else jsonlite::fromJSON(val)

# legend clicking returns trace(s), which shouldn't be simplified...
fromJSONfunc <- if (event %in% c("plotly_legendclick", "plotly_legenddoubleclick")) from_JSON else jsonlite::fromJSON
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsonlite::fromJSON needs to become jsonlite::parseJSON (when that gets released)


if (is.null(val)) val else fromJSONfunc(val)
}
22 changes: 20 additions & 2 deletions inst/examples/shiny/event_data/app.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ ui <- fluidPage(
plotlyOutput("plot"),
verbatimTextOutput("hover"),
verbatimTextOutput("click"),
verbatimTextOutput("brush")
verbatimTextOutput("selected"),
verbatimTextOutput("selecting"),
verbatimTextOutput("brushed"),
verbatimTextOutput("brushing")
)

server <- function(input, output, session) {
Expand All @@ -33,11 +36,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"))
26 changes: 26 additions & 0 deletions inst/examples/shiny/event_data_annotation/app.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
library(shiny)

ui <- fluidPage(
plotlyOutput("p"),
checkboxInput("edit", "Enable edit mode? Capturing annotation click events in edit mode is not possible.", FALSE),
verbatimTextOutput("data")
)

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

output$p <- renderPlotly({
plot_ly(mtcars) %>%
add_annotations(x = ~wt, y = ~mpg, text = row.names(mtcars), captureevents = TRUE)
})

observeEvent(input$edit, {
plotlyProxy("p", session) %>%
plotlyProxyInvoke("reconfig", editable = input$edit)
})

output$data <- renderPrint({
event_data("plotly_clickannotation")
})
}

shinyApp(ui, server)
26 changes: 26 additions & 0 deletions inst/examples/shiny/event_data_legends/app.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
library(shiny)

ui <- fluidPage(
plotlyOutput("gg"),
verbatimTextOutput("click"),
verbatimTextOutput("doubleclick")
)

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

output$gg <- renderPlotly({
ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
geom_point() +
facet_wrap(~vs)
})

output$click <- renderPrint({
event_data("plotly_legendclick")
})

output$doubleclick <- renderPrint({
event_data("plotly_legenddoubleclick")
})
}

shinyApp(ui, server)
67 changes: 67 additions & 0 deletions inst/examples/shiny/event_data_parcoords/app.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
library(plotly)
library(shiny)

ui <- fluidPage(
plotlyOutput("parcoords"),
tableOutput("data")
)

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

iris_numeric <- dplyr::select_if(iris, is.numeric)

output$parcoords <- renderPlotly({
dims <- Map(function(x, y) {
list(values = x, range = range(x), label = y)
}, iris_numeric, names(iris_numeric), USE.NAMES = FALSE)
plot_ly(type = 'parcoords', dimensions = dims, source = "pcoords")
})

# maintain a collection of selection ranges
# since each parcoord dimension is allowed to have multiple
# selected ranges, this reactive values data structure is
# allowed
# list(
# var1 = list(c(min1, max1), c(min2, max2), ...),
# var2 = list(c(min1, max1)),
# ...
# )
ranges <- reactiveValues()
observeEvent(event_data("plotly_restyle", source = "pcoords"), {
d <- event_data("plotly_restyle", source = "pcoords")
# what is the relevant dimension (i.e. variable)?
dimension <- as.numeric(stringr::str_extract(names(d[[1]]), "[0-9]+"))
# careful of the indexing in JS (0) versus R (1)!
dimension_name <- names(iris_numeric)[[dimension + 1]]
# a given dimension can have multiple selected ranges
# these will come in as 3D arrays, but a list of vectors
# is nicer to work with
info <- d[[1]][[1]]
ranges[[dimension_name]] <- if (length(dim(info)) == 3) {
lapply(seq_len(dim(info)[2]), function(i) info[,i,])
} else {
list(as.numeric(info))
}
})

# filter the dataset down to the rows that match the selection ranges
iris_selected <- reactive({
keep <- TRUE
for (i in names(ranges)) {
range_ <- ranges[[i]]
keep_var <- FALSE
for (j in seq_along(range_)) {
rng <- range_[[j]]
keep_var <- keep_var | dplyr::between(iris[[i]], min(rng), max(rng))
}
keep <- keep & keep_var
}
iris[keep, ]
})

output$data <- renderTable({
iris_selected()
})
}

shinyApp(ui, server)
29 changes: 29 additions & 0 deletions inst/examples/shiny/event_priority/app.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
library(plotly)
library(shiny)

ui <- fluidPage(
checkboxInput("priority", "Shiny event priority", FALSE),
plotlyOutput("p")
)

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

output$p <- renderPlotly({
title <- if (input$priority) {
"Clicking on the same point repeatedly will keep triggering console output"
} else {
"Clicking on the same point won't trigger more output"
}

plot_ly(mtcars, x = ~wt, y = ~mpg) %>%
layout(title = title) %>%
config(priority = if (input$priority) "event")
})

observeEvent(event_data("plotly_click"), {
print("clicked!")
})

}

shinyApp(ui, server)
Loading