Search code examples
javascriptselectd3.jsnodesforce-layout

Change the collision behavior of many nodes stored in an array


when using the force layout in d3.js it is possible to push nodes away with the help of the collision force by increasing an imaginary radius that surrounds the nodes.

I created a seperate button named button and i want to use .data() (to select a whole array) to increase the collide radius into 40 of many nodes when click on that button. For example when a filtered number of nodes is stored in an array called abc, i tried this code:

var node =......
    .on("click", function(d, i) {

        abc = start && start.path(d) || [];

        node.style("fill", function(n) {
            if (n == start ) {             
               return "yellow";
            } else if ( n == d) {
               return "green"
            } else if (abc.includes(n)) {
               return "red"
            } else {
               return "lightgrey"
            }
         .....
      }});

button.on("click", function(d) {
   d3.selectAll("circle").data().forEach(d => d.r = 6);
   d3.select(abc).data().r = 40;
   simulation.nodes(data);
   simulation.alpha(0.8).restart();
})

I am able to click on 2 nodes and store these 2 nodes and all the nodes between them in the arrayabc. This is possible with the help of the d3.js function path()which returns the shortest way between 2 nodes.
But unfortunally it does not work. Maybe there is someone who can help me with the problem. The basic idea of pushing nodes away is already discussed in here: Using the force-layout physics for seperated elements Thanks so much!


Solution

  • After several comments I finally have an idea of how you are filtering the node selection.

    In the following demo, the circles have 4 different colours:

    var colours = ["blue", "red", "green", "yellow"];
    
    node.attr("fill", (d, i) => colours[i%4]);
    

    So, when you click the button, we simply filter the nodes with "red" colour and increase their r property, making the collide radius increase, using each:

    node.filter(function() {
        return d3.select(this).attr("fill") === "red"
    }).each(d => d.r = 40);
    

    If you want use data as a getter, you can do it with a forEach:

    node.filter(function() {
        return d3.select(this).attr("fill") === "red"
    }).data().forEach(d => d.r = 40);
    

    Which has the same result.

    Here is a demo, all red nodes will push away the other nodes after the click, with a collide radius of 40:

    var svg = d3.select("svg");
    
    var colours = ["blue", "red", "green", "yellow"];
    
    var data = d3.range(30).map(d => ({
        r: 6
    }));
    
    var simulation = d3.forceSimulation(data)
        .force("x", d3.forceX(150).strength(0.05))
        .force("y", d3.forceY(75).strength(0.05))
        .force("collide", d3.forceCollide(function(d) {
            return d.r + 1;
        }));
    
    var node = svg.selectAll(".circles")
        .data(data)
        .enter()
        .append("circle")
        .attr("r", d => d.r)
        .attr("fill", (d, i) => colours[i%4]);
    
    d3.select("button").on("click", function(d) {
    		node.filter(function(){
    		 return d3.select(this).attr("fill") === "red"
    		}).each(d=>d.r = 40);
        simulation.nodes(data);
        simulation.alpha(0.8).restart();
    })
    
    simulation.nodes(data)
        .on("tick", d => {
            node.attr("cx", d => d.x).attr("cy", d => d.y);
        });
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <button>Click me</button>
    <br>
    <svg></svg>