4
votes

I'm looking for a way to automatically move the y-axis tick labels so that they appear left-justified within the actual plot area. I love the general flexibility of theme components in ggplot, but ran into a wall trying to find a general way to do this.

I know giving axis.text.y a combination of hjust=0 and a negative right margin (*gag*) can accomplish this effect, but the negative margin has to be manually set to match the width of the longest y-axis tick label.

As an example, consider the following code:

library(ggplot2)

set.seed(0)
dat <- data.frame(x = 1:100, y = (1:100) + runif(100, -10, 10))

p1 <- ggplot(dat, aes(x, y)) + 
  geom_line() +
  scale_y_continuous("", breaks = c(0, 30, 60, 90),
                     labels = c(0, 30, 60, "90 units of something")) +
  theme(axis.text.y = element_text(hjust = 0,
                                   margin = margin(0, -3.1, 0, 0, 'cm')))

I think it elegantly incorporates the y-axis label (e.g., "units of something") into the body of the plot, but in order to accomplish it, the -3.1 in the last line has to be found manually (by trial and error), which adds insult to injury: not only am I using a negative margin to pull text where it doesn't want to be -- I'm throwing in some arcane, fragile, hard-coded magic number.

Does anyone know where I can look for a more general and elegant solution to this problem?

1
crude approach but ... ggplot(dat, aes(x, y)) + geom_line() + scale_y_continuous("", breaks=c(0, 30, 60, 90)) + annotate("text", x=-Inf, y=c(0, 30, 60, 90), label=c(0, 30, 60, "90 units of something"), hjust=0 )+ theme(axis.text.y = element_blank())user20650
Thanks! I've toyed with something similar, but hate that you then lose theme control over the tick labels.Andrew Milligan
agreed, there is probably a way using ideas from stackoverflow.com/questions/27667017/…, or stackoverflow.com/questions/45123684/… , but a wee bit esotericuser20650
What do you mean by "lose theme control over the tick labels"?Roman

1 Answers

3
votes

Here's a hack using grobs, which shifts the y-axis labels away from its original position to overlap the plot area.

While I don't consider it exactly elegant (it's a hack that requires conversion in ggplotGrob after all), the positioning is not hard-coded, & you can specify whatever theme control you want in ggplot() before conversion.

Setting up:

library(ggplot2)
library(grid)
library(gtable)

# sample data
set.seed(0)
dat <- data.frame(x=1:100, y=(1:100) + runif(100, -10, 10))

# create ggplot object
p <- ggplot(dat, aes(x, y)) + 
  geom_line() +
  scale_y_continuous("", 
                     breaks = c(0, 30, 60, 90),
                     labels = c(0, 30, 60, "90 units of something")) +
  # left-align y-axis labels
  # you can also specify other theme parameters as desired
  theme(axis.text.y = element_text(hjust = 0))

Hack into grobs:

# convert from ggplot to grob object
gp <- ggplotGrob(p)

# locate the grob that corresponds to y-axis labels
y.label.grob <- gp$grobs[[which(gp$layout$name == "axis-l")]]$children$axis    

# remove y-axis labels from the plot, & shrink the space occupied by them
gp$grobs[[which(gp$layout$name == "axis-l")]] <- zeroGrob()
gp$widths[gp$layout$l[which(gp$layout$name == "axis-l")]] <- unit(0, "cm")

Define a new grob for the y-axis ticks / labels, with the horizontal order [ticks][labels][buffer space]:

# create new gtable
new.y.label.grob <- gtable(heights = unit(1, "npc"))

# place axis ticks in the first column
new.y.label.grob <- gtable_add_cols(new.y.label.grob,
                                    widths = y.label.grob[["widths"]][2])
new.y.label.grob <- gtable_add_grob(new.y.label.grob,
                                    y.label.grob[["grobs"]][[2]],
                                    t = 1, l = 1)

# place axis labels in the second column
new.y.label.grob <- gtable_add_cols(new.y.label.grob,
                                    widths = y.label.grob[["widths"]][1])
new.y.label.grob <- gtable_add_grob(new.y.label.grob,
                                    y.label.grob[["grobs"]][[1]],
                                    t = 1, l = 2)

# add third column that takes up all the remaining space
new.y.label.grob <- gtable_add_cols(new.y.label.grob,
                                    widths = unit(1, "null"))

Add the newly defined y-axis label grob back to the plot grob, in the same location as the plot area:

gp <- gtable_add_grob(gp,
                      new.y.label.grob,
                      t = gp$layout$t[which(gp$layout$name == "panel")],
                      l = gp$layout$l[which(gp$layout$name == "panel")])

Check result:

grid.draw(gp)

result