21

An example using ggplot2 to graph groups of data points and lines connecting the means for each group, mapped with the same aes for shape and for linetype:

p <- ggplot(mtcars, aes(gear, mpg, shape = factor(cyl), linetype = factor(cyl))) + 
  geom_point(size = 2) +
  stat_summary(fun.y = mean, geom = "line", size = 1) +
  scale_shape_manual(values = c(1, 4, 19))

Problem is that point symbols in the legend appear a bit too small to see, relative to the line symbols:

p

Trying to enlarge point size in legend also enlarges lineweight, so that is not useful here.

p1 <- p + guides(shape = guide_legend(override.aes = list(size = 4)))

p1

It would be nice if lineweight were a distinct aesthetic from size. I tried adding

+ guides(linetype = guide_legend(override.aes = list(size = 1)))

which just gives a warning.

> Warning message:
In guide_merge.legend(init, x[[i]]) : Duplicated override.aes is ignored.

It seems to make no difference either if I move the linetype aes out of ggplot() and into stat_summary(). If I wanted only the point symbols, I could eliminate lines from the legend this way.

p2 <- p + guides(shape = guide_legend(override.aes = list(size = 4, linetype = 0)))

p2

Instead, (keeping small point symbols in the graph itself) I want one single legend with both big point symbols as in this last image and thin line symbols as in the first image. Is there a way to do this?

Community
  • 1
  • 1
Scott
  • 765
  • 4
  • 9

4 Answers4

13

It sure does seem to be difficult to set those properties independently. I was only kind of able to come up with a hack. If your real data is much different it will likely have to be adjusted. But what i did was used the override.aes to set the size of the point. Then I went in and built the plot, and then manually changed the line width settings in the actual low-level grid objects. Here's the code

pp<-ggplot(mtcars, aes(gear, mpg, shape = factor(cyl), linetype = factor(cyl))) + 
  geom_point(size = 3) +
  stat_summary(fun.y = mean, geom = "line", size = 1) +
  scale_shape_manual(values = c(1, 4, 19)) +
  guides(shape=guide_legend(override.aes=list(size=5)))

build <- ggplot_build(pp)
gt <- ggplot_gtable(build)

segs <- grepl("geom_path.segments", sapply(gt$grobs[[8]][[1]][[1]]$grobs, '[[', "name"))
gt$grobs[[8]][[1]][[1]]$grobs[segs]<-lapply(gt$grobs[[8]][[1]][[1]]$grobs[segs], 
    function(x) {x$gp$lwd<-2; x})
grid.draw(gt)

The magic number "8" was where gt$grobs[[8]]$name=="guide-box" so i knew I was working the legend. I'm not the best with grid graphics and gtables yet, so perhaps someone might be able to suggest a more elegant way.

MrFlick
  • 195,160
  • 17
  • 277
  • 295
  • +1 for digging into this. At least you've given me a programmatic way to do it - I was getting tired of editing each plot outside r just to adjust legends. I'll try your approach on other data and see if any other ideas show up in the meantime. – Scott Jul 29 '14 at 05:23
  • Having tried this further, I see now why you say 'magic number'. So the trick here is how to index that "guide-box", which won't always be the 8th element in the grobs list. Other than that inconvenience, this method gives the result I was looking for in a single legend. – Scott Jul 31 '14 at 02:22
  • You could create a helper function to do the look-up. Eg `getgtable<-function(n) which(sapply(gt$grobs, \`[[\`, "name")==n); getgtable("guide-box")` – MrFlick Jul 31 '14 at 02:45
  • This is very useful now with the helper function. It works well within plot functions, too. [This question](http://stackoverflow.com/questions/18406991/saving-a-graph-with-ggsave-after-using-ggplot-build-and-ggplot-gtable) helped me with saving the `grob` output. – Scott Aug 03 '14 at 03:35
  • 2
    Cautionary note: To further complicate things, `geom_path.segments` can sometimes be hiding in a `gTree` (and thus won't be found in the `sapply()` command in this answer). It took me a long time to figure out how to access the nested `grobs` within a `gTree`; they are its `children`. So the syntax looks like `gt$grobs[[mn]][[1]][[1]]$grobs[[i]]$children`, where `i` indexes each `grob` named `"GRID.gTree"` in `gt`. Similar procedures used in the answer can then be iterated to adjust the nested `segments`. – Scott Sep 29 '14 at 02:00
10

Using the grid function grid.force(), all the grobs in the ggplot become visible to grid's editing functions, including the legend keys. Thus, grid.gedit can be applied, and the required edit to the plot can be achieved using one line of code. In addition, I increase the width of the legend keys so that the different line types for line segments are clear.

library(ggplot2)
library(grid)
p <- ggplot(mtcars, aes(gear, mpg, shape = factor(cyl), linetype = factor(cyl))) + 
  geom_point(size = 2) +
  stat_summary(fun.y = mean, geom = "line", size = 1) +
  scale_shape_manual(values = c(1, 4, 19)) +
  theme(legend.key.width = unit(1, "cm"))

p

grid.ls(grid.force())    # To get the names of all the grobs in the ggplot

# The edit - to set the size of the point in the legend to 4 mm
grid.gedit("key-[-0-9]-1-1", size = unit(4, "mm"))    

enter image description here

To save the modified plot

  g <- grid.grab()
  ggsave(plot=g, file="test.pdf")
Sandy Muspratt
  • 31,719
  • 12
  • 116
  • 122
  • Your suggestion works, but when I save the graph as PDF with `ggsave(plot=p, file="test.pdf")`, the symbols stay as before applying `grid.gedit()`. – Valentin_Ștefan Jun 29 '17 at 21:09
  • 2
    @Valentine That's because you are saving the original ggplot object, not the modified on-screen grid object. A convenient method is to grid.grab() the on-screen object, then save. That is, inset g <- grid.grab() before the save command, but be sure to save g. See the edit. – Sandy Muspratt Jun 30 '17 at 08:21
2

I see what you mean. Here is a solution that fits what you're looking for, I think. It keeps both of the legends separate, but places them side by side. The labels and title of the shape are left out, so that the labels to the far right correspond to both the shapes and linetypes.

I'm posting this as a separate answer because I think both methods will be valid for future readers.

   p2 <- ggplot(mtcars, aes(gear, mpg, shape = factor(cyl), 
                 linetype = factor(cyl))) + 
     geom_point(size = 2) +
     stat_summary(fun.y = mean, geom = "line", size = 1) +
     # blank labels for the shapes
     scale_shape_manual(name="", values = c(1, 4, 19), 
                        labels=rep("", length(factor(mtcars$cyl))))+
     scale_linetype_discrete(name="Cylinders")+
     # legends arranged horizontally
     theme(legend.box = "horizontal")+
     # ensure that shapes are to the left of the lines
     guides(shape = guide_legend(order = 1), 
            linetype = guide_legend(order = 2))
   p2
Matt74
  • 729
  • 4
  • 8
  • This is a clever idea to disguise two legends as one. It allows setting `override.aes`, etc., for each legend independently. However, that may become less necessary now that the lines don't overlay the point symbols. The legend title being offset to one side is a bit unsatisfying - it would look better centered, just to be picky. But thanks for this solution. – Scott Jul 31 '14 at 01:49
1

One way to ensure separate legends is to give them different names (or other differences that would preclude them being grouped together).

Here's an example based on the code you supplied:

    p <- ggplot(mtcars, aes(gear, mpg, shape = factor(cyl), linetype = factor(cyl))) + 
      geom_point(size = 2) +
      stat_summary(fun.y = mean, geom = "line", size = 1) +
      scale_shape_manual(name="Name 1", values = c(1, 4, 19))+
      scale_linetype_discrete(name="Name2")
    p
Matt74
  • 729
  • 4
  • 8
  • 2
    True, this is often useful. But here I was looking to keep one single legend. After all, how should "Name 1" and "Name 2" differ if they both refer to the same factor (cyl, in this example)? – Scott Jul 30 '14 at 02:06