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>