Reputation:
I have three sets of nodes in a multi-foci force layout. Each node is already rendered in HTML.
Here's what my force layout code looks like:
var node = this.svg.selectAll('path')
.data(data);
// foci is a dictionary that assigns the x and y value based
// on what group a node belongs to.
var foci = {
"Blue" : {
"x" : xScale(0),
"y": height / 2
},
"Red": {
"x" : xScale(1),
"y": height / 2
},
"Purple": {
"x" : xScale(2),
"y": height / 2
},
};
// This helped me position the nodes to their assigned clusters.
var forceX = d3.forceX((d) => foci[d.group].x);
var forceY = d3.forceY((d) => foci[d.group].y);
var force = d3.forceSimulation(data)
.force('x', forceX)
.force('y', forceY)
.force("collide", d3.forceCollide(8))
.on('tick', function() {
node
.attr('transform', (d) => {
return 'translate(' + (d.x - 100) + ',' + (-d.y + 25) + ')';
});
});
What I have been able to accomplish so far is redraw the layouts based on a change in the dropdown, which reinitilizes d3.forceSimulation()
and makes the clusters snap back on the page, as you can see in the gif below.
That is not what I want. I'm trying to make the rearranging as seamless as possible.
UPDATE: By not reinitializing the d3.forceSimulation()
, I can bind the new data to the nodes and change their colors.
Upvotes: 4
Views: 1188
Reputation: 102174
Instead of reinitialising d3.forceSimulation()
you can simply reheat the simulation, using restart()
:
Restarts the simulation’s internal timer and returns the simulation. In conjunction with simulation.alphaTarget or simulation.alpha, this method can be used to “reheat” the simulation during interaction, such as when dragging a node, or to resume the simulation after temporarily pausing it with simulation.stop.
I created a demo to show you, using parts of your code. In this demo, the button randomizes the color of each data point. After that, we reheat the simulation:
force.alpha(0.8).restart();
Check it, clicking "Randomize":
var width = 500, height = 200;
var svg = d3.select("#svgdiv")
.append("svg")
.attr("width", width)
.attr("height", height);
var data = d3.range(100).map(function(d, i){
return {
group: Math.random()*2 > 1 ? "blue" : "red",
id: i
}
});
var xScale = d3.scaleOrdinal()
.domain([0, 1])
.range([100, width-100]);
var foci = {
"blue" : {
"x" : xScale(0),
"y": height / 2
},
"red": {
"x" : xScale(1),
"y": height / 2
}
};
var forceX = d3.forceX((d) => foci[d.group].x);
var forceY = d3.forceY((d) => foci[d.group].y);
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 5)
.attr("fill", (d)=>d.group);
var force = d3.forceSimulation(data)
.velocityDecay(0.65)
.force('x', forceX)
.force('y', forceY)
.force("collide", d3.forceCollide(8));
force.nodes(data)
.on('tick', function() {
node
.attr('transform', (d) => {
return 'translate(' + (d.x) + ',' + (d.y) + ')';
});
});
d3.select("#btn").on("click", function(){
data.forEach(function(d){
d.group = Math.random()*2 > 1 ? "blue" : "red"
})
node.transition().duration(500).attr("fill", (d)=>d.group);
setTimeout(function(){
force.nodes(data);
force.alpha(0.8).restart();
}, 1500)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<button id="btn">Randomize</button>
<div id="svgdiv"><div>
PS: I put the reheat inside a setTimeout
, so you can first see the circles changing colours and, then, moving to the foci positions.
Upvotes: 5