Daniel V
Daniel V

Reputation: 1388

Split y axis in ggplot

I have data in a structure similar to the following structure:

data.frame(x = c("A", "B", "C", "D", "E"), 
           a = abs(rnorm(5)), 
           b = abs(rnorm(5)))

I want to display this in the following manner (forgive the poor paint job): Example

To do this, I have written the below code:

set.seed(20)
data.frame(x = c("A", "B", "C", "D", "E"), 
           a = abs(rnorm(5)), 
           b = abs(rnorm(5))) %>%
  mutate(b = -b) %>%
  gather("source", "amount", a, b) %>%
  ggplot(aes(x = x,
             y = amount,
             fill = source)) +
  geom_col() +
  scale_y_continuous(labels = abs)

Which gives me the following:

Result

How would I go about adding the gap along y = 0 and filling it with the x axis labels?

Upvotes: 4

Views: 1281

Answers (2)

aosmith
aosmith

Reputation: 36076

I can think of a couple of ways to approach this.

You can try to get tricky with facets and free scales, labeling only the top axis using the approach I saw here.

However, you'll see this leaves some awkward space below the plot since it that didn't seem easy to remove (but see here).

library(ggplot2)
library(tidyr)
library(dplyr)

set.seed(20)
dat = data.frame(x = c("A", "B", "C", "D", "E"), 
           a = abs(rnorm(5)), 
           b = abs(rnorm(5))) %>%
    mutate(b = -b) %>%
    gather("source", "amount", a, b) %>%
    mutate(x1 = if_else(source == "a",
                          as.character(x), 
                          paste0(as.character(x), 'no_display')))

# function to suppress labels
delete_no_display = function(v) {
    if_else(stringr::str_detect(v, 'no_display'), '', v)
}

ggplot(dat, aes(x = x1,
               y = amount,
               fill = source)) +
    geom_col() +
    facet_wrap(~source, ncol = 1, scales = "free") +
    scale_x_discrete(name = NULL, label = delete_no_display) + 
    scale_y_continuous(name = NULL,
                       labels = abs,
                       breaks = c(-3, -2, -1, 0, 1, 2, 3), 
                       expand = c(0, 0)) +
    theme(strip.background = element_blank(),
        strip.text.x = element_blank(),
        axis.ticks.x = element_blank())

enter image description here

Another option is to build the plots separately per group and then combine them. You can do this via functions from package cowplot. This package has five helpful vignettes available if you decide to get into the nitty gritty.

This package does have strong opinions on theme, although given the plot you are attempting to make this theme may be what you want so I left it as is.

First I make the two plots. I added a fill legend to the first plot, but that can be removed as needed.

g1 = ggplot(subset(dat, source == "a"), 
            aes(x = x, y = amount, fill = source)) +
    geom_col() +
    scale_x_discrete(name = NULL) + 
    scale_y_continuous(name = NULL, 
                       labels = abs,
                       limits = c(0, 3),
                       expand = expand_scale(mult = c(0, .1) ) ) +
    scale_fill_manual(limits = c("a", "b"), 
                      values = c("#F8766D", "#00BFC4")) +
    theme(plot.margin = margin(0, 0, 0, 0),
          axis.ticks.x = element_blank())

g2 = ggplot(subset(dat, source == "b"), 
            aes(x = x, y = amount, fill = source)) +
    geom_col() +
    scale_x_discrete(name = NULL) + 
    scale_y_continuous(name = NULL, 
                       labels = abs,
                       limits = c(-3, 0),
                       expand = expand_scale(mult = c(.1, 0) ) ) +
    scale_fill_manual(limits = c("a", "b"), 
                      values = c("#F8766D", "#00BFC4"),
                      guide = "none") +
    theme(axis.ticks.x = element_blank(),
          axis.text.x = element_blank(),
          plot.margin = margin(t = 2, unit = "mm"))

Then extract the legend and combine the plots (with no legends) using plot_grid().

library(cowplot)

legend_a = get_legend(g1)

combined = plot_grid(g1 + theme(legend.position = "none"),
          g2,
          ncol = 1, align = "v")

This combined plot then looks like:

enter image description here

You can add the legend back on (see the shared legends vignette) and/or put on an overall y axis label as shown here if you want (although the legend spacing gets funky).

plot_grid(combined, legend_a, 
          rel_widths = c(2, .2),
          scale = c(.93, 1)) +
    draw_label("amount", x = 0, y = .5, angle = 90, vjust = 1)

enter image description here

The downside to the cowplot approach is that the bottom plot ends up slightly larger than the upper one. If I use align = "hv" I'm back to having a little extra space along the bottom. It may be best to remove all axes and then manually insert the labels. I feel like there is info to be gleaned here, but I didn't get that far.

Upvotes: 4

Jon Spring
Jon Spring

Reputation: 66415

We can get most of the way there by moving them to facets:

set.seed(20)
data.frame(x = c("A", "B", "C", "D", "E"), 
           a = abs(rnorm(5)), 
           b = abs(rnorm(5))) %>%
  mutate(b = -b) %>%
  gather("source", "amount", a, b) %>%
  ggplot(aes(x = x,
             y = amount,
             fill = source)) +
  geom_col() + 
  scale_y_continuous(labels = abs) +

  # NEW STUFF:
  facet_wrap(~source, ncol=1, scales = "free_y") +
  theme(strip.text = element_blank())

enter image description here

Here's one way get the x-axis labels in the middle, by making a layer of geom_text and placing it in the y range below the top facet. I'm not aware of a good "built-in" way to do this.

data.frame(x = c("A", "B", "C", "D", "E"), 
           a = abs(rnorm(5)), 
           b = abs(rnorm(5))) %>%
  mutate(b = -b) %>%
  gather("source", "amount", a, b) %>%
  ggplot(data = .,
         aes(x = x,
             y = amount,
             fill = source)) +
  geom_col() + 

  # removing minor_breaks avoids grid lines in the middle space
  scale_y_continuous(labels = abs, minor_breaks = NULL) +
  # this creates a single copy of the text, related to one facet
  geom_text(data = . %>% filter(source == "a"), aes(x, y = -.2, label = x)) +
  # this allows for printing outside the plot range
  coord_cartesian(clip = "off") +
  facet_wrap(~source, ncol=1, scales = "free_y", shrink = TRUE ) +
  theme(strip.text = element_blank(),
        axis.text.x = element_blank())

enter image description here

Upvotes: 4

Related Questions