Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# ggplot2 3.0.0.9000

* `ggsave()` has a new parameter `preset`, which allows plot `width` and
`height` (and `units`) to be specified at once. It recognizes common paper
sizes (such as "a4" and "letter"), common screen resolutions ("4k"), or a
resolution specification such as "1920x1080". All presets default to
landscape orientation, and portrait can be specified by appending an "r"
(for rotated) to the end of the string. This is similar to the `paper`
option for `grDevices::pdf()`, but with landscape being the default as it's
probably a more common choice for plots (@ilarischeinin).

* `stat_contour()`, `stat_density2d()`, `stat_bin2d()`, `stat_binhex()`
now calculate normalized statistics including `nlevel`, `ndensity`, and
`ncount`. Also, `stat_density()` now includes the calculated statistic
Expand Down
96 changes: 95 additions & 1 deletion R/save.r
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
#' @param limitsize When `TRUE` (the default), `ggsave` will not
#' save images larger than 50x50 inches, to prevent the common error of
#' specifying dimensions in pixels.
#' @param preset A character string to specify `width` and `height` (and
#' `units`) at once. Recognizes common paper sizes (such as "a4" and
#' "letter"), common screen resolutions ("4k"), or a resolution specification
#' such as "1920x1080". All presets default to landscape orientation, and
#' portrait can be specified by appending an "r" (for rotated) to the end of
#' the string. This is similar to the `paper` option for
#' \code{\link[grDevices]{pdf}()}, but with landscape being the default as
#' it's more common. Ignored if `width`/`height` was specified.
#' @param ... Other arguments passed on to the graphics device function,
#' as specified by `device`.
#' @export
Expand All @@ -44,9 +52,21 @@
ggsave <- function(filename, plot = last_plot(),
device = NULL, path = NULL, scale = 1,
width = NA, height = NA, units = c("in", "cm", "mm"),
dpi = 300, limitsize = TRUE, ...) {
dpi = 300, limitsize = TRUE, preset = NULL, ...) {

dpi <- parse_dpi(dpi)
if (!is.null(preset)) {
if (is.na(width) && is.na(height)) {
preset_values <- parse_preset(preset, dpi)
width <- preset_values$width
height <- preset_values$height
units <- preset_values$units
} else {
warning(
"Ignoring 'preset = \"", preset, "\"', as width/height was specified."
)
}
}
dev <- plot_dev(device, filename, dpi = dpi)
dim <- plot_dim(c(width, height), scale = scale, units = units,
limitsize = limitsize)
Expand Down Expand Up @@ -87,6 +107,80 @@ parse_dpi <- function(dpi) {
}
}

# Selected size presets obtained from:
# https://en.wikipedia.org/wiki/Paper_size
# https://en.wikipedia.org/wiki/Computer_display_standard
# These are defined here (instead of inside the function below), so that they
# can be accessed directly from within tests.
ggsave_presets <- tibble::tribble(
~names, ~width, ~height, ~units,
"a3", 420, 297, "mm",
"a4", 297, 210, "mm",
"a5", 210, 148, "mm",
"letter", 11, 8.5, "in",
c("legal", "us"), 14, 8.5, "in",
"executive", 10.5, 7.25, "in",
"hd", 1280, 720, "px",
c("fhd", "full hd"), 1920, 1080, "px",
c("4k", "4h uhd"), 3840, 1920, "px",
c("5k", "5h uhd"), 5120, 2880, "px",
c("8k", "8h uhd"), 5120, 2880, "px",
"vga", 640, 480, "px",
"svga", 800, 600, "px",
"xga", 1024, 768, "px",
"wxga", 1280, 800, "px",
"wxga+", 1440, 900, "px",
"uxga", 1600, 1200, "px",
"wsxga+", 1680, 1050, "px",
"wqxga", 2560, 1600, "px"
)

#' Parse a size preset from the user
#'
#' A preset allows `width` and `height` (and `units`) to be specified at once.
#' It recognizes common paper sizes (such as "a4" and "letter"), common screen
#' resolutions ("4k"), or a resolution specification such as "1920x1080".
#'
#' @param preset Size preset from user
#' @return Named list of width, height, and units
#' @noRd
parse_preset <- function(preset, dpi) {
if (!is.character(preset) || length(preset) != 1) {
stop("preset must be a character string.", call. = FALSE)
}
preset <- tolower(preset)

if (preset %in% unlist(ggsave_presets$names)) {
index <-
sapply(ggsave_presets$names, function(x) preset %in% x)
width <- ggsave_presets$width[index]
height <- ggsave_presets$height[index]
units <- ggsave_presets$units[index]
} else if (sub("r$", "", preset) %in% unlist(ggsave_presets$names)) {
index <-
sapply(ggsave_presets$names, function(x) sub("r$", "", preset) %in% x)
width <- ggsave_presets$height[index]
height <- ggsave_presets$width[index]
units <- ggsave_presets$units[index]
} else if (length(grep("^(\\d+)\\s?x\\s?(\\d+)$", preset)) == 1) {
width <- as.numeric(sub("^(\\d+)\\s?x\\s?(\\d+)$", "\\1", preset))
height <- as.numeric(sub("^(\\d+)\\s?x\\s?(\\d+)$", "\\2", preset))
units <- "px"
} else if (length(grep("^(\\d+)\\s?x\\s?(\\d+)r$", preset)) == 1) {
width <- as.numeric(sub("^(\\d+)\\s?x\\s?(\\d+)r$", "\\2", preset))
height <- as.numeric(sub("^(\\d+)\\s?x\\s?(\\d+)r$", "\\1", preset))
units <- "px"
} else {
stop("Unknown preset: ", preset, call. = FALSE)
}
if (units == "px") {
width <- width / dpi
height <- height / dpi
units = "in"
}
list(width = width, height = height, units = units)
}

plot_dim <- function(dim = c(NA, NA), scale = 1, units = c("in", "cm", "mm"),
limitsize = TRUE) {

Expand Down
11 changes: 10 additions & 1 deletion man/ggsave.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions tests/testthat/test-ggsave.R
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,46 @@ test_that("invalid non-single-string DPI values throw an error", {
expect_error(parse_dpi(c(150, 300)), "DPI must be a single number or string")
expect_error(parse_dpi(list(150)), "DPI must be a single number or string")
})

# parse_preset ----------------------------------------------------------------

test_that("presets are formatted as expected", {
expect_true(all(ggsave_presets$width >= ggsave_presets$height))
landscape_presets <- unlist(ggsave_presets$names)
portrait_presets <- paste0(landscape_presets, "r")
expect_false(any(duplicated(c(landscape_presets, portrait_presets))))
})

test_that("invalid preset values throw an error", {
expect_error(parse_preset(letters), "preset must be a character string")
expect_error(parse_preset(1), "preset must be a character string")
expect_error(parse_preset(factor("a")), "preset must be a character string")
expect_error(parse_preset("abc"), "Unknown preset")
})

test_that("presets returned as expected", {
expect_equal(
parse_preset("a4"),
list(width = 297, height = 210, units = "mm")
)
expect_equal(
parse_preset("a4r"),
list(width = 210, height = 297, units = "mm")
)
expect_equal(
parse_preset("fhd", dpi = 300),
list(width = 1920 / 300, height = 1080 / 300, units = "in")
)
expect_equal(
parse_preset("fhdr", dpi = 300),
list(width = 1080 / 300, height = 1920 / 300, units = "in")
)
expect_equal(
parse_preset("1920x1080", dpi = 300),
list(width = 1920 / 300, height = 1080 / 300, units = "in")
)
expect_equal(
parse_preset("1920x1080r", dpi = 300),
list(width = 1080 / 300, height = 1920 / 300, units = "in")
)
})