Skip to content

Commit 5ed2d88

Browse files
authored
Stretching legends (#5515)
* colourbar size is defined in npcs * backport `unitType` * guide assembly preserves null units * Handle null units at guide boxes * set legend size to 1npc during build * better unit recognitionin R3.6 * smart distribution of null units * better detection of relative legend sizes * document use of null units * Add tests * Adapt to #5488 * Fix title spacing bug
1 parent 80db793 commit 5ed2d88

11 files changed

+377
-62
lines changed

R/backports.R

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@ if (getRversion() < "3.3") {
1717

1818
on_load(backport_unit_methods())
1919

20+
unitType <- function(x) {
21+
unit <- attr(x, "unit")
22+
if (!is.null(unit)) {
23+
return(unit)
24+
}
25+
if (is.list(x) && is.unit(x[[1]])) {
26+
unit <- vapply(x, unitType, character(1))
27+
return(unit)
28+
} else if ("fname" %in% names(x)) {
29+
return(x$fname)
30+
}
31+
rep("", length(x)) # we're only interested in simple units for now
32+
}
33+
34+
on_load({
35+
if ("unitType" %in% getNamespaceExports("grid")) {
36+
unitType <- grid::unitType
37+
}
38+
})
39+
2040
# isFALSE() and isTRUE() are available on R (>=3.5)
2141
if (getRversion() < "3.5") {
2242
isFALSE <- function(x) is.logical(x) && length(x) == 1L && !is.na(x) && !x

R/guide-colorbar.R

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ NULL
1515
#' see [guides()].
1616
#'
1717
#' @inheritParams guide_legend
18-
#' @param barwidth A numeric or a [grid::unit()] object specifying
19-
#' the width of the colourbar. Default value is `legend.key.width` or
20-
#' `legend.key.size` in [theme()] or theme.
21-
#' @param barheight A numeric or a [grid::unit()] object specifying
22-
#' the height of the colourbar. Default value is `legend.key.height` or
23-
#' `legend.key.size` in [theme()] or theme.
18+
#' @param barwidth,barheight A numeric or [grid::unit()] object specifying the
19+
#' width and height of the bar respectively. Default value is derived from
20+
#' `legend.key.width`, `legend.key.height` or `legend.key` in [theme()].\cr
21+
#' `r lifecycle::badge("experimental")`: optionally a `"null"` unit to stretch
22+
#' the bar to the available space.
2423
#' @param frame A theme object for rendering a frame drawn around the bar.
2524
#' Usually, the object of `element_rect()` is expected. If `element_blank()`
2625
#' (default), no frame is drawn.
@@ -452,29 +451,29 @@ GuideColourbar <- ggproto(
452451
)
453452
grob <- rasterGrob(
454453
image = image,
455-
width = elements$key.width,
456-
height = elements$key.height,
457-
default.units = "cm",
454+
width = 1,
455+
height = 1,
456+
default.units = "npc",
458457
gp = gpar(col = NA),
459458
interpolate = TRUE
460459
)
461460
} else{
462461
if (params$direction == "horizontal") {
463-
width <- elements$key.width / nrow(decor)
464-
height <- elements$key.height
462+
width <- 1 / nrow(decor)
463+
height <- 1
465464
x <- (seq(nrow(decor)) - 1) * width
466465
y <- 0
467466
} else {
468-
width <- elements$key.width
469-
height <- elements$key.height / nrow(decor)
467+
width <- 1
468+
height <- 1 / nrow(decor)
470469
y <- (seq(nrow(decor)) - 1) * height
471470
x <- 0
472471
}
473472
grob <- rectGrob(
474473
x = x, y = y,
475474
vjust = 0, hjust = 0,
476475
width = width, height = height,
477-
default.units = "cm",
476+
default.units = "npc",
478477
gp = gpar(col = NA, fill = decor$colour)
479478
)
480479
}

R/guide-legend.R

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@
3636
#' (right-aligned) for expressions.
3737
#' @param label.vjust A numeric specifying vertical justification of the label
3838
#' text.
39-
#' @param keywidth A numeric or a [grid::unit()] object specifying
40-
#' the width of the legend key. Default value is `legend.key.width` or
41-
#' `legend.key.size` in [theme()].
42-
#' @param keyheight A numeric or a [grid::unit()] object specifying
43-
#' the height of the legend key. Default value is `legend.key.height` or
44-
#' `legend.key.size` in [theme()].
39+
#' @param keywidth,keyheight A numeric or [grid::unit()] object specifying the
40+
#' width and height of the legend key respectively. Default value is
41+
#' `legend.key.width`, `legend.key.height` or `legend.key` in [theme()].\cr
42+
#' `r lifecycle::badge("experimental")`: optionally a `"null"` unit to stretch
43+
#' keys to the available space.
4544
#' @param key.spacing,key.spacing.x,key.spacing.y A numeric or [grid::unit()]
4645
#' object specifying the distance between key-label pairs in the horizontal
4746
#' direction (`key.spacing.x`), vertical direction (`key.spacing.y`) or both
@@ -603,8 +602,19 @@ GuideLegend <- ggproto(
603602
# Measure title
604603
title_width <- width_cm(grobs$title)
605604
title_height <- height_cm(grobs$title)
606-
extra_width <- max(0, title_width - sum(widths))
607-
extra_height <- max(0, title_height - sum(heights))
605+
606+
# Titles are assumed to have sufficient size when keys are null units
607+
if (is.unit(params$keywidth) && unitType(params$keywidth) == "null") {
608+
extra_width <- 0
609+
} else {
610+
extra_width <- max(0, title_width - sum(widths))
611+
}
612+
if (is.unit(params$keyheight) && unitType(params$keyheight) == "null") {
613+
extra_height <- 0
614+
} else {
615+
extra_height <- max(0, title_height - sum(heights))
616+
}
617+
608618
just <- with(elements$title, rotate_just(angle, hjust, vjust))
609619
hjust <- just$hjust
610620
vjust <- just$vjust
@@ -699,11 +709,19 @@ GuideLegend <- ggproto(
699709
},
700710

701711
assemble_drawing = function(grobs, layout, sizes, params, elements) {
712+
widths <- unit(c(sizes$padding[4], sizes$widths, sizes$padding[2]), "cm")
713+
if (is.unit(params$keywidth) && unitType(params$keywidth) == "null") {
714+
i <- unique(layout$layout$key_col)
715+
widths[i] <- params$keywidth
716+
}
702717

703-
gt <- gtable(
704-
widths = unit(c(sizes$padding[4], sizes$widths, sizes$padding[2]), "cm"),
705-
heights = unit(c(sizes$padding[1], sizes$heights, sizes$padding[3]), "cm")
706-
)
718+
heights <- unit(c(sizes$padding[1], sizes$heights, sizes$padding[3]), "cm")
719+
if (is.unit(params$keyheight) && unitType(params$keyheight) == "null") {
720+
i <- unique(layout$layout$key_row)
721+
heights[i] <- params$keyheight
722+
}
723+
724+
gt <- gtable(widths = widths, heights = heights)
707725

708726
# Add background
709727
if (!is.zero(elements$background)) {

R/guides-.R

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -572,10 +572,12 @@ Guides <- ggproto(
572572
)
573573

574574
# Measure guides
575-
widths <- lapply(grobs, function(g) sum(g$widths))
576-
widths <- inject(unit.c(!!!widths))
577-
heights <- lapply(grobs, function(g) sum(g$heights))
578-
heights <- inject(unit.c(!!!heights))
575+
widths <- lapply(grobs, `[[`, "widths")
576+
heights <- lapply(grobs, `[[`, "heights")
577+
578+
# Check whether legends are stretched in some direction
579+
stretch_x <- any(unlist(lapply(widths, unitType)) == "null")
580+
stretch_y <- any(unlist(lapply(heights, unitType)) == "null")
579581

580582
# Global justification of the complete legend box
581583
global_just <- paste0("legend.justification.", position)
@@ -605,6 +607,8 @@ Guides <- ggproto(
605607
box_xjust <- box_just[1]
606608
box_yjust <- box_just[2]
607609

610+
margin <- theme$legend.box.margin %||% margin()
611+
608612
# setting that is different for vertical and horizontal guide-boxes.
609613
if (identical(theme$legend.box, "horizontal")) {
610614
# Set justification for each legend within the box
@@ -615,13 +619,23 @@ Guides <- ggproto(
615619
height = heightDetails(grobs[[i]]))
616620
)
617621
}
618-
spacing <- theme$legend.spacing.x
622+
623+
spacing <- convertWidth(theme$legend.spacing.x, "cm")
624+
heights <- unit(height_cm(lapply(heights, sum)), "cm")
625+
626+
if (stretch_x) {
627+
widths <- redistribute_null_units(widths, spacing, margin, "width")
628+
vp_width <- unit(1, "npc")
629+
} else {
630+
widths <- inject(unit.c(!!!lapply(widths, sum)))
631+
vp_width <- sum(widths, spacing * (length(grobs) - 1L))
632+
}
619633

620634
# Set global justification
621635
vp <- viewport(
622636
x = global_xjust, y = global_yjust, just = global_just,
623637
height = max(heights),
624-
width = sum(widths, spacing * (length(grobs) - 1L))
638+
width = vp_width
625639
)
626640

627641
# Initialise gtable as legends in a row
@@ -643,12 +657,22 @@ Guides <- ggproto(
643657
width = widthDetails(grobs[[i]]))
644658
)
645659
}
646-
spacing <- theme$legend.spacing.y
660+
661+
spacing <- convertHeight(theme$legend.spacing.y, "cm")
662+
widths <- unit(width_cm(lapply(widths, sum)), "cm")
663+
664+
if (stretch_y) {
665+
heights <- redistribute_null_units(heights, spacing, margin, "height")
666+
vp_height <- unit(1, "npc")
667+
} else {
668+
heights <- inject(unit.c(!!!lapply(heights, sum)))
669+
vp_height <- sum(heights, spacing * (length(grobs) - 1L))
670+
}
647671

648672
# Set global justification
649673
vp <- viewport(
650674
x = global_xjust, y = global_yjust, just = global_just,
651-
height = sum(heights, spacing * (length(grobs) - 1L)),
675+
height = vp_height,
652676
width = max(widths)
653677
)
654678

@@ -664,7 +688,6 @@ Guides <- ggproto(
664688
}
665689

666690
# Add margins around the guide-boxes.
667-
margin <- theme$legend.box.margin %||% margin()
668691
guides <- gtable_add_padding(guides, margin)
669692

670693
# Add legend box background
@@ -678,6 +701,12 @@ Guides <- ggproto(
678701
)
679702

680703
# Set global margin
704+
if (stretch_x) {
705+
global_margin[c(2, 4)] <- unit(0, "cm")
706+
}
707+
if (stretch_y) {
708+
global_margin[c(1, 3)] <- unit(0, "cm")
709+
}
681710
guides <- gtable_add_padding(guides, global_margin)
682711

683712
guides$name <- "guide-box"
@@ -793,3 +822,46 @@ validate_guide <- function(guide) {
793822
}
794823
cli::cli_abort("Unknown guide: {guide}")
795824
}
825+
826+
redistribute_null_units <- function(units, spacing, margin, type = "width") {
827+
828+
has_null <- vapply(units, function(x) any(unitType(x) == "null"), logical(1))
829+
830+
# Early exit when we needn't bother with null units
831+
if (!any(has_null)) {
832+
units <- lapply(units, sum)
833+
units <- inject(unit.c(!!!units))
834+
return(units)
835+
}
836+
837+
# Get spacing between guides and margins in absolute units
838+
size <- switch(type, width = convertWidth, height = convertHeight)
839+
spacing <- size(spacing, "cm", valueOnly = TRUE)
840+
spacing <- sum(rep(spacing, length(units) - 1))
841+
margin <- switch(type, width = margin[c(2, 4)], height = margin[c(1, 3)])
842+
margin <- sum(size(margin, "cm", valueOnly = TRUE))
843+
844+
# Get the absolute parts of the unit
845+
absolute <- vapply(units, function(u) {
846+
u <- absolute.size(u)
847+
u <- size(u, "cm", valueOnly = TRUE)
848+
sum(u)
849+
}, numeric(1))
850+
absolute_sum <- sum(absolute) + spacing + margin
851+
852+
# Get the null parts of the unit
853+
relative <- rep(0, length(units))
854+
relative[has_null] <- vapply(units[has_null], function(u) {
855+
sum(as.numeric(u)[unitType(u) == "null"])
856+
}, numeric(1))
857+
relative_sum <- sum(relative)
858+
859+
if (relative_sum == 0) {
860+
return(unit(absolute, "cm"))
861+
}
862+
863+
relative <- relative / relative_sum
864+
available_space <- unit(1, "npc") - unit(absolute_sum, "cm")
865+
relative_space <- available_space * relative
866+
relative_space + unit(absolute, "cm")
867+
}

man/guide_bins.Rd

Lines changed: 5 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/guide_colourbar.Rd

Lines changed: 5 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/guide_coloursteps.Rd

Lines changed: 5 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/guide_legend.Rd

Lines changed: 5 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)