diff --git a/DESCRIPTION b/DESCRIPTION index e0f89e63..d7b37ef9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: tinyplot Type: Package Title: Lightweight Extension of the Base R Graphics System -Version: 0.0.5.9005 +Version: 0.0.5.9006 Authors@R: c( person( @@ -41,6 +41,7 @@ Imports: grDevices, methods, stats, + tools, utils Suggests: altdoc (>= 0.3.0), diff --git a/NAMESPACE b/NAMESPACE index bb759e56..722beb81 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,13 +10,20 @@ export(tpar) importFrom(grDevices,adjustcolor) importFrom(grDevices,as.raster) importFrom(grDevices,colorRampPalette) +importFrom(grDevices,dev.list) +importFrom(grDevices,dev.new) +importFrom(grDevices,dev.off) importFrom(grDevices,extendrange) importFrom(grDevices,hcl.colors) importFrom(grDevices,hcl.pals) +importFrom(grDevices,jpeg) importFrom(grDevices,palette) importFrom(grDevices,palette.colors) importFrom(grDevices,palette.pals) +importFrom(grDevices,pdf) +importFrom(grDevices,png) importFrom(grDevices,recordGraphics) +importFrom(grDevices,svg) importFrom(grDevices,xy.coords) importFrom(graphics,Axis) importFrom(graphics,abline) @@ -45,6 +52,7 @@ importFrom(stats,model.frame) importFrom(stats,na.omit) importFrom(stats,terms) importFrom(stats,update) +importFrom(tools,file_ext) importFrom(utils,head) importFrom(utils,modifyList) importFrom(utils,tail) diff --git a/NEWS.md b/NEWS.md index 649c6b7c..bf6d509e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # News -## 0.0.5.9005 (development version) +## 0.0.5.9006 (development version) License: @@ -23,6 +23,17 @@ automatically vary line widths by group. (#134 @grantmcdermott) - `tpar()` now accepts standard `par()` arguments in addition to the `tinyplot`-specific ones. This allows users to set or query graphical parameters via a single convenience function, instead having to invoke `tpar` and `par` separately. +- Users can write plots directly to disk using the new `file` argument, +alongside corresponding `width` and `height` arguments for output customization +(both of which are defined in inches). For example, +`tinyplot(..., file = "~/myplot.png", width = 8, height = 5)`. This +implementation relies on a simple internal wrapper around the traditional R +external graphics devices like `png()`, `pdf()`, etc. But it may prove more +convenient, since the current global graphics parameters held in `(t)par()` are +carried over to the external device too and don't need to be reset. Note that +the appropriate device type is determined automatically by the file extension, +which must be one of ".png", ".jpg" (".jpeg"), ".pdf", or ".svg". (#143 +@grantmcdermott) Bug fixes: diff --git a/R/tinyplot.R b/R/tinyplot.R index 3428cae1..00be9975 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -203,6 +203,34 @@ #' @param add logical. If TRUE, then elements are added to the current plot rather #' than drawing a new plot window. Note that the automatic legend for the #' added elements will be turned off. +#' @param file character string giving the file path for writing a plot to disk. +#' If specified, the plot will not be displayed interactively, but rather sent +#' to the appropriate external graphics device (i.e., +#' \code{\link[grDevices]{png}}, \code{\link[grDevices]{jpeg}}, +#' \code{\link[grDevices]{pdf}}, or \code{\link[grDevices]{svg}}). As a point +#' of convenience, note that any global parameters held in `(t)par` are +#' automatically carried over to the external device and don't need to be +#' reset (in contrast to the conventional base R approach that requires +#' manually opening and closing the device). The device type is determined by +#' the file extension at the end of the provided path, and must be one of +#' ".png", ".jpg" (".jpeg"), ".pdf", or ".svg". (Other file types may be +#' supported in the future.) The file dimensions can be controlled by the +#' corresponding `width` and `height` arguments below, otherwise will fall +#' back to the `"file.width"` and `"file.height"` values held in +#' \code{\link[tinyplot]{tpar}} (i.e., both defaulting to 7 inches, and where +#' the default resolution for bitmap files is also specified as 300 +#' DPI). +#' @param width numeric giving the plot width in inches. Together with `height`, +#' typically used in conjunction with the `file` argument above, overriding the +#' default values held in `tpar("file.width", "file.height")`. If either `width` +#' or `height` is specified, but a corresponding `file` argument is not +#' provided as well, then a new interactive graphics device dimensions will be +#' opened along the given dimensions. Note that this interactive resizing may +#' not work consistently from within an IDE like RStudio that has an integrated +#' graphics windows. +#' @param height numeric giving the plot height in inches. Same considerations as +#' `width` (above) apply, e.g. will default to `tpar("file.height")` if not +#' specified. #' @param ... other graphical parameters. See \code{\link[graphics]{par}} or #' the "Details" section of \code{\link[graphics]{plot}}. #' @@ -213,10 +241,11 @@ #' out existing `plot` calls for `tinyplot` (or its shorthand alias `plt`), #' without causing unexpected changes to the output. #' -#' @importFrom grDevices adjustcolor colorRampPalette extendrange palette palette.colors palette.pals hcl.colors hcl.pals xy.coords +#' @importFrom grDevices adjustcolor colorRampPalette extendrange palette palette.colors palette.pals hcl.colors hcl.pals xy.coords png jpeg pdf svg dev.off dev.new dev.list #' @importFrom graphics abline arrows axis Axis box grconvertX grconvertY lines par plot.default plot.new plot.window points polygon segments title mtext text rect #' @importFrom utils modifyList head tail #' @importFrom stats na.omit +#' @importFrom tools file_ext #' #' @examples #' @@ -411,10 +440,58 @@ tinyplot.default = function( ymax = NULL, ribbon_alpha = 0.2, add = FALSE, + file = NULL, + width = NULL, + height = NULL, ...) { dots = list(...) + + # Write plot to output file (if requested) + if (!is.null(file)) { + filepath = file + filewidth = width + fileheight = height + if (is.null(filewidth)) filewidth = .tpar[["file.width"]] + if (is.null(fileheight)) fileheight = .tpar[["file.height"]] + fileres = .tpar[["file.res"]] + # catch to close interactive device if one isn't already open + fkdev = is.null(dev.list()) + # grab existing device pars to pass on to next one + dop = par(no.readonly = TRUE) + # close interactive device if not already open + if (isTRUE(fkdev)) dev.off() + exttype = file_ext(filepath) + if (exttype == "jpg") exttype = "jpeg" + switch(exttype, + png = png(filepath, width = filewidth, height = fileheight, units = "in", res = fileres), + jpeg = jpeg(filepath, width = filewidth, height = fileheight, units = "in", res = fileres), + pdf = pdf(filepath, width = filewidth, height = fileheight), + svg = svg(filepath, width = filewidth, height = fileheight), + stop("\nUnsupported file extension. Only '.png', '.jpg', '.pdf', or '.svg' are allowed.\n") + ) + dop$new = FALSE # catch for some interfaces + par(dop) + on.exit(dev.off(), add = TRUE) + # else statement below for interactive plot with user-specified width/height + } else if (!is.null(width) || !is.null(height)) { + devwidth = width + devheight = height + # if one of width or height is missing, set equal to the other + if (is.null(devwidth)) devwidth = devheight + if (is.null(devheight)) devheight = devwidth + # catch to close interactive device if one isn't already open + fkdev = is.null(dev.list()) + # grab existing device pars to pass on to next one + dop = par(no.readonly = TRUE) + # close interactive device if not already open + if (isTRUE(fkdev)) dev.off() + dev.new(width = devwidth, height = devheight) + dop$new = FALSE # catch for some interfaces + par(dop) + } + # Adding to the previous plot? if (isTRUE(add)) { legend = FALSE # main = sub = xlab = ylab = NULL ## Rather do this later @@ -423,6 +500,9 @@ tinyplot.default = function( # Save current graphical parameters opar = par(no.readonly = TRUE) if (par_restore || !is.null(facet)) { + if (!is.null(file) || !is.null(width) || !is.null(height)) { + opar$new = FALSE # catch for some interfaces + } on.exit(par(opar), add = TRUE) } @@ -1247,6 +1327,7 @@ tinyplot.default = function( last_facet_par = par(no.readonly = TRUE) set_last_facet_par(last_facet_par) } + } diff --git a/R/tpar.R b/R/tpar.R index b1162771..e8671bf5 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -44,6 +44,15 @@ #' `facet.border` \tab\tab Character or integer specifying the facet border colour. If an integer, will correspond to the users default colour palette (see \code{\link[grDevices]{palette}}). Passed \code{\link[graphics]{rect}}. Defaults to `NA` (none).\cr #' \tab\tab\cr #' \tab\tab\cr +#' `file.height` \tab\tab Numeric specifying the height (in inches) of any plot that is written to disk using the `tinyplot(..., file = X)` argument. Defaults to 7.\cr +#' \tab\tab\cr +#' \tab\tab\cr +#' `file.width` \tab\tab Numeric specifying the width (in inches) of any plot that is written to disk using the `tinyplot(..., file = X)` argument. Defaults to 7.\cr +#' \tab\tab\cr +#' \tab\tab\cr +#' `file.res` \tab\tab Numeric specifying the resolution (in dots per square inch) of any plot that is written to disk in bitmap format (i.e., PNG or JPEG) using the `tinyplot(..., file = X)` argument. Defaults to 300.\cr +#' \tab\tab\cr +#' \tab\tab\cr #' `fmar` \tab\tab A numeric vector of form `c(b,l,t,r)` for controlling the (base) margin padding, in terms of lines, between the individual facets in a faceted plot. Defaults to `c(1,1,1,1)`, i.e. a single line of padding around each facet. If more that three facets are detected, the `fmar` parameter is scaled by 0.75 (i.e., three-quarters) to reduce the excess whitespace that would otherwise arise due to the absent axes lines and labels. (An exception is made for 2x2 plots to better match the `cex` expansion logic of the base graphics system under this particular layout.) Similarly, note that an extra 0.5 lines is subtracted from each side of the facet padding for plots that aren't framed, to reduce excess whitespace.\cr #' \tab\tab\cr #' \tab\tab\cr @@ -149,6 +158,24 @@ tpar = function(...) { .tpar$facet.border = facet.border } + if (length(opts$file.width)) { + file.width = as.numeric(opts$file.width) + if(!is.numeric(file.width)) stop("file.width needs to be numeric") + .tpar$file.width = file.width + } + + if (length(opts$file.height)) { + file.height = as.numeric(opts$file.height) + if(!is.numeric(file.height)) stop("file.height needs to be numeric") + .tpar$file.height = file.height + } + + if (length(opts$file.res)) { + file.res = as.numeric(opts$file.res) + if(!is.numeric(file.res)) stop("file.res needs to be numeric") + .tpar$file.res = file.res + } + if (length(opts$fmar)) { fmar = as.numeric(opts$fmar) if(!is.numeric(fmar)) stop("fmar needs to be numeric") diff --git a/R/zzz.R b/R/zzz.R index 8e618f9b..4afbf529 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -6,6 +6,11 @@ assign(".tinyplot_env", new.env(), envir = tnypltptns) .tpar <- new.env() + # Figure output options if written to file + .tpar$file.width <- if(is.null(getOption("tinyplot_file.width"))) 7 else as.numeric(getOption("tinyplot_file.width")) + .tpar$file.height <- if(is.null(getOption("tinyplot_file.height"))) 7 else as.numeric(getOption("tinyplot_file.height")) + .tpar$file.res <- if(is.null(getOption("tinyplot_file.res"))) 300 else as.numeric(getOption("tinyplot_file.res")) + # Facet margin, i.e. gap between the individual facet windows .tpar$fmar <- if(is.null(getOption("tinyplot_fmar"))) c(1,1,1,1) else as.numeric(getOption("tinyplot_fmar")) diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index 529f841a..fb0e384e 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -44,6 +44,9 @@ tinyplot(x, ...) ymax = NULL, ribbon_alpha = 0.2, add = FALSE, + file = NULL, + width = NULL, + height = NULL, ... ) @@ -341,6 +344,37 @@ plot (since filled density plots are converted to ribbon plots internally).} than drawing a new plot window. Note that the automatic legend for the added elements will be turned off.} +\item{file}{character string giving the file path for writing a plot to disk. +If specified, the plot will not be displayed interactively, but rather sent +to the appropriate external graphics device (i.e., +\code{\link[grDevices]{png}}, \code{\link[grDevices]{jpeg}}, +\code{\link[grDevices]{pdf}}, or \code{\link[grDevices]{svg}}). As a point +of convenience, note that any global parameters held in \verb{(t)par} are +automatically carried over to the external device and don't need to be +reset (in contrast to the conventional base R approach that requires +manually opening and closing the device). The device type is determined by +the file extension at the end of the provided path, and must be one of +".png", ".jpg" (".jpeg"), ".pdf", or ".svg". (Other file types may be +supported in the future.) The file dimensions can be controlled by the +corresponding \code{width} and \code{height} arguments below, otherwise will fall +back to the \code{"file.width"} and \code{"file.height"} values held in +\code{\link[tinyplot]{tpar}} (i.e., both defaulting to 7 inches, and where +the default resolution for bitmap files is also specified as 300 +DPI).} + +\item{width}{numeric giving the plot width in inches. Together with \code{height}, +typically used in conjunction with the \code{file} argument above, overriding the +default values held in \code{tpar("file.width", "file.height")}. If either \code{width} +or \code{height} is specified, but a corresponding \code{file} argument is not +provided as well, then a new interactive graphics device dimensions will be +opened along the given dimensions. Note that this interactive resizing may +not work consistently from within an IDE like RStudio that has an integrated +graphics windows.} + +\item{height}{numeric giving the plot height in inches. Same considerations as +\code{width} (above) apply, e.g. will default to \code{tpar("file.height")} if not +specified.} + \item{formula}{a \code{formula} that optionally includes grouping variable(s) after a vertical bar, e.g. \code{y ~ x | z}. One-sided formulae are also permitted, e.g. \code{~ y | z}. Note that the \code{formula} and \code{x} arguments diff --git a/man/tpar.Rd b/man/tpar.Rd index a5df427c..38a8b25c 100644 --- a/man/tpar.Rd +++ b/man/tpar.Rd @@ -53,6 +53,15 @@ parameters that satisify \code{par(..., no.readonly = TRUE)} are evaluated. \code{facet.border} \tab\tab Character or integer specifying the facet border colour. If an integer, will correspond to the users default colour palette (see \code{\link[grDevices]{palette}}). Passed \code{\link[graphics]{rect}}. Defaults to \code{NA} (none).\cr \tab\tab\cr \tab\tab\cr +\code{file.height} \tab\tab Numeric specifying the height (in inches) of any plot that is written to disk using the \code{tinyplot(..., file = X)} argument. Defaults to 7.\cr +\tab\tab\cr +\tab\tab\cr +\code{file.width} \tab\tab Numeric specifying the width (in inches) of any plot that is written to disk using the \code{tinyplot(..., file = X)} argument. Defaults to 7.\cr +\tab\tab\cr +\tab\tab\cr +\code{file.res} \tab\tab Numeric specifying the resolution (in dots per square inch) of any plot that is written to disk in bitmap format (i.e., PNG or JPEG) using the \code{tinyplot(..., file = X)} argument. Defaults to 300.\cr +\tab\tab\cr +\tab\tab\cr \code{fmar} \tab\tab A numeric vector of form \code{c(b,l,t,r)} for controlling the (base) margin padding, in terms of lines, between the individual facets in a faceted plot. Defaults to \code{c(1,1,1,1)}, i.e. a single line of padding around each facet. If more that three facets are detected, the \code{fmar} parameter is scaled by 0.75 (i.e., three-quarters) to reduce the excess whitespace that would otherwise arise due to the absent axes lines and labels. (An exception is made for 2x2 plots to better match the \code{cex} expansion logic of the base graphics system under this particular layout.) Similarly, note that an extra 0.5 lines is subtracted from each side of the facet padding for plots that aren't framed, to reduce excess whitespace.\cr \tab\tab\cr \tab\tab\cr