D3.js中节点的局部力量

3
我希望能够分别对节点的几个子部分应用几个力(forceX 和 forceY)。
更详细地说,我有这个 JSON 数据作为我的节点数据:
[{
    "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
},
....
]

我希望的是仅对具有“Thème 1”主题属性的节点施加一些力,或者对于“Thème 2”值等其他力施加其他力。
我一直在查看源代码以检查是否可以将模拟的子部分分配给力,但我没有找到它。
我得出结论,我需要实现几个次要的,并仅应用它们各自的节点子部分,以处理我之前提到的力。以下是我在d3伪代码中想做的事情:
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( .... ) )

但是考虑到计算,我认为这并不是最优的选择。
是否有可能对模拟节点的子部分施加力而无需创建其他模拟?
实施altocumulus的第二个解决方案:
我尝试了这个解决方案,像这样:
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);      
});

我的质心数组可能会改变,这就是为什么我不得不实现一种动态的方式来实现我的子力。然而,在模拟时钟中,我最终遇到了这个错误:

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

我得出结论,过滤后的数组没有被分配给nodes,但我不知道原因。 PS:我用console.log检查了一下:nodes.filter(...)确实返回了一个填充好的数组,所以这不是问题的根源。

请提供相关的源代码以供审查。 - Fencer04
抱歉,我以为文字说明就足够了^^ 我已经使用了一些伪代码修改了主贴! - elTaan
@elTaan,我很高兴我的回答帮助你解决了问题。但是,你不应该将自己的解决方案编辑到问题中,因为那只是一个“问题”。这只会让未来的读者感到困惑。如果你自己找到了解决方案,可以考虑自我回答你的问题。并且你应该考虑接受最适合你问题的答案;这很可能是你自己的答案。 - altocumulus
好的,谢谢你的建议!我接受了你的答案,因为你的代码片段是有效的,尽管我的不是 :D - elTaan
2个回答

6

要仅对节点子集施加力,您基本上有两个选择:

  1. 实现自己的力,这并不像听起来那么困难,因为

    一种力仅仅是修改节点位置或速度的函数;

–或者,如果您想坚持使用标准力–

  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
    }
    
以下代码片段演示了第二种方法,通过使用子集初始化d3.forceY。在随机分布的圆形的整个集合中,只有每隔一个圆形应用力,并因此移动到底部。

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>


太好了!我必须个性化你的第二个解决方案,但是它出现了一个错误。我编辑了帖子来解释它! - elTaan
覆盖初始化方法是一个好主意。谢谢! - roman-roman
2
最近的mbostock示例,使用隔离力(覆盖“initialize”方法):https://bl.ocks.org/mbostock/b1f0ee970299756bc12d60aedf53c13b - user4275029

2

我修复了altocumulus第二个解决方案的实现:

在我的情况下,我需要为每个质心创建两个力。似乎我们不能为所有的forceX和forceY函数共享相同的初始化程序。

我不得不在循环中为每个质心创建本地变量:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接