elTaan
elTaan

Reputation: 55

Partial forces on nodes in D3.js

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:

[{
    "word": "expression",
    "theme": "Thème 6",
    "radius": 3
}, {
    "word": "théorie",
    "theme": "Thème 4",
    "radius": 27
}, {
    "word": "relativité",
    "theme": "Thème 5",
    "radius": 27
}, {
    "word": "renvoie",
    "theme": "Thème 3",
    "radius": 19
},
....
]

What I want is to apply some forces exclusively to the nodes that have "Thème 1" as a theme attribute, or other forces for the "Thème 2" value, etc ...

I have been looking in the source code to check if we can assign a subpart of the simulation's nodes to a force, but I haven't found it.

I concluded that I would have to implement several secondary d3.simulation() and only apply their respective subpart of nodes in order to handle the forces I mentioned earlier. Here's what I thought to do in d3 pseudo-code :

mainSimulation = d3.forceSimulation()
                        .nodes(allNodes)
                        .force("force1", aD3Force() )
                        .force("force2", anotherD3Force() )
cluster1Simulation = d3.forceSimulation()
                        .nodes(allNodes.filter( d => d.theme === "Thème 1"))
                        .force("subForce1", forceX( .... ) )
cluster2Simulation = d3.forceSimulation()
                        .nodes(allNodes.filter( d => d.theme === "Thème 2"))
                        .force("subForce2", forceY( .... ) )

But I think it's not optimal at all considering the computation.

Is it possible to apply a force on a subpart of the simulation's nodes without having to create other simulations ?

Implementing altocumulus' second solution :

I tried this solution like this :

var forceInit;
Emi.nodes.centroids.forEach( (centroid,i) => {

    let forceX = d3.forceX(centroid.fx);
    let forceY = d3.forceY(centroid.fy);
    if (!forceInit) forceInit = forceX.initialize;  
    let newInit = nodes => { 
        forceInit(nodes.filter(n => n.theme === centroid.label));
    };
    forceX.initialize = newInit;
    forceY.initialize = newInit;

    Emi.simulation.force("X" + i, forceX);
    Emi.simulation.force("Y" + i, forceY);      
});

My centroids array may change, that's why I had to implement a dynamic way to implement my sub-forces. Though, i end up having this error through the simulation ticks :

09:51:55,996 TypeError: nodes.length is undefined
- force() d3.v4.js:10819
- tick/<() d3.v4.js:10559
- map$1.prototype.each() d3.v4.js:483
- tick() d3.v4.js:10558
- step() d3.v4.js:10545
- timerFlush() d3.v4.js:4991
- wake() d3.v4.js:5001

I concluded that the filtered array is not assigned to nodes, and I can't figure why. PS : I checked with a console.log : nodes.filter(...) does return an filled array, so this is not the problem's origin.

Upvotes: 3

Views: 1837

Answers (2)

elTaan
elTaan

Reputation: 55

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);
});

Upvotes: 2

altocumulus
altocumulus

Reputation: 21578

To apply a force to only subset of nodes you basically have to options:

  1. 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–

  1. 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>

Upvotes: 7

Related Questions