19

I'm using facet_grid() to display some data, and I have facet labels that span multiple lines of text (they contain the "\n" character).

require(ggplot2)

#Generate example data
set.seed(3)
df = data.frame(facet_label_text = rep(c("Label A",
                                         "Label B\nvery long label",
                                         "Label C\nshort",
                                         "Label D"),
                                       each = 5),
                time = rep(c(0, 4, 8, 12, 16), times = 4),
                value = runif(20, min=0, max=100))

#Plot test data
ggplot(df, aes(x = time, y = value)) +
    geom_line() +
    facet_grid(facet_label_text ~ .) +
    theme(strip.text.y = element_text(angle = 0, hjust = 0))

So by using the hjust = 0 argument, I can left-align facet label text as a unit.

enter image description here

What I would like to do is left-align each individual line of text. So "Label B" and "very long label" are both aligned along the left side, rather than centered relative to each other (ditto for "Label C" and "short"). Is this possible in ggplot2?

zx8754
  • 52,746
  • 12
  • 114
  • 209
Brain_Food
  • 718
  • 1
  • 7
  • 17

3 Answers3

15

This is fairly straightforward using grid's grid.gedit function to edit the strips.

library(ggplot2)  # v2.1.0
library(grid)

# Your data
set.seed(3)
df = data.frame(facet_label_text = rep(c("Label A",
                                         "Label B\nvery long label",
                                         "Label C\nshort",
                                         "Label D"), 
                                       each = 5),
                time = rep(c(0, 4, 8, 12, 16), times = 4),
                value = runif(20, min=0, max=100))

# Your plot
p = ggplot(df, aes(x = time, y = value)) +
    geom_line() +
    facet_grid(facet_label_text ~ .) +
    theme(strip.text.y = element_text(angle = 0, hjust = 0))
p

# Get a list of grobs in the plot
grid.ls(grid.force())  

# It looks like we need the GRID.text grobs.
# But some care is needed:
# There are GRID.text grobs that are children of the strips;
# but also there are GRID.text grobs that are children of the axes.
# Therefore, a gPath should be set up 
# to get to the GRID.text grobs in the strips

# The edit
grid.gedit(gPath("GRID.stripGrob", "GRID.text"),  
         just = "left", x = unit(0, "npc"))

Or, a few more lines of code to work with a grob object (in place of editing on screen as above):

# Get the ggplot grob
gp = ggplotGrob(p)
grid.ls(grid.force(gp))

# Edit the grob
gp = editGrob(grid.force(gp), gPath("GRID.stripGrob", "GRID.text"), grep = TRUE, global = TRUE,
          just = "left", x = unit(0, "npc"))

# Draw it
grid.newpage()
grid.draw(gp)

enter image description here

Sandy Muspratt
  • 31,719
  • 12
  • 116
  • 122
  • Thanks for the great answer. It seems there's a lot of power in knowing how to modify the grid and grob objects underlying ggplot. Can you recommend a good beginner's resource or guide for grid/grob editing? Thanks again. – Brain_Food Jan 13 '16 at 16:18
  • 1
    After playing around with this a bit, I think you can get the grob without needing to display it first using this minor tweak: `grab = grid.grabExpr(grid.draw(gp))` – Brain_Food Jan 13 '16 at 16:39
  • @Brain_Food AFAIK, there's nothing that brings together grid editing of ggplot2 graphs in a comprehensive way. For `grid`, check out the references on the info page of `r-grid` tag on SO. [Paul Murrell's UseR 2015 workshop](https://www.stat.auckland.ac.nz/~paul/useR2015-grid/) give some detail on editing ggplots using grid functions. Otherwise, SO answers. Also, `gtable` functions are useful. Baptiste has put together [some notes on `gtable`](https://github.com/baptiste/gtable/wiki/Description) – Sandy Muspratt Jan 13 '16 at 23:42
  • This approach works indeed, but is there a way to re-capture the resulting `gTable` object as a `ggplot` object? I'm basically looking for the inverse of `ggplot2::ggplotGrob` ... – balin Apr 09 '18 at 17:00
  • @balin Sorry, `ggplotGrob` is a one way street. – Sandy Muspratt Apr 10 '18 at 02:42
11

Until someone comes along with a real solution, here's a hack: Add space in the labels to get the justification you want.

require(ggplot2)

#Generate example data
set.seed(3)
df = data.frame(facet_label_text = rep(c("Label A",
                                         "Label B           \nvery long label",
                                         "Label C\nshort     ",
                                         "Label D"),
                                       each = 5),
                time = rep(c(0, 4, 8, 12, 16), times = 4),
                value = runif(20, min=0, max=100))

#Plot test data
ggplot(df, aes(x = time, y = value)) +
  geom_line() +
  facet_grid(facet_label_text ~ .) +
  theme(strip.text.y = element_text(angle = 0, hjust = 0))

enter image description here

eipi10
  • 91,525
  • 24
  • 209
  • 285
9

There may be a cleaner way to do this but I didn't find a way to do this within ggplot2. The padwrap function could be more generalized as it basically does just what you requested. To get the justification right, I had to use a mono-spaced font.

# Wrap text with embedded newlines: space padded and lef justified.
# There may be a cleaner way to do this but this works on the one
# example.  If using for ggplot2 plots, make the font `family`
# a monospaced font (e.g. 'Courier')
padwrap <- function(x) {
    # Operates on one string
    padwrap_str <- function(s) {
        sres    <- strsplit(s, "\n")
        max_len <- max(nchar(sres[[1]]))
        paste( sprintf(paste0('%-', max_len, 's'), sres[[1]]), collapse = "\n" )
    }
    # Applys 'padwrap' to a vector of strings
    unlist(lapply(x, padwrap_str))
}

require(ggplot2)

facet_label_text = rep(c("Label A",
                         "Label B\nvery long label",
                         "Label C\nshort",
                         "Label D"), 5)
new_facet_label_text <- padwrap(facet_label_text)

#Generate example data
set.seed(3)
df = data.frame(facet_label_text = new_facet_label_text,
                time = rep(c(0, 4, 8, 12, 16), times = 4),
                value = runif(20, min=0, max=100))

#Plot test data
ggplot(df, aes(x = time, y = value)) +
    geom_line() +
    facet_grid(facet_label_text ~ .) +
    theme(strip.text.y = element_text(angle = 0, hjust = 0, family = 'Courier'))

The strip text is left justified in the image below

enter image description here

steveb
  • 5,382
  • 2
  • 27
  • 36
  • Clever idea. Maybe you could generalize it to non-mono-spaced fonts. Use `strwidth` to get the relative sizes of each character and then use that to set the amount of padding needed for each string. For example: `strwidth(c(LETTERS, letters, " "), units="inches", family="Times")` will give you the widths of all upper and lower case letters plus a space in the *Times* font. – eipi10 Jan 12 '16 at 21:32
  • @eipi10 I didn't know about `strwidth`. When I have a little time I will implement it and see how it works out (unless someone beats me to it). I was wondering how one would get the width of non mono spaced fonts; your suggestion solves that (assuming `strwidth` works as we hope). – steveb Jan 12 '16 at 22:19
  • Try this `plot(strwidth(c(LETTERS, letters, " "), units="inches", family="Times"), 1:53, pch=c(LETTERS, letters, " "))` and this `plot(strwidth(c(LETTERS, letters, " "), units="inches", family="Courier"), 1:53, pch=c(LETTERS, letters, " "))` and a few other fonts and you can see the variation in character width (or not for Courier). – eipi10 Jan 13 '16 at 04:35
  • Thank you and @eipi10 for the great answers. I'm going to go with the answer from Sandy Muspratt because it aligns the text directly without needing to add spaces or use specific fonts. That being said, I learned a lot about text formatting and manipulation in R from your posts. Thanks again. – Brain_Food Jan 13 '16 at 16:49