Banded TreeLayout with inconsistent band sizes

I’m using a custom layout which inherits from TreeLayout to draw shaded bands. The height of the bands is inconsistent, as shown by the red arrows in this image:

You can see here the bottom band is smaller than the others, causing the globe icon to be outside the band. For other diagrams this is a real problem, as there isn’t enough space to show the link (the links are green:the tiny blob of green you can see below the ‘cog’ nodes):

I need either the bands to be a consistent size, set in code (as all the nodes will have a similar size) or to adequately contain the nodes inside them, preferably with a bit of margin for aesthetics.

Here’s my layout:

function BandedTreeLayout() {
    go.TreeLayout.call(this);
    this.layerStyle = go.TreeLayout.LayerUniform;
    this.sorting = go.TreeLayout.SortingAscending;
}
go.Diagram.inherit(BandedTreeLayout, go.TreeLayout);

BandedTreeLayout.prototype.commitLayers = function(layerRects, offset) {
    var bands = this.diagram.findPartForKey("_BANDS");
    if (bands) {
      var model = this.diagram.model;
      bands.location = this.arrangementOrigin.copy().add(offset);
      var arr = bands.data.itemArray;
      for (var i = 0; i < layerRects.length; i++) {
        var itemdata = arr[i];
        if (itemdata) {
            var size = layerRects[i];
            // I've tried various things here, but they only affect the last band...
            //size.height = 600;
            //size.addMargin(new go.Margin(50));
            model.setDataProperty(itemdata, "bounds", size);
        }
      }
    }
    if (window.console) console.log('Layers: ' + layerRects.length);
    for (var it = this.network.vertexes.iterator; it.next();) {
        var v = it.value;
        var n = v.node;
        if (!n.data.dns) {
            continue;
        }
        if (layerRects[n.data.tierOffset]) {
            var pos = v.bounds.position;
            pos.y = layerRects[n.data.tierOffset].y;
            n.move(pos);
        } else {
            if (window.console) console.log('Could not find layerRects with offset ' + n.data.tierOffset);
        }
    }
};
// end BandedTreeLayout

And here’s the bands template (wrapped in a function as it’s called by a couple of different diagrams) with some comments from me about what I’ve tried:

function getBandsTemplate($) {
    return $(go.Part, "Position",
    new go.Binding("itemArray"),
    {
        isLayoutPositioned: false,
        layerName: "Background",
        pickable: false,
        selectable: false,
        itemTemplate:
        $(go.Panel, HORIZONTAL ? "Vertical" : "Horizontal",
            new go.Binding("position", "bounds", function(b) {
                return b.position;
            }),
            new go.Binding("desiredSize", "bounds", function (r) {
                if (r.size.height < 300) r.size.height = 300; // this does nothing
                return r.size;
            }),
            $(go.Shape,
                { stroke: null, strokeWidth: 0 },
                new go.Binding("desiredSize", "bounds", function (r) {
                    return r.size; // tried various things here to no avail
                }),
                new go.Binding("fill", "itemIndex", function(i) {
                    return i % 2 === 0 ? "#FFFFFF" : "#EEEEEE";
                }).ofObject())
        )
    });
}

I’m calling the layout like this, again with additional comments for what I’ve tried:

layout: $(BandedTreeLayout,
{
    angle: HORIZONTAL ? 0 : 90,
    arrangement: HORIZONTAL ? go.TreeLayout.ArrangementVertical : go.TreeLayout.ArrangementHorizontal,
    layerSpacing: 100, // affects all bands, meaning some are too big
    nodeSpacing: 100 // does not help this problem
})

Thanks.

First, I should point out that expressions such as r.size.height = 300 do not modify r. What happens is that r.size returns a new Size object whose height is set to 300 and which is then discarded. Later, r.size will return the original size of the Rect.

Second, setting data.bounds to an instance of Size is wrong – you need to set it to an instance of Rect, because elsewhere bounds is treated as a Rect.

Third, I tried modifying the Swim Bands sample, https://gojs.net/latest/samples/swimBands.html, so that HORIZONTAL is false and so that there was a new go.Binding("desiredSize", "size") on the node template. I also added this code:

  function randomizeSizes() {
    myDiagram.startTransaction();
    myDiagram.nodes.each(function(n) { n.desiredSize = new go.Size(NaN, Math.random() * 60 + 10); });
    myDiagram.commitTransaction("random sizes");
  }

Calling that function repeatedly resulted each time in the nodes getting sized differently. That resulted in another layout each time. And each time the resulting bands were correctly sized and positioned.

Now that was in the modified Swim Bands sample, not using your code. I just wanted to reassure you that the overridden TreeLayout.commitLayers method is being called correctly.

Thanks @walter, I think I’ve got it working now.

Actually I don’t think this is working, unfortunately. I’ve used the swimBands example but extended it with the modifications I’ve made. First here’s the data:

// define the tree node data
var nodearray = [
  { // this is the information needed for the headers of the bands
    key: "_BANDS",
    category: "Bands",
    itemArray: [
      { text: "Zero" },
      { text: "One" },
      { text: "Two" },
      { text: "Three" },
      { text: "Four" },
      { text: "Five" }
    ]
  },
  // these are the regular nodes in the TreeModel
    { key: "0" },
    { key: "1", parent: "0" },
    { key: "2", parent: "1" },
    { key: "3", parent: "2" },
    { key: "4", parent: "3" },
    { key: "5", parent: "4" },
  { key: "root" },
  { key: "oneB", parent: "root" },
  { key: "twoA", parent: "oneB" },
  { key: "twoC", parent: "root" },
  { key: "threeC", parent: "twoC" },
  { key: "threeD", parent: "twoC" },
  { key: "fourC", parent: "twoC" },
    { key: "fourD", parent: "threeD", tierOffset: 4 },
    { key: "twoD", parent: "root" },
    { key: "fiveA", parent: "threeC", tierOffset: 5 }
];

You’ll see I’ve got nodes “0” - “5”, one in each band. This is important, as some of the nodes “fourD” and “fiveA” are not positioned in their natural bands.

I’ve modified BandedTreeLayout.prototype.commitLayers to move nodes around based on a data property:

BandedTreeLayout.prototype.commitLayers = function(layerRects, offset) {
 // update the background object holding the visual "bands"
 var bands = this.diagram.findPartForKey("_BANDS");
 if (bands) {
    var model = this.diagram.model;
    bands.location = this.arrangementOrigin.copy().add(offset);

    // move any nodes to different bands if required
    for (var it = this.network.vertexes.iterator; it.next();) {
        var v = it.value;
        var n = v.node;
        if (layerRects[n.data.tierOffset]) {
            var pos = v.bounds.position;
            pos.y = layerRects[n.data.tierOffset].y;
            n.move(pos);
        }
    }

    // set the bounds of each band via data binding of the "bounds" property
    var arr = bands.data.itemArray;
    for (var i = 0; i < layerRects.length; i++) {
        var itemdata = arr[i];
        if (itemdata) {
            var size = layerRects[i];
            model.setDataProperty(itemdata, "bounds", size);
        }
    }
}
};

What I get first time the diagram loads is this:

But I only have to run the randomizeSizes() function a few times and I see things like this:

I guess it’s the moving of the nodes which causes this. Do I need to trigger something inside commitLayers, or is there a better way to do this?

Many thanks.

Well, yes, if you’re going to move nodes around after commitLayers has been called, of course those nodes might be outside the layer bounds that were passed to that method.

Here’s a version that allows the model to set the band for a node in the data:
https://gojs.net/temp/swimbands2.html

(Sorry, you’ll need to change the code to handle TreeLayout.angle === 90.)

That’s excellent, I’ve got it sorted now. Many thanks @walter