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))
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.
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.
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.
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()
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())
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)
(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"
.
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...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With