block by mbostock e1192fe405703d8321a5187350910e08

Local Variables

Full Screen

It’s often desirable when using D3 to define local behavior, that is, behavior that is specific to an individual element, rather than the same for all elements in a selection. The simplest example of this is passing a function to selection.attr to compute an attribute value for each element.

But what happens if your local behavior is more complicated? What if you want multiple operations (multiple attributes, or elements) to have local behavior, but still share local state between them? For instance, when rendering small multiples of time-series data, you might want the same x-scale for all charts but distinct y-scales to compare the relative (not absolute) performance of each metric.

There are several ways to do this in D3:

  1. Make the y-scale global, but set the domain on the y-scale before use. (Example.)

  2. Use selection.each to create a local context which defines the y-scale. (Example.)

  3. Make the y-scale part of the data. (Example.)

Sadly, none of these approaches are elegant. D3 encourages you to write code that renders all the charts simultaneously, whereby you only have access to in-scope (such as global) variables and the data bound to each element. This structure is typically helpful, by reducing control flow and forcing simpler code, but causes friction here.

Approach 1 is error-prone because you must remember to set the y-scale’s domain before using it; if you don’t, you’ll get the wrong y-values. Approach 2 is cleaner, but the local state is discarded after selection.each returns, so it must be recreated from scratch to update the chart with new data or on interaction. Approach 3 is arguably the most robust, but it’s a little icky to decorate the data. And in more complex cases, it can be awkward to access local state defined by an ancestor (e.g., this.parentNode.__data__).

D3 4.0 introduces a new approach, inspired partly by ES6 Symbols. You can now declare a local variable, similar to a standard var, but whose value is scoped by the DOM. Like so:

var foo = d3.local();

On set, the value is stored on the given element:

foo.set(element, value);

On get, the value is retrieved from given element, or the nearest ancestor that defines it:

var value = foo.get(element);