7

I am trying to create a plot that combines 2 separate legends and a grid of multiple plots. The issue I'm having is I'm finding it difficult to align the legends so they are visible and not overlapping. hopefully the example below will explain what I mean.

To begin I am going to create 2 plots. In these two plots I am only interested in the legends, and I am discarding the actual plot (so please ignore the actual plots in these two plots). To get just the legend I am using the cowplot package.

library(ggplot2)
library(cowplot)
# -------------------------------------------------------------------------
# plot 1 ------------------------------------------------------------------

# create fake data
dfLegend_1 <- data.frame(x = LETTERS[1:10], y = c(1:10))
# set colours
pointColours <- c(A = "#F5736A", B = "#D58D00", C = "#A0A300",
                  D = "#36B300", E = "#00BC7B", F = "#00BCC2",
                  G = "#00ADF4", H = "#928DFF", I = "#E568F0",
                  J = "#808080")

# plot
ggLegend_1 <- ggplot(dfLegend_1, aes(x=x, y=y))+
  geom_point(aes(fill = pointColours), shape = 22, size = 10) +
  scale_fill_manual(values = unname(pointColours),
                    label = names(pointColours),
                    name = 'Variable') +
  theme(legend.key.size = unit(0.5, "cm")) +
  theme_void()

# get legend
legend_1 <- get_legend(ggLegend_1)


# -------------------------------------------------------------------------
# plot 2 ------------------------------------------------------------------


# Create fake data
dflegend_2 <- data.frame(
  x = runif(100),
  y = runif(100),
  z2 = abs(rnorm(100))
)

# plot
ggLegend_2 <- ggplot(dflegend_2, aes(x=x, y = y))+
  geom_point(aes(color = z2), shape = 22, size = 10) +
  scale_color_gradientn(
    colours = rev(colorRampPalette(c('steelblue', '#f7fcfd', 'orange'))(5)),
    limits = c(0,10),
    name = 'Gradient',
    guide = guide_colorbar(
      frame.colour = "black",
      ticks.colour = "black"
    ))

# get legend
legend_2 <- get_legend(ggLegend_2)

Then I am creating many plots (in this example, I am creating 20 individual plots) and plotting them on a grid:

# create data
dfGrid <- data.frame(x = rnorm(10), y = rnorm(10))

# make a list of plots
plotList <- list()
for(i in 1:20){
  plotList[[i]] <- ggplot(dfGrid) +
    geom_ribbon(aes(x = x, ymin = min(y), ymax = 0), fill = "red", alpha = .5) +
    geom_ribbon(aes(x = x, ymin = min(0), ymax = max(y)), fill = "blue", alpha = .5) +
    theme_void()
}

# plot them on  a grid
gridFinal <- cowplot::plot_grid(plotlist = plotList)

Finally, I am joining the two legends together and adding them to the grid of many plots:

# add legends together into on single plot
legendFinal <- plot_grid(legend_2, legend_1, ncol = 1)

# plot everything on the same plot
plot_grid(gridFinal, legendFinal,  rel_widths = c(3, 1))

This results in something that looks like this: example plot

As you can see, the legends overlap and are not very well spaced. I was wondering if there is any way to fit everything in whilst having the legends appropriately spaced and readable?

I should also note, that, in general, there can be any number of variables and any number of gridded plots.

tjebo
  • 21,977
  • 7
  • 58
  • 94
Electrino
  • 2,636
  • 3
  • 18
  • 40

3 Answers3

5

One option to fix your issue would be to switch to patchwork to glue your plots and the legends together. Especially I make use of the design argument to assign more space to the Variable legend. However, you should be aware that legends are much less flexible compared to plots, i.e. the size of legends is in absolute units and will not adjust to the available space. Hence, I'm not sure whether my solution will fit your desire for a "one-size-fits-all" approach.

library(patchwork)

design <- 
"
ABCDEU
FGHIJV
KLMNOV
PQRSTV
"

plotList2 <- c(plotList, list(legend_2, legend_1))

wrap_plots(plotList2) +
  plot_layout(design = design)

enter image description here

stefan
  • 90,330
  • 6
  • 25
  • 51
  • Your point about the size of legends being in absolute units and will not adjust to the available space is definitely an issue. Manually adjusting the size of the plot window will cause the two legends to overlap using both `cowplot` and `patchwork`. – Electrino Feb 07 '23 at 17:53
  • Yep. I would still go for patchwork when it comes to glueing plots (even if I love cowplot for manipulating ggplot's) but haven't come about an approach which works for all cases. Personally I would try with a legend with multiple columns or use a horizontal one placed at the bottom or top. Or ... – stefan Feb 07 '23 at 18:29
  • 2
    Yeah I'm currently using a work around having three columns (with each legend in an individual column). It doesn't actually fix the issue... the overlapping if the plot window is manually resized still exists. But I'll continue searching for a solution. – Electrino Feb 07 '23 at 18:46
2

One way to avoid this is to add align = "v" argument in plot_grid() function, here is the result:

enter image description here

Yas
  • 98
  • 5
  • Unfortunately, using align = 'v' doesn't exactly work for me. It seems to only work for this specific example. For instance, if the variable names are longer than a single letter, then the legends will clip over the plotted grid. – Electrino Feb 23 '23 at 14:37
  • If that's the case then I'm assuming it's more of a padding issue. – Yas Feb 27 '23 at 00:28
2

Based on the bold assumption that the colors in your legend actually represent something shown in the plot, may I suggest a completely different approach to the problem. Instead of awkwardly creating completely unrelated legends, use ggplot's automatic legend creation that will correctly map the legends to your plot aesthetics. I wonder what the color gradient is for, but you can create one for example simply with a fake geom layer. (Im using geom_point(shape = NA)).

This requires you to correctly map your groups (e.g., A/B/C, etc.) to your data. Having done that will allow you to only call geom_ribbon once.

I am then using patchwork to combine your plots and you can collect the guides. This requires all guides (legends) to be exactly the same. Which you ensure by specifying the limits within the scale_ functions.

Better, however, to use facets. This could be achieved by binding the presumed several data frames and using the origin as an ID for faceting.

The ribbons look a bit different because I am actually plotting the ribbons that correspond to y<0 and y>0. Otherwise, I thought having x would be pointless. Can obviously also changed to fill the entire x limits.

library(ggplot2)
library(dplyr)
library(patchwork)

pointColours <- c(
  A = "#F5736A", B = "#D58D00", C = "#A0A300",
  D = "#36B300", E = "#00BC7B", F = "#00BCC2",
  G = "#00ADF4", H = "#928DFF", I = "#E568F0",
  J = "#808080"
)
set.seed(1)
dfGrid <- data.frame(x = rnorm(10), y = rnorm(10))

## assuming that your color mean something, and this something is represented in your legend,
## you need to map the aesthetic to your data. based on this assumption, this example
df_ribbon <-
  dfGrid %>%
  mutate(
    group = ifelse(y <= 0, pointColours["A"], pointColours["H"]),
    ## I find it better to define your ymin and ymax outside of ggplot
    ymin = ifelse(y <= 0, min(y), 0),
    ymax = ifelse(y > 0, max(y), 0)
  )

# ls_p <- rep(
rep(list(ggplot(df_ribbon) +
  ## use fill as an aesthetic
  geom_ribbon(aes(x = x, ymin = ymin, ymax = ymax, fill = I(group)), alpha = .5) +
  ## fake geom_layer
  geom_point(aes(x = min(x), y = min(y), color = y), shape = NA, na.rm = T) +
  scale_color_gradientn(
    colours = rev(colorRampPalette(c("steelblue", "#f7fcfd", "orange"))(5)),
    limits = c(0, 10),
    name = "Gradient",
    guide = guide_colorbar(
      frame.colour = "black",
      ticks.colour = "black"
    )
  ) +
  scale_fill_manual(
    values = unname(pointColours),
    label = names(pointColours),
    ## important to add limits
    limits = pointColours,
    name = "Variable"
  ) +
    theme_void()), 20) %>%
  ## now merge the legends
  patchwork::wrap_plots() +
  plot_layout(guide = "collect")

tjebo
  • 21,977
  • 7
  • 58
  • 94
  • Thanks for your answer. The issue I'm having is that in my example in the original post, I massively simplified my process. Im not actually plotting `geom_ribbon`, I just used that to simplify my example. I'm plotting decision trees using the `ggraph` package. So I cant actually use ggplot's automatic legend creation. My original code is too long to post on SO (hence the simplification). I was hoping for a solution that just involved positioning the legends somehow – Electrino Feb 23 '23 at 14:27
  • @Electrino I should think ggraph would allow you to follow the above idea just the same. I was just guessing that you might want a legend that has actually some relation to your data. I still think you should ideally not separate the legend creation from your plot. Building them together is not only easier to code (compare my code with yours just for this example), but it's actually dangerous to unlink your legend from your plot. You might create plots that show stuff that is actually not in your data, and not even realise. – tjebo Feb 23 '23 at 15:42
  • and if the issue is with ggraph, then ideally you should post an example with this in mind, so we can also help you better. the closer your example is to your actual problem, the better we can help you. You might be stuck in a thought process where you only see one solution to the problem, but there might be different ways. But if we don't know what the actual problem is, we cannot properly help you. This is known as the [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) – tjebo Feb 23 '23 at 15:46
  • @Electrino I have modified the question title to reflect your actual question better. I will still leave my answer because it might hopefully be of use for others (and maybe you might find it useful too, one day :) – tjebo Feb 23 '23 at 15:52