Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to dynamically wrap facet label using ggplot2

Tags:

r

ggplot2

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))
like image 698
user338714 Avatar asked May 20 '13 17:05

user338714


People also ask

How do you wrap facet labels?

How can I wrap facet label text so that long labels are spread across two rows? Use label_wrap_gen() in the labeller argument of your faceting function and set a width (number of characters) for the maximum number of characters before wrapping the strip.

How do I change the size of a facet label in ggplot2?

For that, we use theme() function, which is used to customize the appearance of plot. We can change size of facet labels, using strip. text it should passed with value to produce labels of desired size.

How do you change a facet label?

Change the text of facet labels Facet labels can be modified using the option labeller , which should be a function. In the following R code, facets are labelled by combining the name of the grouping variable with group levels. The labeller function label_both is used.

What is facet in ggplot2?

The facet approach partitions a plot into a matrix of panels. Each panel shows a different subset of the data. This R tutorial describes how to split a graph using ggplot2 package. There are two main functions for faceting : facet_grid()


4 Answers

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())
like image 185
CephBirk Avatar answered Oct 13 '22 11:10

CephBirk


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)
like image 33
user338714 Avatar answered Oct 13 '22 09:10

user338714


(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".

like image 22
baptiste Avatar answered Oct 13 '22 11:10

baptiste


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...

like image 21
dlaehnemann Avatar answered Oct 13 '22 11:10

dlaehnemann