Hi, Walter, thank you for reply, we’ve tried with your recommendations for TreeLayout , but unlucky, the tricky part is to have that “diamond”, “circles” elements to be center-aligned relatively to main nodes, but in the same way main nodes should be aligned center vertically, relative to main branch (like on first screenshot), also “return” dashed arrow to not shift any nodes.
We end up by founding this example Flowgrammer, which in general is a perfect representation of our data flow. What we did is tried to add those small elements, but need an assistance to align them properly.
here is the source code:
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto">
<div id="navSide"
class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div>
<!-- * * * * * * * * * * * * * -->
<!-- Start of GoJS sample code -->
<script src="https://gojs.net/latest/release/go.js"></script>
<script src="https://gojs.net/latest/extensions/ParallelLayout.js"></script>
<div class="p-4 w-full">
<style>
/* Use a Flexbox to make the Palette/Overview/Diagram responsive and size things relatively */
#myFlexDiv {
display: flex;
width: 100%;
height: 95vh;
}
#myPODiv {
display: flex;
}
@media (min-width: 768px) {
#myFlexDiv {
flex-flow: row;
}
#myPODiv {
width: 105px;
height: 100%;
margin-right: 3px;
flex-flow: column;
}
#myPaletteDiv {
height: 80%;
}
#myOverviewDiv {
margin-top: 3px;
flex: 1;
}
#myDiagramDiv {
flex: 1;
}
}
@media (max-width: 767px) {
#myFlexDiv {
flex-flow: column;
align-items: center;
}
#myPODiv {
width: 90%;
height: 105px;
margin-bottom: 3px;
flex-flow: row;
}
#myPaletteDiv {
width: 75%;
}
#myOverviewDiv {
margin-left: 3px;
flex: 1;
}
#myDiagramDiv {
width: 90%;
flex: 1;
}
}
</style>
<script>
// two custom figures, for "For Each" loops
go.Shape.defineFigureGenerator("ForEach", function (shape, w, h) {
var param1 = shape ? shape.parameter1 : NaN; // length of triangular area in direction that it is pointing
if (isNaN(param1)) param1 = 10;
var d = Math.min(h / 2, param1);
var geo = new go.Geometry();
var fig = new go.PathFigure(w, h - d, true);
geo.add(fig);
fig.add(new go.PathSegment(go.PathSegment.Line, w / 2, h));
fig.add(new go.PathSegment(go.PathSegment.Line, 0, h - d));
fig.add(new go.PathSegment(go.PathSegment.Line, 0, 0));
fig.add(new go.PathSegment(go.PathSegment.Line, w, 0).close());
geo.spot1 = go.Spot.TopLeft;
geo.spot2 = new go.Spot(1, 1, 0, Math.min(-d + 2, 0));
return geo;
});
go.Shape.defineFigureGenerator("EndForEach", function (shape, w, h) {
var param1 = shape ? shape.parameter1 : NaN; // length of triangular area in direction that it is pointing
if (isNaN(param1)) param1 = 10;
var d = Math.min(h / 2, param1);
var geo = new go.Geometry();
var fig = new go.PathFigure(w, d, true);
geo.add(fig);
fig.add(new go.PathSegment(go.PathSegment.Line, w, h));
fig.add(new go.PathSegment(go.PathSegment.Line, 0, h));
fig.add(new go.PathSegment(go.PathSegment.Line, 0, d));
fig.add(new go.PathSegment(go.PathSegment.Line, w / 2, 0).close());
geo.spot1 = new go.Spot(0, 0, 0, Math.min(d, 0));
geo.spot2 = go.Spot.BottomRight;
return geo;
});
function init() {
var $ = go.GraphObject.make;
// initialize main Diagram
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
allowMove: false,
allowCopy: false,
"SelectionDeleting": function (e) { // before a delete happens
// handle deletions by excising the node and reconnecting the link where the node had been
new go.List(e.diagram.selection).each(function (part) { deletingNode(part); });
},
initialContentAlignment: go.Spot.Left,
layout: $(ParallelLayout,
{
angle: 90,
layerSpacing: 40,
nodeSpacing: 60,
alignment: go.TreeLayout.AlignmentStart,
setsPortSpot: false,
}
),
"ExternalObjectsDropped": function (e) { // handle drops from the Palette
var newnode = e.diagram.selection.first();
if (!newnode) return;
if (!(newnode instanceof go.Group) && newnode.linksConnected.count === 0) {
// when the selection is dropped but not hooked up to the rest of the graph, delete it
e.diagram.removeParts(e.diagram.selection, false);
} else {
e.diagram.commandHandler.scrollToPart(newnode);
}
},
"undoManager.isEnabled": true
});
// dragged nodes are translucent so that the user can see highlighting of links and nodes
myDiagram.findLayer("Tool").opacity = 0.5;
// some common styles for most of the node templates
function nodeStyle() {
return {
deletable: false,
locationSpot: go.Spot.Center,
mouseDragEnter: function (e, node) {
var sh = node.findObject("SHAPE");
if (sh) sh.fill = "lime";
},
mouseDragLeave: function (e, node) {
var sh = node.findObject("SHAPE");
if (sh) sh.fill = "white";
},
mouseDrop: dropOntoNode
};
}
function shapeStyle() {
return { name: "SHAPE", fill: "white" };
}
function textStyle() {
return [
{ name: "TEXTBLOCK", textAlign: "center", editable: true },
new go.Binding("text").makeTwoWay()
];
}
const makePort = (
name,
align,
fromSpot,
output,
input,
color
) => {
const props = {
fill: color || 'transparent',
stroke: color || 'black',
strokeWidth: 1,
desiredSize: new go.Size(2, 2),
portId: name,
alignment: align,
fromSpot: align,
toSpot: align,
};
return $(go.Shape,
props
);
};
const makePorts = () => {
return [
makePort('T', go.Spot.Top, go.Spot.Top, true, true, 'red'),
makePort('L', go.Spot.Left, go.Spot.Left, true, true, 'gold'),
makePort('R', go.Spot.Right, go.Spot.Right, true, true, 'cyan'),
makePort('B', go.Spot.Bottom, go.Spot.Bottom, true, true, 'magenta')
]
}
// define the Node templates
myDiagram.nodeTemplate = // regular action steps
$(go.Node, "Auto", nodeStyle(),
{ deletable: true }, // override nodeStyle()
{ minSize: new go.Size(250, 80) },
$(go.Shape, shapeStyle()),
$(go.TextBlock, textStyle(),
// { margin: 4 }
)
);
myDiagram.nodeTemplateMap.add("Start",
$(go.Node, "Auto", nodeStyle(),
{ desiredSize: new go.Size(250, 80) },
$(go.Shape, "Rectangle", shapeStyle()),
$(go.TextBlock, textStyle(), "Start")
));
myDiagram.nodeTemplateMap.add("End",
$(go.Node, "Auto", nodeStyle(),
{ desiredSize: new go.Size(250, 80) },
$(go.Shape, "Rectangle", shapeStyle()),
$(go.TextBlock, textStyle(), "End")
));
myDiagram.nodeTemplateMap.add("For",
$(go.Node, "Auto", nodeStyle(),
{ minSize: new go.Size(250, 80) },
$(go.Shape, "ForEach", shapeStyle()),
$(go.TextBlock, textStyle(), "For Each",
// { margin: 4 }
)
));
myDiagram.nodeTemplateMap.add("ForPoint",
$(go.Node, "Auto", nodeStyle(),
{ minSize: new go.Size(40, 40), portId: '' },
$(go.Shape, "Circle", shapeStyle()),
$(go.TextBlock, textStyle(), "R"),
makePorts()
));
myDiagram.nodeTemplateMap.add("EndFor",
$(go.Node, nodeStyle(),
$(go.Shape, "EndForEach", shapeStyle(),
{ desiredSize: new go.Size(4, 4) })
));
myDiagram.nodeTemplateMap.add("While",
$(go.Node, "Auto", nodeStyle(),
{ minSize: new go.Size(250, 80) },
$(go.Shape, "ForEach", shapeStyle(),
{ angle: -90, spot2: new go.Spot(1, 1, -6, 0) }),
$(go.TextBlock, textStyle(), "While",
// { margin: 4 }
)
));
myDiagram.nodeTemplateMap.add("EndWhile",
$(go.Node, nodeStyle(),
$(go.Shape, "Circle", shapeStyle(),
{ desiredSize: new go.Size(4, 4) })
));
myDiagram.nodeTemplateMap.add("If",
$(go.Node, "Auto", nodeStyle(),
{ minSize: new go.Size(250, 80) },
$(go.Shape, "Diamond", shapeStyle()),
$(go.TextBlock, textStyle(), "If")
));
myDiagram.nodeTemplateMap.add("EndIf",
$(go.Node, nodeStyle(),
$(go.Shape, "Diamond", shapeStyle(),
{
desiredSize: new go.Size(4, 4)
}
)
));
myDiagram.nodeTemplateMap.add("Switch",
$(go.Node, "Auto", nodeStyle(),
{ minSize: new go.Size(250, 80) },
$(go.Shape, "TriangleUp", shapeStyle()),
$(go.TextBlock, textStyle(), "Switch")
));
myDiagram.nodeTemplateMap.add("StartSwitch",
$(go.Node, "Auto", nodeStyle(),
{ minSize: new go.Size(40, 40) },
$(go.Shape, "Diamond", shapeStyle()),
$(go.TextBlock, textStyle(), "StartSwitch"),
makePorts()
));
myDiagram.nodeTemplateMap.add("Merge",
$(go.Node, nodeStyle(),
$(go.Shape, "TriangleDown", shapeStyle(),
{ desiredSize: new go.Size(4, 4) })
));
function groupColor(cat) {
switch (cat) {
case "If": return "rgba(255,0,0,0.25)";
case "For": return "rgba(0,255,0,0.25)";
case "While": return "rgba(0,0,255,0.25)";
default: return "rgba(0,0,0,0.25)";
}
}
// define the Group template, required but unseen
myDiagram.groupTemplate =
$(go.Group, "Auto",
{
locationSpot: go.Spot.Center,
// avoidableMargin: 30, // extra space on the sides
layout: $(ParallelLayout,
{
angle: 90,
layerSpacing: 40,
nodeSpacing: 60,
alignment: go.TreeLayout.AlignmentStart,
// setsPortSpot: false,
}
),
mouseDragEnter: function (e, group) {
var sh = group.findObject("SHAPE");
if (sh) { sh.width = Math.max(20, group.actualBounds.width - 20); sh.stroke = "lime"; }
},
mouseDragLeave: function (e, group) {
var sh = group.findObject("SHAPE");
if (sh) sh.stroke = null;
},
mouseDrop: dropOntoNode
},
$(go.Shape, "RoundedRectangle",
{ fill: "rgba(0,0,0,0.55)", strokeWidth: 0, spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight },
new go.Binding("fill", "cat", groupColor)),
$(go.Placeholder),
$(go.Shape, "LineH",
{
name: "SHAPE",
height: 0, alignment: go.Spot.Bottom,
stroke: null, strokeWidth: 8
})
);
myDiagram.linkTemplate =
$(go.Link,
{
selectable: false,
deletable: false,
// routing: go.Link.Orthogonal,
routing: go.Link.AvoidsNodes,
corner: 1,
toShortLength: 2,
// links cannot be deleted
// If a node from the Palette is dragged over this node, its outline will turn green
mouseDragEnter: function (e, link) { if (!isLoopBack(link)) link.isHighlighted = true; },
mouseDragLeave: function (e, link) { link.isHighlighted = false; },
// if a node from the Palette is dropped on a link, the link is replaced by links to and from the new node
mouseDrop: dropOntoLink,
},
// new go.Binding("fromSpot", "fromSpot", go.Spot.parse),
// new go.Binding("toSpot", "toSpot", go.Spot.parse),
$(go.Shape, { isPanelMain: true, stroke: "transparent", strokeWidth: 8 },
new go.Binding("stroke", "isHighlighted", function (h) { return h ? "lime" : "transparent"; }).ofObject()),
$(go.Shape,
{ isPanelMain: true, stroke: "black", strokeWidth: 1.5, },
new go.Binding('strokeDashArray', '', l => l.dash ? [2, 2] : undefined)
),
$(go.Shape, { toArrow: "Standard", strokeWidth: 0 }),
// $(go.TextBlock, { segmentIndex: -2, segmentFraction: 0.75, editable: true },
// new go.Binding("text").makeTwoWay(),
// new go.Binding("background", "text", function(t) { return t ? "white" : null; }))
);
function isLoopBack(link) {
if (!link) return false;
if (link.fromNode.containingGroup !== link.toNode.containingGroup) return false;
var cat = link.fromNode.category;
return (cat === "EndFor" || cat === "EndWhile" || cat === "EndIf");
}
// A node dropped onto a Merge node is spliced into a link coming into that node;
// otherwise it is spliced into a link that is coming out of that node.
function dropOntoNode(e, oldnode) {
if (oldnode instanceof go.Group) {
var merge = oldnode.layout.mergeNode;
if (merge) {
var it = merge.findLinksOutOf();
while (it.next()) {
var link = it.value;
if (link.fromNode.containingGroup !== link.toNode.containingGroup) {
dropOntoLink(e, link);
break;
}
}
}
} else if (oldnode instanceof go.Node) {
var cat = oldnode.category;
if (cat === "Merge" || cat === "End" || cat === "EndFor" || cat === "EndWhile" || cat === "EndIf") {
var link = oldnode.findLinksInto().first();
if (link) dropOntoLink(e, link);
} else {
var link = oldnode.findLinksOutOf().first();
if (link) dropOntoLink(e, link);
}
}
}
// Splice a node into a link.
// If the new node is of category "For" or "While" or "If", create a Group and splice it in,
// and add the new node to that group, and add any other desired nodes and links to that group.
function dropOntoLink(e, oldlink) {
if (!(oldlink instanceof go.Link)) return;
var diagram = e.diagram;
var newnode = diagram.selection.first();
if (!(newnode instanceof go.Node)) return;
if (!newnode.isTopLevel) return;
if (isLoopBack(oldlink)) {
// can't add nodes into links going back to the "For" node
diagram.remove(newnode);
return;
}
var fromnode = oldlink.fromNode;
var tonode = oldlink.toNode;
if (newnode.category === "") { // add simple step into chain of actions
newnode.containingGroup = oldlink.containingGroup;
// Reconnect the existing link to the new node
oldlink.toNode = newnode;
// Then add links from the new node to the old node
if (newnode.category === "If") {
diagram.model.addLinkData({ from: newnode.key, to: tonode.key });
diagram.model.addLinkData({ from: newnode.key, to: tonode.key });
} else {
diagram.model.addLinkData({ from: newnode.key, to: tonode.key });
}
} else if (newnode.category === "For" || newnode.category === "While") { // add loop group
// add group for loop
var groupdata = { isGroup: true, cat: newnode.category };
diagram.model.addNodeData(groupdata);
var group = diagram.findNodeForData(groupdata);
group.containingGroup = oldlink.containingGroup;
diagram.select(group);
newnode.containingGroup = group;
var p1data = { category: "ForPoint" };
diagram.model.addNodeData(p1data);
var p1node = diagram.findNodeForData(p1data);
p1node.containingGroup = group;
p1node.location = e.documentPoint;
// var adata = { category: "" };
// diagram.model.addNodeData(adata);
// var anode = diagram.findNodeForData(adata);
// anode.containingGroup = group;
// anode.location = e.documentPoint;
var p2data = { category: "ForPoint" };
diagram.model.addNodeData(p2data);
var p2node = diagram.findNodeForData(p2data);
p2node.containingGroup = group;
p2node.location = e.documentPoint;
var enddata = { category: "End" + newnode.category };
diagram.model.addNodeData(enddata);
var endnode = diagram.findNodeForData(enddata);
endnode.containingGroup = group;
endnode.location = e.documentPoint;
diagram.model.addLinkData({ from: newnode.key, to: p1node.key });
diagram.model.addLinkData({ from: p1node.key, to: p2node.key });
// diagram.model.addLinkData({ from: p1node.key, to: anode.key });
// diagram.model.addLinkData({ from: anode.key, to: p2node.key });
diagram.model.addLinkData({ from: p2node.key, to: endnode.key, fromPort: 'B' });
// diagram.model.addLinkData({ from: newnode.key, to: endnode.key });
// diagram.model.addLinkData({ from: endnode.key, to: newnode.key }); // TODO: Restore back link
diagram.model.addLinkData({ from: p2node.key, to: p1node.key, dash: true, fromPort: 'R', toPort: 'R' }); // Loop back
// Reconnect the existing link to the new node
oldlink.toNode = newnode;
// Then add a link from the end node to the old node
diagram.model.addLinkData({ from: endnode.key, to: tonode.key });
} else if (newnode.category === "If") { // add Conditional group
// add group for conditional
var groupdata = { isGroup: true, cat: newnode.category };
diagram.model.addNodeData(groupdata);
var group = diagram.findNodeForData(groupdata);
group.containingGroup = oldlink.containingGroup;
diagram.select(group);
newnode.containingGroup = group;
// var startdata = { category: 'IfStart' };
// diagram.model.addNodeData(startdata);
// var startnode = diagram.findNodeForData(startdata);
// startnode.containingGroup = group;
// startnode.location = e.documentPoint;
// diagram.model.addLinkData({ from: newnode.key, to: startnode.key, text: "IfStart" });
var enddata = { category: "EndIf" };
diagram.model.addNodeData(enddata);
var endnode = diagram.findNodeForData(enddata);
endnode.containingGroup = group;
endnode.location = e.documentPoint;
var truedata = { from: newnode.key, to: endnode.key, text: "true" };
diagram.model.addLinkData(truedata);
var truelink = diagram.findLinkForData(truedata);
var falsedata = { from: newnode.key, to: endnode.key, text: "false" };
diagram.model.addLinkData(falsedata);
var falselink = diagram.findLinkForData(falsedata);
// Reconnect the existing link to the new node
oldlink.toNode = newnode;
// Then add a link from the new node to the old node
diagram.model.addLinkData({ from: endnode.key, to: tonode.key });
} else if (newnode.category === "Switch") { // add multi-way Switch group
// add group for loop
var groupdata = { isGroup: true, cat: newnode.category };
diagram.model.addNodeData(groupdata);
var group = diagram.findNodeForData(groupdata);
group.containingGroup = oldlink.containingGroup;
diagram.select(group);
newnode.containingGroup = group;
var startdata = { category: 'StartSwitch' };
diagram.model.addNodeData(startdata);
var startnode = diagram.findNodeForData(startdata);
startnode.containingGroup = group;
startnode.location = e.documentPoint;
diagram.model.addLinkData({ from: newnode.key, to: startnode.key, text: "StartSwitch", toPort: 'T' });
var enddata = { category: "Merge" };
diagram.model.addNodeData(enddata);
var endnode = diagram.findNodeForData(enddata);
endnode.containingGroup = group;
endnode.location = e.documentPoint;
var yesdata = { text: "yes,\ndo it" };
diagram.model.addNodeData(yesdata);
var yesnode = diagram.findNodeForData(yesdata);
yesnode.containingGroup = group;
yesnode.location = e.documentPoint;
diagram.model.addLinkData({ from: startdata.key, to: yesnode.key, text: "yes", fromPort: 'B' });
diagram.model.addLinkData({ from: yesnode.key, to: endnode.key });
var nodata = { text: "no,\ndon't" };
diagram.model.addNodeData(nodata);
var nonode = diagram.findNodeForData(nodata);
nonode.containingGroup = group;
nonode.location = e.documentPoint;
diagram.model.addLinkData({ from: startdata.key, to: nonode.key, text: "no", fromPort: 'R' });
diagram.model.addLinkData({ from: nonode.key, to: endnode.key });
var maybedata = { text: "??" };
diagram.model.addNodeData(maybedata);
var maybenode = diagram.findNodeForData(maybedata);
maybenode.containingGroup = group;
maybenode.location = e.documentPoint;
diagram.model.addLinkData({ from: startdata.key, to: maybenode.key, text: "maybe", fromPort: 'R' });
diagram.model.addLinkData({ from: maybenode.key, to: endnode.key });
var xdata = { text: "x" }; // 4th test option
diagram.model.addNodeData(xdata);
var xnode = diagram.findNodeForData(xdata);
xnode.containingGroup = group;
xnode.location = e.documentPoint;
diagram.model.addLinkData({ from: startdata.key, to: xnode.key, text: "x", fromPort: 'R' });
diagram.model.addLinkData({ from: xnode.key, to: endnode.key });
// Reconnect the existing link to the new node
oldlink.toNode = newnode;
// Then add a link from the end node to the old node
diagram.model.addLinkData({ from: endnode.key, to: tonode.key });
}
diagram.layoutDiagram(true);
}
function deletingNode(node) { // excise node from the chain that it is in
if (!(node instanceof go.Node)) return;
if (node instanceof go.Group) {
var externals = node.findExternalLinksConnected();
var next = null;
externals.each(function (link) {
if (link.fromNode.isMemberOf(node)) next = link.toNode;
});
if (next) {
externals.each(function (link) {
if (link.toNode.isMemberOf(node)) link.toNode = next;
});
}
} else if (node.category === "") {
var next = node.findNodesOutOf().first();
if (next) {
new go.List(node.findLinksInto()).each(function (link) { link.toNode = next; });
}
}
}
// initialize Palette
myPalette =
$(go.Palette, "myPaletteDiv",
{
maxSelectionCount: 1,
nodeTemplateMap: myDiagram.nodeTemplateMap,
model: new go.GraphLinksModel([
{ text: "Action" },
{ text: "For Each", category: "For" },
{ text: "While", category: "While" },
{ text: "If", category: "If" },
{ text: "Switch", category: "Switch" }
])
});
// initialize Overview
myOverview =
$(go.Overview, "myOverviewDiv",
{
observed: myDiagram,
contentAlignment: go.Spot.Center
});
load();
}
// Show the diagram's model in JSON format
function save() {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
myDiagram.model.linkFromPortIdProperty = 'fromPort';
myDiagram.model.linkToPortIdProperty = 'toPort';
}
function newDiagram() {
myDiagram.model = go.GraphObject.make(go.GraphLinksModel,
{
nodeDataArray:
[
{ "key": 1, "text": "S", "category": "Start" },
{ "key": 2, "text": "E", "category": "End" }
],
linkDataArray:
[
{ "from": 1, "to": 2 }
]
});
}
window.addEventListener('DOMContentLoaded', init);
</script>
<div id="sample">
<div id="myFlexDiv">
<div id="myPODiv">
<div id="myPaletteDiv"
style="background-color: floralwhite; border: solid 1px black"></div>
<div id="myOverviewDiv"
style="background-color: whitesmoke; border: solid 1px black"></div>
</div>
<div id="myDiagramDiv"
style="border: solid 1px black"></div>
</div>
<div id="buttons">
<button id="loadModel"
onclick="load()">Load</button>
<button id="saveModel"
onclick="save()">Save</button>
<button onclick="newDiagram()">New Diagram</button>
</div>
<textarea id="mySavedModel"
style="width:100%;height:200px">
{ "class": "GraphLinksModel",
"nodeDataArray": [
{"key":1, "text":"S", "category":"Start"},
{"key":13, "text":"E", "category":"End"}
],
"linkDataArray": [
{"from":1, "to":13}
]}
</textarea>
<!-- Backup Nodes -->
<!--
{"key":1, "text":"S", "category":"Start"},
{"key":-1, "isGroup":true, "cat":"For"},
{"key":2, "text":"For Each", "category":"For", "group":-1},
{"key":3, "text":"Action 1", "group":-1},
{"key":-2, "isGroup":true, "cat":"If", "group":-1},
{"key":4, "text":"If", "category":"If", "group":-2},
{"key":5, "text":"Action 2", "group":-2},
{"key":6, "text":"Action 3", "group":-2},
{"key":-3, "isGroup":true, "cat":"For", "group":-2},
{"key":7, "text":"For Each\n(nested)", "category":"For", "group":-3},
{"key":8, "text":"Action 4", "group":-3},
{"key":9, "text":"", "category":"EndFor", "group":-3},
{"key":10, "text":"", "category":"EndIf", "group":-2},
{"key":11, "text":"Action 5", "group":-1},
{"key":12, "text":"", "category":"EndFor", "group":-1},
{"key":13, "text":"E", "category":"End"}
-->
<!-- BackUpLinks -->
<!-- {"from":1, "to":2},
{"from":2, "to":3},
{"from":3, "to":4},
{"from":4, "to":5, "text":"true"},
{"from":4, "to":6, "text":"false"},
{"from":6, "to":7},
{"from":7, "to":8},
{"from":8, "to":9},
{"from":5, "to":10},
{"from":9, "to":10},
{"from":9, "to":7},
{"from":10, "to":11},
{"from":11, "to":12},
{"from":12, "to":2, "isTreeLink": false },
{"from":12, "to":13} -->
</div>
</div>
<!-- * * * * * * * * * * * * * -->
<!-- End of GoJS sample code -->
</div>
</body>
</html>
How to align those elements? Can it be feasible by some settings or we need to implement custom computation?
example 1
example 2
How to make link go out of port in defined direction? Even if port on the left or right side, link is going from bottom?
To make these look like those:
Ports outgoing directions
Arrow shifting the whole group content (should be just a decoration)
Appreciate any help