As we see in the image, two nodes have two linkages. Instead, I want a single bi-directional link.
Have you specified any fromSpot or toSpot values?
Are you using a layout that is a TreeLayout or LayeredDigraphLayout? Those are directional layouts for which most people want particular directions for the links. You can set TreeLayout.setsPortSpot and TreeLayout.setsChildPortSpot, or LayeredDigraphLayout.setsPortSpots, to false.
Here’s a sample using LayeredDigraphLayout. You can draw new links or delete existing ones in order to play with the graph.
<!DOCTYPE html>
<html>
<head>
<title>LayeredDigraphLayout.setsPortSpots</title>
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:450px"></div>
<button id="myTestButton">Toggle Setting Port Spots</button>
<textarea id="mySavedModel" style="width:100%;height:250px"></textarea>
<script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
<script id="code">
const myDiagram =
new go.Diagram("myDiagramDiv", {
layout: new go.LayeredDigraphLayout(),
"undoManager.isEnabled": true,
"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 =
new go.Node("Vertical", {
selectionObjectName: "CIRCLE",
layerName: "Foreground",
selectionAdorned: false,
shadowOffset: new go.Point(0, 0),
shadowBlur: 20,
shadowColor: "black",
locationSpot: go.Spot.Center,
})
.add(
new go.Shape("Circle", {
name: "CIRCLE",
width: 50, height: 50, portId: "",
fromLinkable: true,
toLinkable: true,
fromLinkableDuplicates: true,
toLinkableDuplicates: true,
cursor: "pointer"
})
.bind("fill", "color"),
new go.TextBlock({
maxSize: new go.Size(200, NaN),
overflow: go.TextBlock.OverflowEllipsis,
toolTip: go.GraphObject.build("ToolTip")
.add(
new go.TextBlock({ margin: 2 })
.bind('text')
)
})
.bind("text")
);
myDiagram.linkTemplate =
new go.Link({
layerName: "Background",
curve: go.Link.Bezier,
selectionAdornmentTemplate:
new go.Adornment('Link')
.add(
new go.Shape({ isPanelMain: true, strokeWidth: 3, stroke: "black" }),
new go.Shape({ isPanelMain: true, strokeWidth: 3, stroke: "white", name: "PIPE", strokeDashArray: [10, 10] })
),
fromEndSegmentLength: 40,
toEndSegmentLength: 40,
selectionChanged: onLinkSelect
})
.add(
new go.Shape({ strokeWidth: 2 }),
new go.Shape({ toArrow: "OpenTriangle", strokeWidth: 2 })
);
function onLinkSelect(obj) {
if (!obj.isSelected) return;
setTimeout(() => {
const animation = new go.Animation();
animation.easing = go.Animation.EaseLinear;
const pipe = obj.adornments.first().findObject("PIPE");
animation.add(pipe, "strokeDashOffset", 20, 0)
// Run indefinitely
animation.runCount = Infinity;
animation.start();
}, 100);
}
myDiagram.model = new go.GraphLinksModel(
[
{ key: 1, text: "1", color: "orange" },
{ key: 2, text: "2", color: "orange" },
{ key: 3, text: "3", color: "orange" },
{ key: 4, text: "4", color: "orange" },
{ key: 5, text: "5", color: "orange" },
{ key: 6, text: "6", color: "orange" },
{ key: 7, text: "7", color: "orange" },
{ key: 8, text: "8", color: "lightgreen" },
{ key: 9, text: "9", color: "lightgreen" },
{ key: 10, text: "10", color: "lightgreen" },
{ key: 11, text: "11", color: "lightgreen" },
{ key: 12, text: "12", color: "lightgreen" },
],
[
{ from: 3, to: 2 },
{ from: 1, to: 3 },
{ from: 1, to: 4 },
{ from: 2, to: 5 },
{ from: 5, to: 6 },
{ from: 6, to: 2 },
{ from: 6, to: 7 },
{ from: 8, to: 9 },
{ from: 9, to: 10 },
{ from: 10, to: 11 },
{ from: 9, to: 11 },
{ from: 8, to: 12 },
]);
document.getElementById("myTestButton").addEventListener("click", e => {
myDiagram.commit(d => {
d.links.each(l => l.fromSpot = l.toSpot = go.Spot.Default);
d.layout.setsPortSpots = !d.layout.setsPortSpots;
});
});
</script>
</body>
</html>