Group overlapping with node without considering its bounds

Hi,

We created a swimlane and created custom layout for pool and swimlane.
We are using LayeredDigraphLayout for diagram. When there is an orphan node without any links outside the group, that node is coming on top of the group.
Please refer to the image below:

Desired behavior is to plot the node outside the bounds of the group like shown below:

Note: when i am giving width in desired size exact to the group width it is plotting outside the group.

Does that disconnected node have a lane assignment? If not, I suggest that such nodes be laid out separately from the rest of the graph laid out by the SwimLaneLayout.

That can be accomplished by customizing that layout’s implementation to remove such nodes/vertexes from the LayeredDigraphLayout.network and overriding LayeredDigraphLayout.commitNodes to call the super method and then position those disconnected nodes programmatically.

You could do that directly in SwimLaneLayout.js/.ts or maybe you could use ArrangingLayout: Arranging Layout of the Class Hierarchy

No, the disconnected node is not part of any lane. My custom layout doesn’t contain that node. All nodes outside the swimlane are at diagram level and using diagram’s layout. my diagram is using LayeredDigraphLayout and i cant change this. Is there a way where it can work with my diagram, layout being LayeredDigraphLayout only?

If you don’t want to use ArrangingLayout you’ll need to override SwimLaneLayout.makeNetwork to exclude and remember the disconnected nodes, and override LayeredDigraphLayout.commitNodes to arrange those nodes, as I described above.

Since the disconnected nodes are not part of swimlane layout how to exclude them in swimlane layout?
Do we have any samples for this kind of case?

I’ve copied the SwimLaneLayout.html sample, defined a CustomSwimLaneLayout, and added a dummy team with the abbreviation “XYZ”:

<!DOCTYPE html>
<html><body>
<script src="go.js"></script>
<script src="SwimLaneLayout.js"></script>
<script id="code">
class CustomSwimLaneLayout extends SwimLaneLayout {
  constructor() {
    super();
    this._singletons = new go.Set();
  }
  makeNetwork(coll) {
    const net = super.makeNetwork(coll);
    new go.Set(net.vertexes).each(v => {
      if (v.edgesCount === 0 && v.node && v.node.data[this.laneProperty] === undefined) {
        this._singletons.add(v.node);
        net.deleteVertex(v);
      }
    })
    return net;
  }
  commitLayers(layerRects, offset) {
    if (layerRects.length === 0) return;

    const bnds = layerRects[0];
    let x = bnds.right + 50;
    let y = bnds.top;
    this._singletons.each(n => {
      n.moveTo(x, y);
      y += n.actualBounds.height + 20;
    });
    this._singletons.clear();

    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;

    for (var i = 0; i < this.laneNames.length; i++) {
      var lane = this.laneNames[i];
      // assume lane names do not conflict with node names
      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");  // won't be a go.Placeholder, but just a regular Shape
      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);
      }
    }
  }
}

var DIRECTION = 90;  // used to customize the layout and the templates, only upon first initialization

function init() {
  if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this

  var $ = go.GraphObject.make;  // for conciseness in defining templates

  myDiagram =
    new go.Diagram("myDiagramDiv",
      { // automatically scale the diagram to fit the viewport's size
        initialAutoScale: go.AutoScale.Uniform,
        // disable user copying of parts
        allowCopy: false,
        // position all of the nodes and route all of the links
        layout:
          $(CustomSwimLaneLayout,
            {
              laneProperty: "group",  // needs to know how to assign vertexes/nodes into lanes/groups
              direction: DIRECTION,  // Group template also depends on DIRECTION
              setsPortSpots: false,
              layerSpacing: 20,
              columnSpacing: 5,
            })
    });

  // replace the default Node template in the nodeTemplateMap
  myDiagram.nodeTemplate =
    $(go.Node, "Vertical",  // the whole node panel
      { // when the DIRECTION is vertical, use the whole Node as the port
        fromSpot: go.Spot.TopBottomSides,
        toSpot: go.Spot.TopBottomSides
      },
      $(go.TextBlock,  // the text label
        new go.Binding("text", "key")),
      $(go.Picture,  // the icon showing the logo
        // You should set the desiredSize (or width and height)
        // whenever you know what size the Picture should be.
        {
          desiredSize: new go.Size(50, 50),
          // when the DIRECTION is horizontal, use this icon as the port
          portId: (DIRECTION === 0 || DIRECTION === 180) ? "" : null,
          fromSpot: go.Spot.LeftRightSides,
          toSpot: go.Spot.LeftRightSides
        },
        new go.Binding("source", "key", convertKeyImage))
    );

  function convertKeyImage(key) {
    if (!key) key = "NE";
    return "https://www.nwoods.com/go/beatpaths/" + key + "_logo-50x50.png";
  }

  // replace the default Link template in the linkTemplateMap
  myDiagram.linkTemplate =
    $(go.Link,  // the whole link panel
      { routing: go.Routing.AvoidsNodes, corner: 10 },
      $(go.Shape,  // the link shape
        { strokeWidth: 1.5 }),
      $(go.Shape,  // the arrowhead
        { toArrow: "Standard", stroke: null })
    );

  myDiagram.groupTemplate = // assumes SwimLaneLayout.direction === 0
    $(go.Group, (DIRECTION === 0 || DIRECTION === 180) ? "Horizontal" : "Vertical",
      {
        layerName: "Background",  // always behind all regular nodes and links
        movable: false,  // user cannot move or copy any lanes
        copyable: false,
        locationObjectName: "PLACEHOLDER",  // this object will be sized and located by SwimLaneLayout
        layout: null,  // no lane lays out its member nodes
        avoidable: false  // don't affect any AvoidsNodes link routes
      },
      $(go.TextBlock, { font: "bold 12pt sans-serif", angle: (DIRECTION === 0 || DIRECTION === 180) ? 270 : 0 },
        new go.Binding("text", "key")),
      $(go.Panel, "Auto",
        $(go.Shape, { fill: "transparent", stroke: "orange" }),
        $(go.Shape, { name: "PLACEHOLDER", fill: null, stroke: null, strokeWidth: 0 })
      ),
      $(go.TextBlock, { font: "bold 12pt sans-serif", angle: (DIRECTION === 0 || DIRECTION === 180) ? 90 : 0 },
        new go.Binding("text", "key"))
    );

  partitionBy('d');
}

// the array of node data describing each team, each division, and each conference
var nodeDataArray = [
  { key: "AFC", isGroup: true },
  { key: "NFC", isGroup: true },
  { key: "AFCE", isGroup: true },
  { key: "AFCN", isGroup: true },
  { key: "AFCS", isGroup: true },
  { key: "AFCW", isGroup: true },
  { key: "NFCE", isGroup: true },
  { key: "NFCN", isGroup: true },
  { key: "NFCS", isGroup: true },
  { key: "NFCW", isGroup: true },
  { key: "NE", conf: "AFC", div: "AFCE" },
  { key: "PIT", conf: "AFC", div: "AFCN" },
  { key: "DAL", conf: "NFC", div: "NFCE" },
  { key: "CLE", conf: "AFC", div: "AFCN" },
  { key: "NYG", conf: "NFC", div: "NFCE" },
  { key: "GB", conf: "NFC", div: "NFCN" },
  { key: "SEA", conf: "NFC", div: "NFCW" },
  { key: "IND", conf: "AFC", div: "AFCS" },
  { key: "MIN", conf: "NFC", div: "NFCN" },
  { key: "PHI", conf: "NFC", div: "NFCE" },
  { key: "DET", conf: "NFC", div: "NFCN" },
  { key: "JAC", conf: "AFC", div: "AFCS" },
  { key: "SD", conf: "AFC", div: "AFCW" },
  { key: "XYZ" },
  { key: "CHI", conf: "NFC", div: "NFCN" },
  { key: "TB", conf: "NFC", div: "NFCS" },
  { key: "KC", conf: "AFC", div: "AFCW" },
  { key: "DEN", conf: "AFC", div: "AFCW" },
  { key: "TEN", conf: "AFC", div: "AFCS" },
  { key: "BUF", conf: "AFC", div: "AFCE" },
  { key: "OAK", conf: "AFC", div: "AFCW" },
  { key: "HOU", conf: "AFC", div: "AFCS" },
  { key: "ATL", conf: "NFC", div: "NFCS" },
  { key: "WAS", conf: "NFC", div: "NFCE" },
  { key: "CIN", conf: "AFC", div: "AFCN" },
  { key: "NYJ", conf: "AFC", div: "AFCE" },
  { key: "CAR", conf: "NFC", div: "NFCS" },
  { key: "NO", conf: "NFC", div: "NFCS" },
  { key: "BAL", conf: "AFC", div: "AFCN" },
  { key: "MIA", conf: "AFC", div: "AFCE" },
  { key: "ARI", conf: "NFC", div: "NFCW" },
  { key: "STL", conf: "NFC", div: "NFCW" },
  { key: "SF", conf: "NFC", div: "NFCW" }
];

// the array of link data objects: the relationships between the nodes
var linkDataArray = [
  { from: "NE", to: "CLE" },
  { from: "NE", to: "DAL" },
  { from: "NE", to: "IND" },
  { from: "PIT", to: "CLE" },
  { from: "DAL", to: "NYG" },
  { from: "DAL", to: "GB" },
  { from: "CLE", to: "SEA" },
  { from: "NYG", to: "DET" },
  { from: "GB", to: "MIN" },
  { from: "GB", to: "PHI" },
  { from: "SEA", to: "PHI" },
  { from: "SEA", to: "CIN" },
  { from: "IND", to: "TB" },
  { from: "IND", to: "JAC" },
  { from: "MIN", to: "SD" },
  { from: "PHI", to: "NYJ" },
  { from: "DET", to: "CHI" },
  { from: "DET", to: "DEN" },
  { from: "JAC", to: "DEN" },
  { from: "SD", to: "DEN" },
  { from: "CHI", to: "OAK" },
  { from: "TB", to: "TEN" },
  { from: "DEN", to: "TEN" },
  { from: "DEN", to: "KC" },
  { from: "DEN", to: "BUF" },
  { from: "TEN", to: "OAK" },
  { from: "TEN", to: "ATL" },
  { from: "TEN", to: "HOU" },
  { from: "BUF", to: "WAS" },
  { from: "OAK", to: "MIA" },
  { from: "HOU", to: "MIA" },
  { from: "HOU", to: "CAR" },
  { from: "WAS", to: "NYJ" },
  { from: "WAS", to: "ARI" },
  { from: "CIN", to: "BAL" },
  { from: "NYJ", to: "MIA" },
  { from: "CAR", to: "ARI" },
  { from: "CAR", to: "STL" },
  { from: "CAR", to: "SF" },
  { from: "NO", to: "SF" },
  { from: "BAL", to: "STL" },
  { from: "BAL", to: "SF" }
];

function partitionBy(a) {
  // create the model and assign it to the Diagram
  var model = new go.GraphLinksModel();
  // depending on how we are partitioning the graph, each node belongs either
  // to a conference group or to a division group
  model.nodeGroupKey = (a === 'c') ? "conf" : "div";
  model.nodeDataArray = nodeDataArray;
  model.linkDataArray = linkDataArray;
  // each node's lane information is the same as the group information
  myDiagram.layout.laneProperty = model.nodeGroupKey;
  // optionally, specify the order of known lane names, without setting laneComparer
  myDiagram.layout.laneNames = (a === 'c') ?
    ["AFC", "NFC"] :
    ["AFCE", "AFCN", "AFCS", "AFCW", "NFCE", "NFCN", "NFCS", "NFCW"];
  myDiagram.model = model;
}
window.addEventListener('DOMContentLoaded', init);
</script>

<div id="sample">
  <p><b>Beat Paths</b>: The 2007 NFL Season, divided by conference or by division</p>
  <div id="myDiagramDiv" style="border: solid 1px gray; margin: 10px; height: 700px"></div>
  <input type="radio" name="A" onclick="partitionBy('c')" id="conferenceButton" />
  <label for="conferenceButton">Conferences</label><br />
  <input type="radio" name="A" onclick="partitionBy('d')" id="divisionButton" checked />
  <label for="divisionButton">Divisions</label><br />
</div>
</body></html>