Abstract
Statistical plots drawn with the ggplot2 package generate numerous grid grobs and viewports which are labelled and organised into a coherent hierarchy. This report describes an example that shows how to manipulate the grobs and viewports in a ggplot2 plot using tools from the grid package, export the result to SVG using gridSVG, then manipulate the result further using XML tools, to produce an interactive ggplot2 graphic for the web.
The grid package for R provides low-level tools for drawing graphical scenes. Higher-level packages, such as lattice and ggplot2 make use of grid to draw sophisticated statistical plots.
A sophisticated statistical plot contains many low-level components, but grid provides tools that allow the low-level components to be organised so that there is a sensible structure to the components of a plot. The grid package also provides functions to access and modify the components of a scene, which allows for very low level customisation of a plot.
The gridSVG package exports a grid scene to SVG while retaining the organisation of the grid grobs and viewports in the scene. This allows the powerful set of XML tools to be brought to bear on a grid scene, which creates even greater possibilities for customising the original scene.
This report describes an example of a complex customisation of a grid scene by exporting to SVG and then post-processing using a combination of grid and XML tools. Figure 1, “An interactive ggplot2 plot” shows the image that we are aiming for. This consists of two ggplot2 plots, one (larger one) above the other. The bottom plot shows a complete time series and the top plot shows a zoomed view of a subset, or "window", of the time series. The bottom plot has a semitransparent rectangle, or "thumb", that shows which subset of the time series is currently visible in the top plot. The image is interactive because the thumb in the bottom plot can be dragged with the mouse to change the subset that is visible in the top plot.
Figure 1. An interactive ggplot2 plot. The bottom plot shows a complete time series. The top plot shows a limited window from the time series. The blue "thumb" on the bottom plot represents the window that is currently being viewed; this thumb may be dragged with the mouse to change the window.
The remainder of this introduction gives a brief outline of how to impose a structure and organisation onto a grid scene, how to modify the scene using grid , how to export the scene to SVG using gridSVG, and how to make further modifications using XML tools. This is followed by an in-depth description of how the plot in Figure 1, “An interactive ggplot2 plot” was generated.
The basic building blocks of a grid scene are graphical objects (grobs), which describe shapes to draw, and viewports, which define subregions of the page in which to draw. These building blocks can be arranged in hierarchies to create trees of graphical objects (gTrees) and trees of viewports (vpTrees). For example, an axis on a plot could be a single parent gTree that contains several child grobs to represent the axis major line, the tick marks, and the tick labels for the axis (see Figure 2, “A grob hierarchy”).
Figure 2. A grid scene consisting of a simple axis and a graph showing how the axis could be represented by a hierarchy of graphical objects: a single parent gTree with several child grobs.
As an example of a hierarchy of viewports, consider a Trellis-like arrangement of multiple plots on a page. An overall parent viewport could be used to define the region on the page that will contain the set of plots and then several child viewports can be used to define the different subregions for the individual plots within the parent viewport (see Figure 3, “A viewport hierarchy”).
Figure 3. A grid scene consisting of multiple viewports that define regions for drawing a Trellis-like plot: a single parent viewport to hold the collection of plots with several children viewports inside.
Taken together, the vpTrees and gTrees that make up a scene can be used to represent the scene with a complex, but rational and predictable hierarchical structure. Figure 4, “A grid scene hierarchy” shows the full tree of grobs and viewports that were created to produce the diagram in Figure 3, “A viewport hierarchy”; there is a parent viewport with a bounding rectangle and text label drawn within it, then four child viewports, each with a rectangle and text label drawn inside. The gridDebug package provides functions for drawing diagrams like the ones in Figure 3, “A viewport hierarchy”, Figure 2, “A grob hierarchy”, and Figure 4, “A grid scene hierarchy”.
Figure 4. A diagram of the combined hierarchy of vpTrees and gTrees within a grid scene that consists of a parent viewport and four child viewports, with a rectangle and text grob drawn within all of the viewports.
The grid package maintains a record of the grob and viewport hierarchies in the current scene and provides functions to access and modify components of the scene.
For example, the
grid.ls()
function can be used to display a list of the grobs
in a scene; the code below shows what we would get
after drawing the scene in Figure 2, “A grob hierarchy”,
which consists of just a parent axis grob
with major line, tick mark, and tick label child grobs.
grid.ls(fullNames=TRUE)
xaxis[axis] lines[major] segments[ticks] text[labels]
We can use the function
grid.edit()
to modify grobs and the function
grid.remove()
to remove grobs from the scene.
The following code changes the tick label text to grey
and removes the axis major line (see Figure 5, “A modified grid scene”).
There is also the
grid.get()
function to access a grob and
grid.set()
to replace a grob.
grid.edit("labels", gp=gpar(col="grey")) grid.remove("major")
Figure 5. The simple scene from Figure 2, “A grob hierarchy” with the tick labels changed to grey and the axis major line removed.
It is also possible to access (but not modify) the viewports
in a
grid
scene. To demonstrate this, the following code draws a
lattice
plot, which creates a number of
grid
viewports in order to draw the plot.
The
downViewport()
function is used to revisit one of the viewports that
lattice
generated
and a semitransparent green overlay is drawn to show that
we are drawing within one of the viewports that was
used to draw the original plot
(see Figure 6, “Revisiting a grid viewport”).
library(lattice) barchart(yield ~ variety | site, data = barley, groups = year, layout = c(1,6), stack = TRUE, auto.key = list(space = "right"), ylab = "Barley Yield (bushels/acre)", scales = list(x = list(rot = 45))) downViewport("plot_01.panel.1.3.vp") grid.rect(gp=gpar(col=NA, fill=rgb(0,1,0,.5)))
Figure 6. A lattice plot with a semitransparent green rectangle drawn within one of the original grid viewports that was used to draw the original plot.
The
gridSVG
package provides functions to translate a
grid
scene to an SVG format. This differs from the SVG format that
is generated by the core
svg()
function because the SVG generated by
gridSVG
retains the hierarchies of viewports and grobs from the
grid
scene.
This is demonstrated in Figure 7, “A gridSVG scene hierarchy”,
which shows a fragment of the SVG code that is generated
by
gridSVG
from the simple scene in
Figure 2, “A grob hierarchy”. The grob hierarchy of a single
parent axis that contains separate children for the axis major
line and the axis ticks is reflected in the nesting of elements in
the SVG code.
Figure 7. A fragment of the SVG code that is generated by the gridSVG package from the grid scene in Figure 2, “A grob hierarchy”. The grob hierarchy from the grid scene is reflected in the nesting of elements in the SVG code produced by gridSVG.
<g id="axis"> <g id="major"> <polyline fill="none" id="major.1" points="54,108 162,108"/> </g> <g id="ticks"> <polyline fill="none" id="ticks.1" points="54,108 54,100.8"/> <polyline fill="none" id="ticks.2" points="81,108 81,100.8"/> <polyline fill="none" id="ticks.3" points="108,108 108,100.8"/> <polyline fill="none" id="ticks.4" points="135,108 135,100.8"/> <polyline fill="none" id="ticks.5" points="162,108 162,100.8"/> </g> </g>
SVG is a dialect of XML, the eXtensible Markup Language; an SVG document is a special case of an XML document. There are a number of powerful tools for working with XML documents, including XPath for specifying subsets of an XML document and XSLT for transforming XML documents to other formats.
With a
grid
scene that has been exported to SVG via
gridSVG,
we have an SVG document with a sensible structure
and powerful
XML tools to access and manipulate the scene at least as much
as we are able to do within R itself using
grid
functions. The code below shows some example
expressions for selecting subsets of the SVG elements
in Figure 7, “A gridSVG scene hierarchy”.
The first example selects the SVG <g> element
with an id
value of "major".
[1] "//svg:g[@id = 'major']"
<g id="major"> <polyline fill="none" id="major.1" points="126,252 378,252"/> </g>
The next example shows how to select just the second
<polyline>
element from within the
<g>
element with an id
value of "ticks".
[1] "//svg:g[@id = 'ticks']/svg:polyline[2]"
<polyline fill="none" id="ticks.2" points="189,252 189,244.8"/>
The final example shows how to extract all <polyline> elements, no matter which <g> element they reside within.
[1] "//svg:polyline"
[[1]] <polyline fill="none" id="major.1" points="126,252 378,252"/> [[2]] <polyline fill="none" id="ticks.1" points="126,252 126,244.8"/> [[3]] <polyline fill="none" id="ticks.2" points="189,252 189,244.8"/> [[4]] <polyline fill="none" id="ticks.3" points="252,252 252,244.8"/> [[5]] <polyline fill="none" id="ticks.4" points="315,252 315,244.8"/> [[6]] <polyline fill="none" id="ticks.5" points="378,252 378,244.8"/>
The remainder of this report describes how Figure 1, “An interactive ggplot2 plot” was generated by taking advantage of the ability to manipulate a grid scene that was originally drawn in R to end up with a dynamic and interactive plot in an SVG format for viewing on the web.
The first step towards Figure 1, “An interactive ggplot2 plot”
requires generating the
ggplot2
plots as standard static plots in R.
The time series shown in these plots is based on the
Nile
data set that comes with R.
df <- data.frame(time=as.numeric(time(Nile)), flow=as.numeric(Nile))
The top plot is a
ggplot2
line plot of the
Nile
data, with time on the x-axis and volume of water flow on
the y-axis.
library(ggplot2)
thePlot <- ggplot(df, aes(x=time, y=flow)) + geom_line()
The bottom plot is essentially the same plot, just with the y-axis labelling adjusted to avoid having too many labels, and the axis labels "turned off". The x-axis label is left out altogether, but the y-axis label is drawn white (to match the background) so that the width of the plot region in the bottom plot is exactly the same as the width of the plot region in the top plot.
botPlot <- thePlot + scale_y_continuous(breaks=c(600, 1200)) + theme(plot.background=element_rect(colour="transparent"), axis.title.y=element_text(colour="white", size=6), axis.title.x=element_blank())
The two plots are drawn together on the same page by creating two grid viewports, one large one that consumes most of the top of the page and a smaller viewport at the bottom, and then drawing each plot in a separate viewport. This operation is captured within a function (below) because we will need to draw this scene more than once.
library(grid)
doplot <- function() { pushViewport(viewport(y=1, height=.8, just="top", name="topvp")) print(thePlot, newpage=FALSE) upViewport() pushViewport(viewport(y=0, height=.2, just="bottom", name="bottomvp")) print(botPlot, newpage=FALSE) upViewport() }
Figure 8, “A static ggplot2 plot” shows the static image that is
produced by doplot()
.
Figure 8. A static image composed of two ggplot2 plots. Both plots show the same complete series, just with different aspect ratios (and different axis labelling).
The final interactive plot in Figure 1, “An interactive ggplot2 plot” is based on a combination of two static scenes, one "normal" width version and one "wide" version. This section describes the construction of those two versions of the scene.
The normal scene has a known width and the wider scene
has a width scale
times as wide.
sceneWidth <- 7 scale <- 4
The content of the normal scene is produced using the
doplot()
function shown above.
doplot()
We now want to calculate some of the physical dimensions of this scene so that we can match the content of the normal scene and the wider scene. In particular, we want to determine how wide the margins are around the top plot in this normal scene (so that we can generate exactly the same margins around the wider scene). To do this calculation, we want to revisit the viewport for the top plot region in the normal scene and determine its width. In recent versions of ggplot2, the grobs and viewports for this scene are not visible by default, so we need to "force" them to gain access (see the section called “Version information” for more information on software versions relevant to this report).
grid.force()
We can now visit the top plot region, calculate its width,
and then calculate the margin widths (based on the known
overall width). The name of the viewport we need can
be obtained by inspection of
grid.ls()
output or using
showViewport()
(see Figure 9, “A viewport diagram”).
This is an example of the usefulness of being able to explore the grid drawing context (viewports) that are generated by ggplot2.
downViewport("panel.3-4-3-4") plotRegionWidth <- convertWidth(unit(1, "npc"), "inches", valueOnly=TRUE) marginWidth <- sceneWidth - plotRegionWidth upViewport(0)
Figure 9. A diagram of some of the viewports involved in drawing
the scene in Figure 8, “A static ggplot2 plot”,
as produced by showViewport()
.
The next step is to add a "thumb" to the bottom of the two
plots in the normal scene.
The steps involved are: navigate to the plot region viewport
in the bottom plot;
create a semitransparent blue rectangle (the width is based
on the scale
we are using);
add an onmousedown
event handler to the
rectangle so that we can do something when the user clicks
on this rectangle; and add the rectangle to the scene
(see Figure 10, “Adding the thumb”).
For now, this thumb is just a static rectangle,
but in the interactive version, the
user will be able to click on this thumb and slide it to change
the time series window that is visible in the top scene.
This is an example of the uesfulness of being able to manipulate and modify the grobs within a grid scene.
downViewport("bottomvp::layout::panel.3-4-3-4") rg <- rectGrob(x=0, width=1/scale, just="left", gp=gpar(col=rgb(0,0,1,.5), fill=rgb(0,0,1,.2)), name="thumb") thumb <- garnishGrob(rg, onmousedown="thumbDown(evt)") grid.draw(thumb) upViewport(0)
Figure 10. The scene in Figure 8, “A static ggplot2 plot” with a (static) rectangular "thumb" added to the bottom plot.
A final step for now is to add javascript code to the scene.
the section called “Javascript event handlers” will discuss the javascript code itself;
for now we are just making sure that the SVG file that we produce
will contain the javascript code.
We can now generate an SVG version of the normal scene
using gridToSVG()
.
grid.script(filename="slider.js") gridToSVG("normalplot.svg")
The wider scene is very straightforward by comparison.
It is just the output from
doplot()
drawn on a wider page (then exported to SVG; see
Figure 11, “A wide scene”).
pdf("wideplot.pdf", width=plotRegionWidth*scale + marginWidth) doplot() gridToSVG("wideplot.svg") dev.off()
The two static scenes that we have generated so far will provide the raw materials for constructing an interactive scene. The interactive scene will be constructed by extracting pieces from the static scenes and combining them together.
The first step is just to read the two static SVG files into R.
normalSVG <- xmlParse("normalplot.svg") wideSVG <- xmlParse("wideplot.svg")
One major piece of reconstruction involves replacing the top plot contents
in the normal scene with the top plot contents from the wide scene.
The following code identifies the top plot within the normal scene
by specifying the
<g>
element with the relevant
id
attribute. This
id
value corresponds to the
name
of the grob used in the drawing of the original scene.
normalPlotSVG <- getNodeSet(normalSVG, "//svg:g[@id='topvp::layout::panel.3-4-3-4.1']", c(svg="http://www.w3.org/2000/svg"))[[1]]
The top plot in the wide scene is accessed with similar code.
The main difference is that we identify the
<g>
element that is the child of the
<g>
element with the relevant
id
.
What we are doing is identifying a parent
element in the normal scene for which we can provide a new
child element from the wide scene.
widePlotSVG <- getNodeSet(wideSVG, "//svg:g[@id='topvp::layout::panel.3-4-3-4.1']/svg:g[@id='panel.3-4-3-4']", c(svg="http://www.w3.org/2000/svg"))[[1]]
Now we remove the
<g>
element child of the top plot in the normal scene and
add a new child, which is the top plot from the wide scene.
This is an example of the power of XML tools for manipulating
SVG content. It also relies on the fact the the SVG content
has retained the coherent structure from the original
grid
scene.
removeChildren(normalPlotSVG, "g") addChildren(normalPlotSVG, widePlotSVG)
A very similar series of steps is followed to replace the original x-axis in the top plot of the normal scene with a wider x-axis from the top plot of the wide scene.
normalAxisSVG <- getNodeSet(normalSVG, "//svg:g[contains(@id, 'topvp::layout::axis-b.4-4-4-4')]", c(svg="http://www.w3.org/2000/svg"))[[1]] wideAxisSVG <- getNodeSet(wideSVG, "//svg:g[contains(@id, 'topvp::layout::axis-b.4-4-4-4')]/child::*", c(svg="http://www.w3.org/2000/svg"))[[1]] removeChildren(normalAxisSVG, "g") addChildren(normalAxisSVG, wideAxisSVG)
At this point, the new scene consists of the bottom plot, the y-axis and x-axis label from the top plot of the normal scene, plus the contents and x-axis from the top plot of the wide scene, which extend far to the right of the scene (see Figure 12, “A mixture of scenes”).
Figure 12. A combination of the bottom plot and y-axis from the normal scene and the top plot content and x-axis from the wide scene.
The final step is to add some more event handling to the scene so that movement of the mouse (with the mouse button over the thumb and the mouse button down) will result in movement of the thumb and changes to the time series window that is shown in the top plot.
We do this by setting
onmouseup
and
onmousemove
attributes on the root
<svg>
element of the scene.
The final step is to save the interactive scene to a file and
this produces the final plot shown in Figure 1, “An interactive ggplot2 plot”
(reproduced in Figure 13, “An interactive ggplot2 plot” for convenience).
addAttributes(getNodeSet(normalSVG, "/svg:svg", c(svg="http://www.w3.org/2000/svg"))[[1]], onmouseup="mUp(evt)", onmousemove=paste("mMove(evt, ", scale, ")", sep="")) saveXML(normalSVG, file="sliderplot.svg")
Figure 13. An interactive ggplot2 plot. This is a copy of Figure 1, “An interactive ggplot2 plot” produced here for convenience.
The scene in Figure 13, “An interactive ggplot2 plot” is not quite exactly the same as the one in Figure 1, “An interactive ggplot2 plot” because the axis ticks are not clipped to the width of the top plot. This clipping can be set up using the same ideas of extracting information from the static scenes and using it to modify the interactive scene (see the section called “Version information” for a link to the complete R code).
The R code described so far has set up the static components of the final scene and the starting positions for the dynamic components of the final scene. For the scene to change in response to user input, we rely on javascript code.
This code consists of three main parts: what to do when the user clicks the mouse button down (within the thumb); what to do when the user lifts the mouse button up again; and what to do when the user moves the mouse (with the mouse button down).
The first two parts are simple: we just keep a global variable indicating whether the mouse is down (within the thumb) or not plus a record of the location where the mouse was first clicked down.
function thumbDown(evt) { movingThumb = true; startx = getMouseX(evt); } function mUp(evt) { if (movingThumb) { movingThumb = false; xoffset = savedoffset; } }
The hard work is done when the mouse moves, but this is still not particularly complex. The basic idea is to determine how far the mouse has moved, calculate a transformation to apply, and then apply that transformation to the thumb. Something similar must be done for the top plot contents to change the time series window that is visible, with the difference being just one of scale and direction (when the thumb slides to the right, the contents of the top plot slide, faster, to the left).
function mMove(evt, scale) { if (movingThumb) { var newX = getMouseX(evt); var target = document.getElementById("thumb"); var xshift = newX - startx; var thumbx = xoffset + xshift; if (moveOK(thumbx)) { /* Move the thumb */ var transMatrix = [1,0,0,1,0,0]; transMatrix[4] = thumbx; transform = "matrix(" + transMatrix.join(' ') + ")"; target.setAttributeNS(null, "transform", transform); /* Move the top plot (the gTree NOT the viewport) */ var plot = document.getElementById("panel.3-4-3-4"); transMatrix = [1,0,0,1,0,0]; transMatrix[4] = -1*scale*thumbx; transform = "matrix(" + transMatrix.join(' ') + ")"; plot.setAttributeNS(null, "transform", transform); /* Move the top axis (the gTree NOT the viewport) */ var axis = document.getElementById("axis-b.4-4-4-4"); axis.setAttributeNS(null, "transform", transform); savedoffset = thumbx; } } }
There are some other details involved, such as code to ensure that the thumb cannot slide off the edges of the bottom plot, but the three functions above constitute the main functionality (see the section called “Version information” for a link to the full javascript code).
This report describes the development of an interactive plot for viewing in a web browser that is constructed by post-processing a scene composed from standard static ggplot2 plots. The construction of the plot depends heavily on several important features of several R packages:
The interactive scene described in this report demonstrates that these tools can be combined to perform quite sophisticated post-processing on a grid scene to produce interesting and useful interactive graphics for the web.
A complete copy of the R code that produces Figure 1, “An interactive ggplot2 plot” is available here. The javascript code is here.
This code is known to run in R 2.16.0 using a fork of gtable and the R2.16 branch of the gridSVG package on R-Forge.
It is anticipated that the code will work with the main version of gridSVG from around April 2013 (following the public release of R version 2.16.0). At the time of writing, it was unclear whether the gtable fork would be merged into any official release.
dynDoc("ggplotSlider.xml", "HTML", force = TRUE, xslParams = c(html.stylesheet = "http://stattech.wordpress.fos.auckland.ac.nz/wp-content/themes/twentyeleven/style.css customStyle.css", base.dir = "HTML", generate.toc = "article toc")) Tue Dec 11 09:33:54 2012