This is incomplete, but it’s what I could make for you during the weekend:
<!DOCTYPE html>
<html>
<head>
<title>Relative location for Group members</title>
<!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/gojs"></script>
<script id="code">
function init() {
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
// allow double-click in background to create a new node
"clickCreatingTool.archetypeNodeData": { text: "Node", color: "white" },
// allow Ctrl-G to call groupSelection()
"commandHandler.archetypeGroupData": { text: "Group", isGroup: true, color: "blue" },
"ModelChanged": function(e) {
if (e.isTransactionFinished) {
document.getElementById("savedModel").textContent = myDiagram.model.toJson();
}
},
// enable undo & redo
"undoManager.isEnabled": true
});
// Define the appearance and behavior for Nodes:
// Assume the data.loc property has a relative location for the node compared to its containing group.
// (If there is no group data, it is the location in document coordinates.)
// toLocation should not depend on any Node.location because they might not have been initialized yet.
function toLocation(data) {
var loc = go.Point.parse(data.loc);
if (data.group !== undefined) {
var groupdata = myDiagram.model.findNodeDataForKey(data.group);
if (groupdata) {
loc.add(toLocation(groupdata));
}
}
return loc;
};
// fromLocation just saves in data.loc either the absolute location if there's no containing Group,
// or the relative location with its containing Group.
function fromLocation(location, data) {
if (data.group !== undefined) {
var group = myDiagram.findNodeForKey(data.group);
if (group) {
var loc = location.copy().subtract(group.location);
data.loc = loc.x.toFixed(2) + " " + loc.y.toFixed(2); //go.Point.stringify(loc);
}
} else {
data.loc = go.Point.stringify(location);
}
};
// this is a Part.dragComputation function for limiting where a Node may be dragged
function stayInGroup(part, pt, gridpt) {
// don't constrain top-level nodes
const grp = part.containingGroup;
if (grp === null) return pt;
// try to stay within the background Shape of the Group
const back = grp.resizeObject;
if (back === null) return pt;
// allow dragging a Node out of a Group if the Shift key is down
if (part.diagram.lastInput.shift) return pt;
const r = back.getDocumentBounds();
const b = part.actualBounds;
const loc = part.location;
// now limit the location appropriately
const x = Math.max(r.x + 10, Math.min(pt.x, r.right - 10 - b.width - 1)) + (loc.x - b.x);
const y = Math.max(r.y + 10, Math.min(pt.y, r.bottom - 10 - b.height - 1)) + (loc.y - b.y);
return new go.Point(x, y);
}
// These nodes have text surrounded by a rounded rectangle
// whose fill color is bound to the node data.
// The user can drag a node by dragging its TextBlock label.
// Dragging from the Shape will start drawing a new link.
myDiagram.nodeTemplate =
$(go.Node, "Auto",
new go.Binding("location", "", toLocation).makeTwoWay(fromLocation),
{ dragComputation: stayInGroup },
$(go.Shape, "RoundedRectangle",
{
fill: "white", // the default fill, if there is no data-binding
portId: "", cursor: "pointer", // the Shape is the port, not the whole Node
// allow all kinds of links from and to this port
fromLinkable: true, fromLinkableSelfNode: true, fromLinkableDuplicates: true,
toLinkable: true, toLinkableSelfNode: true, toLinkableDuplicates: true
},
new go.Binding("fill", "color")),
$(go.TextBlock,
{
font: "bold 14px sans-serif",
stroke: '#333',
margin: 6, // make some extra space for the shape around the text
isMultiline: false, // don't allow newlines in text
editable: true // allow in-place editing by user
},
new go.Binding("text", "text").makeTwoWay()) // the label shows the node data's text
);
// The link shape and arrowhead have their stroke brush data bound to the "color" property
myDiagram.linkTemplate =
$(go.Link,
{ relinkableFrom: true, relinkableTo: true }, // allow the user to relink existing links
$(go.Shape,
{ strokeWidth: 2 },
new go.Binding("stroke", "color")),
$(go.Shape,
{ toArrow: "Standard", stroke: null },
new go.Binding("fill", "color"))
);
// Define the appearance and behavior for Groups:
// Groups consist of a title in the color given by the group node data
// above a translucent gray rectangle surrounding the member parts
myDiagram.groupTemplate =
$(go.Group, "Vertical",
new go.Binding("location", "", toLocation).makeTwoWay(fromLocation),
{
selectionObjectName: "SHAPE", // selection handle goes around shape, not label
resizable: true, resizeObjectName: "SHAPE",
ungroupable: true
}, // enable Ctrl-Shift-G to ungroup a selected Group
$(go.TextBlock,
{
font: "bold 19px sans-serif",
isMultiline: false, // don't allow newlines in text
editable: true // allow in-place editing by user
},
new go.Binding("text", "text").makeTwoWay(),
new go.Binding("stroke", "color")),
$(go.Shape, "Rectangle", // the rectangular shape around the members
{ name: "SHAPE", fill: "rgba(128,128,128,0.2)", stroke: "gray", strokeWidth: 3 },
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify)),
);
// Define the behavior for the Diagram background:
// Create the Diagram's Model:
var nodeDataArray = [
{ key: 1, text: "Alpha", color: "lightblue", loc: "0 0" },
{ key: 2, text: "Beta", color: "orange", loc: "100 0" },
{ key: 3, text: "Gamma", color: "lightgreen", group: 5, loc: "10 30" },
{ key: 4, text: "Delta", color: "pink", group: 5, loc: "100 100" },
{ key: 5, text: "Epsilon", color: "green", isGroup: true, loc: "50 100", size: "170 130" }
];
var linkDataArray = [
{ from: 1, to: 2, color: "blue" },
{ from: 2, to: 2 },
{ from: 3, to: 4, color: "green" },
{ from: 3, to: 1, color: "purple" }
];
myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:400px; height:400px"></div>
<pre id="savedModel"></pre>
</div>
</body>
</html>