Consider used ports in Layered Digraph Layout

Hello,

we have a diagram where each node has 4 ports, displayed as circles on each side of the node:
grafik

Now I connect some nodes from top to bottom and then from left to right:

If I now apply a Layered Digraph Layout, it results in something like the following:

Is there some built-in option for the Layered Digraph Layout to maintain the original “L”-stucture by telling it to consider the ports which are used for connection?
Or, if I need to do it manually, is there any suggestion on how to proceed?

LayeredDigraphLayout (and TreeLayout, for that matter) automatically determine the "layer"s that each node should be in based on the links with which the node is connected to other nodes. So they won’t work in the way that you want.

You might be interested in this code, which is a custom Layout to do something like what you might want.

<!DOCTYPE html>
<html>

<head>
  <title>DirectionalLayout</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
  <meta name="description" content="A TreeLayout-like custom Layout where each link tells the layout which way to grow.">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="../latest/release/go.js"></script>
  <script id="code">
// Assume the graph can be walked like a tree, although this is tolerant of cycles.
// The fromSpot of a link's fromPort determines the direction that the connected node will be placed.
//??? Note that nodes can overlap each other, when they are placed in different subtrees that
// converge on the same area, e.g. right-then-down and down-then-right.
class DirectionalLayout extends go.Layout {
  constructor(init) {
    super();
    this.siblingSpacing = 10;  // parameters for spacing between nodes
    this.layerSpacing = 50;
    if (init) Object.assign(this, init);
  }

  spotToAngle(spot) {
    if (spot.x === 1) return 0;
    if (spot.y === 1) return 90;
    if (spot.x === 0) return 180;
    if (spot.y === 0) return 270;
    return 0;
  }

  doLayout(coll) {
    if (this.network === null) {
      this.network = this.makeNetwork(coll);
    }
    this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);

    var it = this.network.vertexes.iterator;
    while (it.next()) {
      var v = it.value;
      v.isPositioned = false;
      v.angle = 0;
      var n = v.node;
      if (n !== null) {
        var l = n.findTreeParentLink();
        var p = n.findTreeParentNode();
        if (l !== null && p !== null) {
          // remember the angle at which this vertex should be placed relative to its parent
          //?? this assumes the direction can be taken from the port's spot
          var spot = l.fromPort.part === p ? l.fromPort.fromSpot : l.toPort.toSpot;
          v.angle = this.spotToAngle(spot);
        }
      }
    }

    var x = this.arrangementOrigin.x;
    var y = this.arrangementOrigin.y;
    var root = null;
    while (root = this.findRoot()) {
      //?? this assumes separate trees get arranged vertically above each other
      if (y !== this.arrangementOrigin.y) y += root.height / 2;
      // position root and then walk the tree starting at that vertex
      root.centerX = x;
      root.centerY = y;
      root.isPositioned = true;
      this.walkTree(root);
      // determine position of next subtree
      var maxy = this.arrangementOrigin.y;
      this.network.vertexes.each(function (v) {
        if (v.isPositioned) maxy = Math.max(maxy, v.bounds.bottom);
      })
      y = maxy + this.layerSpacing;
    }

    this.updateParts();
    this.network = null;
  }

  findRoot() {
    var root = null;
    // find a root vertex -- one without any incoming edges
    var it = this.network.vertexes.iterator;
    while (it.next()) {
      var v = it.value;
      if (v.isPositioned) continue;  // skip any already-positioned nodes
      if (root === null) root = v;  // in case there are only cycles
      if (v.sourceEdges.count === 0) {
        root = v;  // found a plausible root
        break;
      }
    }
    return root;
  }

  walkTree(v) {
    // group child vertexes by angle
    var rights = [];
    var bottoms = [];
    var lefts = [];
    var tops = [];
    var it = v.destinationVertexes.iterator;
    while (it.next()) {
      var child = it.value;
      if (child.isPositioned) continue;  // ignore already positioned vertexes
      if (child.angle === 0) rights.push(child);
      else if (child.angle === 90) bottoms.push(child);
      else if (child.angle === 180) lefts.push(child);
      else if (child.angle === 270) tops.push(child);
    }
    // now position them and walk them recursively
    this.positionChildren(rights, v, 0);
    this.positionChildren(bottoms, v, 90);
    this.positionChildren(lefts, v, 180);
    this.positionChildren(tops, v, 270);
  }

  positionChildren(arr, parent, angle) {
    if (arr.length === 0) return;
    var cx = parent.centerX;
    var cy = parent.centerY;
    if (arr.length === 1) {
      var main = arr[0];
      // just center the node relative to the parent
      this.positionChild(parent, main, angle, cx, cy);
    } else {
      //??? Choose which child to be aligned with the parent, could be marked on the node data.
      // Maybe sort the children too.
      var mainidx = Math.floor(arr.length / 2);  // for now, just pick a middle one
      var main = arr[mainidx];

      var horiz = (angle === 0 || angle === 180);
      this.positionChild(parent, main, angle, cx, cy);

      // position siblings above main vertex
      var lastchild = main;
      for (var i = mainidx - 1; i >= 0; i--) {
        var child = arr[i];
        if (horiz) {
          cy -= lastchild.height / 2 + this.siblingSpacing + child.height / 2;
        } else {
          cx -= lastchild.width / 2 + this.siblingSpacing + child.width / 2;
        }
        lastchild = child;
        this.positionChild(parent, child, angle, cx, cy);
      }

      // position siblings below main vertex
      cx = parent.centerX;
      cy = parent.centerY;
      lastchild = main;
      for (var i = mainidx + 1; i < arr.length; i++) {
        var child = arr[i];
        if (horiz) {
          cy += lastchild.height / 2 + this.siblingSpacing + child.height / 2;
        } else {
          cx += lastchild.width / 2 + this.siblingSpacing + child.width / 2;
        }
        lastchild = child;
        this.positionChild(parent, child, angle, cx, cy);
      }
    }
  }

  positionChild(parent, child, angle, cx, cy) {
    // add space between layers
    switch (angle) {
      case 0: cx += (parent.width / 2 + this.layerSpacing + child.width / 2); break;
      case 180: cx -= (parent.width / 2 + this.layerSpacing + child.width / 2); break;
      case 90: cy += (parent.height / 2 + this.layerSpacing + child.height / 2); break;
      case 270: cy -= (parent.height / 2 + this.layerSpacing + child.height / 2); break;
    }
    child.centerX = cx;  // position the center of the vertex
    child.centerY = cy;
    child.isPositioned = true;  // mark so that it won't be positioned/walked again
    this.walkTree(child);  // recurse
  }
}  // end of DirectionalLayout

myDiagram =
  new go.Diagram("myDiagramDiv", {
      initialContentAlignment: go.Spot.Center,  // for v1.*
      layout: new DirectionalLayout(),
      "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 makePort(name, spot) {
  return new go.Shape({
      fill: "white", width: 7, height: 7, alignment: spot, alignmentFocus: spot,
      portId: name, fromSpot: spot, toSpot: spot  // fromSpot is needed by DirectionalLayout
    });
}

myDiagram.nodeTemplate =
  new go.Node("Spot", {
      locationSpot: go.Spot.Center, selectionObjectName: "BODY",
      toolTip:
        go.GraphObject.build("ToolTip")
          .add(
            new go.TextBlock()
              .bind("text", "info")
          )
    })
    .add(
      new go.Panel("Auto", { name: "BODY", width: 70, height: 70 })
        .add(
          new go.Shape({ fill: "white" })
            .bind("fill", "color"),
          new go.TextBlock({ name: "TEXT", margin: 7 })
            .bind("text")
        ),
      makePort("t", go.Spot.Top),
      makePort("r", go.Spot.Right),
      makePort("b", go.Spot.Bottom),
      makePort("l", go.Spot.Left),
      go.GraphObject.build("Button", {
          alignment: go.Spot.TopRight, alignmentFocus: go.Spot.TopRight,
          click: function (e, button) {
            var node = button.part;
            var body = node.findObject("BODY");
            node.diagram.commit(function (diagram) {
              if (body.width === 70) {
                body.width = 150;
                body.height = 120;
                button.findObject("TB").text = "-";
              } else {
                body.width = 70;
                body.height = 70;
                button.findObject("TB").text = "+";
              }
            }, "toggled node size");
          }
        })
        .add(new go.TextBlock("+", { name: "TB" }))
    );

myDiagram.linkTemplate =
  new go.Link({ routing: go.Link.Orthogonal, corner: 10 })
    .add(
      new go.Shape(),
      new go.Shape({ toArrow: "Standard" })
    );

myDiagram.model = new go.GraphLinksModel(
  {
    linkFromPortIdProperty: "fp",
    linkToPortIdProperty: "tp",
    nodeDataArray:
      [
        { key: 1, text: "Alpha", color: "lightblue" },
        { key: 2, text: "Beta", color: "orange" },
        { key: 3, text: "Gamma", color: "lightgreen" },
        { key: 4, text: "Delta", color: "pink" },
        { key: 5, text: "Epsilon", color: "yellow" },
        { key: 11, text: "End 1" },
        { key: 12, text: "End 2" },
        { key: 13, text: "End 3" },
        { key: 14, text: "End 4" },
        { key: 21, text: "right" },
        { key: 22, text: "right" },
        { key: 23, text: "down" },
        { key: 24, text: "down" },
        { key: 242, text: "down" },
        { key: 243, text: "right" },
        { key: 25, text: "left" },
        { key: 26, text: "left" },
      ],
    linkDataArray:
      [
        { from: 1, to: 2, fp: "r", tp: "l" },
        { from: 2, to: 3, fp: "r", tp: "l" },
        { from: 2, to: 4, fp: "b", tp: "t" },
        { from: 4, to: 5, fp: "b", tp: "t" },
        { from: 3, to: 11, fp: "r", tp: "l" },
        { from: 3, to: 12, fp: "r", tp: "l" },
        { from: 3, to: 13, fp: "r", tp: "l" },
        { from: 3, to: 14, fp: "r", tp: "l" },
        { from: 5, to: 21, fp: "r", tp: "l" },
        { from: 21, to: 22, fp: "r", tp: "l" },
        { from: 5, to: 23, fp: "b", tp: "t" },
        { from: 23, to: 24, fp: "b", tp: "t" },
        { from: 23, to: 242, fp: "b", tp: "t" },
        { from: 23, to: 243, fp: "r", tp: "l" },
        { from: 5, to: 25, fp: "l", tp: "r" },
        { from: 25, to: 26, fp: "l", tp: "r" },
      ]
  });
  </script>
</body>
</html>

Thank you, we’ll have a look it this!