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)
moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
user1471980
  • 10,127
  • 48
  • 136
  • 235

5 Answers5

33

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

joran
  • 169,992
  • 32
  • 429
  • 468
  • 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. – joran Feb 12 '13 at 20:36
  • 4
    I am a bit intrigued with the `%+%` operator. Can you please explain what it does? – Legend May 08 '13 at 16:44
  • 6
    @Legend 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. – joran May 08 '13 at 17:06
  • +1 Thank you for your prompt response. This is such a fantastic way of re-using the objects! – Legend May 08 '13 at 18:32
  • 1
    @joran how can you make each plot in the grid take equal space so that their x ticks align? – lgd Mar 23 '15 at 15:46
  • @lgd It requires some work. See [here](http://stackoverflow.com/q/26159495/324364) (and the links therein) for some ideas. – joran Mar 23 '15 at 15:50
  • @lgd, a couple of suggestions in my answer below to align the plot space properly. – Nova Jun 23 '20 at 14:18
33

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

Sinan Eldem
  • 5,564
  • 3
  • 36
  • 37
Roman Luštrik
  • 69,533
  • 24
  • 154
  • 197
  • Very nice. I didn't realize that `droplevels()` had a method for `data.frame`s. That's handy! – Josh O'Brien Feb 13 '13 at 19:23
  • 1
    Is there a neat way to force alignment, i.e. keep plot area width same? Prescribe legend width? – mlt May 05 '16 at 06:08
  • @mlt consider moving the legend to the top of the first figure and remove it from the rest. – Roman Luštrik May 05 '16 at 21:04
  • 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. – mlt May 05 '16 at 21:11
  • @mlt consider making a new question, although AFAIK, it's not possible (it's possible to define margins, though). – Roman Luštrik May 06 '16 at 11:45
  • @mlt, check out my suggestions below. – Nova Jun 23 '20 at 14:17
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

zx8754
  • 52,746
  • 12
  • 114
  • 209
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

Nova
  • 5,423
  • 2
  • 42
  • 62
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.

andschar
  • 3,504
  • 2
  • 27
  • 35