Skip to content

Commit dad8a4b

Browse files
authored
Arbitrary positions for guides (#5488)
* Add legends in all positions * Assemble separate guide boxes * Add position argument to guides * reoxygenate * adapt tests * deal with old R units * rename manual position to "inside" * resolve spacing once * omit 'inside' option in justification * Move more responsibility to `Guides$draw()` * Propagate "manual" -> "inside" rename * Fallback for inside position * Rearrange methods into logical order * remove vestigial stuff * Separate numeric inside positioning from `legend.position` argument * Implement plot-wise justification (#4020) * Partially revert bd917cf * Add extra justification theme settings * Document `legend.justification.{position}` * Apply justification * Prevent FP warnings by partial matching * Switch to new inside position * Add test for justification per position * Fix subsetting bug * always add gtable rows/cols * adjust table dimension expectations * adapt test * Don't calculate key sizes twice * Use `calc_element()` * Use conventional indexing * prevent partial matching * Move justification responsiblity to `Guides$package_box()` * Fix bug * incorporate guide_custom * incorporate guide_custom
1 parent 70a4c0e commit dad8a4b

18 files changed

+554
-166
lines changed

R/guide-bins.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ guide_bins <- function(
100100
ticks.length = unit(0.2, "npc"),
101101

102102
# general
103+
position = NULL,
103104
direction = NULL,
104105
default.unit = "line",
105106
override.aes = list(),
@@ -121,6 +122,9 @@ guide_bins <- function(
121122
if (!is.null(title.position)) {
122123
title.position <- arg_match0(title.position, .trbl)
123124
}
125+
if (!is.null(position)) {
126+
position <- arg_match0(position, c(.trbl, "inside"))
127+
}
124128
if (!is.null(direction)) {
125129
direction <- arg_match0(direction, c("horizontal", "vertical"))
126130
}
@@ -169,6 +173,7 @@ guide_bins <- function(
169173
ticks_length = ticks.length,
170174

171175
# general
176+
position = position,
172177
direction = direction,
173178
override.aes = rename_aes(override.aes),
174179
reverse = reverse,

R/guide-colorbar.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ guide_colourbar <- function(
151151
draw.llim = TRUE,
152152

153153
# general
154+
position = NULL,
154155
direction = NULL,
155156
default.unit = "line",
156157
reverse = FALSE,
@@ -171,6 +172,9 @@ guide_colourbar <- function(
171172
if (!is.null(title.position)) {
172173
title.position <- arg_match0(title.position, .trbl)
173174
}
175+
if (!is.null(position)) {
176+
position <- arg_match0(position, c(.trbl, "inside"))
177+
}
174178
if (!is.null(direction)) {
175179
direction <- arg_match0(direction, c("horizontal", "vertical"))
176180
}
@@ -240,6 +244,7 @@ guide_colourbar <- function(
240244
draw_lim = c(isTRUE(draw.llim), isTRUE(draw.ulim)),
241245

242246
# general
247+
position = position,
243248
direction = direction,
244249
reverse = reverse,
245250
order = order,

R/guide-custom.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
guide_custom <- function(
4444
grob, width = grobWidth(grob), height = grobHeight(grob),
4545
title = NULL, title.position = "top", margin = NULL,
46-
position = waiver(), order = 0
46+
position = NULL, order = 0
4747
) {
4848
check_object(grob, is.grob, "a {.cls grob} object")
4949
check_object(width, is.unit, "a {.cls unit} object")

R/guide-legend.R

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
#' object specifying the distance between key-label pairs in the horizontal
4747
#' direction (`key.spacing.x`), vertical direction (`key.spacing.y`) or both
4848
#' (`key.spacing`).
49+
#' @param position A character string indicating where the legend should be
50+
#' placed relative to the plot panels.
4951
#' @param direction A character string indicating the direction of the guide.
5052
#' One of "horizontal" or "vertical."
5153
#' @param default.unit A character string indicating [grid::unit()]
@@ -152,6 +154,7 @@ guide_legend <- function(
152154
key.spacing.y = NULL,
153155

154156
# General
157+
position = NULL,
155158
direction = NULL,
156159
default.unit = "line",
157160
override.aes = list(),
@@ -187,6 +190,9 @@ guide_legend <- function(
187190
if (!is.null(label.position)) {
188191
label.position <- arg_match0(label.position, .trbl)
189192
}
193+
if (!is.null(position)) {
194+
position <- arg_match0(position, c(.trbl, "inside"))
195+
}
190196

191197
new_guide(
192198
# Title
@@ -217,6 +223,7 @@ guide_legend <- function(
217223
byrow = byrow,
218224
reverse = reverse,
219225
order = order,
226+
position = position,
220227

221228
# Fixed parameters
222229
available_aes = "any",

R/guides-.R

Lines changed: 152 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,8 @@ Guides <- ggproto(
262262

263263
## Building ------------------------------------------------------------------
264264

265-
# The `Guides$build()` method is called in ggplotGrob (plot-build.R) and makes
266-
# the guide box for *non-position* scales.
265+
# The `Guides$build()` method is called in ggplot_build (plot-build.R) and
266+
# collects all information needed from the plot.
267267
# Note that position scales are handled in `Coord`s, which have their own
268268
# procedures to do equivalent steps.
269269
#
@@ -283,12 +283,7 @@ Guides <- ggproto(
283283
# 3. Guides$process_layers()
284284
# process layer information and generate geom info.
285285
#
286-
# 4. Guides$draw()
287-
# generate guide grob from each guide object
288-
# one guide grob for one guide object
289-
#
290-
# 5. Guides$assemble()
291-
# arrange all guide grobs
286+
# The resulting guide is then drawn in ggplot_gtable
292287

293288
build = function(self, scales, layers, labels, layer_data) {
294289

@@ -476,104 +471,201 @@ Guides <- ggproto(
476471
invisible()
477472
},
478473

479-
# Loop over every guide, let them draw their grobs
480-
draw = function(self, theme, position, direction) {
481-
Map(
482-
function(guide, params) guide$draw(theme, position, direction, params),
483-
guide = self$guides,
484-
params = self$params
485-
)
486-
},
487-
474+
# The `Guides$assemble()` method is called in ggplot_gtable (plot-build.R) and
475+
# applies the styling from the theme to render each guide and package them
476+
# into guide boxes.
477+
#
478+
# The procedure is as follows
479+
#
480+
# 1. Guides$draw()
481+
# for every guide object, draw one grob,
482+
# then group the grobs in a list per position
483+
#
484+
# 2. Guides$package_box()
485+
# for every position, collect all individual guides and arrange them
486+
# into a guide box which will be inserted into the main gtable
488487
# Combining multiple guides in a guide box
489-
assemble = function(self, theme, position) {
488+
assemble = function(self, theme) {
490489

491490
if (length(self$guides) < 1) {
492491
return(zeroGrob())
493492
}
494493

495-
position <- legend_position(position)
496-
if (position == "none") {
494+
default_position <- theme$legend.position %||% "right"
495+
if (length(default_position) == 2) {
496+
default_position <- "inside"
497+
}
498+
if (default_position == "none") {
497499
return(zeroGrob())
498500
}
499-
default_direction <- if (position == "inside") "vertical" else position
500501

501-
theme$legend.key.width <- theme$legend.key.width %||% theme$legend.key.size
502-
theme$legend.key.height <- theme$legend.key.height %||% theme$legend.key.size
503-
theme$legend.box <- theme$legend.box %||% default_direction
504-
theme$legend.direction <- theme$legend.direction %||% default_direction
505-
theme$legend.box.just <- theme$legend.box.just %||% switch(
506-
position,
507-
inside = c("center", "center"),
508-
vertical = c("left", "top"),
509-
horizontal = c("center", "top")
510-
)
502+
# Populate key sizes
503+
theme$legend.key.width <- calc_element("legend.key.width", theme)
504+
theme$legend.key.height <- calc_element("legend.key.height", theme)
511505

512-
grobs <- self$draw(theme, position, theme$legend.direction)
506+
grobs <- self$draw(theme, default_position, theme$legend.direction)
513507
if (length(grobs) < 1) {
514508
return(zeroGrob())
515509
}
516510
grobs <- grobs[order(names(grobs))]
517511

518512
# Set spacing
519-
theme$legend.spacing <- theme$legend.spacing %||% unit(0.5, "lines")
520-
theme$legend.spacing.y <- theme$legend.spacing.y %||% theme$legend.spacing
521-
theme$legend.spacing.x <- theme$legend.spacing.x %||% theme$legend.spacing
513+
theme$legend.spacing <- theme$legend.spacing %||% unit(0.5, "lines")
514+
theme$legend.spacing.y <- calc_element("legend.spacing.y", theme)
515+
theme$legend.spacing.x <- calc_element("legend.spacing.x", theme)
516+
517+
Map(
518+
grobs = grobs,
519+
position = names(grobs),
520+
self$package_box,
521+
MoreArgs = list(theme = theme)
522+
)
523+
},
524+
525+
# Render the guides into grobs
526+
draw = function(self, theme,
527+
default_position = "right",
528+
direction = NULL,
529+
params = self$params,
530+
guides = self$guides) {
531+
positions <- vapply(
532+
params,
533+
function(p) p$position[1] %||% default_position,
534+
character(1)
535+
)
536+
positions <- factor(positions, levels = c(.trbl, "inside"))
537+
538+
directions <- rep(direction %||% "vertical", length(positions))
539+
if (is.null(direction)) {
540+
directions[positions %in% c("top", "bottom")] <- "horizontal"
541+
}
542+
543+
grobs <- vector("list", length(guides))
544+
for (i in seq_along(grobs)) {
545+
grobs[[i]] <- guides[[i]]$draw(
546+
theme = theme, position = as.character(positions[i]),
547+
direction = directions[i], params = params[[i]]
548+
)
549+
}
550+
split(grobs, positions)
551+
},
552+
553+
package_box = function(grobs, position, theme) {
554+
555+
if (is.zero(grobs) || length(grobs) == 0) {
556+
return(zeroGrob())
557+
}
558+
559+
# Determine default direction
560+
direction <- switch(
561+
position,
562+
inside = , left = , right = "vertical",
563+
top = , bottom = "horizontal"
564+
)
565+
566+
# Populate missing theme arguments
567+
theme$legend.box <- theme$legend.box %||% direction
568+
theme$legend.box.just <- theme$legend.box.just %||% switch(
569+
direction,
570+
vertical = c("left", "top"),
571+
horizontal = c("center", "top")
572+
)
522573

523574
# Measure guides
524575
widths <- lapply(grobs, function(g) sum(g$widths))
525576
widths <- inject(unit.c(!!!widths))
526577
heights <- lapply(grobs, function(g) sum(g$heights))
527578
heights <- inject(unit.c(!!!heights))
528579

580+
# Global justification of the complete legend box
581+
global_just <- paste0("legend.justification.", position)
582+
global_just <- valid.just(calc_element(global_just, theme))
583+
584+
if (position == "inside") {
585+
# The position of inside legends are set by their justification
586+
inside_position <- theme$legend.position.inside %||% global_just
587+
global_xjust <- inside_position[1]
588+
global_yjust <- inside_position[2]
589+
global_margin <- margin()
590+
} else {
591+
global_xjust <- global_just[1]
592+
global_yjust <- global_just[2]
593+
# Legends to the side of the plot need a margin for justification
594+
# relative to the plot panel
595+
global_margin <- margin(
596+
t = 1 - global_yjust, b = global_yjust,
597+
r = 1 - global_xjust, l = global_xjust,
598+
unit = "null"
599+
)
600+
}
601+
529602
# Set the justification of each legend within the legend box
530603
# First value is xjust, second value is yjust
531-
just <- valid.just(theme$legend.box.just)
532-
xjust <- just[1]
533-
yjust <- just[2]
604+
box_just <- valid.just(theme$legend.box.just)
605+
box_xjust <- box_just[1]
606+
box_yjust <- box_just[2]
534607

535608
# setting that is different for vertical and horizontal guide-boxes.
536609
if (identical(theme$legend.box, "horizontal")) {
537-
# Set justification for each legend
610+
# Set justification for each legend within the box
538611
for (i in seq_along(grobs)) {
539612
grobs[[i]] <- editGrob(
540613
grobs[[i]],
541-
vp = viewport(x = xjust, y = yjust, just = c(xjust, yjust),
614+
vp = viewport(x = box_xjust, y = box_yjust, just = box_just,
542615
height = heightDetails(grobs[[i]]))
543616
)
544617
}
618+
spacing <- theme$legend.spacing.x
619+
620+
# Set global justification
621+
vp <- viewport(
622+
x = global_xjust, y = global_yjust, just = global_just,
623+
height = max(heights),
624+
width = sum(widths, spacing * (length(grobs) - 1L))
625+
)
545626

546-
guides <- gtable_row(name = "guides",
547-
grobs = grobs,
548-
widths = widths, height = max(heights))
627+
# Initialise gtable as legends in a row
628+
guides <- gtable_row(
629+
name = "guides", grobs = grobs,
630+
widths = widths, height = max(heights),
631+
vp = vp
632+
)
549633

550-
# add space between the guide-boxes
551-
guides <- gtable_add_col_space(guides, theme$legend.spacing.x)
634+
# Add space between the guide-boxes
635+
guides <- gtable_add_col_space(guides, spacing)
552636

553637
} else { # theme$legend.box == "vertical"
554-
# Set justification for each legend
638+
# Set justification for each legend within the box
555639
for (i in seq_along(grobs)) {
556640
grobs[[i]] <- editGrob(
557641
grobs[[i]],
558-
vp = viewport(x = xjust, y = yjust, just = c(xjust, yjust),
642+
vp = viewport(x = box_xjust, y = box_yjust, just = box_just,
559643
width = widthDetails(grobs[[i]]))
560644
)
561645
}
646+
spacing <- theme$legend.spacing.y
647+
648+
# Set global justification
649+
vp <- viewport(
650+
x = global_xjust, y = global_yjust, just = global_just,
651+
height = sum(heights, spacing * (length(grobs) - 1L)),
652+
width = max(widths)
653+
)
562654

563-
guides <- gtable_col(name = "guides",
564-
grobs = grobs,
565-
width = max(widths), heights = heights)
655+
# Initialise gtable as legends in a column
656+
guides <- gtable_col(
657+
name = "guides", grobs = grobs,
658+
width = max(widths), heights = heights,
659+
vp = vp
660+
)
566661

567-
# add space between the guide-boxes
568-
guides <- gtable_add_row_space(guides, theme$legend.spacing.y)
662+
# Add space between the guide-boxes
663+
guides <- gtable_add_row_space(guides, spacing)
569664
}
570665

571666
# Add margins around the guide-boxes.
572667
margin <- theme$legend.box.margin %||% margin()
573-
guides <- gtable_add_cols(guides, margin[4], pos = 0)
574-
guides <- gtable_add_cols(guides, margin[2], pos = ncol(guides))
575-
guides <- gtable_add_rows(guides, margin[1], pos = 0)
576-
guides <- gtable_add_rows(guides, margin[3], pos = nrow(guides))
668+
guides <- gtable_add_padding(guides, margin)
577669

578670
# Add legend box background
579671
background <- element_grob(theme$legend.box.background %||% element_blank())
@@ -584,6 +676,10 @@ Guides <- ggproto(
584676
z = -Inf, clip = "off",
585677
name = "legend.box.background"
586678
)
679+
680+
# Set global margin
681+
guides <- gtable_add_padding(guides, global_margin)
682+
587683
guides$name <- "guide-box"
588684
guides
589685
},

0 commit comments

Comments
 (0)