Implementing a custom interpolator

In some rare cases, you might find that the built-in D3 interpolators are not enough to handle your visualization requirement. In such situations, you can choose to implement your own interpolator with specific logic to handle your needs. In this recipe, we will examine this approach and demonstrate some interesting use cases.

Getting Ready

Open your local copy of the following file in your web browser:

https://github.com/NickQiZhu/d3-cookbook/blob/master/src/chapter4/custom-interpolator.html

How to do it...

Let's take a look at two different examples of custom interpolator implementation. In the first example, we will implement a custom function capable of interpolating price in dollars, while in the second one we will implement custom interpolator for alphabets. Here is the code to this implementation:

<div id="dollar" class="clear">
    <span>Custom Dollar Interpolation<br></span>
</div>
<div id="alphabet" class="clear">
    <span>Custom Alphabet Interpolation<br></span>
</div>

<script type="text/javascript">
    d3.interpolators.push(function(a, b) { // <-A
      var re = /^$([0-9,.]+)$/, // <-B
        ma, mb, f = d3.format(",.02f"); 
      if ((ma = re.exec(a)) && (mb = re.exec(b))) { // <-C
        a = parseFloat(ma[1]);
        b = parseFloat(mb[1]) - a;  // <-D
        return function(t) {  // <-E
          return "$" + f(a + b * t); // <-F
        };
      }
    });

    d3.interpolators.push(function(a, b) { // <-G
      var re = /^([a-z])$/, ma, mb; // <-H
      if ((ma = re.exec(a)) && (mb = re.exec(b))) { // <-I
        a = a.charCodeAt(0);
        var delta = a - b.charCodeAt(0); // <-J
        return function(t) { // <-K
          return String.fromCharCode(Math.ceil(a - delta * t));
        };
      }
    });

    var dollarScale = d3.scale.linear()
            .domain([0, 11])
            .range(["$0", "$300"]); // <-L
            
    var alphabetScale = d3.scale.linear()
            .domain([0, 27])
            .range(["a", "z"]); // <-M
        
    function render(scale, selector) {        
        var data = [];
        var max = scale.domain()[1];
        
        for (var i = 0; i < max; ++i) data.push(i);      
        
        d3.select(selector).selectAll("div.cell")
                    .data(data)
                .enter()
                    .append("div")
                        .classed("cell", true)
                    .append("span");

        d3.select(selector).selectAll("div.cell")
                    .data(data)
                .exit().remove();

        d3.select(selector).selectAll("div.cell")
                .data(data)
                .style("display", "inline-block")
                .select("span")
                    .text(function(d,i){return scale(d);}); // <-N
    }

    render(dollarScale, "#dollar");
    render(alphabetScale, "#alphabet");
</script>

The preceding code generates the following visual output:

How to do it...

Custom interpolation

How it works...

The first custom interpolator we encounter in this recipe is defined on line A. The custom interpolator function is a bit more involved, so, let's take a closer look at how it works:

d3.interpolators.push(function(a, b) { // <-A
      var re = /^$([0-9,.]+)$/, // <-B
        ma, mb, f = d3.format(",.02f"); 
      if ((ma = re.exec(a)) && (mb = re.exec(b))) { // <-C
        a = parseFloat(ma[1]);
        b = parseFloat(mb[1]) - a;  // <-D
        return function(t) {  // <-E
          return "$" + f(a + b * t); // <-F
        };
      }
    });

Note

This custom interpolator in the following link was directly extracted from D3 Wiki:

https://github.com/mbostock/d3/wiki/Transitions#wiki-d3_interpolators

On the line A, we push an interpolator function into d3.interpolators. This is a global interpolator registry array that contains all known registered interpolators. By default, this registry contains the following interpolators:

  • Number interpolator
  • String interpolator
  • Color interpolator
  • Object interpolator
  • Array interpolator

Any new custom interpolator implementation can be pushed to the tail of the interpolators array which then becomes globally available. An interpolator function is expected to be a factory function that takes the start of the range (a) and the end of the range (b) as its input parameters while returning an implementation of the interpolate function as seen on line E. You might be wondering how D3 knows which interpolator to use when a certain string value is presented. The key to this lies on line B. Typically we use a variable called re defined as a regex pattern of /^$([0-9,.]+)$/, which is then used to match both parameter a and b for any number with a leading dollar sign. If both parameters match the given pattern then the matching interpolate function is constructed and returned; otherwise D3 will continue iterating through d3.interpolators array to find a suitable interpolator.

Instead of an array, d3.interpolators is actually better considered as a FILO stack (though not exactly implemented as a stack), where new interpolators can be pushed to the top of the stack. When selecting an interpolator, D3 will pop and check each suitable interpolator from the top. Therefore, in this case, the interpolator pushed later in the stack takes precedence.

The anonymous interpolate() function created on line E takes a single parameter t with a value ranging from 0 to 1 indicating how far off the interpolated value is from base a.

return function(t) {  // <-E
          return "$" + f(a + b * t); // <-F
        };

You can think of it as a percentage of how far the desired value has traveled from a to b. With that in mind, it becomes clear that in line F it performs the interpolation and calculates the desired value based on the offset t, which effectively interpolates a price string.

Tip

One thing to watch out for here is that the b parameter's value has been changed on line D from the end of the range to the difference between a and b.

b = parseFloat(mb[1]) - a;  // <-D

This is generally considered a bad practice for readability. So, in your own implementations you should avoid modifying input parameters' value in a function.

On the line G, a second custom interpolator was registered to handle single-character lowercase alphabets from a to z:

d3.interpolators.push(function(a, b) { // <-G
      var re = /^([a-z])$/, ma, mb; // <-H
      if ((ma = re.exec(a)) && (mb = re.exec(b))) { // <-I
        a = a.charCodeAt(0);
        var delta = a - b.charCodeAt(0); // <-J
        return function(t) { // <-K
          return String.fromCharCode(Math.ceil(a - delta * t));
        };
      }
});

We quickly noticed that this interpolator function follows a very similar pattern with the previous one. Firstly, it has a regex pattern defined on line H that matches single lowercase alphabets. After the matching is conducted on line I, the start and end of the range a and b were both converted from character values into integer values. A difference between a and b was calculated on line J. The interpolate function again follows exactly the same formula as the first interpolator as shown on line K.

Once these custom interpolators are registered with D3, we can define scales with corresponding ranges without doing any additional work and we will be able to interpolate their values:

var dollarScale = d3.scale.linear()
        .domain([0, 11])
        .range(["$0", "$300"]); // <-L
            
var alphabetScale = d3.scale.linear()
        .domain([0, 27])
        .range(["a", "z"]); // <-M

As expected, the dollarScale function will automatically use the price interpolator, while the alphabetScale function will use our alphabet interpolator, respectively. No additional work is required when invoking the scale function to obtain the value we need, as demonstrated on line N:

.text(function(d,i){
  return scale(d);} // <-N
); 

In isolation, custom interpolator does not appear to be a very important concept; however, later on when exploring other D3 concepts in Chapter 6, Transition with Style, we will explore more powerful techniques when custom interpolator is combined with other D3 constructs to achieve interesting custom effects.

See also

  • If the regular expression used in this chapter is a new concept or a well-known tool in your toolbox and you need a little bit of dusting, you can find a lot of useful resources at http://www.regular-expressions.info
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset