Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 278b670

Browse filesBrowse files
authored
Add kaleido() for static image exporting (#1971)
1 parent bdfdc4b commit 278b670
Copy full SHA for 278b670

File tree

106 files changed

+370
-167
lines changed
Filter options

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Dismiss banner

106 files changed

+370
-167
lines changed

‎.github/workflows/R-CMD-check.yaml

Copy file name to clipboardExpand all lines: .github/workflows/R-CMD-check.yaml
+26-21
Original file line numberDiff line numberDiff line change
@@ -72,35 +72,33 @@ jobs:
7272
key: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-${{ hashFiles('.github/r-depends.rds') }}
7373
restore-keys: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-
7474

75-
- name: Install system dependencies
75+
- name: Install Linux sysdeps
7676
if: runner.os == 'Linux'
7777
run: |
7878
pak::local_system_requirements(execute = TRUE)
7979
pak::pkg_system_requirements("rcmdcheck", execute = TRUE)
8080
shell: Rscript {0}
81-
82-
- uses: actions/setup-node@v1
83-
with:
84-
node-version: ${{ matrix.config.node }}
85-
86-
- name: Install orca
87-
if: matrix.config.visual_tests == true
88-
run: npm install -g electron@6.1.4 orca
89-
shell: bash
90-
91-
- name: Install phantomjs
92-
if: matrix.config.shinytest == true
93-
run: |
94-
pak::pak("shinytest")
95-
shinytest::installDependencies()
96-
pak::pak()
97-
shell: Rscript {0}
98-
81+
9982
- name: Install dependencies
10083
run: |
101-
pak::local_install_dev_deps(upgrade = TRUE)
84+
if (Sys.info()[['sysname']] == 'Darwin') options(pkgType = 'mac.binary')
85+
pak::local_install_dev_deps(upgrade = FALSE)
10286
pak::pkg_install("rcmdcheck")
10387
shell: Rscript {0}
88+
89+
- name: Set up Python 3.8
90+
uses: actions/setup-python@v2
91+
with:
92+
python-version: 3.8
93+
94+
- name: Install kaleido
95+
if: matrix.config.visual_tests == true
96+
run: |
97+
sudo chown -R $UID $CONDA # https://github.com/nextstrain/conda/issues/5
98+
Rscript -e "reticulate::install_miniconda()"
99+
Rscript -e "reticulate::conda_install('r-reticulate', 'python-kaleido')"
100+
Rscript -e "reticulate::conda_install('r-reticulate', 'plotly', channel = 'plotly')"
101+
Rscript -e "reticulate::use_miniconda('r-reticulate')"
104102
105103
- name: Session info
106104
run: |
@@ -109,10 +107,17 @@ jobs:
109107
sessioninfo::session_info(pkgs, include_base = TRUE)
110108
shell: Rscript {0}
111109

110+
- name: Install shinytest deps
111+
if: matrix.config.shinytest == true
112+
run: |
113+
Rscript -e 'shinytest::installDependencies()'
114+
R CMD install .
115+
shell: bash
116+
112117
# Run test() before R CMD check since, for some reason, rcmdcheck::rcmdcheck() skips vdiffr tests
113118
- name: Run Tests
114119
run: |
115-
options(crayon.enabled = TRUE)
120+
options(crayon.enabled = TRUE, testthat.progress.max_fails=1000)
116121
if (!require(devtools)) pak::pak("devtools")
117122
if (!require(reshape2)) pak::pak("reshape2")
118123
res <- devtools::test()

‎DESCRIPTION

Copy file name to clipboardExpand all lines: DESCRIPTION
+3-1
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,15 @@ Suggests:
6767
dendextend,
6868
maptools,
6969
rgeos,
70+
sf,
7071
png,
7172
IRdisplay,
7273
processx,
7374
plotlyGeoAssets,
7475
forcats,
7576
palmerpenguins,
76-
rversions
77+
rversions,
78+
reticulate
7779
LazyData: true
7880
RoxygenNote: 7.1.1
7981
Encoding: UTF-8

‎NAMESPACE

Copy file name to clipboardExpand all lines: NAMESPACE
+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ S3method(print,api)
3434
S3method(print,api_grid)
3535
S3method(print,api_grid_local)
3636
S3method(print,api_plot)
37+
S3method(print,kaleidoScope)
3738
S3method(print,plotly_data)
3839
S3method(to_basic,GeomAbline)
3940
S3method(to_basic,GeomAnnotationMap)
@@ -136,6 +137,7 @@ export(hide_guides)
136137
export(hide_legend)
137138
export(highlight)
138139
export(highlight_key)
140+
export(kaleido)
139141
export(knit_print.api_grid)
140142
export(knit_print.api_grid_local)
141143
export(knit_print.api_plot)
@@ -260,6 +262,7 @@ importFrom(tidyr,unnest)
260262
importFrom(tools,file_ext)
261263
importFrom(tools,file_path_sans_ext)
262264
importFrom(utils,browseURL)
265+
importFrom(utils,capture.output)
263266
importFrom(utils,data)
264267
importFrom(utils,file.edit)
265268
importFrom(utils,getFromNamespace)

‎NEWS.md

Copy file name to clipboardExpand all lines: NEWS.md
+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
* `ggplotly()` now uses the `layout.legend.title` (instead of `layout.annotations`) plotly.js API to convert guides for discrete scales. (#1961)
1010

11+
## New Features
12+
13+
* Added new `kaleido()` function for static image exporting via the [kaleido python package](https://github.com/plotly/Kaleido). See `help(kaleido, package = "plotly")` for installation info and example usage. (#1971)
14+
1115
## Improvements
1216

1317
* `ggplotly()` now better positions axis titles for `facet_wrap()`/`facet_grid()`. (#1975)

‎R/kaleido.R

Copy file name to clipboard
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#' Static image exporting via kaleido
2+
#'
3+
#' Static image exporting via [the kaleido python
4+
#' package](https://github.com/plotly/Kaleido/). `kaleido()` imports
5+
#' kaleido into a \pkg{reticulate}d Python session and returns a `$transform()`
6+
#' method for converting R plots into static images (see examples below).
7+
#'
8+
#' @section Installation:
9+
#'
10+
#' `kaleido()` requires [the kaleido python
11+
#' package](https://github.com/plotly/Kaleido/) to be usable via the \pkg{reticulate} package. Here is a recommended way to do the installation:
12+
#'
13+
#' ```
14+
#' install.packages('reticulate')
15+
#' reticulate::install_miniconda()
16+
#' reticulate::conda_install('r-reticulate', 'python-kaleido')
17+
#' reticulate::conda_install('r-reticulate', 'plotly', channel = 'plotly')
18+
#' reticulate::use_miniconda('r-reticulate')
19+
#' ```
20+
#'
21+
#' @param ... not currently used.
22+
#' @export
23+
#' @return an environment which contains:
24+
#' * `transform()`: a function to convert plots objects into static images,
25+
#' with the following arguments:
26+
#' * `p`: a plot object.
27+
#' * `file`: a file path with a suitable file extension (png, jpg, jpeg,
28+
#' webp, svg, or pdf).
29+
#' * `width`, `height`: The width/height of the exported image in layout
30+
#' pixels. If `scale` is 1, this will also be the width/height of the
31+
#' exported image in physical pixels.
32+
#' * `scale`: The scale factor to use when exporting the figure. A scale
33+
#' factor larger than 1.0 will increase the image resolution with
34+
#' respect to the figure's layout pixel dimensions. Whereas as
35+
#' scale factor of less than 1.0 will decrease the image resolution.
36+
#' * `shutdown()`: a function for shutting down any currently running subprocesses
37+
#' that were launched via `transform()`
38+
#' * `scope`: a reference to the underlying `kaleido.scopes.plotly.PlotlyScope`
39+
#' python object. Modify this object to customize the underlying Chromium
40+
#' subprocess and/or configure other details such as URL to plotly.js, MathJax, etc.
41+
#' @examples
42+
#'
43+
#' \dontrun{
44+
#' scope <- kaleido()
45+
#' tmp <- tempfile(fileext = ".png")
46+
#' scope$transform(plot_ly(x = 1:10), tmp)
47+
#' file.show(tmp)
48+
#' # Remove and garbage collect to remove
49+
#' # R/Python objects and shutdown subprocesses
50+
#' rm(scope); gc()
51+
#' }
52+
#'
53+
kaleido <- function(...) {
54+
if (!rlang::is_installed("reticulate")) {
55+
stop("`kaleido()` requires the reticulate package.")
56+
}
57+
if (!reticulate::py_available(initialize = TRUE)) {
58+
stop("`kaleido()` requires `reticulate::py_available()` to be `TRUE`. Do you need to install python?")
59+
}
60+
61+
py <- reticulate::py
62+
scope_name <- paste0("scope_", new_id())
63+
py[[scope_name]] <- reticulate::import("kaleido")$scopes$plotly$PlotlyScope(
64+
plotlyjs = plotlyMainBundlePath()
65+
)
66+
67+
scope <- py[[scope_name]]
68+
69+
mapbox <- Sys.getenv("MAPBOX_TOKEN", NA)
70+
if (!is.na(mapbox)) {
71+
scope$mapbox_access_token <- mapbox
72+
}
73+
74+
res <- list2env(list(
75+
scope = scope,
76+
# https://github.com/plotly/Kaleido/blob/6a46ecae/repos/kaleido/py/kaleido/scopes/plotly.py#L78-L106
77+
transform = function(p, file = "figure.png", width = NULL, height = NULL, scale = NULL) {
78+
# Perform JSON conversion exactly how the R package would do it
79+
# (this is essentially plotly_json(), without the additional unneeded info)
80+
# and attach as an attribute on the python scope object
81+
scope[["_last_plot"]] <- to_JSON(
82+
plotly_build(p)$x[c("data", "layout", "config")]
83+
)
84+
# On the python side, _last_plot is a string, so use json.loads() to
85+
# convert to dict(). This should be fine since json is a dependency of the
86+
# BaseScope() https://github.com/plotly/Kaleido/blob/586be5/repos/kaleido/py/kaleido/scopes/base.py#L2
87+
transform_cmd <- sprintf(
88+
"%s.transform(sys.modules['json'].loads(%s._last_plot), format='%s', width=%s, height=%s, scale=%s)",
89+
scope_name, scope_name, tools::file_ext(file),
90+
reticulate::r_to_py(width), reticulate::r_to_py(height),
91+
reticulate::r_to_py(scale)
92+
)
93+
# Write the base64 encoded string that transform() returns to disk
94+
# https://github.com/plotly/Kaleido/blame/master/README.md#L52
95+
reticulate::py_run_string(
96+
sprintf("open('%s', 'wb').write(%s)", file, transform_cmd)
97+
)
98+
},
99+
# Shutdown the kaleido subprocesses
100+
# https://github.com/plotly/Kaleido/blob/586be5c/repos/kaleido/py/kaleido/scopes/base.py#L71-L72
101+
shutdown = function() {
102+
reticulate::py_run_string(paste0(scope_name, ".__del__()"))
103+
}
104+
))
105+
106+
# Shutdown subprocesses and delete python scope when
107+
# this object is garbage collected by R
108+
reg.finalizer(res, onexit = TRUE, function(x) {
109+
x$shutdown()
110+
reticulate::py_run_string(paste("del", scope_name))
111+
})
112+
113+
class(res) <- "kaleidoScope"
114+
res
115+
}
116+
117+
118+
#' Print method for kaleido
119+
#'
120+
#' S3 method for [kaleido()].
121+
#'
122+
#' @param x a [kaleido()] object.
123+
#' @param ... currently unused.
124+
#' @export
125+
#' @importFrom utils capture.output
126+
#' @keywords internal
127+
print.kaleidoScope <- function(x, ...) {
128+
args <- formals(x$transform)
129+
cat("$transform: function(", paste(names(args), collapse = ", "), ")\n", sep = "")
130+
cat("$shutdown: function()\n")
131+
cat("$scope: ", utils::capture.output(x$scope))
132+
}

‎R/orca.R

Copy file name to clipboardExpand all lines: R/orca.R
+8-21
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
#' Static image exporting
1+
#' Static image exporting via orca
22
#'
3-
#' Export plotly objects to static images (e.g., pdf, png, jpeg, svg, etc) via the
4-
#' [orca command-line utility](https://github.com/plotly/orca#installation).
5-
#'
6-
#' The `orca()` function is designed for exporting one plotly graph whereas `orca_serve()`
7-
#' is meant for exporting many graphs at once. The former starts and stops an external (nodejs)
8-
#' process everytime it is called whereas the latter starts up a process when called, then
9-
#' returns an `export()` method for exporting graphs as well as a `close()` method for stopping
10-
#' the external (background) process.
3+
#' Superseded by [kaleido()].
114
#'
125
#' @param p a plotly object.
136
#' @param file output filename.
@@ -67,17 +60,14 @@ orca <- function(p, file = "plot.png", format = tools::file_ext(file),
6760
parallel_limit = NULL, verbose = FALSE, debug = FALSE,
6861
safe = FALSE, more_args = NULL, ...) {
6962

63+
.Deprecated("kaleido")
64+
7065
orca_available()
7166

7267
b <- plotly_build(p)
7368

7469
# find the relevant plotly.js bundle
75-
plotlyjs <- plotlyjsBundle(b)
76-
plotlyjs_path <- file.path(plotlyjs$src$file, plotlyjs$script)
77-
# package field means src file path should be relative to pkg dir
78-
if (!is.null(plotlyjs$package)) {
79-
plotlyjs_path <- system.file(plotlyjs_path, package = plotlyjs$package)
80-
}
70+
plotlyjs_path <- plotlyMainBundlePath()
8171

8272
tmp <- tempfile(fileext = ".json")
8373
cat(to_JSON(b$x[c("data", "layout")]), file = tmp)
@@ -141,17 +131,14 @@ orca_serve <- function(port = 5151, mathjax = FALSE, safe = FALSE, request_limit
141131
keep_alive = TRUE, window_max_number = NULL, quiet = FALSE,
142132
debug = FALSE, more_args = NULL, ...) {
143133

134+
.Deprecated("kaleido")
135+
144136
# make sure we have the required infrastructure
145137
orca_available()
146138
try_library("processx", "orca_serve")
147139

148140
# use main bundle since any plot can be thrown at the server
149-
plotlyjs <- plotlyMainBundle()
150-
plotlyjs_path <- file.path(plotlyjs$src$file, plotlyjs$script)
151-
# package field means src file path should be relative to pkg dir
152-
if (!is.null(plotlyjs$package)) {
153-
plotlyjs_path <- system.file(plotlyjs_path, package = plotlyjs$package)
154-
}
141+
plotlyjs_path <- plotlyMainBundlePath()
155142

156143
args <- c(
157144
"serve",

‎R/plotly.R

Copy file name to clipboardExpand all lines: R/plotly.R
+9
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,15 @@ plotlyMainBundle <- function() {
487487
)
488488
}
489489

490+
plotlyMainBundlePath <- function() {
491+
dep <- plotlyMainBundle()
492+
path <- file.path(dep$src$file, dep$script)
493+
if (!is.null(dep$package)) {
494+
path <- system.file(path, package = dep$package)
495+
}
496+
path
497+
}
498+
490499
plotlyHtmlwidgetsCSS <- function() {
491500
htmltools::htmlDependency(
492501
name = "plotly-htmlwidgets-css",

‎man/kaleido.Rd

Copy file name to clipboardExpand all lines: man/kaleido.Rd
+64
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.