I updated the updateDiagram function which does your suggestion however, as it stands, it’s pretty naive. It particularly will fail for the case where adjacent nodes are inserted at the same time. The snippet relies on the incoming node to always exist in the diagram so new nodes can determine a location value. I’ll continue to think about a more generic solution on my end as well.
<!DOCTYPE html>
<html>
<head>
<title>Stream and merge new model data</title>
<!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
<div style="margin-bottom: 10px">
<button onclick="loadDemoData()" style="padding: 8px 16px; cursor: pointer">Restart Stream</button>
<button
onclick="myDiagram.model = new go.GraphLinksModel([], [], {
'linkKeyProperty': 'key'
})"
style="padding: 8px 16px; cursor: pointer; margin-left: 10px"
>
Clear Diagram
</button>
</div>
<div style="width: 100%; display: flex; justify-content: space-between">
<div id="myDiagramDiv" style="flex-grow: 1; height: 400px; border: solid 1px black"></div>
</div>
<textarea id="mySavedModel" style="width: 100%; height: 250px"></textarea>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/release/go.js"></script>
<script id="code">
const $ = go.GraphObject.make;
const myDiagram = new go.Diagram("myDiagramDiv", {
// make sure the top-left corner of the viewport is occupied
contentAlignment: new go.Spot(0, 0.25),
layout: $(go.TreeLayout, {
layerSpacing: 100,
nodeSpacing: 50,
setsChildPortSpot: false,
sorting: go.TreeSorting.Ascending,
}),
padding: new go.Margin(30, 250, 30, 30),
scrollMargin: new go.Margin(30, 250, 30, 30),
autoScrollRegion: new go.Margin(16, 48, 16, 48),
allowMove: false,
allowCopy: false,
maxSelectionCount: 1,
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,
"Auto",
{ locationSpot: go.Spot.Center },
new go.Binding("location", "loc", go.Point.parse),
$(go.Shape, { fill: "white" }, new go.Binding("fill", "color")),
$(go.TextBlock, { margin: 8 }, new go.Binding("text"))
);
myDiagram.linkTemplate = $(
go.Link,
{
routing: go.Link.AvoidsNodes,
reshapable: true,
corner: 10,
fromEndSegmentLength: 20,
toEndSegmentLength: 20,
deletable: false,
movable: false,
},
// the highlight path Shape
$(
go.Shape,
{ isPanelMain: true, strokeWidth: 10, stroke: "transparent" },
// when highlighted, show this thick Shape in red
new go.Binding("stroke", "isHighlighted", (h) => (h ? "red" : "transparent")).ofObject()
),
// the normal path Shape
$(go.Shape, { isPanelMain: true, strokeWidth: 2 }),
$(go.Shape, { toArrow: "OpenTriangle" })
);
// Initialize with empty model
myDiagram.model = new go.GraphLinksModel([], [], {
linkKeyProperty: "key",
});
// Function to update diagram with full model data using merge
function updateDiagram(modelData) {
const model = myDiagram.model;
model.commit((m) => {
if (modelData.modelData !== undefined) {
m.assignAllDataProperties(m.modelData, modelData.modelData);
}
m.mergeNodeDataArray(
modelData.nodeDataArray.map((n) => {
// check if node exists in diagram already
const node = myDiagram.findNodeForKey(n.key);
// if not, try to position it relative to its incoming link's from node
if (node === null) {
const incomingLink = modelData.linkDataArray.find((l) => l.to === n.key);
if (incomingLink !== undefined) {
const fromNode = myDiagram.findNodeForKey(incomingLink.from);
if (fromNode !== null) {
const fromLoc = fromNode.location;
return { ...n, loc: fromLoc.x + 150 + " " + fromLoc.y };
}
}
}
return n;
})
);
if (modelData.linkDataArray !== undefined && m instanceof go.GraphLinksModel) {
m.mergeLinkDataArray(modelData.linkDataArray);
}
}, "merge data");
}
// Demo data - full diagram JSON for each step
function loadDemoData() {
const demoData = [
{
class: "go.GraphLinksModel",
nodeDataArray: [{ key: 1, text: "Alpha", color: "lightblue" }],
linkDataArray: [],
},
{
class: "go.GraphLinksModel",
nodeDataArray: [
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 3, text: "Gamma", color: "lightgreen" },
],
linkDataArray: [{ key: -1, from: 1, to: 3 }],
},
{
class: "go.GraphLinksModel",
nodeDataArray: [
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 3, text: "Gamma", color: "lightgreen" },
{ key: 4, text: "Delta", color: "pink" },
],
linkDataArray: [
{ key: -1, from: 1, to: 3 },
{ key: -2, from: 3, to: 4 },
],
},
{
class: "go.GraphLinksModel",
nodeDataArray: [
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 3, text: "Gamma", color: "lightgreen" },
{ key: 4, text: "Delta", color: "pink" },
{ key: 5, text: "Epsilon", color: "yellow" },
],
linkDataArray: [
{ key: -1, from: 1, to: 3 },
{ key: -2, from: 3, to: 4 },
{ key: -3, from: 4, to: 5 },
],
},
{
class: "go.GraphLinksModel",
nodeDataArray: [
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 2, text: "Beta", color: "orange" },
{ key: 3, text: "Gamma", color: "lightgreen" },
{ key: 4, text: "Delta", color: "pink" },
{ key: 5, text: "Epsilon", color: "yellow" },
],
linkDataArray: [
{ key: -1, from: 1, to: 2 },
{ key: -4, from: 2, to: 3 },
{ key: -2, from: 3, to: 4 },
{ key: -3, from: 4, to: 5 },
],
},
{
class: "go.GraphLinksModel",
nodeDataArray: [
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 2, text: "Beta", color: "orange" },
{ key: 3, text: "Gamma", color: "lightgreen" },
{ key: 3.5, text: "Gamma+", color: "violet" },
{ key: 4, text: "Delta", color: "pink" },
{ key: 5, text: "Epsilon", color: "yellow" },
],
linkDataArray: [
{ key: -1, from: 1, to: 2 },
{ key: -4, from: 2, to: 3 },
{ key: -2, from: 3, to: 3.5 },
{ key: -5, from: 3.5, to: 4 },
{ key: -3, from: 4, to: 5 },
],
},
];
// Simulate streaming with delays
demoData.forEach((data, index) => {
setTimeout(() => updateDiagram(data), index * 1000);
});
}
// Start demo on page load
loadDemoData();
</script>
</body>
</html>