Adding a horizonal line to separate band of bars in geom_col in ggplot2

I guess my question is very simple for someone who knows ggplot very well but I spent a lot of time trying different ways. I want to draw a horizontal line that pass-through y axis to separate each band of the bars produced using geom_col. For example, I want to draw a horizontal line that separate bars of meat from maize. Here is my code, example data and the design of the figure I want to produce.

library(tidyverse)
library(ggplot2)

#  sample data 
Food = c("meat", "meat", "meat", "meat", "wheat","wheat","wheat", "wheat", "maize","maize","maize","maize")
Subgroup = c("Male", "Female", "Urban", "Rural", "Male",  "Female", "Urban", "Rural",  "Male",  "Female","Urban", "Rural")
mean = c(8.66, 10.45,  9.88,  7.32, 21.04, 19.65, 20.26, 20.87, 51.06 , 44.51,  47.60, 48.40)
df <- data.frame(Food, Subgroup,  mean)

#Color code
colorPanel = c('#083c5d','#2d004b','#106d8e','#7d103d')

# Plot
Plot_FBGDS <-  ggplot(df, aes(x = Food, y = mean,  fill = Subgroup)) + 
  geom_col(stat = "identity", position = position_dodge(-0.9), width = 0.82) + 
  
  scale_y_continuous(breaks = c(0,20, 40, 60,80), expand = c(0,0),
                     limits = c(0,100), 
                     labels = function(x) paste0(x, "%")) +
  coord_flip() +  
  scale_fill_manual(values =  colorPanel) + 
  labs( x= " ", 
        y = " ") 
    

Solution 1:

Try this, using geom_vline with manually specified xintercept.

# Plot
ggplot(df, aes(x = Food, y = mean,  fill = Subgroup)) + 
  geom_col(stat = "identity", position = position_dodge(-0.9), width = 0.82) + 
  
  scale_y_continuous(breaks = c(0,20, 40, 60,80), expand = c(0,0),
                     limits = c(0,100), 
                     labels = function(x) paste0(x, "%")) +
  geom_vline(xintercept = c(0.5, 1.5, 2.5, 3.5)) + 
  coord_flip() +  
  scale_fill_manual(values =  colorPanel) + 
  labs( x= " ", 
        y = " ") 

Note that geom_vline typically produces a vertical line, but since you have coord_flip it becomes horizontal. Without the coord_flip, you would use geom_hline and set the yintercept parameter instead.

Also, if I may suggest an alternative way to visualize this using facet_wrap instead, I would say the option below looks a lot better and you can style the facets using the strip_ properties in plot_theme

# Plot
ggplot(df, aes(x = Subgroup, y = mean,  fill = Subgroup)) + 
  geom_col(stat = "identity", position = position_dodge(-0.9), width = 0.82) + 
  
  scale_y_continuous(breaks = c(0,20, 40, 60,80), expand = c(0,0),
                     limits = c(0,100), 
                     labels = function(x) paste0(x, "%")) +
  coord_flip() +  
  scale_fill_manual(values =  colorPanel) + 
  facet_wrap(~Food, ncol=1)
  labs( x= " ", 
        y = " ") +
  theme(
    legend.position = "none"
  )

Solution 2:

I'm going to build off the answer already posted here from @geoff. OP requested lines that extend beyond the plot area into the axis label. It's true that normally geoms are restricted to the panel area alone, but that's only by default. For any of the coord_*() functions, you can change the default clipping from "on" to "off" via clip="off".

For geom_vline() and geom_hline(), these seem to be automatically clipped to the panel area (more on that below), but for just about all other geoms, you can extend beyond the plot area. We can use this to our advantage in using geom_segment() and specifying the lines.

In this example, I'm going to need to create a linesdata data frame outside of the plot to make drawing the lines a bit easier (and reference that dataset in geom_segment()). We also need to change clip="off" within coord_flip(), and finally, I had to adjust the value for the staring value of the line (y here because we flip the axis) to be some negative value to get below the axis.

linesdata <- data.frame(
  xvals = c(0.5, 1.5, 2.5, 3.5),
  Subgroup=NA   # required because it complains for fill, which I cannot specify again for geom_segment.
)

ggplot(df, aes(x = Food, y = mean,  fill = Subgroup)) + 
  geom_col(position = position_dodge(-0.9), width = 0.82) + 
  
  scale_y_continuous(breaks = c(0,20, 40, 60,80), expand = c(0,0),
                     limits = c(0,100), 
                     labels = function(x) paste0(x, "%")) +
  coord_flip(clip="off") + 
  geom_segment(
    data=linesdata, y=-5, yend=Inf,
    aes(x=xvals, xend=xvals)
  ) +
  scale_fill_manual(values =  colorPanel) + 
  labs( x= " ", 
        y = " ")

enter image description here

Note that I had to include Subgroup as a column in the linesdata data frame. The way to avoid having to do this would be to specify the fill= aesthetic inside geom_col instead of globally... but it works this way too.

The reason why geom_hline and vline always clip

Interestingly, you'll note that even though yend=Inf, the line does not extend beyond the panel area in the positive direction! I had no idea this works this way, but It seems Inf is specially designed to clip to the panel area no matter what. I'm pretty sure that geom_vline() and geom_hline() are using values of Inf and -Inf under the hood. If I change the value to y=-Inf inside geom_segment() you can see it doesn't extend the same way as specifying a number:

ggplot(df, aes(x = Food, y = mean,  fill = Subgroup)) + 
  geom_col(position = position_dodge(-0.9), width = 0.82) + 
  
  scale_y_continuous(breaks = c(0,20, 40, 60,80), expand = c(0,0),
                     limits = c(0,100), 
                     labels = function(x) paste0(x, "%")) +
  coord_flip(clip="off") + 
  geom_segment(
    data=linesdata, y=-Inf, yend=Inf,
    aes(x=xvals, xend=xvals)
  ) +
  scale_fill_manual(values =  colorPanel) + 
  labs( x= " ", 
        y = " ")

enter image description here

The only problem here is that you have to play around a bit to find the right value for the starting y value via trial and error. I found -5 did the trick pretty well.