Here’s another one, this time using LayeredDigraphLayout instead of TreeLayout:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 700px"></div>
<div>
<button id="add">Add Box</button>
<button onclick="load(0)">Reload 0</button>
<button onclick="load(1)">Reload 1</button>
</div>
<script src="go.js"></script>
<script>
// Route all orthogonal links to turn at the same relative x point.
// This only works for direction: 0.
class ParallelLDLayout extends go.LayeredDigraphLayout {
constructor() {
super();
this.alignOption = go.LayeredDigraphLayout.AlignAll;
this.layeringOption = go.LayeredDigraphLayout.LayerLongestPathSource;
this.layerSpacing = 50;
this.linkSpacing = 20;
}
commitLinks() {
super.commitLinks();
this.network.vertexes.each(v => {
if (v.node /*&& v.node.category === "group-end"*/) {
// assume this.direction === 0
let x = v.node.port.getDocumentBounds().x - 30;
v.sourceEdges.each(e => {
const link = e.link;
if (link && link.isOrthogonal) {
const num = link.pointsCount;
if (num >= 6) {
const pts = link.points.copy();
const p3 = pts.elt(num-3);
pts.setElt(3, new go.Point(x, p3.y));
const p2 = pts.elt(num-4);
pts.setElt(2, new go.Point(x, p2.y));
link.points = pts;
}
}
});
}
});
}
}
function init() {
const $ = go.GraphObject.make;
myDiagram = new go.Diagram("myDiagramDiv",
{
layout: new ParallelLDLayout()
});
myDiagram.nodeTemplateMap.add("global", $(go.Node, "Spot",
$(go.Shape, "Circle", {
fill: "transparent",
stroke: '#01778e',
width: 50,
height: 50,
portId: "",
fromLinkable: true,
toLinkable: true,
cursor: "pointer",
}),
$(go.TextBlock,
new go.Binding("text", "key"),
{
verticalAlignment: go.Spot.Center,
textAlign: "center",
stroke: '#1d2024',
}),
$(go.Shape, "Rectangle", {
fill: null,
stroke: null,
desiredSize: new go.Size(0, 0),
alignment: new go.Spot(0, 0.5, 0, 0),
portId: "Left",
}),
$(go.Shape, "Rectangle", {
fill: null,
stroke: null,
desiredSize: new go.Size(0, 0),
alignment: new go.Spot(1, 0.5, 0, 0),
portId: "Right",
}),
));
myDiagram.nodeTemplate = $(go.Node, "Spot",
$(go.Shape, "Rectangle", {
fill: "transparent",
stroke: "transparent",
width: 124,
height: 152,
portId: "",
fromLinkable: true,
toLinkable: true,
cursor: "pointer",
}),
$(go.Panel, "Position",
$(go.Shape, "RoundedRectangle", {
fill: "transparent",
width: 64,
height: 64,
position: new go.Point(0, 12),
}),
$(go.TextBlock,
new go.Binding("text", "key"),
{
width: 64,
position: new go.Point(0, 84),
verticalAlignment: go.Spot.Center,
textAlign: "center",
},
),
),
$(go.Shape, "Rectangle", {
fill: null,
stroke: null,
desiredSize: new go.Size(0, 0),
alignment: new go.Spot(0, 0.5, 30, 0),
portId: "Left",
}),
$(go.Shape, "Rectangle", {
fill: null,
stroke: null,
desiredSize: new go.Size(0, 0),
alignment: new go.Spot(1, 0.5, -30, 0),
portId: "Right",
}),
);
myDiagram.nodeTemplateMap.add("group-start",
$(go.Node));
myDiagram.nodeTemplateMap.add("group-end",
$(go.Node));
myDiagram.linkTemplate = $(go.Link,
//go.Link.Orthogonal,
{
relinkableFrom: true,
relinkableTo: true,
selectable: true,
reshapable: true,
corner: 10,
toShortLength: 8,
fromEndSegmentLength: 40,
toEndSegmentLength: 40,
routing: go.Link.Orthogonal, //go.Link.AvoidsNodes,
},
$(go.Shape, { strokeWidth: 1 }),
$(go.Shape, { toArrow: "Standard" }),
);
myDiagram.groupTemplate = $(go.Group, "Spot",
{
layout: new ParallelLDLayout()
},
$(go.Panel, "Horizontal",
$(go.Shape, "Rectangle", {
width: 30,
fill: "transparent",
strokeWidth: 0,
stretch: go.GraphObject.Fill
}),
$(go.Panel, "Auto",
$(go.Shape, "Rectangle", {
fill: "transparent",
strokeWidth: 0,
stretch: go.GraphObject.Fill
}),
$(go.Panel, "Auto",
$(go.Shape, "RoundedRectangle", // surrounds everything
{
parameter1: 10,
fill: "white",
stroke: '#999999',
minSize: new go.Size(240, 220),
spot1: go.Spot.TopLeft,
spot2:go.Spot.BottomRight
}
),
$(go.Panel, "Vertical", // position header above the subgraph
$(go.Panel, "Auto", {
height: 28,
alignment: go.Spot.Left,
padding: new go.Margin(8, 12, 0, 12),
},
$(go.TextBlock, // stage title near top, next to button
new go.Binding("text", "key")
)
),
$(go.Placeholder, // represents area for all member parts
{
padding: new go.Margin(4, 0, 4, 0),
background: "transparent",
minSize: new go.Size(240, 160),
}),
$(go.Panel, "Auto", {
height: 28,
stretch: go.GraphObject.Fill
})
)
)
),
$(go.Shape, "Rectangle", {
width: 30,
fill: "transparent",
strokeWidth: 0,
stretch: go.GraphObject.Fill
})
),
$(go.Shape, "Rectangle", {
fill: null,
stroke: null,
desiredSize: new go.Size(0, 0),
alignment: new go.Spot(0, 0.5, 30, 0),
portId: "Left",
}),
$(go.Shape, "Rectangle", {
fill: null,
stroke: null,
desiredSize: new go.Size(0, 0),
alignment: new go.Spot(1, 0.5, -30, 0),
portId: "Right",
}
));
load(0);
}
function load(choice) {
let nda = [];
let lda = [];
if (choice === 0) {
nda = [
{
key: "Start",
category: "global",
},
{
key: "End",
category: "global",
},
{
key: "Group1",
isGroup: true,
},
{
key: "Group1-start",
group: "Group1",
category: "group-start"
},
{
key: "Group1-end",
group: "Group1",
category: "group-end"
},
{
key: "Box1",
group: "Group1",
},
{
key: "Box2",
group: "Group1",
},
{
key: "Box3",
group: "Group1",
},
{
key: "Box4",
group: "Group1",
}
];
lda = [
{
from: "Start",
to: "Group1",
fromPort: "Right",
toPort: "Left",
},
{
from: "Group1",
to: "End",
fromPort: "Right",
toPort: "Left",
},
{
from: "Group1-start",
to: "Box1",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box1",
to: "Box2",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box2",
to: "Box3",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box2",
to: "Box4",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box3",
to: "Group1-end",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box4",
to: "Group1-end",
fromPort: "Right",
toPort: "Left",
}
];
} else if (choice === 1) {
nda = [
{
key: "Start",
category: "global",
},
{
key: "End",
category: "global",
},
{
key: "Group1",
isGroup: true,
},
{
key: "Group1-start",
group: "Group1",
category: "group-start"
},
{
key: "Group1-end",
group: "Group1",
category: "group-end"
},
{
key: "Box1",
group: "Group1",
},
{
key: "Box2",
group: "Group1",
},
{
key: "Box3",
group: "Group1",
},
{
key: "Box4",
group: "Group1",
},
{
key: "Box5",
group: "Group1",
},
{
key: "Box6",
group: "Group1",
},
{
key: "Box7",
group: "Group1",
}
];
lda = [
{
from: "Start",
to: "Group1",
fromPort: "Right",
toPort: "Left",
},
{
from: "Group1",
to: "End",
fromPort: "Right",
toPort: "Left",
},
{
from: "Group1-start",
to: "Box1",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box1",
to: "Box2",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box1",
to: "Box7",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box2",
to: "Box3",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box2",
to: "Box6",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box3",
to: "Box4",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box3",
to: "Box5",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box4",
to: "Group1-end",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box5",
to: "Group1-end",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box6",
to: "Group1-end",
fromPort: "Right",
toPort: "Left",
},
{
from: "Box7",
to: "Group1-end",
fromPort: "Right",
toPort: "Left",
}
];
}
const model = new go.GraphLinksModel();
model.nodeDataArray = nda;
model.linkDataArray = lda;
model.linkFromPortIdProperty = "fromPort";
model.linkToPortIdProperty = "toPort";
model.nodeGroupKey = "group";
myDiagram.model = model;
}
window.addEventListener("DOMContentLoaded", init);
document.getElementById('add').addEventListener('click', e => {
const groupKey = 'Group1'
const endKey = `${groupKey}-end`;
const existingKey = 'Box1';
const newdata = { group: groupKey };
myDiagram.model.addNodeData(newdata);
const newKey = newdata.key;
myDiagram.model.addLinkData({
from: existingKey,
fromPort: "Right",
to: newKey,
toPort: "Left",
});
myDiagram.model.addLinkData({
from: newKey,
fromPort: "Right",
to: endKey,
toPort: "Left",
});
});
</script>
</body>
</html>