1. Simple linking
  2. Tooltips
  3. Hyper-links
  4. Animation
  5. SVG GUIs and hiding & showing elements of a plot
  6. European Exchange Rate time series
  7. Radioboxes controlling multiple lattice panels
  8. Ternary plot
  9. Beta density Exploration
  10. Graphviz interaction
  11. K-nearest neighbors animation
  12. Dynamic linking lattice plots via legends
  13. Function size density with dynamic function name labels

SVGAnnotation Examples

The SVGAnnotation package is an approach to using the cairo-based SVG graphics device in R and then post-processing the results to add tooltips, hyperlinks, animation and other SVG effects. Here are some simple-minded examples.

  • Simple linking
  • This is an example of simple linking: you move the mouse over a point and the points changes color and so do the points in the other plots for the corresponding observation

    svg("pairs_link.svg", 14, 10)
    pairs(mtcars[,1:3], cex = 2)
    dev.off()
    
    doc = xmlParse("pairs_link.svg")
    linkPlots(doc)
    saveXML(doc, "pairs_link.svg")
          
  • Tooltips
  • This illustrates how we can add tooltips to the axes labels and/or the title and for each of the points in the plot. Move the pointer/cursor to be over the title or one of the axes labels to see a more detailed explanation of the plot or the units of the variables. Similarly, if you move the pointer over one of the points in the plot, a tooltip will pop-up and display the values of all the variables for that observation.
     # Create the basic plot
    svg("axes_tips.svg")
    plot(mpg ~ wt, mtcars, main = "The title")
    text(median(mtcars$wt), median(mtcars$mpg), "the medians")
    dev.off()
    
     # Post process it
    doc = xmlParse("axes_tips.svg")
    
    addCSS(doc)
    
     # Add tooltips to the
    ax = getAxesLabelNodes(doc)
    tips = c("1974 Motor Trend US magazine fuel consumption data",
              "Weight in units of lb/1000", "Miles per US gallon")
    invisible(sapply(seq(along = ax), function(i) addToolTips(ax[[i]], tips[[i]])))
    
     # tooltips for the points. The text is the name = value sequence for all the variables for that point.
    addToolTips(doc, apply(mtcars, 1, function(x) paste(names(mtcars), x, sep = " = ", collapse = ", ")), doc = doc)
    
    saveXML(doc, "axes_tips.svg")      
    

  • Hyper-links
  • We can add hyperlinks on, for example, axes labels or the title or points in the plot. Click on the title or axes labels and you will visit another page.
  • Animation
  • We can add animations to a plot. For example, the following plot displays traffic data from PEMS (Freeway Performance Measurement System ), and specifically data recorded by loop detectors/sensors in the tarmac on freeways in California. It shows points for each 5 minute period within a day. The horizontal axis measures the proportion of time that a car is over the traffic sensor. The vertical axes shows the number of cars that passed over the sensor within that 5 minute period. We animate this over 6 different days. The points move to their value on the next day. When they "arrive", the color changes to identify the day. A simple way of identifying which day is which is to display the name of the day in the top-left corner and have the different days become visible and then invisible at different times. (The data are not organized starting at the beginning of each day, so the results are incorrect, but the basic animation steps are what is of interest in this example.) There is a great deal more we could do to make this useful but this is more an illustration of the ability to add animation than a carefully designed use of animation.

          
    
           
  • SVG GUIs and hiding & showing elements of a plot
  • In R, when we want to remove something from a plot, we have to redraw the entire plot (without the element). Similarly, if we want to add to a plot, we sometimes have to redraw the entire plot. With SVG, we can make elements visible and invisible. How do we instruct the plot to show/hide something? Radio buttons are one way. And thanks to the work of Andreas Neumann and Andreas Winter at http://www.carto.net/papers/svg/gui we can build live, interactive GUIs with SVG and ECMAscript.

    In this example, we draw two density curves on the same plot. Then we put two checkboxes on the right of the plot. When one toggles either of these (by clickin on it), the corresponding series is made visible/invisible.

    The code to do this is
    x = rnorm(100)
    y = rnorm(200, 1, .5)
    
        
    svg('densities.svg')
      plot(density(x), ylim = c(0, 1))
      lines(density(y), col = "red")
    dev.off()
    
    radioShowHide("densities.svg")    
        
    Of course, the real action is in both the function radioShowHide() in the SVGAnnotation package and the SVG GUI components. But the steps in radioShowHide() are quite straightforward manipulation of the existing SVG plot generated by R.
  • European Exchange Rate time series
  • We can do the same thing as above with real data! Let's plot the exchange rate for different currencies against the Euro.

    The code for creating this is:

    eu = read.csv("~/Downloads/eurofxref-hist.csv", header = TRUE, na.strings = "N/A")
    eu$Date = as.POSIXct(strptime(eu$Date, "%Y-%m-%d"))
    
      # Discard currencies that are mostly missing
    eu = eu[ , sapply(eu, function(x) sum(is.na(x)))/nrow(eu) 
    
        
        ##################################
    # Draw the plot, using a log scale for exchange rate.
    svg("euSeries.svg")
    matplot(eu[,1], as.data.frame(lapply(eu[,-1], log)),
             type = "l", xlab = "Date", ylab = "log(exchange rate)",
               main = "European Exchange Rate")
    abline(h = 1, col = "gray")
    dev.off()
    
    radioShowHide("euSeries.svg", within = TRUE, labels = currency.names[ match(names(eu)[-1], names(currency.names))])
    

    We would like to be able to do much better. For example, we'd like that

    • when we eliminate a series, we adjust the Y axis if appropriate to zoom in
    • we allow the user to zoom in on a region of the Y axis
    • we allow the user to zoom in on a region of the X axis, i.e. a specific period of time.

  • Radioboxes controlling multiple lattice panels
  • We'd like to go further and have a lattice plot that divided the series into years, with each of the 10 panels having all 24 currencies within that year. We would have the checkboxes beside the collection of panels and toggling an individual checkbox would toggle the appearance of that currency on all of the 10 panels.

    The code is divided into two steps: arranging the data, and then creating and post-processing the SVG plot.

    eu = read.csv("~/Downloads/eurofxref-hist.csv", header = TRUE, na.strings = "N/A")
    eu$Date = as.POSIXct(strptime(eu$Date, "%Y-%m-%d"))
    
    eu.sub = eu[as.POSIXlt(eu$Date)$year >= 105, c("Date", "GBP", "USD", "CAD", "CHF")]
    
    st = data.frame(Date = rep(eu.sub$Date, ncol(eu.sub) - 1),
                    rate = unlist(eu.sub[-1]),
                    currency = rep(names(eu.sub)[-1], each = nrow(eu.sub)))
    
        
      # Create the plot, but don't print it. We'll do that next.
    plt = xyplot( log(rate) ~ as.POSIXlt(st$Date)$yday | as.POSIXlt(st$Date)$year, st , group = currency,
                     auto.key = list(columns = length(unique(st$currency))), type = "l")
    
    svg('euSeries4.svg')
    print(plt)
    dev.off()
    
      # Now put in the JavaScript code, change the width of the viewBox, add checkboxes, etc.
      # But don't set the id's on the series within the panels. We'll do that ourselves.
    doc = radioShowHide("euSeries4.svg", within = NA, labels = levels(st$currency), save = FALSE,
                          id.prefix = "", numPanels = length(unique(as.POSIXlt(st$Date)$year))) # Put the currency.names[ .... ] back
    tmp = o = getPlotRegionNodes(doc)
    
         # Just getting the plot regions includes the tick marks, etc. so
         # we have to be more specific
    isPlotRegion = sapply(o, function(x) xmlSize(x) == 4 && nrow(as(x[[1]], "SVGPath")) > 100)
    o = o[ isPlotRegion ]
    
    annotatePanel = 
     function(i, o) {
                     # for this plot region
               els = xmlChildren(o[[i]])
               sapply(seq(along = els),
                       function(k) {
                          addAttributes(els[[k]], id = paste("panel", i, k, sep = "-"))
                          newXMLNode("title", levels(st$currency)[k], parent = els[[k]])
                       })
               TRUE
             }
    
    sapply( seq(along = o), annotatePanel, o)
    
    saveXML(doc, docName(doc))
    
    The post-processing step involves finding the plot region nodes for a lattice plot. Here we just check the length of the path to ignore regions which are just tick marks or the panel rectangle. After that we do essentially
  • Ternary plot
  • We'll take a less common plot that comes from a non-standard package, vcd - Visualizing Categorical Data. This is a ternary plot where the sum of the three variables is 1 for each point. We put a tooltip on each point in the plot and the tooltip displays all the values for the observation. We also put links on the title and the three axes labels to bring the viewer to pages explaining each of these. The code is also present to add tooltips to these text segments.

    The code to create and process the plot is

    library(vcd)
    
    svg('ternaryplot.svg')
    ternaryplot(
      Hitters[,2:4],
      pch = as.character(Positions),
      col = colors[as.numeric(Positions)],
      main = "Baseball Hitters Data")
    dev.off()
    
    doc = xmlParse('ternaryplot.svg')
    pts = getNodeSet(doc, "//x:g[@clip-path='url(#clip1)']/x:g", "x")
    pts = pts[ sapply(pts, xmlSize) == 1 ]
    
    numberVars = sapply(Hitters, is, "numeric")
    Hitters[sapply(Hitters, is, "factor")] = sapply(Hitters[sapply(Hitters, is, "factor")], as.character)
    
    invisible(
     sapply(seq(along = pts),
            function(i) {
              els = Hitters[i, ]
              els[numberVars] = sapply(els[numberVars], formatC, 4)
              newXMLNode("title", paste(names(Hitters), els, sep = " = ", collapse = ", "), parent = pts[[i]])
            }))
    
    
    # Under #clip
    plotRegion = getNodeSet(doc, "//x:g[@clip-path='url(#clip1)']", "x")[[1]]
    
    if(names(plotRegion)[1] == "g")
      titleNode = plotRegion[[1]]
    
    labelNodes = plotRegion[3:5]
    tips = c(
             putouts = "A play in which a batter or a baserunner is retired",
             assists = "A fielding and throwing of a baseball in such a way that enables a teammate to put out a runner",
      errors = "A defensive fielding or throwing misplay by a player when a play normally should have resulted in an out or prevented an advance by a base runner")
    
    urls = c(putouts = "http://www.thefreedictionary.com/putout",
             assists = "http://en.wikipedia.org/wiki/Assist_(baseball)",
             error = "http://www.thefreedictionary.com/error"
            )
    
    sapply(seq(along = labelNodes),
           function(i) {
             #addToolTips(labelNodes[[i]], tips[i])
             addLink(labelNodes[[i]], urls[i])
           })
    
    
    addLink(plotRegion[[1]], "http://bm2.genes.nig.ac.jp/RGM2/R_current/library/vcd/man/Hitters.html")
    
    #
    addCSS(doc)
    
    saveXML(doc, docName(doc))
    

  • Beta density Exploration
  • The following illustrates a simple-minded way to allow for interactive graphics that appear to be doing R computations on the fly. (This would be possible if we resurrected the "R as a browser plugin/extension" that we had with SNetscape.) The example displays a Beta density and allows the viewer to change the parameters α and β by dragging the respective slider. (The slider comes from Andreas Neumann's SVG GUI tools.)

    How is this done? In the "obvious" way! We compute the curve for all combinations of α and β in the range we consider, discretized of course! We hide all but the first one. When the viewer moves the slider, we find the corresponding curve, make the current one invisible, and the new one visible. We have 900 curves in this display. It takes some time to load these, but then the interactive response is relatively rapid.

    The R code is

    alpha = seq(.01, by = 0.05, length = 30)
    beta = seq(.01, by = 0.05, length = 30)
    
    grid = expand.grid(alpha, beta)
    
    f = 'beta.svg'
    svg(f)
    plot(0, type = "n", xlim = c(0, 1), ylim = c(0, 1.5), xlab = "X", ylab = "density",
             main = "Density of beta distribution")
    apply(grid, 1, function(p) curve(dbeta(x, p[1], p[2]), 0, 1, n = 300, add = TRUE))
    #curve(dbeta(x, .3, .7), 0, 1, add = TRUE)
    #curve(dbeta(x, .7, .3), 0, 1, add = TRUE, col = "red")
    dev.off()
    
    #######
    # Now post-process.
    
    doc = xmlParse(f)
    box = getViewBox(doc)
    p = getPlotRegionNodes(doc)[[1]]
    
    grid = expand.grid(seq(along = alpha), seq(along = beta))
    ids = paste("curve", grid[,1], grid[,2], sep = "-")
    invisible(
              sapply(seq(along = ids),
                     function(i)
                         addAttributes(p[[i]], .attrs = c(id = ids[i], visibility = "hidden"))))
    
    addAttributes(p[[1]], .attrs = c(visibility = "visible"))
    
    ##########
    
    svg = xmlRoot(doc)
    
    enlargeSVGViewBox(doc, y = 100, svg = svg)
    
    newXMLNode("g", attrs = c(id = "slider-alpha"), parent = svg)
    newXMLNode("g", attrs = c(id = "slider-beta"), parent = svg)
    
    newXMLNode("text",  attrs = c(x = "20",  y = box[2, 2], id = "statusText"), "", parent = svg)
    
    addAttributes(svg, onload = sprintf("init(evt, %d, %d);", length(alpha), length(beta)))
    
    addECMAScripts(doc, findJScripts(c("mapApp.js", "helper_functions.js", "slider.js", "betaSlider.js")), FALSE)
    
    addCSS(doc)
    
    defs = getNodeSet(doc, "//x:defs", "x")[[1]]
    
    newXMLNode("symbol", attrs = c(id = "sliderSymbol",  overflow = "visible"),
    	    newXMLNode("line", attrs = c(x1 = "0",  y1 = "-10",  x2 = "0",  y2 = "10",
                                             stroke = "dimgray", 'stroke-width' = "5",
                                             'pointer-events' = "none")),
                parent = defs)
    
    saveXML(doc, docName(doc))
    
    The callbacks for the slider that determine which curves to make visible and invisible is in betaSlider.js. It is quite simple.
  • Graphviz interaction
  • The following is a display of a simple random graph generated via the graph package and rendered via the Rgraphviz package. With some functions in the SVGAnnotation package, we can annotate the resulting SVG.

    As you mouse over the nodes, the edges from that node to the other nodes are higlighted and the color of the other edges are changed to a light grey. When we move out of the node, we restore the original view.


    We can deal with other layouts two with complex edges made up of multi-part curves (splines).

  • K-nearest neighbors animation
  • The simple idea here is to display a scatter plot giving the relationship between two predictor variables. There may be more in our data set. As the viewer moves over the observations in the plot, the k nearest observations (computed using all the predictors) are highlighted by drawing lines to them.

    Note that you have to put the mouse precisely on the pixels of a point, not just in a point.

    This illustrates how we can dynamically construct SVG elements within the ECMA/JavaScript code rather than creating all possible lines first.

  • Dynamic linking lattice plots via legends
  • In these two examples, we illusrate how one might use linking across "plots", or specifically panels, within a lattice display. The points in each panel do not correspond to each other whatsoever, so regular linking makes no sense. However, when we draw a lattice plot using groups within a panel (i.e. via the groups parameter, then different points in different panels do have a correspondence, i.e. being part of the same group. So we add interactive capabilities to the plot to allow the viewer mouse over a lable in the key/legend of the plot. This highlights the corresponding element in the panels.

    In our first example, we have an xyplot with three panels and four groups.

    In our second example, we have a densityplot() with a single panel and multiple groups. These data inllustrate the distribution of the size of functions in elements of my search path. As the viewer mouses over the key/legend labels, the corresponding density plot is highlighted by increasing its stroke-width. We have also added a tooltip to indicate the number of observations in the particular group.
    In this example, we have added a rectangle behind the label to allow the viewer to mouse over any part of the label, not just the stroke of the letters.

  • Function size density with dynamic function name labels
  • We extend the previous example displaying the densities of the size of functions in different packages on the search path. In this version, when the viewer moves over a label in the legend, we display the names of functions in that package that have more than 20 top-level expressions in the body. We display them centered at their "size" (number of top-level expressions) and increase their location vertically to try to avoid occlusion. When the viewer moves the mouse away from the legend label, we hide these again.

    Duncan Temple Lang <duncan@wald.ucdavis.edu>
    Last modified: Thu Apr 16 05:47:53 PDT 2009