Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multiple inside guide box with different position #6210

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

Yunuuuu
Copy link

@Yunuuuu Yunuuuu commented Nov 28, 2024

Try to fix #5712.

This enables users to set legend.position.inside and legend.justification.inside in theme of guide_legend and guide_colourbar.

Here is an example:

ggplot(mpg, aes(displ, hwy, shape = drv, colour = cty, size = year)) +
    geom_point(aes(alpha = cyl)) +
    guides(
        colour = guide_colourbar(
            position = "inside",
            theme = theme(legend.position.inside = c(1, 0))
        ),
        size = guide_legend(position = "top"),
        alpha = guide_legend(
            position = "inside",
            theme = theme(legend.position.inside = c(0, 1))
        ),
        shape = guide_legend(position = "left")
    )

image

Copy link
Collaborator

@teunbrand teunbrand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Yunuuuu, thanks for preparing a PR for this issue!

The PR appears to break a visual test for the alignment of legends with the inside position. It is important to us to preserve current behaviour without regressions.

Next, it is hard to tell from the PR what the assembly rules for inside legends are. When legends share legend.position.inside values, they merge (are placed in the same guide-box). It seems that justification also factors into this and I'm not convinced that it should. It'd be great if the logic for this is expressed more clearly, perhaps through code comments.

Lastly, there are a few fussy changes to the code I'd like to propose, but it'd make more sense to go over this after the PR adresses the previous points.

@Yunuuuu
Copy link
Author

Yunuuuu commented Nov 29, 2024

@teunbrand Thank you for the reviewing. I have made the following changes based on your feedback:

  1. Added more comments and explanations throughout the code to improve clarity.
  2. Fixed the test error that was identified.
  3. Merged the guide legends based solely on legend.position.inside, as it seems sufficient to identify the legend groups.

@Yunuuuu
Copy link
Author

Yunuuuu commented Nov 29, 2024

I'll check it

@Yunuuuu
Copy link
Author

Yunuuuu commented Nov 29, 2024

Should we always keep the inside guide box, even if there are no inside guide legends?

@teunbrand
Copy link
Collaborator

Should we always keep the inside guide box, even if there are no inside guide legends?

An empty inside guide box should give a zeroGrob() for consistency in gtable layout

@Yunuuuu
Copy link
Author

Yunuuuu commented Nov 29, 2024

should fixed

Copy link
Collaborator

@teunbrand teunbrand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for getting the PR to work!
My overall feeling about this PR is that it is a little bit on the more invasive side, whereas I think it could be more surgical than it currently is.
If we lift the responsibility of managing the positions into Guides$assemble(), we should have to change less code elsewhere.

R/guide-legend.R Outdated Show resolved Hide resolved
R/guides-.R Outdated Show resolved Hide resolved
R/guides-.R Outdated Show resolved Hide resolved
R/guides-.R Outdated Show resolved Hide resolved
R/guides-.R Outdated
Comment on lines 587 to 607
if (startsWith(position, "inside")) {
# Global justification of the complete legend box
global_just <- valid.just(calc_element(
"legend.justification.inside", theme
))
# for inside guide legends, the position was attached in
# each grob of the input grobs (which should share the same position)
inside_position <- attr(.subset2(grobs, 1L), "inside_position") %||%
# fallback to original method of ggplot2 <=3.5.1
.subset2(theme, "legend.position.inside") %||% global_just
global_xjust <- global_just[1]
global_yjust <- global_just[2]
x <- inside_position[1]
y <- inside_position[2]
global_margin <- margin()
} else {
# Global justification of the complete legend box
global_just <- paste0("legend.justification.", position)
global_just <- valid.just(calc_element(global_just, theme))
x <- global_xjust <- global_just[1]
y <- global_yjust <- global_just[2]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might not even have to alter the logic of Guides$package_box() if we just manage everything well in Guides$assemble().

Copy link
Author

@Yunuuuu Yunuuuu Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solved, but I haven't found a way to avoid changing Guides$package_box().

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to explore this if you feel this gets stuck

R/guides-.R Outdated Show resolved Hide resolved
@Yunuuuu
Copy link
Author

Yunuuuu commented Nov 29, 2024

I'll try to make some new changes

@Yunuuuu
Copy link
Author

Yunuuuu commented Nov 29, 2024

Thank you for your thorough review and all the valuable advice.

@Yunuuuu
Copy link
Author

Yunuuuu commented Nov 29, 2024

After writing some examples, I’ve realized that it’s necessary to allow users to set the inside justification for each inside legend. In this way, it would be beneficial for splitting legends based on their justifications, as users may be confused about which justification will be applied if we only split legends with the same inside position into groups.

@Yunuuuu
Copy link
Author

Yunuuuu commented Nov 29, 2024

I’m unable to run the tests on my local machine, as there are always some errors. I’m unable to update the test snapshots.

Copy link
Collaborator

@teunbrand teunbrand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the changes, Yunuuuu!
I apologise but I still have a few more to suggest. I'd be happy to help out trying to find a solution for not editing Guides$package_box() as much as is reasonable.

I’m unable to update the test snapshots.

I'm happy to do this for you if you like.

Comment on lines +525 to +529
if (is.null(inside_just)) {
inside_justs[[i]] <- default_inside_just
} else {
inside_justs[[i]] <- valid.just(inside_just)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (is.null(inside_just)) {
inside_justs[[i]] <- default_inside_just
} else {
inside_justs[[i]] <- valid.just(inside_just)
}
inside_justs[[i]] <- valid.just(inside_just %||% default_inside_just)

Comment on lines +518 to +519
for (i in seq_along(positions)) {
if (identical(positions[i], "inside")) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (i in seq_along(positions)) {
if (identical(positions[i], "inside")) {
for (i in seq_along(positions)[positions == "inside"]) {

Comment on lines +541 to +557
positions <- positions[keep]
inside_positions <- inside_positions[keep]
inside_justs <- inside_justs[keep]
groups <- groups[keep]

# we group the guide legends
locs <- vec_group_loc(groups)
indices <- locs$loc
grobs <- vec_chop(grobs, indices = indices)
names(grobs) <- locs$key

# for each group, they share the same locations,
# so we only extract the first one of `positions` and `inside_positions`
first_indice <- lapply(indices, `[[`, 1L)
positions <- vec_chop(positions, indices = first_indice)
inside_positions <- vec_chop(inside_positions, indices = first_indice)
inside_justs <- vec_chop(inside_justs, indices = first_indice)
Copy link
Collaborator

@teunbrand teunbrand Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be simplified by tracking positions/inside_positions/inside_justs in a data.frame.
vec_group_loc() treats rows that are the same as the same group, and the locs$key should allow you to skip having to subset the first index.

Comment on lines +518 to +533
inside_legends <- legends[startsWith(names(legends), "inside")]
if (length(inside_legends)) {
for (i in seq_along(inside_legends)) {
table <- gtable_add_grob(
table, inside_legends[[i]], clip = "off",
t = place$t, b = place$b, l = place$l, r = place$r,
name = paste("guide-box-inside", i, sep = "-")
)
}
} else { # to be consistent with original gtable layout
table <- gtable_add_grob(
table, zeroGrob(), clip = "off",
t = place$t, b = place$b, l = place$l, r = place$r,
name = "guide-box-inside"
)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be possible to avoid this by combining all packaged inside guides into a single grob.

Comment on lines +578 to 582
positions <- positions %||% vapply(
params,
function(p) p$position[1] %||% default_position,
character(1)
function(p) p$position[1] %||% "right",
character(1), USE.NAMES = FALSE
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we assume here that the Guides$draw() method receives a well-formed positions argument, so we don't have to fall back here? You could even change positions = NULL to positions in the arguments to signal that it is a required argument.

Comment on lines +605 to +606
package_box = function(grobs, position, theme,
inside_position = NULL, inside_just = NULL) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we could prevent having to edit this method.
We can just make a theme for every iteration that has the correct inside_position and inside_just baked in.

Comment on lines +584 to +589
directions <- ifelse(
positions %in% c("top", "bottom"),
"horizontal", "vertical"
)
} else {
directions <- rep(direction, length(positions))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not understanding this change. The else branch repeats NULL some times, which remains NULL because it has 0-length?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Multiple legends with position="inside"
2 participants