12

Is it possible to adjust errorbars in ggplot2 so that they are plotted only in one direction (e.g. only upwards but not downwards)?

df <- data.frame(trt = factor(c(1, 1, 2, 2)), resp = c(1, 5, 3, 4),
                 group = factor(c(1, 2, 1, 2)), se = c(0.1, 0.3, 0.3, 0.2))
df2 <- df[c(1,3), ]

limits <- aes(ymax = resp + se, ymin = resp - se)
dodge <- position_dodge(width = 0.9)

p <- ggplot(df, aes(fill = group, y = resp, x = trt))
p + geom_bar(position = dodge, stat = "identity") +
    geom_errorbar(limits, position = dodge, width = 0.25)

Rplot

Henrik
  • 65,555
  • 14
  • 143
  • 159
jokel
  • 982
  • 3
  • 11
  • 15

5 Answers5

22

An easy work-around would be to plot the error bars first:

p +
  geom_errorbar(limits, position = dodge, width=0.25) +
  geom_bar(position = dodge, stat = "identity")

enter image description here

Henrik
  • 65,555
  • 14
  • 143
  • 159
7

Henrik's advice is excellent in this case, but I would suggest that you take a look at where the upper and lower limits are set.

 limits <- aes(ymax = resp + se, ymin = resp - se)

In that line, you explicitly tell ggplot to put the lower extension by setting ymin as resp - se; if you just set it as resp, then you'd have only the upper extension.

 limits <- aes(ymax = resp + se, ymin = resp)

... although you'd also have a black line at the top of the bar. To make this look cleaner, you could add a black outline to the entire bar.

p <- ggplot(df, aes(fill = group, y = resp, x = trt))+
  geom_bar(position = dodge, stat = "identity") +
  geom_bar(position = dodge, stat = "identity", 
           color="black", show_guide=FALSE)+
  geom_errorbar(limits, position = dodge, width = 0.25)
p

bar plot

Notice how I duplicated the bar plot layer, but with a "black" color, which adds that outline. The legend was turned off in the colored layer because I personally prefer to avoid the diagonal lines in the legend.

Matt74
  • 729
  • 4
  • 8
6

It's actually pretty easy to implement this for use in the general case (where you can't hide the lower error bar under another plot element) now that they've made it easy to extend ggplot2 with custom geoms.

Copy the code from geom_errorbar from the github repository for ggplot2 into a new .R file. Then make a few modifications, as follows:

geom_uperrorbar <- function(mapping = NULL, data = NULL,
   stat = "identity", position = "identity",
   ...,
   na.rm = FALSE,
   show.legend = NA,
   inherit.aes = TRUE) {
   layer(
      data = data,
      mapping = mapping,
      stat = stat,
      geom = GeomUperrorbar,
      position = position,
      show.legend = show.legend,
      inherit.aes = inherit.aes,
      params = list(
         na.rm = na.rm,
         ...
      )
   )
}

The only two changes necessary above were to change geom_errorbar to geom_uperrorbar and geom = GeomErrorbar to geom = GeomUperrorbar.

GeomUperrorbar <- ggproto("GeomUperrorbar", Geom,
   default_aes = aes(colour = "black", size = 0.5, linetype = 1, width = 0.5,
      alpha = NA),

   draw_key = draw_key_path,

Note that this comment is interrupting partway through a function which continues in the code blocks below. Above we just changed GeomErrorbar to GeomUperrorbar twice.

   required_aes = c("x", "y", "ymax"),

   setup_data = function(data, params) {
      data$width <- data$width %||%
         params$width %||% (resolution(data$x, FALSE) * 0.9)

      transform(data,
         xmin = x - width / 2, xmax = x + width / 2, width = NULL
      )
   },

Interrupting the function again. Above we changed the required aesthetics to be x, y and ymax, i.e. replacing ymin with y. We need y to start the vertical line there (instead of at ymin) and we no longer need ymin because there's not going to be a horizontal line there.

   draw_panel = function(data, panel_scales, coord, width = NULL) {
      GeomPath$draw_panel(data.frame(
         x = as.vector(rbind(data$xmin, data$xmax, NA, data$x,   data$x)),
         y = as.vector(rbind(data$ymax, data$ymax, NA, data$ymax, data$y)),
         colour = rep(data$colour, each = 5),
         alpha = rep(data$alpha, each = 5),
         size = rep(data$size, each = 5),
         linetype = rep(data$linetype, each = 5),
         group = rep(1:(nrow(data)), each = 5),
         stringsAsFactors = FALSE,
         row.names = 1:(nrow(data) * 5)
      ), panel_scales, coord)
   }
)

Here we removed the last three elements of the vectors passed to x and y, which were for the lower error bar. In addition, we changed the last element from ymin to y, because we want the line to start at y, not ymin.

"%||%" <- function(a, b) {
   if (!is.null(a)) a else b
}

This last bit is just a convenience function used in the code that needs to be defined.

If you source the document including all of this code, then you can use geom_uperrorbar just like geom_errorbar, or even pass geom = "uperrorbar" to stat_summary, using y instead of ymin.

Sean Hughes
  • 328
  • 3
  • 7
3

Henriks answer works if all of the bars are larger than the error bars. If you have error bars that are larger than your data bars, however, the tail end of the 'bottom' tail (negative error bar for positive data bar or vice versa) will be seen because it will cross the 0 y axis. If all of your data are in the same direction, you can set your limits ymin equal to 0.

The problem with this approach is that if you have mixed data bars (some positive and some negative) you can't set a limit to 0. For example, if the original data were:

df <- data.frame(trt = factor(c(1, 1, 2, 2)), resp = c(1, 5, -3, 4),
             group = factor(c(1, 2, 1, 2)), se = c(2, 0.3, 4, 0.2))
df2 <- df[c(1,3), ]

limits <- aes(ymax = resp + se, ymin = resp - se)
dodge <- position_dodge(width = 0.9)

p <- ggplot(df, aes(fill = group, y = resp, x = trt))
p + geom_errorbar(limits, position = dodge, width = 0.25) +
    geom_bar(position = dodge, stat = "identity") 

The resulting plot would have error bars that extend beneath trt 1, group 1 and above trt 2, group1.

This can be fixed by adjusting the limit statment with an ifelse() clause:

limits <- aes(ymax = ifelse(resp>0,resp + se,resp/2),
              ymin = ifelse(resp<0,resp - se,resp/2))

p + geom_errorbar(limits, position = dodge, width = 0.25) +
    geom_bar(position = dodge, stat = "identity") 

In this case, if the data are positive, the error bar max remains as normal, but the error bar min is adjusted to half of the databar height, so it remains hidden behind the databar.

  • The solution is neat but we see a thin black line for the error bar where it touches the bar (and if geom_point is used instead). The solution refered to by @Robert Wagner is suitable for other geom. – Denis Cousineau Jul 08 '22 at 21:22
1

Check out this solution:

R ggplot: suppress bottom of error bar on geom_bar

You just make your min/max both your upper limit and overlay a geom_linerange. Super hacky and clever

Robert Wagner
  • 150
  • 1
  • 4