From 379e482a8b13f794cf824169fca8798ef4941cc5 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 2 Nov 2018 10:40:26 -0500 Subject: [PATCH 01/54] add restyle event, addresses #1321 --- inst/htmlwidgets/plotly.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index 0f7b8d6b3e..9574c481b9 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -274,6 +274,12 @@ HTMLWidgets.widget({ JSON.stringify(d) ); }); + graphDiv.on('plotly_restyle', function(d) { + Shiny.onInputChange( + ".clientValue-plotly_restyle-" + x.source, + JSON.stringify(d) + ); + }); graphDiv.on('plotly_hover', function(d) { Shiny.onInputChange( ".clientValue-plotly_hover-" + x.source, From 06de984992bf81da7fbc7af5833aba2056c18490 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 8 Nov 2018 12:28:58 -0600 Subject: [PATCH 02/54] add an example of mapping parcoord ranges back to the observations of interest --- .../examples/shiny/event_data_parcoords/app.R | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 inst/examples/shiny/event_data_parcoords/app.R diff --git a/inst/examples/shiny/event_data_parcoords/app.R b/inst/examples/shiny/event_data_parcoords/app.R new file mode 100644 index 0000000000..5afecb0d1f --- /dev/null +++ b/inst/examples/shiny/event_data_parcoords/app.R @@ -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) From 39553e694f593f72a7b5480be2ecfd0ea0e557f3 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 19 Nov 2018 20:22:13 -0600 Subject: [PATCH 03/54] New shiny input event 'plotly_brush' returns the x/y range of rectangular brush or convex hull of lasso brush --- inst/examples/shiny/event_data/app.R | 8 +++++++- inst/htmlwidgets/plotly.js | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/inst/examples/shiny/event_data/app.R b/inst/examples/shiny/event_data/app.R index 5127f02484..fbe4108ef5 100644 --- a/inst/examples/shiny/event_data/app.R +++ b/inst/examples/shiny/event_data/app.R @@ -6,7 +6,8 @@ ui <- fluidPage( plotlyOutput("plot"), verbatimTextOutput("hover"), verbatimTextOutput("click"), - verbatimTextOutput("brush") + verbatimTextOutput("brush"), + verbatimTextOutput("brush_limits") ) server <- function(input, output, session) { @@ -38,6 +39,11 @@ server <- function(input, output, session) { if (is.null(d)) "Click and drag events (i.e., select/lasso) appear here (double-click to clear)" else d }) + output$brush_limits <- renderPrint({ + d <- event_data("plotly_brush") + if (is.null(d)) "Extents of the selection brush will appear here." else d + }) + } shinyApp(ui, server, options = list(display.mode = "showcase")) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index 6344bdedc8..8b4ca264f2 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -309,6 +309,11 @@ HTMLWidgets.widget({ ".clientValue-plotly_selected-" + x.source, JSON.stringify(eventDataWithKey(d)) ); + var limits = d.range ? d.range : d.lassoPoints; + Shiny.onInputChange( + ".clientValue-plotly_brush-" + x.source, + JSON.stringify(limits) + ); } }); graphDiv.on('plotly_unhover', function(eventData) { From eb5a093439409c6bec2ea3033e1130d56b68581f Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 26 Nov 2018 18:47:01 -0600 Subject: [PATCH 04/54] add plotly_selecting and plotly_brushing --- inst/examples/shiny/event_data/app.R | 18 +++++++++++++++--- inst/htmlwidgets/plotly.js | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/inst/examples/shiny/event_data/app.R b/inst/examples/shiny/event_data/app.R index fbe4108ef5..2cc478c3a9 100644 --- a/inst/examples/shiny/event_data/app.R +++ b/inst/examples/shiny/event_data/app.R @@ -6,8 +6,10 @@ ui <- fluidPage( plotlyOutput("plot"), verbatimTextOutput("hover"), verbatimTextOutput("click"), + verbatimTextOutput("selected"), + verbatimTextOutput("selecting"), verbatimTextOutput("brush"), - verbatimTextOutput("brush_limits") + verbatimTextOutput("brushing") ) server <- function(input, output, session) { @@ -34,16 +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$brush_limits <- renderPrint({ + 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_brush") 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")) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index 8b4ca264f2..045c85d54b 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -316,6 +316,19 @@ HTMLWidgets.widget({ ); } }); + graphDiv.on('plotly_selecting', function(d) { + if (d) { + Shiny.onInputChange( + ".clientValue-plotly_selecting-" + x.source, + JSON.stringify(eventDataWithKey(d)) + ); + var limits = d.range ? d.range : d.lassoPoints; + Shiny.onInputChange( + ".clientValue-plotly_brushing-" + x.source, + JSON.stringify(limits) + ); + } + }); graphDiv.on('plotly_unhover', function(eventData) { Shiny.onInputChange(".clientValue-plotly_hover-" + x.source, null); }); @@ -325,6 +338,9 @@ HTMLWidgets.widget({ // 'plotly_deselect' is code for doubleclick when in select mode graphDiv.on('plotly_deselect', function(eventData) { Shiny.onInputChange(".clientValue-plotly_selected-" + x.source, null); + Shiny.onInputChange(".clientValue-plotly_selecting-" + x.source, null); + Shiny.onInputChange(".clientValue-plotly_brush-" + x.source, null); + Shiny.onInputChange(".clientValue-plotly_brushing-" + x.source, null); Shiny.onInputChange(".clientValue-plotly_click-" + x.source, null); }); } From f949d7ca7ba30a52592a32a42e466f53bcb79684 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 30 Nov 2018 13:58:19 -0600 Subject: [PATCH 05/54] use Shiny.setInputValue() over Shiny.onInputChange() --- inst/htmlwidgets/plotly.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index 829f79a25b..a22e62d61a 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -274,25 +274,25 @@ HTMLWidgets.widget({ if (HTMLWidgets.shinyMode) { // https://plot.ly/javascript/zoom-events/ graphDiv.on('plotly_relayout', function(d) { - Shiny.onInputChange( + Shiny.setInputValue( ".clientValue-plotly_relayout-" + x.source, JSON.stringify(d) ); }); graphDiv.on('plotly_restyle', function(d) { - Shiny.onInputChange( + Shiny.setInputValue( ".clientValue-plotly_restyle-" + x.source, JSON.stringify(d) ); }); graphDiv.on('plotly_hover', function(d) { - Shiny.onInputChange( + Shiny.setInputValue( ".clientValue-plotly_hover-" + x.source, JSON.stringify(eventDataWithKey(d)) ); }); graphDiv.on('plotly_click', function(d) { - Shiny.onInputChange( + Shiny.setInputValue( ".clientValue-plotly_click-" + x.source, JSON.stringify(eventDataWithKey(d)) ); @@ -305,12 +305,12 @@ HTMLWidgets.widget({ // even in the empty selection case, `d` is truthy (an object), // and the 'plotly_deselect' event will reset this input if (d) { - Shiny.onInputChange( + Shiny.setInputValue( ".clientValue-plotly_selected-" + x.source, JSON.stringify(eventDataWithKey(d)) ); var limits = d.range ? d.range : d.lassoPoints; - Shiny.onInputChange( + Shiny.setInputValue( ".clientValue-plotly_brush-" + x.source, JSON.stringify(limits) ); @@ -318,30 +318,30 @@ HTMLWidgets.widget({ }); graphDiv.on('plotly_selecting', function(d) { if (d) { - Shiny.onInputChange( + Shiny.setInputValue( ".clientValue-plotly_selecting-" + x.source, JSON.stringify(eventDataWithKey(d)) ); var limits = d.range ? d.range : d.lassoPoints; - Shiny.onInputChange( + Shiny.setInputValue( ".clientValue-plotly_brushing-" + x.source, JSON.stringify(limits) ); } }); graphDiv.on('plotly_unhover', function(eventData) { - Shiny.onInputChange(".clientValue-plotly_hover-" + x.source, null); + Shiny.setInputValue(".clientValue-plotly_hover-" + x.source, null); }); graphDiv.on('plotly_doubleclick', function(eventData) { - Shiny.onInputChange(".clientValue-plotly_click-" + x.source, null); + Shiny.setInputValue(".clientValue-plotly_click-" + x.source, null); }); // 'plotly_deselect' is code for doubleclick when in select mode graphDiv.on('plotly_deselect', function(eventData) { - Shiny.onInputChange(".clientValue-plotly_selected-" + x.source, null); - Shiny.onInputChange(".clientValue-plotly_selecting-" + x.source, null); - Shiny.onInputChange(".clientValue-plotly_brush-" + x.source, null); - Shiny.onInputChange(".clientValue-plotly_brushing-" + x.source, null); - Shiny.onInputChange(".clientValue-plotly_click-" + x.source, null); + Shiny.setInputValue(".clientValue-plotly_selected-" + x.source, null); + Shiny.setInputValue(".clientValue-plotly_selecting-" + x.source, null); + Shiny.setInputValue(".clientValue-plotly_brush-" + x.source, null); + Shiny.setInputValue(".clientValue-plotly_brushing-" + x.source, null); + Shiny.setInputValue(".clientValue-plotly_click-" + x.source, null); }); } From 849f5f11c1a5f8e249d44d480d0e754b30443a91 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 30 Nov 2018 16:22:19 -0600 Subject: [PATCH 06/54] Relay full annotation info on 'plotly_clickannotation' event --- inst/htmlwidgets/plotly.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index a22e62d61a..670cb272b3 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -343,6 +343,11 @@ HTMLWidgets.widget({ Shiny.setInputValue(".clientValue-plotly_brushing-" + x.source, null); Shiny.setInputValue(".clientValue-plotly_click-" + x.source, null); }); + + graphDiv.on('plotly_clickannotation', function(d) { + Shiny.setInputValue(".clientValue-plotly_clickannotation-" + x.source, JSON.stringify(d.fullAnnotation)); + }); + } // Given an array of {curveNumber: x, pointNumber: y} objects, From 5514bfe10d88051c932561af0786d0d46378a7fc Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 30 Nov 2018 16:23:03 -0600 Subject: [PATCH 07/54] Relay 'plotly_afterplot' event --- inst/htmlwidgets/plotly.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index 670cb272b3..e6e04a53e3 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -348,6 +348,10 @@ HTMLWidgets.widget({ Shiny.setInputValue(".clientValue-plotly_clickannotation-" + x.source, JSON.stringify(d.fullAnnotation)); }); + graphDiv.on('plotly_afterplot', function() { + Shiny.setInputValue(".clientValue-plotly_afterplot-" + x.source, "afterplot", {priority: "event"}); + }); + } // Given an array of {curveNumber: x, pointNumber: y} objects, From 5a54e414aefa10f89930ba63651babd230daf23e Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 30 Nov 2018 16:23:43 -0600 Subject: [PATCH 08/54] Relay traces tied to legend events --- inst/htmlwidgets/plotly.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index e6e04a53e3..95fae36a88 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -352,6 +352,29 @@ HTMLWidgets.widget({ Shiny.setInputValue(".clientValue-plotly_afterplot-" + x.source, "afterplot", {priority: "event"}); }); + var legendEventData = function(d) { + // if legendgroup is not relevant just return the trace + var trace = d.data[d.curveNumber]; + if (!trace.legendgroup) { + Shiny.setInputValue(".clientValue-plotly_legendclick-" + x.source, trace); + return; + } + + // if legendgroup was specified, return all traces that match the group + var legendgrps = d.data.map(function(trace){ return trace.legendgroup; }); + var traces = []; + for (i = 0; i < legendgrps.length; i++) { + if (legendgrps[i] == trace.legendgroup) { + traces.push(d.data[i]); + } + } + + Shiny.setInputValue(".clientValue-plotly_legendclick-" + x.source, traces); + }; + + graphDiv.on('plotly_legendclick', legendEventData); + graphDiv.on('plotly_legenddoubleclick', legendEventData); + } // Given an array of {curveNumber: x, pointNumber: y} objects, From f23838f080527fe48bd93fd7c4774583885bd013 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 30 Nov 2018 18:32:44 -0600 Subject: [PATCH 09/54] Allow shiny input priority to be configurable at the plot-level, closes #1043 --- R/layout.R | 6 +++- R/proxy.R | 3 +- inst/examples/shiny/event_priority/app.R | 29 ++++++++++++++++ inst/htmlwidgets/plotly.js | 43 +++++++++++++++--------- 4 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 inst/examples/shiny/event_priority/app.R diff --git a/R/layout.R b/R/layout.R index 09f9c9d4aa..694b8cdc5d 100644 --- a/R/layout.R +++ b/R/layout.R @@ -101,6 +101,9 @@ rangeslider <- function(p, start = NULL, end = NULL, ...) { #' (see [here](https://github.com/ropensci/plotly/blob/master/inst/examples/rmd/MathJax/index.Rmd) #' for an **rmarkdown** example and #' [here](https://github.com/ropensci/plotly/blob/master/inst/examples/rmd/MathJax/index.Rmd) for a **shiny** example). +#' @param priority the priority of shiny input events. If `NULL` (the default), then +#' [event_data()] becomes invalidated only when the input value changes. If `"event"`, +#' then the event will always fire, regardless of whether the value has changed. #' @author Carson Sievert #' @export #' @examples @@ -131,7 +134,7 @@ rangeslider <- function(p, start = NULL, end = NULL, ...) { #' config(p, locale = "zh-CN") #' -config <- function(p, ..., collaborate = TRUE, cloud = FALSE, locale = NULL, mathjax = NULL) { +config <- function(p, ..., collaborate = TRUE, cloud = FALSE, locale = NULL, mathjax = NULL, priority = NULL) { if (!is.null(locale)) { p$dependencies <- c( @@ -170,6 +173,7 @@ config <- function(p, ..., collaborate = TRUE, cloud = FALSE, locale = NULL, mat } p$x$config$cloud <- cloud + p$x$config$priority <- priority p } diff --git a/R/proxy.R b/R/proxy.R index 8f34339da3..eb204b34ba 100644 --- a/R/proxy.R +++ b/R/proxy.R @@ -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" ) } diff --git a/inst/examples/shiny/event_priority/app.R b/inst/examples/shiny/event_priority/app.R new file mode 100644 index 0000000000..f711d76928 --- /dev/null +++ b/inst/examples/shiny/event_priority/app.R @@ -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) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index 95fae36a88..7f70a39886 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -272,29 +272,35 @@ HTMLWidgets.widget({ // send user input event data to shiny if (HTMLWidgets.shinyMode) { + var priority = x.config.priority ? {priority: x.config.priority} : undefined; + // https://plot.ly/javascript/zoom-events/ graphDiv.on('plotly_relayout', function(d) { Shiny.setInputValue( ".clientValue-plotly_relayout-" + x.source, - JSON.stringify(d) + JSON.stringify(d), + priority ); }); graphDiv.on('plotly_restyle', function(d) { Shiny.setInputValue( ".clientValue-plotly_restyle-" + x.source, - JSON.stringify(d) + JSON.stringify(d), + priority ); }); graphDiv.on('plotly_hover', function(d) { Shiny.setInputValue( ".clientValue-plotly_hover-" + x.source, - JSON.stringify(eventDataWithKey(d)) + JSON.stringify(eventDataWithKey(d)), + priority ); }); graphDiv.on('plotly_click', function(d) { Shiny.setInputValue( ".clientValue-plotly_click-" + x.source, - JSON.stringify(eventDataWithKey(d)) + JSON.stringify(eventDataWithKey(d)), + priority ); }); graphDiv.on('plotly_selected', function(d) { @@ -307,12 +313,14 @@ HTMLWidgets.widget({ if (d) { Shiny.setInputValue( ".clientValue-plotly_selected-" + x.source, - JSON.stringify(eventDataWithKey(d)) + JSON.stringify(eventDataWithKey(d)), + priority ); var limits = d.range ? d.range : d.lassoPoints; Shiny.setInputValue( ".clientValue-plotly_brush-" + x.source, - JSON.stringify(limits) + JSON.stringify(limits), + priority ); } }); @@ -320,34 +328,37 @@ HTMLWidgets.widget({ if (d) { Shiny.setInputValue( ".clientValue-plotly_selecting-" + x.source, - JSON.stringify(eventDataWithKey(d)) + JSON.stringify(eventDataWithKey(d)), + priority ); var limits = d.range ? d.range : d.lassoPoints; Shiny.setInputValue( ".clientValue-plotly_brushing-" + x.source, - JSON.stringify(limits) + JSON.stringify(limits), + priority ); } }); graphDiv.on('plotly_unhover', function(eventData) { - Shiny.setInputValue(".clientValue-plotly_hover-" + x.source, null); + Shiny.setInputValue(".clientValue-plotly_hover-" + x.source, null, priority); }); graphDiv.on('plotly_doubleclick', function(eventData) { - Shiny.setInputValue(".clientValue-plotly_click-" + x.source, null); + Shiny.setInputValue(".clientValue-plotly_click-" + x.source, null, priority); }); // 'plotly_deselect' is code for doubleclick when in select mode graphDiv.on('plotly_deselect', function(eventData) { - Shiny.setInputValue(".clientValue-plotly_selected-" + x.source, null); - Shiny.setInputValue(".clientValue-plotly_selecting-" + x.source, null); - Shiny.setInputValue(".clientValue-plotly_brush-" + x.source, null); - Shiny.setInputValue(".clientValue-plotly_brushing-" + x.source, null); - Shiny.setInputValue(".clientValue-plotly_click-" + x.source, null); + Shiny.setInputValue(".clientValue-plotly_selected-" + x.source, null, priority); + Shiny.setInputValue(".clientValue-plotly_selecting-" + x.source, null, priority); + Shiny.setInputValue(".clientValue-plotly_brush-" + x.source, null, priority); + Shiny.setInputValue(".clientValue-plotly_brushing-" + x.source, null, priority); + Shiny.setInputValue(".clientValue-plotly_click-" + x.source, null, priority); }); graphDiv.on('plotly_clickannotation', function(d) { - Shiny.setInputValue(".clientValue-plotly_clickannotation-" + x.source, JSON.stringify(d.fullAnnotation)); + Shiny.setInputValue(".clientValue-plotly_clickannotation-" + x.source, JSON.stringify(d.fullAnnotation), priority); }); + // This is a 'true' event -- always give it priority graphDiv.on('plotly_afterplot', function() { Shiny.setInputValue(".clientValue-plotly_afterplot-" + x.source, "afterplot", {priority: "event"}); }); From 55763975c9e9dc1510f0ab68ad24b4c0d684a648 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 30 Nov 2018 18:38:22 -0600 Subject: [PATCH 10/54] fix legend event data and add example app --- inst/examples/shiny/event_data_legends/app.R | 26 ++++++++++++++++++++ inst/htmlwidgets/plotly.js | 25 +++++++++++++------ 2 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 inst/examples/shiny/event_data_legends/app.R diff --git a/inst/examples/shiny/event_data_legends/app.R b/inst/examples/shiny/event_data_legends/app.R new file mode 100644 index 0000000000..7aa6dca838 --- /dev/null +++ b/inst/examples/shiny/event_data_legends/app.R @@ -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) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index 7f70a39886..b4f87ec0c6 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -366,10 +366,7 @@ HTMLWidgets.widget({ var legendEventData = function(d) { // if legendgroup is not relevant just return the trace var trace = d.data[d.curveNumber]; - if (!trace.legendgroup) { - Shiny.setInputValue(".clientValue-plotly_legendclick-" + x.source, trace); - return; - } + if (!trace.legendgroup) return trace; // if legendgroup was specified, return all traces that match the group var legendgrps = d.data.map(function(trace){ return trace.legendgroup; }); @@ -380,13 +377,25 @@ HTMLWidgets.widget({ } } - Shiny.setInputValue(".clientValue-plotly_legendclick-" + x.source, traces); + return traces; }; - graphDiv.on('plotly_legendclick', legendEventData); - graphDiv.on('plotly_legenddoubleclick', legendEventData); + graphDiv.on('plotly_legendclick', function(d) { + Shiny.setInputValue( + ".clientValue-plotly_legendclick-" + x.source, + JSON.stringify(legendEventData(d)), + priority + ); + }); + graphDiv.on('plotly_legenddoubleclick', function(d) { + Shiny.setInputValue( + ".clientValue-plotly_legenddoubleclick-" + x.source, + JSON.stringify(legendEventData(d)), + priority + ); + }); - } + } // Given an array of {curveNumber: x, pointNumber: y} objects, // return a hash of { From fe1f07587747ee37fb184036688189b5a6240225 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 30 Nov 2018 18:39:36 -0600 Subject: [PATCH 11/54] add a 'proxy method' for modifying the plot config; add annotation example app for proof-of-concept --- .../shiny/event_data_annotation/app.R | 26 +++++++++++++++++++ inst/htmlwidgets/plotly.js | 7 +++++ 2 files changed, 33 insertions(+) create mode 100644 inst/examples/shiny/event_data_annotation/app.R diff --git a/inst/examples/shiny/event_data_annotation/app.R b/inst/examples/shiny/event_data_annotation/app.R new file mode 100644 index 0000000000..347ddb44e2 --- /dev/null +++ b/inst/examples/shiny/event_data_annotation/app.R @@ -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) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index b4f87ec0c6..f5caeb653a 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -192,6 +192,13 @@ HTMLWidgets.widget({ if (!gd) { throw new Error("Couldn't find plotly graph with id: " + msg.id); } + // This isn't an official plotly.js method, but it's the only current way to + // change just the configuration of a plot + // https://community.plot.ly/t/update-config-function/9057 + if (msg.method == "reconfig") { + Plotly.react(gd, gd.data, gd.layout, msg.args); + return; + } if (!Plotly[msg.method]) { throw new Error("Unknown method " + msg.method); } From 80a750de9ab2bf55dd8e39ca075aa1e59cc2405b Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Sun, 2 Dec 2018 16:33:15 -0600 Subject: [PATCH 12/54] add double-click, unhover, and deselect events --- inst/htmlwidgets/plotly.js | 70 +++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js index f5caeb653a..a9d3fdb919 100644 --- a/inst/htmlwidgets/plotly.js +++ b/inst/htmlwidgets/plotly.js @@ -347,27 +347,79 @@ HTMLWidgets.widget({ } }); graphDiv.on('plotly_unhover', function(eventData) { - Shiny.setInputValue(".clientValue-plotly_hover-" + x.source, null, priority); + Shiny.setInputValue( + ".clientValue-plotly_hover-" + x.source, + null, + priority + ); + Shiny.setInputValue( + ".clientValue-plotly_unhover-" + x.source, + JSON.stringify(el.id), + {priority: "event"} + ); }); graphDiv.on('plotly_doubleclick', function(eventData) { - Shiny.setInputValue(".clientValue-plotly_click-" + x.source, null, priority); + Shiny.setInputValue( + ".clientValue-plotly_click-" + x.source, + null, + priority + ); + Shiny.setInputValue( + ".clientValue-plotly_doubleclick-" + x.source, + JSON.stringify(el.id), + {priority: "event"} + ); }); + // 'plotly_deselect' is code for doubleclick when in select mode graphDiv.on('plotly_deselect', function(eventData) { - Shiny.setInputValue(".clientValue-plotly_selected-" + x.source, null, priority); - Shiny.setInputValue(".clientValue-plotly_selecting-" + x.source, null, priority); - Shiny.setInputValue(".clientValue-plotly_brush-" + x.source, null, priority); - Shiny.setInputValue(".clientValue-plotly_brushing-" + x.source, null, priority); - Shiny.setInputValue(".clientValue-plotly_click-" + x.source, null, priority); + Shiny.setInputValue( + ".clientValue-plotly_selected-" + x.source, + null, + priority + ); + Shiny.setInputValue( + ".clientValue-plotly_selecting-" + x.source, + null, + priority + ); + Shiny.setInputValue( + ".clientValue-plotly_brush-" + x.source, + null, + priority + ); + Shiny.setInputValue( + ".clientValue-plotly_brushing-" + x.source, + null, + priority + ); + Shiny.setInputValue( + ".clientValue-plotly_click-" + x.source, + null, + priority + ); + Shiny.setInputValue( + ".clientValue-plotly_deselect-" + x.source, + JSON.stringify(el.id), + {priority: "event"} + ); }); graphDiv.on('plotly_clickannotation', function(d) { - Shiny.setInputValue(".clientValue-plotly_clickannotation-" + x.source, JSON.stringify(d.fullAnnotation), priority); + Shiny.setInputValue( + ".clientValue-plotly_clickannotation-" + x.source, + JSON.stringify(d.fullAnnotation), + priority + ); }); // This is a 'true' event -- always give it priority graphDiv.on('plotly_afterplot', function() { - Shiny.setInputValue(".clientValue-plotly_afterplot-" + x.source, "afterplot", {priority: "event"}); + Shiny.setInputValue( + ".clientValue-plotly_afterplot-" + x.source, + JSON.stringify(el.id), + {priority: "event"} + ); }); var legendEventData = function(d) { From 1522f59f5caf8c4b1d8b0f5de4004988edf0aa51 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Sun, 2 Dec 2018 16:54:55 -0600 Subject: [PATCH 13/54] update list of supported events and use match.arg() to make sure a supported event is provided --- R/shiny.R | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/R/shiny.R b/R/shiny.R index d5c3147692..3be9752626 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -68,14 +68,21 @@ prepareWidget <- function(x) { #' 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_brush", "plotly_brushing", + "plotly_deselect", "plotly_relayout", "plotly_legendclick", + "plotly_legenddoubleclick", "plotly_afterplot" + ), + source = "A", + session = shiny::getDefaultReactiveDomain() +) { 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) + src <- sprintf(".clientValue-%s-%s", match.arg(event), source) val <- session$rootScope()$input[[src]] if (is.null(val)) val else jsonlite::fromJSON(val) } From e7f718e5b1bfe584866799bba265a2f1fd92571f Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 3 Dec 2018 10:54:38 -0600 Subject: [PATCH 14/54] avoid fromJSON simplification for legend events --- R/shiny.R | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/R/shiny.R b/R/shiny.R index 3be9752626..6df6ffaf2a 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -82,7 +82,14 @@ event_data <- function( stop("No reactive domain detected. This function can only be called \n", "from within a reactive shiny context.") } - src <- sprintf(".clientValue-%s-%s", match.arg(event), source) + + # obtain the input value + event <- match.arg(event) + src <- sprintf(".clientValue-%s-%s", event, source) 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 + + if (is.null(val)) val else fromJSONfunc(val) } From 1e10fc7f7693abc86814e8103bdf371ff3ced89a Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 3 Dec 2018 12:32:18 -0600 Subject: [PATCH 15/54] document --- DESCRIPTION | 2 +- man/config.Rd | 6 +++++- man/event_data.Rd | 8 +++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index aacfee00af..ac201edf38 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -71,6 +71,6 @@ Suggests: plotlyGeoAssets, forcats LazyData: true -RoxygenNote: 6.1.0 +RoxygenNote: 6.1.1 Encoding: UTF-8 Roxygen: list(markdown = TRUE) diff --git a/man/config.Rd b/man/config.Rd index ea710d7726..4a2797cb9f 100644 --- a/man/config.Rd +++ b/man/config.Rd @@ -5,7 +5,7 @@ \title{Set the default configuration for plotly} \usage{ config(p, ..., collaborate = TRUE, cloud = FALSE, locale = NULL, - mathjax = NULL) + mathjax = NULL, priority = NULL) } \arguments{ \item{p}{a plotly object} @@ -29,6 +29,10 @@ you must \code{