Search code examples
rggplot2labelggtextggfittext

ggplot2: How to dynamically wrap/resize/rescale x axis labels so they won't overlap


I'm trying to achieve a solution for wrapping x axis labels so they won't overlap. I know this question has been asked several times, and that there are some good answers. However, no solution that I've seen answers how to re-wrap the labels as the plot gets resized.

Three different answers at SO make me believe this could be attainable.

  1. This solution wrote a custom-made geom for fitting the bar's label size to fit within the bar's width, dynamically as you resize the plot.

  2. This solution relies on an extension package for ggplot2 called ggtext. The solution allows dynamic word wrapping of the plot's title, as you resize the plot, based on creating a element_textbox().

  3. This solution relies on another extension called ggfittext. It shows how the size of the label inside the bar can vary dynamically to fit the bar's dimensions as you resize the plot. Essentially, it addresses the same problem as solution (1) above, but is much more powerful. In fact, and this is the feature that makes me hopeful, it relies on a general solution geom_fit_text() to fit text inside rectangles, not just geom_bar()s.

Some demo data to work with

1. Just to show the typical output when x axis labels are overlapping

  library(tidyverse)
  
  my_mtcars <-
    mtcars[15:20,] %>% 
    rownames_to_column("cars")
  
  my_mtcars %>%
    ggplot(aes(x = cars, y = mpg, fill = cars)) + 
    geom_bar(stat = "identity")

Created on 2021-01-29 by the reprex package (v0.3.0)


2. When we use ggfittext we can see how labels inside the bars shrink in size to fit the bar

  library(tidyverse)
  library(ggfittext)
#> Warning: package 'ggfittext' was built under R version 4.0.3
  
  my_mtcars <-
    mtcars[15:20,] %>% 
    rownames_to_column("cars")
  
  my_mtcars %>%
    ggplot(aes(x = cars, y = mpg, fill = cars)) + 
    geom_bar(stat = "identity") +
    geom_bar_text(aes(label = cars), 
      color = "blue", 
      vjust = 1, 
      size = 7 * ggplot2::.pt, 
      min.size = 0,
      padding.x = grid::unit(0, "pt"),
      padding.y = grid::unit(0, "pt"))
#> Warning: Ignoring unknown aesthetics: label

Created on 2021-01-29 by the reprex package (v0.3.0)


3. ggfittext has the reflow argument that promotes text wrapping

  library(tidyverse)
  library(ggfittext)
#> Warning: package 'ggfittext' was built under R version 4.0.3
  
  my_mtcars <-
    mtcars[15:20,] %>% 
    rownames_to_column("cars")
  
  my_mtcars %>%
    ggplot(aes(x = cars, y = mpg, fill = cars)) + 
    geom_bar(stat = "identity") +
    geom_bar_text(aes(label = cars), 
      color = "blue", 
      vjust = 1, 
      size = 7 * ggplot2::.pt, 
      min.size = 0,
      padding.x = grid::unit(0, "pt"),
      padding.y = grid::unit(0, "pt"),
      reflow = TRUE ## <--------------- added this
      )
#> Warning: Ignoring unknown aesthetics: label

Created on 2021-01-29 by the reprex package (v0.3.0)


My question

I don't know how to do it, but could we get x axis labels wrapped/resized/rescaled dynamically, by somehow letting ggfittext do the hard work for us? In the naïve way I see this, the text within the bars is already rendered the right way, can we just "copy" this rendering somehow to the axis labels?


Solution

  • How about we just place the ggfittext text below the y-axis? We turn off clipping and set the oob and limits to suit our data. Should probably tweak the axis.text.x size to align better with the x-axis title.

    library(tidyverse)
    #> Warning: package 'tidyr' was built under R version 4.0.3
    #> Warning: package 'readr' was built under R version 4.0.3
    #> Warning: package 'dplyr' was built under R version 4.0.3
    library(ggfittext)
    #> Warning: package 'ggfittext' was built under R version 4.0.3
    
    my_mtcars <-
      mtcars[15:20,] %>% 
      rownames_to_column("cars")
    
    my_mtcars %>%
      ggplot(aes(x = cars, y = mpg, fill = cars)) + 
      geom_bar(stat = "identity") +
      geom_fit_text(aes(label = cars, y = -4),
                    reflow = TRUE, height = 50,
                    show.legend = FALSE) +
      scale_y_continuous(oob = scales::oob_keep,
                         limits = c(0, NA)) +
      coord_cartesian(clip = "off") +
      theme(axis.text.x = element_text(colour = "transparent", size = 18))
    

    Created on 2021-01-29 by the reprex package (v0.3.0)

    EDIT: Getting the labels out of the grob

    library(tidyverse)
    library(ggfittext)
    
    my_mtcars <-
      mtcars[15:20,] %>% 
      rownames_to_column("cars")
    
    p <- my_mtcars %>%
      ggplot(aes(x = cars, y = mpg, fill = cars)) + 
      geom_bar(stat = "identity") +
      geom_fit_text(aes(label = cars, y = -1),
                    reflow = TRUE, height = 50,
                    show.legend = FALSE) +
      scale_y_continuous(oob = scales::oob_keep,
                         limits = c(0, NA)) +
      coord_cartesian(clip = "off") +
      theme(axis.text.x = element_text(colour = "transparent", size = 18))
    
    grob <- grid::makeContent(layer_grob(p, 2)[[1]])$children
    
    sizes <- vapply(grob, function(x){x$gp$fontsize}, numeric(1))
    labels <- unname(vapply(grob, function(x){x$label}, character(1)))
    print(labels)
    #> [1] "Cadillac\nFleetwood"  "Lincoln\nContinental" "Chrysler\nImperial"  
    #> [4] "Fiat 128"             "Honda Civic"          "Toyota\nCorolla"
    

    Created on 2021-01-29 by the reprex package (v0.3.0)