1

This is a follow-up to a question (and answer) about dynamic word wrapping of x axis labels.


EDIT - TL;DR Summary


(A) Summary

  1. I have a ggplot object made with geom_bar() and ggfittext::geom_bar_text().
  2. I want to extract bar labels from that object.
  3. I also want to extract the font size for those labels.
  4. I'm trying to do the above for two kinds of plots: a typical bar chart and one split to facets.

(B) Desired output

  • character vector with bar labels text
  • numeric vector with bar labels' sizes

(C) ggplot objects of question
1 - object p

library(ggfittext)
library(tidyverse)

my_cats_df <-
  data.frame(breed = c("Domestic Cat", "Persian", "Siamese", "Maine Coon"),
             weight = c(9, 10, 7, 17),
             breed_description = c("the only domesticated species in the family Felidae",
                                   "a long-haired, round face and short muzzle",
                                   "blue almond-shaped eyes; a triangular head shape",
                                   "the largest domesticated cat breed") )


p <-
  my_cats_df %>%
  ggplot(aes(x = breed, y = weight, fill = breed)) +
  geom_bar(stat = "identity") +
  geom_bar_text(aes(label = breed_description), 
                min.size = 0,
                reflow = TRUE)

2 - object p_facets_by_var

my_cats_df_by_sex <- data.frame(breed = rep(c("Domestic Cat", "Persian", "Siamese", "Maine Coon"), each = 2),
                                sex = rep(c("male", "female"), 2),
                                weight = c(11, 6, 12, 8, 10, 5, 20, 15),
                                breed_description = rep(c("the only domesticated species in the family Felidae",
                                               "a long-haired, round face and short muzzle",
                                               "blue almond-shaped eyes; a triangular head shape",
                                               "the largest domesticated cat breed"), each = 2))

p_facets_by_var <- 
  my_cats_df_by_sex %>%
  ggplot(aes(x = breed, y = weight, fill = breed)) +
  geom_bar(stat = "identity") +
  geom_bar_text(aes(label = breed_description), 
                min.size = 0,
                reflow = TRUE) +
  facet_wrap(~ sex)

The story behind the question

Read if you want context, otherwise skip all sections below.

Problem

x axis labels overlap. Example:

library(tidyverse)

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

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

Background

I need to generate plots in an automated pipeline, for different types of data, and therefore cannot rely on manual solutions for text wrapping such as stringr::str_wrap(). This is because they require parameters that may be good for one dataset but not necessarily for others. In addition, such solutions are not robust to changes in graphics device, for example shrinking the plot's size is likely to scramble the text formatting.

In the same vein, if we plot using facet_wrap()/facet_grid(), it means less space for text, and default styling parameters (such as text size) make it likely to have the text scrambled somewhere.

My idea

I want to achieve dynamic text wrapping, robust to changes in plot's dimensions or when dividing to facets. One way to do it is using the package ggfittext, whose purpose is fitting text within boxes. Its algorithm figures out how much space there's for text, and accordingly wraps the text and/or reduces text size to fit within the box.

Unfortunately, ggfittext is currently for geoms only, and does not have an element_text() subclass that could make it work directly with scale x labels.

I want to come up with the following workflow, and ideally wrap this within a custom function:

  1. Draw a bar plot and fit text within bars using ggfittext.
  2. Save the plot into object p.
  3. Extract from p the fitted text: both text format and size.
  4. Re-plot:
    • pass text extracted in step (3) to scale_x_discrete()
    • pass text size extracted in step (3) to theme(axis.text.x = element_text(size = ...)
    • don't include any ggfittext geom

This answer brought me pretty close to the final output, but I think it deserves a separate post, hence the current.

Example

library(ggfittext)
#> Warning: package 'ggfittext' was built under R version 4.0.3
library(tidyverse)

my_cats_df <-
  data.frame(breed = c("Domestic Cat", "Persian", "Siamese", "Maine Coon"),
             weight = c(9, 10, 7, 17),
             breed_description = c("the only domesticated species in the family Felidae",
                                   "a long-haired, round face and short muzzle",
                                   "blue almond-shaped eyes; a triangular head shape",
                                   "the largest domesticated cat breed") )


p <-
  my_cats_df %>%
  ggplot(aes(x = breed, y = weight, fill = breed)) +
  geom_bar(stat = "identity") +
  geom_bar_text(aes(label = breed_description), 
                min.size = 0,
                reflow = TRUE)
#> Warning: Ignoring unknown aesthetics: label

p


## the following is using @teunbrand's solution (https://stackoverflow.com/a/65958754/6105259)
## but I don't understand this at all
grob <- grid::makeContent(layer_grob(p, 2)[[1]])$children

grob
#> (fittexttree[GRID.fittexttree.79])

sizes <- vapply(grob, function(x){x$gp$fontsize}, numeric(1))
#> Error in vapply(grob, function(x) {: values must be length 1,
#>  but FUN(X[[1]]) result is length 0

labels <- unname(vapply(grob, function(x){x$label}, character(1)))
#> Error in vapply(grob, function(x) {: values must be length 1,
#>  but FUN(X[[1]]) result is length 0

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

Example (2) -- faceted plot

library(tidyverse)
library(ggfittext)
#> Warning: package 'ggfittext' was built under R version 4.0.3

my_cats_df_by_sex <- data.frame(breed = rep(c("Domestic Cat", "Persian", "Siamese", "Maine Coon"), each = 2),
                                sex = rep(c("male", "female"), 2),
                                weight = c(11, 6, 12, 8, 10, 5, 20, 15),
                                breed_description = rep(c("the only domesticated species in the family Felidae",
                                               "a long-haired, round face and short muzzle",
                                               "blue almond-shaped eyes; a triangular head shape",
                                               "the largest domesticated cat breed"), each = 2))

p_facets_by_var <- 
  my_cats_df_by_sex %>%
  ggplot(aes(x = breed, y = weight, fill = breed)) +
  geom_bar(stat = "identity") +
  geom_bar_text(aes(label = breed_description), 
                min.size = 0,
                reflow = TRUE) +
  facet_wrap(~ sex)
#> Warning: Ignoring unknown aesthetics: label

p_facets_by_var

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


In both cases above (either p or p_facets_by_var), I'd like to extract the labels text and size from the plot object. A character vector should be passed to scale_x_discrete() for re-plotting.

As for the axis labels size, in the case ggfittext rendered various sizes for bars, we should just take the smallest and pass that to theme(axis.text.x = element_text(size = ...) for the re-plot.

My hope is to generalize this workflow as much as possible, perhaps with a function. Unfortunately, I'm clueless about extracting information from a given ggplot object. I don't even know how to read it (for example, grid::makeContent(layer_grob(p, 2)[[1]])$children returns (fittexttree[GRID.fittexttree.79])), how to deal with it?).

My desired solution here is to get some function/workflow for ending up with a plot that has scale x labels rendered by ggfittext. If someone can also lay out guidelines how to work with the underlying mechanism to generalize this solution to other plots (e.g., how would we get the rendered labels for a geom_boxplot()?) it would very useful. Thanks!

Emman
  • 3,695
  • 2
  • 20
  • 44
  • I've also been digging into this issue trying to build the solution you're proposing, and haven't been able to do so. I don't think ggplot provides an easy way to access bar widths (because R is an awful language). The closest I got was `plot_object$layers[[1]]` but it says "width" is null – Matt Jan 30 '23 at 22:09

0 Answers0