Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tweening numbers in D3 v4 not working anymore like in v3

I'm trying to figure out why my tweening numbers (counting up or down) code in Version 4 of D3 doesn't function any more.

Here is my code:

var pieText = svg4.append("text")
    .attr("class", "pieLabel")
    .attr("x", 0)
    .attr("y", 0)
    .text(0)
    .attr("dy", "0.2em")
    .style("font-size", 19)
    .style("fill", "#46596b")
    .attr("text-anchor", "middle");

d3.selectAll(".pieLabel").transition()
  .delay(500)
  .duration(1000)
  .tween("text", function(d) {
    var i = d3.interpolate(this.textContent, d.value);
    return function(t) {
      this.textContent = form(i(t));
    };
  });

The console.log tells me that the interpolation works fine. So what has changed? And how do I get it to work?

Thanks for your help.

like image 556
NickJan Avatar asked Aug 23 '17 06:08

NickJan


2 Answers

The problem here is just this inside the inner function, which will no longer work as it worked in v3.

Let's prove it. Have a look at the console here, using D3 v3, this is the DOM element:

d3.select("p").transition()
  .tween("foo", function(d) {
    var i = d3.interpolate(0, 1);
    return function(t) {
      console.log(this)
    };
  });
<script src="https://getfirebug.com/firebug-lite-debug.js"></script>
<script src="https://d3js.org/d3.v3.min.js"></script>
<p></p>

Now the same snippet, using D3 v4... this is now the window object:

d3.select("p").transition()
  .tween("foo", function(d) {
    var i = d3.interpolate(0, 1);
    return function(t) {
      console.log(this)
    };
  });
<script src="https://getfirebug.com/firebug-lite-debug.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<p></p>

Solution: keep the reference to this in the outer function as a variable (traditionally named self, but here I'll name it node):

d3.selectAll(".pieLabel").transition()
    .delay(500)
    .duration(1000)
    .tween("text", function(d) {
        var node = this;
        //keep a reference to 'this'
        var i = d3.interpolate(node.textContent, d.value);
        return function(t) {
            node.textContent = form(i(t));
            //use that reference in the inner function
        };
    });

Here is your code with that change only:

var widthpie = 250,
  heightpie = 300,
  radius = Math.min(widthpie, heightpie) / 2;

var data = [{
  antwort: "A",
  value: 0.5
}, {
  antwort: "B",
  value: 0.4
}];

var form = d3.format(",%");

var body4 = d3.select("#chart1");

var svg4 = body4.selectAll("svg.Pie")
  .data(data)
  .enter()
  .append("svg")
  .attr("width", widthpie)
  .attr("height", heightpie)
  .append("g")
  .attr("transform", "translate(" + widthpie / 2 + "," + heightpie / 2 + ")");

var pieText = svg4.append("text")
  .attr("class", "pieLabel")
  .attr("x", 0)
  .attr("y", 0)
  .text(0)
  .attr("dy", "0.2em")
  .style("font-size", 19)
  .style("fill", "#46596b")
  .attr("text-anchor", "middle");

d3.selectAll(".pieLabel").transition()
  .delay(500)
  .duration(1000)
  .tween("text", function(d) {
    var node = this;
    var i = d3.interpolate(node.textContent, d.value);
    return function(t) {
      node.textContent = form(i(t));
    };
  });
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="chart1"></div>

PS: I'm using Firebug in the snippets because S.O. snippet don't log the window object correctly.

like image 149
Gerardo Furtado Avatar answered Oct 22 '22 22:10

Gerardo Furtado


The problem is caused by a change which is not documented in the changelog. D3 v3 calls the inner function returned by the callback passed to transition.tween() on every tick passing the current node as the this context:

tweens[--n].call(node, e);

As of v4, however, this is no longer the case when the inner function is called:

tween[i].call(null, t);

Passing null to that function will be replaced with the global object. In this case this in your function no longer points to current element as was true for v3. The remedy was already been laid out by Gerardo in his answer and is even suggested by the documentation on v4:

transition.tween("attr.fill", function() {
  var node = this, i = d3.interpolateRgb(node.getAttribute("fill"), "blue");
  return function(t) {
    node.setAttribute("fill", i(t));
  };
});

Hence, you need a closure to keep a reference to the current element as it is referenced as this in the callback provided to transition.tween().

like image 40
altocumulus Avatar answered Oct 22 '22 23:10

altocumulus