blob: b74d439747f6e81630680ed621764eecdb142d60 [file] [log] [blame]
Bill Evansb62414a2020-09-14 12:33:38 -07001#' Helper functions to use various graphics devices
2#'
3#' These helper functions generalize the use of strings (e.g.,
4#' `"svg"`, `"pdf"`) or graphic device functions (e.g.,
5#' `grDevices::svg`, `grDevices::pdf`) for in-table plots.
6#'
7#' @param filename Passed through to the graphics device.
8#' @param width,height Plot dimensions in pixels.
9#' @param res The resolution of the plot; default is 300.
10#' @param ... extra parameters passing to the graphics-device function.
11#' @param dev Character (e.g., "svg", "pdf") or function (e.g.,
12#' `grDevices::svg`, `grDevices::pdf`).
13#' @name graphics_helpers
14NULL
15
16#' @describeIn graphics_helpers Generalize 'res' and 'filename across dev functions
17#' @details
18#' - `graphics_dev` generalizes the use of 'res' and plot dimensions
19#' across graphic devices. Raster-based devices (e.g., 'png',
20#' 'jpeg', 'tiff', 'bmp') tend to use 'res' and the width/height
21#' units default to pixels. All other devices (e.g., 'pdf', 'svg')
22#' tend to use inches as the default units for width/height, and
23#' error when 'res' is provided.
24#'
25#' The current heuristic is the look for the 'res' argument in the
26#' function's formals; if that is present, then it is assumed that
27#' the default units are in pixels, so 'width', 'height', and 'res'
28#' are passed through unmodified. If 'res' is not present, then
29#' 'width' and 'height' are converted from pixels to inches, and
30#' 'res' is not passed to the function
31#'
32#' Another purpose of this function is to generalize the different
33#' graphic functions' use of 'file=' versus 'filename='.
34#' @return 'graphics_dev': nothing, a plot device is opened
35graphics_dev <- function(filename, width, height, res, ..., dev) {
36 dev <- match.fun(dev)
37 frmls <- names(formals(dev))
38 dots <- list(...)
39 if ("res" %in% frmls) {
40 dots <- c(dots, list(width = width, height = height, res = res))
41 } else {
42 dots <- c(dots, list(width = width / res, height = height / res))
43 }
44 filenames <- c("file", "filename")
45 found <- na.omit(match(frmls, filenames))
46 if (length(found)) {
47 dots <- c(dots, setNames(filename, filenames[ found ]))
48 } else {
49 stop("could not find a 'file' argument in graphics dev")
50 }
51 do.call(dev, dots)
52}
53
54#' @describeIn graphics_helpers Determine if plot device is svg-like
55#' @details
56#' - `is_svg` determines if the plot device is svg-like, typically one
57#' of `"svg", `grDevices::svg`, or `svglite::svglite`
58#' @return 'is_svg': logical
59is_svg <- function(dev) {
60 if (is.character(dev)) {
61 return(grepl("svg", dev))
62 }
63 if (is.function(dev)) {
64 return(any(sapply(formals(dev), function(f) {
65 tryCatch(any(grepl("svg", as.character(f))),
66 error = function(e) FALSE)
67 })))
68 }
69 stop("unrecognized graphics 'dev': ", paste(class(dev), collapse = ","))
70}
71
72#' @describeIn graphics_helpers Determine filename extension
73#' @details
74#'
75#' - `dev_chr` determines the filename extension for the applicable
76#' plot function; when `dev` is a string, then it is returned
77#' unchanged; when `dev` is a function, the formals of the function
78#' are checked for clues (i.e., default value of a `file=` argument)
79#' @return `dev_chr`: character
80#' @importFrom tools file_ext
81dev_chr <- function(dev) {
82 ext <- ""
83 if (is.character(dev)) {
84 ext <- if (dev == "svglite") "svg" else dev
85 } else if (is.function(dev)) {
86 frmls <- formals(dev)
87 filearg <- grep("^file(name)?$", names(frmls), value = TRUE)
88 if (length(filearg)) {
89 ext <- grep("\\.[[:alpha:]]+$", unlist(sapply(frmls[filearg], as.character)),
90 value = TRUE)
91 ext <- unique(tools::file_ext(ext))[1]
92 }
93 }
94 if (is.na(ext) || !nzchar(ext)) {
95 warning("could not determine filename extension from graphic device")
96 ext <- ""
97 }
98 return(ext)
99}
100
101#' Combine file (or svg text) and parameters into a 'kableExtraInlinePlots' object
Hao Zhu6fc251d2020-10-18 23:53:07 -0400102#'
Bill Evansb62414a2020-09-14 12:33:38 -0700103#' @param filename Passed through to the graphics device.
104#' @param file_ext Character, something like "png".
105#' @param dev Character (e.g., "svg", "pdf") or function (e.g.,
106#' @param width,height Plot dimensions in pixels.
107#' @param res The resolution of the plot; default is 300.
108#' @param del If the file is svg-like, then the default action is to
109#' read the file into an embedded SVG object; once done, the file is
110#' no longer used. The default action is to delete this file early,
111#' set this to 'FALSE' to keep the file.
112#' @return list object, with class 'kableExtraInlinePlots'
113make_inline_plot <- function(filename, file_ext, dev,
114 width, height, res,
115 del = TRUE) {
Hao Zhu6fc251d2020-10-18 23:53:07 -0400116 if ((is_svg(file_ext) || is_svg(dev))) {
Bill Evansb62414a2020-09-14 12:33:38 -0700117 svg_xml <- xml2::read_xml(filename)
118 svg_text <- as.character(svg_xml)
119 if (del) {
120 unlink(filename)
121 filename <- character(0)
122 }
123 } else {
124 if (!is_latex()) {
125 filename <- paste0("file:///", normalizePath(filename, winslash = "/"))
126 }
127 svg_text <- NULL
128 }
129 out <- list(path = filename, dev = file_ext, type = "line",
130 width = width, height = height, res = res,
131 svg_text = svg_text)
132 class(out) <- c("kableExtraInlinePlots", "list")
133 return(out)
134}
135
136#' Convert arguments for a single call into Map-able args
137#'
138#' @param ... Arbitrary arguments to be possibly converted into lists
139#' of arguments.
140#' @param lengths Allowable lengths of the arguments, typically 1 and
141#' the length of the main variable (e.g., "x"). If 'NA' (default),
142#' it is not enforced.
143#' @param passthru Character vector of variables to pass through with
144#' no conversion to lists of values. Extra names (not provided in
145#' `...`) are ignored.
146#' @param notlen1vec Character vector of variables that are known to
147#' be length over 1 for a single plot call, so it will always be
148#' list-ified and extra care to ensure it is grouped correctly.
149#' Extra names (not provided in `...`) are ignored.
150#' @param notlen1lst Character vector of variables that are lists, so
151#' the inner list length is not checked/enforced. (For example, if a
152#' single plot call takes an argument `list(a=1,b=2,d=3)` and the
153#' multi-data call creates three plots, then a naive match might
154#' think that the first plot would get `list(a=1)`, second plot gets
155#' `list(b=2)`, etc. Adding that list-argument to this 'notlen1lst'
156#' will ensure that the full list is passed correctly.) Extra names
157#' (not provided in `...`) are ignored.
158#' @param ignore Character vector of variables to ignore, never
159#' returned. (Generally one can control this by not adding the
160#' variable in the first place, but having this here allows some
161#' sanity checks and/or programmatic usage.)
162#' @return list, generally a list of embedded lists
163listify_args <- function(..., lengths = NA,
164 passthru = c("x", "y"),
165 notlen1vec = c("lim", "xlim", "ylim"),
166 notlen1lst = c("minmax", "min", "max"),
167 ignore = c("same_lim")) {
168 indots <- list(...)
169 dotnms <- sapply(match.call(expand.dots=FALSE)$..., deparse)
170 neednames <- if (is.null(names(indots))) {
171 rep(TRUE, length(indots))
172 } else !nzchar(names(indots))
173 if (any(neednames)) {
174 names(indots)[ neednames ] <- dotnms[ neednames ]
175 }
176 dots <- indots[ intersect(names(indots), passthru) ]
177
178 # these are elements that are not typically length-1, so we need to
179 # listify slightly differently
180 nms <- intersect(names(indots), notlen1vec)
181 if (length(nms)) {
182 dots <- c(dots, Map(
183 function(L, nm) {
184 if (is.null(L)) return(list(NULL))
185 if (!is.list(L)) return(list(L))
Hao Zhuf6b60e82020-10-21 18:58:19 -0400186 if ((length(lengths) == 1 && is.na(lengths)) || length(L) %in% lengths) return(L)
Bill Evansb62414a2020-09-14 12:33:38 -0700187 stop("length of '", nm, "' must be one of: ", paste(lengths, collapse = " or "))
188 }, indots[ nms ], nms))
189 }
190
191 # these are a little special in that the argument must be a list
192 # (regardless of its internal length)
193 nms <- intersect(names(indots), notlen1lst)
194 if (length(nms)) {
195 dots <- c(dots, Map(
196 function(L, nm) {
197 if (is.null(L)) return(list(NULL))
198 if (!length(L)) return(list(list()))
199 if (!is.list(L[[1]])) return (list(L))
Hao Zhuf6b60e82020-10-21 18:58:19 -0400200 if ((length(lengths) == 1 && is.na(lengths)) || length(L) %in% lengths) return(L)
Bill Evansb62414a2020-09-14 12:33:38 -0700201 stop("length of '", nm, "' must be one of: ", paste(lengths, collapse = " or "))
202 }, indots[ nms ], nms))
203 }
204
205 # the remainder, those that we don't know about explicitly and are
206 # not intentionally ignored
207 nms <- setdiff(names(indots), c(passthru, notlen1vec, notlen1lst, ignore))
208 if (length(nms)) {
209 dots <- c(dots, Map(
210 function(V, nm) {
211 if (is.null(V)) return(list(NULL))
212 if (is.function(V)) return(list(V))
Hao Zhuf6b60e82020-10-21 18:58:19 -0400213 if ((length(lengths) == 1 && is.na(lengths)) || length(V) %in% lengths) return(V)
Bill Evansb62414a2020-09-14 12:33:38 -0700214 stop("length of '", nm, "' must be one of: ", paste(lengths, collapse = " or "))
215 }, indots[ nms ], nms))
216 }
217
218 dots
219}