3

I am trying to create a timeline using D3.js v4. I have successfully created my scatter plot with its axis and a brush to allow users define a specific time period.

I want to allow users to be able to 'play' the timeline just like an audio/video player having an indicator that will move from left to right using a customizable duration. In order to achieve this I've placed a vertical line with a transition to act as the indicator.

My problem is that I cannot retrieve the x-axis coordinates while the transition is running. I want to achieve this because the x-axis values need to interact with another part of the code.

I've tried everything including playing around with tween and attrTween functions but I couldn't make it work. Ideally I would like the indicator to begin and stop within the brush limits.

svg.append("g")
    .attr("class", "brush")
    .call(brush)
    .call(brush.move, x.range());

svg.append('line')
    .attr("class", "timeline-indicator")
    .attr("stroke-width", 2)
    .attr("stroke", "black")
    .attr("x1", 0)
    .attr("y1", 0)
    .attr("x2", 0)
    .attr("y2", height)
    .transition()
        .duration(9000)
        .attr("x1", 500)
        .attr("y1", 0)
        .attr("x2", 500)
        .attr("y2", height);
stelioslogothetis
  • 9,371
  • 3
  • 28
  • 53

2 Answers2

3

Andrew's answer is a nice one and probably the traditional way to do it.

However, just for the sake of curiosity, here is an alternative answer using d3.timer.

The math is, of course, a bit more complicated. Also, have in mind that the elapsed time is apparent:

The exact values may vary depending on your JavaScript runtime and what else your computer is doing. Note that the first elapsed time is 3ms: this is the elapsed time since the timer started, not since the timer was scheduled.

Check the demo:

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

var circle = svg.append("circle")
  .attr("cx", 30)
  .attr("cy", 50)
  .attr("r", 20)
  .attr("fill", "tan")
  .attr("stroke", "dimgray");

var timer = d3.timer(move);

function move(t) {
  if (t > 2000) timer.stop();
  console.log("position now is: " + ~~(30 + (t / 2000) * 440))
  circle.attr("cx", 30 + (t / 2000) * 440)
}
.as-console-wrapper { max-height: 30% !important;}
<script src="https://d3js.org/d3.v4.min.js"></script>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • Ah, I missed this implementation when updating my answer, I briefly (not seeing this answer) had implemented a timer with `transition.on`, keeping transition separate from the timer function which did the actual check on the cx property of the circle. I've since cut my d3.timer implentation out, as this is better use of d3.timer. – Andrew Reid Jun 19 '17 at 23:40
2

You should be able to accomplish this using a tween function in your transition. A tween function will trigger on every tick of the transition and is one way to call a function every tick.

The tween method needs an attribute name (as it is intended to provide custom interpolation for an attribute), but this can be a dummy attribute, or one that isn't changed (as in my example below). Documentation on the method is here.

In my example I pull the x property (well, cx property) of a circle as it moves across the screen, using a tween function:

 .tween("attr.fill", function() {
        var node = this;
        return function(t) { 
         console.log(node.getAttribute("cx"));
        }
      })

Here is a snippet of it at work:

var svg = d3.select("body").append("svg")
  .attr("width",400)
  .attr("height",400);
  
var circle = svg.append("circle")
  .attr("cx",20)
  .attr("cy",20)
  .attr("r",10);
 
circle.transition()
  .attr("cx",380)
  .attr("cy",20)
  .tween("attr.fill", function() {
    var node = this;
    return function(t) { 
     console.log(node.getAttribute("cx"));
    }
  })
  .duration(1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • Thank you so much. It worked! I just had to replace 'cx' with 'x1' and use scaleLinear to convert pixels to my x-axis values. I'll have a look at the d3.timer too. – user3389819 Jun 19 '17 at 23:23
  • @user3389819 I just wrote an answer using `d3.timer`. However, stick with Andrew's solution: it's way easier. – Gerardo Furtado Jun 19 '17 at 23:33