jandraor
jandraor

Reputation: 349

Transitioning circles, one by one

I've got the following html structure.

<div id = 'divTest'>
  <div id = 'divSVG'>
  </div>
  <button id ='bStep' type="button">Step</button> 
  <button id = 'bRun'  type="button">Entire run</button> 
 </div>

Using D3 I created 10 randomly positioned circles inside an imaginary square whose top left corner is at (5, 5) & whose length side is ten.

 const n = 10; // number of circles
 //Creating random circles inside a square whose l = 10
 //The top left corner of the square is at (5, 5)
 const x_ini = 5;
 const y_ini = 5;
 const x_length = 10;
 const y_length = 10
 const dataset = [];

 for(let i = 0; i < n; i ++) {
   const randomNumberX = Math.random()
   const x = randomNumberX * x_length + x_ini;
   const randomNumberY = Math.random()
   const y = randomNumberY * y_length + y_ini;
   const pair = [x, y];
   dataset[i] = pair;
 }

 const svg = d3.select('#divSVG')
               .append('svg')
               .attr('width', 300)
               .attr('height', 300);

 const circles = svg.selectAll('.circleTest')
                    .data(dataset)
                    .enter()
                    .append('circle')
                    .attr('cx', d => d[0])
                    .attr('cy', d => d[1])
                    .attr('class', 'circleTest')
                    .attr('r', 1)
                    .attr('fill', 'black');

To the step button, I added a function that moves a circle to another imaginary circle and its class is changed.

//Moves one circle to an imaginary square whose top left corner is at (100, 5)
 d3.select('#bStep')
   .on('click', () => {
     const x_ini = 100;
     const y_ini = 5;
     const x_length = 10;
     const y_length = 10;
     const randomNumberX = Math.random()
     const x = randomNumberX * x_length + x_ini;
     const randomNumberY = Math.random()
     const y = randomNumberY * y_length + y_ini;
     const circle = d3.select('.circleTest')
                      .transition()
                      .duration(1000)
                      .attr('cx', x)
                      .attr('cy', y)
                      .attr('fill', 'red')
                      .attr('class', 'circleTest2')

});

I want that by clicking 'Entire run' button, all circles move not all the same time, but transitioned one by one based on some input data. For instance, based on 'instructions' vector, in the first transition only one circle is moved, then three circles are moved and so on. How can I accomplish that?

d3.select('#bRun')
  .on('click', () => {
    const instructions = [1, 3, 0, 2, 4, 1]
    // Move all circles based on some input like 'instructions'
  });

Here is the working example: https://jsfiddle.net/jandraor/91nwpb7a/42/

Upvotes: 3

Views: 114

Answers (2)

Shashank
Shashank

Reputation: 5660

Here's one approach to perform that kind of transition based on a different array.

const instructions = [1, 3, 0, 2, 4, 1];
var pointer = 0;

function moveCircles () {
    if(!instructions[pointer]) {
    console.log('No circles to be transitioned');
    setTimeout(function () { moveCircles(); }, 1000);
    return ;
  }
     // Move all circles based on some input like 'instructions'
   if(pointer > instructions.length-1) {
    console.log("No more instructions.");
    return ;
   }
   if(!d3.selectAll('circle.circleTest:not(.transitioned)').size()) {
    console.log('No more circles to be transitioned');
    return ;
   }

   // transition circles
   var circles = d3.selectAll('circle.circleTest:not(.transitioned)')
      .filter(function (d, i) {
           return i < instructions[pointer];
      });

   circles.transition()
     .duration(1000).on('end', function (d, i) {
     if(i === circles.size()-1) {
        pointer++;
        moveCircles();
      }
     }).attr('cx', x)
     .attr('cy', y)
     .attr('fill', 'red')
     .attr('class', 'transitioned');
}    

d3.select('#bRun')
 .on('click', () => {
        moveCircles();
 });

I'm assigning a class transitioned to the circles already transitioned which you can reset by any way of your choice.

Explanation:

  1. Pointer to the instructions array.
  2. On click, check if pointer is out of bounds of the instructions array or if there are no more circles left to be transitioned or if instructions[pointer] === 0.
  3. Select all circles with no transitioned class, filter them based on the instructions[pointer] and transition them.
  4. Increment the pointer in every scenario.
  5. Recursive call logic in #2 and #3.

Here's a working snippet:

              
const n = 10; // number of circles
//Creating random circles inside a square whose l = 10
//The top left corner of the square is at (5, 5)
const x_ini = 5;
const y_ini = 5;
const x_length = 10;
const y_length = 10
const dataset = [];
for(let i = 0; i < n; i ++) {
  const randomNumberX = Math.random()
  const x = randomNumberX * x_length + x_ini;
  const randomNumberY = Math.random()
  const y = randomNumberY * y_length + y_ini;
  const pair = [x, y];
  dataset[i] = pair;
}

const svg = d3.select('#divSVG')
              .append('svg')
              .attr('width', 300)
              .attr('height', 300);
              
const circles = svg.selectAll('.circleTest')
   .data(dataset)
   .enter()
   .append('circle')
   .attr('cx', d => d[0])
   .attr('cy', d => d[1])
   .attr('class', 'circleTest')
   .attr('r', 1)
   .attr('fill', 'black');
 
 		// transition co-ordinates computation
    const x_ini_to = 100;
    const y_ini_to = 5;
    const randomNumberX = Math.random()
    const x = randomNumberX * x_length + x_ini_to;
    const randomNumberY = Math.random()
    const y = randomNumberY * y_length + y_ini_to;
     
 //Moves one circle to an imaginary square whose top left corner is at (100, 5)
 d3.select('#bStep')
   .on('click', () => {
     const circle = d3.select('.circleTest')
                      .transition()
                        .duration(1000)
                        .attr('cx', x)
                        .attr('cy', y)
                        .attr('fill', 'red')
                        .attr('class', 'circleTest2')
   
   });
      
	const instructions = [1, 3, 0, 2, 4, 1];
	var pointer = 0;
  
  function moveCircles () {
  		if(!instructions[pointer]) {
      	pointer++;
        console.log('No circles to be transitioned');
        setTimeout(function () { moveCircles(); }, 1000);
        return ;
      }
         // Move all circles based on some input like 'instructions'
       if(pointer > instructions.length-1) {
       	console.log("No more instructions.");
        return ;
       }
       if(!d3.selectAll('circle.circleTest:not(.transitioned)').size()) {
       	console.log('No more circles to be transitioned');
        return ;
       }
       
       // transition circles
       var circles = d3.selectAll('circle.circleTest:not(.transitioned)').filter(function (d, i) {
       		return i < instructions[pointer];
       });
       
       circles.transition()
         .duration(1000).on('end', function (d, i) {
         if(i === circles.size()-1) {
            pointer++;
            moveCircles();
          }
         }).attr('cx', x)
         .attr('cy', y)
         .attr('fill', 'red')
         .attr('class', 'transitioned');
  }    
  
   d3.select('#bRun')
     .on('click', () => {
     		moveCircles();
     });
<script src="https://d3js.org/d3.v4.min.js"></script>

<div id = 'divTest'>
  <div id = 'divSVG'>
  
  </div>
 <button id ='bStep' type="button">Step</button> 
<button id = 'bRun'  type="button">Entire run</button> 
</div>

JSFIDDLE: https://jsfiddle.net/shashank2104/91nwpb7a/71/

Edit: My bad that I thought the circles were to be transitioned on every click but the Entire Run wouldn't make sense. Anyway, fixed it now. Hope this is clear enough. If not, please let me know. And yes, do match the Step button click accordingly for the proper sync.

Upvotes: 1

Bryan
Bryan

Reputation: 687

Here's one possible answer. I created a function that gets called recursively with the next array position (arrayPos) each time until the instructions array has been processed.

d3.select('#bRun')
  .on('click', () => {
    const instructions = [1, 3, 0, 2, 4, 1]
    // Start the process with the array and the first array position - would have called it index but uses that in the filter.
    moveCircles(instructions, 0);
  });

/**
 * This function recursively calls itself until the instructions array is complete.
 */
function moveCircles(instructions, arrayPos) {
	const duration = 1000;
	if (arrayPos < instructions.length) {
  	// If the instruction is zero we delay anyway - may not be desired behaviour.
  	if (instructions[arrayPos] === 0) {
    	setTimeout(() => {
      	// Call this function with the next array position.
      	moveCircles(instructions, arrayPos + 1)
      }, duration)
    }
    const x_ini = 100;
    const y_ini = 5;
    const x_length = 10;
    const y_length = 10;
    const circles = d3.selectAll('.circleTest').filter( (value, index) => {
    	return index < instructions[arrayPos]
    })

    circles.transition()
         .duration(duration)
         .on('end', () => {
           // Call this function with the next array position.
           moveCircles(instructions, arrayPos + 1)
         })
         .attr('cx', () => {
           return Math.random() * x_length + x_ini;
         })
         .attr('cy',  () => {
           return Math.random() * y_length + y_ini;
         })
         .attr('fill', 'red')
         .attr('class', 'circleTest2')
  }
}   

Notes

  • Used selectAll to get all the remaining circles and then filtered them to have an array of the size required by the instructions.
  • I moved x and y random calculations into the attr functions to have a random position for each circle move.
  • When the instruction is zero, I added a setTimeout of the same duration as the transition - this may not be required so just remove the setTimeout and leave the call to moveCircles().
  • The original single step button calls the same moveCircles function with an array of just one instruction to move one circle.

jsfiddle - https://jsfiddle.net/bryanwadd/hfd9onpv/

Upvotes: 1

Related Questions