4

I have a ggplot with 3 ribbons. I can produce this plot with the following code:

library(ggplot2)
library(RColorBrewer)

data <- data.frame(
  date = seq.Date(as.Date("2018-01-01"), as.Date("2018-01-31"), by= "days"), 
  value = runif(min = 0, max = 1, n = 31) 
)


breaks <- c(0.1, 0.2, 0.3)
reds <- brewer.pal(3, "Reds")


pl <- ggplot2::ggplot(data = data,
                      aes(x = date, y = value)) +

  geom_ribbon(
    aes(
      x = date,
      ymin = value * (1 - breaks[1]),
      ymax = value * (1 + breaks[1])
    ),
    fill = reds[3],
    alpha = 0.4
  )  +

  geom_ribbon(
    aes(
      x = date,
      ymin = value * (1 - breaks[2]),
      ymax = value * (1 + breaks[2])
    ),
    fill = reds[2],
    alpha = 0.4
  )  +

  geom_ribbon(
    aes(
      x = date,
      ymin = value * (1 - breaks[2]),
      ymax = value * (1 + breaks[2])
    ),
    fill = reds[1],
    alpha = 0.4
  )  +

  geom_line(size = 1); pl

This works perfectly and does what I want.

enter image description here

My question is how can I generalize the amount of ribbons in my code. If I want to add a new ribbon I can copy/paste my code but that's not what I want... I would like only to extend the breaks-vector (c(0.1, 0.2, 0.3, 0.4)) and then the plot should contain automatically 4 ribbons (or even more). In my case the later plot will be produced by a function. This function should only contain the breaks (and data) as parameters.

I thought I can do this with a for-loop around the geom_ribbon and store the the results an a list. But I didn't succeeded :-(

Have anybody an idea? Thanks a lot in advance!

M. Schmid
  • 79
  • 7

3 Answers3

3

If you make the breaks part of your dataset, you can reshape the data so that the breaks are a variable you can then assign to an aesthetic—in this case, fill. I remember answering a similar question a while ago, although that one was actually more complex in its calculations.

To make the breaks vector a column of the data frame, I'm just adding it as a list. Each observation has this same set of breaks.

data %>%
  mutate(brk = list(breaks))
#> # A tibble: 31 x 3
#>    date        value brk      
#>    <date>      <dbl> <list>   
#>  1 2018-01-01 0.0502 <dbl [3]>
#>  2 2018-01-02 0.190  <dbl [3]>
#>  3 2018-01-03 0.409  <dbl [3]>
#>  4 2018-01-04 0.453  <dbl [3]>
#>  5 2018-01-05 0.295  <dbl [3]>
#>  6 2018-01-06 0.170  <dbl [3]>
#>  7 2018-01-07 0.592  <dbl [3]>
#>  8 2018-01-08 0.315  <dbl [3]>
#>  9 2018-01-09 0.118  <dbl [3]>
#> 10 2018-01-10 0.374  <dbl [3]>
#> # ... with 21 more rows

Unnesting the list column then separates those break values so that the date-value combinations are repeated, once for each break. Since there are 3 breaks, there are now 3 times the rows.

data %>%
  mutate(brk = list(breaks)) %>%
  unnest()
#> # A tibble: 93 x 3
#>    date        value   brk
#>    <date>      <dbl> <dbl>
#>  1 2018-01-01 0.0502   0.1
#>  2 2018-01-01 0.0502   0.2
#>  3 2018-01-01 0.0502   0.3
#>  4 2018-01-02 0.190    0.1
#>  5 2018-01-02 0.190    0.2
#>  6 2018-01-02 0.190    0.3
#>  7 2018-01-03 0.409    0.1
#>  8 2018-01-03 0.409    0.2
#>  9 2018-01-03 0.409    0.3
#> 10 2018-01-04 0.453    0.1
#> # ... with 83 more rows

For convenience in using these breaks as a discrete variable, I created a column that's simply the break values as a factor, and reversed its order. The thing that's tricky here (and in the previous question I linked to) is the ordering. ggplot layers get built each on top of the previous ones, so if the widest ribbon gets drawn last, it will block all the smaller ones. The default order for the breaks would be in numeric order, but because I made a factor of them, I can reverse the levels so that the widest one—0.3—will be drawn first, and therefore be layered beneath the next layers.

Lastly, to make the line: for this, you only want the dates and values, and you don't need them repeated the way I've done from unnesting, so I take the distinct combinations of date and value inside the geom_line. You could do this in other ways, including creating two data frames, one with repeats and one without, but I generally prefer doing things all in one pipe.

data %>%
  mutate(brk = list(breaks)) %>%
  unnest() %>%
  mutate(brk_fct = as.factor(brk) %>% fct_rev()) %>%
  ggplot(aes(x = date, y = value)) +
    geom_ribbon(aes(ymin = value * (1 - brk), ymax = value * (1 + brk), fill = brk_fct)) +
    geom_line(data = . %>% distinct(date, value)) +
    scale_fill_brewer(palette = "Reds")

Created on 2018-10-05 by the reprex package (v0.2.1)

camille
  • 16,432
  • 18
  • 38
  • 60
0

My first instinct is to make a long dataset with breaks as a new column a la the other answer. However, you can add layers in loops.

Adding layers with loops can be tricky, since ggplot2 delays evaluation until the plot is rendered (see the explanation here). We can force evaluation by using "unquoting" via tidyeval code.

You'll see I loop through the number of breaks and add a layer for each of them, but force evaluation by unquoting with !!.

You'll see I also use rev() to reverse the color palette.

reds = brewer.pal(length(breaks), "Reds")

p1 = ggplot(data = data,
               aes(x = date, y = value))
for(i in 1:length(breaks)) {
    p1 = p1 + geom_ribbon( aes(ymin = value*(1 - !!breaks[i]),
                          ymax = value*(1 + !!breaks[i])),
                      fill = rev(reds)[i],
                      alpha = .4)
}
p1 + geom_line(size = 1)

enter image description here

aosmith
  • 34,856
  • 9
  • 84
  • 118
0

This is basically a base version of @camille's answer (which I didn't see while composing). Anyway, can just as well post it...

ggplot(data = data, aes(x = date, y = value)) +
  geom_line(size = 1) +
  geom_ribbon(data = merge(expand.grid(date = data$date, breaks = breaks), data), 
              aes(ymin = value * (1 - breaks), ymax = value * (1 + breaks),
                  fill = factor(breaks, levels = rev(unique(breaks)))), alpha = 0.4) +
 scale_fill_brewer(palette = "Reds")
Henrik
  • 65,555
  • 14
  • 143
  • 159