diff --git a/NEWS.md b/NEWS.md index c527454cdd..02cbf5465f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # ggplot2 (development version) +* `coord_*(expand)` can now take a logical vector to control expansion at any + side of the panel (top, right, bottom, left) (@teunbrand, #6020) * (Breaking) The defaults for all geoms can be set at one in the theme. (@teunbrand based on pioneering work by @dpseidel, #2239) * A new `theme(geom)` argument is used to track these defaults. diff --git a/R/coord-.R b/R/coord-.R index 57cf351f92..0d0bb5ecb9 100644 --- a/R/coord-.R +++ b/R/coord-.R @@ -184,10 +184,11 @@ Coord <- ggproto("Coord", # Will generally have to return FALSE for coordinate systems that enforce a fixed aspect ratio. is_free = function() FALSE, - setup_params = function(data) { + setup_params = function(self, data) { list( guide_default = guide_axis(), - guide_missing = guide_none() + guide_missing = guide_none(), + expand = parse_coord_expand(self$expand %||% TRUE) ) }, @@ -243,6 +244,26 @@ render_axis <- function(panel_params, axis, scale, position, theme) { } } +# Elaborates an 'expand' argument for every side (top, right, bottom or left) +parse_coord_expand <- function(expand) { + check_logical(expand) + if (anyNA(expand)) { + cli::cli_abort("{.arg expand} cannot contain missing values.") + } + + if (!is_named(expand)) { + return(rep_len(expand, 4)) + } + + # Match by top/right/bottom/left + out <- rep(TRUE, 4) + i <- match(names(expand), .trbl) + if (sum(!is.na(i)) > 0) { + out[i] <- unname(expand)[!is.na(i)] + } + out +} + # Utility function to check coord limits check_coord_limits <- function( limits, arg = caller_arg(limits), call = caller_env() diff --git a/R/coord-cartesian-.R b/R/coord-cartesian-.R index 74f46433db..885918c3d1 100644 --- a/R/coord-cartesian-.R +++ b/R/coord-cartesian-.R @@ -9,6 +9,10 @@ #' @param expand If `TRUE`, the default, adds a small expansion factor to #' the limits to ensure that data and axes don't overlap. If `FALSE`, #' limits are taken exactly from the data or `xlim`/`ylim`. +#' Giving a logical vector will separately control the expansion for the four +#' directions (top, left, bottom and right). The `expand` argument will be +#' recycled to length 4 if necessary. Alternatively, can be a named logical +#' vector to control a single direction, e.g. `expand = c(bottom = FALSE)`. #' @param default Is this the default coordinate system? If `FALSE` (the default), #' then replacing this coordinate system with another one creates a message alerting #' the user that the coordinate system is being replaced. If `TRUE`, that warning @@ -100,8 +104,8 @@ CoordCartesian <- ggproto("CoordCartesian", Coord, setup_panel_params = function(self, scale_x, scale_y, params = list()) { c( - view_scales_from_scale(scale_x, self$limits$x, self$expand), - view_scales_from_scale(scale_y, self$limits$y, self$expand) + view_scales_from_scale(scale_x, self$limits$x, params$expand[c(4, 2)]), + view_scales_from_scale(scale_y, self$limits$y, params$expand[c(3, 1)]) ) }, diff --git a/R/coord-flip.R b/R/coord-flip.R index eb46d12669..502ff56f88 100644 --- a/R/coord-flip.R +++ b/R/coord-flip.R @@ -89,6 +89,7 @@ CoordFlip <- ggproto("CoordFlip", CoordCartesian, }, setup_panel_params = function(self, scale_x, scale_y, params = list()) { + params$expand <- params$expand[c(2, 1, 4, 3)] parent <- ggproto_parent(CoordCartesian, self) panel_params <- parent$setup_panel_params(scale_x, scale_y, params) flip_axis_labels(panel_params) diff --git a/R/coord-radial.R b/R/coord-radial.R index 2f44e1ae4b..3a5ccf1ee2 100644 --- a/R/coord-radial.R +++ b/R/coord-radial.R @@ -66,7 +66,6 @@ coord_radial <- function(theta = "x", check_bool(r.axis.inside, allow_null = TRUE) } - check_bool(expand) check_bool(rotate.angle) check_number_decimal(start, allow_infinite = FALSE) check_number_decimal(end, allow_infinite = FALSE, allow_null = TRUE) @@ -139,8 +138,8 @@ CoordRadial <- ggproto("CoordRadial", Coord, setup_panel_params = function(self, scale_x, scale_y, params = list()) { params <- c( - view_scales_polar(scale_x, self$theta, expand = self$expand), - view_scales_polar(scale_y, self$theta, expand = self$expand), + view_scales_polar(scale_x, self$theta, expand = params$expand[c(4, 2)]), + view_scales_polar(scale_y, self$theta, expand = params$expand[c(3, 1)]), list(bbox = polar_bbox(self$arc, inner_radius = self$inner_radius), arc = self$arc, inner_radius = self$inner_radius) ) @@ -469,27 +468,27 @@ CoordRadial <- ggproto("CoordRadial", Coord, }, setup_params = function(self, data) { - if (isFALSE(self$r_axis_inside)) { - place <- in_arc(c(0, 0.5, 1, 1.5) * pi, self$arc) - if (place[1]) { - return(list(r_axis = "left", fake_arc = c(0, 2) * pi)) - } - if (place[3]) { - return(list(r_axis = "left", fake_arc = c(1, 3)* pi)) - } - if (place[2]) { - return(list(r_axis = "bottom", fake_arc = c(0.5, 2.5) * pi)) - } - if (place[4]) { - return(list(r_axis = "bottom", fake_arc = c(1.5, 3.5) * pi)) - } + params <- ggproto_parent(Coord, self)$setup_params(data) + if (!isFALSE(self$r_axis_inside)) { + return(params) + } + + place <- in_arc(c(0, 0.5, 1, 1.5) * pi, self$arc) + if (!any(place)) { cli::cli_warn(c( "No appropriate placement found for {.arg r_axis_inside}.", i = "Axis will be placed at panel edge." )) - self$r_axis_inside <- TRUE + params$r_axis_inside <- TRUE + return(params) } - return(NULL) + + params$r_axis <- if (any(place[c(1, 3)])) "left" else "bottom" + params$fake_arc <- switch( + which(place[c(1, 3, 2, 4)])[1], + c(0, 2), c(1, 3), c(0.5, 2.5), c(1.5, 3.5) + ) * pi + params } ) diff --git a/R/coord-sf.R b/R/coord-sf.R index a14b3c718c..f129947dc0 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -18,12 +18,10 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, }, setup_params = function(self, data) { - crs <- self$determine_crs(data) + params <- ggproto_parent(Coord, self)$setup_params(data) - params <- list( - crs = crs, - default_crs = self$default_crs - ) + params$crs <- self$determine_crs(data) + params$default_crs <- self$default_crs self$params <- params params @@ -170,8 +168,8 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, setup_panel_params = function(self, scale_x, scale_y, params = list()) { # expansion factors for scale limits - expansion_x <- default_expansion(scale_x, expand = self$expand) - expansion_y <- default_expansion(scale_y, expand = self$expand) + expansion_x <- default_expansion(scale_x, expand = params$expand[c(4, 2)]) + expansion_y <- default_expansion(scale_y, expand = params$expand[c(3, 1)]) # get scale limits and coord limits and merge together # coord limits take precedence over scale limits diff --git a/R/coord-transform.R b/R/coord-transform.R index 83ffd7b9ee..1253529fdd 100644 --- a/R/coord-transform.R +++ b/R/coord-transform.R @@ -153,8 +153,8 @@ CoordTrans <- ggproto("CoordTrans", Coord, setup_panel_params = function(self, scale_x, scale_y, params = list()) { c( - view_scales_from_scale_with_coord_trans(scale_x, self$limits$x, self$trans$x, self$expand), - view_scales_from_scale_with_coord_trans(scale_y, self$limits$y, self$trans$y, self$expand) + view_scales_from_scale_with_coord_trans(scale_x, self$limits$x, self$trans$x, params$expand[c(4, 2)]), + view_scales_from_scale_with_coord_trans(scale_y, self$limits$y, self$trans$y, params$expand[c(3, 1)]) ) }, diff --git a/R/scale-expansion.R b/R/scale-expansion.R index e3392fc5bf..0edb01f1b8 100644 --- a/R/scale-expansion.R +++ b/R/scale-expansion.R @@ -98,11 +98,24 @@ expand_range4 <- function(limits, expand) { #' default_expansion <- function(scale, discrete = expansion(add = 0.6), continuous = expansion(mult = 0.05), expand = TRUE) { - if (!expand) { - return(expansion(0, 0)) + out <- expansion() + if (!any(expand)) { + return(out) } + scale_expand <- scale$expand %|W|% + if (scale$is_discrete()) discrete else continuous - scale$expand %|W|% if (scale$is_discrete()) discrete else continuous + # for backward compatibility, we ensure expansions have expected length + expand <- rep_len(expand, 2L) + scale_expand <- rep_len(scale_expand, 4) + + if (expand[1]) { + out[1:2] <- scale_expand[1:2] + } + if (expand[2]) { + out[3:4] <- scale_expand[3:4] + } + out } #' Expand limits in (possibly) transformed space diff --git a/man/coord_cartesian.Rd b/man/coord_cartesian.Rd index 5c39f4d288..20987083a5 100644 --- a/man/coord_cartesian.Rd +++ b/man/coord_cartesian.Rd @@ -17,7 +17,11 @@ coord_cartesian( \item{expand}{If \code{TRUE}, the default, adds a small expansion factor to the limits to ensure that data and axes don't overlap. If \code{FALSE}, -limits are taken exactly from the data or \code{xlim}/\code{ylim}.} +limits are taken exactly from the data or \code{xlim}/\code{ylim}. +Giving a logical vector will separately control the expansion for the four +directions (top, left, bottom and right). The \code{expand} argument will be +recycled to length 4 if necessary. Alternatively, can be a named logical +vector to control a single direction, e.g. \code{expand = c(bottom = FALSE)}.} \item{default}{Is this the default coordinate system? If \code{FALSE} (the default), then replacing this coordinate system with another one creates a message alerting diff --git a/man/coord_fixed.Rd b/man/coord_fixed.Rd index fc8c052506..8877019a91 100644 --- a/man/coord_fixed.Rd +++ b/man/coord_fixed.Rd @@ -14,7 +14,11 @@ coord_fixed(ratio = 1, xlim = NULL, ylim = NULL, expand = TRUE, clip = "on") \item{expand}{If \code{TRUE}, the default, adds a small expansion factor to the limits to ensure that data and axes don't overlap. If \code{FALSE}, -limits are taken exactly from the data or \code{xlim}/\code{ylim}.} +limits are taken exactly from the data or \code{xlim}/\code{ylim}. +Giving a logical vector will separately control the expansion for the four +directions (top, left, bottom and right). The \code{expand} argument will be +recycled to length 4 if necessary. Alternatively, can be a named logical +vector to control a single direction, e.g. \code{expand = c(bottom = FALSE)}.} \item{clip}{Should drawing be clipped to the extent of the plot panel? A setting of \code{"on"} (the default) means yes, and a setting of \code{"off"} diff --git a/man/coord_flip.Rd b/man/coord_flip.Rd index be69644cf0..48ea2e1dba 100644 --- a/man/coord_flip.Rd +++ b/man/coord_flip.Rd @@ -11,7 +11,11 @@ coord_flip(xlim = NULL, ylim = NULL, expand = TRUE, clip = "on") \item{expand}{If \code{TRUE}, the default, adds a small expansion factor to the limits to ensure that data and axes don't overlap. If \code{FALSE}, -limits are taken exactly from the data or \code{xlim}/\code{ylim}.} +limits are taken exactly from the data or \code{xlim}/\code{ylim}. +Giving a logical vector will separately control the expansion for the four +directions (top, left, bottom and right). The \code{expand} argument will be +recycled to length 4 if necessary. Alternatively, can be a named logical +vector to control a single direction, e.g. \code{expand = c(bottom = FALSE)}.} \item{clip}{Should drawing be clipped to the extent of the plot panel? A setting of \code{"on"} (the default) means yes, and a setting of \code{"off"} diff --git a/man/coord_map.Rd b/man/coord_map.Rd index 3aacd167d7..913768f29e 100644 --- a/man/coord_map.Rd +++ b/man/coord_map.Rd @@ -40,7 +40,11 @@ means no. For details, please see \code{\link[=coord_cartesian]{coord_cartesian( \item{expand}{If \code{TRUE}, the default, adds a small expansion factor to the limits to ensure that data and axes don't overlap. If \code{FALSE}, -limits are taken exactly from the data or \code{xlim}/\code{ylim}.} +limits are taken exactly from the data or \code{xlim}/\code{ylim}. +Giving a logical vector will separately control the expansion for the four +directions (top, left, bottom and right). The \code{expand} argument will be +recycled to length 4 if necessary. Alternatively, can be a named logical +vector to control a single direction, e.g. \code{expand = c(bottom = FALSE)}.} } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} diff --git a/man/coord_trans.Rd b/man/coord_trans.Rd index bea5b54716..d1f46dc1ee 100644 --- a/man/coord_trans.Rd +++ b/man/coord_trans.Rd @@ -33,7 +33,11 @@ legend, the plot title, or the plot margins.} \item{expand}{If \code{TRUE}, the default, adds a small expansion factor to the limits to ensure that data and axes don't overlap. If \code{FALSE}, -limits are taken exactly from the data or \code{xlim}/\code{ylim}.} +limits are taken exactly from the data or \code{xlim}/\code{ylim}. +Giving a logical vector will separately control the expansion for the four +directions (top, left, bottom and right). The \code{expand} argument will be +recycled to length 4 if necessary. Alternatively, can be a named logical +vector to control a single direction, e.g. \code{expand = c(bottom = FALSE)}.} } \description{ \code{coord_trans()} is different to scale transformations in that it occurs after diff --git a/man/ggsf.Rd b/man/ggsf.Rd index c4ec76bed1..7424d4107f 100644 --- a/man/ggsf.Rd +++ b/man/ggsf.Rd @@ -99,7 +99,11 @@ though they would be visible in the final plot region.} \item{expand}{If \code{TRUE}, the default, adds a small expansion factor to the limits to ensure that data and axes don't overlap. If \code{FALSE}, -limits are taken exactly from the data or \code{xlim}/\code{ylim}.} +limits are taken exactly from the data or \code{xlim}/\code{ylim}. +Giving a logical vector will separately control the expansion for the four +directions (top, left, bottom and right). The \code{expand} argument will be +recycled to length 4 if necessary. Alternatively, can be a named logical +vector to control a single direction, e.g. \code{expand = c(bottom = FALSE)}.} \item{crs}{The coordinate reference system (CRS) into which all data should be projected before plotting. If not specified, will use the CRS defined diff --git a/tests/testthat/helper-plot-data.R b/tests/testthat/helper-plot-data.R index bc1f81f2c9..13e36d861a 100644 --- a/tests/testthat/helper-plot-data.R +++ b/tests/testthat/helper-plot-data.R @@ -5,7 +5,7 @@ cdata <- function(plot) { lapply(pieces$data, function(d) { dapply(d, "PANEL", function(panel_data) { scales <- pieces$layout$get_scales(panel_data$PANEL[1]) - panel_params <- plot$coordinates$setup_panel_params(scales$x, scales$y) + panel_params <- plot$coordinates$setup_panel_params(scales$x, scales$y, params = pieces$layout$coord_params) plot$coordinates$transform(panel_data, panel_params) }) }) diff --git a/tests/testthat/test-coord-.R b/tests/testthat/test-coord-.R index 76a174454c..6a50369bbc 100644 --- a/tests/testthat/test-coord-.R +++ b/tests/testthat/test-coord-.R @@ -75,3 +75,19 @@ test_that("coords append a column to the layout correctly", { expect_equal(test$COORD, c(1, 2, 1)) }) +test_that("coord expand takes a vector", { + + base <- ggplot() + lims(x = c(0, 10), y = c(0, 10)) + + p <- ggplot_build(base + coord_cartesian(expand = c(TRUE, FALSE, FALSE, TRUE))) + pp <- p$layout$panel_params[[1]] + expect_equal(pp$x.range, c(-0.5, 10)) + expect_equal(pp$y.range, c(0, 10.5)) + + p <- ggplot_build(base + coord_cartesian(expand = c(top = FALSE, left = FALSE))) + pp <- p$layout$panel_params[[1]] + expect_equal(pp$x.range, c(0, 10.5)) + expect_equal(pp$y.range, c(-0.5, 10)) + +}) +