Links back and forth from two nodes

I have this diagram, but I’d like to have the link from B Co to Thunder Road to run back from B Co to Thunder Road running parallel to the link that goes from Thunder Road to B Co instead of looping all the way around.

I’m open to a different type of diagram if that would be better (this is a layered diagraph, which I’m only using because I based it off of another diagram I created). I would like to have one node in the center with other nodes around it with the possibility of multiple arrows (some going towards the center, some going away) going to/from the center to any given surrounding node. Thank you!

I don’t know how your LayeredDigraphLayout is declared, but my guess is that you need to set LayeredDigraphLayout.setsPortSpots to false. The default behavior makes all of the links go in the direction specified by the layout, as you can see.

Thank you! This works well, but (of course) it created another issue. Any suggestion on how to make my link labels not overlap like this?

What would you want for a solution?

I think it would make the most sense to stagger the vertical placement of the link panels so that they are spread out along the link and don’t overlap. Is that doable? Any other suggestion on how to show this without overlap? Thank you!

Well, if the labels are wider than both of the connected nodes, and if there are enough links between the two nodes, there will be no way to avoid overlapping labels.

What’s your link template?

My thought was to add more space between the two nodes so that the link panels can be spread out vertically. I don’t mind if the link panel overlaps with another link, I just don’t want it to overlap with another panel because then the label cannot be read. Does that make sense?

Yes, that should be achievable. It will take a while before I have time to implement this though.

I think the strategy is to recognize when there are multiple links close together; if so, change the label’s GraphObject.segmentOffset or GraphObject.segmentFraction. This can be done in an override of LayeredDigraphLayout.commitLinks.

Thanks, I’ll give it a shot. I am already looking through each node and each link, so it probably makes sense to do it here. My issue is that I can’t figure out an effective way to first find any two nodes with 2+ links between them and then loop through those links so that I can adjust the segmentFraction for each link. The idea was to count the number of links, divide that into 100%, and increment each link’s segmentFraction by that amount. Here’s what I have so far, so maybe I can leverage this or maybe there’s a better way to approach this then looping through everything. Thanks!

myDiagram =
      $$(go.Diagram, "finrelsDiagramDiv",
        {
          initialContentAlignment: go.Spot.Center,
          initialAutoScale: go.Diagram.UniformToFill,
          layout: $$(go.LayeredDigraphLayout,
            { direction: 90, layerSpacing: 100, layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource }),
            "LayoutCompleted": function(e) {
              // Push all link panels to the very beginning of the link (close to the bottom of the parent) as a starting point to be adjusted below
              e.diagram.links.each(function(l) { l.elt(2).segmentIndex = -Infinity; });
              // loop through every node on the diagram
              e.diagram.nodes.each(function(n) {
                // get all of the links that leave from the node
                var outLinks = n.findLinksOutOf();
                // count the number of links leaving the node
                var numOuts = outLinks.count

                // if there are any links leaving the node, style the link based on the following cases:
                if (numOuts > 0) {
                  
                  outLinks.each(function(link) {
                    // follow the link to it's destination node and determine how many total links are entering this destination node
                    var subsNumOfIns = link.toNode.findLinksInto().count

                    if(numOuts === 1 && subsNumOfIns === 1) {
                      // if the link is the only one out of the parent and the only one into the sub, put the label halfway along the link
                      link.elt(2).segmentIndex = NaN;
                      link.elt(2).segmentFraction = 0.5;
                    } else if (numOuts === 1 && subsNumOfIns > 1) {
                      // if the link is the only one out of the parent but one of many into the sub, put the label right below the parent
                      link.elt(2).segmentIndex = NaN;
                      link.elt(2).segmentFraction = 0.3;
                    } else if (numOuts > 1 && subsNumOfIns === 1) {
                      // if the link is one of many out of the parent but the only one into the sub, put the label right above the sub
                      link.elt(2).segmentIndex = NaN;
                      link.elt(2).segmentFraction = 0.7;
                    } else if (numOuts > 1 && subsNumOfIns > 1) {
                      // if the link is one of many out of the parent and one of many into the sub:
                        if(link.toNode.position.y - link.fromNode.position.y < 225) {
                        // if only one row apart (i.e. less than 175 points on the coordinate plane apart)
                          // make the links straight lines (e.g. "Normal") instead of orthagonal
                          link.routing = go.Link.Normal
                          // remove the end segment lenths (the vertical portions of the links immediately above/below nodes)
                          link.fromEndSegmentLength = 0;
                          link.toEndSegmentLength = 0;
                          // have the link label appear 1/4 of the way along the straight line or 40% along if there are more than 3 outs from the entity (to give more space)
                          link.elt(1).segmentIndex = NaN;
                          if(numOuts <= 3) {
                            link.elt(1).segmentFraction = 0.25;
                          } else {
                            link.elt(1).segmentFraction = 0.4;
                          }
                        } else {
                        // if more than one row apart (i.e. more than 175 points on the coordinate plane apart)
                          // style this differently - for now just leave it as orthagonal
                        }
                    }
                  });
                }
              });
            },
          "undoManager.isEnabled": true
        }
      );

Call Node.findLinksBetween to find all of the links connecting two nodes in either direction.

Try something like this custom layout:

<!DOCTYPE html>
<html>
<head>
  <title>LayeredDigraphLayout spreading link labels</title>
  <!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
  <meta name="description" content="A custom LayeredDigraphLayout that vertically spreads out link labels to reduce their overlaps">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://unpkg.com/gojs"></script>
  <script id="code">

  function LabelSpreadingLDLayout() {
    go.LayeredDigraphLayout.call(this);
  }
  go.Diagram.inherit(LabelSpreadingLDLayout, go.LayeredDigraphLayout);

  // An override of LayeredDigraphLayout.commitLinks that between each layer assigns
  // link label segmentFraction to alternate values to reduce the overlap of labels.
  LabelSpreadingLDLayout.prototype.commitLinks = function() {
    go.LayeredDigraphLayout.prototype.commitLinks.call(this);
    // group all links by their "from" node/vertex layer
    var allverts = new Map();
    this.network.vertexes.each(v => {
      if (!v.node) return;
      let layer = allverts.get(v.layer);
      if (!layer) { layer = []; allverts.set(v.layer, layer); }
      layer.push(v);
    });
    for (const [layer, verts] of allverts) {
      // for each group of links, sort by starting X
      const links = [];
      for (const v of verts) {
        v.destinationEdges.each(e => {
          if (e.link) links.push(e.link);
        });
      }
      if (links.length > 1) {
        if (this.direction === 90 || this.direction === 270) {
          links.sort((a, b) => a.getPoint(0).x - b.getPoint(0).x);
        } else {
          links.sort((a, b) => a.getPoint(0).y - b.getPoint(0).y);
        }
        let i = 0;
        for (const l of links) {
          const lab = l.findMidLabel();
          if (lab) {
            lab.segmentFraction = (i%(4-1) + 1)/4;
            i++;
          }
        }
      }
    }
  }
  // end LabelSpreadingLDLayout

  function init() {
    var $ = go.GraphObject.make;

    // initialize main Diagram
    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          "linkingTool.archetypeLinkData": { label: "?%", code: 17 },
          layout: $(LabelSpreadingLDLayout,
                    { direction: 90, layerSpacing: 100, setsPortSpots: false }),
          "undoManager.isEnabled": true
        });

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        { minSize: new go.Size(100, 50) },
        $(go.Shape,
          {
            fill: "white", portId: "", cursor: "pointer",
            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 },  // more direct line style
        { fromSpot: go.Spot.TopBottomSides, toSpot: go.Spot.TopBottomSides },  // if direction === 90
        //{ fromSpot: go.Spot.LeftRightSides, toSpot: go.Spot.LeftRightSides },  // if direction === 0
        $(go.Shape),
        $(go.TextBlock, { background: "white", segmentIndex: NaN, segmentFraction: 0.5 },
          new go.Binding("text", "", function(d) { return (d.label || "") + "[Code " + (d.code || "") + "]"; }))
      );

    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>
  <button id="loadModel" onclick="load()">Load</button>
  <button id="saveModel" onclick="save()">Save</button>
  <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":5},
{"key":6,"from":2,"to":6,"label":"25%","code":6},
{"key":7,"from":3,"to":6,"label":"77%","code":7}
]}
  </textarea>
</body>
</html>