Skip to content

Commit ba97a84

Browse files
authored
Merge pull request #1040 from ropensci/feature/proxy
Modify plotly graphs in shiny via plotly.js
2 parents 86f4af0 + 3dd536e commit ba97a84

File tree

7 files changed

+235
-1
lines changed

7 files changed

+235
-1
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)

NEWS.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
## NEW FEATURES & IMPROVEMENTS
44

5+
* It is now possible to modify (i.e., update without a full redraw) plotly graphs inside of a shiny app via the new `plotlyProxy()` and `plotlyProxyInvoke()` functions. For examples, see `demo("proxy-relayout", package = "plotly")` and `demo("proxy-mapbox", package = "plotly")`. Closes #580.
56
* The `schema()` function now returns the plot schema (rather just printing it), making it easier to acquire/use values from the official plot schema. See `help(schema)` for an example. Fixes #1038.
67

7-
88
## CHANGES
99

1010
## BUG FIXES

R/proxy.R

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
#' @rdname plotlyProxy
12+
#' @export
13+
#' @examples
14+
#'
15+
#' demo("proxy-mapbox", package = "plotly")
16+
#' demo("proxy-relayout", package = "plotly")
17+
plotlyProxy <- function(outputId, session = shiny::getDefaultReactiveDomain(),
18+
deferUntilFlush = TRUE) {
19+
20+
# implementation very similar to leaflet::leafletProxy & DT:dataTableProxy
21+
if (is.null(session)) {
22+
stop("plotlyProxy must be called from the server function of a Shiny app")
23+
}
24+
25+
if (!is.null(session$ns) && nzchar(session$ns(NULL)) &&
26+
# TODO: require a recent version of R and use startsWith()?
27+
substring(outputId, 1, nchar(session$ns(""))) != session$ns("")) {
28+
outputId <- session$ns(outputId)
29+
}
30+
structure(
31+
list(
32+
session = session,
33+
id = outputId,
34+
deferUntilFlush = deferUntilFlush
35+
# TODO: is there actually a use-case for this?
36+
#x = structure(list(), leafletData = data),
37+
#dependencies = NULL
38+
),
39+
class = "plotly_proxy"
40+
)
41+
}
42+
43+
44+
# ----------------------------------------------------------------------
45+
# TODO: implement some higher-level functions, say `plotlyProxyLayout()`,
46+
# `plotlyProxyAddTraces()`, `plotlyProxyStyle()`, that pick the right
47+
# method, except formula/data mappings, and possibly some argument checking
48+
# ----------------------------------------------------------------------
49+
50+
51+
#' @param p a plotly proxy object (created with \code{plotlyProxy})
52+
#' @param method a plotlyjs method to invoke. For a list of options,
53+
#' visit the \href{https://plot.ly/javascript/plotlyjs-function-reference}{plotlyjs function reference}
54+
#' @param ... unnamed arguments passed onto the plotly.js method
55+
#' @rdname plotlyProxy
56+
#' @export
57+
plotlyProxyInvoke <- function(p, method, ...) {
58+
59+
if (!is.proxy(p))
60+
stop("p must be a proxy object. See `help(plotlyProxy)`", call. = FALSE)
61+
62+
if (missing(method))
63+
stop(
64+
"Must provide a plotly.js method (as a character string of length 1).\n",
65+
sprintf("Valid options include: '%s'",
66+
paste(plotlyjs_methods(), collapse = "', '")),
67+
call. = FALSE
68+
)
69+
70+
method <- match.arg(method, plotlyjs_methods())
71+
72+
msg <- list(
73+
id = p$id,
74+
method = method,
75+
# TODO: can we leverage the plotly_build() infrastructure in a smart way?
76+
# args = evalFormula(list(...), data)
77+
args = list(...)
78+
)
79+
80+
if (isTRUE(p$deferUntilFlushed)) {
81+
82+
p$session$onFlushed(function() {
83+
p$session$sendCustomMessage("plotly-calls", msg)
84+
}, once = TRUE)
85+
86+
} else {
87+
88+
p$session$sendCustomMessage("plotly-calls", msg)
89+
90+
}
91+
92+
p
93+
}
94+
95+
96+
plotlyjs_methods <- function() {
97+
c(
98+
"restyle", "relayout", "update", "addTraces", "deleteTraces", "moveTraces",
99+
"extendTraces", "prependTraces", "purge", "toImage", "downloadImage"
100+
)
101+
}
102+
103+
104+
is.proxy <- function(x) {
105+
inherits(x, "plotly_proxy")
106+
}

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 <- if (length(d[["xaxis.range[0]"]])) d[["xaxis.range[0]"]] else d[["xaxis.range"]][1]
21+
xmax <- if (length(d[["xaxis.range[1]"]])) d[["xaxis.range[1]"]] else 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

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

0 commit comments

Comments
 (0)