45

I'm looking for a way to dynamically wrap the strip label text in a facet_wrap or facet_grid call. I've found a way to accomplish this using strwrap, but I need to specify a width for the output to work as desired. Often the number of facets is not known in advance, so this method requires me to iteratively adjust the width parameter based on the dataset and plot size. Is it possible to dynamically specify a width for the wrap function, or is there another option for labeling facets that would work better?

library(ggplot2)
df = expand.grid(group=paste(c("Very Very Very Long Group Name "), 1:9),
                 x=rnorm(5), y=rnorm(5), stringsAsFactors=FALSE)

df$groupwrap = unlist(lapply(strwrap(df$group, width=30, simplify=FALSE), paste, 
                             collapse="\n"))
p = ggplot(df) +
  geom_point(aes(x=x, y=y)) +
  facet_wrap(~groupwrap)

UPDATE: Based on the guidance provided by @baptiste and @thunk, I came up with the option below. Currently, it only works for a specified font family and size, but ideally, one should be able to also use the default theme settings. Maybe someone with more ggplot2 experience has some suggestions for improvement.

library('grid')
grobs <- ggplotGrob(p)

sum = sum(sapply(grobs$width, function(x) convertWidth(x, "in")))
panels_width = par("din")[1] - sum  # inches

df$group = as.factor(df$group)
npanels = nlevels(df$group)
if (class(p$facet)[1] == "wrap") {
  cols = n2mfrow(npanels)[1]
} else {
  cols = npanels
}

ps = 12
family = "sans"
pad = 0.01  # inches
panel_width = panels_width / cols
char_width = strwidth(levels(df$group)[
  which.max(nchar(levels(df$group)))], units="inches", cex=ps / par("ps"), 
                      family=family) / max(nchar(levels(df$group)))
width = floor((panel_width - pad)/ char_width)  # characters

df$groupwrap = unlist(lapply(strwrap(df$group, width=width, simplify=FALSE), 
                             paste, collapse="\n"))
ggplot(df) +
  geom_point(aes(x=x, y=y)) +
  facet_wrap(~groupwrap) +
  theme(strip.text.x=element_text(size=ps, family=family))
user338714
  • 2,315
  • 5
  • 27
  • 36
  • possible duplicate of [R: ggplot2, can I make the facet/strip text wrap around?](http://stackoverflow.com/questions/5574157/r-ggplot2-can-i-make-the-facet-strip-text-wrap-around) – baptiste May 24 '13 at 14:23
  • (not that it had an answer, but the two should be linked) – baptiste May 24 '13 at 14:23

4 Answers4

77

Since this question was posted, the new label_wrap_gen() function with ggplot2 (>= 1.0.0, I think) handles this nicely:

facet_wrap(~groupwrap, labeller = labeller(groupwrap = label_wrap_gen(10)))

Note that you have to specify a width for it to work.

For older ggplot2 versions:

facet_wrap(~groupwrap, labeller = label_wrap_gen())
shir
  • 51
  • 6
CephBirk
  • 6,422
  • 5
  • 56
  • 74
  • 2
    +1, this post should be much further up. I just had to change the syntax for recent ggplot — not specifying the width did nothing for me. – slhck Jan 19 '17 at 14:28
  • 1
    THIS IS FABULOUS. Thanks. – David M Nov 15 '17 at 16:32
  • 1
    Great tip, thanks! I use `label_wrap_gen()` without parameters and have no issues. (ggplot2 3.3.2) – yuk Sep 03 '20 at 18:04
  • I believe this is the best approach as of 2021. You can also use this approach with custom labels, see here: https://stackoverflow.com/questions/62944834/how-to-use-label-wrap-gen-with-as-labeller-in-facet-wrap – fry Oct 19 '21 at 11:35
  • FYI, I'm running ggplot2 version 3.4.1 and only the "older" versions' syntax worked for me. That is `facet_wrap(~groupwrap, labeller = label_wrap_gen())`. Thanks heaps, all the same. – James N Jul 13 '23 at 01:56
10

Thanks to the guidance from @baptiste and @thunk, I created the function below, which seems to do a pretty good job of automatically wrapping facet labels. Suggestions for improvement are always welcome, though.

strwrap_strip_text = function(p, pad=0.05) { 
  # get facet font attributes
  th = theme_get()
  if (length(p$theme) > 0L)
    th = th + p$theme

  require("grid")
  grobs <- ggplotGrob(p)

  # wrap strip x text
  if ((class(p$facet)[1] == "grid" && !is.null(names(p$facet$cols))) ||
        class(p$facet)[1] == "wrap")
  {
    ps = calc_element("strip.text.x", th)[["size"]]
    family = calc_element("strip.text.x", th)[["family"]]
    face = calc_element("strip.text.x", th)[["face"]]

    if (class(p$facet)[1] == "wrap") {
      nm = names(p$facet$facets)
    } else {
      nm = names(p$facet$cols)
    }

    # get number of facet columns
    levs = levels(factor(p$data[[nm]]))
    npanels = length(levs)
    if (class(p$facet)[1] == "wrap") {
      cols = n2mfrow(npanels)[1]
    } else {
      cols = npanels
    }

    # get plot width
    sum = sum(sapply(grobs$width, function(x) convertWidth(x, "in")))
    panels_width = par("din")[1] - sum  # inches
    # determine strwrap width
    panel_width = panels_width / cols
    mx_ind = which.max(nchar(levs))
    char_width = strwidth(levs[mx_ind], units="inches", cex=ps / par("ps"), 
                          family=family, font=gpar(fontface=face)$font) / 
      nchar(levs[mx_ind])
    width = floor((panel_width - pad)/ char_width)  # characters

    # wrap facet text
    p$data[[nm]] = unlist(lapply(strwrap(p$data[[nm]], width=width, 
                                         simplify=FALSE), paste, collapse="\n"))
  }

  if (class(p$facet)[1] == "grid" && !is.null(names(p$facet$rows))) {  
    ps = calc_element("strip.text.y", th)[["size"]]
    family = calc_element("strip.text.y", th)[["family"]]
    face = calc_element("strip.text.y", th)[["face"]]

    nm = names(p$facet$rows)

    # get number of facet columns
    levs = levels(factor(p$data[[nm]]))
    rows = length(levs)

    # get plot height
    sum = sum(sapply(grobs$height, function(x) convertWidth(x, "in")))
    panels_height = par("din")[2] - sum  # inches
    # determine strwrap width
    panels_height = panels_height / rows
    mx_ind = which.max(nchar(levs))
    char_height = strwidth(levs[mx_ind], units="inches", cex=ps / par("ps"), 
                           family=family, font=gpar(fontface=face)$font) / 
      nchar(levs[mx_ind])
    width = floor((panels_height - pad)/ char_height)  # characters

    # wrap facet text
    p$data[[nm]] = unlist(lapply(strwrap(p$data[[nm]], width=width, 
                                         simplify=FALSE), paste, collapse="\n"))
  }

  invisible(p)
}

To use the function, call it in place of print.

library(ggplot2)
df = expand.grid(group=paste(c("Very Very Very Long Group Name "), 1:4),
                 group1=paste(c("Very Very Very Long Group Name "), 5:8),
                 x=rnorm(5), y=rnorm(5), stringsAsFactors=FALSE)

p = ggplot(df) +
  geom_point(aes(x=x, y=y)) +
  facet_grid(group1~group)
strwrap_strip_text(p)
user338714
  • 2,315
  • 5
  • 27
  • 36
  • nice job! I've just remembered a keyword to find an [earlier discussion of this problem](http://stackoverflow.com/a/5587413/471093) – baptiste May 24 '13 at 14:22
3

(too long as a comment, but not a real answer either)

I don't think a general solution will exist directly within ggplot2; it's the classic problem of self-reference for grid units: ggplot2 wants to calculate the viewport sizes on-the-fly, while the strwrap would need to know a firm width to decide how to split the text. (there was a very similar question, but I forget when and where).

You could however write a helping function to estimate how much wrapping you'll need before plotting. In pseudo code,

# takes the facetting variable and device size
estimate_wrap = function(f, size=8, fudge=1){ 

    n = nlevels(f)
    for (loop over the labels of strwidth wider than (full.size * fudge) / n){
     new_factor_level[ii] = strwrap(label[ii], available width)
    }

  return(new_factor)
}

(with some standard unit conversions required)

Of course, things would get more complicated if you wanted to use space="free".

baptiste
  • 75,767
  • 19
  • 198
  • 294
  • Thanks for the pseudo code. Combined with suggestions from @thunk, there appears to be a better option than the iterative approach I'm currently using. The challenge will be understanding the `grid` package in more detail. – user338714 May 22 '13 at 13:56
  • If you get any further, please go ahead and post an answer yourself. Just curious... – dlaehnemann May 22 '13 at 19:12
1

Also too long for a comment but no full answer. It goes along the lines of baptiste's answer, but with a few more pointers:

p <- ggplot(df) + geom_point(aes(x=x, y=y)) + facet_wrap(~groupwrap)

# get the grobs of the plot and get the widths of the columns
grobs <- ggplotGrob(p)
grobs$width

# here you would have to use convertWidth from gridDebug package
# to convert all the units in the widths to the same unit (say 'pt'),
# including exctraction from the strings they are in -- also, I
# couldn't make it work neither for the unit 'null' nor for 'grobwidth',
# so you'll have to add up all the other ones, neglect grobwidth, and
# subtract all the widths that are not null (which is the width of each
# panel) from the device width
library('grid')
convertWidth(DO FOR EACH ELEMENT OF grobs$width)
sum <- SUM_UP_ALL_THE_NON-PANEL_WIDTHS

# get the width of the graphics device
device <- par('din')[1]

# get width of all panels in a row
panels_width <- device - sum

# get total number of panels in your case
df$group <- as.factor(df$group)
npanels <- nlevels(df$group)

# get number of panels per row (i.e. number of columns in graph) with
# the function that ggplot2 uses internally
cols <- n2mfrow(npanels)

# get estimate of width of single panel
panel_width <- panels_width / cols

Sorry that this is still patchy in parts. But that is as far as I got, so I hope these ideas might help along the way...

dlaehnemann
  • 671
  • 5
  • 17
  • `convertWidth` is in grid, no? – baptiste May 21 '13 at 12:21
  • Cheers for that, changed the package loading! – dlaehnemann May 21 '13 at 12:50
  • the fact that ggplot2 produces null units is precisely why a general dynamic solution is kind of impossible. Thus, I'm not sure it is necessary to even go to the trouble of looking at the gtable, since the width information that can be extracted from there will never be complete (but it can help with legend width, axis titles, etc.). A fudge factor to account for those annotations seemed easier ;) – baptiste May 21 '13 at 13:04
  • Thanks for the reply, made me stumble upon an earlier omission (`n2mfrow`). I agree that the gtable inspection is probably too much effort and would suggest just using the graphic device width (`par('din')[1]`) divided the number of columns (`n2mfrow(npanels)`) and maybe subtract a certain amount to account for all the margins, axes and so on. This amount (which you call a fudge factor?!) would have to be determined through some trial and error initially and should be raltive to the graphic device size - but it then should work for different panel numbers... – dlaehnemann May 21 '13 at 13:38
  • for lack of a better name, that's what i meant, yet :) – baptiste May 21 '13 at 13:41
  • one would have to distinguish between `facet_wrap` and `facet_grid`; the latter shouldn't use n2mfrow. – baptiste May 21 '13 at 13:42
  • Well, `facet_wrap` was used here, so that's where I looked. And `facet_grid` obviously doesn't have to determine the columns in such a way, as they are derived from the data directly. But if so you please, go ahead and distinguish! ;) – dlaehnemann May 21 '13 at 13:46