Skip to content

Commit 236a814

Browse files
authored
Merge pull request #1268 from ropensci/partial-bundles
Partial bundles
2 parents c62fe4a + e9ff0f3 commit 236a814

File tree

8 files changed

+357
-5
lines changed

8 files changed

+357
-5
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export(layout)
149149
export(mutate)
150150
export(mutate_)
151151
export(offline)
152+
export(partial_bundle)
152153
export(plot_dendro)
153154
export(plot_geo)
154155
export(plot_ly)

NEWS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
* Upgraded to plotly.js v1.38.1. A _huge_ amount of features and improvements have been made since v1.29.2 (i.e., the version included in the last CRAN release of the R package - v4.7.1). Highlights include a complete re-write of `scattergl` to make it nearly feature complete with `scatter`, localization of text rendering (i.e., international translations), and two new trace types (`violin` & `table`). Read more about the v1.32.0 release [here](https://codeburst.io/notes-from-the-latest-plotly-js-release-b035a5b43e21) and the complete list of changes [here](https://github.com/plotly/plotly.js/releases).
66
* Support for **sf** (simple feature) data structures was added to `plot_ly()`, `plot_mapbox()`, and `plot_geo()` (via the new `add_sf()` function). See [this blog post](https://blog.cpsievert.me/2018/03/30/visualizing-geo-spatial-data-with-sf-and-plotly) for an overview.
77
* New "special arguments" `stroke`, `strokes`, `alpha_stroke`, `span`, and `spans` were added for easier control over the stroke (i.e., outline) appearance of various (filled) graphical marks. For an overview, see the **sf** blog post linked to in the bullet point above and the new package demos (list all demos with `demo(package = "plotly")`).
8-
* One may now inform `ggplotly()` about the relevant **shiny** output size via `session$clientData`. This ensures `ggplotly()` sizing is closer to **ggplot2** sizing, even on window resize. For an example, run `plotly_example("shiny", "ggplotly_sizing")`.
98
* The selection (i.e., linked-brushing) mode can now switch from 'transient' to 'persistent' by holding the 'shift' key. It's still possible to _force_ persistent selection by setting `persistent = TRUE` in `highlight()`, but `persistent = FALSE` (the default) is now recommended since it allows one to switch between [persistent/transient selection](https://plotly-book.cpsievert.me/linking-views-without-shiny.html#transient-versus-persistent-selection) in the browser, rather than at the command line.
9+
* The new `partial_bundle()` function makes it easy to leverage [partial bundles of plotly.js](https://github.com/plotly/plotly.js#partial-bundles) for reduced file sizes and faster render times.
10+
* One may now inform `ggplotly()` about the relevant **shiny** output size via `session$clientData`. This ensures `ggplotly()` sizing is closer to **ggplot2** sizing, even on window resize. For an example, run `plotly_example("shiny", "ggplotly_sizing")`.
1011
* Instead of an error, `ggplotly(NULL, "message")` and `plotly_build(NULL, "message")` now returns `htmltools::div("message")`, making it easier to relay messages in shiny when data isn't yet ready to plot (see #1116)
1112
* The `animation_button()` function gains a `label` argument, making it easier to control the label of an animation button generated through the `frame` API (see #1205).
1213

R/partial_bundles.R

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#' Use a partial bundle of plotly.js
2+
#'
3+
#' Leveraging plotly.js' partial bundles can lead to smaller file sizes
4+
#' and faster rendering. The full list of available bundles, and the
5+
#' trace types that they support, are available
6+
#' [here](https://github.com/plotly/plotly.js/blob/master/dist/README.md#partial-bundles)
7+
#'
8+
#' @details WARNING: use this function with caution when rendering multiple
9+
#' plotly graphs on a single website. That's because, if multiple plotly.js
10+
#' bundles are used, the most recent bundle will override the other bundles.
11+
#' See the examples section for an example.
12+
#'
13+
#' @param p a plotly object.
14+
#' @param type name of the (partial) bundle. The default, `'auto'`, attempts to
15+
#' find the smallest single bundle that can render `p`. If no single partial bundle
16+
#' can render `p`, then the full bundle is used.
17+
#' @param local whether or not to download the partial bundle so that it can be
18+
#' viewed later without an internet connection.
19+
#' @param minified whether or not to use a minified js file (non-minified file can be useful for debugging plotly.js)
20+
#' @author Carson Sievert
21+
#' @export
22+
#' @examples
23+
#'
24+
#' # ----------------------------------------------------------------------
25+
#' # This function is always safe to use when rendering a single
26+
#' # plotly graph. In this case, we get a 3x file reduction.
27+
#' # ----------------------------------------------------------------------
28+
#'
29+
#' library(plotly)
30+
#' p <- plot_ly(x = 1:10, y = 1:10) %>% add_markers()
31+
#' save_widget <- function(p, f) {
32+
#' owd <- setwd(dirname(f))
33+
#' on.exit(setwd(owd))
34+
#' htmlwidgets::saveWidget(p, f)
35+
#' mb <- round(file.info(f)$size / 1e6, 3)
36+
#' message("File is: ", mb," MB")
37+
#' }
38+
#' f1 <- tempfile(fileext = ".html")
39+
#' f2 <- tempfile(fileext = ".html")
40+
#' save_widget(p, f1)
41+
#' save_widget(partial_bundle(p), f2)
42+
#'
43+
#' # ----------------------------------------------------------------------
44+
#' # But, since plotly.js bundles override one another,
45+
#' # be careful when putting multiple graphs in a larger document!
46+
#' # Note how the surface (part of the gl3d bundle) renders, but the
47+
#' # heatmap (part of the cartesian bundle) doesn't...
48+
#' # ----------------------------------------------------------------------
49+
#'
50+
#' library(htmltools)
51+
#' p1 <- plot_ly(z = ~volcano) %>%
52+
#' add_heatmap() %>%
53+
#' partial_bundle()
54+
#' p2 <- plot_ly(z = ~volcano) %>%
55+
#' add_surface() %>%
56+
#' partial_bundle()
57+
#' browsable(tagList(p1, p2))
58+
#'
59+
60+
61+
partial_bundle <- function(p, type = "auto", local = TRUE, minified = TRUE) {
62+
63+
if (!is.plotly(p)) stop("The first argument to `partial_bundle()` must be a plotly object", call. = FALSE)
64+
65+
# Amongst all the 'print-time' htmlwidget dependencies,
66+
# find the plotly.js dependency and attach some meta-info
67+
idx <- plotlyjsBundleIDX(p)
68+
p$dependencies[[idx]]$local <- local
69+
p$dependencies[[idx]]$minified <- minified
70+
p$dependencies[[idx]]$partial_bundle <- match.arg(type, c("auto", "main", names(bundleTraceMap)))
71+
72+
# unfortunately, htmlwidgets doesn't appear to support manipulation of
73+
# dependencies field during preRenderHook(), so we need to explicitly build
74+
plotly_build(p)
75+
}
76+
77+
verify_partial_bundle <- function(p) {
78+
# return early if we're using the main bundle (the default)
79+
currentBundle <- plotlyjsBundle(p)
80+
bundleType <- currentBundle$partial_bundle %||% "main"
81+
if (identical(bundleType, "main")) return(p)
82+
83+
# grab all the required trace types
84+
types <- unique(vapply(p$x$data, function(x) x[["type"]] %||% "scatter", character(1)))
85+
86+
if (identical(bundleType, "auto")) {
87+
88+
# resolve an auto bundle by using the 1st bundle that supports all the types
89+
# (ordering of bundleTraceMap is important!)
90+
for (i in seq_along(bundleTraceMap)) {
91+
if (all(types %in% bundleTraceMap[[i]])) {
92+
bundleType <- names(bundleTraceMap)[[i]]
93+
break
94+
}
95+
}
96+
97+
if (identical(bundleType, "auto")) {
98+
warning(
99+
"Couldn't find a single partial bundle that would support this plotly ",
100+
"visualization. Using the main (full) bundle instead."
101+
)
102+
p$dependencies[[plotlyjsBundleIDX(p)]] <- plotlyMainBundle()
103+
return(p)
104+
}
105+
106+
}
107+
108+
# verify that this partial bundle actually supports this viz
109+
# (at this point, bundleType should never be 'auto' or 'main')
110+
missingTypes <- setdiff(types, bundleTraceMap[[bundleType]])
111+
if (length(missingTypes)) {
112+
msg <- sprint(
113+
"The '%s' bundle supports the following trace types: '%s'.\n\n",
114+
"This plotly visualization contains the following trace types: '%s'",
115+
bundle, paste(missingTypes, collapse = "', '"), paste(missingTypes, collapse = "', '")
116+
)
117+
stop(msg, call. = FALSE)
118+
}
119+
120+
idx <- plotlyjsBundleIDX(p)
121+
bundle_name <- sprintf("plotly-%s", bundleType)
122+
bundle_script <- sprintf(
123+
"plotly-%s-%s.%sjs",
124+
bundleType, currentBundle$version,
125+
if (isTRUE(currentBundle$minified)) "min." else ""
126+
)
127+
128+
p$dependencies[[idx]]$name <- bundle_name
129+
p$dependencies[[idx]]$script <- bundle_script
130+
p$dependencies[[idx]]$src <- list(href = "https://cdn.plot.ly")
131+
132+
# download the relevant bundle
133+
if (isTRUE(p$dependencies[[idx]]$local)) {
134+
# TODO: implement a caching mechanism?
135+
try_library("curl", "partial_bundle")
136+
tmpfile <- file.path(tempdir(), bundle_script)
137+
p$dependencies[[idx]]$src$file <- dirname(tmpfile)
138+
if (!file.exists(tmpfile)) {
139+
curl::curl_download(paste0("https://cdn.plot.ly/", bundle_script), tmpfile)
140+
}
141+
}
142+
143+
p
144+
}
145+
146+
plotlyjsBundleIDX <- function(p) {
147+
depNames <- sapply(p$dependencies, "[[", "name")
148+
bundleNames <- paste0("plotly-", c("main", "auto", names(bundleTraceMap)))
149+
idx <- which(depNames %in% bundleNames)
150+
if (length(idx) != 1) stop("Couldn't find the plotlyjs bundle")
151+
idx
152+
}
153+
154+
plotlyjsBundle <- function(p) {
155+
p$dependencies[[plotlyjsBundleIDX(p)]]
156+
}
157+
158+
# TODO: create this object in inst/plotlyjs.R from the dist/README.md
159+
bundleTraceMap <- list(
160+
basic = c(
161+
"scatter",
162+
"bar",
163+
"pie"
164+
),
165+
cartesian = c(
166+
"scatter",
167+
"bar",
168+
"pie",
169+
"box",
170+
"heatmap",
171+
"histogram",
172+
"histogram2d",
173+
"histogram2dcontour",
174+
"contour",
175+
"scatterternary",
176+
"violin"
177+
),
178+
geo = c(
179+
"scatter",
180+
"scattergeo",
181+
"choropleth"
182+
),
183+
gl3d = c(
184+
"scatter",
185+
"scatter3d",
186+
"surface",
187+
"mesh3d",
188+
"cone"
189+
),
190+
gl2d = c(
191+
"scatter",
192+
"scattergl",
193+
"splom",
194+
"pointcloud",
195+
"heatmapgl",
196+
"contourgl",
197+
"parcoords"
198+
),
199+
mapbox = c(
200+
"scatter",
201+
"scattermapbox"
202+
),
203+
finance = c(
204+
"scatter",
205+
"bar",
206+
"pie",
207+
"histogram",
208+
"ohlc",
209+
"candlestick"
210+
)
211+
)

R/plotly.R

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ as_widget <- function(x, ...) {
405405
dependencies = c(
406406
list(typedArrayPolyfill()),
407407
crosstalk::crosstalkLibs(),
408+
list(plotlyHtmlwidgetsCSS()),
408409
list(plotlyMainBundle())
409410
)
410411
)
@@ -414,18 +415,28 @@ typedArrayPolyfill <- function() {
414415
htmltools::htmlDependency(
415416
"typedarray", "0.1",
416417
src = depPath("typedarray"),
417-
script = "typedarray.min.js"
418+
script = "typedarray.min.js",
419+
all_files = FALSE
418420
)
419421
}
420422

421423
# TODO: suggest a plotlyBundles package that has trace-level bundles
422424
# and bundle size at print time.
423425
plotlyMainBundle <- function() {
424426
htmltools::htmlDependency(
425-
"plotlyjs", "1.38.1",
427+
"plotly-main", "1.38.1",
426428
src = depPath("plotlyjs"),
427429
script = "plotly-latest.min.js",
428-
stylesheet = "plotly-htmlwidgets.css"
430+
all_files = FALSE
431+
)
432+
}
433+
434+
plotlyHtmlwidgetsCSS <- function() {
435+
htmltools::htmlDependency(
436+
"plotly-htmlwidgets-css", "1.38.1",
437+
src = depPath("plotlyjs"),
438+
stylesheet = "plotly-htmlwidgets.css",
439+
all_files = FALSE
429440
)
430441
}
431442

R/plotly_build.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,9 @@ plotly_build.plotly <- function(p, registerFrames = TRUE) {
375375
# box up 'data_array' attributes where appropriate
376376
p <- verify_attr_spec(p)
377377

378+
# if a partial bundle was specified, make sure it supports the visualization
379+
p <- verify_partial_bundle(p)
380+
378381
# make sure plots don't get sent out of the network (for enterprise)
379382
p$x$base_url <- get_domain()
380383
p

inst/htmlwidgets/plotly.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ HTMLWidgets.widget({
203203
for (var i = 0; i < mapboxIDs.length; i++) {
204204
var id = mapboxIDs[i];
205205
var mapOpts = x.layout[id] || {};
206-
var args = mapOpts._fitBounds || {}
206+
var args = mapOpts._fitBounds || {};
207207
if (!args) {
208208
continue;
209209
}

man/partial_bundle.Rd

Lines changed: 75 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)