Place a legend for each facet_wrap grid in ggplot2
Asked Answered
G

5

34

I have this data frame:

x <- data.frame(
  Date = factor(rep(
    c("12/1/2011", "1/2/2012", "2/1/2012", "2/10/2012", "2/13/2012"),
    3
  )),
  Server = factor(rep(c("A", "B", "C"), each = 5L)),
  FileSystem = factor(c(
    "/", "/var", "tmp", "/db", "/app", "C:", "D:", "F:", "/restore",
    "G:", "/", "/tmp", "/data", "/Storage", "/database"
  )),
  PercentUsed = c(
    60L, 50L, 90L, 86L, 90L, 67L, 67L, 34L, 89L, 56L, 90L, 78L,
    67L, 34L, 12L
  )
)
x
#>         Date Server FileSystem PercentUsed
#> 1  12/1/2011      A          /          60
#> 2   1/2/2012      A       /var          50
#> 3   2/1/2012      A        tmp          90
#> 4  2/10/2012      A        /db          86
#> 5  2/13/2012      A       /app          90
#> 6  12/1/2011      B         C:          67
#> 7   1/2/2012      B         D:          67
#> 8   2/1/2012      B         F:          34
#> 9  2/10/2012      B   /restore          89
#> 10 2/13/2012      B         G:          56
#> 11 12/1/2011      C          /          90
#> 12  1/2/2012      C       /tmp          78
#> 13  2/1/2012      C      /data          67
#> 14 2/10/2012      C   /Storage          34
#> 15 2/13/2012      C  /database          12

I would like to put a legend right next to each facet_wrap grid, its own FileSystem:

When I do this, it puts the legend on the side of the plot for all of the FileSystem. Is it possible to put FileSystem belong to each server next to each grid?

ggplot(x, aes(Date, PercentUsed, group=1, colour=FileSystem)) + 
     geom_jitter(size=0.5) + geom_smooth(method="loess", se=T) + 
     facet_wrap(~Server, ncol=1)
Gilbreath answered 12/2, 2013 at 19:48 Comment(0)
I
34

Meh, @joran beat me to it (my gridExtra was out of date but took me 10 minutes to realize it). Here's a similar solution, but this one skins the cat generically by levels in Server.

library(gridExtra)
out <- by(data = x, INDICES = x$Server, FUN = function(m) {
      m <- droplevels(m)
      m <- ggplot(m, aes(Date, PercentUsed, group=1, colour = FileSystem)) + 
         geom_jitter(size=2) + geom_smooth(method="loess", se=T)
   })
do.call(grid.arrange, out)

# If you want to supply the parameters to grid.arrange
do.call(grid.arrange, c(out, ncol=3))

image

Icken answered 12/2, 2013 at 20:39 Comment(6)
Very nice. I didn't realize that droplevels() had a method for data.frames. That's handy!Stempien
Is there a neat way to force alignment, i.e. keep plot area width same? Prescribe legend width?Aggappora
@Aggappora consider moving the legend to the top of the first figure and remove it from the rest.Sabinesabino
Indeed I ended up moving all legends to top in a meanwhile. I can't use a single legend as they unique per plot. Though it would be nice to know if there is a way to prescribe widths.Aggappora
@Aggappora consider making a new question, although AFAIK, it's not possible (it's possible to define margins, though).Sabinesabino
@mlt, check out my suggestions below.Dalia
T
34

The best way to do this is with the gridExtra package:

library(gridExtra)

xs <- split(x,f = x$Server)
p1 <- ggplot(xs$A,aes(x = Date,y = PercentUsed,group = 1,colour = FileSystem)) + 
        geom_jitter(size=0.5) + 
        geom_smooth(method="loess", se=T) + 
        facet_wrap(~Server, ncol=1)

p2 <- p1 %+% xs$B
p3 <- p1 %+% xs$C

grid.arrange(p1,p2,p3)

enter image description here

Thyroxine answered 12/2, 2013 at 20:25 Comment(7)
I should note that you made the points very small in geom_jitter and I'm not sure why, but I left it as is. The points are there, but hard to see.Thyroxine
I am a bit intrigued with the %+% operator. Can you please explain what it does?Atrocious
@Atrocious It's a way to make a ggplot object "modular" in the sense that you can use it to simply drop in a new data frame, but use all the same geom specifications from a previous plot. Of course, it will only work if the column names all match, and if you haven't used any other data frames in other layers.Thyroxine
+1 Thank you for your prompt response. This is such a fantastic way of re-using the objects!Atrocious
@Thyroxine how can you make each plot in the grid take equal space so that their x ticks align?Culicid
@Culicid It requires some work. See here (and the links therein) for some ideas.Thyroxine
@lgd, a couple of suggestions in my answer below to align the plot space properly.Dalia
I
34

Meh, @joran beat me to it (my gridExtra was out of date but took me 10 minutes to realize it). Here's a similar solution, but this one skins the cat generically by levels in Server.

library(gridExtra)
out <- by(data = x, INDICES = x$Server, FUN = function(m) {
      m <- droplevels(m)
      m <- ggplot(m, aes(Date, PercentUsed, group=1, colour = FileSystem)) + 
         geom_jitter(size=2) + geom_smooth(method="loess", se=T)
   })
do.call(grid.arrange, out)

# If you want to supply the parameters to grid.arrange
do.call(grid.arrange, c(out, ncol=3))

image

Icken answered 12/2, 2013 at 20:39 Comment(6)
Very nice. I didn't realize that droplevels() had a method for data.frames. That's handy!Stempien
Is there a neat way to force alignment, i.e. keep plot area width same? Prescribe legend width?Aggappora
@Aggappora consider moving the legend to the top of the first figure and remove it from the rest.Sabinesabino
Indeed I ended up moving all legends to top in a meanwhile. I can't use a single legend as they unique per plot. Though it would be nice to know if there is a way to prescribe widths.Aggappora
@Aggappora consider making a new question, although AFAIK, it's not possible (it's possible to define margins, though).Sabinesabino
@mlt, check out my suggestions below.Dalia
C
15

Instead of using facets, we could make a list of plots per group, then use cowplot::plot_grid for plotting. Each will have it's own legend:

# make list of plots
ggList <- lapply(split(x, x$Server), function(i) {
  ggplot(i, aes(Date, PercentUsed, group = 1, colour = FileSystem)) + 
    geom_jitter(size = 2) +
    geom_smooth(method = "loess", se = TRUE)})

# plot as grid in 1 columns
cowplot::plot_grid(plotlist = ggList, ncol = 1,
                   align = 'v', labels = levels(x$Server))

As suggested by @Axeman, we could add labels using facet_grid(~Server), instead of labels = levels(x$Server).

enter image description here

Cavein answered 16/2, 2018 at 8:0 Comment(0)
D
3

I liked @joran's answer and provide a couple of options based off of their code as a starting point. Both options address the issue of mis-aligned facets.

Legends outside facets

If you choose a monospaced font for your legend items, you can use str_pad to add padding on the right-hand side of all legend entries, forcing the length of each to be consistent.

If you're willing to use a monospaced font, this is a quick fix.

library(ggplot2)
library(dplyr)
library(gridExtra)
library(stringr)

l <- max(nchar(as.character(x$FileSystem)))
mylevels <- as.character(levels(x$FileSystem))
mylevels <- str_pad(mylevels, width = l, side = "right", pad = " ")
x <- mutate(x, FileSystem = factor(str_pad(FileSystem, width = l, side = "right", pad = " "),
            levels = mylevels))
windowsFonts("Lucida Sans Typewriter" = windowsFont("Lucida Sans Typewriter"))
xs <- split(x,f = x$Server)
p1 <- ggplot(xs$A,aes(x = Date,y = PercentUsed,group = 1,colour = FileSystem)) + 
  geom_jitter(size=0.5) + 
  geom_smooth(method="loess", se=T) + 
  facet_wrap(~Server, ncol=1) +
  theme(legend.text = element_text(family = "Lucida Sans Typewriter"))

p2 <- p1 %+% xs$B
p3 <- p1 %+% xs$C

grid.arrange(p1,p2,p3)

enter image description here

Legends inside facets

If you don't mind legends inside each facet, you can add extra space to each facet with the "expand" argument inside scale call:

library(lubridate)
x <- mutate(x, Date = as.Date(as.character(Date), format = "%m/%d/%Y"))
xs <- split(x,f = x$Server)
p1 <- ggplot(xs$A,aes(x = Date,y = PercentUsed,group = 1,colour = FileSystem)) + 
  geom_jitter(size=0.5) + 
  scale_x_date(expand = expansion(add = c(5, 20)),
               date_labels = "%d-%m-%Y") +
  geom_smooth(method="loess", se=T) + 
  facet_wrap(~Server, ncol=1) +
  theme_bw() +
  theme(legend.position = c(0.9, 0.5))

p2 <- p1 %+% xs$B
p3 <- p1 %+% xs$C

grid.arrange(p1,p2,p3)

enter image description here

Dalia answered 23/6, 2020 at 14:10 Comment(0)
S
2

Besides gridExtra and cowplot there's, also patchwork in the game now. Hence you can do the following:

require(ggplot2)
require(patchwork)
# split
dfs = split(df, f = df$Server)
# apply ggplot function and write to list
gg_l = lapply(dfs, function(x) {
  ggplot(x, aes(x = Date,y = PercentUsed, group = 1, colour = FileSystem)) + 
    geom_jitter(size = 0.5) + 
    geom_smooth(method = "loess", se = TRUE) + 
    facet_wrap(~ Server, ncol = 1)
})
# patchwork
wrap_plots(gg_l, ncol = 1)

enter image description here

You can combine the plots also manually, have a look here. I used the OP's data for df.

Sparge answered 1/6, 2021 at 15:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.