Julien
Julien

Reputation: 992

problems creating a d3.js timer

I have created an animation based on the times of Olympic athletes skiing. I've converted the times into milliseconds to create the animation duration like so:

.transition()
        .duration(function(d){
          return d.time_miliseconds / 4
        })
        .attr("cx", 720)

I would now like to show the time passing by in the animation. Something like this: http://www.wsj.com/graphics/2018-winter-olympics-art-of-the-millisecond/ Where on the top left the time is passing by while the animation is happening.

I'm not entirely sure how to create this timer. Here I've created a getTimeData function but am not sure how to implement it or if this is the right approach:

function getTimeData(){
              var now = new Date;
              var milliseconds = now.getMilliseconds()
              var seconds = now.getSeconds()
              var minutes = now.getMinutes()
              return {"ms": milliseconds, "seconds": seconds, "minutes": minutes}
            }

you can view all of my code on my bl.ock: https://bl.ocks.org/JulienAssouline/256a51554899b8619ba7918590a569f1

To clarify I would like to show the times in Minutes, Seconds, and Milliseconds.

Upvotes: 3

Views: 490

Answers (2)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

An alternative to the excellent Andrew's answer is getting the maximum time...

var maxTime = d3.max(data, function(d) {
    return d.time_miliseconds
});

... and using a attrTween to print the text element:

textSelection.transition()
    .duration(maxTime / 4)
    .attrTween("text", function(d) {
        var that = this
        var i = d3.interpolateNumber(0, maxTime);
        return function(t) {
            return d3.select(that).text("Time: " + i(t));
        }
    });

Pay attention to the fact that, in this approach, the selected d3.ease changes the rate of the text change.

Here is your modified bl.ocks: https://bl.ocks.org/anonymous/7bdaf9f1a6a0123f83229a92b42460a0/3cfd14c76080ac2461ab91ce98a3bbe253ba8765

Upvotes: 2

Andrew Reid
Andrew Reid

Reputation: 38151

You could start a timer (in this case I'm using d3.timer) when the transitions start and stop it upon transition end. This timer can be used to show the elapsed time.

Multiple Transitions

D3 transitions include the ability to listen for transition start and end events. However, when applying a transition on a selection of multiple elements, you are actually creating multiple transitions - one for each element in the selection. Consequently, these listeners trigger for every element that is being transitioned. This is less of a problem in initiating a transition if everything starts at the same time: you can call a function to start timing as many times as you want with little effect. This is a problem if transitions occur over different durations: you have to wait until the last transition finishes before calling a function to stop timing.

One possible option to detecting the end of multiple transitions is using a counter to keep track of how many transitions have finished:

var counter = 0;
selection.transition()
  .on("end", function() { 
    counter++; 
    if (counter == selection.size()) { // everything is done }
  })

One other consideration is that multiple transitions won't start at the same time, even if scheduled to start immediately - the browser will start each in quick succession but not simultaneously if dealing with milliseconds.

d3 Timer & Precision

It's relatively straightforward to get the timer going,

var timer = d3.timer(function(t) { // callback function
  if (t > 5000) timer.stop();  // run for 5 seconds
  d3.select("p").html(t);       // update some text
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<p></p>

However, keep in mind that the timer does not run continuously, it runs repeatedly. It does not trigger the callback each possible moment - that would be an infinite number of operations - but rather as quickly as possible, which will vary depending on circumstance. As noted by the d3 documentation, d3.timer:

Schedules a new timer, invoking the specified callback repeatedly until the timer is stopped. (link)

This is why it is possible for the timer to exceed the total time as the above snippet.

Example

Multiple transitions will begin at slightly different times - normally these may be imperceptible, but with a timer indicating when everything is done, transitioning multiple elements may result in a wonky time being displayed.

Further, even a single element being transitioned will still result in repeated, not continuous evaluation of the transition, so you could have reported times that are over or under the scheduled max duration that you have set.

The example below should demonstrate this, the transitions are scheduled to end after 10 seconds (10 elements, with duration = i*1000+1000, with max i being 9), but the drawn time will likely not align with that to a high degree of precision when dealing with milliseconds:

var width = 600;
var height = 500;

var svg = d3.select("body").append("svg")
  .attr("width",width)
  .attr("height",height);
  
var text = svg.append("text")
  .attr("x", 50)
  .attr("y", 40);
  
var circles = svg.selectAll("circle")
  .data(d3.range(10))
  .enter()
  .append("circle")
  .attr("cx", 30)
  .attr("cy", function(d) { return d * 15 + 45; })
  .attr("r",6);
  
var completed = 0;
var n = circles.size(); 

circles.transition()
  .on("start", function(d,i) { if(i ==0) startTimer();  })
  .on("end", function() { completed++ })
  .attr("cx", width-30)
  .duration(function(d) { return d*1000+1000; })

var format = d3.timeFormat("%S.%L");

function startTimer() {
  var timer = d3.timer(function(t) { 
    if (completed >= n) timer.stop();
    text.text("milliseconds:" + format(t));
  });
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

Precision

The computer won't get milliseconds right necessarily, depending on how you scale data this might be a problem - if you are slowing things down, then you can round this error out easily. If you are showing things in real time, perhaps not.

If showing the transitions in realtime, then the millisecond difference visually is imperceptible, the only issue is that the timer shows a different elapsed time than what is intended. To solve this we can use a slightly different approach:

  • Set a timer to begin on the first transition (as above)
  • Stop the timer when the maximum duration has elapsed (pre-cacluate)
  • Set the text showing the time to the max duration if the timer has stopped, so that it tells you the max duration, not when it actually stopped:

var width = 600;
var height = 300;

var svg = d3.select("body").append("svg")
  .attr("width",width)
  .attr("height",height);
  
var text = svg.append("text")
  .attr("x", 50)
  .attr("y", 40);
  
var circles = svg.selectAll("circle")
  .data(d3.range(10))
  .enter()
  .append("circle")
  .attr("cx", 30)
  .attr("cy", function(d) { return d * 15 + 45; })
  .attr("r",6);
  
var maxTime = d3.max(circles.data(), function(d) {
  return d * 1000 + 1000;  
})

circles.transition()
  .on("start", function(d,i) { if(i==0) startTimer() })
  .attr("cx", width-30)
  .duration(function(d) { return d*1000+1000; })

var format = d3.timeFormat("%S.%L");

function startTimer() {
  var timer = d3.timer(function(t) { 
    if (t > maxTime) {
      timer.stop();
      text.text("milliseconds" + format(maxTime));  // ensure final time is right.
    }
    else {
      text.text("milliseconds:" + format(t));      
    }
  });
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

To format the numbers, I'd suggest that you just use a d3 number format. The above two snippets (not the first one) use a basic format, but this is easy to adapt.

Upvotes: 2

Related Questions