0

When making a geom_point() that has both color = column_1 and size = column_2 options passed through, ggplot provides two separate legends. One for the color column and one for the size. This is great.

I would like to split the two legends so the bit which maps onto color is shown across the top horizontally and the bit that maps onto size is shown on the right-hand side of the plotregion vertically.

The data and code below reproduce the graph shown below. In that graph I would like the size shown on the right-hand size of the graph vertically and the bit that maps onto the actor's name to be shown along the top as it is.

Is this kind of thing possible? I've found ways to put both of them on the left-hand side but that's not really what I want as you read the actor's name left to right in the plot, and you read size top to bottom, so I want the legends to display in the same way the reader would naturally read the data.

df <- structure(list(count = c(1025, 360, 625, 1108, 3018, 7376, 16318, 
19114, 16947, 21532, 2088, 923, 1109, 1751, 3710, 7160, 13904, 
20096, 17049, 24597, 2094, 607, 817, 1340, 2909, 6667, 13870, 
18657, 17502, 34533, 1132, 447, 606, 940, 2038, 4564, 12141, 
19197, 18426, 31272, 1144, 387, 646, 1081, 2164, 5451, 12343, 
16194, 16783, 24880, 1450, 549, 759, 1278, 2568, 5623, 11406, 
15957, 16445, 22850, 1707, 788, 1023, 1594, 3292, 6852, 14749, 
18550, 13815, 19754, 1977, 819, 1051, 1522, 2873, 5469, 10692, 
14740, 12352, 16335, 1256, 554, 633, 946, 1780, 3301, 6260, 10608, 
11575, 20720, 1365, 547, 565, 1066, 2177, 4650, 9590, 11570, 
8160, 11119, 13175, 3088, 2869, 3375, 5123, 7292, 9714, 9088, 
5927, 10775, 8387, 1954, 1817, 1996, 2776, 3972, 5746, 5968, 
3965, 5969), doctor = structure(c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 
1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 
2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 
2L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 
3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 4L, 
4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 
4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 5L, 5L, 5L, 
5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 
5L), .Label = c("Christopher Eccleston", "David Tennant", "Matt Smith", 
"Peter Capaldi", "Jodie Whitaker"), class = "factor"), rating = c(1, 
2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 
2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 
2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 
2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 
2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 
2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), season_num = c(27L, 
27L, 27L, 27L, 27L, 27L, 27L, 27L, 27L, 27L, 28L, 28L, 28L, 28L, 
28L, 28L, 28L, 28L, 28L, 28L, 29L, 29L, 29L, 29L, 29L, 29L, 29L, 
29L, 29L, 29L, 30L, 30L, 30L, 30L, 30L, 30L, 30L, 30L, 30L, 30L, 
31L, 31L, 31L, 31L, 31L, 31L, 31L, 31L, 31L, 31L, 32L, 32L, 32L, 
32L, 32L, 32L, 32L, 32L, 32L, 32L, 33L, 33L, 33L, 33L, 33L, 33L, 
33L, 33L, 33L, 33L, 34L, 34L, 34L, 34L, 34L, 34L, 34L, 34L, 34L, 
34L, 35L, 35L, 35L, 35L, 35L, 35L, 35L, 35L, 35L, 35L, 36L, 36L, 
36L, 36L, 36L, 36L, 36L, 36L, 36L, 36L, 37L, 37L, 37L, 37L, 37L, 
37L, 37L, 37L, 37L, 37L, 38L, 38L, 38L, 38L, 38L, 38L, 38L, 38L, 
38L, 38L)), row.names = c(NA, -120L), groups = structure(list(
    season_num = 27:38, .rows = structure(list(1:10, 11:20, 21:30, 
        31:40, 41:50, 51:60, 61:70, 71:80, 81:90, 91:100, 101:110, 
        111:120), ptype = integer(0), class = c("vctrs_list_of", 
    "vctrs_vctr", "list"))), row.names = c(NA, -12L), class = c("tbl_df", 
"tbl", "data.frame"), .drop = TRUE), class = c("grouped_df", 
"tbl_df", "tbl", "data.frame"))
  df %>% 
  ggplot() + 
  geom_point(aes(x = factor(season_num), y = rating, size = count, color = doctor)) +
  labs(x = "Season", y = "Rating (1-10)", title = "IMDb ratings distributions by Season") +
  theme(legend.position = 'top',
        legend.title = element_blank(),
        plot.title = element_text(size = 10),
        axis.title.x = element_text(size = 10),
        axis.title.y = element_text(size = 10)) +
  scale_size_continuous(range = c(1,8)) +
  scale_y_continuous(limits=c(1, 10), breaks=c(seq(1, 10, by = 1))) +
  scale_x_discrete(breaks=c(seq(27, 38, by = 1))) +
  scale_color_brewer(palette = "Dark2")

enter image description here

C.Robin
  • 1,085
  • 1
  • 10
  • 23

1 Answers1

1

I do not think this is possible with ggplot2-only functions. However, a common trick is:

  1. to make a plot without the legend,
  2. make other plots with target legends,
  3. extract the legends from these plots,
  4. arrange everything in a grid using packages like cowplot or gridExtra

You can find some examples of this process on SO:

Here is an example with the provided data, I have not put much effort in arranging the grid because it can change a lot depending on the package you choose in the end. It is just to showcase the process.

library(cowplot) 
library(ggplot2)

 # plot without legend
main_plot <-  ggplot(data = df) + 
    geom_point(aes(x = factor(season_num), y = rating, size = count, color = doctor)) +
    labs(x = "Season", y = "Rating (1-10)", title = "IMDb ratings distributions by Season") +
    theme(legend.position = 'none',
          legend.title = element_blank(),
          plot.title = element_text(size = 10),
          axis.title.x = element_text(size = 10),
          axis.title.y = element_text(size = 10)) +
    scale_size_continuous(range = c(1,8)) +
    scale_y_continuous(limits=c(1, 10), breaks=c(seq(1, 10, by = 1))) +
    scale_x_discrete(breaks=c(seq(27, 38, by = 1))) +
    scale_color_brewer(palette = "Dark2")

# color legend, top, horizontally
color_plot <-  ggplot(data = df) + 
  geom_point(aes(x = factor(season_num), y = rating, color = doctor)) +
  theme(legend.position = 'top',
        legend.title = element_blank()) +
  scale_color_brewer(palette = "Dark2")

color_legend <- cowplot::get_legend(color_plot)

# size legend, right-hand side, vertically
size_plot <-  ggplot(data = df) + 
  geom_point(aes(x = factor(season_num), y = rating, size = count)) +
  theme(legend.position = 'right',
        legend.title = element_blank()) +
  scale_size_continuous(range = c(1,8))

size_legend <- cowplot::get_legend(size_plot)

# combine all these elements
cowplot::plot_grid(plotlist = list(color_legend,NULL, main_plot, size_legend),
          rel_heights = c(1, 5),
          rel_widths =  c(4, 1)) 

Output:

output

Paul
  • 2,850
  • 1
  • 12
  • 37
  • 1
    This is great Paul. Thank you so much for taking the time to answer my question. I have managed to get it to work. I took a look at the other answers you shared too. It seems like there hasn't been an update to the approach in a while, even though based on the SO questions, it's somewhat common thing ppl want to do. I feel adding the functionality to ggplot2 could be acheived relatively easily, but alas. I can trim the code down a bit by removing a lot of the aesthetic options on the two plots that are just made to get the legend at least! – C.Robin Jul 23 '21 at 11:50
  • 1
    @C.Robin I thought it was possible to solve it using `guides()` and `guide_legend()` but it is not the case... Maybe it is time to open a feature request on [GitHub](https://github.com/tidyverse/ggplot2) – Paul Jul 23 '21 at 11:55
  • Ooh exciting. I might just do that. As an R noob this feels like a big step for me! haha – C.Robin Jul 23 '21 at 11:56
  • 2
    Specifically, you could propose that `guide_legend()` and `guide_colourbar()` get a `position` argument, just like `guide_axis()` has. I think it's a great idea if this would be implemented, and I couldn't find a pre-existing issue mentioning this option. – teunbrand Jul 23 '21 at 12:16