Force directed layout sends some child nodes far, far away from parent

I am testing out the force directed layout for some hierarchical tree data. But I am getting an issue with some of the larger nodes getting shot way, way off screen on the initial placement. I have tried manipulating the different parameters like electrical charge, gravity, and all of that with little success. I was hoping someone might be able to give me some pointers. I found some mentions of similar issues but that was from 10 years ago and the response was that the issue would be fixed in version 3.

I think my issue has to do with some nodes having very deep trees under them while other nodes are just direct to the top level parent with little to no children. I’ve attached a screenshot of what I am getting. Note that the lines going off to the top right go very, very far away. So far in fact, that upon initially loading the screen, I have it set to fit everything into the view so all nodes are completely invisible. It took me a while to figure out that I had to zoom way in before I could even start seeing the nodes at all.

I’ve seen this behavior before, but I haven’t been able to reproduce it. Is the behavior better if you set ForceDirectedLayout.infinityDistance to a smaller value than the default of 1000?

I tried adjusting down the infinity distance. It seems to cause the single nodes, that are already close to the parent to overlap more and sort of group closer to the top left. But it doesn’t seem to affect the distance of those far away nodes.

These are the parameters I am currently using and have adjusted around:

    myDiagram.layout.defaultElectricalCharge = 150;
    myDiagram.layout.arrangesToOrigin = false;
    myDiagram.layout.arrangementSpacing = go.Size.parse("100 100");
    myDiagram.layout.defaultGravitationalMass = 300;
    myDiagram.layout.defaultSpringStiffness = 0.05;
    myDiagram.layout.defaultSpringLength = 50;
    myDiagram.layout.maxSpringLength = 50;
    myDiagram.layout.epsilonDistance = 1;
    myDiagram.layout.maxIterations = 100;
    myDiagram.layout.infinityDistance = 500;

Is there anyway to simply put a max distance between nodes? That alone would mostly solve the issue. I have it setup to allow dragging them around and place them manually. But with a larger graph like this, they just get thrown so far away, the zooming in and huge amount of scrolling required make it prohibitive.

We will try to track down this problem.

Is there an easy way to export as json the data set and relations so that I could attach. I’m sure it would be helpful to replicate the issue.

myDiagram.model.toJson()

Ok, well if it is helpful, here is my node data array.

https://pastebin.com/YK9KWJsJ

thanks for the model

Just FYI, that is a smaller model that still has the problem. The larger models I have tried go way farther until the nodes are invisible when zooming to fit.

Try something like this, using RadialLayout first and then using ForceDirectedLayout:

<!DOCTYPE html>
<html>
<head>
<title>Minimal GoJS Sample</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="../release/go.js"></script>
<script src="../extensions/RadialLayout.js"></script>
<script id="code">
  function SmarterForceDirectedLayout() {
    go.ForceDirectedLayout.call(this);
    this.radial = new RadialLayout();
    this.radial.layerThickness = 200;
  }
  go.Diagram.inherit(SmarterForceDirectedLayout, go.ForceDirectedLayout);

  SmarterForceDirectedLayout.prototype.needsClusterLayout = function() {
    if (go.ForceDirectedLayout.prototype.needsClusterLayout.call(this)) {
      this.radial.network = this.network;
      this.radial.doLayout(this.diagram);
    }
    return false;
  }

  function init() {
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          layout: $(SmarterForceDirectedLayout, { maxIterations: 200 })
        });

    myDiagram.nodeTemplate =
      $(go.Node, "Spot",
        { width: 50, height: 50 },
        $(go.Shape, "Circle",
          { fill: "white" }),
        $(go.TextBlock,
          new go.Binding("text", "key"))
      );

    myDiagram.model = new go.TreeModel(
      [
  { "key": "177", "parent": "4160", "size": 260, "is_tree_expanded": "1" },
  . . .
  { "key": "18883", "parent": "947", "size": 100, "is_tree_expanded": "1" }
    ]);
  }
</script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
</body>
</html>

That seems to have made the local layouts better. The nodes are more in a circle around the parent. But it doesn’t seem to affect the far off nodes. They remain way, way out there. Either way, thank you for your response thus far.

Are any of the “far away” nodes connected with any nodes in the main body?

Yes, they are all connected to the main body. I don’t have any nodes without parents. Except for the first/primary node of course.

Is there a way to put a hard limit on the length of an edge between two nodes?

There is a ForceDirectedLayout.moveLimit, but that applies to each iteration, not for the total layout.

I think it’s possible to implement a limit on the total distance from some point, but we’ll need to experiment with that.

Here you go. You can control the area within which the nodes will be pushed by setting RadialForceDirectedLayout.area. You can see the default value for that Rect in the constructor:

  function RadialForceDirectedLayout() {
    go.ForceDirectedLayout.call(this);
    this.radial = new RadialLayout();
    this.radial.layerThickness = 200;
    this.area = new go.Rect(-9999, -9999, 19999, 19999);
  }
  go.Diagram.inherit(RadialForceDirectedLayout, go.ForceDirectedLayout);

  RadialForceDirectedLayout.prototype.needsClusterLayout = function() {
    // only do the RadialLayout the first time
    if (go.ForceDirectedLayout.prototype.needsClusterLayout.call(this)) {
      this.radial.network = this.network;
      this.radial.doLayout(this.diagram);
    }
    return false;
  }

  RadialForceDirectedLayout.prototype.electricalFieldX = function(x, y) {
    var r = this.area;
    if (x > r.right) return r.right-x;
    if (x < r.left) return r.left-x;
    return 0;
  }

  RadialForceDirectedLayout.prototype.electricalFieldY = function(x, y) {
    var r = this.area;
    if (y > r.bottom) return r.bottom-y;
    if (y < r.top) return r.top-y;
    return 0;
  }

For version 3 of GoJS we have rewritten ForceDirectedLayout and have not seen any numerical instabilities that were causing this problem. We have also improved the initial “pre-layout” and made it more flexible.