Issues with collapsed groups with LayeredDigraphLayout

Use Case 1 - Group After Node

Model

"nodes": [
    { "key": 1, "name": "Start Node" },
    { "key": 4, "name": "After Test" },
    { "key": 20, "name": "Test", "isGroup": true },
    { "key": 21, "name": "Path 1.0", "group": 20 },
    { "key": 22, "name": "Path 2.0", "group": 20 },
    { "key": 23, "name": "Path 3.0", "group": 20 },
    { "key": 24, "name": "Path 1.1", "group": 20 },
    { "key": 26, "name": "Path 2.1", "group": 20 },
    { "key": 28, "name": "Path 2.2", "group": 20 }
],
"links": [
    { "from": 1, "to": 20 },
    { "from": 1, "to": 21, "transparent": true },
    { "from": 1, "to": 22, "transparent": true },
    { "from": 1, "to": 23, "transparent": true },
    { "from": 21, "to": 24 },
    { "from": 22, "to": 26 },
    { "from": 26, "to": 28 },
    { "from": 24, "to": 4, "transparent": true },
    { "from": 28, "to": 4, "transparent": true },
    { "from": 23, "to": 4, "transparent": true },
    { "from": 20, "to": 4 }
]

Expanded

Collapsed

Looks great except my band labels are no longer sequentially ordered. Where’s the “+3” labeled band?

Use Case 2 - Group After Another Group

Model

"nodes": [
    { "key": 1, "name": "Start Node" },
    { "key": 10, "name": "Branch", "isGroup": true },
    { "key": 11, "name": "Right Path", "group": 10 },
    { "key": 12, "name": "Left Path", "group": 10 },
    { "key": 2, "name": "After Left Path" },
    { "key": 4, "name": "After Test" },
    { "key": 20, "name": "Test", "isGroup": true },
    { "key": 21, "name": "Path 1.0", "group": 20 },
    { "key": 22, "name": "Path 2.0", "group": 20 },
    { "key": 23, "name": "Path 3.0", "group": 20 },
    { "key": 24, "name": "Path 1.1", "group": 20 },
    { "key": 26, "name": "Path 2.1", "group": 20 },
    { "key": 28, "name": "Path 2.2", "group": 20 }
],
"links": [
    { "from": 1, "to": 10 },
    { "from": 1, "to": 11, "transparent": true },
    { "from": 1, "to": 12, "transparent": true },
    { "from": 11, "to": 20 },
    { "from": 11, "to": 21, "transparent": true },
    { "from": 11, "to": 22, "transparent": true },
    { "from": 11, "to": 23, "transparent": true },
    { "from": 21, "to": 24 },
    { "from": 22, "to": 26 },
    { "from": 26, "to": 28 },
    { "from": 24, "to": 4, "transparent": true },
    { "from": 28, "to": 4, "transparent": true },
    { "from": 23, "to": 4, "transparent": true },
    { "from": 20, "to": 4 },
    { "from": 12, "to": 2 }
]

Expanded

Collapsed

Notice the “Test” collapsed group is in the wrong band. It should be in the “+2” labeled band. The band labels are not sequentially ordered. Where’s the “+4” labeled band?

To get this to work, I had to set my group.layout as follows.

new go.Binding("layout", "", function(data) {
    var group = diagram.findPartForKey(data.key);
    return group.isSubGraphExpanded ? null : $$(go.GridLayout);
}),

Any suggestions on getting my collapsed groups working properly?

Do you have Group.layout normally set to null? When Group.layout is null things should be laid out as if the Group weren’t there at all. Which could explain why it isn’t quite to your liking when Groups are collapsed.

How are the bands determined in your app? What determines their visibility?

What would you expect to happen in your second case if the Branch group (in band +1) were to be collapsed?

I normally have Group.layout = null when expanded to make the group surround it’s member nodes and behave like it’s not there with regards to layout.

When collapsed, I need it to be like a real node and be included in the layout in a band. So I set the layout when collapsed to a layout to get this to work.

I followed your Layer Bands using a Background Part sample and switched it to a LayeredDigraphLayout for my band implementation. Up until this point, the LayeredDigraphLayout automatically positioned the nodes in the correct band.

I copied this code directly from your sample for band visibility.

// make each band visible or not, depending on whether there is a layer for it
for (var it = bands.elements; it.next(); ) {
    var idx = it.key;
    var elt = it.value;  // the item panel representing a band
    elt.visible = idx < layerRects.length;
}

If the “Branch” group were collapsed, it currently behaves as expected.

If I collapse both groups, I see the same issue as above.

I create my band items like this assigning a sequential key to each.

function _createBandItems() {
    var MAX_BANDS = 1000;
    var items = [];
    for(var i = 0; i < MAX_BANDS; i++) {
        items.push({ key: i });
    }
    return items;
}

In my band header template, I render the band header text like this.

new go.Binding("text", "", function(data) {
    return data.key
        ? RB.getText("JourneyDiagram.bandHeader.subsequent.title.fmt", [data.key])
        : RB.getText("JourneyDiagram.bandHeader.first.title");
})

Resource bundle properties

JourneyDiagram.bandHeader.first.title = Start
JourneyDiagram.bandHeader.subsequent.title.fmt = +{0}

Just noticed that the band labels are incorrect if just the “Branch” group were collapsed. The “+1” band label is missing.

I think what is happening is that the “missing” bands are actually there, but they have zero width, so you don’t see them. The reason that they have zero width is because there are no visible nodes in those bands. Note how in your last screenshot, after collapsing the “Test” group, there’s no “+5” band but there is a “+3” band, because there’s a regular node (“After Left Path”) in it.

Why are there extra “layers” introduced before and/or after each collapsed group? Because LayeredDigraphLayout, unlike TreeLayout, introduces extra “dummy” vertexes in order to route links which cross over layers. These dummy vertexes do not have any real Nodes associated with them, and they have zero size, but they do introduce a layer between what we see as the “real” layers of nodes.

Those dummy vertexes are an inherent part of the design of LayeredDigraphLayout, so you can’t get around them. (In other words, it has nothing to do with groups, collapsed or expanded.) You could easily fix the issue with apparently-missing bands by renaming/renumbering them to skip over the zero-width layers. You’ll need to do that in any case if you are using LayeredDigraphLayout, unless you can be assured that your graph structures are such that there will never be any links that cross over layers. I suppose if they really are tree-structured, that would be OK, but then you would be using TreeLayout instead and wouldn’t have run into this complication.

But I think the real issue is that you’d like to treat collapsed groups as if they were simple nodes. I’ll need to investigate this, but it should be easy enough to customize the LayeredDigraphLayout to do that kind of treatment.

Actually, how do you define your Link template? I assume there’s a Binding on data.transparent.

I’ll try your suggestion to ignore zero-width layers to get the correct band header labels.

We do have use cases where a node may have multiple inputs. So we do need a LayeredDiagraphLayout as opposed to a TreeLayout.

My link template does bind on data.transparent as follows.

$$(go.Shape, "Rectangle",
    {
        strokeDashArray: this.toStrokeDashArray(linkBorderStyle)
    },
    new go.Binding("stroke", "", function(data) {
        return data.transparent
            ? TRANSPARENT
            : data[TRANSIENT] ? addNodeLinkColor : linkColor;
    }),
    new go.Binding("strokeWidth", "", function(data) {
        return data.transparent
            ? TRANSPARENT
            : data[TRANSIENT] ? addNodeLinkWidth : linkWidth;
    }),
    new go.Binding("strokeDashArray", "", function(data) {
        return self.toStrokeDashArray(data[TRANSIENT]
            ? addNodeLinkBorderStyle
            : linkBorderStyle);
    }),
    new go.Binding("fill", "", function(data) {
        return data.transparent
            ? TRANSPARENT
            : data[TRANSIENT] ? addNodeLinkColor : linkColor;
    })
),

I have the following constant at the top of my file.

var TRANSPARENT = "transparent";

That’s pretty complicated – is that just the link path? What is your Link template?

My code has evolved over time from your simple sample into a complex body of code that meets the needs of my project.

My link template is defined a follows.

    diagram.linkTemplate = self._getLinkTemplate();

The excerpt above is from my _getLinkTemplate() function. Don’t want to post my entire link template function because of copyright concerns. If you need something specific from that function, let me know. I’m basically just setting the link properties from styles loaded dynamically from CSS to support theming.

Sorry, I wasn’t questioning your need to do that. I was asking about whether you bound “visible” or “opacity” on the Link object itself, and if so, how.

I make the link invisible by setting stroke = “transparent”. You can see that in the excerpt above.

It would have been easier to bind Link.opacity:

    myDiagram.linkTemplate =
      $(go.Link,
        new go.Binding("opacity", "transparent", function(v) { return v ? 0 : 1; }),
        $(go.Shape)
      );

That way you wouldn’t need to data bind to “” but to “transient” (or whatever value is TRANSIENT), and the conversion functions there would be simpler.

Anyway, I need to experiment with customizing the layout.

You already have a class inheriting from LayeredDigraphLayout, don’t you? Just add this method override:

// delete all duplicate edges to or from collapsed groups
CustomLDLayout.prototype.makeNetwork = function(coll) {
  var net = go.LayeredDigraphLayout.prototype.makeNetwork.call(this, coll);

  var duplicates = new go.Set(go.LayoutEdge);
  var dests = new go.Set(go.LayoutVertex);
  net.vertexes.each(function(v) {
    dests.clear();
    v.destinationEdges.each(function(e) {
      if (dests.contains(e.toVertex)) {
        duplicates.add(e);
      } else {
        dests.add(e.toVertex);
      }
    });
  });

  duplicates.each(function(e) {
    net.deleteEdge(e);
  });

  return net;
};

The problem was that there were duplicate links from (for example) “Right Path” to “Test” when “Test” was collapsed. The duplicate links causes the introduction of the dummy nodes in order to support routing.

For anyone who cares, here’s the complete code for the test case that I worked with:

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

// delete all duplicate edges to or from collapsed groups
CustomLDLayout.prototype.makeNetwork = function(coll) {
  var net = go.LayeredDigraphLayout.prototype.makeNetwork.call(this, coll);

  var duplicates = new go.Set(go.LayoutEdge);
  var dests = new go.Set(go.LayoutVertex);
  net.vertexes.each(function(v) {
    dests.clear();
    v.destinationEdges.each(function(e) {
      if (dests.contains(e.toVertex)) {
        duplicates.add(e);
      } else {
        dests.add(e.toVertex);
      }
    });
  });

  duplicates.each(function(e) {
    net.deleteEdge(e);
  });

  return net;
};

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

    myDiagram =
      $(go.Diagram, "myDiagram",
        { initialContentAlignment: go.Spot.Center, "undoManager.isEnabled": false,
          layout: $(CustomLDLayout, { isRouting: false, columnSpacing: 10, layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource })
        });

    myDiagram.linkTemplate =
      $(go.Link,
        new go.Binding("opacity", "transparent", function(v) { return v ? 0 : 1; }),
        $(go.Shape)
      );

    myDiagram.groupTemplate =
      $(go.Group, "Auto",
        { layerName: "Background" },
        { layout: null },
        new go.Binding("layout", "isSubGraphExpanded", function(exp) { return exp ? null : $(go.Layout); }).ofObject(),
        $(go.Shape, { fill: "whitesmoke", strokeWidth: 0 }),
        $(go.Panel, "Spot",
          $(go.Placeholder, { padding: 5, minSize: new go.Size(100, 50) }),
          $(go.TextBlock, { alignment: go.Spot.TopRight, alignmentFocus: go.Spot.TopRight }, new go.Binding("text", "name")),
          $("SubGraphExpanderButton", { alignment: go.Spot.TopLeft, alignmentFocus: go.Spot.TopLeft })
        )
      );

    myDiagram.model = new go.GraphLinksModel([
        { "key": 1, "name": "Start Node" },
        { "key": 10, "name": "Branch", "isGroup": true },
        { "key": 11, "name": "Right Path", "group": 10 },
        { "key": 12, "name": "Left Path", "group": 10 },
        { "key": 2, "name": "After Left Path" },
        { "key": 4, "name": "After Test" },
        { "key": 20, "name": "Test", "isGroup": true },
        { "key": 21, "name": "Path 1.0", "group": 20 },
        { "key": 22, "name": "Path 2.0", "group": 20 },
        { "key": 23, "name": "Path 3.0", "group": 20 },
        { "key": 24, "name": "Path 1.1", "group": 20 },
        { "key": 26, "name": "Path 2.1", "group": 20 },
        { "key": 28, "name": "Path 2.2", "group": 20 }
      ], [
        { "from": 1, "to": 10 },
        { "from": 1, "to": 11, "transparent": true },
        { "from": 1, "to": 12, "transparent": true },
        { "from": 11, "to": 20 },
        { "from": 11, "to": 21, "transparent": true },
        { "from": 11, "to": 22, "transparent": true },
        { "from": 11, "to": 23, "transparent": true },
        { "from": 21, "to": 24 },
        { "from": 22, "to": 26 },
        { "from": 26, "to": 28 },
        { "from": 24, "to": 4, "transparent": true },
        { "from": 28, "to": 4, "transparent": true },
        { "from": 23, "to": 4, "transparent": true },
        { "from": 20, "to": 4 },
        { "from": 12, "to": 2 }
    ]);
  }

Don’t pay any attention to how much I display a lack of style. I was just trying to get the “layers” correct.

Absolute Perfection! Many Thanks!!!