Skip to content

Commit 20b4f4b

Browse files
committed
Introducing plotlyProxy and plotlyProxyInvoke; fixes #580
1 parent 29dd170 commit 20b4f4b

File tree

6 files changed

+231
-0
lines changed

6 files changed

+231
-0
lines changed

NAMESPACE

+2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ export(plot_ly)
172172
export(plot_mapbox)
173173
export(plotly)
174174
export(plotlyOutput)
175+
export(plotlyProxy)
176+
export(plotlyProxyInvoke)
175177
export(plotly_IMAGE)
176178
export(plotly_POST)
177179
export(plotly_build)

R/proxy.R

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#' Modify a plotly object inside a shiny app
2+
#'
3+
#' @param outputId single-element character vector indicating the output ID
4+
#' map to modify (if invoked from a Shiny module, the namespace will be added
5+
#' automatically)
6+
#' @param session the Shiny session object to which the map belongs; usually the
7+
#' default value will suffice.
8+
#' @param deferUntilFlush indicates whether actions performed against this
9+
#' instance should be carried out right away, or whether they should be held
10+
#' until after the next time all of the outputs are updated.
11+
#'
12+
#' @rdname plotlyProxy
13+
#' @export
14+
#' @examples
15+
#'
16+
#' demo("proxy-mapbox", package = "plotly")
17+
#' demo("proxy-relayout", package = "plotly")
18+
#'
19+
20+
plotlyProxy <- function(outputId, session = shiny::getDefaultReactiveDomain(),
21+
deferUntilFlush = TRUE) {
22+
23+
# implementation very similar to leaflet::leafletProxy & DT:dataTableProxy
24+
if (is.null(session)) {
25+
stop("plotlyProxy must be called from the server function of a Shiny app")
26+
}
27+
if (!is.null(session$ns) && nzchar(session$ns(NULL)) &&
28+
!startsWith(outputId, session$ns(""))) {
29+
outputId <- session$ns(outputId)
30+
}
31+
structure(
32+
list(
33+
session = session,
34+
id = outputId,
35+
deferUntilFlush = deferUntilFlush
36+
# TODO: is there actually a use-case for this?
37+
#x = structure(list(), leafletData = data),
38+
#dependencies = NULL
39+
),
40+
class = "plotly_proxy"
41+
)
42+
}
43+
44+
45+
#' @param p a plotly proxy object (created with \code{plotlyProxy})
46+
#' @param method a plotlyjs method to invoke. For a list of options,
47+
#' visit the \href{https://plot.ly/javascript/plotlyjs-function-reference}
48+
#' {plotlyjs function reference}
49+
#' @param ... unnamed arguments passed onto
50+
#' @rdname plotlyProxy
51+
#' @export
52+
plotlyProxyInvoke <- function(p, method, ...) {
53+
54+
if (!is.proxy(p))
55+
stop("p must be a proxy object. See `help(plotlyProxy)`", call. = FALSE)
56+
57+
if (missing(method))
58+
stop(
59+
"Must provide a plotly.js method (as a character string of length 1).\n",
60+
sprintf("Valid options include: '%s'",
61+
paste(plotlyjs_methods(), collapse = "', '")),
62+
call. = FALSE
63+
)
64+
65+
method <- match.arg(method, plotlyjs_methods())
66+
67+
msg <- list(
68+
id = p$id,
69+
method = method,
70+
# TODO: can we leverage the plotly_build() infrastructure in a smart way?
71+
# args = evalFormula(list(...), data)
72+
args = list(...)
73+
)
74+
75+
if (isTRUE(p$deferUntilFlushed)) {
76+
77+
p$session$onFlushed(function() {
78+
p$session$sendCustomMessage("plotly-calls", msg)
79+
}, once = TRUE)
80+
81+
} else {
82+
83+
p$session$sendCustomMessage("plotly-calls", msg)
84+
85+
}
86+
87+
p
88+
}
89+
90+
91+
plotlyjs_methods <- function() {
92+
c(
93+
"restyle", "relayout", "update", "addTraces", "deleteTraces", "moveTraces",
94+
"extendTraces", "prependTraces", "purge", "toImage", "downloadImage"
95+
)
96+
}
97+
98+
99+
is.proxy <- function(x) {
100+
inherits(x, "plotly_proxy")
101+
}

demo/proxy-mapbox.R

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
library(shiny)
2+
3+
# get all the available mapbox styles
4+
mapStyles <- schema()$layout$layoutAttributes$mapbox$style$values
5+
6+
ui <- fluidPage(
7+
selectInput("style", "Select a mapbox style", mapStyles),
8+
plotlyOutput("map")
9+
)
10+
11+
server <- function(input, output, session) {
12+
13+
output$map <- renderPlotly({
14+
plot_mapbox()
15+
})
16+
17+
observeEvent(input$style, {
18+
plotlyProxy("map", session) %>%
19+
plotlyProxyInvoke(
20+
"relayout",
21+
list(mapbox = list(style = input$style))
22+
)
23+
})
24+
25+
}
26+
27+
shinyApp(ui, server)

demo/proxy-relayout.R

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
library(shiny)
2+
library(plotly)
3+
4+
ui <- fluidPage(
5+
plotlyOutput("plot")
6+
)
7+
8+
server <- function(input, output, session) {
9+
10+
p <- ggplot(txhousing) +
11+
geom_line(aes(date, median, group = city))
12+
13+
output$plot <- renderPlotly({
14+
ggplotly(p, dynamicTicks = TRUE) %>%
15+
rangeslider()
16+
})
17+
18+
observeEvent(event_data("plotly_relayout"), {
19+
d <- event_data("plotly_relayout")
20+
xmin <- d[["xaxis.range[0]"]] %||% d[["xaxis.range"]][1]
21+
xmax <- d[["xaxis.range[1]"]] %||% d[["xaxis.range"]][2]
22+
if (is.null(xmin) || is.null(xmax)) return(NULL)
23+
24+
# compute the y-range based on the new x-range
25+
idx <- with(txhousing, xmin <= date & date <= xmax)
26+
yrng <- extendrange(txhousing$median[idx])
27+
28+
plotlyProxy("plot", session) %>%
29+
plotlyProxyInvoke("relayout", list(yaxis = list(range = yrng)))
30+
})
31+
32+
yRange <- range(txhousing$median, na.rm = TRUE)
33+
observeEvent(event_data("plotly_doubleclick"), {
34+
35+
plotlyProxy("plot", session) %>%
36+
plotlyProxyInvoke("relayout", list(yaxis = list(range = yRange)))
37+
38+
})
39+
40+
41+
}
42+
43+
shinyApp(ui, server)

inst/htmlwidgets/plotly.js

+17
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,23 @@ HTMLWidgets.widget({
159159

160160
}
161161

162+
// Trigger plotly.js calls defined via `plotlyProxy()`
163+
plot.then(function() {
164+
if (HTMLWidgets.shinyMode) {
165+
Shiny.addCustomMessageHandler("plotly-calls", function(msg) {
166+
var gd = document.getElementById(msg.id);
167+
if (!gd) {
168+
throw new Error("Couldn't find plotly graph with id: " + msg.id);
169+
}
170+
if (!Plotly[msg.method]) {
171+
throw new Error("Unknown method " + msg.method);
172+
}
173+
var args = [gd].concat(msg.args);
174+
Plotly[msg.method].apply(null, args);
175+
});
176+
}
177+
});
178+
162179
// Attach attributes (e.g., "key", "z") to plotly event data
163180
function eventDataWithKey(eventData) {
164181
if (eventData === undefined || !eventData.hasOwnProperty("points")) {

man/plotlyProxy.Rd

+41
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)