3

I'm creating lineplots using ggplot() and geom_line() for a corridor of values that develops over time.

It may happen sometimes that the upper bound is below the lower bound (which I'll call "inversion"), and I would like to highlight regions where this happens in my plot, say by using a different background color.

Searching both Google and StackOverflow has not led me anywhere.

Here is an artificial example:

library(tidyverse)
library(RcppRoll)

set.seed(42)
N <- 100
l <- 5
a <- rgamma(n = N, shape = 2)
d <- tibble(x = 1:N, upper = roll_maxr(a, n = l), lower = roll_minr(a + lag(a), n = l)) %>% mutate(inversion = upper < lower)
dl <- pivot_longer(d, cols = c("upper", "lower"), names_to = "Bounds", values_to = "bound_vals")

ggplot(dl, mapping = aes(x = x, y = bound_vals, color = Bounds)) + geom_line(linewidth = 1) + theme_light()

This produces the following plot:

example plot

As you can see, inversion occurs in a few places, e.g. around x = 50. I would like for the plot to have a darker (say gray) background where it does, based on the inversion column already in the tibble. How can I do this?

Thank you very much for the help!

BestGirl
  • 319
  • 1
  • 13

2 Answers2

5

One option to achieve your desired result would be to use ggh4x::stat_difference like so. Note that to this end we have to use the wide dataset and accordingly add the lines via two geom_line.

library(ggplot2)
library(ggh4x)

ggplot(d, mapping = aes(x = x)) + 
  stat_difference(aes(ymin = lower, ymax = upper)) +
  geom_line(aes(y = lower, color = "lower"), linewidth = 1) + 
  geom_line(aes(y = upper, color = "upper"), linewidth = 1) + 
  scale_fill_manual(values = c("+" = "transparent", "-" = "darkgrey"),
                    breaks = "-",
                    labels = "Inversion") +
  theme_light() +
  labs(color = "Bounds")

enter image description here

EDIT Of course is it also possible to draw background rects for the intersection regions. But I don't know of any out-of-the-box option, i.e. the tricky part is to compute the x values where the lines intersect which requires some effort and approximation. Here is one approach but probably not the most efficient one.

library(tidyverse)

# Compute intersection points and prepare data to draw rects
n <- 20 # Increase for a better approximation 
rect <- data.frame(
  x = seq(1, N, length.out = N * n)
)

# Shamefully stolen from ggh4x
rle_id <- function(x) with(rle(x), rep.int(seq_along(values), lengths))

rect <- rect |> 
  mutate(lower = approx(d$x, d$lower, x)[["y"]],
         upper = approx(d$x, d$upper, x)[["y"]],
         inversion = upper < lower,
         rle = with(rle(inversion & !is.na(inversion)), rep.int(seq_along(values), lengths))
        ) |>
  filter(inversion) |> 
  group_by(rle) |> 
  slice(c(1, n())) |>
  mutate(label = c("xmin", "xmax")) |> 
  ungroup() |> 
  select(x, rle, label) |> 
  pivot_wider(names_from = label, values_from = x)


ggplot(dl, mapping = aes(x = x, y = bound_vals, color = Bounds)) + 
  geom_line(linewidth = 1) + 
  geom_rect(data = rect, aes(xmin = xmin, xmax = xmax, group = rle), 
            ymin = -Inf, ymax = Inf, fill = "darkgrey", alpha = .3, inherit.aes = FALSE) +
  theme_light()
#> Warning: Removed 9 rows containing missing values (`geom_line()`).

stefan
  • 90,330
  • 6
  • 25
  • 51
  • Thanks, that certainly works, though it's not quite what I'm after yet. Is there a way to extend the shaded regions along the vertical axis? Something akin to [this image](https://www.nber.org/sites/default/files/2020-11/recessions%20figure%20111620.jpg). – BestGirl Jan 03 '23 at 09:54
  • 1
    Unfortunately I don't know of any out of the box option for that case but see my edit for one approach to achieve that. – stefan Jan 03 '23 at 10:33
  • Thanks again. Side question: I usually use `%>%` for piping data, is there an advantage to `|>`? – BestGirl Jan 03 '23 at 13:26
  • 1
    Hm. IMHO the main advantage is that `|>` is available out-of-the-box. That was my main reason to switch. But as of the moment there are some limitations so in some cases `%>%` can't be simply replaced by `|>`. – stefan Jan 03 '23 at 14:43
  • Thank you again. I was able to work out something that I think reflects the logic of my problem a bit better, but your solution was inspiring and helpful. Thank you for investing your time to help out a random stranger on the Internet, it's much appreciated. – BestGirl Jan 04 '23 at 11:43
2

Answering myself, the following worked for me in the end (also using actual data and plots grouped with facet_wrap()); h/t to @stefan, whose approach with geom_rect() I recycled:

library(tidyverse)
library(RcppRoll)

set.seed(42)
N <- 100
l <- 5
a <- rgamma(n = N, shape = 2)
d <- tibble(x = 1:N, upper = roll_maxr(a, n = l), lower = roll_minr(a + lag(a), n = l)) %>%
    mutate(inversion = upper < lower,
           inversionLag = if_else(is.na(lag(inversion)), FALSE, lag(inversion)),
            inversionLead = if_else(is.na(lead(inversion)), FALSE, lead(inversion)),
        inversionStart = inversion & !inversionLag,
        inversionEnd = inversion & !inversionLead
    )
dl <- pivot_longer(d, cols = c("upper", "lower"), names_to = "Bounds", values_to = "bound_vals")

iS <- d %>% filter(inversionStart) %>% select(x) %>% rowid_to_column() %>% rename(iS = x)
iE <- d %>% filter(inversionEnd) %>% select(x) %>% rowid_to_column() %>% rename(iE = x)
iD <- iS %>% full_join(iE, by = c("rowid"))

g <- ggplot(dl, mapping = aes(x = x, y = bound_vals, color = Bounds)) +
    geom_line(linewidth = 1) +
    geom_rect(data = iD, mapping = aes(xmin = iS, xmax = iE, fill = "Inversion"), ymin = -Inf, ymax = Inf, alpha = 0.3, inherit.aes = FALSE) +
    scale_fill_manual(name = "Inversions", values = "darkgray") +
    theme_light()
g

This gives

sample output

which is pretty much what I was after.

BestGirl
  • 319
  • 1
  • 13