ggplot2: Make multiple line+ribbon's with legend

Viewed 663

I want to make a plot in ggplot2 with

  • multiple lines,
  • each of which has a surrounding ribbon,
  • with user-specified colors,
  • and with a legend that shows the line color and ribbon color.

(Scroll down to see an example of a plot that almost achieves what I seek.) I've seen related questions but none that address all these issues together, and I've looked for examples online but can't find any like this. I'm brand new to ggplot2, so apologies for such a basic question but maybe your answer will help others too.

Here's a complete example of terribly inelegant attempts to cobble together a graph that almost accomplishes what I want. I've tried with both wide-format data and with long-format data. (And yes, I use = instead of <-, and I don't use spaces as many recommend, but those are separate issues... :-)

# Housekeeping:
graphics.off()
library("ggplot2")
library("tidyr") # for pivot_longer()

# Set up data:
set.seed(47405)
x = 1:10
y1 = 1 + 0.1*x + rnorm(length(x),0,0.2)  # line 1
y2 = 2 + 0.2*x + rnorm(length(x),0,0.2)  # line 2
y1lo = y1 - 0.2  # ribbon 1 low
y1hi = y1 + 0.2  # ribbon 1 high
y2lo = y2 - 0.3  # ribbon 2 low
y2hi = y2 + 0.3  # ribbon 2 high

# Wide format data frame:
wideDF = data.frame( x=x , 
                     y1lo=y1lo , y1=y1 , y1hi=y1hi ,
                     y2lo=y2lo , y2=y2 , y2hi=y2hi )

# Long format data frame:
longDF = pivot_longer( wideDF , cols=!x , names_to="line" , values_to="y" )
longDF$fill = NA
longDF$fill[grep( "1" , longDF$line  )] = "y1"
longDF$fill[grep( "2" , longDF$line  )] = "y2"

An inelegant way using wide-format, but with no legend:

ggplot( wideDF , aes(x=x) ) +
  geom_ribbon( aes( ymin=y1lo , ymax=y1hi ) , fill="pink" , alpha=0.5 ) +
  geom_line( aes( y=y1 ) , col="red" ) +
  geom_ribbon( aes( ymin=y2lo , ymax=y2hi ) , fill="lightblue" , alpha=0.5 ) +
  geom_line( aes( y=y2 ) , col="blue" ) +
  labs( title="Using WIDE data frame" , y="Y label" , x="X label" ) +
  geom_text( x = max(wideDF$x) , 
             y = wideDF$y1[wideDF$x==max(wideDF$x)] ,
             hjust = 1 , vjust = -0.5 , 
             label="Y1" , color = "red" ) +
  geom_text( x = max(wideDF$x) , 
             y = wideDF$y2[wideDF$x==max(wideDF$x)] , 
             hjust = 1 , vjust = -0.5 , 
             label="Y2" , color = "blue" )

enter image description here

An inelegant way using long-format, still with no legend:

ggplot( ) + 
  xlim(range(longDF$x)) + ylim(range(longDF$y)) +
  geom_ribbon( aes( x=longDF$x[longDF$line=="y1"] ,
                    ymin=longDF$y[longDF$line=="y1lo"] ,
                    ymax=longDF$y[longDF$line=="y1hi"] ), 
               fill="pink" , alpha=0.5  ) +
  geom_line( aes( x=longDF$x[longDF$line=="y1"] ,
                  y=longDF$y[longDF$line=="y1"] ) ,
             color="red" ) +
  geom_ribbon( aes( x=longDF$x[longDF$line=="y2"] ,
                    ymin=longDF$y[longDF$line=="y2lo"] ,
                    ymax=longDF$y[longDF$line=="y2hi"] ), 
               fill="lightblue" , alpha=0.5  ) +
  geom_line( aes( x=longDF$x[longDF$line=="y2"] ,
                  y=longDF$y[longDF$line=="y2"] ) ,
             color="blue" ) +
  labs( title="Using LONG data frame" , y="Y label" , x="X label" ) +
  geom_text( aes(
             x = max(longDF$x) , 
             y = longDF$y[longDF$x==max(longDF$x) & longDF$line=="y1"] ) ,
             hjust = 1 , vjust = -0.5 , 
             label="Y1" , color = "red" ) +
  geom_text( aes( x = max(longDF$x) ,
             y = longDF$y[longDF$x==max(longDF$x) & longDF$line=="y2"] ) ,
             hjust = 1 , vjust = -0.5 ,
             label="Y2" , color = "blue" )

enter image description here

So, my questions:

  1. How can multiple line+ribbon graphs be coded efficiently? The code above is very inelegant, and I'm obviously missing fundamental concepts regarding aes() and data= in ggplot2. Answers that not only show code but also briefly explain what's going on with aes() would be awesome.

  2. How can a legend be added to these plots? (Of course, with a legend, do not need geom_text() elements as in the examples above.)

  3. For multiple line+ribbon plots in ggplot, is in more useful to have wide-format or long-format data frames?

Thanks!

1 Answers

Maybe this is what you are looking for ...

  1. General lesson: If you want a legend you have to map somethin on an aesthetic, i.e. put color and/or fill inside aes()

  2. Neither your wide nor your long dataset are suitable for easy plotting. Instead, starting from your long df, I first get rid of the numbers in your line column and make the dataset wider so we have just y, ymin, ymax (after doing so we end up with tidy data, as 1 and 2 are categories of one variabale, while y, ymin, ymax are different variables). This allows us to set up your plot with only two geom layers. Additonally we don't have to use complicated and error prone codelike longDF$x[longDF$line=="y2"] to get the values we like to plot.

  3. For the text labels I use group_by(longDF1, fill) %>% top_n(1, x) as data which simply picks the rows for each line with the top x value.

  4. Finally, to get the colors right set them via scale_xxx_manual

library(dplyr)
library("ggplot2")
library("tidyr") # for pivot_longer()

# Set up data:
set.seed(47405)
x = 1:10
y1 = 1 + 0.1*x + rnorm(length(x),0,0.2)  # line 1
y2 = 2 + 0.2*x + rnorm(length(x),0,0.2)  # line 2
y1lo = y1 - 0.2  # ribbon 1 low
y1hi = y1 + 0.2  # ribbon 1 high
y2lo = y2 - 0.3  # ribbon 2 low
y2hi = y2 + 0.3  # ribbon 2 high

# Wide format data frame:
wideDF = data.frame( x=x , 
                     y1lo=y1lo , y1=y1 , y1hi=y1hi ,
                     y2lo=y2lo , y2=y2 , y2hi=y2hi )

# Long format data frame:
longDF = pivot_longer( wideDF , cols=!x , names_to="line" , values_to="y" )
longDF$fill = NA
longDF$fill[grep( "1" , longDF$line  )] = "y1"
longDF$fill[grep( "2" , longDF$line  )] = "y2"

longDF1 <- longDF %>% 
  mutate(line = gsub("\\d", "", line)) %>% 
  pivot_wider(id_cols = c(x, fill), names_from = line, values_from = y)

ggplot(longDF1) +
  geom_ribbon(aes(x=x, ymin=ylo, ymax=yhi, fill = fill), alpha=0.5) +
  geom_line(aes(x=x, y=y, color = fill)) +
  labs( title="Using LONG data frame" , y="Y label" , x="X label" ) +
  geom_text(data = group_by(longDF1, fill) %>% top_n(1, x), 
            aes(x = x, y = y, label = toupper(fill), color = fill), 
            hjust = 1 , vjust = -0.5, show.legend = FALSE) +
  scale_color_manual(values = c(y1 = "red", y2 = "blue")) + 
  scale_fill_manual(values = c(y1 = "pink", y2 = "lightblue"))

Related