Misha Moroshko
Misha Moroshko

Reputation: 171321

How to use transition with timer in d3?

I'm trying to animate (with transition) an item entering a realtime chart.

As you can see, when the "Add circle" button is clicked, the circle stays at the bottom of the chart, and not animating upwards.

Interestingly, adding .ease(d3.easeLinear) to the transition solves the issue.

Why?

Is it conceptually wrong to use transition with timer in d3?

const width = 300;
const height = 100;
const margin = { top: 0, right: 30, bottom: 30, left: 50 };

const main = d3.select('.chart')
  .append('svg')
    .attr('width', width + margin.left + margin.right)
    .attr('height', height + margin.top + margin.bottom)
  .append('g')
    .attr('transform', `translate(${margin.left}, ${margin.top})`);

const now = Date.now();
const xScale = d3.scaleTime().domain([now - 10000, now]).range([0, width]);
const xAxis = d3.axisBottom(xScale).ticks(5);

const xAxisG = main.append('g')
  .attr('transform', `translate(0, ${height})`)
  .call(xAxis);

let data = [];

function drawCircles() {
  const t = d3.transition().duration(1000); //.ease(d3.easeLinear);
  const update = main.selectAll('circle').data(data);
  
  update.enter()
      .append('circle')
      .attr('r', d => d.value)
    .merge(update)
      .attr('cx', d => xScale(d.timestamp))
      .attr('cy', height)
      .transition(t)
      .attr('cy', height / 2);
}

d3.timer(() => {
  const now = Date.now();

  xScale.domain([now - 10000, now]);
  xAxisG.call(xAxis);
  
  drawCircles();
});

d3.select('.add-circle').on('click', () => {
  data.push({
    timestamp: Date.now() - 3000,
    value: 10
  });
});
svg {
  margin: 30px;
}
.add-circle {
  margin-left: 200px;
}
<script src="https://unpkg.com/[email protected]/build/d3.js"></script>
<button class="add-circle">Add circle</button>
<div class="chart"></div>

Upvotes: 1

Views: 1072

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

The problem here is not the lack of an easing. The problem is most probably the mix of d3.timer and selection.transition.

Let's see it step by step. You said that this:

const t = d3.transition().duration(1000);

Doesn't work, while this:

const t = d3.transition().duration(1000).ease(d3.easeLinear);

Works. Well, that's not the fact: it's not working. Whoever uses easeLinear can easily see that this is not a linear transition! Something funny is happening here...

Now, let's change the easing. The default easing (i.e., if you don't set any) is easeCubic. Therefore, if we do this:

const t = d3.transition().duration(1000).ease(d3.easeCubic);

It will have the same effect of your first code, without ease. Thus, it's not the fact that we set an ease that made the transition happen.

I tested all easings with your code, and some of them "worked", like easeLinear or easeCircle. The worked here is between quotes because the transition was not the normal one, the expected one. Some of them, like easeCubic, had no effect at all.

And that's because you can't mix d3.timer with transitions. I didn't have the chance to have a look at the source code, but most probably they use the same methods (like requestAnimationFrame() perhaps) and are conflicting.

Solution: move the transition to outside the function called by your d3.timer.

There are different ways for implementing that proposed solution (different coders will create different alternatives), but this is mine. First, we define a transitioning function:

function transitioning(elem) {
    const t = d3.transition().duration(1000).ease(d3.easeCubic);
    d3.select(elem)
        .transition(t)
        .attr('cy', height / 2);
}

And then, inside your function drawCircles, we call transitioning for each circle. However, we have to set a flag, so we don't call the transitioning function again and again to the same element:

function drawCircles() {

    const update = main.selectAll('circle').data(data);

    update.enter()
        .append('circle')
        .attr('r', d => d.value)
        .attr('cy', height)
        .attr("flag", 0)
        .merge(update)
        .attr('cx', d => xScale(d.timestamp))
        .each(function() {
            if (!(+d3.select(this).attr("flag"))) {
                transitioning(this)
            }
            d3.select(this).attr("flag", 1);
        });
}

And this is your updated CodePen: http://codepen.io/anon/pen/ZLyXma?editors=0010

In the above CodePen, I'm using easeCubic. But you can see, in this other Pen, that it works without setting any easing: http://codepen.io/anon/pen/apwLxR?editors=0010

Upvotes: 2

Related Questions