Under the hood of ggplot2 graphics in R

Originally, we were planning to include this material in our popular Beautiful plotting ggplot2 cheatsheet. But once we finished writing the post we realized that only a certain type of person (I’m looking at you) would really want to perform this level of customization on their graphics. Illustrator would do the job a lot more quickly!

This post was inspired by a useful answer to a question on stackoverflow by Matthew Plourde. You should also take a look at Carson Sievert’s nice tool to visualize the structure of a ggplot graphic which is a more elegant alternative to the brute force approach below.

Quick-setup: The dataset

We’re using the same data from our cheat sheet — data from the National Morbidity and Mortality Air Pollution Study (NMMAPS). To make the plots manageable we’re limiting the data to Chicago and 1997-2000.

library(ggplot2)
nmmaps<-read.csv("X:/zev/workshops/r_workshop_isee2014/slides/chicago-nmmaps.csv", as.is=T)
nmmaps$date<-as.Date(nmmaps$date)
nmmaps<-nmmaps[nmmaps$date>as.Date("1996-12-31"),]
nmmaps$year<-substring(nmmaps$date,1,4)
head(nmmaps)
##      city       date death temp dewpoint      pm10        o3 time season
## 3654 chic 1997-01-01   137 36.0    37.50 13.052268  5.659256 3654 winter
## 3655 chic 1997-01-02   123 45.0    47.25 41.948600  5.525417 3655 winter
## 3656 chic 1997-01-03   127 40.0    38.00 27.041751  6.288548 3656 winter
## 3657 chic 1997-01-04   146 51.5    45.50 25.072573  7.537758 3657 winter
## 3658 chic 1997-01-05   102 27.0    11.25 15.343121 20.760798 3658 winter
## 3659 chic 1997-01-06   127 17.0     5.75  9.364655 14.940874 3659 winter
##      year
## 3654 1997
## 3655 1997
## 3656 1997
## 3657 1997
## 3658 1997
## 3659 1997

Under the hood

When you really want to fiddle with your plots you can use the functions ggplot_gtable and ggplot_build to access the inner workings of plot objects. But be careful, this is not for the faint of heart – there is very little documentation and Hadley Wickham, ggplot2’s creator, specifically warns that the functions could change in the future and not to rely too heavily on them. You’ve been warned.

The initial plot

As an example, let’s revisit this plot from the ggplot2 cheat sheet:

ggplot(nmmaps, aes(date,temp))+geom_point(color="chartreuse4")+
  facet_wrap(~year, ncol=2)

plot of chunk unnamed-chunk-3

We want to eliminate the second x-axis

Let’s say you think it’s overkill to label the x-axis twice and want to remove the axis from the second plot. To get at the plot guts you can use the functions ggplot_gtable and ggplot_build. The ggplot_build function outputs a list of data frames (one for each layer) and a panel object with information about axis limits among other things. The ggplot_gtable function, which takes the ggplot_build object as input, builds all grid graphical objects (known as “grobs”) necessary for displaying the plot. You can manipulate the output from ggplot_build.

Let’s start by running these two functions on our plot:

g<-ggplot(nmmaps, aes(date,temp))+geom_point(color="chartreuse4")+
  facet_wrap(~year, ncol=2)

gTable <- ggplot_gtable(ggplot_build(g))

Now we have a list-type object of grobs, one grob for each piece of the plot. So, for example, you have a grob for the 1997 grey header strip and a grob for the x axis on the plot for the year 2000. Figuring out what grob contains the pieces you need is not easy. For a plot with little detail you might use str(gTable) to manually look at the pieces and see if you can figure things out. In our case we have 20 grobs so a manual inspection will not work well.

You might also look at Carson Sievert’s nice tool to visualize the structure of a ggplot graphic which is a preferable alternative to brute force.

In this example, my first step was to print each of the grobs to a separate page in a PDF and investigate:

library(grid) # we need grid for several functions
pdf("grobs.pdf")
for(i in 1:length(gTable$grobs)){
  grid.draw(gTable$grobs[[i]])
  grid.text(i, x = unit(0.1, "npc"), y = unit(0.1, "npc"))
  grid.newpage()
}
dev.off()

In looking at the grobs PDF you might start to see patterns. You’ll see that grobs 2-5 look like background panels – the background panels for each of our four plots. And, 6-9 look like the strips at the top of the plots. And 14 and 15 are blank and 16 and 17 have axis. Yes! The x-axes on the first two plots are blank and both 16 and 17 have an x-axis. So it appears to me that grob 17 is what I need.

From gTable$grobs[[17]] I used trial and error, looking at the object, running attributes() on the object to track down the pieces I needed. This took some time…

# step-by-step I'm tracking down the axis labels
gTable$grobs[[17]]
attributes(gTable$grobs[[17]])
attributes(gTable$grobs[[17]]$children)
attributes(gTable$grobs[[17]]$children[2])
attributes(gTable$grobs[[17]]$children[2]$axis)
gTable$grobs[[17]]$children[2]$axis$grobs 
attributes(gTable$grobs[[17]]$children[2]$axis$grobs[[2]])

And here it is!!

gTable$grobs[[17]]$children[2]$axis$grobs[[2]]$label 
## [1] "1997" "1998" "1999" "2000" "2001"

Now that I’ve identified the piece I can make my change assigning the empty string instead of the years:

gTable$grobs[[17]]$children[2]$axis$grobs[[2]]$label<-""

Since gTable is a grob (type class(gTable) to see) I can plot it using grid.draw():

grid.draw(gTable)

plot of chunk unnamed-chunk-9

That worked but I think I’ll also get rid of the tick marks. Again, trial and error to find the ticks. We will change the y attribute of the ticks and make this 0. It can’t be simply the number 0, though, it needs to be a unit object. A unit object is a numeric value with an associated unit and allows us to set x and/or y using centimeters or on a scale of 0-1 (also known as Normalised Parent Coordinates, npc).

gTable$grobs[[17]]$children[2]$axis$grobs[[1]]$y<-rep(unit(0, units="cm"),5)
grid.draw(gTable)

plot of chunk unnamed-chunk-10

Let’s play a bit more and make a couple of changes to the x-axis on the 3rd plot. I want to rotate the text (actually it looks better without rotating, this is for demonstration purposes). If you look at the attributes for the axis grob you may see rot which is rotate:

# attributes(gTable$grobs[[16]]$children[2]$axis$grobs[[2]])

# Change the rotation
gTable$grobs[[16]]$children[2]$axis$grobs[[2]]$rot<-30

# Rotation make text cramped with text so add x and y space
gTable$grobs[[16]]$children[2]$axis$grobs[[2]]$x<-
  gTable$grobs[[16]]$children[2]$axis$grobs[[2]]$x-unit(0.025, "npc")

gTable$grobs[[16]]$children[2]$axis$grobs[[2]]$y<-
  gTable$grobs[[16]]$children[2]$axis$grobs[[2]]$y-rep(unit(0.1, units="cm"),5)

And a last step, we need to move the x-label left and since we rotated the text we need to move the x-label down a bit to allow space.

gTable$grobs[[18]]$x<-unit(0.25, units="npc")
gTable$grobs[[18]]$y<-unit(0.1, units="npc")
grid.draw(gTable)

plot of chunk unnamed-chunk-12

Voila!

2 responses

Leave a Reply to zev@zevross.com Cancel reply

Your email address will not be published. Required fields are marked *