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.> 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")
> 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.
> 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.
> 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.
> 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.
> 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.
> library(ggplot2)
> qplot(mpg, wt, data=mtcars, colour=cyl)
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 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]
> 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")
Paul Murrell
Department of Statistics
The University of Auckland
New Zealand
paul@stat.auckland.ac.nz
2009,
ggplot2 : Elegant graphics for data analysis ,2012, R package version 0.1.1.99
gtable : Arrange grobs in tables. ,