From ae9c0340ddf18ac8569cf13f93b4862776a5d1e6 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Fri, 2 Aug 2024 16:35:32 +0200 Subject: [PATCH 1/9] `default_expansion()` handles longer `expand` arg --- R/scale-expansion.R | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/R/scale-expansion.R b/R/scale-expansion.R index 8ec72c2a78..8e7c206961 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 From fade32ad172df2b25ff8421b5c3fa3d3af76b7a2 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Mon, 5 Aug 2024 10:00:22 +0200 Subject: [PATCH 2/9] Parser for expand argument --- R/coord-.R | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/R/coord-.R b/R/coord-.R index 6736059fbc..787809a3c5 100644 --- a/R/coord-.R +++ b/R/coord-.R @@ -229,6 +229,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() From e4a7409c34ba86acc9f57e12120d8c1144658872 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Mon, 5 Aug 2024 10:05:13 +0200 Subject: [PATCH 3/9] insert parser at relevant places --- R/coord-cartesian-.R | 5 +++-- R/coord-radial.R | 7 ++++--- R/coord-sf.R | 5 +++-- R/coord-transform.R | 5 +++-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/R/coord-cartesian-.R b/R/coord-cartesian-.R index 74f46433db..7d1e022a49 100644 --- a/R/coord-cartesian-.R +++ b/R/coord-cartesian-.R @@ -99,9 +99,10 @@ CoordCartesian <- ggproto("CoordCartesian", Coord, }, setup_panel_params = function(self, scale_x, scale_y, params = list()) { + expand <- parse_coord_expand(self$expand) 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, expand[c(4, 2)]), + view_scales_from_scale(scale_y, self$limits$y, expand[c(3, 1)]) ) }, diff --git a/R/coord-radial.R b/R/coord-radial.R index c47d55bb56..d20a6ba75d 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) @@ -138,9 +137,11 @@ CoordRadial <- ggproto("CoordRadial", Coord, setup_panel_params = function(self, scale_x, scale_y, params = list()) { + expand <- parse_coord_expand(self$expand) + 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 = expand[c(4, 2)]), + view_scales_polar(scale_y, self$theta, expand = expand[c(3, 1)]), list(bbox = polar_bbox(self$arc, inner_radius = self$inner_radius), arc = self$arc, inner_radius = self$inner_radius) ) diff --git a/R/coord-sf.R b/R/coord-sf.R index f861ae2d28..697f1deafc 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -170,8 +170,9 @@ 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) + expand <- parse_coord_expand(self$expand) + expansion_x <- default_expansion(scale_x, expand = expand[c(4, 2)]) + expansion_y <- default_expansion(scale_y, expand = 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..83fa8bba0b 100644 --- a/R/coord-transform.R +++ b/R/coord-transform.R @@ -152,9 +152,10 @@ CoordTrans <- ggproto("CoordTrans", Coord, }, setup_panel_params = function(self, scale_x, scale_y, params = list()) { + expand <- parse_coord_expand(self$expand) 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, expand[c(4, 2)]), + view_scales_from_scale_with_coord_trans(scale_y, self$limits$y, self$trans$y, expand[c(3, 1)]) ) }, From b5d27689c793aaf68295b810c70656a29a50cee6 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Mon, 5 Aug 2024 11:40:30 +0200 Subject: [PATCH 4/9] Use Coord$setup_params() to setup expand --- R/coord-.R | 5 +++-- R/coord-cartesian-.R | 5 ++--- R/coord-radial.R | 38 ++++++++++++++++++-------------------- R/coord-sf.R | 13 +++++-------- R/coord-transform.R | 5 ++--- 5 files changed, 30 insertions(+), 36 deletions(-) diff --git a/R/coord-.R b/R/coord-.R index 787809a3c5..4e00719bc7 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) ) }, diff --git a/R/coord-cartesian-.R b/R/coord-cartesian-.R index 7d1e022a49..eb8ad1cc3b 100644 --- a/R/coord-cartesian-.R +++ b/R/coord-cartesian-.R @@ -99,10 +99,9 @@ CoordCartesian <- ggproto("CoordCartesian", Coord, }, setup_panel_params = function(self, scale_x, scale_y, params = list()) { - expand <- parse_coord_expand(self$expand) c( - view_scales_from_scale(scale_x, self$limits$x, expand[c(4, 2)]), - view_scales_from_scale(scale_y, self$limits$y, expand[c(3, 1)]) + 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-radial.R b/R/coord-radial.R index d20a6ba75d..08dfa7975d 100644 --- a/R/coord-radial.R +++ b/R/coord-radial.R @@ -137,11 +137,9 @@ CoordRadial <- ggproto("CoordRadial", Coord, setup_panel_params = function(self, scale_x, scale_y, params = list()) { - expand <- parse_coord_expand(self$expand) - params <- c( - view_scales_polar(scale_x, self$theta, expand = expand[c(4, 2)]), - view_scales_polar(scale_y, self$theta, expand = expand[c(3, 1)]), + 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) ) @@ -449,27 +447,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 697f1deafc..0b1098ab80 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,9 +168,8 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, setup_panel_params = function(self, scale_x, scale_y, params = list()) { # expansion factors for scale limits - expand <- parse_coord_expand(self$expand) - expansion_x <- default_expansion(scale_x, expand = expand[c(4, 2)]) - expansion_y <- default_expansion(scale_y, expand = expand[c(3, 1)]) + 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 83fa8bba0b..1253529fdd 100644 --- a/R/coord-transform.R +++ b/R/coord-transform.R @@ -152,10 +152,9 @@ CoordTrans <- ggproto("CoordTrans", Coord, }, setup_panel_params = function(self, scale_x, scale_y, params = list()) { - expand <- parse_coord_expand(self$expand) c( - view_scales_from_scale_with_coord_trans(scale_x, self$limits$x, self$trans$x, expand[c(4, 2)]), - view_scales_from_scale_with_coord_trans(scale_y, self$limits$y, self$trans$y, expand[c(3, 1)]) + 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)]) ) }, From f369fe95fb021af2d9ba46e66155cc7d46a766af Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Mon, 5 Aug 2024 11:52:37 +0200 Subject: [PATCH 5/9] fix plot helper not including params --- tests/testthat/helper-plot-data.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) }) }) From b853c8cf65f79c72c8807e7040d83f0dd5d241f9 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Mon, 5 Aug 2024 11:52:46 +0200 Subject: [PATCH 6/9] Redocument parameter --- R/coord-cartesian-.R | 4 ++++ man/coord_cartesian.Rd | 6 +++++- man/coord_fixed.Rd | 6 +++++- man/coord_flip.Rd | 6 +++++- man/coord_map.Rd | 6 +++++- man/coord_trans.Rd | 6 +++++- man/ggsf.Rd | 6 +++++- 7 files changed, 34 insertions(+), 6 deletions(-) diff --git a/R/coord-cartesian-.R b/R/coord-cartesian-.R index eb8ad1cc3b..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 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 3b8ff90bd7..b21bc863ff 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 From 54c289eda6a44a422985215aa1c39d74a788e37e Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Mon, 5 Aug 2024 12:07:54 +0200 Subject: [PATCH 7/9] fix `coord_flip()` case --- R/coord-flip.R | 1 + 1 file changed, 1 insertion(+) 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) From ca9e188141fc4b3f781a401c0657f4027413db3f Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Mon, 5 Aug 2024 12:20:33 +0200 Subject: [PATCH 8/9] add test --- tests/testthat/test-coord-.R | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/testthat/test-coord-.R b/tests/testthat/test-coord-.R index b372478981..ed6abaa040 100644 --- a/tests/testthat/test-coord-.R +++ b/tests/testthat/test-coord-.R @@ -76,3 +76,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)) + +}) + From f0a379e1464e14e5b00a1c9b5968eb25a643dfb2 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Mon, 5 Aug 2024 12:25:44 +0200 Subject: [PATCH 9/9] add news bullet --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 4c0ea1e891..d85ffe3324 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) * Missing values from discrete palettes are no longer translated (@teunbrand, #5929). * Fixed bug in `facet_grid(margins = TRUE)` when using expresssions