2
votes

I regularly make barplots with the values of the bars additionally included as annotations (geom_text). Generally, I prefer these values to be right aligned (in contrast to putting the labels on the top of the bars). When plotting a faceted barplot I place these values on the maximum value within each group (which I calculate before) plus a little additional space which I add by multiplying the x value (I don’t use nudge_x since its absolute value might be fitting for some facets but not for others).

What annoys me with this approach is the remaining axis label under the annotation. See the image below (axis labels 15, 100, and 2.5). I would like to limit the x axis labels to (something close to) the maximum value in each facet and not extend all the way until the annotations.

I was wondering whether there is a better approach available.

(I am aware that I could produce the desired graph with group_split and e.g patchwork. My interest here is whether there is a direct way to limit the axis limits/labels of each individual facet).

Many thanks.

library(tidyverse)
#> Warning: package 'dplyr' was built under R version 3.6.2
#> Warning: package 'forcats' was built under R version 3.6.3

mtcars %>% 
  group_by(cyl, gear) %>% 
  summarise(n_obs=n()) %>% 
  mutate(n_obs=case_when(gear==4 ~ n_obs*100,
                         TRUE ~ as.numeric(n_obs))) %>% 
  group_by(gear) %>% 
  mutate(n_obs_max=max(n_obs, na.rm=T)) %>% 
  ggplot()+
  geom_bar(aes(y=cyl,
               x=n_obs),
           stat="identity")+
  geom_text(aes(y=cyl,
                x=n_obs_max*1.20,
                label=n_obs))+
  facet_wrap(vars(gear),
             scales="free_x")

Created on 2020-03-08 by the reprex package (v0.3.0)

UPDATE

Following up to @stafan's helpful answer below, here a modification and partial answer to my question.

Function passed to the breaks argument

  my_breaks <- function(x) {

    #calculates the max value on the x axis for each facet
    new_x=max(x) 

    #adjusts this max value for a) the extension of the x axis by the 
    #expand=expansion(mult=c(0, 0.3)) which was needed to have enough space 
    #for the annotation; and the factor added to the position of the 
    #annotations with   x=max_n_obs*1.10; the result is the maximum value 
    #of the bars in each facet;
    old_max <- new_x/1.3/1.1 

    #create 5 labels; the maximum is the highest bar in each facet
    my_pretty=labeling::extended(0, old_max, m=5) 

    #round these values 
    my_pretty=signif(my_pretty, digits=-2) 

    #remove the highest label(s)
    my_pretty=head(unique(my_pretty), -1) 

    #combine the remaining labels and the maximum value of the highest bar
    my_pretty=c(my_pretty, old_max) 
    my_pretty
}

Applied to my (modified) expample, this produces what I have been looking for (see graph below).

library(tidyverse)
#> Warning: package 'dplyr' was built under R version 3.6.2
#> Warning: package 'forcats' was built under R version 3.6.3

my_breaks <- function(x) {
  new_x=max(x)
  old_max <- new_x/1.2/1.05
  #old_max
  my_pretty=labeling::extended(0, old_max, m=5)
  my_pretty=signif(my_pretty, digits=-2)
  my_pretty=head(unique(my_pretty), -1)
  my_pretty=c(my_pretty, old_max)
  my_pretty

}  

mtcars %>% 
  group_by(cyl, gear) %>% 
  summarise(n_obs=n()) %>% 
  mutate(n_obs=case_when(gear==4 ~ n_obs*100,
                         TRUE ~ as.numeric(n_obs))) %>% 
  group_by(gear) %>% 
  mutate(n_obs_max=max(n_obs, na.rm=T)) %>% 
  ggplot()+
  geom_bar(aes(y=cyl,
               x=n_obs),
           stat="identity")+
  geom_text(aes(y=cyl,
                x=n_obs_max*1.20,
                label=n_obs))+
  scale_x_continuous(breaks=my_breaks1,
                     expand=expansion(mult=c(0, 0.05)))+
  facet_wrap(vars(gear),
             scales="free_x")

A drawback of this function is that the values for the scale expansion (1.3) and the factor for positioning the labels (1.1) are ‘hard coded’ into the function. What would be convenient would be to specify these values when passing on the function in the ggplot scale command, e.g something like

scale_x_continuous(breaks=my_breaks(expansion=1.3, pos.factor=1.1))

Unforuntately, I haven’t figured out how this works.

Created on 2020-03-09 by the reprex package (v0.3.0)

1
The code in your question does not reproduce the image you show (normal bars in the code, horizontal bars in the image)...dario
@dario I guess it's because I am using ggplot 3.3.zoowalk
I see (3.2.1 here). So ggplot2s behaviour changes ?! Interesting... As for your question... I don't see how this could be done using facet_. Maybe this so post has some alternative ideas?dario
@dario - yes, ggplot 3.3 introduced a number of new, nice features. As for the link, I was looking for an approach more 'direct' than patchwork. thx again.zoowalk

1 Answers

1
votes

Try this.

  1. I expanded the y axis.
  2. I adjusted the breaks. I borrowed the general idea from here. Function my_breaks returns pretty_breaks but removes the last value.

(Note: I also switched the aesthetics, y = nobs and x = cyl and used coord_flip, because running your code on my machine has not reproduced your plot (ggplot 3.3.0)):

library(tidyverse)
#> Warning: package 'forcats' was built under R version 3.6.3

my_breaks <- function(x, n = 5, drop = 2) {
  breaks <- seq(x[[1]], x[[2]], length.out = n)
  breaks <- scales::pretty_breaks()(breaks)
  breaks <- breaks[1:(length(breaks) - drop)]
  breaks
}

mtcars %>% 
  group_by(cyl, gear) %>% 
  summarise(n_obs = n()) %>% 
  mutate(n_obs = case_when(
    gear == 4 ~ n_obs * 100,
    TRUE ~ as.numeric(n_obs))) %>% 
  group_by(gear) %>% 
  mutate(n_obs_max = max(n_obs, na.rm=T)) %>% 
  ggplot(aes(x = cyl))+
  geom_bar(aes(y = n_obs), stat="identity")+
  geom_text(aes(y = n_obs_max * 1.2, label = n_obs))+
  facet_wrap(vars(gear), scales = "free_x") + 
  scale_y_continuous(breaks = function(x) my_breaks(x, 5, 2),
                     expand = expand_scale(mult = c(0.05, .2))) +
  coord_flip()
#> Warning: `expand_scale()` is deprecated; use `expansion()` instead.

Created on 2020-03-09 by the reprex package (v0.3.0)