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")
> 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")
> 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")
> 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")
> 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")
> grid.edit("tb", gp=gpar(col="grey"))
> 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")
> 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")
> grid.force()
> 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"))
> grid.edit("box", gp=gpar(fill="grey"))
> grid.reorder("tbt", "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")
> grid.revert()
> 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.
> 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")
> grid.edit("tbt", label="hello world")
Paul Murrell
Department of Statistics
The University of Auckland
New Zealand
paul@stat.auckland.ac.nz
ggplot2 : Elegant graphics for data analysis , 2009,
gtable : Arrange grobs in tables. , 2012, R package version 0.1.1.99