A graphical toolkit for visualization
Protovis
Overview
Examples
Documentation
Download
Index
« Previous / Next »

How-To: Scale Interaction

Let’s add some useful interaction to the simple line chart included with the example gallery. The goal is that when the user mouses over the plot, we highlight the closest point in the data with a dot, as well as showing the exact value. (You may have already seen an example of this interaction technique with the Minnesota Employment example.)

With a Single Series

The data associated with the line is an array of xy-coordinates. So the first thing we need is a global i to store the index of the coordinates nearest the mouse. It can default to -1, indicating that the mouse is not over the visualization:

var i = -1;

Next, we need to add a dot to the line, highlighting the value closest to the mouse’s x-coordinate:

var dot = line.add(pv.Dot)
    .visible(function() i >= 0)
    .data(function() [data[i]])
    .fillStyle(function() line.strokeStyle())
    .strokeStyle("#000")
    .size(20)
    .lineWidth(1);

Note that the dot is invisible if i is negative (the default value of -1). The data is a single-element array containing the xy-coordinate closest to the mouse; by doing it this way, we can inherit all of the other properties from the line. Most importantly, we inherit the bottom and left properties which place the dot in the desired position. We also use property chaining to set the fill color of the dot to match the line’s stroke color.

We might also want to add a second dot to the visualization in the lower-left corner, accompanied by a label showing the value of the y-coordinate. That can be done simply as:

dot.add(pv.Dot)
    .left(10)
    .bottom(10)
  .anchor("right").add(pv.Label)
    .text(function(d) d.y.toFixed(2));

Lastly, we need to specify event handlers to wire everything up. Ideally, we could add those directly to the root panel and be done with it, but there are some minor flickering issues caused by child elements. So for now, we use an invisible bar to capture the events flicker-free:

vis.add(pv.Bar)
    .fillStyle("rgba(0,0,0,.001)")
    .event("mouseout", function() {
        i = -1;
        return vis;
      })
    .event("mousemove", function() {
        var mx = x.invert(vis.mouse().x);
        i = pv.search(data.map(function(d) d.x), mx);
        i = i < 0 ? (-i - 2) : i;
        return vis;
      });

Let’s look more closely at what these event handlers are doing.

The job of the “mouseout” event handler is to clear the global i, by setting it to -1. It then returns vis which causes the root panel to be re-rendered. We could equivalently call vis.render().

The “mouseover” event handler requires slightly more work: given the mouse position, it needs to calculate the index of the closest xy-coordinate in the data. This requires two steps: first, the mouse’s x-coordinate is inverted, mapping the pixel location back into a value along the x-axis. Second, we perform a binary search on the data, finding the index of the closest xy-coordinate. Note that this requires the data array to be sorted by x-coordinate. Lastly, because pv.search will return a negative number if the exact value is not found, we invert the value to set i as the insertion point. (See the API reference for details.)

Putting everything together:

With Multiple Series

The above example works great with a single series, but what if we have multiple series of data? In that case, the data will be a two-dimensional array (an array of series, where each series in an array of xy-coordinates), and a panel to replicate our line for each series:

var panel = vis.add(pv.Panel)
    .data(data);

Our line will be added to this panel, rather than the root vis, so that it gets replicated. Thus, since our mouseover dot is added to the line, it too is automatically moved to the child panel. However, notice that the data property for the mouseover dot was computed using the global variable data. In this case, we’ll need to be more specific and reference the data for each series. But since the series is the datum for the child panel, this is trivial:

line.add(pv.Dot)
    .visible(function() i >= 0)
    .data(function(d) [d[i]])
    ...

There will now be multiple labels in the lower-left corner, as well. So we’ll need to offset the bottom position so they don’t overlap, like so:

dot.add(pv.Dot)
    .left(10)
    .bottom(function() this.parent.index * 12 + 10)
    ...

Lastly, we’ll need to update the “mouseover” event handler slightly, since data is now a two-dimensional array. If we assume that the x-coordinates for our data are the same for all series, we can simply replace the reference to data with data[0] (the first series). If our data had different x-coordinates per series, note that we’d need multiple i indexes as well, and we’d need to do a separate binary search per series.

Putting it all together:

Copyright 2010 Stanford Visualization Group