Interaction with many diagrams

Hi, I’m a new GoJS user in my project with angular 6, and I’d just like to know if it was possible to interact with many diagrams with different nodes or the same nodes. Because when i try, they are not a link between the diagrams

GoJS does not support rendering Links, or really any GraphObjects, across multiple Diagrams (i.e., multiple HTML DIV elements).

But the user can drag-and-drop from one Diagram to another. That is clearly demonstrated in any sample that includes a Palette.

Perhaps you want to show multiple subgraphs within one Diagram? Could you describe more thoroughly what you want?

I would like to represent nodes on different layers. I have about 5 layers and each layer contains nodes and each node can interact with another node whatever its layer. So my idea was to have for each layer a div (diagram) that will contain the nodes of its layer. The problem is at the level of the diagrams since the nodes do not belong to the same diagram it does not manage to make the connection between another diagram. But if it is possible to have a diagram and subdiagrams that interact with each other that can solve my problem.

Is there a reason you could not use Groups arranged as in Swim Lanes ? Note how in that sample the user can draw new links between nodes that are in different lanes.

PoolLayout, defined in that sample, is responsible for arranging its groups to be positioned above/below each other, touching, and aligned and with the same lengths.

i didn’t see it, i think that i can use.
thank

Consider also an alternative arrangement:

Also take a look at SwimLaneLayout extension: Beat Paths with Lanes for Divisions Using SwimLaneLayout
It is implemented at: https://gojs.net/latest/extensions/SwimLaneLayout.js
It is documented at: SwimLaneLayout | GoJS API

Note in the “Swim Bands” diagram the layers cut across the general direction or flow. This is unlike the “Swim Lanes” diagram, where as in a real life swimming pool, the direction or flow is within a lane – you are not supposed to cross between lanes.

The “Swimlane Layout” sample demonstrates how nodes can be assigned to different groups and the SwimlaneLayout arranges all of the nodes and links to put them into layers.

The advantage of these two diagram types is that the layout applies to all of the nodes and links in the diagram, excluding groups. So all of the relationships are considered in doing the layout.

The disadvantage of the “Swim Lanes” diagram type is that each group has its own layout (Group.layout), which means that there is no coordination between groups/lanes when doing the layout.

Thank, walter.
I think that i will use swimlanes, but when i implement PoolLyaout in typescript, the method comparer is never called, and i haven’t the same resultat my pools and my lanes overlap also the lanes. Can you help me.

import * as go from 'gojs';

export class PoolLayout extends go.GridLayout {

  private static MINLENGTH = 200;  // this controls the minimum length of any swimlane
  private static MINBREADTH = 20;  // this controls the minimum breadth of any non-collapsed swimlane

  constructor() {
    super();
    this.cellSize = new go.Size(1, 1);
    this.wrappingColumn = 1;
    this.wrappingWidth = Infinity;
    this.isRealtime = false;  // don't continuously layout while dragging
    this.alignment = go.GridLayout.Position;
    // This sorts based on the location of each Group.
    // This is useful when Groups can be moved up and down in order to change their order.

    this.comparer = function(a, b) {
      const ay = a.location.y;
      const by = b.location.y;
      if (isNaN(ay) || isNaN(by)) { return 0; }
      if (ay < by) { return -1; }
      if (ay > by) { return 1; }
      return 0;
    };
  }

  private  computeMinLaneSize(lane) {
    if (!lane.isSubGraphExpanded) {return new go.Size(PoolLayout.MINLENGTH, 1); }
    return new go.Size(PoolLayout.MINLENGTH, PoolLayout.MINBREADTH);
  }

  private  computeMinPoolSize(pool) {
    // assert(pool instanceof go.Group && pool.category === 'Pool');
    let len = PoolLayout.MINLENGTH;
    pool.memberParts.each(function(lane) {
      // pools ought to only contain lanes, not plain Nodes
      if (!(lane instanceof go.Group)) {return; }
      const holder = lane.placeholder;
      if (holder !== null) {
        const sz = holder.actualBounds;
        len = Math.max(len, sz.width);
      }
    });
    return new go.Size(len, NaN);
  }

  private  computeLaneSize(lane) {
    // assert(lane instanceof go.Group && lane.category !== 'Pool');
    const sz = this.computeMinLaneSize(lane);
    if (lane.isSubGraphExpanded) {
      const holder = lane.placeholder;
      if (holder !== null) {
        const hsz = holder.actualBounds;
        sz.height = Math.max(sz.height, hsz.height);
      }
    }
    // minimum breadth needs to be big enough to hold the header
    const hdr = lane.findObject('HEADER');
    if (hdr !== null) {sz.height = Math.max(sz.height, hdr.actualBounds.height); }
    return sz;
  }

  doLayout = function(coll) {
    const poolLayout = this;
    const diagram = poolLayout.diagram;
    if (diagram === null) { return; }
    diagram.startTransaction('PoolLayout');
    const pool = poolLayout.group;
    if (pool !== null && pool.category === 'Pool') {
      // make sure all of the Group Shapes are big enough
      const minsize = poolLayout.computeMinPoolSize.bind(poolLayout, pool)();
      pool.memberParts.each(function(lane) {
        if (!(lane instanceof go.Group)) { return; }
        if (lane.category !== 'Pool') {
          const shape = lane.resizeObject;
          if (shape !== null) {  // change the desiredSize to be big enough in both directions
            const sz = poolLayout.computeLaneSize.bind(poolLayout, lane)();
            shape.width = (isNaN(shape.width) ? minsize.width : Math.max(shape.width, minsize.width));
            shape.height = (!isNaN(shape.height)) ? Math.max(shape.height, sz.height) : sz.height;
            const cell = lane.resizeCellSize;
            if (!isNaN(shape.width) && !isNaN(cell.width) && cell.width > 0) {
              shape.width = Math.ceil(shape.width / cell.width) * cell.width; }
            if (!isNaN(shape.height) && !isNaN(cell.height) && cell.height > 0) {
              shape.height = Math.ceil(shape.height / cell.height) * cell.height; }
          }
        }
      });
    }
    diagram.commitTransaction('PoolLayout');
  };

}

import * as go from 'gojs';

export class DetailsObjet {

  private $ = go.GraphObject.make;
  constructor() {}
  public partContextMenu =
  this.$(go.Adornment, 'Vertical',
      this.makeButton('Properties',
          (e, obj) => {  // the OBJ is this Button
              const contextmenu = <go.Adornment>obj.part;  // the Button is in the context menu Adornment
              const part = contextmenu.adornedPart;  // the adornedPart is the Part that the context menu adorns
              // now can do something with PART, or with its data, or with the Adornment (the context menu)
              if (part instanceof go.Link) { alert(this.linkInfo(part.data));
              } else if (part instanceof go.Group) { alert(this.groupInfo(contextmenu));
              } else { alert(this.nodeInfo(part.data)); }
          }),
      this.makeButton('Cut',
                 (e, obj) => e.diagram.commandHandler.cutSelection(),
                 o => o.diagram.commandHandler.canCutSelection()),
      this.makeButton('Copy',
                 (e, obj) => e.diagram.commandHandler.copySelection(),
                 o => o.diagram.commandHandler.canCopySelection()),
      this.makeButton('Paste',
                 (e, obj) => e.diagram.commandHandler.pasteSelection(e.diagram.lastInput.documentPoint),
                 o => o.diagram.commandHandler.canPasteSelection()),
      this.makeButton('Delete',
                 (e, obj) => e.diagram.commandHandler.deleteSelection(),
                 o => o.diagram.commandHandler.canDeleteSelection()),
      this.makeButton('Undo',
                 (e, obj) => e.diagram.commandHandler.undo(),
                 o => o.diagram.commandHandler.canUndo()),
      this.makeButton('Redo',
                 (e, obj) => e.diagram.commandHandler.redo(),
                 o => o.diagram.commandHandler.canRedo()),
      this.makeButton('Group',
                 (e, obj) => e.diagram.commandHandler.groupSelection(),
                 o => o.diagram.commandHandler.canGroupSelection()),
      this.makeButton('Ungroup',
                 (e, obj) => e.diagram.commandHandler.ungroupSelection(),
                 o => o.diagram.commandHandler.canUngroupSelection())
      );

  nodeInfo(d) {  // Tooltip info for a node data object
    let str = 'Node ' + d.key + ': ' + d.text + '\n';
    if (d.group) {
      str += 'member of ' + d.group;
    } else {
      str += 'top-level node'; }
    return str;
  }

  linkInfo(d) {  // Tooltip info for a link data object
    return 'Link:\nfrom ' + d.from + ' to ' + d.to;
  }

  groupInfo(adornment: go.Adornment) {  // takes the tooltip, not a group node data object
    const g = <go.Group>adornment.adornedPart;  // get the Group that the tooltip adorns
    const mems = g.memberParts.count;
    const links = g.memberParts.filter(p => p instanceof go.Link).count;
    return 'Group ' + g.data.key + ': ' + g.data.text + '\n' + mems + ' members including ' + links + ' links';
  }

  makeButton(text: string, action: (e: go.InputEvent, obj: go.GraphObject) => void, visiblePredicate?: (obj: go.GraphObject) => boolean) {
    if (visiblePredicate === undefined) {
      visiblePredicate = o => true;
    }
    return this.$('ContextMenuButton',
                this.$(go.TextBlock, text),
                { click: action },
                // don't bother with binding GraphObject.visible if there's no predicate
                visiblePredicate ? new go.Binding('visible', '', visiblePredicate).ofObject() : {});
  }

  diagramInfo(model: go.GraphLinksModel) {  // Tooltip info for the diagram's model
        return 'Model:\n' + model.nodeDataArray.length + ' nodes, ' + model.linkDataArray.length + ' links';
    }

  stayInGroup(part, pt, gridpt) {
    // don't constrain top-level nodes
    const grp = part.containingGroup;
    if (grp === null) {return pt; }
    // try to stay within the background Shape of the Group
    const back = grp.resizeObject;
    if (back === null) {return pt; }
    // allow dragging a Node out of a Group if the Shift key is down
    if (part.diagram.lastInput.shift) {return pt; }
    const p1 = back.getDocumentPoint(go.Spot.TopLeft);
    const p2 = back.getDocumentPoint(go.Spot.BottomRight);
    const b = part.actualBounds;
    const loc = part.location;
    // find the padding inside the group's placeholder that is around the member parts
    const m = grp.placeholder.padding;
    // now limit the location appropriately
    const x = Math.max(p1.x + m.left, Math.min(pt.x, p2.x - m.right - b.width - 1)) + (loc.x - b.x);
    const y = Math.max(p1.y + m.top, Math.min(pt.y, p2.y - m.bottom - b.height - 1)) + (loc.y - b.y);
    return new go.Point(x, y);
  }

  updateCrossLaneLinks(group) {
    group.findExternalLinksConnected().each(function(l) {
      l.visible = (l.fromNode.isVisible() && l.toNode.isVisible());
    });
  }

  groupStyle() {  // common settings for both Lane and Pool Groups
    return [
      {
        layerName: 'Background',  // all pools and lanes are always behind all nodes and links
        background: 'transparent',  // can grab anywhere in bounds
        movable: true, // allows users to re-order by dragging
        copyable: false,  // can't copy lanes or pools
        avoidable: false,  // don't impede AvoidsNodes routed Links
        minLocation: new go.Point(NaN, -Infinity),  // only allow vertical movement
        maxLocation: new go.Point(NaN, Infinity),
        resizeCellSize: new go.Size(60 , 60)
      },
      new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify)
    ];
  }
}

test5() {
    const $ = go.GraphObject.make;
    this.diagram = new go.Diagram();
    const laneResizingtool = new LaneResizingtool();
    console.log(laneResizingtool);
    this.diagram.toolManager.resizingTool = laneResizingtool;
    console.log(this.diagram.toolManager);
    this.diagram.toolManager.isEnabled = true;
    this.diagram.mouseDragOver = function(e) {
      if (!e.diagram.selection.all(function(n) { return n instanceof go.Group; })) {
        e.diagram.currentCursor = 'not-allowed';
        }
      };
    this.diagram.mouseDrop = function(e) {
        if (!e.diagram.selection.all(function(n) { return n instanceof go.Group; })) {
          e.diagram.currentTool.doCancel();
        }
      };
    this.diagram.commandHandler.copiesGroupKey = true;
  //  this.diagram.SelectionMoved = laneResizingtool.relayoutDiagram;
    this.diagram.animationManager.isEnabled = true;
    this.diagram.undoManager.isEnabled = true;
    this.diagram.nodeTemplate =
    $(go.Node, 'Table',
    {
      locationObjectName: 'BODY',
      locationSpot: go.Spot.Center,
      selectionObjectName: 'BODY',
    },
    new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify),
    $(go.Shape, 'Rectangle',
      { fill: 'white', portId: '', cursor: 'pointer', fromLinkable: true, toLinkable: true }),
    $(go.TextBlock, { margin: 5 },
      new go.Binding('text', 'key')),
    { dragComputation: this.detail.stayInGroup } // limit dragging of Nodes to stay within the containing Group, defined above
  );
    this.diagram.linkTemplate = GoLinkTemplate.getLinkTemplateTest();

    this.diagram.toolManager.clickCreatingTool.archetypeNodeData = {
      leftArray: [],
      rightArray: [],
      topArray: [],
      bottomArray: []
    };

    this.diagram.groupTemplate =
        $(go.Group, 'Horizontal', this.detail.groupStyle(),
         {
            selectionObjectName: 'SHAPE',  // selecting a lane causes the body of the lane to be highlit, not the label
            resizable: true, resizeObjectName: 'SHAPE',  // the custom resizeAdornmentTemplate only permits two kinds of resizing
            layout: $(go.LayeredDigraphLayout,  // automatically lay out the lane's subgraph
              {
                // isInitial: false,  // don't even do initial layout
                // isOngoing: false,  // don't invalidate layout when nodes or links are added or removed
                direction: 0,
                columnSpacing: 10,
                layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource
              }),
            computesBoundsAfterDrag: true,  // needed to prevent recomputing Group.placeholder bounds too soon
            computesBoundsIncludingLinks: false,  // to reduce occurrences of links going briefly outside the lane
            computesBoundsIncludingLocation: true,  // to support empty space at top-left corner of lane
            handlesDragDropForMembers: true,  // don't need to define handlers on member Nodes and Links
            mouseDrop: function(e, grp: go.Group ) {  // dropping a copy of some Nodes and Links onto this Group adds them to this Group
              if (!e.shift) {return; } // cannot change groups with an unmodified drag-and-drop
              // don't allow drag-and-dropping a mix of regular Nodes and Groups
              if (!e.diagram.selection.any(function(n) { return n instanceof go.Group; })) {
                const ok = grp.addMembers(grp.diagram.selection, true);
                if (ok) {
                  this.detail.updateCrossLaneLinks(grp);
                } else {
                  grp.diagram.currentTool.doCancel();
                }
              } else {
                e.diagram.currentTool.doCancel();
              }
            },
            subGraphExpandedChanged: function(grp) {
              const shp = grp.resizeObject;
              if (grp.diagram.undoManager.isUndoingRedoing) {return; }
              if (grp.isSubGraphExpanded) {
                shp.height = grp._savedBreadth;
              } else {
                grp._savedBreadth = shp.height;
                shp.height = NaN;
              }
              this.detail.updateCrossLaneLinks(grp);
            }
          },
          new go.Binding('isSubGraphExpanded', 'expanded').makeTwoWay(),
          // the lane header consisting of a Shape and a TextBlock
          $(go.Panel, 'Horizontal',
            {
              name: 'HEADER',
              angle: 270,  // maybe rotate the header to read sideways going up
              alignment: go.Spot.Left
            },
            $(go.Panel, 'Horizontal',  // this is hidden when the swimlane is collapsed
              new go.Binding('visible', 'isSubGraphExpanded').ofObject(),
              $(go.Shape, 'Diamond',
                { width: 8, height: 8, fill: 'white' },
                new go.Binding('fill', 'color')),
              $(go.TextBlock,  // the lane label
                { font: 'bold 13pt sans-serif', editable: false, margin: new go.Margin(2, 0, 0, 0) },
                new go.Binding('text', 'text').makeTwoWay())
            ),
            $('SubGraphExpanderButton', { margin: 10 })  // but this remains always visible!
          ),  // end Horizontal Panel
          $(go.Panel, 'Auto',  // the lane consisting of a background Shape and a Placeholder representing the subgraph
            $(go.Shape, 'Rectangle',  // this is the resized object
              { name: 'SHAPE', fill: 'white' },
              new go.Binding('fill', 'color'),
              new go.Binding('desiredSize', 'size', go.Size.parse).makeTwoWay(go.Size.stringify)),
            $(go.Placeholder,
              { padding: 12, alignment: go.Spot.TopLeft }),
            $(go.TextBlock,  // this TextBlock is only seen when the swimlane is collapsed
              {
                name: 'LABEL',
                font: 'bold 13pt sans-serif', editable: false,
                angle: 0, alignment: go.Spot.TopLeft, margin: new go.Margin(2, 0, 0, 4)
              },
              new go.Binding('visible', 'isSubGraphExpanded', function(e) { return !e; }).ofObject(),
              new go.Binding('text', 'text').makeTwoWay())
          )  // end Auto Panel
        );  // end Group

    this.diagram.groupTemplate.resizeAdornmentTemplate =
        $(go.Adornment, 'Spot',
          $(go.Placeholder),
          $(go.Shape,  // for changing the length of a lane
            {
              alignment: go.Spot.Right,
              desiredSize: new go.Size(7, 50),
              fill: 'lightblue', stroke: 'dodgerblue',
              cursor: 'col-resize'
            },
            new go.Binding('visible', '', function(ad) {
              if (ad.adornedPart === null) { return false; }
              return ad.adornedPart.isSubGraphExpanded;
            }).ofObject()),
          $(go.Shape,  // for changing the breadth of a lane
            {
              alignment: go.Spot.Bottom,
              desiredSize: new go.Size(50, 7),
              fill: 'lightblue', stroke: 'dodgerblue',
              cursor: 'row-resize'
            },
            new go.Binding('visible', '', function(ad) {
              if (ad.adornedPart === null) { return false; }
              return ad.adornedPart.isSubGraphExpanded;
            }).ofObject())
        );

      this.diagram.groupTemplateMap.add('Pool',
        $(go.Group, 'Auto',
        this.detail.groupStyle(),
          { // use a simple layout that ignores links to stack the 'lane' Groups on top of each other
            layout: $(PoolLayout, { spacing: new go.Size(0, 0) })  // no space between lanes
          },
          $(go.Shape,
            { fill: 'white' },
            new go.Binding('fill', 'color')),
          $(go.Panel, 'Table',
            { defaultColumnSeparatorStroke: 'black' },
            $(go.Panel, 'Horizontal',
              { column: 0, angle: 270 },
              $(go.TextBlock,
                { font: 'bold 16pt sans-serif', editable: false, margin: new go.Margin(2, 0, 0, 0) },
                new go.Binding('text').makeTwoWay())
            ),
            $('SubGraphExpanderButton', { margin: 20 }),  // but this remains always visible!
            $(go.Placeholder,
              { column: 1 })
          )
        ));

        this.diagram.linkTemplate = GoLinkTemplate.getLinkTemplateTest();

      const test = new go.GraphLinksModel([ // node data
        { key: 'Pool1', text: 'Pool', isGroup: true, category: 'Pool' },
        { key: 'Pool2', text: 'Pool2', isGroup: true, category: 'Pool' },
        { key: 'Lane1', text: 'Lane1', isGroup: true, group: 'Pool1', color: 'lightblue' },
        { key: 'Lane2', text: 'Lane2', isGroup: true, group: 'Pool1', color: 'lightgreen' },
        { key: 'Lane3', text: 'Lane3', isGroup: true, group: 'Pool1', color: 'lightyellow' },
        { key: 'Lane4', text: 'Lane4', isGroup: true, group: 'Pool1', color: 'orange' },
        { key: 'oneA', group: 'Lane1' },
        { key: 'oneB', group: 'Lane1' },
        { key: 'oneC', group: 'Lane1' },
        { key: 'oneD', group: 'Lane1' },
        { key: 'twoA', group: 'Lane2' },
        { key: 'twoB', group: 'Lane2' },
        { key: 'twoC', group: 'Lane2' },
        { key: 'twoD', group: 'Lane2' },
        { key: 'twoE', group: 'Lane2' },
        { key: 'twoF', group: 'Lane2' },
        { key: 'twoG', group: 'Lane2' },
        { key: 'fourA', group: 'Lane4' },
        { key: 'fourB', group: 'Lane4' },
        { key: 'fourC', group: 'Lane4' },
        { key: 'fourD', group: 'Lane4' }, { key: 'fourE', loc: '100 200' , group: 'Lane4' },
        { key: 'fourF', loc: '100 230' , group: 'Lane4' }, { key: 'four', loc: '100 240' , group: 'Lane4' },
        { key: 'Lane5', text: 'Lane5', isGroup: true, group: 'Pool2', color: 'lightyellow' },
        { key: 'Lane6', text: 'Lane6', isGroup: true, group: 'Pool2', color: 'lightgreen' },
        { key: 'fiveA', group: 'Lane5' },
        { key: 'sixA', group: 'Lane6' }
      ],
      [ // link data
        { from: 'oneA', to: 'oneB' },
        { from: 'oneA', to: 'oneC' },
        { from: 'oneB', to: 'oneD' },
        { from: 'oneC', to: 'oneD' },
        { from: 'twoA', to: 'twoB' },
        { from: 'twoA', to: 'twoC' },
        { from: 'twoA', to: 'twoF' },
        { from: 'twoB', to: 'twoD' },
        { from: 'twoC', to: 'twoD' },
        { from: 'twoD', to: 'twoG' },
        { from: 'twoE', to: 'twoG' },
        { from: 'twoF', to: 'twoG' },
        { from: 'fourA', to: 'fourB' },
        { from: 'fourB', to: 'fourC' }, { from: 'four', to: 'fourF' },
        { from: 'fourC', to: 'fourD' }
      ]);

      this.diagram.model = test;
  }

import { DetailsObjet } from './detail-objet';

import * as go from 'gojs';
import { CustomLink } from './go.custom';

export class GoLinkTemplate {

  private static detail: DetailsObjet = new DetailsObjet();

  public static getLinkTemplate(): go.Link {
    const $ = go.GraphObject.make;
    // go.Diagram.inherit(CustomLink, go.Link);
    const link: go.Link =
      $(CustomLink,
        new go.Binding('routing', 'routing'),
        {
          routing: go.Link.AvoidsNodes,
          corner: 5,
          curve: go.Link.JumpGap,
          reshapable: true,
          resegmentable: true,
          relinkableFrom: true,
          relinkableTo: true
        },
        new go.Binding('points').makeTwoWay(),
        $(go.Shape, { stroke: '#2F4F4F', strokeWidth: 2, toArrow: 'OpenTriangle' }),
        { // this tooltip Adornment is shared by all links
          toolTip:
          $(go.Adornment, 'Auto',
              $(go.Shape, { fill: '#FFFFCC' }),
              $(go.TextBlock, { margin: 4 },  // the tooltip shows the result of calling linkInfo(data)
                  new go.Binding('text', '', this.detail.linkInfo))
              ),
          // the same context menu Adornment is shared by all links
          contextMenu: this.detail.partContextMenu
      }
      );

      return link;
  }

  public static getLinkTemplateTest(): go.Link {
    const $ = go.GraphObject.make;
    // go.Diagram.inherit(CustomLink, go.Link);
    const link: go.Link =
    $(go.Link,
      { routing: go.Link.AvoidsNodes, corner: 5 },
      { relinkableFrom: true, relinkableTo: true },
      $(go.Shape)
    );

      return link;
  }
}

result:

I haven’t translated the whole sample, but I did try translating PoolLayout.ts, starting from your code. I noticed that your override of doLayout wasn’t calling the super method. Also some calls to bind were unnecessary, some type declarations were missing, and I changed some function(...) { ... } to lambdas: (...) => { ... }.

import * as go from '../release/go.js';

export class PoolLayout extends go.GridLayout {

  private static MINLENGTH = 200;  // this controls the minimum length of any swimlane
  private static MINBREADTH = 20;  // this controls the minimum breadth of any non-collapsed swimlane

  constructor() {
    super();
    this.cellSize = new go.Size(1, 1);
    this.wrappingColumn = 1;
    this.wrappingWidth = Infinity;
    this.isRealtime = false;  // don't continuously layout while dragging
    this.alignment = go.GridLayout.Position;
    // This sorts based on the location of each Group.
    // This is useful when Groups can be moved up and down in order to change their order.
    this.comparer = function(a, b) {
      const ay = a.location.y;
      const by = b.location.y;
      if (isNaN(ay) || isNaN(by)) { return 0; }
      if (ay < by) { return -1; }
      if (ay > by) { return 1; }
      return 0;
    };
  }

  private  computeMinLaneSize(lane: go.Group) {
    if (!lane.isSubGraphExpanded) {return new go.Size(PoolLayout.MINLENGTH, 1); }
    return new go.Size(PoolLayout.MINLENGTH, PoolLayout.MINBREADTH);
  }

  private  computeMinPoolSize(pool: go.Group) {
    // assert(pool instanceof go.Group && pool.category === 'Pool');
    let len = PoolLayout.MINLENGTH;
    pool.memberParts.each((lane: go.Part) => {
      // pools ought to only contain lanes, not plain Nodes
      if (!(lane instanceof go.Group)) return;
      const holder = lane.placeholder;
      if (holder !== null) {
        const sz = holder.actualBounds;
        len = Math.max(len, sz.width);
      }
    });
    return new go.Size(len, NaN);
  }

  private  computeLaneSize(lane: go.Group) {
    // assert(lane instanceof go.Group && lane.category !== 'Pool');
    const sz = this.computeMinLaneSize(lane);
    if (lane.isSubGraphExpanded) {
      const holder = lane.placeholder;
      if (holder !== null) {
        const hsz = holder.actualBounds;
        sz.height = Math.max(sz.height, hsz.height);
      }
    }
    // minimum breadth needs to be big enough to hold the header
    const hdr = lane.findObject('HEADER');
    if (hdr !== null) {sz.height = Math.max(sz.height, hdr.actualBounds.height); }
    return sz;
  }

  public doLayout(coll: go.Diagram | go.Group | go.Iterable<go.Part>) {
    const diagram = this.diagram;
    if (diagram === null) return;
    diagram.startTransaction('PoolLayout');
    const pool = this.group;
    if (pool !== null && pool.category === 'Pool') {
      // make sure all of the Group Shapes are big enough
      const minsize = this.computeMinPoolSize(pool);
      pool.memberParts.each((lane: go.Part) => {
        if (!(lane instanceof go.Group)) return;
        if (lane.category !== 'Pool') {
          const shape = lane.resizeObject;
          if (shape !== null) {  // change the desiredSize to be big enough in both directions
            const sz = this.computeLaneSize(lane);
            shape.width = (isNaN(shape.width) ? minsize.width : Math.max(shape.width, minsize.width));
            shape.height = (!isNaN(shape.height)) ? Math.max(shape.height, sz.height) : sz.height;
            const cell = lane.resizeCellSize;
            if (!isNaN(shape.width) && !isNaN(cell.width) && cell.width > 0) {
              shape.width = Math.ceil(shape.width / cell.width) * cell.width; }
            if (!isNaN(shape.height) && !isNaN(cell.height) && cell.height > 0) {
              shape.height = Math.ceil(shape.height / cell.height) * cell.height; }
          }
        }
      });
    }
    // now do all of the usual stuff, according to whatever properties have been set on this GridLayout
    super.doLayout(coll);
    diagram.commitTransaction('PoolLayout');
  };
}