I don’t have time now to do this in GoDiagram, but here’s a solution in GoJS. Note that the templates don’t really matter except that the Group template has to have a TwoWay Binding on “isSubGraphExpanded” so that the copied model/diagram has the groups in the desired expansion state. If you don’t want to remember that state in the model, you can work around it with additional code to restore the Groups in the copied Diagram the way that they are in the main Diagram.
<!DOCTYPE html>
<html>
<head>
<title>Replacing Collapsed Groups with Simple Nodes</title>
<!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
<meta name="description" content="Write out a GraphLinksModel in JSON form where the collapsed Groups have been replaced by simple Nodes">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:350px"></div>
<button id="myInitButton">Initial</button>
<button id="myFullButton">Full JSON</button>
<button id="myTopOnlyButton">No Collapsed Groups JSON, Loaded</button>
<textarea id="mySavedModel" style="width:100%;height:450px"></textarea>
<script src="../release/go.js"></script>
<script id="code">
const $ = go.GraphObject.make;
myDiagram =
new go.Diagram("myDiagramDiv",
{
// the layout doesn't really matter
layout: $(go.TreeLayout, { setsPortSpot: false, setsChildPortSpot: false })
});
// the templates don't really matter,
// EXCEPT that the group template(s) need to have a TwoWay Binding on "isSubGraphExpanded"
// so that the copied model produces a copied diagram with the right state for the groups
myDiagram.nodeTemplate =
$(go.Node, "Auto",
$(go.Shape, { fill: "white" },
new go.Binding("fill", "color")),
$(go.TextBlock, { margin: 8 },
new go.Binding("text"))
);
myDiagram.groupTemplate =
$(go.Group, "Vertical",
{
defaultAlignment: go.Spot.Left,
layout: $(go.GridLayout, { wrappingColumn: 1, cellSize: new go.Size(1, 1) })
},
new go.Binding("isSubGraphExpanded").makeTwoWay(),
$(go.Panel, "Horizontal",
$("SubGraphExpanderButton"),
$(go.TextBlock,
new go.Binding("text"))
),
$(go.Panel, "Auto",
$(go.Shape, { fill: "transparent", strokeWidth: 1.5 },
new go.Binding("stroke", "color")),
$(go.Placeholder, { padding: 10 })
)
);
myDiagram.linkTemplate =
$(go.Link,
{ relinkableFrom: true, relinkableTo: true },
$(go.Shape, { stroke: "blue", strokeWidth: 2 }),
$(go.Shape, { toArrow: "OpenTriangle" })
);
function init() {
myDiagram.model = new go.GraphLinksModel(
{
nodeDataArray: [
{ key: -1, text: "Group 1", isGroup: true },
{ key: 1, text: "Alpha", color: "lightblue", group: -1 },
{ key: 2, text: "Beta", color: "orange", group: -1 },
{ key: 3, text: "Gamma", color: "orange", group: -1 },
{ key: -11, text: "SubGroup 1a", isGroup: true, group: -1 },
{ key: 4, text: "Theta", color: "lightgreen", group: -11 },
{ key: -12, text: "SubGroup 1b", isGroup: true, group: -1 },
{ key: 5, text: "Iota", color: "lightblue", group: -12 },
{ key: -2, text: "Group 2", isGroup: true },
{ key: 11, text: "Delta", color: "lightgreen", group: -2 },
{ key: 12, text: "Epsilon", color: "pink", group: -2 },
{ key: -22, text: "SubGroup 2", isGroup: true, group: -2 },
{ key: 21, text: "Zeta", color: "orange", group: -22 },
{ key: 13, text: "Eta", color: "lightgreen", group: -2 },
{ key: -3, text: "Group 3", isGroup: true },
{ key: 31, text: "Kappa", color: "lightgreen", group: -3 },
{ key: 32, text: "Lambda", color: "lightblue", group: -3 },
{ key: 33, text: "Mu", color: "pink", group: -3 },
{ key: 99, text: "Omega", color: "gray" } // test: not a group and not in a group
],
linkDataArray: [
{ from: 1, to: 11 },
{ from: 1, to: 12 },
{ from: 2, to: 21 },
{ from: 3, to: 21 },
{ from: 3, to: 4 },
{ from: 21, to: 4 },
{ from: 21, to: 4 },
{ from: 13, to: 4 },
{ from: 4, to: 5 },
{ from: 5, to: 99 },
{ from: 11, to: 31 },
{ from: 12, to: 32 },
{ from: 13, to: 33 },
]
});
}
init();
document.getElementById("myInitButton").addEventListener("click", e => {
init();
});
document.getElementById("myFullButton").addEventListener("click", e => {
document.getElementById("mySavedModel").textContent = myDiagram.model.toJson();
});
document.getElementById("myTopOnlyButton").addEventListener("click", e => {
// copy the model
const model = go.Model.fromJson(myDiagram.model.toJson());
// construct a new view-less Diagram from the copied Model
const diag = new go.Diagram(
{ // no HTMLDivElement, so we need to specify a viewSize
viewSize: new go.Size(100, 100),
nodeTemplateMap: myDiagram.nodeTemplateMap,
groupTemplateMap: myDiagram.groupTemplateMap,
linkTemplateMap: myDiagram.linkTemplateMap,
"InitialLayoutCompleted": e => {
replaceCollapsedGroupsWithNodes(diag);
// show the simpler JSON
document.getElementById("mySavedModel").textContent = diag.model.toJson();
// OPTIONAL: show the new model in the current diagram
myDiagram.model = go.Model.fromJson(diag.model.toJson());
}
});
diag.model = model;
});
function replaceCollapsedGroupsWithNodes(diag) {
diag.findTopLevelGroups().each(g => walkGroupsReplaceCollapsed(g));
}
function walkGroupsReplaceCollapsed(group) {
if (group.isSubGraphExpanded) {
group.memberParts.each(mem => {
if (mem instanceof go.Group) walkGroupsReplaceCollapsed(mem);
});
} else {
const diag = group.diagram;
const model = diag.model;
const nogroup = { __gohashid: undefined, isGroup: false };
const nodedata = Object.assign({}, group.data, nogroup);
delete nodedata.isGroup;
model.addNodeData(nodedata);
const node = diag.findNodeForData(nodedata);
group.findExternalLinksConnected().each(link => {
const from = link.fromNode;
if (from && (from === group || from.isMemberOf(group))) {
link.fromNode = node;
}
const to = link.toNode;
if (to && (to === group || to.isMemberOf(group))) {
link.toNode = node;
}
});
if (group.containingGroup) node.containingGroup = group.containingGroup;
diag.remove(group);
}
}
</script>
</body>
</html>