Nodes and Links overlapping in ForceDirectedLayout after upgrade to GoJs v3.0.x

Hello,

we were using ForceDirectedLayout with GoJs v2.1.34 earlier. Recently updated to GoJs v3.0.18.
After update we are facing issues of nodes and links overlapping on each other.

Below are 2 sample fiddles with above layout and respective issue.

Force-directed layout on GoJs v2.1.34

Force-directed layout on gojs v3.0.19

what property can be handled to achieve similar to old version look?.
Also, layout changes on multiple re-run which was not happening earlier.
Is there any property to generate graph with nodes at similar location on each re-run without saving location info.

Thanks.

Yes, we reimplemented ForceDirectedLayout to do a better job on larger graphs.

If you increase the ForceDirectedLayout.defaultElectricalCharge to something like 800, the results seem similar to me.

In order to produce the same result each time for the same graph with the same sized nodes, set ForceDirectedLayout.randomNumberGenerator to null, or to a seeded PRNG of your own choosing. This is true for all versions.

Hello walter,

Thanks for quick reply.

For your solution to diagram changing layout on every re-run. We tried with provided property, and it is working as per our requirement. Thanks for the solution.

For our other problem
We tried using ForceDirectedLayout.defaultElectricalCharge but we feel that link distance/length seems more than our earlier look. is there some other property that can be used with this to minimize link length?

Also, we tried few properties and found one which was giving more similar result and even links length were within acceptable limits, ForceDirectedLayout.prelayoutSpread. Can you confirm whether we can use this property for our use case? are there any limitations to this property or some dependency that we should consider?
Force Directed layout on gojs v3.0.19 with prelayoutSpread

While using above property (prelayoutSpread) we still encountered issue when we had multiple root nodes and nodes were getting added dynamically. In that case graph was still not getting plotted properly on expanding (dynamic addition of nodes) nodes.
Once nodes are visible, on re-run, graph was getting aligned properly with proper spacing.
So, if we want our diagram to layout properly just as we dynamically add child nodes on click/expand of root/parent nodes, is there any solution to this case?

Thanks, and regards

Yes, ForceDirectedLayout.prelayoutSpread helps.

While playing with ForceDirectedLayout properties in your jsfiddle, I noticed that some of the nodes were actually bigger than they appeared to be. You can see how some of the links do not meet up with the visible bounds of the yellow Shape or the text in the TextBlock. That’s yet another cause of why things are more spread out – the layout believes the nodes are wider than they need to be.

I think that problem is due to your requiring the TextBlock to be 150 wide. Maybe that is what you want, but because you are using a “Table” Panel, I think it would be better not to set its width but to set its stretch to go.Stretch.Horizontal. That does cause some of the nodes to have three lines of text instead of just two lines of text, but at least the layout thinks the nodes are actually as big as they appear to be.

Hello,

Regarding my previous question, part in which we were trying preLayoutSpread property with multiple root nodes. We are facing few other issues where distance between root nodes keeps on increasing on repeated expand and collapse actions. Verify on below demo, Click Expand and then Collapse buttons repeatedly in sequence for few times. Notice distance between root nodes keeps increasing.

Force directed layout multiple root nodes issue

And our main issue of not achieving proper layout when we dynamically add nodes is still pending. Verify in the same demo on clicking Expand button and then clicking Refresh button, resulting layout after both actions are different even though data is same. How can we achieve same look from result of Refresh in Expand flow?

Do suggest any configuration available for these issues?

It appears that you want each “expand” or “collapse” to be laid out in the same way each time. It might be easiest to just define a subclass of ForceDirectedLayout that resets randomNumberGenerator each time.

Regarding the problems with root nodes being far away, that’s because they are still participating in the ForceDirectedLayout each time, until they are at least ForceDirectedLayout.infinityDistance away. One solution would be to use ArrangingLayout to put the singleton nodes in a row or column.

I’ve done all of those changes in a cleaned-up version of your code:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:800px"></div>
  <button onclick="refresh()">Refresh</button>
  <button onclick="toggle()">Expand/Collapse</button>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/create-gojs-kit/dist/extensions/ArrangingLayout.js"></script>
  <script id="code">
class RepeatableForceDirectedLayout extends go.ForceDirectedLayout {
  constructor(init) {
    super();
    this.randomNumberGenerator = null;
    if (init) Object.assign(this, init);
  }

  doLayout(coll) {
    this.randomNumberGenerator = null;
    super.doLayout(coll);
  }
}

// one-time initialization
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      layout:
        new ArrangingLayout({
          primaryLayout:
            new RepeatableForceDirectedLayout({
              defaultSpringLength: 30,
              defaultElectricalCharge: 200,
              prelayoutSpread: 50,
            })
        }),
      "animationManager.isEnabled": false,
      autoScale: go.Diagram.Uniform,
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  new go.Node("Vertical")
    .add(
      new go.Shape({ fill: "yellow", width: 100, height: 40 }),
      new go.TextBlock({ stretch: go.Stretch.Horizontal, textAlign: "center" })
        .bind("text", "name")
    );
// end one-time initialization


const additionalNodes = [
    { key: 1, name: "Demo Assembly Component ID-1" },
    { key: 2, name: "Demo Assembly Component ID-2" },
    { key: 3, name: "Demo Assembly Component ID-3" },
    { key: 4, name: "Demo Assembly Component ID-4" },
    { key: 5, name: "Demo Assembly Component ID-5" },
    { key: 6, name: "Demo Assembly Component ID-6" },
    { key: 7, name: "Demo Assembly Component ID-7" },
    { key: 8, name: "Demo Assembly Component ID-8" },
    { key: 9, name: "Demo Assembly Component ID-9" },
    { key: 10, name: "Demo Assembly Component ID-10" },
    { key: 11, name: "Demo Assembly Component ID-11" },
    { key: 12, name: "Demo Assembly Component ID-12" },
    { key: 13, name: "Demo Assembly Component ID-13" },
    { key: 14, name: "Demo Assembly Component ID-14" },
    { key: 15, name: "Demo Assembly Component ID-15" },
    { key: 16, name: "Demo Assembly Component ID-16" },
    { key: 17, name: "Demo Assembly Component ID-17" },
    { key: 18, name: "Demo Assembly Component ID-18" },
    { key: 19, name: "Demo Assembly Component ID-19" },
    { key: 20, name: "Demo Assembly Component ID-20" },
    { key: 21, name: "Demo Assembly Component ID-21" },
    { key: 22, name: "Demo Assembly Component ID-22" },
    { key: 23, name: "Demo Assembly Component ID-23" },
    { key: 24, name: "Demo Assembly Component ID-24" },
    { key: 25, name: "Demo Assembly Component ID-25" },
    { key: 26, name: "Demo Assembly Component ID-26" },
    { key: 27, name: "Demo Assembly Component ID-27" },
    { key: 28, name: "Demo Assembly Component ID-28" },
    { key: 29, name: "Demo Assembly Component ID-29" },
    { key: 30, name: "Demo Assembly Component ID-30" },
    { key: 31, name: "Demo Assembly Component ID-31" },
    { key: 32, name: "Demo Assembly Component ID-32" },
    { key: 33, name: "Demo Assembly Component ID-33" },
    { key: 34, name: "Demo Assembly Component ID-34" },
    { key: 50, name: "Demo Assembly Component ID-35" },
    { key: 51, name: "Demo Assembly Component ID-36" },
    { key: 52, name: "Demo Assembly Component ID-37" },
    { key: 53, name: "Demo Assembly Component ID-38" },
    { key: 54, name: "Demo Assembly Component ID-39" },
    { key: 55, name: "Demo Assembly Component ID-40" },
    { key: 56, name: "Demo Assembly Component ID-41" },
    { key: 57, name: "Demo Assembly Component ID-42" },
    { key: 58, name: "Demo Assembly Component ID-43" },
    { key: 59, name: "Demo Assembly Component ID-44" },
    { key: 60, name: "Demo Assembly Component ID-45" },
    { key: 61, name: "Demo Assembly Component ID-46" },
    { key: 62, name: "Demo Assembly Component ID-47" },
  ];

const additionalLinks = [
    { from: 0, to: 1 },
    { from: 1, to: 2 },
    { from: 1, to: 3 },
    { from: 0, to: 4 },
    { from: 4, to: 5 },
    { from: 4, to: 6 },
    { from: 4, to: 7 },
    { from: 0, to: 8 },
    { from: 8, to: 9 },
    { from: 9, to: 10 },
    { from: 9, to: 11 },
    { from: 9, to: 12 },
    { from: 8, to: 13 },
    { from: 13, to: 14 },
    { from: 13, to: 15 },
    { from: 13, to: 16 },
    { from: 8, to: 17 },
    { from: 8, to: 18 },
    { from: 8, to: 19 },
    { from: 19, to: 20 },
    { from: 19, to: 21 },
    { from: 19, to: 22 },
    { from: 8, to: 23 },
    { from: 0, to: 24 },
    { from: 24, to: 25 },
    { from: 24, to: 26 },
    { from: 24, to: 27 },
    { from: 24, to: 28 },
    { from: 24, to: 29 },
    { from: 24, to: 30 },
    { from: 24, to: 31 },
    { from: 24, to: 32 },
    { from: 24, to: 33 },
    { from: 0, to: 34 },
    { from: 34, to: 9 },
    { from: 34, to: 13 },
    { from: 34, to: 17 },
    { from: 34, to: 18 },
    { from: 34, to: 19 },
    { from: 34, to: 23 },
    { from: 0, to: 50 },
    { from: 50, to: 51 },
    { from: 50, to: 52 },
    { from: 50, to: 53 },
    { from: 50, to: 54 },
    { from: 50, to: 55 },
    { from: 50, to: 56 },
    { from: 50, to: 57 },
    { from: 50, to: 58 },
    { from: 50, to: 59 },
    { from: 50, to: 60 },
    { from: 50, to: 61 },
    { from: 50, to: 62 },
  ];

const baseNodes = [
  { key: 0, name: "Demo Assembly Component ID-0" },
  { key: 63, name: "Demo Assembly Component ID-48" },
];

const baseLinks = [
];

function refresh() {
  myDiagram.model = new go.GraphLinksModel(
      [...baseNodes, ...additionalNodes],
      [...baseLinks, ...additionalLinks]
    );
}

function toggle() {
  if (myDiagram.model.nodeDataArray.length > baseNodes.length) {
    myDiagram.model.commit(m => {
      m.removeNodeDataCollection(additionalNodes);
      m.removeLinkDataCollection(additionalLinks);
    });
  } else {
    myDiagram.model.commit(m => {
      m.addNodeDataCollection(additionalNodes);
      m.addLinkDataCollection(additionalLinks);
    });
  }
}

window.addEventListener("DOMContentLoaded", () => refresh());
  </script>
</body>
</html>