OK, here you go:
<!DOCTYPE html>
<html>
<head>
<title>Left Right Layout</title>
<!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
<script src="go.js"></script>
<script id="code">
class LeftRightLayout extends go.Layout {
constructor() {
super();
this.isViewportSized = true;
}
doLayout(coll) {
var diagram = this.diagram;
if (diagram === null) return;
coll = this.collectParts(coll);
var left = null;
var right = null;
coll.each(function(part) {
if (part instanceof go.Node) {
// customize this to identify the parts (probnably Groups) that should go at the top and
// at the bottom of the viewport
if (part.key === "Left") left = part;
else if (part.key === "Right") right = part;
}
});
if (left) coll.remove(left);
if (right) coll.remove(right);
// implementations of doLayout that do not make use of a LayoutNetwork
// need to perform their own transactions
diagram.startTransaction("LeftRightLayout");
var vb = new go.Rect(0, 0, diagram.viewportBounds.width, diagram.viewportBounds.height);
var spacew = vb.width;
var spaceh = vb.height;
// move left and right parts to their respective positions in the viewport
if (left) {
if (left instanceof go.Group) {
var bnds = diagram.computePartsBounds(left.memberParts);
diagram.moveParts(left.memberParts, new go.Point(vb.x + left.placeholder.margin.left, vb.centerY - bnds.centerY))
} else {
var bnds = left.actualBounds;
left.moveTo(vb.x, vb.centerY - bnds.height/2);
}
spacew -= left.actualBounds.width + 1;
vb.x += left.actualBounds.width + 1;
vb.width = Math.max(0, vb.width - left.actualBounds.width + 1);
}
if (right) {
if (right instanceof go.Group) {
var bnds = diagram.computePartsBounds(right.memberParts);
diagram.moveParts(right.memberParts, new go.Point(vb.right - bnds.centerX - right.actualBounds.width/2, vb.centerY - bnds.centerY))
} else {
var bnds = right.actualBounds;
right.moveTo(vb.right - right.actualBounds.width, vb.centerY - bnds.height/2);
}
spacew -= right.actualBounds.width + 1;
vb.width = Math.max(0, vb.width - right.actualBounds.width + 1);
}
// now VB has the remaining available area
// // arrange the rest of the parts that are in the middle
// var glay = new go.GridLayout();
// glay.doLayout(coll);
// var bnds = diagram.computePartsBounds(coll);
// // move rest of parts to center of viewport
// diagram.moveParts(coll, new go.Point(vb.centerX - bnds.centerX, vb.centerY - bnds.centerY));
diagram.commitTransaction("TopBottomLayout");
}
} // end of TopBottomLayout
function init() {
var $ = go.GraphObject.make;
myDiagram =
new go.Diagram("myDiagramDiv",
{
layout: $(LeftRightLayout),
padding: 0,
allowHorizontalScroll: false,
//allowVerticalScroll: false,
//allowZoom: false,
"animationManager.isInitial": false,
"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();
}
}
});
myDiagram.nodeTemplate =
$(go.Node, "Vertical",
{ locationObjectName: "ICON", locationSpot: go.Spot.Center },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Shape, "Circle",
{ name: "ICON", width: 40, height: 40, fill: "dodgerblue", strokeWidth: 0, portId: "" }),
$(go.TextBlock,
new go.Binding("text"))
);
myDiagram.nodeTemplateMap.add("Left",
$(go.Node, "Vertical",
{ copyable: false, deletable: false, movable: false },
$(go.Shape, "Circle",
{ width: 40, height: 40, fill: "lightgreen", strokeWidth: 0, portId: "" }),
$(go.TextBlock,
new go.Binding("text"))
));
myDiagram.nodeTemplateMap.add("Right",
$(go.Node, "Vertical",
{ copyable: false, deletable: false, movable: false },
$(go.Shape, "Circle",
{ width: 40, height: 40, fill: "lightgreen", strokeWidth: 0, portId: "" }),
$(go.TextBlock,
new go.Binding("text"))
));
myDiagram.groupTemplate =
$(go.Group, "Auto",
{
selectable: false,
isInDocumentBounds: false,
layout: $(go.GridLayout, { wrappingColumn: 1 })
},
$(go.Shape, { fill: "#EEEEEE", strokeWidth: 0 }),
$(go.Placeholder, { padding: new go.Margin(2000, 10) })
);
myDiagram.model = $(go.GraphLinksModel,
{
nodeCategoryProperty: "group",
nodeDataArray: [
{ key: "Left", isGroup: true },
{ key: 1, group: "Left", text: "A" },
{ key: 2, group: "Left", text: "B" },
{ key: "Right", isGroup: true },
{ key: 11, group: "Right", text: "X" },
{ key: 12, group: "Right", text: "Y" },
{ key: 13, group: "Right", text: "Z" },
{ key: 100, text: "Alpha", loc: "250 200" },
{ key: 101, text: "Beta", loc: "200 300" },
{ key: 102, text: "Gamma", loc: "400 200" },
{ key: 103, text: "Delta", loc: "350 350" },
],
linkDataArray: [
{ from: 1, to: 100 },
{ from: 2, to: 101 },
{ from: 100, to: 101 },
{ from: 100, to: 103 },
{ from: 101, to: 102 },
{ from: 102, to: 11 },
{ from: 102, to: 12 },
{ from: 103, to: 13 },
]
});
}
</script>
</head>
<body onload="init()">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px; background-color: #DDDDDD"></div>
The model saved in JSON format after each transaction:
<textarea id="mySavedModel" style="width:100%;height:250px"></textarea>
</body>
</html>
This produces: