I want to apply several forces (forceX and forceY) respectively to several subparts of nodes.
To be more explanatory, I have this JSON as data for my nodes:
I fixed my own implementation of altocumulus' second solution :
In my case, I had to create two forces per centroid. It seems like we can't share the same initializer for all the forceX and forceY functions.
I had to create local variables for each centroid on the loop :
Emi.nodes.centroids.forEach((centroid, i) => {
let forceX = d3.forceX(centroid.fx);
let forceY = d3.forceY(centroid.fy);
let forceXInit = forceX.initialize;
let forceYInit = forceY.initialize;
forceX.initialize = nodes => {
forceXInit(nodes.filter(n => n.theme === centroid.label))
};
forceY.initialize = nodes => {
forceYInit(nodes.filter(n => n.theme === centroid.label))
};
Emi.simulation.force("X" + i, forceX);
Emi.simulation.force("Y" + i, forceY);
});
To apply a force to only subset of nodes you basically have to options:
Implement your own force, which is not as difficult as it may sound, because
A force is simply a function that modifies nodes’ positions or velocities;
–or, if you want to stick to the standard forces–
Create a standard force and overwrite its force.initialize() method, which will
Assigns the array of nodes to this force.
By filtering the nodes and assigning only those you are interested in, you can control on which nodes the force should act upon:
// Custom implementation of a force applied to only every second node
var pickyForce = d3.forceY(height);
// Save the default initialization method
var init = pickyForce.initialize;
// Custom implementation of .initialize() calling the saved method with only
// a subset of nodes
pickyForce.initialize = function(nodes) {
// Filter subset of nodes and delegate to saved initialization.
init(nodes.filter(function(n,i) { return i%2; })); // Apply to every 2nd node
}
The following snippet demonstrates the second approach by initializing a d3.forceY
with a subset of nodes. From the entire set of randomly distributed circles only every second one will have the force applied and will thereby be moved to the bottom.
var width = 600;
var height = 500;
var nodes = d3.range(500).map(function() {
return {
"x": Math.random() * width,
"y": Math.random() * height
};
});
var circle = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 3)
.attr("fill", "black");
// Custom implementation of a force applied to only every second node
var pickyForce = d3.forceY(height).strength(.025);
// Save the default initialization method
var init = pickyForce.initialize;
// Custom implementation of initialize call the save method with only a subset of nodes
pickyForce.initialize = function(nodes) {
init(nodes.filter(function(n,i) { return i%2; })); // Apply to every 2nd node
}
var simulation = d3.forceSimulation()
.nodes(nodes)
.force("pickyCenter", pickyForce)
.on("tick", tick);
function tick() {
circle
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
}
<script src="https://d3js.org/d3.v4.js"></script>