Using D3’s .timer Method

      Before we take a look at how to use D3’s .timer method let’s first take a look why we should even bother to do so when we could be using JavaScript’s built in setInterval() method. In order to do so let’s take a look at D3’s api and their explanation of D3’s timers:

      “D3 internally maintains an efficient timer queue so that thousands of timers can be processed concurrently with minimal overhead; in addition, this timer queue guarantees consistent timing of animations when concurrent or staged transitions are scheduled. If your browser supports it, the timer queue will use requestAnimationFrame for fluid and efficient animation. The timer queue is also smart about using setTimeout when there is a long delay before the next scheduled event.”

      Now you might be asking yourself why it would make a difference whether you are using the timer used by the browser of if you are using D3’s timer. In many cases there would be no difference, but the difference begins when you start using one of D3’s several transition methods. These transition methods area all run on D3’s timer and if you want to be running another function concurrently (let’s say a collision detection) with the one you are calling on your transition method, in order to have them consistently on the same timer D3’s .timer() method would be a far better choice then .setInterval().

       Let’s look at what the D3’s timer actually does, “Start a custom animation timer, invoking the specified function repeatedly until it returns true.” It is important to realize here that all the functions called with a D3 timer will be running concurrently. Now this sets up a very interesting issue when working with functions inside the D3 timer. Let’s look at two very similar snippets of code. The first one will not work while the second one will. See if you can spot the differences:

var w = 1200;
var h = 550;
var enemySet = [];
var enemyCount = 20;
var radius = 20;
var currentScore = 0;
var highScore = 0;
var collisionCount = 0;
var mouse;

for (var x=0;x<enemyCount; x++) {
  enemySet.push(radius);
}

var svg = d3.select("body").append("svg").attr("width", w).attr("height", h);

svg.on('mousemove', function() {
  var loc = d3.mouse(this);
  mouse = {x: loc[0], y: loc[1]};
  d3.select('.hero').attr('cx', mouse.x).attr('cy', mouse.y);
});

var enemies = svg.selectAll("circle")
  .data(enemySet)
  .enter()
  .append("circle")
  .attr("class", "enemy");

var hero = svg.selectAll("circle.hero").data([1]).enter().append("circle").attr('class','hero').attr("cx", w-100)
  .attr("cy", h-100)
  .attr("r", 20)
  .style('fill', 'red');

var update = function (enemies) {
  enemies.transition().duration(1500).ease("linear").attr("cx", function() {
    return (Math.floor(Math.random() * w) + 20);
  })
    .attr("cy", function() {
      return (Math.floor(Math.random() * h) + 20);
    })
    .attr("r", function(d) {
      return d;
    }).each('end', function(){
      update( d3.select(this));
    });
};

update(enemies);

var collisionTest = false;
var collisionDetection = function() {
  var collision= false;
  enemies.each(function() {
    var cx = parseInt(this.cx.animVal.value) + radius;
    var cy = parseInt(this.cy.animVal.value) + radius;
    var xAxis = cx - mouse.x;
    var yAxis = cy - mouse.y;

    if (Math.sqrt(xAxis*xAxis + yAxis*yAxis) < radius*2) {
      collision = true;
      currentScore = 0;
    }
  });

  if (collision) {
    currentScore = 0;
    if (collision !== collisionTest) {
      collisionCount++;
      if (collisionCount > 5) {
        alert("You lose!");
        collisionCount = 0;
      }
    }
  }
  collisionTest = collision;
};

d3.timer(collisionDetection);

var w = 1200;
var h = 550;
var enemySet = [];
var enemyCount = 20;
var radius = 20;
var currentScore = 0;
var highScore = 0;
var collisionCount = 0;
var mouse = {x: 500, y: 500};

for (var x=0;x<enemyCount; x++) {
  enemySet.push(radius);
}

var svg = d3.select("body").append("svg").attr("width", w).attr("height", h);

svg.on('mousemove', function() {
  var loc = d3.mouse(this);
  mouse = {x: loc[0], y: loc[1]};
  d3.select('.hero').attr('cx', mouse.x).attr('cy', mouse.y);
});

var enemies = svg.selectAll("circle")
  .data(enemySet)
  .enter()
  .append("circle")
  .attr("class", "enemy")
  .attr("cx", 200)
  .attr("cy", 200);


var hero = svg.selectAll("circle.hero").data([1]).enter().append("circle").attr('class','hero').attr("cx", w-100)
  .attr("cy", h-100)
  .attr("r", 20)
  .style('fill', 'red');

var update = function (enemies) {
  enemies.transition().duration(1500).ease("linear").attr("cx", function() {
    return (Math.floor(Math.random() * w) + 20);
  })
    .attr("cy", function() {
      return (Math.floor(Math.random() * h) + 20);
    })
    .attr("r", function(d) {
      return d;
    }).each('end', function(){
      update( d3.select(this));
    });
};

update(enemies);

var collisionTest = false;
var collisionDetection = function() {
  var collision= false;
  enemies.each(function() {
    var cx = parseInt(this.cx.animVal.value) + radius;
    var cy = parseInt(this.cy.animVal.value) + radius;
    var xAxis = cx - mouse.x;
    var yAxis = cy - mouse.y;

    if (Math.sqrt(xAxis*xAxis + yAxis*yAxis) < radius*2) {
      collision = true;
      currentScore = 0;
    }
  });

  if (collision) {
    currentScore = 0;
    if (collision !== collisionTest) {
      collisionCount++;
      if (collisionCount > 5) {
        alert("You lose!");
        collisionCount = 0;
      }
    }
  }
  collisionTest = collision;
};

d3.timer(collisionDetection);

      These two snippets of code are a bit longer than usual, but I wanted to demonstrate how a couple of small changes can completely break the code. The way D3’s library was built, it is incredibly hard to debug since the code will crash on the D3’s side of the library and it’ll give you very little information as to what’s actually going on. The difference between these two snippets is in the instantiation of the elements. All though both snipped have the both the mouse and enemies elements initialized outside of the .timer and .transition methods, their positions are not instantiated until they are called inside the .timer and .transition methods. Now let’s remember that even though they are being placed at different points in our program, the fact that timers “processed concurrently” means that they are getting called at the same time and that at the point that they are called one has not been able to instantiate the object with a position before the other one is called.

      Although this might be hard to follow, the solution for this problem is not, when using D3 methods which make use of D3’s timers make sure that all values being used inside those methods have been instantiated before they are called.

Paulo Diniz