Fork me on GitHub

R Interactive Graphics with SVG

It seems computer history is full of examples of forgotten concepts from programmers ahead of their time.

Before d3 (2011) and Even Protovis (2009)

Long before the 2011 release of d3.js and 2009 launch of Protovis and even before Hans Rosling's famous example, the folks at carto.net were doing amazing and revolutionary interactive graphics in SVG. Consider this 2006 Interactive Map of Yosemite described in the paper by Juliana Williams and Andreas Neumann.

Building on this body of knowledge, some well known R contributors Deborah Nolan and Duncan Temple Lang (authors of the upcoming book XML and Web Technologies for Data Sciences with R (Use R!) ) created the R package SVGAnnotation described in Journal of Statistical Software Vol 46 Issue 1 (submitted paper and html version). The article demonstrates how to achieve SVG interactivity straight from R still a year before d3.js first release. Embedded below are two examples of many, each created directly from R. You should see some animation and tooltips on hover.

Now let's fast forward 3 years to 2013 where the concept of the HTML5/SVG/javascript combination for interactive graphics is ultra popular and well established through d3.js. The novel element of d3 is its ability to bind data to elements of a document to create the 3 d's of d3--data-driven documents.

Just a Little Bit of Glue

For R, we just need a little bit of glue to blend the old with the new to directly harness the power of SVG interactivity and d3 data binding. It seems there are 3 types of glue available:

  1. Let R do the data and then send the data to Javascript to create the SVG graphics. This is the process employed by rCharts, clickme, d3network, googleVis, gigvis, and tabplotd3.

  2. Let R both do the data and render the graph then export the SVG to get interactivity from Javascript. We see this with the new and improved gridSVG and the previously mentioned SVGAnnotation.

  3. Use 1. or 2. and then maintain bidirectional communication between R and Javascript through shiny, Rook, or some other web server type interface.

I believe the choice of method will depend on the user's competence in R and/or Javascript, their desire for customization, and the need for R's data calculation abilities post-render. I have posted a lot about using method 1 with rCharts and clickme, so I wanted to start a series of demos using method 2. Method 3 is fairly trivial with the full-featured Shiny and Rook once we have 1 and 2 conquered.

Method 2 | R Draw and Render SVG with a d3 Reverse Data Bind

Much of the R to SVG conversion is already shown in this blog from the R Mecca in New Zealand. As Simon Potter has extended SVGAnnotation while improving gridSVG, he has documented the process and the improvements on this blog and in his soon-to-be-marked Masters' thesis. Since there are still few examples of the data bind step that we know and love from d3 but in reverse, I thought I would share a quick experiment doing a reverse data bind using d3 on a R/gridSVG exported graph. For those not familiar with d3, Mike Bostock's Three Little Circles will be very helpful.

If we choose gridSVG, we lose base graphics (SVGAnnotation does allow base), but I believe its advantages overcome this loss, and we still have the super-powerful grid graphics libraries lattice and ggplot2. For this example, I will build on top of a ggplot2 scatter plot example from Winston Chang's Cookbook from R). This is the supporting website for his book

.

Here is the slightly modified code that will produce our starting chart. Notice the smoothing that would be difficult to achieve with just javascript.

Now would be a good time to get the newest gridSVG with install.packages("gridSVG", repos="http://R-Forge.R-project.org").

#get the latest version of gridSVG
#install.packages("gridSVG", repos="http://R-Forge.R-project.org")

require(ggplot2)

set.seed(955)
# Make some noisily increasing data
dat <- data.frame(cond = rep(c("A", "B"), each=10),
                  xvar = 1:20 + rnorm(20,sd=3),
                  yvar = 1:20 + rnorm(20,sd=3))
# cond         xvar         yvar
#    A -4.252354091  3.473157275
#    A  1.702317971  0.005939612
#   ... 
#    B 17.793359218 19.718587761
#    B 19.319909163 19.647899863

g4 <- ggplot(dat, aes(x=xvar, y=yvar)) +
  geom_smooth() +  #we'll see why order is important
  geom_point(shape=19, aes(color = cond), size=5) 

g4

plot of chunk unnamed-chunk-2

There are lots of ways that we might add a tooltip. gridSVG on its own can easily handle this and even animation. However, I really want to use d3. A full d3-style data bind will be a fine way to achieve this extra functionality. When we export our graphic, we will get an impercetibly different SVG copy of our ggplot2 graphic above. If you don't believe it is SVG, left-click on the graphic and Inspect Element. I told you it was SVG. If you still don't believe me, zoom in to 400% and see if you can tell a difference between SVG and png.

require(gridSVG)
#print our ggplot2 graphic again
g4
#export to SVG file and R object
#grid.export deprecates the older gridToSVG
g4.svg <- grid.export("plot1.svg",addClasses=TRUE)
#print our newly exported SVG inline
cat(saveXML(g4.svg$svg))

0 10 20 30 -5 0 5 10 15 20 xvar yvar cond A B

Of course the objective is to get more than just a more scalable graphic. Let's bind some data to get some simple tooltips. ggplot2 stores the data for our graph within our g4 object. We can see it.

str(g4)
head(g4$data)

We will need some way to give this data to Javascript. Javascript likes JSON, so let's use the package rjson to send our data. I will assume that we are more comfortable flattening our data in R. I use cat below but if we are using knitr or slidify we can just write the script inline.

cat(
  '<script> ourdata=',
  rjson::toJSON(apply(g4$data,MARGIN=1,FUN=function(x)return(list(x)))),
  '</script>'
)

After this ourdata in Javascript should contain an array of 20 arrays, each representing the data for a point on our graph. To show off some d3, we will get our data in perfect bindable form with this.

cat(
  '<script> dataToBind = ',
  'd3.entries(ourdata.map(function(d,i) {return d[0]}))',
  '</script>'
)

Finally, we are ready for the data bind. We used gridSVG(...,addClasses=TRUE) to ease our d3.select. In future examples, we will see other ways we might accomplish this.

cat(
  '<script>\n',
  'scatterPoints = d3.select(".points").selectAll("use");\n',
  'scatterPoints.data(dataToBind)',
  '</script>\n'
)

Once our data is bound, we are well on our way to some tooltips. Let's add some simple tooltips that tell us the x and y data from R (note not the x,y SVG coordinates).

cat('<script>\n',
'scatterPoints  
    .on("mouseover", function(d) {      
      //Create the tooltip label
      var tooltip = d3.select(this.parentNode).append("g");
      tooltip
        .attr("id","tooltip")
        .attr("transform","translate("+(d3.select(this).attr("x")+10)+","+d3.select(this).attr("y")+")")
        .append("rect")
          .attr("stroke","white")
          .attr("stroke-opacity",.5)
          .attr("fill","white")
          .attr("fill-opacity",.5)
          .attr("height",30)
          .attr("width",50)
          .attr("rx",5)
          .attr("x",2)
          .attr("y",5);
      tooltip.append("text")
        .attr("transform","scale(1,-1)")
        .attr("x",5)
        .attr("y",-22)
        .attr("text-anchor","start")
        .attr("stroke","gray")
        .attr("fill","gray")
        .attr("fill-opacity",1)
        .attr("opacity",1)
        .text("x:" + Math.round(d.value.xvar*100)/100);
      tooltip.append("text")
        .attr("transform","scale(1,-1)")
        .attr("x",5)
        .attr("y",-10)
        .attr("text-anchor","start")
        .attr("stroke","gray")
        .attr("fill","gray")      
        .attr("fill-opacity",1)
        .attr("opacity",1)
        .text("y:" + Math.round(d.value.yvar*100)/100);
    })              
    .on("mouseout", function(d) {       
        d3.select("#tooltip").remove();  
    });',
'</script>'
)

Not perfect, but I think our progress is admirable. We can save the fancier tooltips and additional interactivity for later. Please don't consider any of what is shown here as best practice. Much of this is experimental. We will explore many other ways of accomplishing and extending what we have done. If you have suggestions and ideas, please share them.

I strongly encourage you to visit this blog and Paul Murrell's summary page to see a much more thorough discussion and additional examples. Some other quick (and I emphasize quick) experiments that I have done are listed below.