Writing grid Extensions

by Paul Murrell

New hook functions, makeContext() and makeContent(), have been added to the grid graphics package. These functions allow an alternative approach to developing custom grobs when a grob can only decide what to draw at drawing time rather than when the grob is created. For custom grobs that are based on this new approach, the grid.force() function provides access to low-level grobs that are otherwise invisible because they are only generated at drawing time. These functions lead to greater flexibility in the development of custom grobs and more power to modify the result after drawing is complete.

From R 2.16.0 there is a new recommended approach to developing customised grid grobs. In a nutshell, two new "hook" functions, makeContext() and makeContent() have been added to grid to provide an alternative to the existing hook functions preDrawDetails(), drawDetails(), and postDrawDetails(). This article provides a simple demonstration of the use of these new functions and discusses the benefits that accrue from this new approach.

A simple grid customisation

In order to demonstrate the development of a custom grid grob, we will consider several different ways to produce a "boxed label" with grid. This grob will draw a text label and surround the text with a box (with rounded corners). The simplest way to implement this sort of thing in grid is to write a function that makes several calls to draw existing grid grobs. For example, the following code defines a textbox() function that takes a single argument, a text label, and calls grid.text() to draw the label and then grid.roundrect() to draw a box around the label. The stringWidth() and stringHeight() functions are used to make sure that the box is the right size for the label.
> library(grid)
> textbox <- function(label) {
+     grid.text(label, name="text")
+     grid.roundrect(width=1.5*stringWidth(label),
+                    height=1.5*stringHeight(label),
+                    name="box")
+ }
The following code shows the function in action and the output is shown below the code.
> grid.newpage()
> textbox("test")

This function has produced two grobs, a text grob and a roundrect grob.
> grid.ls(fullNames=TRUE)
text[text]
roundrect[box]
However, there is no connection between these grobs. For example, if we modify the text (code below), the roundrect stays the same size and becomes too small for the text (see the output below the code).
> grid.edit("text", label="hello world")

It is possible to group the two grobs together by constructing a gTree to contain them both. For example, the following code redefines the textbox() function so that it generates a gTree containing a text grob and a roundrect grob and then draws the gTree.
> textbox <- function(label) {
+     tg <- textGrob(label, name="text")
+     rr <- roundrectGrob(
+               width=1.5*stringWidth(label),
+               height=1.5*stringHeight(label),
+               name="box") 
+     grid.draw(gTree(children=gList(tg, rr),
+                     name="tb"))
+ }
This version of the function produces the same output as the previous version.
> grid.newpage()
> textbox("test")

However, the scene now consists of a single gTree that contains the text grob and the roundrect grob.
> grid.ls(fullNames=TRUE)
gTree[tb]
  text[text]
  roundrect[box]
But the contents of the gTree are fixed at creation time, so if we modify the text grob child of the gTree, the roundrect child is still not updated.
> grid.edit("tb::text", label="hello world")

The old drawDetails() hook

The behaviour of the text box can be made more coherent if we delay the construction of the box until drawing time (i.e., recalculate the box every time that we draw). This can be achieved in grid by creating a custom grob and then defining a drawDetails() method for the custom grob. For example, the following code redefines the textbox() function so that it generates a custom "textbox" grob and draws that.
> textbox <- function(label, 
+                     name=NULL, gp=NULL, vp=NULL) {
+     grid.draw(grob(label=label, 
+                    name=name, gp=gp, vp=vp, 
+                    cl="textbox"))
+ }
For this function to draw anything, we also need to define a drawDetails() method for "textbox" grobs. Such a method is shown in the code below, which is almost identical to the first version of textbox(); all that we have done is delay the generation of the text and box until drawing time.
> drawDetails.textbox <- function(x, ...) {
+     grid.text(x$label, name="text")
+     grid.roundrect(
+         width=1.5*stringWidth(x$label),
+         height=1.5*stringHeight(x$label),
+         name="box")
+ }
The following code shows the new textbox() function in action and shows that it produces exactly the same output as the first version.
> grid.newpage()
> textbox("test", name="tb")

One big difference is that only one "textbox" grob was generated, rather than separate text and roundrect grobs. The latter are only generated at drawing time and are not retained.
> grid.ls(fullNames=TRUE)
textbox[tb]
Another big difference is that, if we modify that one grob, both the text and the box are updated.
> grid.edit("tb", label="hello world")

However, one problem with this solution is that the text and box are no longer visible as separate grobs. For example, it is possible to change both text and box to grey (as below), but it is not possible to affect either just the text or just the box separately.
> grid.edit("tb", gp=gpar(col="grey"))

In other words, we have a convenient high-level interface to the combined text and box, but we only have that high-level interface.

The new makeContent() hook

The new makeContent() function provides an alternative way to develop a custom grid grob. The main difference is that, whereas a drawDetails() method typically calls grid functions to draw output, a makeContent() method calls grid functions to generate grobs. The standard behaviour for grobs automatically takes care of drawing the content. To continue our example, the following code redefines textbox() yet again. This is almost identical to the previous version of textbox(). The one important difference here is that the gTree() function is used to generate a custom gTree, rather than calling the grob() function to generate a simple custom grob. This is necessary whenever a makeContent() method generates more than one grob. We also create a different class than before, called "textboxtree" so that we do not have both drawDetails() and makeContent() methods defined for the same class.
> textbox <- function(label, 
+                     name=NULL, gp=NULL, vp=NULL) {
+     grid.draw(gTree(label=label, 
+                     name=name, gp=gp, vp=vp, 
+                     cl="textboxtree"))
+ }
So that this custom gTree will draw something, we define a makeContent() method. This is similar to the drawDetails() method above because it generates a text grob and a roundrect grob, but it does not draw them, it simply adds these grobs as children of the gTree. The modified gTree must be returned as the result of this function so that grid can draw the generated content.
> makeContent.textboxtree <- function(x) {
+     t <- textGrob(x$label,
+                   name="text")
+     rr <- roundrectGrob(width=1.5*grobWidth(t), 
+                         height=1.5*grobHeight(t),
+                         name="box")
+     setChildren(x, gList(t, rr))
+ }
The following code shows that the new textbox() function produces exactly the same output as before.
> grid.newpage()
> textbox("test", name="tbt")

As with the drawDetails() approach, the scene consists of only one grob, this time a "textboxtree" grob.
> grid.ls(fullNames=TRUE)
textboxtree[tbt]
Furthermore, if we modify that one grob, both the text and the box are updated.
> grid.edit("tbt", label="hello world")

In summary, the makeContent() approach behaves exactly the same as the drawDetails() approach. The advantages of the makeContent() approach lie in the extra things that it allows us to do.

The grid.force() function

The new function grid.force() affects any custom grobs that have a makeContent() method. This function replaces the original grob with the modified grob that is returned by the makeContent() method. For example, if we use grid.force() on a scene that contains a "textboxtree" grob, the output of the scene is unaffected (see below).
> grid.force()

However, the scene now consists of a gTree with a text grob and a roundrect grob as its children (rather than just a single "textboxtree" object).
> grid.ls(fullNames=TRUE)
forcedgrob[tbt]
  text[text]
  forcedgrob[box]
Now that we can see the individual components of the text box, we can modify them independently. For example, the following code just modifies the box component of the scene, but not the text component.
> grid.edit("box", gp=gpar(col="grey"))

In other words, in addition to the convenient high-level interface to the text box, we can now also access a low-level interface to the individual components of the text box.

The grid.reorder() function

Another new grid function in R 2.16.0 is the grid.reorder() function. This function allows reordering of the children of a gTree. In order to demonstrate the importance of the ordering of grobs, the following code modifies the box component of the current scene so that it is filled grey.
> grid.edit("box", gp=gpar(fill="grey"))

The unfortunate result is that the text disappears. This happens because the children of the gTree "tbt" are in the order "text first followed by box", so the box is drawn on top of the text. The grid.reorder() function allows us to fix this sort of thing by changing the order of the children. For example, the following code sends the child "box" to the back.
> grid.reorder("tbt", "box")

A listing of the grobs in the scene shows the new order, with the text now drawn after the box.
> grid.ls(fullNames=TRUE)
forcedgrob[tbt]
  forcedgrob[box]
  text[text]
The first argument to the grid.reorder() function identifies a gTree in the current scene and the second argument specifies a new ordering for the children of that gTree. The example above demonstrates that it is not necessary to name all children in the reordering; by default, the children named are used first in the new ordering and then all subsequent children are used. For example, the following code would produce the same result.
> grid.reorder("tbt", c("box", "text"))
A third argument, back, can be set to FALSE, in which case the unnamed children are used first, followed by the children named in the second argument, to produce a new ordering.

The grid.revert() function

One downside of calling grid.force() is that the convenient high-level interface to a custom grob is no longer available. For example, changing the label on the text box no longer has any effect.
> grid.edit("tbt", label="test")

The new grid.revert() function is provided to reverse the effect of grid.force() and replace the individual components of the custom grob with the original grob. The following code and shows this function in action. It also demonstrates that the reversion will lose any changes that were made to any of the individual components. We return to the scene that we had before the call to grid.force().
> grid.revert()

In other words, for grobs that generate content at drawing time, we can have either the high-level interface or the low-level interface, but not both at once.

A reminder

All of the discussion in this article applies to custom grid grobs that need to calculate what to draw at drawing time. If the entire content of a grob or gTree can be generated at creation time, rather than having to wait until drawing time, then things are much easier, and it is possible to have both a high-level interface and low-level access both at the same time. It is only when the content must be generated at drawing time that the design decisions and functions described in this article become necessary.

A more complex example

The ggplot2 package , via the gtable package , relies on grid grobs that generate their content at drawing time. For example, the following code uses the qplot() function from the ggplot2 package to draw a plot (see Figure
link).
> library(ggplot2)
> qplot(mpg, wt, data=mtcars, colour=cyl)

A plot produced by the qplot() function from the ggplot2 package.

This entire plot is represented by a single grob, which generates the content to draw the plot at drawing time.
> grid.ls(fullNames=TRUE)
gtable[layout]
It is possible to modify many features of the plot via the ggplot2 theming system, but there is no direct access to the low-level grobs that are generated at drawing time to draw the plot. However, we can gain access to the low-level grobs using grid.force().
> grid.force()
This does not change the appearance of the plot, but the low-level grobs that draw the plot are now visible.
> grid.ls(fullNames=TRUE)
forcedgrob[layout]
  forcedgrob[background.1-6-6-1]
  forcedgrob[spacer.4-3-4-3]
  forcedgrob[panel.3-4-3-4]
    gTree[grill.gTree.147]
      rect[panel.background.rect.138]
      polyline[panel.grid.minor.y.polyline.140]
      polyline[panel.grid.minor.x.polyline.142]
      polyline[panel.grid.major.y.polyline.144]
      polyline[panel.grid.major.x.polyline.146]
    points[geom_point.points.134]
    zeroGrob[panel.border.zeroGrob.135]
  forcedgrob[axis-l.3-3-3-3]
    zeroGrob[axis.line.y.zeroGrob.157]
    forcedgrob[axis]
      forcedgrob[axis.1-1-1-1]
      forcedgrob[axis.1-2-1-2]
  forcedgrob[axis-b.4-4-4-4]
    zeroGrob[axis.line.x.zeroGrob.151]
    forcedgrob[axis]
      forcedgrob[axis.1-1-1-1]
      forcedgrob[axis.2-1-2-1]
  forcedgrob[xlab.5-4-5-4]
  forcedgrob[ylab.3-2-3-2]
  forcedgrob[guide-box.3-5-3-5]
    forcedgrob[guides.2-2-2-2]
      forcedgrob[background.1-6-5-1]
      forcedgrob[bar.4-2-4-2]
      forcedgrob[label.4-4-4-4]
      forcedgrob[title.2-5-2-2]
      forcedgrob[ticks.4-2-4-2]
  forcedgrob[title.2-4-2-4]
One interesting thing that we can do now that we have low-level access is to insert a new grob within this hierarchy of existing grobs. The following code does this by adding a rectangle as a child of the "guide-box" gTree (the legend or key to the right of the plot).
> grid.add("guide-box", grep=TRUE,
+          rectGrob(gp=gpar(col=NA, fill="grey90"),
+                   name="guide-bg"))
If we just list the contents of this "guide-box" we can see the rectangle grob that we have added. Because this new child is the last child, it is drawn after the other contents of the legend (i.e., on top of the other contents of the legend; see Figure
link).
> grid.ls(grid.get("guide-box", grep=TRUE),
+         fullNames=TRUE)
forcedgrob[guide-box.3-5-3-5]
  forcedgrob[guides.2-2-2-2]
    forcedgrob[background.1-6-5-1]
    forcedgrob[bar.4-2-4-2]
    forcedgrob[label.4-4-4-4]
    forcedgrob[title.2-5-2-2]
    forcedgrob[ticks.4-2-4-2]
  rect[guide-bg]

The plot from Figure link with a rectangle added to (on top of) the tree of grobs that represent the legend on the right of the plot.

The following code uses grid.reorder() to fix up the order of the grobs in the "guide box" so that the new rectangle is at the back.
> grid.reorder("guide-box", "guide-bg", grep=TRUE)
A listing of the grobs in the "guide box" now shows the added rectangle at the start, which means it will be drawn behind the other content in the legend (see Figure link).
> grid.ls(grid.get("guide-box", grep=TRUE),
+         fullNames=TRUE)
forcedgrob[guide-box.3-5-3-5]
  rect[guide-bg]
  forcedgrob[guides.2-2-2-2]
    forcedgrob[background.1-6-5-1]
    forcedgrob[bar.4-2-4-2]
    forcedgrob[label.4-4-4-4]
    forcedgrob[title.2-5-2-2]
    forcedgrob[ticks.4-2-4-2]

The plot from Figure link with the rectangle shifted to the back of the tree of grobs that represent the legend on the right of the plot.

The new makeContext() hook

Another situation that leads to developing a custom grob occurs when the drawing context for a grob has to be determined at drawing time. In this situation, rather than generating grobs at drawing time, viewports must be generated at drawing time. The old solution for this situation was to write a preDrawDetails() method to generate and push one or more viewports (and a postDrawDetails() method to clean up again after drawing). The new solution is to use the makeContext() function to just generate one or more viewports (and grid automatically takes care of pushing the viewports and cleaning up afterwards). To demonstrate the new approach, we will reimplement the "boxed label" example again. The textbox() function will remain the same as before, just generating a "textboxtree".
> textbox <- function(label, 
+                     name=NULL, gp=NULL, vp=NULL) {
+     grid.draw(gTree(label=label, 
+                     name=name, gp=gp, vp=vp, 
+                     cl="textboxtree"))
+ }
However, we will create a makeContext() method that sets up a viewport to draw the text and box within. This viewport is sized appropriately for the text label and is combined with any existing viewports in the vp slot of the object. It is important that the method returns the modified object.
> makeContext.textboxtree <- function(x) {
+     tbvp <- 
+         viewport(width=1.5*stringWidth(x$label),
+                  height=1.5*stringHeight(x$label))
+     if (is.null(x$vp))
+         x$vp <- tbvp
+     else
+         x$vp <- vpStack(x$vp, tbvp)
+     x
+ }
The makeContent() method becomes simpler now because the box just fills the entire viewport that was set up in the makeContext() method.
> makeContent.textboxtree <- function(x) {
+     t <- textGrob(x$label, name="text")
+     rr <- roundrectGrob(name="box")
+     setChildren(x, gList(t, rr))
+ }
The following code shows that the textbox() function produces exactly the same output as before.
> grid.newpage()
> textbox("test", name="tbt")

Because the drawing context is generated at drawing time, modifying the text label updates the viewport that both text and box are drawn in, so the box expands with the text.
> grid.edit("tbt", label="hello world")

Another reminder

Modifying the drawing context at drawing time is not always necessary. When creating a custom grob, it is often simpler just to set up the drawing context at creation time by creating childrenvp for the children of a gTree. It is only when the generation of drawing context has to be delayed until drawing time that a makeContext() method becomes necessary.

When to use makeContent() or makeContext()

These functions are only necessary when it is not possible to determine either the drawing context or the drawing content at creation time. These functions may be used in other situations. For example, the main example used in this article does not strictly require using makeContent() because editDetails() could be used instead. So a developer could choose to use makeContent() in some cases because it may be easier than writing a editDetails() method. One example where makeContent() is necessary is the roundrect grob, which must perform conversion of locations to inches to determine what to draw and that calculation needs to be done at drawing time to make sure that it will work in any drawing context.\footnote{This is why the grob listing on page \pageref{page:force} shows a forcedgrob for the box; grid.force() replaced the roundrect with the polygon that the roundrect generated at drawing time in its makeContent() method.} Another example is axis grobs when there is no explicit specification of tick mark locations. In that case, the axis grob must wait until drawing time to know the native scale of the viewport it is being drawn in. Only then can the axis grob determine an appropriate set of tick mark locations. Finally, the gtable example demonstrates that a grob may wish to delay construction of its content until drawing time because the grob is expected to undergo dramatic changes before drawing occurs. In this case, it is inefficient to update the contents of the grob every time it is modified; it is better to wait until drawing time and perform the construction only once. These are only examples of situations that might motivate the use of makeContext() and makeContent(). In some cases, the decision will be forced, but in other cases the choice may be deliberate, so there is no fixed rule for when we might need to use these functions. It is also important to remember that simpler options may exist because grid already delays many calculations until drawing time via the use of gp and vp slots on grobs and the use of units for locations and dimensions. While it would be wrong to characterise these functions as a "last resort", developers of custom grobs should think at least twice before deciding that they are the best solution.

Summary

The functions makeContext() and makeContent() provide a new approach to developing custom grid grobs (replacing the old approach based on drawDetails() and preDrawDetails()). The advantage of the new approach is that, for grobs that generate content at drawing time, it is possible to access and edit the low-level content that is generated at drawing time by calling the grid.force() function. Another new function, grid.reorder(), allows the modification of the order of the children of a gTree. Together, these new features allow for greater flexibility in the development of custom grid grobs and greater powers to access and modify the low-level details of a grid scene.

Availability

At the time of writing, the new grid functions makeContext(), makeContent(), grid.force(), grid.revert(), and grid.reorder() are only available in the development version of R. They will be part of the R version 2.16.0 release. The ggplot2 examples depend on a fork of the gtable package with a new implementation based on makeContext() and makeContent(), which is available at github (link). A more technical document decsribing the development and testing of these changes is available from the R developer web site (link).

Paul Murrell
Department of Statistics
The University of Auckland
New Zealand
paul@stat.auckland.ac.nz

Bibliography

Hadley Wickham, ggplot2 : Elegant graphics for data analysis , 2009,

Hadley Wickham, gtable : Arrange grobs in tables. , 2012, R package version 0.1.1.99