by Paul Murrell http://orcid.org/0000-0002-3224-8858
Version 1: Thursday 19 July 2018
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.
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).
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).
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))
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()
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)
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)
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()
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)
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)
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)
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)
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)
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.
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)
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.
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)
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).
Murrell, P. (2018). "Echoes of the Future" Technical Report 2018-07, Department of Statistics, The University of Auckland. [ bib ]
This document
by Paul
Murrell is licensed under a Creative
Commons Attribution 4.0 International License.