score:2

Accepted answer

So I took some liberties with your code to make it more d3-ish. My attempt here is to make your data drive the visualization by chaining all the transitions instead of hooking the end event and moving to the next index.

<head>
  <!-- Load d3.js -->
  <script src="https://d3js.org/d3.v4.js"></script>
  <style>

  </style>
</head>
<body>
  <button onclick="runTheSimulation()">RUN THE SIMULATION</button>
  <div id="container"></div>
  <script>
    var data = [
      {
        time: 0,
        info: [
          { id: 'A', x: 10, y: 20 },
          { id: 'B', x: 40, y: 90 },
          { id: 'C', x: 10, y: 90 },
          { id: 'D', x: 80, y: 70 },
        ],
      },
      {
        time: 0.5,
        info: [
          { id: 'A', x: 20, y: 30 },
          { id: 'B', x: 60, y: 70 },
          { id: 'C', x: 100, y: 10 },
          { id: 'D', x: 10, y: 32 },
        ],
      },
      {
        time: 1,
        info: [
          { id: 'A', x: 50, y: 60 },
          { id: 'B', x: 80, y: 50 },
          { id: 'C', x: 80, y: 10 },
          { id: 'D', x: 50, y: 50 },
        ],
      },
      {
        time: 1.5,
        info: [
          { id: 'A', x: 40, y: 50 },
          { id: 'B', x: 100, y: 30 },
          { id: 'C', x: 80, y: 90 },
          { id: 'D', x: 80, y: 40 },
        ],
      },
      {
        time: 2,
        info: [
          { id: 'A', x: 60, y: 10 },
          { id: 'B', x: 0, y: 50 },
          { id: 'C', x: 10, y: 50 },
          { id: 'D', x: 30, y: 60 },
        ],
      },
    ];

    var margin = { top: 30, right: 30, bottom: 30, left: 30 },
      width = 300 - margin.left - margin.right,
      height = 300 - margin.top - margin.bottom;

    // append the svg object to the body of the page
    var svg = d3
      .select('#container')
      .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 + ')');

    // Add X axis
    var x = d3.scaleLinear().domain([0, 100]).range([0, width]);
    svg
      .append('g')
      .attr('transform', 'translate(0,' + height + ')')
      .call(d3.axisBottom(x));

    // Add Y axis
    var y = d3.scaleLinear().domain([0, 100]).range([height, 0]);
    svg.append('g').call(d3.axisLeft(y));
    
    
    function runTheSimulation() {

        var chainedTransition,
            prevTime = 0,
            elapsedTime = 0;

        // create a null selection which reprensents each timepiont
        svg
          .selectAll(null)
          .data(data)
          .enter()
          .each(function(d,i){

            // for each timepoint, update the data
            points = svg.selectAll('circle')
              .data(d.info, (d) => d.id);

            // handle enter to create circles first time through
            points
              .enter()
              .append('circle')
              .attr('r', 8)
              .attr('fill', 'white')
              .attr('stroke', 'black')
              .attr('cx', d => x(d.x))
              .attr('cy', d => y(d.y));   

            // calculate how long we'll transition 
            // between this timepoint and last
            let transTime = (d.time - prevTime) * 1000;

            // chain the transition for all data updates
            chainedTransition = points
              .transition()
              .duration(transTime)
              .delay(elapsedTime)
              .ease(d3.easeLinear)
              .attr('cx', d => x(d.x))
              .attr('cy', d => y(d.y));

            // rememeber this time
            prevTime = d.time;
            // and elpased time for chaining
            elapsedTime += transTime;

          });
    }
  </script>
</body>

score:2

I believe you'll find this a lot easier if you restructure your data. Instead of having a data array with frames containing each node's position, try a data array containing each node over time, eg:

var data = [ {"id":"A", frames: [{time:0, x: 1},{time:1,x: 2}]}, ...

This allows you to bind the data once, transition each element separately (triggering an end event without having to count how many transitions have occured as you aren't waiting for a batch to finish), and you could even store the current frame on the datum, rather than tracking it separately.

I used the following structure:

enter image description here

Which I got by running your code through a basic transpose, located in the snippet below.

Each item in the data array represents one element in the DOM - consistent with the D3 idiom. The index of current frame being drawn is stored in d.currentFrame, while all frames are stored in d.frames.

Basically your transition functionality would be fairly simple:

  function transition(d) {
  
    // Is there another frame?
    if(d.currentFrame == d.frames.length-1 ) return; // don't keep going if there is no more data.
    
    // Change in time
    var dt = -d.frames[d.currentFrame++].time + d.frames[d.currentFrame].time;

    // Do the transition:
    d3.select(this)
     .transition()
     .duration(dt*1000)
     .attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
     .attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})   
     .on("end", transition) // and repeat for this element
   
  } 

We can trigger the run simulation by resetting the current frame and triggering the transition:

  function runTheSimulation() {
    svg.selectAll("circle")
     .each(function(d) { d.currentFrame = 0; })
     .attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
     .attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
     .each(transition);
  }

Each data point in this case doesn't actually need to have the same steps, either in number or values, which I believe is in keeping with D3's idioim: each datum is separate and independent from the processing of the others: there is no need to wait for a batch of transitions to complete, or use the time periods of the others.

Putting it together:

var data = [{"time": 0, "info":[{"id": "A", "x": 10, "y": 20},
                                                        {"id": "B", "x": 40, "y": 90},
                                    {"id": "C", "x": 10, "y": 90},
                                    {"id": "D", "x": 80, "y": 70}]},
        {"time": 0.5, "info":[{"id": "A", "x": 20, "y": 30},
                                                        {"id": "B", "x": 60, "y": 70},
                            {"id": "C", "x": 100, "y": 10},
                            {"id": "D", "x": 10, "y": 32}]},
        {"time": 1, "info":[{"id": "A", "x": 50, "y": 60},
                           {"id": "B", "x": 80, "y": 50},
                           {"id": "C", "x": 80, "y": 10},
                           {"id": "D", "x": 50, "y": 50}]},
                {"time": 1.5, "info":[{"id": "A", "x": 40, "y": 50},
                           {"id": "B", "x": 100, "y": 30},
                           {"id": "C", "x": 80, "y": 90},
                           {"id": "D", "x": 80, "y": 40}]},
        {"time": 2, "info":[{"id": "A", "x": 60, "y": 10},
                              {"id": "B", "x": 0, "y": 50},
                              {"id": "C", "x": 10, "y": 50},
                              {"id": "D", "x": 30, "y": 60}]}];


// Manipulate array:
var newData = data[0].info.map((_, i) => { return  {currentFrame: 0, id: data[0].info[i].id ,frames:data.map((row,t) => { return { x: row.info[i].x, y: row.info[i].y, time: data[t].time }}) } });

var margin = {top: 30, right: 30, bottom: 30, left: 30},
        width = 300 - margin.left - margin.right,
        height = 300 - margin.top - margin.bottom;

    // append the svg object to the body of the page
    var svg = d3.select("#container")
      .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 + ")");

     // Add X axis
    var x = d3.scaleLinear()
      .domain([0, 100])
      .range([ 0, width ]);
    svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));

    // Add Y axis
    var y = d3.scaleLinear()
      .domain([0, 100])
      .range([ height, 0]);
    svg.append("g")
      .call(d3.axisLeft(y));
      
      
   var points = svg.selectAll('circle')
          .data(newData)
          .enter()
          .append('circle')
            .attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
            .attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
            .attr('r', 8)
            .attr('fill', 'white')
            .attr('stroke', 'black')

  
  function runTheSimulation() {
    svg.selectAll("circle")
     .each(function(d) { d.currentFrame = 0; })
     .attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
     .attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
     .each(transition);
  }
  
  function transition(d) {
  
    // Is there another frame?
    if(d.currentFrame == d.frames.length-1 ) return; // don't keep going if there is no more data.
    
    // Change in time
    var dt = -d.frames[d.currentFrame++].time + d.frames[d.currentFrame].time;

    // Do the transition:
    d3.select(this)
     .transition()
     .duration(dt*1000)
     .attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
     .attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})   
     .on("end", transition)
     
 
  
  } 
body {
  background: #20262E;
  padding: 20px;
  font-family: Helvetica;
}

#app {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  transition: all 0.2s;
}

li {
  margin: 8px 0;
}

h2 {
  font-weight: bold;
  margin-bottom: 15px;
}

.done {
  color: rgba(0, 0, 0, 0.3);
  text-decoration: line-through;
}

input {
  margin-right: 5px;
}
<head>
  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
  <!-- jQuery -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Bootstrap JavaScript -->
  <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
  <!-- Load d3.js -->
  <script src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<button onclick="runTheSimulation()">RUN THE SIMULATION</button>
<div id="container"></div>
</body>


Related Query

More Query from same tag