最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Partial forces on nodes in D3.js - Stack Overflow

programmeradmin1浏览0评论

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

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.

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

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.

Share edited Sep 21, 2016 at 9:15 elTaan asked Sep 19, 2016 at 14:12 elTaanelTaan 557 bronze badges 4
  • Please provide relevant source code for review. – Fencer04 Commented Sep 19, 2016 at 15:20
  • Sorry, I though that a text explanation would have been enough ^^ I edited the main post with some pseudo-code ! – elTaan Commented Sep 20, 2016 at 8:00
  • @elTaan I am glad, may answer helped solving your problem. You should not edit your own solution into your question, though, because it is just that a question. This will only confuse future readers. If you found a solution yourself, you might instead self-answer your question. And you should consider accepting whatever answer you feel fits your problem best; which may very well be your own answer. – altocumulus Commented Sep 21, 2016 at 9:17
  • Alright, thanks for your tips! I accepted your answer, since your snippet code is working, despite mine is not :D – elTaan Commented Sep 21, 2016 at 10:42
Add a ment  | 

2 Answers 2

Reset to default 7

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/d3.v4.js"></script>

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);
});
发布评论

评论列表(0)

  1. 暂无评论