diff --git a/NEWS.md b/NEWS.md
index a15a099b5c..d86340c476 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,13 @@
# ggplot2 (development version)
+* The spacing between legend keys and their labels, in addition to legends
+ and their titles, is now controlled by the text's `margin` setting. Not
+ specifying margins will automatically add appropriate text margins. To
+ control the spacing within a legend between keys, the new
+ `key.spacing.{x/y}` argument can be used. This leaves the
+ `legend.spacing` dedicated to controlling the spacing between
+ different guides (#5455).
+
* In the theme element hierarchy, parent elements that are a strict subclass
of child elements now confer their subclass upon the children (#5457).
diff --git a/R/guide-bins.R b/R/guide-bins.R
index f20adee759..1f2228c8c3 100644
--- a/R/guide-bins.R
+++ b/R/guide-bins.R
@@ -341,13 +341,6 @@ GuideBins <- ggproto(
}
key$.label[c(1, n_labels)[!params$show.limits]] <- ""
- just <- switch(
- params$direction,
- horizontal = elements$text$vjust,
- vertical = elements$text$hjust,
- 0.5
- )
-
if (params$direction == "vertical") {
key$.value <- 1 - key$.value
}
@@ -356,7 +349,6 @@ GuideBins <- ggproto(
elements$text,
label = key$.label,
x = unit(key$.value, "npc"),
- y = rep(just, nrow(key)),
margin_x = FALSE,
margin_y = TRUE,
flip = params$direction == "vertical"
diff --git a/R/guide-colorbar.R b/R/guide-colorbar.R
index 7e71eaba0c..265d6bec61 100644
--- a/R/guide-colorbar.R
+++ b/R/guide-colorbar.R
@@ -307,9 +307,6 @@ GuideColourbar <- ggproto(
ticks_length = unit(0.2, "npc"),
background = "legend.background",
margin = "legend.margin",
- spacing = "legend.spacing",
- spacing.x = "legend.spacing.x",
- spacing.y = "legend.spacing.y",
key = "legend.key",
key.height = "legend.key.height",
key.width = "legend.key.width",
@@ -413,17 +410,10 @@ GuideColourbar <- ggproto(
return(list(labels = zeroGrob()))
}
- just <- if (params$direction == "horizontal") {
- elements$text$vjust
- } else {
- elements$text$hjust
- }
-
list(labels = flip_element_grob(
elements$text,
label = validate_labels(key$.label),
x = unit(key$.value, "npc"),
- y = rep(just, nrow(key)),
margin_x = FALSE,
margin_y = TRUE,
flip = params$direction == "vertical"
diff --git a/R/guide-legend.R b/R/guide-legend.R
index 341bee47c8..fe383f81d0 100644
--- a/R/guide-legend.R
+++ b/R/guide-legend.R
@@ -42,6 +42,10 @@
#' @param keyheight A numeric or a [grid::unit()] object specifying
#' the height of the legend key. Default value is `legend.key.height` or
#' `legend.key.size` in [theme()].
+#' @param key.spacing,key.spacing.x,key.spacing.y A numeric or [grid::unit()]
+#' object specifying the distance between key-label pairs in the horizontal
+#' direction (`key.spacing.x`), vertical direction (`key.spacing.y`) or both
+#' (`key.spacing`).
#' @param direction A character string indicating the direction of the guide.
#' One of "horizontal" or "vertical."
#' @param default.unit A character string indicating [grid::unit()]
@@ -143,6 +147,9 @@ guide_legend <- function(
# Key size
keywidth = NULL,
keyheight = NULL,
+ key.spacing = NULL,
+ key.spacing.x = NULL,
+ key.spacing.y = NULL,
# General
direction = NULL,
@@ -156,12 +163,24 @@ guide_legend <- function(
...
) {
# Resolve key sizes
- if (!inherits(keywidth, c("NULL", "unit"))) {
+ if (!(is.null(keywidth) || is.unit(keywidth))) {
keywidth <- unit(keywidth, default.unit)
}
- if (!inherits(keyheight, c("NULL", "unit"))) {
+ if (!(is.null(keyheight) || is.unit(keyheight))) {
keyheight <- unit(keyheight, default.unit)
}
+
+ # Resolve spacing
+ key.spacing.x <- key.spacing.x %||% key.spacing
+ if (!is.null(key.spacing.x) || is.unit(key.spacing.x)) {
+ key.spacing.x <- unit(key.spacing.x, default.unit)
+ }
+ key.spacing.y <- key.spacing.y %||% key.spacing
+ if (!is.null(key.spacing.y) || is.unit(key.spacing.y)) {
+ key.spacing.y <- unit(key.spacing.y, default.unit)
+ }
+
+
if (!is.null(title.position)) {
title.position <- arg_match0(title.position, .trbl)
}
@@ -187,6 +206,8 @@ guide_legend <- function(
# Key size
keywidth = keywidth,
keyheight = keyheight,
+ key.spacing.x = key.spacing.x,
+ key.spacing.y = key.spacing.y,
# General
direction = direction,
@@ -226,9 +247,10 @@ GuideLegend <- ggproto(
keywidth = NULL,
keyheight = NULL,
+ key.spacing.x = NULL,
+ key.spacing.y = NULL,
# General
- direction = NULL,
override.aes = list(),
nrow = NULL,
ncol = NULL,
@@ -249,9 +271,6 @@ GuideLegend <- ggproto(
elements = list(
background = "legend.background",
margin = "legend.margin",
- spacing = "legend.spacing",
- spacing.x = "legend.spacing.x",
- spacing.y = "legend.spacing.y",
key = "legend.key",
key.height = "legend.key.height",
key.width = "legend.key.width",
@@ -436,13 +455,35 @@ GuideLegend <- ggproto(
elements$text$size %||% 11
gap <- unit(gap * 0.5, "pt")
# Should maybe be elements$spacing.{x/y} instead of the theme's spacing?
- elements$hgap <- width_cm( theme$legend.spacing.x %||% gap)
- elements$vgap <- height_cm(theme$legend.spacing.y %||% gap)
+
+ if (params$direction == "vertical") {
+ # For backward compatibility, vertical default is no spacing
+ vgap <- params$key.spacing.y %||% unit(0, "pt")
+ } else {
+ vgap <- params$key.spacing.y %||% gap
+ }
+
+ elements$hgap <- width_cm( params$key.spacing.x %||% gap)
+ elements$vgap <- height_cm(vgap)
elements$padding <- convertUnit(
elements$margin %||% margin(),
"cm", valueOnly = TRUE
)
+ # When no explicit margin has been set, either in this guide or in the
+ # theme, we set a default text margin to leave a small gap in between
+ # the label and the key.
+ if (is.null(params$label.theme$margin %||% theme$legend.text$margin) &&
+ !inherits(elements$text, "element_blank")) {
+ i <- match(params$label.position, .trbl[c(3, 4, 1, 2)])
+ elements$text$margin[i] <- elements$text$margin[i] + gap
+ }
+ if (is.null(params$title.theme$margin %||% theme$legend.title$margin) &&
+ !inherits(elements$title, "element_blank")) {
+ i <- match(params$title.position, .trbl[c(3, 4, 1, 2)])
+ elements$title$margin[i] <- elements$title$margin[i] + gap
+ }
+
# Evaluate backgrounds early
if (!is.null(elements$background)) {
elements$background <- ggname(
@@ -527,22 +568,23 @@ GuideLegend <- ggproto(
hgap <- elements$hgap %||% 0
widths <- switch(
params$label.position,
- "left" = list(label_widths, hgap, widths, hgap),
- "right" = list(widths, hgap, label_widths, hgap),
- list(pmax(label_widths, widths), hgap * (!byrow))
+ "left" = list(label_widths, widths, hgap),
+ "right" = list(widths, label_widths, hgap),
+ list(pmax(label_widths, widths), hgap)
)
widths <- head(vec_interleave(!!!widths), -1)
vgap <- elements$vgap %||% 0
heights <- switch(
params$label.position,
- "top" = list(label_heights, vgap, heights, vgap),
- "bottom" = list(heights, vgap, label_heights, vgap),
- list(pmax(label_heights, heights), vgap * (byrow))
+ "top" = list(label_heights, heights, vgap),
+ "bottom" = list(heights, label_heights, vgap),
+ list(pmax(label_heights, heights), vgap)
)
heights <- head(vec_interleave(!!!heights), -1)
has_title <- !is.zero(grobs$title)
+
if (has_title) {
# Measure title
title_width <- width_cm(grobs$title)
@@ -551,14 +593,14 @@ GuideLegend <- ggproto(
# Combine title with rest of the sizes based on its position
widths <- switch(
params$title.position,
- "left" = c(title_width, hgap, widths),
- "right" = c(widths, hgap, title_width),
+ "left" = c(title_width, widths),
+ "right" = c(widths, title_width),
c(widths, max(0, title_width - sum(widths)))
)
heights <- switch(
params$title.position,
- "top" = c(title_height, vgap, heights),
- "bottom" = c(heights, vgap, title_height),
+ "top" = c(title_height, heights),
+ "bottom" = c(heights, title_height),
c(heights, max(0, title_height - sum(heights)))
)
}
@@ -595,20 +637,20 @@ GuideLegend <- ggproto(
switch(
params$label.position,
"top" = {
- key_row <- key_row * 2
- label_row <- label_row * 2 - 2
+ key_row <- key_row + df$R
+ label_row <- key_row - 1
},
"bottom" = {
- key_row <- key_row * 2 - 2
- label_row <- label_row * 2
+ key_row <- key_row + df$R - 1
+ label_row <- key_row + 1
},
"left" = {
- key_col <- key_col * 2
- label_col <- label_col * 2 - 2
+ key_col <- key_col + df$C
+ label_col <- key_col - 1
},
"right" = {
- key_col <- key_col * 2 - 2
- label_col <- label_col * 2
+ key_col <- key_col + df$C - 1
+ label_col <- key_col + 1
}
)
@@ -617,8 +659,8 @@ GuideLegend <- ggproto(
switch(
params$title.position,
"top" = {
- key_row <- key_row + 2
- label_row <- label_row + 2
+ key_row <- key_row + 1
+ label_row <- label_row + 1
title_row <- 2
title_col <- seq_along(sizes$widths) + 1
},
@@ -627,8 +669,8 @@ GuideLegend <- ggproto(
title_col <- seq_along(sizes$widths) + 1
},
"left" = {
- key_col <- key_col + 2
- label_col <- label_col + 2
+ key_col <- key_col + 1
+ label_col <- label_col + 1
title_row <- seq_along(sizes$heights) + 1
title_col <- 2
},
diff --git a/man/guide_legend.Rd b/man/guide_legend.Rd
index 21dcbe7833..224de5587a 100644
--- a/man/guide_legend.Rd
+++ b/man/guide_legend.Rd
@@ -17,6 +17,9 @@ guide_legend(
label.vjust = NULL,
keywidth = NULL,
keyheight = NULL,
+ key.spacing = NULL,
+ key.spacing.x = NULL,
+ key.spacing.y = NULL,
direction = NULL,
default.unit = "line",
override.aes = list(),
@@ -74,6 +77,11 @@ the width of the legend key. Default value is \code{legend.key.width} or
the height of the legend key. Default value is \code{legend.key.height} or
\code{legend.key.size} in \code{\link[=theme]{theme()}}.}
+\item{key.spacing, key.spacing.x, key.spacing.y}{A numeric or \code{\link[grid:unit]{grid::unit()}}
+object specifying the distance between key-label pairs in the horizontal
+direction (\code{key.spacing.x}), vertical direction (\code{key.spacing.y}) or both
+(\code{key.spacing}).}
+
\item{direction}{A character string indicating the direction of the guide.
One of "horizontal" or "vertical."}
diff --git a/tests/testthat/_snaps/guides/legend-with-widely-spaced-keys.svg b/tests/testthat/_snaps/guides/legend-with-widely-spaced-keys.svg
new file mode 100644
index 0000000000..ee015eb369
--- /dev/null
+++ b/tests/testthat/_snaps/guides/legend-with-widely-spaced-keys.svg
@@ -0,0 +1,109 @@
+
+
diff --git a/tests/testthat/_snaps/guides/rotated-guide-titles-and-labels.svg b/tests/testthat/_snaps/guides/rotated-guide-titles-and-labels.svg
index fa41704f31..875a76f42f 100644
--- a/tests/testthat/_snaps/guides/rotated-guide-titles-and-labels.svg
+++ b/tests/testthat/_snaps/guides/rotated-guide-titles-and-labels.svg
@@ -79,11 +79,11 @@
-5.0
-7.5
-10.0
-12.5
-15.0
+5.0
+7.5
+10.0
+12.5
+15.0
rotated guide titles and labels
diff --git a/tests/testthat/_snaps/guides/vertical-gap-of-1cm-between-guide-title-and-guide.svg b/tests/testthat/_snaps/guides/vertical-gap-of-1cm-between-guide-title-and-guide.svg
index a2fd873e1e..73345206cb 100644
--- a/tests/testthat/_snaps/guides/vertical-gap-of-1cm-between-guide-title-and-guide.svg
+++ b/tests/testthat/_snaps/guides/vertical-gap-of-1cm-between-guide-title-and-guide.svg
@@ -55,35 +55,35 @@
3.0
x
y
-
-y
-
-
-
-
-
-
-
-
-
-
-
-1.0
-1.5
-2.0
-2.5
-3.0
-
-factor(x)
-
-
-
-
-
-
-1
-2
-3
+
+y
+
+
+
+
+
+
+
+
+
+
+
+1.0
+1.5
+2.0
+2.5
+3.0
+
+factor(x)
+
+
+
+
+
+
+1
+2
+3
vertical gap of 1cm between guide title and guide
diff --git a/tests/testthat/test-guides.R b/tests/testthat/test-guides.R
index 2ea4d3a696..0d029c8b1b 100644
--- a/tests/testthat/test-guides.R
+++ b/tests/testthat/test-guides.R
@@ -338,6 +338,16 @@ test_that("guide_colourbar warns about discrete scales", {
})
+test_that("guide_legend uses key.spacing correctly", {
+ p <- ggplot(mtcars, aes(disp, mpg, colour = factor(carb))) +
+ geom_point() +
+ guides(colour = guide_legend(
+ ncol = 2, key.spacing.y = 1, key.spacing.x = 2
+ ))
+
+ expect_doppelganger("legend with widely spaced keys", p)
+})
+
# Visual tests ------------------------------------------------------------
test_that("axis guides are drawn correctly", {
@@ -636,10 +646,10 @@ test_that("guides title and text are positioned correctly", {
scale_fill_continuous(name = "the\ncontinuous\ncolorscale")
)
expect_doppelganger("vertical gap of 1cm between guide title and guide",
- p + theme(legend.spacing.y = grid::unit(1, "cm"))
+ p + theme(legend.title = element_text(margin = margin(b = 1, unit = "cm")))
)
expect_doppelganger("horizontal gap of 1cm between guide and guide text",
- p + theme(legend.spacing.x = grid::unit(1, "cm"))
+ p + theme(legend.text = element_text(margin = margin(l = 1, unit = "cm")))
)
# now test label positioning, alignment, etc
@@ -654,8 +664,8 @@ test_that("guides title and text are positioned correctly", {
expect_doppelganger("guide title and text positioning and alignment via themes",
p + theme(
- legend.title = element_text(hjust = 0.5, margin = margin(t = 30)),
- legend.text = element_text(hjust = 1, margin = margin(l = 5, t = 10, b = 10))
+ legend.title = element_text(hjust = 0.5, margin = margin(t = 30, b = 5.5)),
+ legend.text = element_text(hjust = 1, margin = margin(l = 10.5, t = 10, b = 10))
)
)