Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Single on event for multiple transitions

Tags:

d3.js

I'm modifying this example to have multiple lines and updating it to use v4. The code I wrote works, but the way my update function recurses is particularly inelegant:

function repeat() {
  paths.each(d => d.push(random()));
  paths.attr("d", line)
      .attr("transform", null)
    .transition()
      .attr("transform", "translate(" + x(0) + ")")
      .duration(750)
      .ease(d3.easeLinear);
  paths.each(d => d.shift());
  d3.select({}).transition().duration(750).on('end', repeat);
}

Note the transition on an empty selection just to create a 750 ms timeout between calls. I wanted to do this instead:

function repeat() {
  paths.each(d => d.push(random()));
  paths.attr("d", line)
      .attr("transform", null)
    .transition()
      .attr("transform", "translate(" + x(0) + ")")
      .duration(750)
      .ease(d3.easeLinear)
      .on('end', repeat);
  paths.each(d => d.shift());
}

However, this results in multiple concurrent calls to repeat, one for each element in the selection.

Is there a way I can simplify my code to get rid of the extra transition? It seems like there must be a cleaner approach.

like image 485
Altay_H Avatar asked Mar 06 '23 22:03

Altay_H


1 Answers

D3-transitions can be a bit tricky because the end event triggers at the end of each element's transition. There is no transition.on("endAll", ..., so since you don't want to trigger the repeat function twice at the end of the transition you are using an empty transition to delay calling repeat.

You could use a counter to see when the last transition is done, but this too is less clean. Ultimately, you will get less elegant code when managing multiple events with transitions that act on elements individually.

Instead, since each element's timer and transition events for a d3-transition are specific to that element (even if you set them as a group, each element has it's own timer and events), create a repeat function that is specific to each element:

function scroll(d) {
   d.push(Math.random());

   // Transition
   d3.select(this).attr("transform",null)
     .attr("d",line)
     .transition()
      .attr("transform", "translate(" + x(0) + ")")
      .duration(750) 
      .ease(d3.easeLinear)  
      .on("end",scroll);  // repeat

   d.shift();
}

And call with selection.each(scroll); Now we are managing each transition (which handles each item in a selection separately) with a function that handles one item in the selection at a time.

This allows you to:

  • drop the empty transition,
  • access d directly if you call this function with .each() (instead of using paths.each(... twice in the function),

Other considerations:

  • you can apply this function to any group of paths you have a selection for without modifying the function itself.

  • the element being transitioned is this

Here it is at work:

let n = 40;
let random = d3.randomUniform(-1, 1);

let data = [d3.range(n).map(random), d3.range(n).map(random)];

let margin = {top: 6, right: 0, bottom: 6, left: 40};
let width = 960 - margin.right;
let height = 120 - margin.top - margin.bottom;

let x = d3.scaleLinear()
    .domain([1, n - 2])
    .range([0, width]);

let y = d3.scaleLinear()
    .domain([-1, 1])
    .range([height, 0]);

let line = d3.line()
    .x(function(d, i) { return x(i); })
    .y(function(d, i) { return y(d); })
    .curve(d3.curveBasis);

let svg = d3.select("body").append("p").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .style("margin-left", -margin.left + "px")
  .append("g")
  	.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

svg.append("defs").append("clipPath")
    .attr("id", "clip")
  .append("rect")
    .attr("width", width)
    .attr("height", height);

svg.append("g")
    .attr("class", "y axis")
    .call(d3.axisLeft(y).ticks(5));

let paths = svg.append('g')
    .attr('id', 'lines')
    .attr('clip-path', 'url(#clip)')
  .selectAll('path').data(data).enter()
  .append('path')
    .attr('class', 'line')
    .attr('stroke', (d, i) => d3.schemeCategory10[i])
    .attr('d', line)
	.each(scroll);
	
	
function scroll(d) {
   d.push(random());
          
   d3.select(this).attr("transform",null)
     .attr("d",line)
     .transition()
       .attr("transform", "translate(" + x(0) + ")")
       .duration(750)
       .ease(d3.easeLinear)  
       .on("end",scroll);

  d.shift();
}
#lines {
    fill: none;
    stroke: black;
    stroke-width: 1.5px;
}
.y.axis path {
    stroke: black;
}
p {
    padding: 40px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
like image 88
Andrew Reid Avatar answered Apr 19 '23 22:04

Andrew Reid