This is a follow-up to a question (and answer) about dynamic word wrapping of x axis labels.
EDIT - TL;DR Summary
(A) Summary
- I have a ggplot object made with
geom_bar()
andggfittext::geom_bar_text()
. - I want to extract bar labels from that object.
- I also want to extract the font size for those labels.
- 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:
- Draw a bar plot and fit text within bars using
ggfittext
. - Save the plot into object
p
. - Extract from
p
the fitted text: both text format and size. - 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
- pass text extracted in step (3) to
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!