The Web Browser as an R Graphics Device

Paul Murrell
The University of Auckland
November 2016

Hadley: What would your ideal R graphics engine look like?
Me: :(

It all began with a phone conversation with Hadley Wickham. We mostly discussed limitations of the current R graphics system, but Hadley then asked what would the ideal graphics system look like to me. I was stumped; it has been so long since I considered starting over from scratch ...

Me: What would my ideal R graphics engine look like?
Me: HTML + SVG + CSS

Also, I have trouble thinking on my feet. So it was only much later that I thought properly about what I would be interested in building. And it occurred to me that the combination of HTML + SVG + CSS is something that I really like.

Me: How can I render HTML + SVG + CSS
without any effort?
Me: Get a web browser to do the rendering!

I have done plenty of generating HTML + SVG + CSS, either manually or programmatically with R. I have also done a bit of manipulating HTML + SVG + CSS with javascript. But what I would like to do is generate AND manipulate HTML + SVG + CSS with R. However, I do NOT want to have to be responsible for the rendering (at least not for initial experimentation). Can I get a browser to do the hard work for me ... ?

The 'DOM' package

A websocket connection between R and a browser


library("DOM")
            

The first step is to load the 'DOM' package.


page <- htmlPage("<p>Hello world!</p>")
            

The htmlPage() function opens a new page in a web browser and returns a websocket connection which we can use to send requests from R to the browser.

The 'DOM' package

A DOM API for querying and modifying web page content


appendChild(page, htmlNode("<p>Explicit HTML</p>"))
            

The 'DOM' package implements some of the standard DOM interface to the content of an HTML document. For example, the appendChild() function allows us to append new content to the end of a web page.

The 'DOM' package

A DOM API for querying and modifying web page content


appendChild(page, htmlNode("<p>More HTML</p>"), 
            parent=css("p"))
replaceChild(page, htmlNode("<p>Replacement HTML</p>"), 
             oldChild=css("p:nth-child(2)"))
setAttribute(page, css("p"), "style", "text-align: right")
removeChild(page, css("p"))
getElementsByTagName(page, "p")
            

These are some other examples of DOM actions: we can append content to a specific location, we can replace or remove content, and we can query the document for its content.

The 'DOM' package

Generate HTML content from R


library("xtable")
html <- print(xtable(head(iris)), 
                     type="html", 
                     print.results=FALSE,
                     comment=FALSE)
appendChild(page, htmlNode(html), response=nodePtr())
            

Because we are in R, there are useful tools for programmatically generating HTML content. This example shows the 'xtable' package being used to generate an HTML table, which is then appended to the web page.

The 'DOM' package

Callback from the browser to R


callback <- function() {
    cat("You clicked?\n")
}
setAttribute(page, css("p"), "onclick", 
             'RDOM.Rcall("callback", this, null, null)')
            

The websocket connection is two-way, so the browser can also send requests to R. This code sets up the web page to call an R function whenever there is a mouse click on the first paragraph of text.


moveMouse <- function(x0, y0, x1, y1) {
    x <- seq(x0, x1, length.out=100)
    y <- seq(y0, y1, length.out=100)
    for (i in 1:100) {
        system(paste0("xdotool mousemove ", x[i], " ", y[i]))
    }
}
# Assign globally so other code chunks can see
assign("moveMouse", moveMouse, .GlobalEnv)
moveMouse(750, 270, 850, 170)
mouseClick <- function() {
    system("xdotool click 1")
}
assign("mouseClick", mouseClick, .GlobalEnv)
mouseClick()
Sys.sleep(1)
mouseClick()
            

Calls from the browser are limited to a call of the form ... RDOM.Rcall(name-of-R-function, pointer-to-DOM-element(s), format(s)-of-DOM-element(s)-passed-to-R-function, callback) This is another advantage to using the browser to render the web page content: the browser can respond to user events on the content of the web page document. The 'DOM' package allows R to respond to these user events in the browser. This also demonstrates that communication between R and the browser is asynchronous. R is not blocked waiting for the browser to callback(). The callback() function runs when the browser sends a request on the web socket. Most 'DOM' functions block by default so that we can run R code one step at a time as usual, but they can also be called asynchronously.

The 'DOM' package

Append SVG content


appendChild(page, 
            svgNode('<svg xmlns="http://www.w3.org/2000/svg">
                         <circle cx="50" cy="50" r="50"/>
                    </svg>'), 
            ns=TRUE)
            

We can also embed an SVG image within a web page, though namespace information must be provided.

The 'DOM' package

Generate SVG content from R


library(lattice)
xyplot(mpg ~ disp, mtcars, pch=16, col=rgb(0,0,1,.5), cex=2)
library(gridSVG)
svg <- grid.export(NULL)$svg
dev.off()
library(XML)
appendChild(page, svgNode(saveXML(svg)), ns=TRUE, 
            response=nodePtr())
            

As with HTML content, we can use R packages to generate the SVG content that we want to add to the web page. This code uses 'lattice' to draw a plot and 'gridSVG' to generate an SVG version of the plot, which is then appended to the page.

Interactive graphics


points <- 
    getNodeSet(svg, "//svg:use",
               namespaces=c(svg="http://www.w3.org/2000/svg"))
for (node in points) {
  xmlAttrs(node) <- 
    c(onclick='RDOM.Rcall("info", this, ["HTML", "CSS"], null)',
      "pointer-events"="all")
}
page <- htmlPage('<p>Click a point to see more info</p>')
appendChild(page, svgNode(saveXML(svg)), ns=TRUE, 
            response=nodePtr())
appendChild(page, htmlNode('<pre id="info"></pre>'))
info <- function(html, css) {
    index <- as.numeric(gsub(".+[.]", "", css))
    newPre <- paste0('<pre id="info">',
                     paste(capture.output(mtcars[index,]), 
                           collapse="\n"), "</pre>")
    replaceChild(page, oldChild=css("#info"), async=TRUE,
                 newChild=htmlNode(newPre))
}
            

The browser can respond to user events on the SVG content of the web page document, and we can make that response a request to R, which can in turn modify the web page. This code creates a simple interactive graphic where a mouse click on a data point displays more information about the data point.


moveMouse(908, 373, 908, 273)
mouseClick()
Sys.sleep(2)
moveMouse(908, 273, 1407, 763)
mouseClick()
            

The code above: identifies all 'use' elements in the SVG; adds an 'onclick' attribute to them that will call the R function info(); starts a new web page containing a paragraph of instructions; appends the SVG plot; appends an empty 'pre' element; defines an R function info(). After running the above code, a click on one of the data symbols in the plot in the browser will fill the 'pre' element on the web page with information about the data point.

Talk to existing web page


require(xtable)
require(XML)
          

page <- filePage(file.path(getwd(), "song.html"))
pres <- getElementsByTagName(page, "pre", response=css())
for (i in seq_along(pres)) {
    setAttribute(page, pres[i], "onclick",
                 'RDOM.Rcall("replaceWithTable", this, 
                             [ "HTML", "CSS" ], null)')
}
replaceWithTable <- function(target, targetCSS) {
    text <- xmlValue(xmlRoot(xmlParse(target)))
    wordCount <- table(strsplit(gsub("\n", " ", text),
                                " +")[[1]])
    wordTab <- print(xtable(wordCount), type="html",
                     print.results=FALSE, comment=FALSE)
    replaceChild(page, newChild=htmlNode(wordTab), 
                 oldChild=targetCSS, async=TRUE)
}
            

This code also shows a two-way interaction between the browser and R. The difference is that we are using the filePage() function to open an web page that someone else has previously created. Clicking on a paragraph calls back to R and replaces the paragraph with a table of counts of the words in the paragraph.


moveMouse(1000, 300, 900, 200)
mouseClick()
            

Requires user script (RDOM.user.js) and greasemonkey

This example demonstrates the filePage() function, which allows us to make a websocket connection to an existing web page (on the local file system; there is also a urlPage()). For this to work, the web browser should be Firefox, the greasemonkey plugin must be installed, and the RDOM.user.js user script must be installed in that. This example also demonstrates that I am interested in working with both HTML and SVG content from R.

Questions for the audience

  • How is this different from 'Shiny'?
  • How is this different from 'RSelenium'?
  • How is this different from 'rdom'?
  • How is this different from "R Notebooks"?

The 'DOM' package: is lower-level than 'Shiny' (more flexible and more dangerous); also, 'DOM' responds to user events, rather than just driving the browser (like 'RSelenium'); 'DOM' renders web content and responds to user events, rather than just allowing querying of web content (like 'rdom'); and 'DOM' is more general-purpose than just allowing chunks of code in a web page to be run (like "R Notebooks"). 'DOM' differs from 'rvest' because the latter interacts with the browser via HTTP (rather than a web socket), so is "limited" to HTTP actions (GET, PUT, ...).

Summary

The 'DOM' package ...

  • allows me to dynamically generate and modify the content of a web page.
  • provides a basis for experimenting with HTML, SVG, and CSS "graphics" from R.

Acknowledgements

  • The httpuv R package (for websockets)
  • The jsonlite R package (for JSON messages on websockets)
  • The css-select-generator JS library (for CSS selectors)
  • The bowser JS library (for identifying the browser)

  • The xtable R package (for generating HTML)
  • The lattice R package (for drawing plots)
  • The gridSVG R package (for generating SVG)
  • The XML R package (for manipulating SVG)