Simon Potter simon.potter@auckland.ac.nz
and Paul Murrell p.murrell@auckland.ac.nz
Department of Statistics, University of Auckland
October 10, 2012
Abstract: The gridSVG package exports grid images to an SVG format for viewing on the web. This article describes new features in gridSVG that allow grid coordinate system information to be exported along with the image. This allows the SVG image to be modified dynamically in a web browser, with full knowledge of coordinate system information, such as the scales on plot axes. As a consequence, it is now possible to create more complex and sophisticated dynamic and interactive R graphics for the web.
grid is an alternative graphics system to the traditional base graphics system provided by R [1]. Two key features of grid distinguish it from the base graphics system, graphics objects (grobs) and viewports.
Viewports are how grid defines a drawing context and plotting region. All drawing occurs relative to the coordinate system within a viewport. Viewports have a location and dimension and set scales on the horizontal and vertical axes. Crucially, they also have a name so we know how to refer to them.
Graphics objects (grobs) store information necessary to
describe how a particular object is to be drawn. For example,
a grid circleGrob
contains the information
used to describe a circle, in particular its location and its
radius. As with viewports, graphics objects also have names.
The following code provides a simple demonstration of these
grid facilities.
A viewport is pushed in the centre of the page with
specific x- and y-scales, then axes are drawn
on the bottom and left edges of this viewport,
and a set of data symbols are drawn within the viewport,
relative to the scales. The viewport is given the name
panelvp
and the data symbols are given the
name datapoints
.
R> library(grid) R> R> x <- runif(10, 5, 15) R> y <- runif(10, 5, 15)
R> panelvp <- viewport(width=0.8, height=0.8, R+ xscale = c(0, 20), R+ yscale = c(0, 20), R+ name = "panelvp") R> pushViewport(panelvp) R> grid.xaxis() R> grid.yaxis() R> grid.points(x, y, pch = 16, name = "datapoints") R> upViewport()
One advantage of having named viewports is that it is possible to revisit the viewport to add more output later. This works because revisiting a viewport means not only revisiting that region on the page, but also revisiting the coordinate system imposed on that region by the viewport scales. For example, the following code adds an extra (red) data point to the original plot (relative to the original scales).
R> downViewport("panelvp") R> grid.points(3, 14, pch=16, R> size=unit(4, "mm"), R> gp=gpar(col="red"))
The task that gridSVG [2] performs is to translate viewports and graphics objects into SVG [3] equivalents. In particular, the exported SVG image retains the naming information on viewports and graphics objects. The advantage of this is we can still refer to the same information in grid and in SVG. In addition, we are able to annotate grid grobs to take advantage of SVG features such as hyperlinking and animation.
When exporting grid graphics as SVG, instead of positioning within a viewport, all drawing occurs within a single pixel-based coordinate system. This document describes how gridSVG now exports additional information so that the original grid coordinate systems are still available within the SVG document.
To demonstrate this, we will show how to add a new data symbol to a plot that has been drawn with grid and then exported with gridSVG. This is similar to the task performed in the previous section, but the difference is that we will add points to the plot without using grid itself, or even without using R at all. We will just make use of the information that has been exported by gridSVG and stored with the plot itself.
Firstly, consider the following code which draws a simple plot
using grid, exactly as before, but this time
exports the plot to
to SVG in a file
called "pointsPlot.svg"
using gridSVG.
The file can then be
viewed in a web browser.
R> library(gridSVG)
R> panelvp <- viewport(width=0.8, height=0.8, R> xscale = c(0, 20), R> yscale = c(0, 20), R> name = "panelvp") R> pushViewport(panelvp) R> grid.xaxis() R> grid.yaxis() R> grid.points(x, y, pch = 16, name = "datapoints") R> upViewport()
R> gridToSVG("pointsPlot.svg")
We will now consider the task of modifying this exported plot so that we can add extra information, such as a new data point. An important point is that we want to add a new data point relative to the axis coordinate systems on the plot and an important difference is that we are going to do this using only the information that was exported by gridSVG, with no further help from grid.
When the SVG file was
exported, all of the locations on the plot were transformed
into pixels. This means that in our SVG file, none
of the axis scales exist, and the locations of points are no
longer native coordinates, but absolutely positioned
pixels. See the x
and y
attributes
in the <use>
elements below as a
demonstration of this process.
<g id="datapoints"> <defs> <symbol id="gridSVG.pch16" viewBox="-5 -5 10 10" overflow="visible"> <circle cx="0" cy="0" r="3.75"/> </symbol> </defs> <use id="datapoints.1" xlink:href="#gridSVG.pch16" x="282.57" y="176.28" width="8.62" height="8.62" stroke-width="1.16px" fill="rgb(0,0,0)" stroke="none" stroke-opacity="0" fill-opacity="1" transform="translate(-4.31,-4.31)"/> <use id="datapoints.2" xlink:href="#gridSVG.pch16" x="336.56" y="159.93" width="8.62" height="8.62" stroke-width="1.16px" fill="rgb(0,0,0)" stroke="none" stroke-opacity="0" fill-opacity="1" transform="translate(-4.31,-4.31)"/> <use id="datapoints.3" xlink:href="#gridSVG.pch16" x="341" y="331.39" width="8.62" height="8.62" stroke-width="1.16px" fill="rgb(0,0,0)" stroke="none" stroke-opacity="0" fill-opacity="1" transform="translate(-4.31,-4.31)"/> <use id="datapoints.4" xlink:href="#gridSVG.pch16" x="227.71" y="201.93" width="8.62" height="8.62" stroke-width="1.16px" fill="rgb(0,0,0)" stroke="none" stroke-opacity="0" fill-opacity="1" transform="translate(-4.31,-4.31)"/> <use id="datapoints.5" xlink:href="#gridSVG.pch16" x="270.14" y="264.23" width="8.62" height="8.62" stroke-width="1.16px" fill="rgb(0,0,0)" stroke="none" stroke-opacity="0" fill-opacity="1" transform="translate(-4.31,-4.31)"/> <use id="datapoints.6" xlink:href="#gridSVG.pch16" x="199.04" y="259.99" width="8.62" height="8.62" stroke-width="1.16px" fill="rgb(0,0,0)" stroke="none" stroke-opacity="0" fill-opacity="1" transform="translate(-4.31,-4.31)"/> <use id="datapoints.7" xlink:href="#gridSVG.pch16" x="207.43" y="177.39" width="8.62" height="8.62" stroke-width="1.16px" fill="rgb(0,0,0)" stroke="none" stroke-opacity="0" fill-opacity="1" transform="translate(-4.31,-4.31)"/> <use id="datapoints.8" xlink:href="#gridSVG.pch16" x="179.95" y="308.35" width="8.62" height="8.62" stroke-width="1.16px" fill="rgb(0,0,0)" stroke="none" stroke-opacity="0" fill-opacity="1" transform="translate(-4.31,-4.31)"/> <use id="datapoints.9" xlink:href="#gridSVG.pch16" x="292.92" y="241.21" width="8.62" height="8.62" stroke-width="1.16px" fill="rgb(0,0,0)" stroke="none" stroke-opacity="0" fill-opacity="1" transform="translate(-4.31,-4.31)"/> <use id="datapoints.10" xlink:href="#gridSVG.pch16" x="265.2" y="222.79" width="8.62" height="8.62" stroke-width="1.16px" fill="rgb(0,0,0)" stroke="none" stroke-opacity="0" fill-opacity="1" transform="translate(-4.31,-4.31)"/> </g>
Prior to version 1.0 of gridSVG, this task was awkward because it was difficult to determine the correct location of a new point in terms of pixels. Recent changes in gridSVG have enabled us to keep viewport information by exporting viewport metadata in the form of JSON [4], a structured data format. This enables us to be able to retain viewport locations and scales so that we can now transform pixel locations to native coordinates, and vice versa.
The following fragment shows the coordinates file that is
exported by gridSVG. It is exported in the form of
a JavaScript statement that assigns an object literal
to a variable, gridSVGCoords
.
var gridSVGCoords = { "ROOT": { "x": 0, "y": 0, "width": 504, "height": 504, "xscale": [ 0, 504 ], "yscale": [ 0, 504 ], "inch": 72 }, "panelvp.1": { "x": 50.4, "y": 50.4, "width": 403.2, "height": 403.2, "xscale": [ 0, 20 ], "yscale": [ 0, 20 ], "inch": 72 } };
This shows all of the information available to gridSVG. This JavaScript object contains a list of viewport names, with each viewport name associated with its metadata. This metadata includes the viewport location and dimensions in terms of SVG pixels. Also included are the axis scales, along with the resolution that the viewport was exported at. The resolution simply represents the number of pixels that span an inch.
This coordinate information is important for use with JavaScript functions which are also exported by gridSVG. Examples of such functions are shown in the next section.
In this section, we will consider the task of modifying the exported SVG image using only a web browser, with no connection to R at all.
We can modify an SVG image within a web browser
by executing JavaScript code to insert SVG
elements representing points into the plot. To start off we
first load the image into the browser. This loads
the SVG image, and executes any JavaScript
code that is referenced or included by the image. By
default gridSVG exports coordinate information to
a JavaScript file, along with a
utility JavaScript file that contains functions useful
for working with gridSVG graphics. In particular, the
utility code includes functions that enable us to do unit
conversion in the browser, e.g. from native
to npc
or to inches
.
Because gridSVG must perform some name manipulation to
ensure that SVG element id
s are unique, a couple
of JavaScript utility functions require
introduction. Firstly, although not stricly necessary, if we know the
name of the grob, we can find out which viewport path it belonged to
by calling grobViewport()
.
JS> grobViewport("datapoints"); "panelvp.1"
We see that the viewport name is not exactly what we chose
in R, but suffixed with a numeric index. Now that we
can query the viewport name, we know which viewport to draw into
and the SVG element that we can add elements
to. However, the issue remains that we really want to be able to
use native
units in the browser, rather
than SVG pixels. To remedy this, unit conversion
functions have been created. These functions are:
viewportConvertX
viewportConvertY
viewportConvertWidth
viewportConvertHeight
The first two conversion functions take three mandatory parameters, the viewport you want the location of, the size of the unit, and what type of unit it is. Optionally a fourth parameter can be specified to determine what the unit is converted to, by default this is SVG pixels. The value returned from this function is a number representing the location in the new unit.
The second two conversion functions are the same but the fourth
parameter, the new type of unit, is mandatory. This means we can
convert between inches
, native
and npc
in the browser without requiring an
instance of R available, so long as we stick to our
existing viewports.
As an example of how we might use these functions, we can find
out where the coordinates (3, 14) are in the
viewport panelvp
(the main plot region) by running
the following code:
JS> viewportConvertX("panelvp.1", 3, "native"); 110.45 JS> viewportConvertY("panelvp.1", 14, "native"); 283.1
We now know that the location of (3, 14) in SVG pixels is (110.45, 283.1). Using this information we can insert a new point into our plot at that location. We also want the the radius of this point to be 2mm, so we can work out how big the point is going to be in a similar manner. The following code shows that a radius of 2mm will translate to 5.67 SVG pixels.
JS> viewportConvertWidth("panelvp.1", 2, "mm", "svg"); 5.67
To insert this new point this requires some knowledge of JavaScript, and knowledge of the SVG DOM. To demonstrate this, a red SVG circle is going to be inserted into the plot at (3, 14) with a radius of 2mm using JavaScript.
// Getting the element that contains all existing points var panel = document.getElementById("panelvp.1"); // Creating an SVG circle element var c = document.createElementNS("http://www.w3.org/2000/svg", "circle"); // Setting some SVG properties relating to the appearance // of the circle c.setAttribute("stroke", "rgb(255,0,0)"); c.setAttribute("fill", "rgb(255,0,0)"); c.setAttribute("fill-opacity", 1); // Setting the location and radius of our points // via the gridSVG conversion functions c.setAttribute("cx", viewportConvertX("panelvp.1", 3, "native")); c.setAttribute("cy", viewportConvertY("panelvp.1", 14, "native")); c.setAttribute("r", viewportConvertWidth("panelvp.1", 2, "mm", "svg")); // Adding the point to the same "viewport" as the existing points panel.appendChild(c);
The image below provides a live demonstration of this task. When the "Click to add point" button is clicked, the browser generates a new SVG object and adds it to the image, relative to the plot scales, using JavaScript code and the information that was exported from R.
This example provides a very simple demonstration of the idea that an SVG image can be manipulated within a web browser by writing JavaScript code. The significance of the new development in gridSVG is that, if an image was generated by grid and exported to SVG using gridSVG, extra information and JavaScript functions are now exported with the image so that the image can be manipulated with full knowledge of all grid coordinate systems that were used to draw the original image (such as plot axis scales).
Much more complex applications of this idea are possible, including complex animations and interactions, limited only by the amount and complexity of the JavaScript code that is required. One possibility with great potential involves leveraging third-party JavaScript libraries for manipulating web page, particularly SVG, content. The d3.js library is one very powerful example.
In this section, we will consider modifying the exported SVG image using R, but in an entirely different R session, without recourse to grid and without any access to the original R code that was used to generate the original image.
When an SVG image is modified within a web browser using JavaScript, as in the previous section, all changes are lost when the image is reloaded. In this section, we will consider a more permanent modification of the image that generates a new SVG file.
We will make use of the gridSVG and XML [5] packages to modify our SVG image. As gridSVG automatically loads the XML package, all of the functionality from the XML package is readily available to us.
The first step is to parse the image, so that it is represented as a document within R.
R> svgdoc <- xmlParse("pointsPlot.svg")
We know that the name of the viewport we are looking for has the exported name of "panelvp.1"
. An XPath [6] query can be created to collect this viewport.
R> # Getting the object representing our viewport that contains R> # our data points R> panel <- getNodeSet(svgdoc, R+ "//svg:g[contains(@id, 'panelvp')]", R+ c(svg="http://www.w3.org/2000/svg"))[[1]]
Now we need to read in the JavaScript file that
contains the coordinates information. However, some cleanup is
needed because the code is designed to be immediately loaded
within a browser, and is thus not simply JSON. We need
to clean up the data so that it is able to be parsed by
the fromJSON()
function.
R> # Reading in, cleaning up and importing the coordinate system R> jsonData <- readCoordsJS("pointsPlot.svg.coords.js")
We now have valid JSON in the form of a character
vector. Using this, we can initialise a coordinate system
in R by utilising both gridSVGCoords()
and fromJSON()
. Nothing is returned
from gridSVGCoords()
because we are setting
coordinate information. If we call gridSVGCoords()
with no parameters we can get the coordinate information
back.
R> gridSVGCoords(fromJSON(jsonData))
Now that a coordinate system is initialised we are able convert
coordinates into SVG pixels. This means we can create
a <circle>
element and correctly position it
using native
units at (3, 14).
R> # Creating an SVG circle element to insert into our image R> # that is red, and at (3, 14), with a radius of 2mm R> circ <- newXMLNode("circle", R+ parent = panel, R+ attrs = list(cx = viewportConvertX("panelvp.1", 3, "native"), R+ cy = viewportConvertY("panelvp.1", 14, "native"), R+ r = viewportConvertWidth("panelvp.1", 2, "mm", "svg"), R+ stroke = "red", R+ fill = "red", R+ "fill-opacity" = 1))
Note that we have used the viewportConvert*
functions to position the circle at the correct location and
with the correct radius. This demonstrates that the same functions that
are available in JavaScript are also available
in R (via the gridSVG package).
This point has been inserted into the same SVG group as
the rest of the points by setting the parent
parameter to the object representing the viewport group.
The only thing left to do is write out the new XML file with the point added.
R> # Saving a new file for the modified image R> saveXML(svgdoc, file = "newPointsPlot.svg")
The new SVG image is located
at "newPointsPlot.svg"
and when loaded
into the browser shows the new point. The appearance of the plot
should be identical to the modifications we made
using JavaScript, except these modifications are
permanent and are able to be distributed to others.
This example provides a very simple demonstration of the idea that an SVG image can be manipulated in R using the XML package. The significance of the new development in gridSVG is that, if an image was generated by grid and exported to SVG using gridSVG, extra information is now exported with the image, and new functions are available from gridSVG to work with that exported information, so that the image can be manipulated with full knowledge of all grid coordinate systems that were used to draw the original image (such as plot axis scales).
Much more complex modifications of an image are possible, limited only by the amount and complexity of the R code that is required. One possibility with great potential involves using R on the server to dynamically manipulate SVG content within a web page on-the-fly.
This article describes several new features of the gridSVG package. The main idea is that grid coordinate system information is now exported, in a JSON format, along with the image (in an SVG format). In addition, JavaScript functions are exported to support the manipulation of the SVG image within a web browser, using this coordinate system information. Furthermore, new R functions are provided so that the SVG image, and its associated coordinate information, can be loaded back into a different R session to manipulate the SVG image within R (using the XML package). These features significantly enhance the framework that gridSVG provides for developing dynamic and interactive R graphics for the web.
This document is licensed under a Creative Commons Attribution 3.0 New Zealand License . The code is freely available under the GPL. The features described in this article were added to version 1.0-0 of gridSVG, which is available on R-Forge (if not on CRAN).