Partial forces on nodes in D3.js

前端 未结 2 1684
小蘑菇
小蘑菇 2021-01-14 19:54

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:

相关标签:
2条回答
  • 2021-01-14 20:27

    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 讨论(0)
  • 2021-01-14 20:50

    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>

    0 讨论(0)
提交回复
热议问题