Skip to content

Independent justification of legends #6417

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

Open
bakaburg1 opened this issue Apr 10, 2025 · 12 comments · May be fixed by #6431
Open

Independent justification of legends #6417

bakaburg1 opened this issue Apr 10, 2025 · 12 comments · May be fixed by #6431
Labels
feature a feature request or enhancement guides 📏

Comments

@bakaburg1
Copy link

bakaburg1 commented Apr 10, 2025

Hello,

I'm trying to reproduce a map like the following:

Image

but I cannot find a way to have two different legends both positioned on one side but one on top and one below:

ggplot(rnaturalearth::ne_countries() |> mutate(value = sample(1:10, n(), replace = T))) +
    geom_sf(
        aes(fill = value, color = value, geometry = geometry)
    ) +
    guides(
        fill = guide_colorbar(
            position = "left",
            theme = theme(
                legend.justification.left = "top" 
            )
        ),
        color = guide_legend(
            position = "left",
            theme = theme(
                legend.justification.left = "bottom" 
            )
        )
        ) +
    theme_minimal() 
```r

![Image](https://github.com/user-attachments/assets/d2f5d70e-86c1-4bb1-b749-b3d759019c64)
(image rendering is not working for some reason but you can see them at the links)

This is a silly but reproducible example, I had to override a number of ggproto to get an aspect similar to the original.

"legend.justification.left" inside guides is ignored.

It works if used on the general theme(), but then is applied to both legends:

```r
ggplot(rnaturalearth::ne_countries() |> mutate(value = sample(1:10, n(), replace = T))) +
    geom_sf(
        aes(fill = value, color = value, geometry = geometry)
    ) +
    guides(
        fill = guide_colorbar(
            position = "left",
            theme = theme(
                legend.justification.left = "top" 
            )
        ),
        color = guide_legend(
            position = "left",
            theme = theme(
                legend.justification.left = "bottom" 
            )
        )
        ) +
    theme_minimal() +
    theme(
        legend.justification.left = "bottom" 
    )
```r

![Image](https://github.com/user-attachments/assets/09224b7c-753b-4051-8632-a5bfb5219c7d)

Am I doing something wrong, or is it a known limitation?

The only workaround I found is to use legend.spacing.y = rel(7) (any suggestion for a better unit() is welcome), but it's totally ad hoc and depends on the legends' lengths and numbers (which in the real plot may indeed change). Also, it would be great to have a guide specific to legend.spacing.y...

If this is a current limitation, can you guide me on which ggproto method to override? The goal is to justify the custom legend independently of the other ones.
@teunbrand
Copy link
Collaborator

The development documentation reads under guide_legend(theme):

Arguments that apply to combined legends (the legend box) are ignored, including legend.position, ⁠legend.justification.*⁠, legend.location and ⁠legend.box.*⁠.

So this is currently not possible. The ggproto object in charge is Guides, but this is an internal class not intended for extension.

@bakaburg1
Copy link
Author

This is a pity. I'd like to mark it as a feature request then.

In the meantime, if you can point me to how to extend the Guide proto to get this, I'd be grateful. In my final plot I'm already using a modified version of GuideLegend anyway (to use the same colors for the small country legend as in the main fill legend).

@teunbrand teunbrand added the feature a feature request or enhancement label Apr 10, 2025
@teunbrand
Copy link
Collaborator

There is some guidance on extending the Guide class here: https://ggplot2.tidyverse.org/articles/extending-ggplot2.html#creating-new-guides. But as you're modifying GuideLegend, you might already have seen this.

The legend justification is a property of the guide-box, which is an ephemoral structure without associated class. There is no extending the guide-box. It is managed by the Guides class, but nobody should try to extend this either.

@Yunuuuu
Copy link
Contributor

Yunuuuu commented Apr 10, 2025

A workaround for this is to use inside guide legends, which support separate justifications and positions with the development version of ggplot2. However, you’ll need to adjust the plot margins accordingly:

library(ggplot2)
rnaturalearth::ne_countries() |>
  dplyr::mutate(value = sample(1:10, dplyr::n(), replace = TRUE)) |>
  ggplot() +
  geom_sf(
    aes(fill = value, color = value, geometry = geometry)
  ) +
  guides(
    fill = guide_colorbar(
      position = "inside",
      theme = theme(
        legend.justification.inside = c(1.5, 1),
        legend.position.inside = c(0, 1)
      )
    ),
    color = guide_legend(
      position = "inside",
      theme = theme(
        legend.justification.inside = c(1.5, 0),
        legend.position.inside = c(0, 0)
      )
    )
  ) +
  theme_minimal() +
  theme(plot.margin = margin(l = 50))

Image

@bakaburg1
Copy link
Author

bakaburg1 commented Apr 12, 2025

ok, very odd, your code produces this for me:

Image

I literally copy pasted your code in callr::r_vanilla call to avoid any interaction.

I also tried with the development version but that gave me:
Error in make_title(...) : unused arguments (list(), NULL)

@Yunuuuu
Copy link
Contributor

Yunuuuu commented Apr 12, 2025

It seems you're still using a version of ggplot2 that doesn't support multiple inside guide legends. Could you try reinstalling the development version of ggplot2 and test again? Also, would you mind sharing the output of sessioninfo::session_info("ggplot2", info = "packages") so we can double-check the version?

@bakaburg1
Copy link
Author

ok, now it works thanks!
I can worki with this, but it would be nice if there was a more propoer solution not involving manual addition. better than nothing at least!

@Yunuuuu
Copy link
Contributor

Yunuuuu commented Apr 13, 2025

That would depend on the core ggplot2 developers, but in my opinion, it's quite difficult to implement this feature. Since guide legends around the plot can only occupy a single position (top, left, bottom, or right), it's challenging to avoid overlaps when multiple guide legends with different justifications are involved. However, I do like the feature requested on StackOverflow: stackoverflow:https://stackoverflow.com/questions/52060601/ggplot-multiple-legends-arrangement, which could help address the overlapping issue.

@teunbrand
Copy link
Collaborator

How would you propose the following would be resolved? E.g. we want shape and size to be at the top, but colour to be at the middle. However, shape and size already take op more than half of the height, so there is no place to position the colour legend.

What should happen? Does that change when we resize the plotting window so that enough space becomes available? Do we group legends first based on their justification values and then place them?

library(ggplot2)
ggplot(mpg, aes(displ, hwy)) +
  geom_point(aes(shape = factor(year), colour = drv, size = cty)) +
  guides(
    shape  = guide_legend(theme = theme(legend.justification.right = c(0, 1))),
    size   = guide_legend(theme = theme(legend.justification.right = c(0, 1))),
    colour = guide_legend(theme = theme(legend.justification.right = c(0, 0.5)))
  )

Created on 2025-04-14 with reprex v2.1.1

@Yunuuuu
Copy link
Contributor

Yunuuuu commented Apr 14, 2025

Yes, I do think that this feature is challenging to implement.

However, I think the multiple legends arrangement should be a possible feature—similar to how facet_wrap() arranges facet panels. Users could be allowed to control the layout of a whole guide groups (multiple guide legends) by specifying the number of rows or columns (nrow, ncol), and also the nrow or ncol for the individual legend keys themselves to fine-tune spacing.

If that works, we could arrange the shape and size guides side by side in two columns, and then place color below or in the middle section. But I agree—it might still be tricky to make it robust, especially under dynamic layout constraints.

Just like Legend 1 and Legend 3 (shape and size), Legend 2 (color)
Image

@teunbrand
Copy link
Collaborator

Thanks for the input Yunuuuu!

However, I think the multiple legends arrangement should be a possible feature

Can we give this separate consideration in a new issue?

As for this current issue, perhaps we should allow theme(legend.spacing = unit(1, "null")) to indicate that we want to spread legends across the available space?

@teunbrand teunbrand linked a pull request Apr 15, 2025 that will close this issue
@Yunuuuu
Copy link
Contributor

Yunuuuu commented Apr 17, 2025

Thanks, I'll open a new issue for the multiple legends arrangement as a feature request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature a feature request or enhancement guides 📏
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants