Skip to content

Commit f62c50e

Browse files
committed
Track plot events in user's session data to throw informative errors
Also introduces new R functions to explictly register/un-register events
1 parent c52909a commit f62c50e

File tree

16 files changed

+244
-77
lines changed

16 files changed

+244
-77
lines changed

NAMESPACE

Lines changed: 2 additions & 0 deletions
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_)

R/layout.R

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,6 @@ rangeslider <- function(p, start = NULL, end = NULL, ...) {
101101
#' (see [here](https://github.com/ropensci/plotly/blob/master/inst/examples/rmd/MathJax/index.Rmd)
102102
#' for an **rmarkdown** example and
103103
#' [here](https://github.com/ropensci/plotly/blob/master/inst/examples/rmd/MathJax/index.Rmd) for a **shiny** example).
104-
#' @param shinyInputs plotly.js events to register as shiny input values
105-
#' @param shinyEvents plotly.js events to register as shiny input values with event priority
106104
#' @author Carson Sievert
107105
#' @export
108106
#' @examples
@@ -133,9 +131,7 @@ rangeslider <- function(p, start = NULL, end = NULL, ...) {
133131
#' config(p, locale = "zh-CN")
134132
#'
135133

136-
config <- function(p, ..., collaborate = TRUE, cloud = FALSE, locale = NULL, mathjax = NULL,
137-
shinyInputs = c("plotly_hover", "plotly_click", "plotly_selected", "plotly_relayout"),
138-
shinyEvents = c("plotly_doubleclick", "plotly_deselect", "plotly_afterplot")) {
134+
config <- function(p, ..., collaborate = TRUE, cloud = FALSE, locale = NULL, mathjax = NULL) {
139135

140136
if (!is.null(locale)) {
141137
p$dependencies <- c(
@@ -174,8 +170,6 @@ config <- function(p, ..., collaborate = TRUE, cloud = FALSE, locale = NULL, mat
174170
}
175171

176172
p$x$config$cloud <- cloud
177-
p$x$config$shinyInputs <- I(validate_event_names(shinyInputs))
178-
p$x$config$shinyEvents <- I(validate_event_names(shinyEvents))
179173

180174
p
181175
}
@@ -192,12 +186,3 @@ validate_event_names <- function(events) {
192186
call. = FALSE
193187
)
194188
}
195-
196-
shiny_input_events <- function() {
197-
c(
198-
"plotly_hover", "plotly_unhover", "plotly_click", "plotly_doubleclick",
199-
"plotly_selected", "plotly_selecting", "plotly_brushed", "plotly_brushing",
200-
"plotly_deselect", "plotly_relayout", "plotly_restyle", "plotly_legendclick",
201-
"plotly_legenddoubleclick", "plotly_clickannotation", "plotly_afterplot"
202-
)
203-
}

R/plotly_build.R

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

369+
# set the default plotly.js events to register in shiny
370+
p <- shiny_defaults_set(p)
371+
369372
p <- verify_guides(p)
370373

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

R/shiny.R

Lines changed: 124 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,27 @@ 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+
inputIDs <- paste(p$x$shinyInputs, p$x$source, sep = "-")
71+
session$userData$plotlyShinyEventIDs <- unique(c(
72+
session$userData$plotlyShinyEventIDs,
73+
eventIDs
74+
))
75+
session$userData$plotlyShinyInputIDs <- unique(c(
76+
session$userData$plotlyShinyInputIDs,
77+
inputIDs
78+
))
6379
}
6480

6581

@@ -74,6 +90,7 @@ prepareWidget <- function(x) {
7490
#' events emitted from that specific plot.
7591
#' @param session a shiny session object (the default should almost always be used).
7692
#' @export
93+
#' @seealso [event_register], [event_unregister]
7794
#' @references
7895
#' * <https://plotly-book.cpsievert.me/shiny-plotly-inputs.html>
7996
#' * <https://plot.ly/javascript/plotlyjs-function-reference/>
@@ -90,20 +107,118 @@ event_data <- function(
90107
"plotly_legenddoubleclick", "plotly_clickannotation", "plotly_afterplot"
91108
),
92109
source = "A",
93-
session = shiny::getDefaultReactiveDomain()
110+
session = shiny::getDefaultReactiveDomain(),
111+
priority = c("input", "event")
94112
) {
95113
if (is.null(session)) {
96114
stop("No reactive domain detected. This function can only be called \n",
97115
"from within a reactive shiny context.")
98116
}
99117

100-
# make sure the input event is sensible
101118
event <- match.arg(event)
102-
src <- sprintf(".clientValue-%s-%s", event, source)
103-
val <- session$rootScope()$input[[src]]
119+
# TODO: resolve priority
120+
priority <- match.arg(priority)
121+
# check to see if this event-source-priority combo is registered
122+
eventID <- paste(event, source, sep = "-")
123+
keyName <- if (priority == "event") "plotlyShinyEventIDs" else "plotlyShinyInputIDs"
124+
if (!eventID %in% session$userData[[keyName]]) {
125+
stop(
126+
"The '", event, "' event has not been registered for a source ID ",
127+
"of '", source, "' with priority '", priority, "'. ",
128+
"Please add `event_register(p, '", event, "', '", priority, "')` to your plot `p`."
129+
)
130+
}
131+
132+
inputName <- sprintf(".clientValue-%s-%s", event, source)
133+
val <- session$rootScope()$input[[inputName]]
104134

105135
# legend clicking returns trace(s), which shouldn't be simplified...
106-
fromJSONfunc <- if (event %in% c("plotly_legendclick", "plotly_legenddoubleclick")) from_JSON else jsonlite::parse_json
136+
parseJSON <- if (event %in% c("plotly_legendclick", "plotly_legenddoubleclick")) {
137+
from_JSON
138+
} else {
139+
function(x) jsonlite::parse_json(x, simplifyVector = TRUE)
140+
}
107141

108-
if (is.null(val)) val else fromJSONfunc(val)
142+
if (is.null(val)) val else parseJSON(val)
143+
}
144+
145+
146+
#' Register a shiny input value
147+
#'
148+
#' @inheritParams event_data
149+
#' @seealso [event_data]
150+
#' @export
151+
#' @author Carson Sievert
152+
event_register <- function(p, event = NULL, priority = c("input", "event")) {
153+
priority <- match.arg(priority)
154+
event <- match.arg(event, event_data_events())
155+
if (priority == "event") shiny_event_add(p, event) else shiny_input_add(p, event)
156+
}
157+
158+
#' Un-register a shiny input value
159+
#'
160+
#' @inheritParams event_data
161+
#' @seealso [event_data]
162+
#' @export
163+
#' @author Carson Sievert
164+
event_unregister <- function(p, event = NULL, priority = c("input", "event")) {
165+
priority <- match.arg(priority)
166+
event <- match.arg(event, event_data_events())
167+
if (priority == "event") shiny_event_remove(p, event) else shiny_input_remove(p, event)
168+
}
169+
170+
171+
# helpers
172+
shiny_event_add <- function(p, event) {
173+
p <- shiny_defaults_set(p)
174+
p$x$shinyEvents <- unique(c(p$x$shinyEvents, event))
175+
p
176+
}
177+
178+
shiny_input_add <- function(p, event) {
179+
p <- shiny_defaults_set(p)
180+
p$x$shinyInputs <- unique(c(p$x$shinyInputs, event))
181+
p
182+
}
183+
184+
shiny_event_remove <- function(p, event) {
185+
p <- shiny_defaults_set(p)
186+
p$x$shinyEvents <- setdiff(p$x$shinyEvents, event)
187+
p
188+
}
189+
190+
shiny_input_remove <- function(p, event) {
191+
p <- shiny_defaults_set(p)
192+
p$x$shinyInputs <- setdiff(p$x$shinyInputs, event)
193+
p
194+
}
195+
196+
shiny_defaults_set <- function(p) {
197+
p$x$shinyEvents <- p$x$shinyEvents %||% shiny_event_defaults()
198+
p$x$shinyInputs <- p$x$shinyInputs %||% shiny_input_defaults()
199+
p
200+
}
201+
202+
shiny_input_defaults <- function() {
203+
c(
204+
"plotly_hover",
205+
"plotly_click",
206+
"plotly_selected",
207+
"plotly_relayout",
208+
"plotly_brushed",
209+
"plotly_brushing",
210+
"plotly_clickannotation"
211+
)
212+
}
213+
214+
shiny_event_defaults <- function() {
215+
c(
216+
"plotly_doubleclick",
217+
"plotly_deselect",
218+
"plotly_afterplot"
219+
)
220+
}
221+
222+
event_data_events <- function() {
223+
eval(formals(event_data)$event)
109224
}

R/utils.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -959,7 +959,7 @@ to_JSON <- function(x, ...) {
959959

960960
# preferred defaults for toJSON mapping
961961
from_JSON <- function(x, ...) {
962-
jsonlite::parse_json(x, simplifyDataFrame = FALSE, simplifyMatrix = FALSE, ...)
962+
jsonlite::parse_json(x, simplifyVector = TRUE, simplifyDataFrame = FALSE, simplifyMatrix = FALSE, ...)
963963
}
964964

965965
i <- function(x) {

inst/examples/shiny/event_data/app.R

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,8 @@ server <- function(input, output, session) {
2525
plot_ly(mtcars, x = ~mpg, y = ~wt, key = nms)
2626
}
2727
p %>%
28-
layout(dragmode = "select") %>%
29-
config(
30-
shinyInputs = c(
31-
"plotly_hover",
32-
"plotly_hover",
33-
"plotly_click",
34-
"plotly_selected",
35-
"plotly_selecting",
36-
"plotly_brushed",
37-
"plotly_brushing"
38-
)
39-
)
28+
layout(dragmode = "select") %>%
29+
event_register("plotly_selecting")
4030
})
4131

4232
output$hover <- renderPrint({

inst/examples/shiny/event_data_annotation/app.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ server <- function(input, output, session) {
1010

1111
output$p <- renderPlotly({
1212
plot_ly(mtcars) %>%
13-
add_annotations(x = ~wt, y = ~mpg, text = row.names(mtcars), captureevents = TRUE) %>%
14-
config(shinyInputs = "plotly_clickannotation")
13+
add_annotations(x = ~wt, y = ~mpg, text = row.names(mtcars), captureevents = TRUE)
1514
})
1615

1716
observeEvent(input$edit, {

inst/examples/shiny/event_data_legends/app.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ ui <- fluidPage(
99
server <- function(input, output, session) {
1010

1111
output$gg <- renderPlotly({
12-
ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
12+
p <- ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
1313
geom_point() +
1414
facet_wrap(~vs)
15+
ggplotly(p) %>%
16+
event_register("plotly_legendclick") %>%
17+
event_register("plotly_legenddoubleclick")
1518
})
1619

1720
output$click <- renderPrint({

inst/examples/shiny/event_data_parcoords/app.R

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ server <- function(input, output, session) {
1515
list(values = x, range = range(x), label = y)
1616
}, iris_numeric, names(iris_numeric), USE.NAMES = FALSE)
1717
plot_ly(type = 'parcoords', dimensions = dims, source = "pcoords") %>%
18-
layout(margin = list(r = 30))
18+
layout(margin = list(r = 30)) %>%
19+
config(shinyInputs = "plotly_restyle")
1920
})
2021

2122
# maintain a collection of selection ranges
@@ -39,32 +40,37 @@ server <- function(input, output, session) {
3940
# a given dimension can have multiple selected ranges
4041
# these will come in as 3D arrays, but a list of vectors
4142
# is nicer to work with
42-
info <- d[[1]][[1]]
43-
ranges[[dimension_name]] <- if (length(dim(info)) == 3) {
44-
lapply(seq_len(dim(info)[2]), function(i) info[,i,])
45-
} else {
46-
list(as.numeric(info))
47-
}
43+
browser()
44+
ranges[[dimension_name]] <- d[[1]][[1]]
45+
46+
#print(info)
47+
#browser()
48+
#ranges[[dimension_name]] <- if (length(dim(info)) == 3) {
49+
# lapply(seq_len(dim(info)[2]), function(i) info[,i,])
50+
#} else {
51+
# list(as.numeric(info))
52+
#}
4853
})
4954

50-
# filter the dataset down to the rows that match the selection ranges
51-
iris_selected <- reactive({
52-
keep <- TRUE
53-
for (i in names(ranges)) {
54-
range_ <- ranges[[i]]
55-
keep_var <- FALSE
56-
for (j in seq_along(range_)) {
57-
rng <- range_[[j]]
58-
keep_var <- keep_var | dplyr::between(iris[[i]], min(rng), max(rng))
59-
}
60-
keep <- keep & keep_var
61-
}
62-
iris[keep, ]
63-
})
64-
65-
output$data <- renderPrint({
66-
tibble::as_tibble(iris_selected())
67-
})
55+
## filter the dataset down to the rows that match the selection ranges
56+
#iris_selected <- reactive({
57+
# keep <- TRUE
58+
# print(ranges)
59+
# for (i in names(ranges)) {
60+
# range_ <- ranges[[i]]
61+
# keep_var <- FALSE
62+
# for (j in seq_along(range_)) {
63+
# rng <- range_[[j]]
64+
# keep_var <- keep_var | dplyr::between(iris[[i]], min(rng), max(rng))
65+
# }
66+
# keep <- keep & keep_var
67+
# }
68+
# iris[keep, ]
69+
#})
70+
#
71+
#output$data <- renderPrint({
72+
# tibble::as_tibble(iris_selected())
73+
#})
6874
}
6975

7076
shinyApp(ui, server)

inst/examples/shiny/proxy_relayout/app.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ server <- function(input, output, session) {
1717

1818
observeEvent(event_data("plotly_relayout"), {
1919
d <- event_data("plotly_relayout")
20+
# unfortunately, the data structure emitted is different depending on
21+
# whether the relayout is triggered from the rangeslider or the plot
2022
xmin <- if (length(d[["xaxis.range[0]"]])) d[["xaxis.range[0]"]] else d[["xaxis.range"]][1]
2123
xmax <- if (length(d[["xaxis.range[1]"]])) d[["xaxis.range[1]"]] else d[["xaxis.range"]][2]
2224
if (is.null(xmin) || is.null(xmax)) return(NULL)

inst/htmlwidgets/plotly.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,11 +352,11 @@ HTMLWidgets.widget({
352352
});
353353
}
354354

355-
var shinyInputs = x.config.shinyInputs || [];
355+
var shinyInputs = x.shinyInputs || [];
356356
shinyInputs.map(function(input) {
357357
return registerShinyValue(input, false);
358358
});
359-
var shinyEvents = x.config.shinyEvents || [];
359+
var shinyEvents = x.shinyEvents || [];
360360
shinyEvents.map(function(event) {
361361
return registerShinyValue(event, true);
362362
});

man/config.Rd

Lines changed: 1 addition & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)