File: graphics_helpers.R

package info (click to toggle)
r-cran-kableextra 1.4.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,400 kB
  • sloc: javascript: 579; makefile: 2
file content (219 lines) | stat: -rw-r--r-- 9,015 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#' Helper functions to use various graphics devices
#'
#' These helper functions generalize the use of strings (e.g.,
#' `"svg"`, `"pdf"`) or graphic device functions (e.g.,
#' `grDevices::svg`, `grDevices::pdf`) for in-table plots.
#'
#' @param filename Passed through to the graphics device.
#' @param width,height Plot dimensions in pixels.
#' @param res The resolution of the plot; default is 300.
#' @param ... extra parameters passing to the graphics-device function.
#' @param dev Character (e.g., "svg", "pdf") or function (e.g.,
#' `grDevices::svg`, `grDevices::pdf`).
#' @name graphics_helpers
NULL

#' @describeIn graphics_helpers Generalize `res` and `filename` across device functions
#' @details
#' - `graphics_dev` generalizes the use of 'res' and plot dimensions
#'   across graphic devices. Raster-based devices (e.g., 'png',
#'   'jpeg', 'tiff', 'bmp') tend to use 'res' and the width/height
#'   units default to pixels. All other devices (e.g., 'pdf', 'svg')
#'   tend to use inches as the default units for width/height, and
#'   error when 'res' is provided.
#'
#'   The current heuristic is the look for the 'res' argument in the
#'   function's formals; if that is present, then it is assumed that
#'   the default units are in pixels, so 'width', 'height', and 'res'
#'   are passed through unmodified. If 'res' is not present, then
#'   'width' and 'height' are converted from pixels to inches, and
#'   'res' is not passed to the function
#'
#'   Another purpose of this function is to generalize the different
#'   graphic functions' use of 'file=' versus 'filename='.
#' @return `graphics_dev`: nothing, a plot device is opened
graphics_dev <- function(filename, width, height, res, ..., dev) {
  dev <- match.fun(dev)
  frmls <- names(formals(dev))
  dots <- list(...)
  if ("res" %in% frmls) {
    dots <- c(dots, list(width = width, height = height, res = res))
  } else {
    dots <- c(dots, list(width = width / res, height = height / res))
  }
  filenames <- c("file", "filename")
  found <- na.omit(match(frmls, filenames))[1]
  if (length(found)) {
    dots <- c(dots, setNames(filename, filenames[ found ]))
  } else {
    stop("could not find a 'file' argument in graphics dev")
  }
  do.call(dev, dots)
}

#' @describeIn graphics_helpers Determine if plot device is svg-like
#' @details
#' - `is_svg` determines if the plot device is svg-like, typically one
#'   of `"svg", `grDevices::svg`, or `svglite::svglite`
#' @return 'is_svg': logical
is_svg <- function(dev) {
  if (is.character(dev)) {
    return(grepl("svg", dev))
  }
  if (is.function(dev)) {
    return(any(sapply(formals(dev), function(f) {
      tryCatch(any(grepl("svg", as.character(f))),
               error = function(e) FALSE)
    })))
  }
  stop("unrecognized graphics 'dev': ", paste(class(dev), collapse = ","))
}

#' @describeIn graphics_helpers Determine filename extension
#' @details
#'
#' - `dev_chr` determines the filename extension for the applicable
#'   plot function; when `dev` is a string, then it is returned
#'   unchanged; when `dev` is a function, the formals of the function
#'   are checked for clues (i.e., default value of a `file=` argument)
#' @return `dev_chr`: character
#' @importFrom tools file_ext
dev_chr <- function(dev) {
  ext <- ""
  if (is.character(dev)) {
    ext <- if (dev == "svglite") "svg" else dev
  } else if (is.function(dev)) {
    frmls <- formals(dev)
    filearg <- grep("^file(name)?$", names(frmls), value = TRUE)
    if (length(filearg)) {
      ext <- grep("\\.[[:alpha:]]+$", unlist(sapply(frmls[filearg], as.character)),
                  value = TRUE)
      ext <- unique(tools::file_ext(ext))[1]
    }
  }
  if (is.na(ext) || !nzchar(ext)) {
    warning("could not determine filename extension from graphic device")
    ext <- ""
  }
  return(ext)
}

#' Combine file (or svg text) and parameters into a `kableExtraInlinePlots` object
#'
#' @param filename Passed through to the graphics device.
#' @param file_ext Character, something like "png".
#' @param dev Character (e.g., "svg", "pdf") or function (e.g.,
#' @param width,height Plot dimensions in pixels.
#' @param res The resolution of the plot; default is 300.
#' @param del If the file is svg-like, then the default action is to
#'   read the file into an embedded SVG object; once done, the file is
#'   no longer used. The default action is to delete this file early,
#'   set this to 'FALSE' to keep the file.
#' @return list object, with class `kableExtraInlinePlots`
make_inline_plot <- function(filename, file_ext, dev,
                             width, height, res,
                             del = TRUE) {
  if ((is_svg(file_ext) || is_svg(dev))) {
    svg_xml <- xml2::read_xml(filename)
    svg_text <- as.character(svg_xml)
    if (del) {
      unlink(filename)
      filename <- character(0)
    }
  } else {
    if (!is_latex()) {
      filename <- paste0("file:///", normalizePath(filename, winslash = "/"))
    }
    svg_text <- NULL
  }
  out <- list(path = filename, dev = file_ext, type = "line",
              width = width, height = height, res = res,
              svg_text = svg_text)
  class(out) <- c("kableExtraInlinePlots", "list")
  return(out)
}

#' Convert arguments for a single call into Map-able args
#'
#' @param ... Arbitrary arguments to be possibly converted into lists
#'   of arguments.
#' @param lengths Allowable lengths of the arguments, typically 1 and
#'   the length of the main variable (e.g., "x"). If  `NA` (default),
#'   it is not enforced.
#' @param passthru Character vector of variables to pass through with
#'   no conversion to lists of values. Extra names (not provided in
#'   `...`) are ignored.
#' @param notlen1vec Character vector of variables that are known to
#'   be length over 1 for a single plot call, so it will always be
#'   list-ified and extra care to ensure it is grouped correctly.
#'   Extra names (not provided in `...`) are ignored.
#' @param notlen1lst Character vector of variables that are lists, so
#'   the inner list length is not checked/enforced. (For example, if a
#'   single plot call takes an argument `list(a=1,b=2,d=3)` and the
#'   multi-data call creates three plots, then a naive match might
#'   think that the first plot would get `list(a=1)`, second plot gets
#'   `list(b=2)`, etc. Adding that list-argument to this 'notlen1lst'
#'   will ensure that the full list is passed correctly.) Extra names
#'   (not provided in `...`) are ignored.
#' @param ignore Character vector of variables to ignore, never
#'   returned. (Generally one can control this by not adding the
#'   variable in the first place, but having this here allows some
#'   sanity checks and/or programmatic usage.)
#' @return list, generally a list of embedded lists
listify_args <- function(..., lengths = NA,
                         passthru = c("x", "y"),
                         notlen1vec = c("lim", "xlim", "ylim"),
                         notlen1lst = c("minmax", "min", "max"),
                         ignore = c("same_lim")) {
  indots <- list(...)
  dotnms <- sapply(match.call(expand.dots=FALSE)$..., deparse)
  neednames <- if (is.null(names(indots))) {
    rep(TRUE, length(indots))
  } else !nzchar(names(indots))
  if (any(neednames)) {
    names(indots)[ neednames ] <- dotnms[ neednames ]
  }
  dots <- indots[ intersect(names(indots), passthru) ]

  # these are elements that are not typically length-1, so we need to
  # listify slightly differently
  nms <- intersect(names(indots), notlen1vec)
  if (length(nms)) {
    dots <- c(dots, Map(
      function(L, nm) {
        if (is.null(L)) return(list(NULL))
        if (!is.list(L)) return(list(L))
        if ((length(lengths) == 1 && is.na(lengths)) || length(L) %in% lengths) return(L)
        stop("length of '", nm, "' must be one of: ", paste(lengths, collapse = " or "))
      }, indots[ nms ], nms))
  }

  # these are a little special in that the argument must be a list
  # (regardless of its internal length)
  nms <- intersect(names(indots), notlen1lst)
  if (length(nms)) {
    dots <- c(dots, Map(
      function(L, nm) {
        if (is.null(L)) return(list(NULL))
        if (!length(L)) return(list(list()))
        if (!is.list(L[[1]])) return (list(L))
        if ((length(lengths) == 1 && is.na(lengths)) || length(L) %in% lengths) return(L)
        stop("length of '", nm, "' must be one of: ", paste(lengths, collapse = " or "))
      }, indots[ nms ], nms))
  }

  # the remainder, those that we don't know about explicitly and are
  # not intentionally ignored
  nms <- setdiff(names(indots), c(passthru, notlen1vec, notlen1lst, ignore))
  if (length(nms)) {
    dots <- c(dots, Map(
      function(V, nm) {
        if (is.null(V)) return(list(NULL))
        if (is.function(V)) return(list(V))
        if ((length(lengths) == 1 && is.na(lengths)) || length(V) %in% lengths) return(V)
        stop("length of '", nm, "' must be one of: ", paste(lengths, collapse = " or "))
      }, indots[ nms ], nms))
  }

  dots
}