Echoes of the Future

by Paul Murrell http://orcid.org/0000-0002-3224-8858

Version 1: Thursday 19 July 2018


Creative Commons License
This document by Paul Murrell is licensed under a Creative Commons Attribution 4.0 International License.


This report discusses ways to combine graphics output from the 'graphics' package and the 'grid' package in R and introduces a new function echoGrob in the 'gridGraphics' package.

Table of Contents:

1. The problem

A question on R-help (Ed Siefker, "drc, ggplot2, and gridExtra", 2018-05-18) asked about arranging a 'grid'-based graphical table alongside a 'graphics'-based plot. The image below shows the sort of result we are looking for. On the left is a 'graphics' scatterplot and on the right is output from the grid.table function from the 'gridExtra' package (Auguie, 2017).

plot of chunk unnamed-chunk-4

This report discusses some different solutions to this problem, including a new approach based on the echoGrob function from the 'gridGraphics' package (Murrell and Wen, 2018).

2. Combining 'graphics' and 'grid' output

The 'graphics' and 'grid' graphics systems in R (R Core Team, 2018) are completely independent from each other. When it comes to arranging multiple plots on a page, this means that 'grid'-based plots, such as those from the 'lattice' package (Sarkar, 2008) or those from the 'ggplot2' package (Wickham, 2009), do not respond to par(mfrow) from the 'graphics' package. Conversely, 'graphics'-based plots do not respond to viewports from 'grid', or higher level 'grid'-based tools such as the grid.arrange function from the 'gridExtra' package.

Although the two graphics systems ignore each other, it is still possible to draw both 'graphics' and 'grid' output together on the same page. The following code provides a simple demonstration; if we draw a 'graphics' scatterplot and a 'gridExtra' table on the same page, both types of output are drawn; the latter just completely overlaps with the former.

library(gridExtra)
mtcarsCols <- mtcars[c(1, 3)]
plot(mpg ~ disp, mtcarsCols)
grid.table(head(mtcarsCols))
plot of chunk unnamed-chunk-6

Because the two graphics systems essentially ignore each other, we can produce simple arrangements by arranging output for each graphics system separately. The following code does this by shifting the scatterplot to the left, using par(mfrow), and the table to the right, using a 'grid' viewport.

library(grid)
par(mfrow=c(1, 2))
plot(mpg ~ disp, mtcarsCols)
pushViewport(viewport(x=.5, width=.5, just="left"))
grid.table(head(mtcarsCols))
popViewport()
plot of chunk unnamed-chunk-7

The 'gridBase' package allows us to do something similar, but ensures that the 'grid' viewport is coherent with the 'graphics' figure regions.

library(gridBase)

In the code below, we set up an arrangement of two plots as before, with par(mfrow). The 'graphics' plot is drawn on the left and then we start an empty plot on the right with a call to plot.new. The call to baseViewports generates a set of 'grid' viewports that correspond to the plot, figure, and inner region for the plot on the right. We push the inner region viewport then the figure region viewport and draw the 'gridExtra' table within that.

par(mfrow=c(1, 2))
plot(mpg ~ disp, mtcarsCols)
plot.new()
basevps <- baseViewports()
pushViewport(basevps$inner, basevps$figure)
grid.table(head(mtcarsCols))
popViewport(2)
plot of chunk unnamed-chunk-8

The advantage of working with properly aligned viewports like this is that we can locate output precisely, without guesswork, and programmatically. For example, the following code places the 'gridExtra' table just inside the top-right corner of the 'graphics' plot.

In this code, we draw only one 'graphics' plot on the page, then set up 'grid' viewports corresponding to that plot. This time we also push the viewport corresponding to the plot region. This allows us to position a viewport just inside the top-right corner of the plot region. Rather than drawing the 'gridExtra' table directly, we first generate a table grob (an object representing the table). This allows us to size the viewport to precisely fit the table, so that when we draw the table it fits snugly up in the top-right corner of the plot.

The table that 'gridExtra' creates is actually a "gtable" from the 'gtable' package (Wickham, 2016) and we need to load that package to be able to query the width and height of the table.

library(gtable)
plot(mpg ~ disp, mtcarsCols)
basevps <- baseViewports()
pushViewport(basevps$inner, basevps$figure, basevps$plot)
gt <- tableGrob(head(mtcarsCols))
pushViewport(viewport(x=unit(1, "npc") - unit(1, "mm"),
                      y=unit(1, "npc") - unit(1, "mm"),
                      just=c("right", "top"),
                      width=gtable_width(gt),
                      height=gtable_height(gt)))
grid.draw(gt)
popViewport(4)
plot of chunk unnamed-chunk-9

The 'gridGraphics' package provides a completely different approach. The grid.echo function can be used to convert a 'graphics' plot into a 'grid' equivalent and then everything can be arranged using 'grid'-based tools.

In the following code, we create two viewports, one on the left and one on the right, to arrange the 'graphics' plot and the 'gridExtra' table side by side. The 'gridExtra' table is 'grid' output, so we can just navigate down to the viewport on the right and draw it. To draw the 'graphics' plot, we navigate down to the viewport on the left and call grid.echo, giving it a function that calls plot to draw the plot.

The grid.echo function opens a device off-screen that is the same size as the viewport that we are drawing into, calls the function that we gave it to draw the plot off-screen, and then draws a 'grid' version of the off-screen plot in the viewport that we want to draw into.

library(gridGraphics)
pushViewport(viewport(x=0, width=.5, just="left", name="left"))
upViewport()
pushViewport(viewport(x=.5, width=.5, just="left", name="right"))
upViewport()
downViewport("right")
grid.table(head(mtcarsCols))
upViewport()
downViewport("left")
grid.echo(function() plot(mpg ~ disp, mtcarsCols),
          newpage=FALSE)
upViewport()
plot of chunk grid.echo

The result looks the same as several previous examples, but it is different because all of the drawing was performed by 'grid'. The advantage of having everything 'grid'-based is that we can use 'grid' tools to add to or modify the combined plots. The following code provides an example.

We navigate to the viewport that corresponds to the plot region for the converted 'graphics' plot and draw a filled circle over the first data point in the plot. We also call grid.move.to to define the start of a line segment at that data point. We then navigate to the first column of the first (data) row of the table. Within that cell, we push a viewport with clipping turned off, draw a rectangle around the cell, and call grid.line.to to draw a line segment from the data point in the plot to the left edge of the 'gridExtra' table cell.

row1 <- mtcarsCols[1,]
downViewport("plot1-window-1-1")
grid.points(row1$disp, row1$mpg, pch=16)
grid.move.to(row1$disp, row1$mpg, default.units="native")
upViewport(0)
downViewport("rowhead-bg.2-1-2-1")
pushViewport(viewport(clip="off"))
grid.rect(gp=gpar(fill=NA))
grid.line.to(0, .5)
upViewport(0)
plot of chunk unnamed-chunk-11

3. The echoGrob function

One disadvantage of the grid.echo approach shown above is that using explicit viewports to arrange output can make your head hurt from too much thinking, or your fingers hurt from too much typing (or both). Fortunately, there are high-level functions that hide the complexity of viewports from us, such as grid.arrange from 'gridExtra'. A "gtable" from 'gtable' similarly hides a lot of viewport machinery.

The complication with using these higher-level tools is that they arrange 'grid' grobs; we must create a graphical object representing some output rather than just drawing the output. This means that we cannot use grid.echo because that just draws output, it does not return any grobs.

One way that we can create a grob from output is with the grid.grab function. For example, the following code draws a 'graphics' plot, generates a 'grid' version with grid.echo, and then creates a grob by calling grid.grab on the result.

plot(mpg ~ disp, mtcarsCols)
grid.echo()
pg <- grid.grab()

We can then pass that grob, pg in this case, to functions like grid.arrange. The following code arranges this grob alongside the table grob that we created previously.

grid.arrange(pg, gt, ncol=2)
plot of chunk grid.arrange

But there is a problem - the y-axis label is missing from the plot. This happens because the plot that we echoed and then grabbed was drawn within a full page, so the dimensions of the plot are only correct for drawing on a full page (not half a page like we have done).

The following code creates a new grob, this time making sure to draw, echo, and grab the plot within a device that is only half the width of a normal device.

pdf(NULL, width=3.5, height=7)
dev.control("enable")
plot(mpg ~ disp, mtcarsCols)
grid.echo()
pg <- grid.grab()
dev.off()
grid.arrange(pg, gt, ncol=2)
plot of chunk unnamed-chunk-14

The result is now correct (the y-axis label is back). However, we still have a problem. What we have done is create a grob based on a graphics device that is sized to be exactly as big as the region that the grob is going to be drawn within. But the reason we are using grid.arrange is so that we do not have to figure out for ourselves how big each region in our arrangement is going to be. For more complex arrangements, we definitely do not want to have to calculate region sizes ourselves.

What we need is a function that will do the echo and grab only when it is time to draw (when grid.arrange has already figured out the region sizes). This is the purpose of the new echoGrob function in the 'gridGraphics' package, which allows us to convert 'graphics' output that will be drawn in the future.

The echoGrob function creates a grob with class "echogrob" that records a function, but does not evaluate the function (echoGrob also accepts a "recordedPlot"; see Murrell, 2015 and Murrell et al., 2015). This is an example of a 'grid' grob that calculates what it will draw only when it comes time to draw the grob. There is a makeContent method (Murrell, 2013) for "echogrob"s that is run at drawing time and this determines the size of the viewport that drawing is occurring within, opens an off-screen device of that size, calls grid.echo on that device (which opens another off-screen device, calls the function that was stored in the "echogrob" grob, then draws a 'grid' version on the first off-screen device), then grabs the 'grid' output and returns the resulting grob.

The following code shows how we might use echoGrob to draw a 'graphics' plot (with the correct proportions and the y-axis visible) next to a 'gridExtra' table, using grid.arrange to do the arranging.

pg <- echoGrob(function() plot(mpg ~ disp, mtcarsCols))
grid.arrange(pg, gt, ncol=2)
plot of chunk unnamed-chunk-15

The following code demonstrates that this echoGrob approach automatically adapts to the region that it is asked to draw within. We create an arrangement that still only has two regions, but we make the region on the right just wide enough to fit the table and allow the region on the left to fill up the remaining width. This is a simple example of how we can benefit from leaving the calculation of regions up to grid.arrange.

widths <- unit.c(unit(1, "null"), gtable_width(gt))
grid.arrange(pg, gt, widths=widths)
plot of chunk adapt

The plot on the left is wider than before because the table on the right is only taking up as much room as it needs.

4. Editing echoGrob output

A disadvantage of using tools like echoGrob (and "gtable"s and grid.arrange), that have a makeContent method to determine what to draw at drawing time, is that they do not record what they draw on the 'grid' display list. The output from grid.ls below shows that, after the drawing that we did above, there is a single collective grob on the 'grid' display list, which represents the plot and table that we have drawn.

grid.ls()
  arrange

This means that we cannot modify the output of these tools. However, we can gain access to everything that has been drawn by calling the grid.force function. The output from grid.ls below shows that, after calling grid.force, we have access to all of the individual grobs representing the individual lines, points, and text that make up the plot and table that we have drawn.

grid.force()
grid.ls()
  arrange
    arrange.1-1-1-1
      arrange.1-1-1-1
        plot1-plot-1-points-1
        plot1-plot-1-bottom-axis-line-1
        plot1-plot-1-bottom-axis-ticks-1
        plot1-plot-1-bottom-axis-labels-1
        plot1-plot-1-left-axis-line-1
        plot1-plot-1-left-axis-ticks-1
        plot1-plot-1-left-axis-labels-1
        plot1-plot-1-box-1
        plot1-plot-1-xlab-1
        plot1-plot-1-ylab-1
    arrange.1-2-1-2
      rowhead-bg.1-1-1-1
      rowhead-bg.2-1-2-1
      rowhead-bg.3-1-3-1
      rowhead-bg.4-1-4-1
      rowhead-bg.5-1-5-1
      rowhead-bg.6-1-6-1
      rowhead-bg.7-1-7-1
      colhead-bg.1-2-1-2
      colhead-bg.1-3-1-3
      core-bg.2-2-2-2
      core-bg.3-2-3-2
      core-bg.4-2-4-2
      core-bg.5-2-5-2
      core-bg.6-2-6-2
      core-bg.7-2-7-2
      core-bg.2-3-2-3
      core-bg.3-3-3-3
      core-bg.4-3-4-3
      core-bg.5-3-5-3
      core-bg.6-3-6-3
      core-bg.7-3-7-3
      rowhead-fg.1-1-1-1
      colhead-fg.1-2-1-2
      core-fg.2-2-2-2
      rowhead-fg.2-1-2-1
      colhead-fg.1-3-1-3
      core-fg.3-2-3-2
      rowhead-fg.3-1-3-1
      core-fg.4-2-4-2
      rowhead-fg.4-1-4-1
      core-fg.5-2-5-2
      rowhead-fg.5-1-5-1
      core-fg.6-2-6-2
      rowhead-fg.6-1-6-1
      core-fg.7-2-7-2
      rowhead-fg.7-1-7-1
      core-fg.2-3-2-3
      core-fg.3-3-3-3
      core-fg.4-3-4-3
      core-fg.5-3-5-3
      core-fg.6-3-6-3
      core-fg.7-3-7-3

With these low-level grobs (and viewports) available, we can use the same code that we used in a previous example to draw a line from a data point on the plot to a cell in the table.

row1 <- mtcarsCols[1,]
downViewport("plot1-window-1-1")
grid.points(row1$disp, row1$mpg, pch=16)
grid.move.to(row1$disp, row1$mpg, default.units="native")
upViewport(0)
downViewport("rowhead-bg.2-1-2-1")
pushViewport(viewport(clip="off"))
grid.rect(gp=gpar(fill=NA))
grid.line.to(0, .5)
upViewport(0)
plot of chunk unnamed-chunk-20

5. Summary

Output from the 'graphics' and 'grid' packages can co-exist on a page, but the difficulty is in coordinating them. The 'gridBase' package helps with creating 'grid' viewports that correspond to 'graphics' figure and plot regions. The 'gridGraphics' package converts 'graphics' output to 'grid' output so that we can arrange 'graphics' output using 'grid' viewports.

The 'gridExtra' package provides convenient tools so that we do not have to use 'grid' viewports directly to arrange output. The new function echoGrob in 'gridGraphics' allows us to use 'gridExtra' to arrange 'graphics' output as well as 'grid' output.

6. Addendum

An upcoming release of the 'cowplot' package (Wilke, 2018) will provide convenient tools so that we do not even have to use 'gridGraphics' directly. For example, the following code combines a 'graphics' plot function with the 'gridExtra' table that we created earlier.

library(cowplot)
plot_grid(function() plot(mpg ~ disp, mtcarsCols), gt, ncol=2)
plot of chunk unnamed-chunk-22

7. Technical requirements

The examples and discussion in this document relate to version 0.4-0 of the 'gridGraphics' package, which in turn relies on the development version of R (revision r74931), which will become R version 3.6.0. The 'cowplot' example requires at least version 0.9.99, which at the time of writing was only available on github.

This report was generated within a Docker container (see Resources section below).

8. Resources

How to cite this document

Murrell, P. (2018). "Echoes of the Future" Technical Report 2018-07, Department of Statistics, The University of Auckland. [ bib ]

9. References

[Auguie, 2017]
Auguie, B. (2017). gridExtra: Miscellaneous Functions for "Grid" Graphics. R package version 2.3. [ bib | http ]
[Murrell, 2013]
Murrell, P. (2013). Changes to grid for R 3.0.0. The R Journal, 5(2):148--160. [ bib | .html ]
[Murrell, 2014]
Murrell, P. (2014). gridBase: Integration of base and grid graphics. R package version 0.4-7. [ bib | http ]
[Murrell, 2015]
Murrell, P. (2015). The gridGraphics Package. The R Journal, 7(1):151--162. [ bib | .html ]
[Murrell et al., 2015]
Murrell, P., Ooms, J., and Allaire, J. J. (2015). Recording and replaying the graphics engine display list. Technical Report 2015-07, Department of Statistics, The University of Auckland. [ bib ]
[Murrell and Wen, 2018]
Murrell, P. and Wen, Z. (2018). gridGraphics: Redraw Base Graphics Using 'grid' Graphics. R package version 0.3-1. [ bib | http ]
[R Core Team, 2018]
R Core Team (2018). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. [ bib | http ]
[Sarkar, 2008]
Sarkar, D. (2008). Lattice: Multivariate Data Visualization with R. Springer, New York. ISBN 978-0-387-75968-5. [ bib | http ]
[Wickham, 2009]
Wickham, H. (2009). ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York. [ bib | http ]
[Wickham, 2016]
Wickham, H. (2016). gtable: Arrange 'Grobs' in Tables. R package version 0.2.0. [ bib | http ]
[Wilke, 2018]
Wilke, C. O. (2018). cowplot: Streamlined Plot Theme and Plot Annotations for 'ggplot2'. R package version 0.9.3. [ bib | http ]

Creative Commons License
This document by Paul Murrell is licensed under a Creative Commons Attribution 4.0 International License.