|
| 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 | +} |
0 commit comments