Skip to content

Commit 745e159

Browse files
committed
Add orca_serve R6 class for bulk image exporting
1 parent b5034a4 commit 745e159

File tree

3 files changed

+154
-10
lines changed

3 files changed

+154
-10
lines changed

DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ Imports:
4444
crosstalk,
4545
purrr,
4646
data.table,
47-
promises
47+
promises,
48+
R6
4849
Suggests:
4950
MASS,
5051
maps,

R/imports.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#' @importFrom tidyr unnest
77
#' @importFrom viridisLite viridis
88
#' @importFrom jsonlite toJSON fromJSON
9-
#' @importFrom httr GET POST PATCH content config add_headers stop_for_status
9+
#' @importFrom httr GET POST PATCH content config add_headers stop_for_status warn_for_status
1010
#' @importFrom htmlwidgets createWidget sizingPolicy saveWidget onRender prependContent
1111
#' @importFrom lazyeval f_eval is_formula all_dots is_lang f_new
1212
#' @importFrom tibble as_tibble

R/orca.R

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#' hang during image generating are skipped.
2323
#' @export
2424
#' @author Carson Sievert
25+
#' @seealso [orca_serve]
2526
#' @examples
2627
#'
2728
#' \dontrun{
@@ -35,13 +36,7 @@ orca <- function(p, file = "plot.png", format = tools::file_ext(file),
3536
parallel_limit = NULL, verbose = FALSE, debug = FALSE,
3637
safe = FALSE) {
3738

38-
if (Sys.which("orca") == "") {
39-
stop(
40-
"The orca command-line utility is required to use the `orca()` function.\n\n",
41-
"Follow the installation instructions here -- https://github.com/plotly/orca#installation",
42-
call. = FALSE
43-
)
44-
}
39+
orca_available()
4540

4641
b <- plotly_build(p)
4742

@@ -68,8 +63,156 @@ orca <- function(p, file = "plot.png", format = tools::file_ext(file),
6863
if (!is.null(parallel_limit)) args <- c(args, "--parallel-limit", parallel_limit)
6964
if (!is.null(tryNULL(mapbox_token()))) args <- c(args, "--mapbox-access-token", mapbox_token())
7065
if (isTRUE(mathjax)) args <- c(args, "--mathjax", file.path(mathjax_path(), "MathJax.js"))
71-
66+
7267
# TODO: point to local topojson? Should this only work if plot_geo(standalone = TRUE)?
7368
try_library("processx", "orca")
7469
invisible(processx::run("orca", args, echo = TRUE, spinner = TRUE))
7570
}
71+
72+
#' Orca image export server
73+
#'
74+
#' @description Compared to [orca], [orca_serve] is more efficient at exporting many plotly
75+
#' graphs because the former must startup/shutdown an external process for every image.
76+
#' The server (background) process is launched upon initialization of a [orca_serve] class
77+
#' (i.e., when the `new()` method is called). The `export()` method accepts any valid plotly
78+
#' object as input and spits out an image file to disk. To kill the background server process,
79+
#' use `close()`.
80+
#'
81+
#' @section Constructor:
82+
#'
83+
#' \code{orca_serve$new(port = 5151)}
84+
#'
85+
#' \describe{
86+
#' \item{\code{port}}{Sets the server's port number.}
87+
#' \item{\code{mathjax}}{
88+
#' whether or not to include MathJax (required to render [TeX]).
89+
#' If `TRUE`, the PLOTLY_MATHJAX_PATH environment variable must be set and point
90+
#' to the location of MathJax (this variable is also used to render [TeX] in
91+
#' interactive graphs, see [config]).
92+
#' }
93+
#' \item{\code{safe}}{
94+
#' Turns on safe mode: where figures likely to make browser window hang during image generating are skipped.
95+
#' }
96+
#' \item{\code{request_limit}}{
97+
#' Sets a request limit that makes orca exit when reached.
98+
#' }
99+
#' \item{\code{keep_alive}}{
100+
#' Turn on keep alive mode where orca will (try to) relaunch server if process unexpectedly exits.
101+
#' }
102+
#' \item{\code{window_max_number}}{
103+
#' Sets maximum number of browser windows the server can keep open at a given time.
104+
#' }
105+
#' \item{\code{quiet}}{Suppress all logging info.}
106+
#' \item{\code{debug}}{Starts app in debug mode.}
107+
#' }
108+
#'
109+
#' @export
110+
#' @author Carson Sievert
111+
#' @seealso [orca]
112+
#' @examples
113+
#'
114+
#' \dontrun{
115+
#' # launch the server
116+
#' server <- orca_serve$new()
117+
#'
118+
#' # export as many graphs as you'd like
119+
#' server$export(qplot(1:10), "test1.pdf")
120+
#' server$export(plot_ly(x = 1:10, y = 1:10), "test2.pdf")
121+
#'
122+
#' # the underlying process is exposed as a field, so you
123+
#' # have full control over the external process
124+
#' server$process$is_alive()
125+
#'
126+
#' # convenience method for closing down the server
127+
#' server$close()
128+
#'
129+
#' # remove the exported files from disk
130+
#' unlink("test1.pdf")
131+
#' unlink("test2.pdf")
132+
#' }
133+
#'
134+
orca_serve <- R6::R6Class(
135+
"orcaServe",
136+
public = list(
137+
process = NULL,
138+
port = NULL,
139+
140+
initialize = function(port = 5151, mathjax = FALSE, safe = FALSE, request_limit = NULL,
141+
keep_alive = TRUE, window_max_number = NULL, quiet = FALSE,
142+
debug = FALSE, ...) {
143+
144+
# make sure we have the required infrastructure
145+
orca_available()
146+
try_library("processx", "orca")
147+
148+
# use main bundle since any plot can be thrown at the server
149+
plotlyjs <- plotlyMainBundle()
150+
plotlyjs_file <- file.path(plotlyjs$src$file, plotlyjs$script)
151+
152+
args <- c(
153+
"serve",
154+
"-p", port,
155+
"--plotly", plotlyjs_file,
156+
if (safe) "--safe-mode",
157+
if (orca_version() >= "1.1.1") "--graph-only",
158+
if (keep_alive) "--keep-alive",
159+
if (debug) "--debug",
160+
if (quiet) "--quiet"
161+
)
162+
163+
if (!is.null(request_limit))
164+
args <- c(args, "--request-limit", request_limit)
165+
166+
if (!is.null(window_max_number))
167+
args <- c(args, "--window-max-number", window_max_number)
168+
169+
if (!is.null(tryNULL(mapbox_token())))
170+
args <- c(args, "--mapbox-access-token", mapbox_token())
171+
172+
if (isTRUE(mathjax))
173+
args <- c(args, "--mathjax", file.path(mathjax_path(), "MathJax.js"))
174+
175+
self$process <- processx::process$new("orca", args, ...)
176+
self$port <- port
177+
},
178+
export = function(p, file = "plot.png", format = tools::file_ext(file), width = 1000, height = 500) {
179+
180+
# request/response model works similarly to plotly_IMAGE()
181+
bod <- list(
182+
figure = plotly_build(p)$x[c("data", "layout")],
183+
format = format,
184+
width = width,
185+
height = height,
186+
scale = scale
187+
)
188+
res <- httr::POST(
189+
paste0("http://localhost:", self$port),
190+
body = to_JSON(bod)
191+
)
192+
httr::stop_for_status(res)
193+
httr::warn_for_status(res)
194+
con <- httr::content(res, as = "raw")
195+
writeBin(con, file)
196+
},
197+
close = function() {
198+
self$process$kill()
199+
}
200+
)
201+
)
202+
203+
orca_available <- function() {
204+
if (Sys.which("orca") == "") {
205+
stop(
206+
"The orca command-line utility is required for this functionality.\n\n",
207+
"Please follow the installation instructions here -- https://github.com/plotly/orca#installation",
208+
call. = FALSE
209+
)
210+
}
211+
212+
TRUE
213+
}
214+
215+
orca_version <- function() {
216+
orca_available()
217+
as.package_version(system("orca --version", intern = TRUE))
218+
}

0 commit comments

Comments
 (0)