Skip to content

Add kaleido() for static image exporting #1971

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 42 commits into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
367b21a
wip implement kaleido() for image exporting
cpsievert Jul 2, 2021
cae8919
Fix sf on mac
cpsievert Jul 2, 2021
1c592ae
Try again
cpsievert Jul 2, 2021
1fcd709
sf is such a headache
cpsievert Jul 2, 2021
9f1c8fb
cleanup print method
cpsievert Jul 2, 2021
22ea199
attempt to 'fix' conda install
cpsievert Jul 2, 2021
d88de96
attempt to 'fix' conda install
cpsievert Jul 2, 2021
88b1d3f
attempt to 'fix' conda install
cpsievert Jul 2, 2021
40bd2b1
attempt to 'fix' conda install
cpsievert Jul 2, 2021
053beb7
attempt to 'fix' conda install
cpsievert Jul 2, 2021
093c771
attempt to 'fix' conda install
cpsievert Jul 2, 2021
bf029d3
attempt to 'fix' conda install
cpsievert Jul 2, 2021
525eb1e
give up on conda
cpsievert Jul 2, 2021
a43ceed
attempt to 'fix' conda install
cpsievert Jul 2, 2021
bef0e18
bring back sf
cpsievert Jul 2, 2021
8c39425
install kaleido in reticulate env
cpsievert Jul 2, 2021
0d7dfc0
sffff
cpsievert Jul 2, 2021
260c6e6
ignore sf again
cpsievert Jul 2, 2021
d26fb06
fix
cpsievert Jul 2, 2021
5a12147
debug
cpsievert Jul 2, 2021
e3b565e
Use import() for better namespacing
cpsievert Jul 2, 2021
73caad6
:fingers-crossed:
cpsievert Jul 2, 2021
52a4674
use a special conda env
cpsievert Jul 2, 2021
df8d6cd
try installing miniconda first
cpsievert Jul 2, 2021
705235f
use miniconda
cpsievert Jul 2, 2021
d101fcc
go back to r-reticulate
cpsievert Jul 2, 2021
9519cab
debug
cpsievert Jul 2, 2021
03c9f42
progress
cpsievert Jul 2, 2021
779c3f3
increase max failures
cpsievert Jul 2, 2021
edeed4b
new snaps
cpsievert Jul 2, 2021
95dbd87
missed one
cpsievert Jul 2, 2021
11ea89a
install
cpsievert Jul 2, 2021
79f646b
accept new baselines
cpsievert Jul 14, 2021
ee06116
fix install
cpsievert Jul 14, 2021
9bb78ec
update news
cpsievert Jul 14, 2021
5c948f4
add installation section
cpsievert Jul 14, 2021
0a37c6a
update news
cpsievert Jul 14, 2021
b8eb4bd
add back sf
cpsievert Jul 14, 2021
6963800
don't upgrade
cpsievert Jul 14, 2021
225be14
don't upgrade
cpsievert Jul 14, 2021
2839004
approve new visual baselines
cpsievert Jul 14, 2021
a928096
pak isn't ready for prod
cpsievert Jul 14, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 26 additions & 21 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,35 +72,33 @@ jobs:
key: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-${{ hashFiles('.github/r-depends.rds') }}
restore-keys: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-

- name: Install system dependencies
- name: Install Linux sysdeps
if: runner.os == 'Linux'
run: |
pak::local_system_requirements(execute = TRUE)
pak::pkg_system_requirements("rcmdcheck", execute = TRUE)
shell: Rscript {0}

- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.config.node }}

- name: Install orca
if: matrix.config.visual_tests == true
run: npm install -g [email protected] orca
shell: bash

- name: Install phantomjs
if: matrix.config.shinytest == true
run: |
pak::pak("shinytest")
shinytest::installDependencies()
pak::pak()
shell: Rscript {0}


- name: Install dependencies
run: |
pak::local_install_dev_deps(upgrade = TRUE)
if (Sys.info()[['sysname']] == 'Darwin') options(pkgType = 'mac.binary')
pak::local_install_dev_deps(upgrade = FALSE)
pak::pkg_install("rcmdcheck")
shell: Rscript {0}

- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8

- name: Install kaleido
if: matrix.config.visual_tests == true
run: |
sudo chown -R $UID $CONDA # https://github.com/nextstrain/conda/issues/5
Rscript -e "reticulate::install_miniconda()"
Rscript -e "reticulate::conda_install('r-reticulate', 'python-kaleido')"
Rscript -e "reticulate::conda_install('r-reticulate', 'plotly', channel = 'plotly')"
Rscript -e "reticulate::use_miniconda('r-reticulate')"

- name: Session info
run: |
Expand All @@ -109,10 +107,17 @@ jobs:
sessioninfo::session_info(pkgs, include_base = TRUE)
shell: Rscript {0}

- name: Install shinytest deps
if: matrix.config.shinytest == true
run: |
Rscript -e 'shinytest::installDependencies()'
R CMD install .
shell: bash

# Run test() before R CMD check since, for some reason, rcmdcheck::rcmdcheck() skips vdiffr tests
- name: Run Tests
run: |
options(crayon.enabled = TRUE)
options(crayon.enabled = TRUE, testthat.progress.max_fails=1000)
if (!require(devtools)) pak::pak("devtools")
if (!require(reshape2)) pak::pak("reshape2")
res <- devtools::test()
Expand Down
4 changes: 3 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ Suggests:
dendextend,
maptools,
rgeos,
sf,
png,
IRdisplay,
processx,
plotlyGeoAssets,
forcats,
palmerpenguins,
rversions
rversions,
reticulate
LazyData: true
RoxygenNote: 7.1.1
Encoding: UTF-8
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ S3method(print,api)
S3method(print,api_grid)
S3method(print,api_grid_local)
S3method(print,api_plot)
S3method(print,kaleidoScope)
S3method(print,plotly_data)
S3method(to_basic,GeomAbline)
S3method(to_basic,GeomAnnotationMap)
Expand Down Expand Up @@ -136,6 +137,7 @@ export(hide_guides)
export(hide_legend)
export(highlight)
export(highlight_key)
export(kaleido)
export(knit_print.api_grid)
export(knit_print.api_grid_local)
export(knit_print.api_plot)
Expand Down Expand Up @@ -260,6 +262,7 @@ importFrom(tidyr,unnest)
importFrom(tools,file_ext)
importFrom(tools,file_path_sans_ext)
importFrom(utils,browseURL)
importFrom(utils,capture.output)
importFrom(utils,data)
importFrom(utils,file.edit)
importFrom(utils,getFromNamespace)
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

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

## New Features

* 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)

## Improvements

* `ggplotly()` now better positions axis titles for `facet_wrap()`/`facet_grid()`. (#1975)
Expand Down
132 changes: 132 additions & 0 deletions R/kaleido.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#' Static image exporting via kaleido
#'
#' Static image exporting via [the kaleido python
#' package](https://github.com/plotly/Kaleido/). `kaleido()` imports
#' kaleido into a \pkg{reticulate}d Python session and returns a `$transform()`
#' method for converting R plots into static images (see examples below).
#'
#' @section Installation:
#'
#' `kaleido()` requires [the kaleido python
#' package](https://github.com/plotly/Kaleido/) to be usable via the \pkg{reticulate} package. Here is a recommended way to do the installation:
#'
#' ```
#' install.packages('reticulate')
#' reticulate::install_miniconda()
#' reticulate::conda_install('r-reticulate', 'python-kaleido')
#' reticulate::conda_install('r-reticulate', 'plotly', channel = 'plotly')
#' reticulate::use_miniconda('r-reticulate')
#' ```
#'
#' @param ... not currently used.
#' @export
#' @return an environment which contains:
#' * `transform()`: a function to convert plots objects into static images,
#' with the following arguments:
#' * `p`: a plot object.
#' * `file`: a file path with a suitable file extension (png, jpg, jpeg,
#' webp, svg, or pdf).
#' * `width`, `height`: The width/height of the exported image in layout
#' pixels. If `scale` is 1, this will also be the width/height of the
#' exported image in physical pixels.
#' * `scale`: The scale factor to use when exporting the figure. A scale
#' factor larger than 1.0 will increase the image resolution with
#' respect to the figure's layout pixel dimensions. Whereas as
#' scale factor of less than 1.0 will decrease the image resolution.
#' * `shutdown()`: a function for shutting down any currently running subprocesses
#' that were launched via `transform()`
#' * `scope`: a reference to the underlying `kaleido.scopes.plotly.PlotlyScope`
#' python object. Modify this object to customize the underlying Chromium
#' subprocess and/or configure other details such as URL to plotly.js, MathJax, etc.
#' @examples
#'
#' \dontrun{
#' scope <- kaleido()
#' tmp <- tempfile(fileext = ".png")
#' scope$transform(plot_ly(x = 1:10), tmp)
#' file.show(tmp)
#' # Remove and garbage collect to remove
#' # R/Python objects and shutdown subprocesses
#' rm(scope); gc()
#' }
#'
kaleido <- function(...) {
if (!rlang::is_installed("reticulate")) {
stop("`kaleido()` requires the reticulate package.")
}
if (!reticulate::py_available(initialize = TRUE)) {
stop("`kaleido()` requires `reticulate::py_available()` to be `TRUE`. Do you need to install python?")
}

py <- reticulate::py
scope_name <- paste0("scope_", new_id())
py[[scope_name]] <- reticulate::import("kaleido")$scopes$plotly$PlotlyScope(
plotlyjs = plotlyMainBundlePath()
)

scope <- py[[scope_name]]

mapbox <- Sys.getenv("MAPBOX_TOKEN", NA)
if (!is.na(mapbox)) {
scope$mapbox_access_token <- mapbox
}

res <- list2env(list(
scope = scope,
# https://github.com/plotly/Kaleido/blob/6a46ecae/repos/kaleido/py/kaleido/scopes/plotly.py#L78-L106
transform = function(p, file = "figure.png", width = NULL, height = NULL, scale = NULL) {
# Perform JSON conversion exactly how the R package would do it
# (this is essentially plotly_json(), without the additional unneeded info)
# and attach as an attribute on the python scope object
scope[["_last_plot"]] <- to_JSON(
plotly_build(p)$x[c("data", "layout", "config")]
)
# On the python side, _last_plot is a string, so use json.loads() to
# convert to dict(). This should be fine since json is a dependency of the
# BaseScope() https://github.com/plotly/Kaleido/blob/586be5/repos/kaleido/py/kaleido/scopes/base.py#L2
transform_cmd <- sprintf(
"%s.transform(sys.modules['json'].loads(%s._last_plot), format='%s', width=%s, height=%s, scale=%s)",
scope_name, scope_name, tools::file_ext(file),
reticulate::r_to_py(width), reticulate::r_to_py(height),
reticulate::r_to_py(scale)
)
# Write the base64 encoded string that transform() returns to disk
# https://github.com/plotly/Kaleido/blame/master/README.md#L52
reticulate::py_run_string(
sprintf("open('%s', 'wb').write(%s)", file, transform_cmd)
)
},
# Shutdown the kaleido subprocesses
# https://github.com/plotly/Kaleido/blob/586be5c/repos/kaleido/py/kaleido/scopes/base.py#L71-L72
shutdown = function() {
reticulate::py_run_string(paste0(scope_name, ".__del__()"))
}
))

# Shutdown subprocesses and delete python scope when
# this object is garbage collected by R
reg.finalizer(res, onexit = TRUE, function(x) {
x$shutdown()
reticulate::py_run_string(paste("del", scope_name))
})

class(res) <- "kaleidoScope"
res
}


#' Print method for kaleido
#'
#' S3 method for [kaleido()].
#'
#' @param x a [kaleido()] object.
#' @param ... currently unused.
#' @export
#' @importFrom utils capture.output
#' @keywords internal
print.kaleidoScope <- function(x, ...) {
args <- formals(x$transform)
cat("$transform: function(", paste(names(args), collapse = ", "), ")\n", sep = "")
cat("$shutdown: function()\n")
cat("$scope: ", utils::capture.output(x$scope))
}
29 changes: 8 additions & 21 deletions R/orca.R
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
#' Static image exporting
#' Static image exporting via orca
#'
#' Export plotly objects to static images (e.g., pdf, png, jpeg, svg, etc) via the
#' [orca command-line utility](https://github.com/plotly/orca#installation).
#'
#' The `orca()` function is designed for exporting one plotly graph whereas `orca_serve()`
#' is meant for exporting many graphs at once. The former starts and stops an external (nodejs)
#' process everytime it is called whereas the latter starts up a process when called, then
#' returns an `export()` method for exporting graphs as well as a `close()` method for stopping
#' the external (background) process.
#' Superseded by [kaleido()].
#'
#' @param p a plotly object.
#' @param file output filename.
Expand Down Expand Up @@ -67,17 +60,14 @@ orca <- function(p, file = "plot.png", format = tools::file_ext(file),
parallel_limit = NULL, verbose = FALSE, debug = FALSE,
safe = FALSE, more_args = NULL, ...) {

.Deprecated("kaleido")

orca_available()

b <- plotly_build(p)

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

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

.Deprecated("kaleido")

# make sure we have the required infrastructure
orca_available()
try_library("processx", "orca_serve")

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

args <- c(
"serve",
Expand Down
9 changes: 9 additions & 0 deletions R/plotly.R
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,15 @@ plotlyMainBundle <- function() {
)
}

plotlyMainBundlePath <- function() {
dep <- plotlyMainBundle()
path <- file.path(dep$src$file, dep$script)
if (!is.null(dep$package)) {
path <- system.file(path, package = dep$package)
}
path
}

plotlyHtmlwidgetsCSS <- function() {
htmltools::htmlDependency(
name = "plotly-htmlwidgets-css",
Expand Down
64 changes: 64 additions & 0 deletions man/kaleido.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading