Note: event handlers are inherited by children, but not (currently) from the prototype.
var vis = new pv.Panel() .def("i", -1) .width(150) .height(150); vis.add(pv.Bar) .data([1, 1.2, 1.7, 1.5, .7, .2]) .bottom(0) .width(20) .height(function(d) d * 80) .left(function() this.index * 25) .fillStyle(function() vis.i() == this.index ? "orange" : "steelblue") .event("mouseover", function() vis.i(this.index)) .event("mouseout", function() vis.i(-1)) .anchor("top").add(pv.Label) .visible(function() vis.i() >= 0) .textStyle("white"); vis.render();
This example is discussed in more detail in the local variables documentation.
new pv.Panel() .width(150) .height(150) .add(pv.Panel) .data([1, 1.2, 1.7, 1.5, .7, .2]) .left(function() this.index * 25) .add(pv.Bar) .bottom(0) .width(20) .height(function(d) d * 80) .def("fillStyle", "steelblue") .event("mouseover", function() this.fillStyle("orange")) // override .event("mouseout", function() this.fillStyle(undefined)) // restore .title(function() this.index) .root.render();
new pv.Panel() .width(150) .height(150) .add(pv.Panel) .data([1, 1.2, 1.7, 1.5, .7, .2]) .left(function() this.index * 25) .add(pv.Panel) // group bar and label for redraw .def("active", false) .add(pv.Bar) .bottom(0) .width(20) .height(function(d) d * 80) .fillStyle(function() this.parent.active() ? "orange" : "steelblue") .event("mouseover", function() this.parent.active(true)) .event("mouseout", function() this.parent.active(false)) .anchor("top").add(pv.Label) .visible(function() this.parent.active()) .textStyle("white") .root.render();
Hyperlinks can be implemented using the cursor property and “click” event. For best usability, you should also set the status on “mouseover” and “mouseout” events, as well as a title property for a tooltip. (Perhaps in the future, we'll make an href convenience method that sets all these properties as once.) For example:
new pv.Panel() .width(200) .height(200) .add(pv.Image) .url("http://vis.stanford.edu/protovis/ex/stanford.png") .cursor("pointer") .title("Go to stanford.edu") .event("mouseover", function() self.status = "Go to \"http://stanford.edu\"") .event("mouseout", function() self.status = "") .event("click", function() self.location = "http://stanford.edu") .root.render();
Note that the event handler gets passed the full data stack, like other property functions. So you can compute the target URL from data if needed:
.event("click", function(n) self.location = "http://svn.prefuse.org/flare/trunk/flare/flare/src/" + n.keys.join("/") + ".as")
See Treemaps for a live example.
External controls. This is the simplest type of interaction to support, if everything happens outside of Protovis. User interface elements are created externally, and event handlers on those interface elements trigger changes to the Protovis specification, which is then re-rendered. It may be useful to support additional user interface elements that are not part of the HTML standard, such as sliders. As with time-based animation, efficient update of the visualization based on minimal changes may also be required for fluid interaction (see below).
Example: periodic table of elements, with range sliders to highlight elements with the given atomic radii; crimespotting with external controls to filter by time of day or type of crime; other Schneiderman dynamic query examples.
Tooltips. Another trivial form of interaction, provided that tooltips are supported natively by the rendering platform (true of SVG, VML and Flash). The tooltip text can be defined staticly. Of course, rich tooltips (arbitrary graphics, HTML) may be more difficult to implement.
Example: pie chart showing value on hover.
Scrolling and zooming. While browsers can support native scrolling for large visualizations, there may be other cases where we want to make scrolling part of the visualization; for example if the data set is very large and loaded (or computed) dynamically. Furthermore, browsers do not support zooming (with the exception of the iPhone and primitive text size adjustments), so having a way to zoom to a particular region is needed. Additionally, scrolling and zooming may be data-driven: for example, you might have a map of crimes, and want to resize and re-center the display to show all crimes that fit the current query.
Example: Google Finance-style focus + context, where you see the relative performance of a stock over time in detail for some fixed time period, and performance for the entire history along the bottom, allowing you to select the date range for the focus; ggobi examples.
Mark-driven events. Some forms of interaction are driven by discrete marks. For example, hovering over an individual crime in crimespotting will place an aura around related crimes (crimes of the same type). In the homicide visualization, clicking on a point in a scatterplot allows query relaxation: selecting related crimes by date, then week, then month. In the Job Voyager, clicking on an individual area brings that occupation to focus, hiding other occupations.
As with sliders for external controls, Protovis may also benefit from integrated user interface widgets, such as pop-up menus, or shift-selection.
Example: Job Voyager, clicking on an individual area.
Coordinate-driven events. Other forms of interaction are driven by coordinate spaces. For example, in the homicide visualization, dragging a box across the scatterplot determines a dynamic query (selected range in two dimensions that is used to filter or highlight visual elements).
This will require that we compute the local coordinates of events relative to the parent panel of the associated mark. Note that Protovis doesn't use traditional Cartesian coordinates, but instead margins, so we'll compute left, right, top and bottom for mouse events. Event handlers can then translate this back into abstract coordinates using a scale.
Example: Job Voyager, tooltip varies along x-axis by year (although technically, this may be better considered a mark-driven event, and the implementation of area and line updated accordingly such that title is not a fixed property); homicide visualization, selecting a date or age range on the scatterplot filters points on a map; some sort of linked scatterplot and pie chart, where selecting a range in two dimensions with the scatterplot shows a rollup in the pie chart by a third dimension (encoded with color).
Many of these interactive features are dependent on efficient rendering: efficient re-evaluation of properties, and efficent re-application of evaluated properties to the display (scene). We can treat these as partially-separable problems.
Because properties can be specified as functions rather than constants, we can't know a priori which functions need to be re-evaluated—unless we want to get into parsing the code itself and looking for data dependencies, which is hard. So how will Protovis know which properties need recomputing? The user might designate some properties as “volatile” so that Protovis knows to recompute them even if their definitions haven't changed. (Constants can never be volatile.) Or, we could assume functions are volatile, and have a way of marking functions as “cached” so they aren't recomputed.
Or, we could force users to dirty properties explicitly even if they haven't changed, so that Protovis knows to recompute them. Different transitions may make different properties dirty, so this may be a more precise solution than declaring properties to be volatile.
Or, we could assume functions are only dependent on data, and recompute functions if the associated data has changed. Though, in many cases the data itself is computed as a function (e.g., panel recursion) so this could trigger unnecessary recomputations if only a small part of the data changes.
A related optimization is tracking with properties are functions and which are constants, such that the overhead of function invocation can be avoided for constant properties. This has already been implemented.
Given a new scene graph, the next difficulty is efficiently updating the display to match the new scene graph. First, we must identify the parts of the scene graph that have changed and need to be updated. If we mark parts of the scene graph as dirty, or issue update requests on parts of the scene graph (rather than the entire thing), we can be more efficient about updating. The level of specificity might be a mark (all of its instances and children), a mark instance, or individual properties on a given mark instance.
However, incremental updates of the scene graph are tricky if the changes to the scene graph are structrual: i.e., if new SVG elements need to be created, removed, or re-ordered. New SVG elements may even be needed with non-structural changes to the scene graph, as in the case of a new title attribute (requiring an a element) or fill on a panel (requiring a rect element).
A further difficulty is inserting SVG elements into the DOM at the correct location. Determining this location requires careful bookkeeping, especially since a panel may reuse the g element from the parent panel if no transform is needed. It is also not as simple as looking at the previous or next mark in the containing panel: a new mark may have been added to the panel, and may not have siblings, or those siblings may be invisible. Also consider the reverse property (and potentially a future zIndex property), which may require re-ordering SVG elements, even if the scene graph changes are not structural.
Regenerating the entire SVG tree on re-render is not only inefficient, but may also interfere with event handling. For example, if a render is triggered from a mouseover event, the element that triggered the event will be removed from the DOM (potentially replaced with an identical element), and may fire a spurious mouseout event (unconfirmed). Similarly if an event handler triggers a manipulation of an SVG element, such as changing the fill color, a re-render will cause the new fill color to be discarded when the SVG tree is recreated.
Structural changes to scene graphs (and the SVG tree) should be comparatively rare
The ideal API for manipulating the visualization specification from an event handler is as yet unclear. The current (2.6) behavior allows evaluated properties to be reassigned from an event handler. This allows for concise proof-of-concept demonstrations of interactivity, but suffers from a number of drawbacks:
1. Implied properties are not updated correctly. For example, if the endAngle property of a wedge is modified, the angle property will not be updated accordingly: the angle property is non-null because it was previously computed as an implied property, and we no longer know it's implied. (Admittedly, an easy fix to this problem is to track more explicitly which properties are implied, and update them accordingly.)
2. Undoing temporary manipulations is tedious. For example, if I want to change the fill color of a bar on mouse over, I have to squirrel away the previous fill color so I can restore it on mouse out. The more complex my temporary manipulation, the more bookkeeping required to undo it, making drift (visual discrepancies) more likely.
3. It overloads the behavior of property methods (again), while limiting the types of changes that can be made from event handlers. Inside an event handler, calling fillStyle("red") doesn't redefine the fillStyle property to the constant "red", as it does elsewhere; it simply overrides the evaluated property for the mark instance that is associated with the event. This is concise by confusing.
4. Furthermore, if you wanted to redefine a mark property in an event handler, you couldn't—short of setting the internal $fillStyle, which is a hack. This is true even if your event handler calls another function to redefine the visualization. However, it's only true of the mark that generated the event; the other marks don't have an index property set, so their property method behavior is unchanged. Talk about confusing!
One alternative may be to allow the specification of property overrides from an event handler (e.g., mouseover). These overrides can then easily be removed from another event handlers (e.g., mouseout).