Skip to content

Commit 23ab492

Browse files
authored
multiple inside guide box with different position (#6210)
* multiple inside guide box with different position * merge guide legends depend on `legend.position.inside` only * fix inside legend justification * no inside guide box if no inside guide legends * add `guide-box-index` when there is no inside legends * fix inside guide box area * manage position in `Guides$assemble()` * revert * revert * revert * fix inside legend coordinates * fix test error * no need to prepare inside legends when empty * allow set the inside justification for each legend * test multiple inside legends with different positions * fix R CMD check error * code notes * Update R/guides-.R Co-authored-by: Teun van den Brand <[email protected]> * apply suggestion Co-authored-by: Teun van den Brand <[email protected]> * avoid modify package_box * accept snapshot * Update R/guides-.R Co-authored-by: Teun van den Brand <[email protected]> * accept the suggestion * new bullet * try to linearise logic * tweak formatting
1 parent 73b4119 commit 23ab492

File tree

4 files changed

+187
-25
lines changed

4 files changed

+187
-25
lines changed

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# ggplot2 (development version)
22

3+
* `guide_*()` can now accept two inside legend theme elements:
4+
`legend.position.inside` and `legend.justification.inside`, allowing inside
5+
legends to be placed at different positions. Only inside legends with the same
6+
position and justification will be merged (@Yunuuuu, #6210).
37
* New stat: `stat_manual()` for arbitrary computations (@teunbrand, #3501)
48
* Reversal of a dimension, typically 'x' or 'y', is now controlled by the
59
`reverse` argument in `coord_cartesian()`, `coord_fixed()`, `coord_radial()`

R/guides-.R

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ Guides <- ggproto(
471471
# for every position, collect all individual guides and arrange them
472472
# into a guide box which will be inserted into the main gtable
473473
# Combining multiple guides in a guide box
474-
assemble = function(self, theme) {
474+
assemble = function(self, theme, params = self$params, guides = self$guides) {
475475

476476
if (length(self$guides) < 1) {
477477
return(zeroGrob())
@@ -485,42 +485,95 @@ Guides <- ggproto(
485485
return(zeroGrob())
486486
}
487487

488+
# extract the guide position
489+
positions <- vapply(
490+
params,
491+
function(p) p$position[1] %||% default_position,
492+
character(1), USE.NAMES = FALSE
493+
)
494+
488495
# Populate key sizes
489496
theme$legend.key.width <- calc_element("legend.key.width", theme)
490497
theme$legend.key.height <- calc_element("legend.key.height", theme)
491498

492-
grobs <- self$draw(theme, default_position, theme$legend.direction)
499+
grobs <- self$draw(theme, positions, theme$legend.direction)
500+
keep <- !vapply(grobs, is.zero, logical(1), USE.NAMES = FALSE)
501+
grobs <- grobs[keep]
493502
if (length(grobs) < 1) {
494503
return(zeroGrob())
495504
}
496-
grobs <- grobs[order(names(grobs))]
505+
506+
# prepare the position of inside legends
507+
default_inside_just <- calc_element("legend.justification.inside", theme)
508+
default_inside_position <- calc_element("legend.position.inside", theme)
509+
510+
groups <- data_frame0(
511+
positions = positions,
512+
justs = list(NULL),
513+
coords = list(NULL)
514+
)
515+
516+
# we grouped the legends by the positions, for inside legends, they'll be
517+
# splitted by the actual inside coordinate
518+
for (i in which(positions == "inside")) {
519+
# the actual inside position and justification can be set in each guide
520+
# by `theme` argument, here, we won't use `calc_element()` which will
521+
# use inherits from `legend.justification` or `legend.position`, we only
522+
# follow the inside elements from the guide theme
523+
just <- params[[i]]$theme[["legend.justification.inside"]]
524+
just <- valid.just(just %||% default_inside_just)
525+
coord <- params[[i]]$theme[["legend.position.inside"]]
526+
coord <- coord %||% default_inside_position %||% just
527+
528+
groups$justs[[i]] <- just
529+
groups$coord[[i]] <- coord
530+
}
531+
532+
groups <- vec_group_loc(vec_slice(groups, keep))
533+
grobs <- vec_chop(grobs, indices = groups$loc)
534+
names(grobs) <- groups$key$positions
497535

498536
# Set spacing
499537
theme$legend.spacing <- theme$legend.spacing %||% unit(0.5, "lines")
500538
theme$legend.spacing.y <- calc_element("legend.spacing.y", theme)
501539
theme$legend.spacing.x <- calc_element("legend.spacing.x", theme)
502540

503-
Map(
504-
grobs = grobs,
505-
position = names(grobs),
506-
self$package_box,
507-
MoreArgs = list(theme = theme)
508-
)
541+
# prepare output
542+
for (i in vec_seq_along(groups)) {
543+
adjust <- NULL
544+
position <- groups$key$position[i]
545+
if (position == "inside") {
546+
adjust <- theme(
547+
legend.position.inside = groups$key$coord[[i]],
548+
legend.justification.inside = groups$key$justs[[i]]
549+
)
550+
}
551+
grobs[[i]] <- self$package_box(grobs[[i]], position, theme + adjust)
552+
}
553+
554+
# merge inside grobs into single gtable
555+
is_inside <- names(grobs) == "inside"
556+
if (sum(is_inside) > 1) {
557+
inside <- gtable(unit(1, "npc"), unit(1, "npc"))
558+
inside <- gtable_add_grob(
559+
inside, grobs[is_inside],
560+
t = 1, l = 1, clip = "off",
561+
name = paste0("guide-box-inside-", seq_len(sum(is_inside)))
562+
)
563+
grobs <- grobs[!is_inside]
564+
grobs$inside <- inside
565+
}
566+
567+
# fill in missing guides
568+
grobs[setdiff(c(.trbl, "inside"), names(grobs))] <- list(zeroGrob())
569+
570+
grobs
509571
},
510572

511573
# Render the guides into grobs
512-
draw = function(self, theme,
513-
default_position = "right",
514-
direction = NULL,
574+
draw = function(self, theme, positions, direction = NULL,
515575
params = self$params,
516576
guides = self$guides) {
517-
positions <- vapply(
518-
params,
519-
function(p) p$position[1] %||% default_position,
520-
character(1)
521-
)
522-
positions <- factor(positions, levels = c(.trbl, "inside"))
523-
524577
directions <- rep(direction %||% "vertical", length(positions))
525578
if (is.null(direction)) {
526579
directions[positions %in% c("top", "bottom")] <- "horizontal"
@@ -529,14 +582,16 @@ Guides <- ggproto(
529582
grobs <- vector("list", length(guides))
530583
for (i in seq_along(grobs)) {
531584
grobs[[i]] <- guides[[i]]$draw(
532-
theme = theme, position = as.character(positions[i]),
585+
theme = theme, position = positions[i],
533586
direction = directions[i], params = params[[i]]
534587
)
535588
}
536-
keep <- !vapply(grobs, is.zero, logical(1))
537-
split(grobs[keep], positions[keep])
589+
grobs
538590
},
539591

592+
# here, we put `inside_position` and `inside_just` in the last, so that it
593+
# won't break current implement of patchwork, which depends on the top three
594+
# arguments to collect guides
540595
package_box = function(grobs, position, theme) {
541596

542597
if (is.zero(grobs) || length(grobs) == 0) {
@@ -699,7 +754,6 @@ Guides <- ggproto(
699754
guides$name <- "guide-box"
700755
guides
701756
},
702-
703757
## Utilities -----------------------------------------------------------------
704758

705759
print = function(self) {
Lines changed: 85 additions & 0 deletions
Loading

tests/testthat/test-guides.R

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ test_that("empty guides are dropped", {
156156
expect_equal(nrow(gd), 0)
157157

158158
# Draw guides
159-
guides <- p$plot$guides$draw(theme_gray(), direction = "vertical")
159+
guides <- p$plot$guides$assemble(theme_gray())
160160

161161
# All guide-boxes should be empty
162-
expect_equal(lengths(guides, use.names = FALSE), rep(0, 5))
162+
expect_true(is.zero(guides))
163163
})
164164

165165
test_that("bins can be parsed by guides for all scale types", {
@@ -282,6 +282,25 @@ test_that("guides are positioned correctly", {
282282
expect_doppelganger("legend inside plot, bottom left of legend at center",
283283
p2 + theme(legend.justification = c(0,0), legend.position.inside = c(0.5,0.5))
284284
)
285+
expect_doppelganger("legend inside plot, multiple positions",
286+
p2 +
287+
guides(
288+
colour = guide_colourbar(
289+
position = "inside",
290+
theme = theme(
291+
legend.position.inside = c(0, 1),
292+
legend.justification.inside = c(0, 1)
293+
)
294+
),
295+
fill = guide_legend(
296+
position = "inside",
297+
theme = theme(
298+
legend.position.inside = c(1, 0),
299+
legend.justification.inside = c(1, 0)
300+
)
301+
)
302+
)
303+
)
285304
})
286305

287306
test_that("guides title and text are positioned correctly", {

0 commit comments

Comments
 (0)