Working with the gridSVG Coordinate System

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.

Introduction

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()
Basic plot produced by grid
A basic plot produced by grid.

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 basic plot produced by grid with an extra point added
A basic plot produced by grid with an extra point added.

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.

The gridSVG Coordinate System

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")
Basic plot produced by gridSVG
A basic plot produced by gridSVG.

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.

Browser-based Modification

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 ids 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:

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.

0 5 10 15 20 0 5 10 15 20 x = y =

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.

Modification via the XML package

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")
gridSVG plot modified using XML package
A gridSVG plot modified using the XML package.

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.

Conclusion

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.

Downloads

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).

References

  1. R Development Core Team (2012). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. ISBN 3-900051-07-0.
  2. Murrell, P. and Potter, S. (2012). gridSVG: Export grid graphics as SVG. https://r-forge.r-project.org/projects/gridsvg/. R package version 1.0-0.
  3. W3C (2011). Scalable Vector Graphics (SVG) 1.1 (Second Edition) Specification. http://www.w3.org/TR/SVG/.
  4. JSON, http://json.org/.
  5. Lang, D. T. (2012). XML: Tools for parsing and generating XML within R and S-Plus.. http://www.omegahat.org/RSXML/. R package version 3.9-4
  6. W3C (1999). XML Path Language (XPath) Version 1.0 Specification. http://www.w3.org/TR/xpath/.