Search code examples
d3.jsd3-force-directed

nodes without edges escaping the graph in D3 v4


I have built a graph that dynamically loads a few hundreds (sometimes thousands) nodes using D3.js V4. As much as I try to tune its forces, there are clusters of nodes that may get less cluttered, but consequentially the repel forces push nodes that have no edges to the boundaries of the canvas.

It seems there is no easy way to set a single node without edges gravitate towards a closer distance to the center

Following is a screenshot (notice the node on the upper-right corner): enter image description here

Here are the forces involved:

    var repelForce = d3.forceManyBody()
                     .strength(-200)
                     .distanceMax(400)
                     .distanceMin(150);



var gSimulation = d3.forceSimulation()
    .force('link', d3.forceLink().id((d) => d.id))
    .force('charge', repelForce)
    .force('center', d3.forceCenter(gwidth / 2, gheight / 2));

Looking forward to alternative ways to tune or optimize nodes that are specifically un-edged...


Solution

  • Try applying two additional forces:

    forceY and forceX, these can act as gravitational forces to keep nodes clustered around the center. From the API documentation:

    The x- and y-positioning forces push nodes towards a desired position along the given dimension with a configurable strength. The radial force is similar, except it pushes nodes towards the closest point on a given circle. The strength of the force is proportional to the one-dimensional distance between the node’s position and the target position. While these forces can be used to position individual nodes, they are intended primarily for global forces that apply to all (or most) nodes. (API documentation)

    The use of these forces is likely an easy solution to keeping nodes from wandering too far due to repelling forces. To apply:

    simulation.force("forceX", d3.forceX(width/2).strength(k) )
      .force("forceY", d3.forceY(height/2).strength(k) );
    

    While this approach should work, if you only want to apply these forces to the unlinked nodes, you can apply the force selectively:

      simulation
       .force("forceX",d3.forceX(width/2).strength(function(d){ return hasLinks(d) ? 0 : 0.05; }) )
       .force("forceY",d3.forceY(height/2).strength(function(d){ return hasLinks(d) ? 0 : 0.05; }) )
    

    Where hasLinks() is some function to determine if the node is lonely. If it is, then a non-zero strength is applied, if it has links, the directional force strength is set to zero. Here's a quick block. You may need to tinker with the strength values to keep the nodes in view for your force layout.

    If you're worried that fixed values for the strength might not work with dynamic data, you could increase the strength of these two forces if nodes are detected outside the bounding box/svg.