Link separation and label overlap avoidance

The graphs I am working with often have multiple links that have the same source and destination node. These links sometimes contain visible text labels.
I am using a LayeredDigraphLayout with the following settings:
layout: goMake(go.LayeredDigraphLayout, { direction: 90, isOngoing: false, isInitial: false, layerSpacing: 100 })
The link configuration:
goMake(go.Link, { toShortLength: 3, relinkableFrom: true, relinkableTo: true, reshapable: true, resegmentable: true }, new go.Binding('points').makeTwoWay()

With this current configuration, the behaviour of the routing when auto layout is performed makes some links completely overlap so it looks as though they have “merged” in to one. Sometimes this occurs, and sometimes it doesn’t. This is shown below:

image

The desired behaviour is that links would be spaced out when they have the same source and destination nodes, ideally with some avoidance of the labels (ones that are too long will be truncated). The desired behaviour would look something like this after an auto layout:

image

Maybe something like this:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Editor</title>
  <!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
  function init() {
    var $ = go.GraphObject.make;

    // initialize main Diagram
    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          layout: $(go.LayeredDigraphLayout,
                    { direction: 90, layerSpacing: 50, setsPortSpots: false }),
          "undoManager.isEnabled": true
        });

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        { minSize: new go.Size(100, 50) },
        $(go.Shape,
          { fill: "white", portId: "", cursor: "pointer",
            fromSpot: go.Spot.BottomSide, toSpot: go.Spot.TopSide,
            fromLinkable: true, toLinkable: true,
            fromLinkableDuplicates: true, toLinkableDuplicates: true
          },
          new go.Binding("figure", "shape"),
          new go.Binding("fill", "shape", function(s) { return s === "Triangle" ? "lightgreen" : "lightblue"; })),
        $(go.TextBlock,
          new go.Binding("text", "key", function(k) { return "Entity " + k; }))
      );

    myDiagram.linkTemplate =
      $(go.Link,
        { fromEndSegmentLength: 0, toEndSegmentLength: 0 },
        $(go.Shape),
        $(go.TextBlock, { segmentIndex: 1, background: "white" },
          new go.Binding("text", "", function(d) { return (d.label || "") + "[Code " + (d.code || "") + "]"; }),
          new go.Binding("segmentFraction", "key", function(k) { return k % 2 === 0 ? 0.333 : 0.667; }))
      );

    load();
  }

  // save a model to and load a model from Json text, displayed below the Diagram
  function save() {
    var str = myDiagram.model.toJson();
    document.getElementById("mySavedModel").value = str;
  }
  function load() {
    var str = document.getElementById("mySavedModel").value;
    myDiagram.model = go.Model.fromJson(str);
  }
  </script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="width: 100%; height: 400px; border: solid 1px black"></div>
  <div id="buttons">
    <button id="loadModel" onclick="load()">Load</button>
    <button id="saveModel" onclick="save()">Save</button>
  </div>
  <textarea id="mySavedModel" style="width:100%;height:200px">
{ "class": "GraphLinksModel",
  "linkKeyProperty": "key",
  "nodeDataArray": [
{"key":1,"shape":"Triangle"},
{"key":2,"shape":"Triangle"},
{"key":3,"shape":"Triangle"},
{"key":4,"shape":"Rectangle"},
{"key":5,"shape":"Rectangle"},
{"key":6,"shape":"Rectangle"}
],
  "linkDataArray": [
{"key":1,"from":1,"to":4,"label":"75%","code":1},
{"key":2,"from":2,"to":4,"label":"13%","code":2},
{"key":3,"from":3,"to":4,"label":"12%","code":3},
{"key":4,"from":1,"to":5,"label":"100%","code":4},
{"key":5,"from":1,"to":4,"label":"30%","code":6},
{"key":6,"from":2,"to":6,"label":"25%","code":7},
{"key":7,"from":3,"to":6,"label":"77%","code":8}
]}
  </textarea>
</body>
</html>

It depends on setting LayeredDigraphLayout.setsPortSpots to false so that the node/port can specify “…Side” Spots for fromSpot and toSpot.

It also depends on changing the segmentFraction so that the labels don’t overlap as much.