[EDIT: updated to fix the vertical alignment of expanded ports]
<!DOCTYPE html>
<html>
<head>
<title>Tethered, Expandable, Labeled Ports</title>
<!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:700px"></div>
<script src="https://unpkg.com/gojs"></script>
<script id="code">
const $ = go.GraphObject.make;
const myDiagram =
$(go.Diagram, "myDiagramDiv",
{
layout: $(go.LayeredDigraphLayout),
"undoManager.isEnabled": true
});
const EW = 30;
const PW = 13;
const PH = 13;
const PLW = 13;
const PLH = 32;
function portStyle() {
return { width: PW-1, height: PH-1, fill: "transparent" };
}
PortTemplateSimple =
$(go.Panel, "Spot",
$(go.Shape, portStyle(),
new go.Binding("portId", "id")),
new go.Binding("position", "itemIndex", simplePosition).ofObject()
);
function simplePosition(i, panel) {
if (panel.panel.itemArray.length <= 3) return new go.Point(0, i*PH);
return new go.Point(0, 0);
}
PortTemplateExpanded =
$(go.Panel,
$(go.Shape, portStyle(),
new go.Binding("portId", "id"),
new go.Binding("position", "itemIndex", i => new go.Point(0, i*PH)).ofObject()),
$(go.Shape, { strokeDashArray: [2, 2] },
new go.Binding("geometry", "itemIndex", indexToGeo).ofObject()),
);
function indexToGeo(i, shape) {
const tot = shape.panel.panel.itemArray.length;
return new go.Geometry().add(new go.PathFigure(0, 0)
.add(new go.PathSegment(go.PathSegment.Move, EW+PW, tot*PH))
.add(new go.PathSegment(go.PathSegment.Move, EW+PW, tot*PH/2))
.add(new go.PathSegment(go.PathSegment.Line, PW, i*PH + PH/2)));
}
PortTemplateExpandedLabeled =
$(go.Panel,
$(go.Shape, portStyle(),
new go.Binding("portId", "id"),
new go.Binding("position", "itemIndex", i => new go.Point(0, i*PLH)).ofObject()),
$(go.TextBlock, { alignment: new go.Spot(0.5, 0.5, 0, 1) },
new go.Binding("text", "id"),
new go.Binding("position", "itemIndex", i => new go.Point(0, i*PLH + PH + 1)).ofObject()),
$(go.Shape, { strokeDashArray: [2, 2] },
new go.Binding("geometry", "itemIndex", indexToGeoL).ofObject()),
);
function indexToGeoL(i, shape) {
const tot = shape.panel.panel.itemArray.length;
return new go.Geometry().add(new go.PathFigure(0, 0)
.add(new go.PathSegment(go.PathSegment.Move, EW+PLW, tot*PLH))
.add(new go.PathSegment(go.PathSegment.Move, EW+PLW, tot*PLH/2))
.add(new go.PathSegment(go.PathSegment.Line, PLW, i*PLH + PH/2)));
}
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{ locationSpot: go.Spot.Center, locationObjectName: "BODY",
layoutConditions: go.Part.LayoutStandard & ~go.Part.LayoutNodeSized },
$(go.Panel, "Table",
{ name: "BODY", alignmentFocusName: "SHAPE" },
$(go.Shape, { name: "SHAPE", row: 0, width: 50, height: 50, fill: "lightblue", strokeWidth: 0, portId: "" }),
$(go.TextBlock, { row: 1, stretch: go.GraphObject.Horizontal, maxLines: 1, overflow: go.TextBlock.OverflowEllipsis },
new go.Binding("text"))
),
$(go.Panel, "Spot",
{ alignment: go.Spot.Left, alignmentFocus: go.Spot.Right },
$(go.Panel,
new go.Binding("itemArray", "ports"),
{ itemTemplate: PortTemplateSimple },
new go.Binding("itemTemplate", "", data => data.expanded
? (data.labeled ? PortTemplateExpandedLabeled : PortTemplateExpanded)
: PortTemplateSimple)
),
$(go.TextBlock, { visible: false, alignment: new go.Spot(0.5, 0.5, 0, 1) },
new go.Binding("text", "ports", a => a.length.toString()),
new go.Binding("visible", "", data => !data.expanded && data.ports.length > 3))
)
);
myDiagram.model = $(go.GraphLinksModel,
{
linkFromPortIdProperty: "fpid",
linkToPortIdProperty: "tpid",
nodeDataArray: [
{
key: 1, text: "Just 2",
ports: [ { id: "a" }, { id: "b" } ]
},
{
key: 2, text: "No Labs", expanded: true,
ports: [ { id: "a" }, { id: "b" }, { id: "c" } ]
},
{
key: 3, text: "Labels", expanded: true, labeled: true,
ports: [ { id: "a" }, { id: "b" }, { id: "c" } ]
},
{
key: 4, text: "Collapsed", expanded: false,
ports: [ { id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }, { id: "e" }, { id: "f" }, { id: "g" } ]
},
{
key: 5, text: "Expanded", expanded: true,
ports: [ { id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }, { id: "e" }, { id: "f" }, { id: "g" } ]
}
],
linkDataArray: [
{ from: 1, to: 2, tpid: "a" },
{ from: 1, to: 3, tpid: "c" },
{ from: 1, to: 4, tpid: "c" },
{ from: 1, to: 5, tpid: "e" }
]
});
</script>
</body>
</html>
produces: