Inconsistent Connection Distribution Between Node and Side Ports

Problem :
In the diagram, connections can be made through two different ports:

  • One port allows connecting to the entire node.
  • The other port is for connecting to a specific side of the node.

The issue is that when we use the side port and make multiple connections, they are automatically and evenly distributed along that side

However, when we use the port for the entire node, the distribution starts from scratch and doesn’t align with how side connections are laid out.

Expected Behavior : What we want is consistent distribution behavior, whether connections are made to a specific side or to the whole node. The layout of the connections should be handled the same way in both cases.



One idea is to have each Node only have one port – the big one with the spots being go.Spot.AllSides.

Then the user can somehow choose whether a Link should always connect at one particular side or on any side. There are various ways you can design that.

In the code below, to let a user control whether it’s a specific side or not at the time that the user is drawing a new Link, the user can hold down the Ctrl modifier, or the mouse point can be in the port (but not any other element).

But an alternative way to specify that could be to ask the user which side it should be, or no specific side. Or you could let them edit it, for either or both ends of an existing Link. It’s your choice for how you want to let the user decide.

Try this 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:400px"></div>
  Each Node has only one port, with spots that are AllSides.
  Each Link has a Binding on the <b>Link.toSpot</b> property.
  Note how the link from Delta to Alpha always is on the bottom side of the toNode,
  regardless of where Delta is relative to Alpha.
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script id="code">
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "LinkDrawn": e => {
        const link = e.subject;
        const p = e.diagram.lastInput.documentPoint;
        // When the Ctrl modifier is held down at the time the link is connected,
        // or when the mouse is over the port and not any other element like the TextBlock,
        // decide which side is closest to the mouse point that the link should connect at that.
        if (link.toPort !== null && (e.diagram.lastInput.control || e.diagram.findObjectAt(p) == link.toPort)) {
          const dir = nearestSideDirection(link.toPort.getDocumentBounds(), p);
          link.toSpot = dir === 0 ? go.Spot.RightSide : (dir === 90 ? go.Spot.BottomSide : (dir === 180 ? go.Spot.LeftSide : go.Spot.TopSide));
        }
      },
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

  function nearestSideDirection(r, p) {  // this will be a method on Rect in v3.1
    let res = 0;
    const spotx = r === null ? p.x : ((p.x - r.x) / (r.width > 0 ? r.width : 1));
    const spoty = r === null ? p.y : ((p.y - r.y) / (r.height > 0 ? r.height : 1));
    if (spotx > spoty) {
      if (spotx > 1 - spoty) {
        // res = 0;
      } else if (spotx < 1 - spoty) {
        res = 270;
      } else {
        res = 315;
      }
    } else if (spotx < spoty) {
      if (spotx > 1 - spoty) {
        res = 90;
      } else if (spotx < 1 - spoty) {
        res = 180;
      } else {
        res = 135;
      }
    } else {
      if (spotx < 0.5) {
        res = 225;
      } else if (spotx > 0.5) {
        res = 45;
      // } else {
      //   res = 0;
      }
    }
    return res;
  }

myDiagram.nodeTemplate =
  new go.Node("Auto")
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape({
          fill: "white", portId: "", cursor: "pointer",
          fromLinkable: true, fromSpot: go.Spot.AllSides,
          toLinkable: true, toSpot: go.Spot.AllSides
        })
        .bind("fill", "color"),
      new go.TextBlock({ margin: 8 })
        .bind("text")
    );

myDiagram.linkTemplate =
  new go.Link({ routing: go.Link.Orthogonal, corner: 10 })
    .bindTwoWay("toSpot", "toSpot", go.Spot.parse, go.Spot.stringify)
    .add(
      new go.Shape(),
      new go.Shape({ toArrow: "OpenTriangle" })
    );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "Alpha", color: "lightblue" },
  { key: 2, text: "Beta", color: "orange" },
  { key: 3, text: "Gamma", color: "lightgreen" },
  { key: 4, text: "Delta", color: "pink" }
],
[
  { from: 1, to: 2 },
  { from: 1, to: 3 },
  { from: 3, to: 4 },
  { from: 4, to: 1, toSpot: "BottomSide" }
]);
  </script>
</body>
</html>