Combining go.LayeredDigraphLayout with SwimLaneLayout not working

Hi, I am working on layout the diagram. I have nodes (with already known lanes) and edges.
I used SwimLaneLayout to have lanes sitting up and down of each other and to layout diagram using go.LayeredDiagraphLayout. they both work very well when they are separate but when I try to combine both of them then I lost lanes.

here is my diagram having nodes in lanes

code for above diagram

myDiagram = new go.Diagram('myDiagramDiv', {
                           initialAutoScale: go.AutoScale.Uniform,
                           allowCopy: false,
                           layout: new SwimLaneLayout({
                               laneProperty: 'group',
                               direction: DIRECTION,
                               setsPortSpots: true,
                               layerSpacing: 20,
                               columnSpacing: 5,
                               commitLayers: function (layerRects, offset) {
                                   if (layerRects.length === 0) return;
                                   var horiz = this.direction === 0 || this.direction === 180;
                                   var forwards = this.direction === 0 || this.direction === 90;
                                   var rect = layerRects[forwards ? layerRects.length - 1 : 0];
                                   var totallength = horiz ? rect.right : rect.bottom;
                                   if (horiz) {
                                       offset.y -= this.columnSpacing * 3 / 2;
                                   } else {
                                       offset.x -= this.columnSpacing * 3 / 2;
                                   }
                                   for (var i = 0; i < this.laneNames.length; i++) {
                                       var lane = this.laneNames[i];
                                       var group = this.diagram.findNodeForKey(lane);
                                       if (group === null) {
                                           this.diagram.model.addNodeData({ key: lane, isGroup: true });
                                           group = this.diagram.findNodeForKey(lane);
                                       }
                                       if (horiz) {
                                           group.location = new go.Point(-this.layerSpacing / 2, this.lanePositions.get(lane) * this.columnSpacing + offset.y);
                                       } else {
                                           group.location = new go.Point(this.lanePositions.get(lane) * this.columnSpacing + offset.x, -this.layerSpacing / 2);
                                       }
                                       var ph = group.findObject('PLACEHOLDER');
                                       if (ph === null) ph = group;
                                       if (horiz) {
                                           ph.desiredSize = new go.Size(totallength, this.laneBreadths.get(lane) * this.columnSpacing);
                                       } else {
                                           ph.desiredSize = new go.Size(this.laneBreadths.get(lane) * this.columnSpacing, totallength);
                                       }
                                   }
                               },
                           })
                       });

here is diagram by applying layeredDiagraphLayout along with SwimLaneLayout but it don’t show lanes and nodes in their own lanes.

code for the above diagram

myDiagram = new go.Diagram('myDiagramDiv', {
                           initialAutoScale: go.AutoScale.Uniform,
                           allowCopy: false,
                           layout: new SwimLaneLayout({
                               laneProperty: 'group',
                               direction: DIRECTION,
                               setsPortSpots: true,
                               layerSpacing: 20,
                               columnSpacing: 5,
                               commitLayers: function (layerRects, offset) {
                                   if (layerRects.length === 0) return;
                                   var horiz = this.direction === 0 || this.direction === 180;
                                   var forwards = this.direction === 0 || this.direction === 90;
                                   var rect = layerRects[forwards ? layerRects.length - 1 : 0];
                                   var totallength = horiz ? rect.right : rect.bottom;
                                   if (horiz) {
                                       offset.y -= this.columnSpacing * 3 / 2;
                                   } else {
                                       offset.x -= this.columnSpacing * 3 / 2;
                                   }
                                   for (var i = 0; i < this.laneNames.length; i++) {
                                       var lane = this.laneNames[i];
                                       var group = this.diagram.findNodeForKey(lane);
                                       if (group === null) {
                                           this.diagram.model.addNodeData({ key: lane, isGroup: true });
                                           group = this.diagram.findNodeForKey(lane);
                                       }
                                       if (horiz) {
                                           group.location = new go.Point(-this.layerSpacing / 2, this.lanePositions.get(lane) * this.columnSpacing + offset.y);
                                       } else {
                                           group.location = new go.Point(this.lanePositions.get(lane) * this.columnSpacing + offset.x, -this.layerSpacing / 2);
                                       }
                                       var ph = group.findObject('PLACEHOLDER');
                                       if (ph === null) ph = group;
                                       if (horiz) {
                                           ph.desiredSize = new go.Size(totallength, this.laneBreadths.get(lane) * this.columnSpacing);
                                       } else {
                                           ph.desiredSize = new go.Size(this.laneBreadths.get(lane) * this.columnSpacing, totallength);
                                       }
                                   }
                               },
                               doLayout: function (coll) {
                                   var diagram = this.diagram;
                                   if (diagram === null) return;
                                   diagram.startTransaction("custom layout");
                                   console.log(this, this?.iterator)
                                   diagram.nodes.each(function (n) {
                                       coll.add(n);
                                   });
                                   console.log(coll)
                                   // Custom logic to adjust node positioning
                                   var layout = new go.LayeredDigraphLayout();
                                   layout.direction = this.direction; // use the same direction as the SwimLaneLayout
                                   layout.setsPortSpots = false;
                                   layout.layerSpacing = this.layerSpacing;
                                   layout.columnSpacing = this.columnSpacing;
                                   layout.doLayout(coll);
                                   diagram.commitTransaction("custom layout");
                               }
                           })
                       });

Desired Result:
I want diagram with lanes and arranged nodes to have as less cross links as we can either forward links or back links (links going to any node which has already processed/past for the current node).

Thanks in advance

Are you only asking about the positioning of the nodes? Not about how the links are routed?

Yes, the second application of LayeredDigraphLayout has no knowledge about lanes, which is why you are getting those results – it is just discarding all of the work done by SwimLaneLayout.

So the change in the ordering of nodes has to happen within SwimLaneLayout. If you look at the implementation of SwimLaneLayout, you will see an override of LayeredDigraphLayout.reduceCrossings that calls reduceCrossings on a SwimLaneLayout.reducer object.

Try setting that property to an instance of this experimental class, below. For example:

            layout:
              new SwimLaneLayout({
                  reducer: new CrossingsReducer(),
                  . . .
                })

CrossingsReducer.js:

/*
 *  Copyright (C) 1998-2024 by Northwoods Software Corporation. All Rights Reserved.
 */

class CrossingsReducer {
  constructor() {
    this.dataFunction = x => x.data;
    this._laneIndexes = new go.Map();
    // internal state
    this._aset = new go.Set();
    this._bset = new go.Set();
  }

  get laneIndexes() { return this._laneIndexes; }
  set laneIndexes(value) { this._laneIndexes = value; }

  // assume the AARR is sorted by lane
  reduceCrossings(aarr, barr) {
    if (aarr.length <= 1) return false;
    // iterate over continguous sections of the Array that have the same lane
    let count = 0;
    let s = 0;
    let lane = this.findLane(aarr[s]);
    let changed = true;
    while (count < 10 && changed) {
      changed = false;
      let e = s;
      while (e < aarr.length) {
        while (e < aarr.length && this.findLane(aarr[e]) === lane) e++;
        changed |= this._reduceCrossingsPartial(aarr, s, e, barr);
        if (e < aarr.length) lane = this.findLane(aarr[e]);
        s = e;
      }
      count++;
      s = 0;
    }
    this._aset.clear();
    this._bset.clear();
    return count > 1;
  }

  findLane(node) {
    return this.dataFunction(node).lane || '';
  }
  getLaneIndex(node) {
    return this.laneIndexes.get(this.findLane(node));
  }
  getIndex(node) {
    return this.dataFunction(node).index || 0;
  }
  getBary(node) {
    return this.dataFunction(node).bary || 0;
  }
  setBary(node, val) {
    this.dataFunction(node).bary = val;
  }
  getConnectedNodesIterator(node) {
    if (node instanceof go.Node) {
      return node.findNodesConnected();
    } else {
      return new go.List().iterator;
    }
  }

  _reduceCrossingsPartial(aarr, start, end, barr) {
    if (!Array.isArray(aarr)) throw new Error('expected an Array, not ' + arr);
    if (aarr.length <= 1) return false; // nothing to do
    if (start < 0) start = 0;
    if (end > aarr.length) end = aarr.length;
    if (start >= end - 1) return false; // nothing to do
    this._aset.clear();
    for (let i = start; i < end; i++) this._aset.add(aarr[i]);
    const arr = this._aset.toArray(); // just the nodes of AARR from START up to END
    this._bset.clear();
    this._bset.addAll(barr);

    const roots = this._aset.copy();
    const conns = [];
    while (roots.count > 0) {
      const root = roots.first();
      if (this.getConnectedNodesIterator(root).count > 0) {
        const conn = new go.Set();
        this._follow(root, this._aset, this._bset, conn);
        conns.push(conn);
        roots.removeAll(conn);
      } else {
        this.setBary(root, this._aset.count);
        roots.remove(root);
      }
    }
    
    conns.sort((c1, c2) => {
      if (c1.count < c2.count) return -1;
      if (c1.count > c2.count) return 1;
      return 0;
    });

    conns.forEach(conn => {
      conn.each(node => {
        if (this._aset.has(node)) {
          let bary = 0;
          const it = this.getConnectedNodesIterator(node);
          const num = it.count;
          it.each(m => {
            if (!this._bset.has(m)) return;
            bary += this.getLaneIndex(m) * 10000 + this.getIndex(m);
          });
          this.setBary(node, num ? bary / num : arr.length);
        }
      });
    });

    arr.sort((x, y) => {
      const xc = this.getBary(x);
      const yc = this.getBary(y);
      if (xc < yc) return -1;
      if (xc > yc) return 1;
      return 0;
    });

    let modified = false;
    for (let i = 0; i < arr.length; i++) {
      if (aarr[start + i] !== arr[i]) modified = true;
      aarr[start + i] = arr[i];
    }
    return modified;
  }

  _follow(node, aset, bset, conn) {
    if (conn.has(node)) return;
    conn.add(node);
    this.getConnectedNodesIterator(node).each(m => {
      if (bset.has(m)) {
        if (!conn.has(m)) conn.add(m);
        //this._follow(m, bset, aset, conn);
      }
    });
  }
} // end of CrossingsReducer

Thanks Walter for quick response really appreciate it.

So there are two things

  1. Is it possible to use both LayeredDiagraph and SwimLane layout applied to a diagram? if so, can you help with above given code as its not applying both for me.

  2. Other is to reduce the links crossing where possible or move node close to its target node lane. for example in below first example I added images with before and after (manually arranged) nodes. The Crossing Reducer didn’t help with the below two example.

Example 1
Before

After (manually arranged)

I want the nodes to be close to the lane of its target, as in example 1 there are 4 nodes coming out from XOR Rule1 node two have no further nodes attached so then can be in center and the node (1)Very high risk country Payment not sent to prohibited country/regions has target node in lane after and the node (2)Very high risk country Payment sent to prohibited country/regions has target node in before lane but their current positions make their outgoing links to cross each other but if somehow I can move any node to close to its lane (in this case swap both nodes can help a alot),]. Is there any idea/help for this problem i can get?

In below example there is possibility that nodes can be arranged without crossing each other links (as we can see in before and after image) but they still cross which I want to avoid.

Example 2

I hope all above will make sense, is there anything I do to achieve the desired result.

SwimLaneLayout is a LayeredDigraphLayout – it inherits from that class.

If you could provide me the models that you used, I can see if there’s a bug in the CrossingsReducer code.

here you can access the model with code file. Its actually quite large.

I got it, thanks.

Hi @walter,

I also have an example where there are two lanes, when there is a large group of nodes just in one lane which itself look like a a complete diagram but using SwimLane layout it doesn’t look very well maybe because of not enough spacing.

As you can see in below image it is look much better, are there any options I can use with SwimLane to make it look better like below.

Both diagrams are generated using same data but first one using SwimLane (there are no x, y assigned to any node) and other one is generated by assigning x, y values of each node using custom algorithm.

Is there a reason you can’t just use your custom algorithm?

yeah that algorithm don’t work completely correct as you can see there are multiple selected nodes in the image which were at the below white space which I selected and move up to show what I’m looking for.

Can I achieve something like this with SwimLaneLayout?

I don’t know what your algorithm is, so I cannot guess whether it could be merged into SwimLaneLayout.

No I mean are there any properties for SwimLane which can help to make it look like the one I want.

It requires programming. When I get some free time I’ll try to debug the CrossingsReducer, and maybe that will be good enough.

I have updated the code above for that experimental CrossingsReducer class. But it requires an updated SwimLaneLayout too that will be in the next release, 3.0.12.