Search code examples
rggplot2geom-text

Aligning a geom_text layer vertically on a bar chart


I currently have the geom_text set to the center of the bar chart...

enter image description here

library(dplyr)
library(ggplot2)

enroll_bar <- enroll_cohort %>%
    filter(chrt_grad != 2013) %>%
    mutate(college_enrolled = factor(college_enrolled),
           chrt_grad = factor(chrt_grad)) %>%
    mutate(label_height = cumsum(n)) %>%
    ggplot() +
    geom_col(mapping = aes(x = chrt_grad,
                           y = n,
                           fill = fct_rev(college_enrolled))) +
    geom_text(aes(x = chrt_grad, y = label_height, label = n), color = "white",
              position = position_stack(vjust = 0.5)) +
    scale_y_continuous(expand = expansion(mult = c(0, 0.1))) +
    labs(x = NULL, y = NULL) +
    scale_fill_manual(labels = c("Enrolled", "Not Enrolled"),
                      values = c("#00aeff", "#005488"))


How do I align the text vertically, so the geom_text layer is set at the same y-axis position for each bar? Also, is there a way to do this so the text is always aligned no matter the scale of the y-axis because this is a parameterized report and the y-axis values change with each report?


Solution

  • You can set a uniform label height for each group using if_else (or case_when for >2 groups). For a single plot, you can simply set a value, e.g., label_height = if_else(college_enrolled == "Enrolled", 20000, 3000). To make the relative height consistent across multiple plots, you can instead set label_height as a proportion of the y-axis range:

    library(tidyverse)
    
    # make a fake dataset
    enroll_cohort <- expand_grid(
        chrt_grad = factor(2014:2021),
        college_enrolled = factor(c("Enrolled", "Not Enrolled")),
      ) %>%
      mutate(
        n = sample(18000:26000, 16),
        n = if_else(college_enrolled == "Enrolled", n, as.integer(n / 3))
      )
    
    enroll_bar <- enroll_cohort %>% 
      group_by(chrt_grad) %>%            # find each bar's height by summing up `n`
      mutate(bar_height = sum(n)) %>%    # within each year
      ungroup() %>%
      mutate(label_height = if_else(
        college_enrolled == "Enrolled", 
        max(bar_height) * .6,            # axis height is max() of bar heights;
        max(bar_height) * .1             # set label_height as % of axis height
      )) %>%
      ggplot() +
      geom_col(aes(x = chrt_grad, y = n, fill = college_enrolled), color = NA) +
      geom_text(
        aes(x = chrt_grad, y = label_height, label = n), 
        color = "white"
      ) +
      scale_y_continuous(expand = expansion(mult = c(0, 0.1))) +
      labs(x = NULL, y = NULL) +
      scale_fill_manual(values = c("#00aeff", "#005488"))
    
    A stacked bar chart with y axis 0 to about 30,000. Value labels are at the same height for each group regardless of y value.

    If we generate another dataset with a different range of n values -- e.g., ~1200 - ~2000 -- the text labels stay at the same relative positions: A stacked bar chart with y axis 0 to 2500. Value labels are at the same height for each group regardless of y value.