Reputation: 349
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
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:
transitioned
class, filter them based on the instructions[pointer]
and transition them.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
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
jsfiddle - https://jsfiddle.net/bryanwadd/hfd9onpv/
Upvotes: 1