How to overlay a barplot on top of other plot with a different geom, by mapping the barplot positions to the original plot's scale

Viewed 109

I'm trying to overlay a barplot on top of an existing plot, by mapping the positions of the new barplot to the values of the original plot.

Example

Using the mpg dataset from the {ggplot2} package, I want to assess the relationship between the variables cty and manufacturer. To keep this simple, I'll focus on 3 manufacturers only.

Step 1 -- data for analysis

I'm going to add another column that categorizes the cty values into 3 categories "low", "medium", "high".

library(ggplot2)
library(dplyr)

my_manuf <- c("audi", "ford", "dodge")

my_df <-
  mpg %>%
  filter(manufacturer %in% my_manuf) %>%
  select(manufacturer, cty) %>%
  mutate(cty_range = case_when(between(cty, 9, 13) ~ "low",
                               between(cty, 13, 16) ~ "medium",
                               between(cty, 16, 21) ~ "high"))

my_df
#> # A tibble: 80 x 3
#>    manufacturer   cty cty_range
#>    <chr>        <int> <chr>    
#>  1 audi            18 high     
#>  2 audi            21 high     
#>  3 audi            20 high     
#>  4 audi            21 high     
#>  5 audi            16 medium   
#>  6 audi            18 high     
#>  7 audi            18 high     
#>  8 audi            18 high     
#>  9 audi            16 medium   
#> 10 audi            20 high     
#> # ... with 70 more rows

Step 2 -- fit a (very) simple model

2.1) Going to fit the model lm(cty ~ manufacturer)

library(emmeans)
library(ggeffects)

model_results <- 
  my_df %>%
  lm(cty ~ manufacturer, data = .) %>%
  ggeffects::ggemmeans(terms = "manufacturer") %>%
  as_tibble() %>%
  rename(manufacturer = x)

model_results
#> # A tibble: 3 x 6
#>   manufacturer predicted std.error conf.low conf.high group
#>   <fct>            <dbl>     <dbl>    <dbl>     <dbl> <fct>
#> 1 audi              17.6     0.521     16.6      18.6 1    
#> 2 dodge             13.1     0.364     12.4      13.9 1    
#> 3 ford              14       0.442     13.1      14.9 1

2.2) Going to merge model_results with my_df to be able to add raw data points to the plot.

data_for_plot <- left_join(my_df, model_results)

Step 3 -- visualize

p <-
  data_for_plot %>%
  ggplot(aes(x = manufacturer, y = predicted)) +
  geom_label(aes(label = round(predicted, 2))) +
  geom_errorbar(aes(ymin = conf.low, ymax = conf.high), width = 0.2) +
  geom_jitter(aes(x = manufacturer, y = predicted, color = cty_range)) +
  expand_limits(y = c(12, 21)) +
  theme_minimal()

p

image


Desired output

I want to add a barplot aligned to each manufacturer, to account for the proportion of the cty_range variable in each of its categories ("low"/"medium"/"high"). In my mind I imagine it as something like the following (not drawn to scale but values are real):

image_with_demo



I know there are tools from packages such as patchwork and cowplot. However, here I want to map the barplots to the x-axis scale, such that the barplot relevant to audi will be positioned respectively, etc.

Second, I wish there's an easy way to achieve this by simply adding another "barplot prop" geom to the structure that constructs p. This is because p already originates from data_for_plot, which has all the info we need to compute the proportions for the barplots (i.e., via dplyr::count() or alike).

1 Answers

Making use of patchwork::inset_elementyou could do:

library(ggplot2)
library(dplyr)

my_df_bar <- my_df %>% 
  count(manufacturer, cty_range) %>% 
  group_by(manufacturer) %>% 
  mutate(pct = n / sum(n),
         cty_range = factor(cty_range, levels = c("low", "medium", "high")))

p_bar <- ggplot(my_df_bar, aes(manufacturer, pct, fill = cty_range)) +
  geom_col(position = position_dodge2(width = .9)) +
  geom_text(aes(y = pct - .01, label =scales::percent(pct)), 
            position = position_dodge2(width = .9),
            size = 8 / .pt, vjust = 1) +
  theme_void() +
  guides(fill = "none")

library(patchwork)  

p + inset_element(p_bar, 0, .8, 1, 1)

EDIT Personally I would go for patchwork. (; But as an alternative approach you could achieve your result like so.

Most tricky part is to put the bars on top of the error bars and jitters which requires some transformation of the data similar to the ones necessary in case of a second-axis. Not sure whether it is easier to generalize this approach.

trans <- 21
scale <- 5

breaks_fun <- function(x) {
  scales::breaks_extended()(x + trans) - trans
}

p <-
  data_for_plot %>%
  ggplot(aes(x = manufacturer, y = predicted - trans)) +
  geom_label(aes(label = round(predicted, 2))) +
  geom_errorbar(aes(ymin = conf.low - trans, 
                    ymax = conf.high - trans), width = 0.2) +
  geom_jitter(aes(x = manufacturer, y = predicted - trans, color = cty_range)) +
  scale_y_continuous(breaks = breaks_fun, labels = ~ .x + trans) +
  theme_minimal()

pct <- function(count, group) {
  count / tapply(count, group, sum)[group]
}

p +
  geom_bar(aes(fill = cty_range,
               y = scale * after_stat(pct(count, x))),
            stat = "count", position = position_dodge2(width = .9)) +
  geom_text(aes(group = cty_range,
                y = scale * after_stat(pct(count, x)) - .1,
                label = scales::percent(after_stat(pct(count, x)))),
            stat = "count", position = position_dodge2(width = .9),
            size = 8 /.pt, vjust = 1) +
  guides(fill = "none") +
  coord_flip()

Related