A graphical toolkit for visualization
Protovis
Overview
Examples
Documentation
Download
Index

Local Variables

Description

From the API reference for def:

def(name, value): Defines a local variable on this mark. Local variables are initialized once per mark (i.e., per parent panel instance), and can be used to store local state for the mark. Here are a few reasons you might want to use def:

1. To store local state. For example, say you were visualizing employment statistics, and your root panel had an array of occupations. In a child panel, you might want to initialize a local scale, and reference it from a property function:

.def("y", function(d) pv.Scale.linear(0, pv.max(d.values)).range(0, h))
.height(function(d) this.y()(d))

In this example, this.y() returns the defined local scale. We then invoke the scale function, passing in the datum, to compute the height. Note that defs are similar to fixed properties: they are only evaluated once per parent panel, and this.y() returns a function, rather than automatically evaluating this function as a property.

2. To store temporary state for interaction. Say you have an array of bars, and you want to color the bar differently if the mouse is over it. Use def to define a local variable, and event handlers to override this variable interactively:

.def("i", -1)
.event("mouseover", function() this.i(this.index))
.event("mouseout", function() this.i(-1))
.fillStyle(function() this.i() == this.index ? "red" : "blue")

Notice that this.i() can be used both to set the value of i (when an argument is specified), and to get the value of i (when no arguments are specified). In this way, it's like other property methods.

3. To specify fixed properties efficiently. Sometimes, the value of a property may be locally a constant, but dependent on parent panel data which is variable. In this scenario, you can use def to define a property; it will only get computed once per mark, rather than once per datum.

Example: Interaction

new pv.Panel()
    .width(150)
    .height(150)
  .add(pv.Bar)
    .def("i", -1)
    .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() this.i() == this.index ? "red" : "black")
    .event("mouseover", function() this.i(this.index))
    .event("mouseout", function() this.i(-1))
    .title(function() this.index)
  .root.render();

In this example, the local variable, or “def”, is named i. It stores the index of the mark under the mouse. Breaking it down:

  1. The local i is declared with an initial constant value of -1: def("i", -1). The value of -1 indicates that the mouse is not over any of the bars. The second argument to def is optional; it specifies an initializer, which can either be a function or a constant. If a function, it is passed data arguments like other property functions.
  2. The local i is dereferenced for the fillStyle property: if its value matches the index, the bar is colored red, and if not, black.
  3. Two events are handled: “mouseover”, which sets i to the current index, and “mouseout”, which clears the value of i.
  4. The return value of the event handlers is this, which is a reference to the bar. That causes the bar to be re-rendered (and recolored!) after the event is handled.

When you declare a def, a method gets added to the mark, much like a property. It behaves similarly to property methods: calling the method with no arguments (in this example, this.i()) _gets_ its value, while calling the method with an argument _sets_ the value.

For the purpose of comparison, what would happen without a def?

new pv.Panel()
    .width(150)
    .height(150)
  .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("black")
    .event("mouseover", function() this.fillStyle("red"))
    .event("mouseout", function() this.fillStyle("black"))
    .title(function() this.index)
  .root.render();

What To Avoid: Partial Redraw

There’s another trick you can use to achieve this recoloring effect. But, it is strongly discouraged because it takes advantage of how the rendering system currently detects which part of the tree to rebuild. It is included for explanatory purposes but this is an anti-pattern not to be copied.

The trick is make the bar a singleton, and replicate a new parent panel instead:

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)
    .fillStyle("black")
    .event("mouseover", function() this.fillStyle("red")) // DANGER!
    .event("mouseout", function() this.fillStyle("black")) // DANGER!
    .title(function() this.index)
  .root.render();

It works because the event handler returns a reference to the bar whose fill style was changed (but not the parent panel). That bar is then redrawn, but—importantly—the other copies of that bar in other panels are not redrawn. If the rendering engine knew all of the dependencies between properties, and could be smarter about which part of the tree to re-evaluate, this hack would break. So don't do it.

Yet another reason to avoid this strategy is that it’s more stateful and less declarative. For example, if you wanted to use this hack to change multiple properties on mouseover, you have to be careful to exactly undo those changes on mouseout. On the other hand, if you use a local variable (such as i, above) to store the index of the active mark, you can define all the properties declaratively, and there's never an issue of crufty state.

The Right Way: Local Changes

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", "black")
    .event("mouseover", function() this.fillStyle("red")) // override
    .event("mouseout", function() this.fillStyle(undefined)) // restore
    .title(function() this.index)
  .root.render();
Copyright 2010 Stanford Visualization Group