Setting nodes to specific bands

Hi All :)

I want to create this graph using bands as: Bands

and I want to setting each node his bands:

I added band attribute and using that code but It’s doesn’t work all the time - In case we have Node without edges / in case there is a n1 —> n2 and band(n1) > band(n2) it’s doesn’t work

Actual:

The code:

public initDiagram(): go.Diagram {
        // this controls whether the layout is horizontal and the layer bands are vertical, or vice-versa:
        let HORIZONTAL = this.horizontal;  // this constant parameter can only be set here, not dynamically

        // Perform a TreeLayout where commitLayers is overridden to modify the background Part whose key is "_BANDS".
        function LayeredTreeLayout() {
            go.TreeLayout.call(this);

            this.treeStyle = go.TreeLayout.StyleLayered;  // required
            // new in GoJS v1.4
            this.layerStyle = go.TreeLayout.LayerUniform;

            // don't move subtrees closer together, to maintain possible empty spaces between layers
            this.compaction = go.TreeLayout.CompactionNone;
            // move the parent node towards the top of its subtree area
            this.alignment = go.TreeLayout.AlignmentStart;

            // sort a parent's child vertexes by the value of the index property
            function compareIndexes(v, w) {
                var vidx = v.index;
                if (vidx === undefined) vidx = 0;
                var widx = w.index;
                if (widx === undefined) widx = 0;
                return vidx - widx;
            }

            this.sorting = go.TreeLayout.SortingAscending;
            this.comparer = compareIndexes;

            //this.setsPortSpot = false;
            this.setsChildPortSpot = false;
        }

        go.Diagram.inherit(LayeredTreeLayout, go.TreeLayout);

        // Modify the standard LayoutNetwork by making children with the same "band" value as their
        // parents actually be children of the grandparent.
        LayeredTreeLayout.prototype.makeNetwork = function (coll) {
            var net = go.TreeLayout.prototype.makeNetwork.call(this, coll);
            // annotate every child with an index, used for sorting
            for (var it = net.vertexes.iterator; it.next();) {
                var parent = it.value;
                var idx = 0;
                for (var cit = parent.destinationVertexes; cit.next();) {
                    var child = cit.value;
                    child.index = idx;
                    idx += 10000;
                }
            }
            // now look for children with the same band value as their parent
            for (var it = net.vertexes.iterator; it.next();) {
                var parent = it.value;
                // Should this be recursively looking for grandchildren/greatgrandchildren that
                // have the same band as this parent node??  Assume that is NOT required.
                var parentband = parent.node.data.band;
                var edges = [];
                for (var eit = parent.destinationEdges; eit.next();) {
                    var edge = eit.value;
                    var child = edge.toVertex;
                    var childband = child.node.data.band;
                    if (childband <= parentband) edges.push(edge);
                }
                // for each LayoutEdge that connects the parent vertex with a child vertex
                // whose node has the same band #, reconnect the edge with the parent's parent vertex
                var grandparent = parent.sourceVertexes.first();
                if (grandparent !== null) {
                    var cidx = 1;
                    for (var i = 0; i < edges.length; i++) {
                        var e = edges[i];
                        parent.deleteDestinationEdge(e);
                        e.fromVertex = grandparent;
                        grandparent.addDestinationEdge(e);
                        var child = e.toVertex;
                        child.index = parent.index + cidx;
                        cidx++;
                    }
                }
            }
            return net;
        };

        LayeredTreeLayout.prototype.assignTreeVertexValues = function (v) {
            if (v.node && v.node.data && v.node.data.band) {
                v.originalLevel = v.level;  // remember tree assignment
                v.level = Math.max(v.level, v.node.data.band);  // shift down to meet band requirement
            }
        };

        LayeredTreeLayout.prototype.commitLayers = function (layerRects, offset) {
            for (var it = this.network.vertexes.iterator; it.next();) {
                var v = it.value;
                var n = v.node;
                if (n && v.originalLevel) {
                    // the band specifies the horizontal position
                    var diff = n.data.band - v.originalLevel;
                    if (diff > 0) {
                        var pos = v.bounds.position;
                        // this assumes that the angle is zero: rightward growth
                        pos.x = layerRects[v.level].x;
                        n.move(pos);
                    }
                }
            }

            // update the background object holding the visual "bands"
            var bands = this.diagram.findPartForKey("_BANDS");
            if (bands) {
                bands.layerRects = layerRects;  // remember the Array of Rect

                var model = this.diagram.model;
                for (var it = this.network.vertexes.iterator; it.next();) {
                    var v = it.value;
                    model.setDataProperty(v.node.data, "level", v.level);
                }

                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) {
                        model.setDataProperty(itemdata, "bounds", layerRects[i]);
                    }
                }
            }
        };

and the nodes are:

0: {band: 0, category: “simple”, key: “Q0”,…}
1: {band: 1, category: “simple”, key: “Q1”,…}
2: {band: 2, category: “simple”, key: “Q2”,…}
3: {band: 3, category: “simple”, key: “Q3”,…}
4: {band: 4, category: “simple”, key: “Q4”,…}
5: {band: 5, category: “simple”, key: “Q5”,…}
6: {band: 6, category: “simple”, key: “Q6”,…}
7: {category: “Bands”,…}

category: “Bands”
itemArray: [{text: “(0 inc)”}, {text: “(0 std)”}, {text: “(0 dec)”}, {text: “((0 top) inc)”}, {text: “(top std)”},…]

  1. 0: {text: “(0 inc)”}
  2. 1: {text: “(0 std)”}
  3. 2: {text: “(0 dec)”}
  4. 3: {text: “((0 top) inc)”}
  5. 4: {text: “(top std)”}
  6. 5: {text: “(top inc)”}
  7. 6: {text: “(L1 std)”}

Please help me guys :(

Thanks and have a nice day :)

Ah, the problem is that singleton nodes (vertexes) are treated as separate trees. OK, here are two updated samples.

https://gojs.net/extras/swimBandsBackgroundPart.html using a single background Part holding a Panel for each band; can use either a TreeModel or a GraphLinksModel

https://gojs.net/extras/swimBandsGroups.html using a Group for each band; requires a GraphLinksModel

I have made other improvements and cleanups from when your code was originally written many years ago, which seems to have been written around version 1.4. Your code has its model data organized like that in swimBandsBackgroundParts.

Hi! Thanks I used the first example but…

I send to Q3 band 0 and it displayed him in band 1 (please see my attached screenshot)

can you help me figure this out?

There are my nodes:

0: {category: “VerticalBands”,…}
1: {band: 0, category: “simple”, key: “Q0”,…}
2: {band: 0, category: “simple”, key: “Q3”,…}
3: {band: 0, category: “simple”, key: “Q5”,…}
4: {band: 1, category: “simple”, key: “Q1”,…}
5: {band: 1, category: “simple”, key: “Q4”,…}
. 6: {band: 1, category: “simple”, key: “Q6”,…}
. 7: {band: 2, category: “simple”, key: “Q2”,…}

and bands:

0: {category: “VerticalBands”,…}

category: “VerticalBands”
itemArray: [{text: “((0 inf) dec)”}, {text: “(0 std)”}, {text: “((minf 0) inc)”}]

. 0: {text: "((0 inf) dec)"}
. 1: {text: "(0 std)"}
. 2: {text: "((minf 0) inc)"}

. key: “_BANDS”

Another question: This is support by vertical and not horizontal display?

Thank you so much!

Have a nice day :)

Amit.

BandedTreeLayout is coded to only work with TreeLayout.angle being zero.

If I have time tomorrow I may work on this. In the meantime, all of the code is right there for you to adapt for your own purposes.

Thanks!

and please see my another comment of Q3 band - he doesn’t get his band (0) - he displayed in band (1)

how could I fix it? :(

Thanks!

Amit.

<!DOCTYPE html>
<html>
<head>
  <title>Swim Bands using a Background Part</title>
  <!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
  <meta name="description" content="implementing background bands using a single background Part">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="../release/go.js"></script>
  <script id="code">
  function init() {
    var $ = go.GraphObject.make;
    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          layout: $(BandedTreeLayout),  // custom layout is defined below
          "undoManager.isEnabled": true
        });

    myDiagram.nodeTemplate =
      $(go.Node, go.Panel.Auto,
        $(go.Shape, "Rectangle",
          { fill: "white" }),
        $(go.TextBlock, { margin: 5 },
          new go.Binding("text", "key")));

    // there should be a single object of this category;
    // it will be modified by BandedTreeLayout to display visual "bands"
    myDiagram.nodeTemplateMap.add("VerticalBands",
      $(go.Part, "Position",
        {
          isLayoutPositioned: false,  // but still in document bounds
          locationSpot: new go.Spot(0, 0, 0, 16),  // account for header height
          layerName: "Background",
          pickable: false,
          selectable: false,
          itemTemplate:
            $(go.Panel, "Vertical",
              new go.Binding("opacity", "visible", function(v) { return v ? 1 : 0; }),
              new go.Binding("position", "bounds", function(b) { return b.position; }),
              $(go.TextBlock,
                {
                  stretch: go.GraphObject.Horizontal,
                  textAlign: "center",
                  wrap: go.TextBlock.None,
                  font: "bold 11pt sans-serif",
                  background: $(go.Brush, go.Brush.Linear, { 0: "lightgray", 1: "whitesmoke" })
                },
                new go.Binding("text"),
                new go.Binding("width", "bounds", function(r) { return r.width; })),
              // for separator lines:
              //$(go.Shape, "LineV",
              //  { stroke: "gray", alignment: go.Spot.Left, width: 1 },
              //  new go.Binding("height", "bounds", function(r) { return r.height; }),
              //  new go.Binding("visible", "itemIndex", function(i) { return i > 0; }).ofObject()),
              // for rectangular bands:
              $(go.Shape,
                { stroke: null, strokeWidth: 0 },
                new go.Binding("desiredSize", "bounds", function(r) { return r.size; }),
                new go.Binding("fill", "itemIndex", function(i) { return i % 2 == 0 ? "white" : "whitesmoke"; }).ofObject())
            )
        },
        new go.Binding("itemArray")
      ));

    myDiagram.linkTemplate =
      $(go.Link,
        { routing: go.Link.AvoidsNodes },
        $(go.Shape));

    // define the tree node data
    var nodearray = [
      {
        key: "_BANDS",
        category: "VerticalBands",
        itemArray: [
          { visible: false },
          { text: "One" },
          { text: "Two" },
          { text: "Three" }
        ]
      },
      { band: 1, category: "simple", key: "Q0" },
      { band: 1, category: "simple", key: "Q3", parent: "Q0" },
      { band: 1, category: "simple", key: "Q5", parent: "Q3" },
      { band: 2, category: "simple", key: "Q1" },
      { band: 2, category: "simple", key: "Q4", parent: "Q3" },
      { band: 2, category: "simple", key: "Q6", parent: "Q3" },
      { band: 3, category: "simple", key: "Q2" }
    ];

    myDiagram.model = new go.TreeModel(nodearray);
  }


  // Perform a TreeLayout where the node's actual tree-layer is specified by the "band" property on the node data.
  // This implementation only works when angle == 0, but could be easily modified to support other angles.
  function BandedTreeLayout() {
    go.TreeLayout.call(this);

    this.treeStyle = go.TreeLayout.StyleLayered;  // required
    // new in GoJS v1.4
    this.layerStyle = go.TreeLayout.LayerUniform;

    // don't move subtrees closer together, to maintain possible empty spaces between layers
    this.compaction = go.TreeLayout.CompactionNone;
    // move the parent node towards the top of its subtree area
    this.alignment = go.TreeLayout.AlignmentStart;

    // sort a parent's child vertexes by the value of the index property
    function compareIndexes(v, w) {
      var vidx = v.index;
      if (vidx === undefined) vidx = 0;
      var widx = w.index;
      if (widx === undefined) widx = 0;
      return vidx-widx;
    }
    this.sorting = go.TreeLayout.SortingAscending;
    this.comparer = compareIndexes;

    //this.setsPortSpot = false;
    this.setsChildPortSpot = false;
  }
  go.Diagram.inherit(BandedTreeLayout, go.TreeLayout);

  // Modify the standard LayoutNetwork by making children with the same "band" value as their
  // parents actually be children of the grandparent.
  BandedTreeLayout.prototype.makeNetwork = function(coll) {
    var net = go.TreeLayout.prototype.makeNetwork.call(this, coll);
    // add artificial root and link with all root vertexes
    var singles = [];
    for (var it = net.vertexes.iterator; it.next();) {
      var v = it.value;
      if (v.node && v.sourceEdges.count === 0) {
        singles.push(v);
      }
    }
    if (singles.length > 0) {
      var dummyroot = net.createVertex();
      net.addVertex(dummyroot);
      singles.forEach(function(v) {
        net.linkVertexes(dummyroot, v, null);
      });
    }
    // annotate every child with an index, used for sorting
    for (var it = net.vertexes.iterator; it.next();) {
      var parent = it.value;
      var idx = 0;
      for (var cit = parent.destinationVertexes; cit.next();) {
        var child = cit.value;
        child.index = idx;
        idx += 10000;
      }
    }
    // now look for children with the same band value as their parent
    for (var it = net.vertexes.iterator; it.next();) {
      var parent = it.value;
      if (!parent.node) continue;
      // Should this be recursively looking for grandchildren/greatgrandchildren that
      // have the same band as this parent node??  Assume that is NOT required.
      var parentband = parent.node.data.band;
      var edges = [];
      for (var eit = parent.destinationEdges; eit.next();) {
        var edge = eit.value;
        var child = edge.toVertex;
        if (!child.node) continue;
        var childband = child.node.data.band;
        if (childband <= parentband) edges.push(edge);
      }
      // for each LayoutEdge that connects the parent vertex with a child vertex
      // whose node has the same band #, reconnect the edge with the parent's parent vertex
      var grandparent = parent.sourceVertexes.first();
      if (grandparent !== null) {
        var cidx = 1;
        for (var i = 0; i < edges.length; i++) {
          var e = edges[i];
          parent.deleteDestinationEdge(e);
          e.fromVertex = grandparent;
          grandparent.addDestinationEdge(e);
          var child = e.toVertex;
          child.index = parent.index + cidx;
          cidx++;
        }
      }
    }
    return net;
  };

  BandedTreeLayout.prototype.assignTreeVertexValues = function(v) {
    if (v.node && v.node.data && v.node.data.band) {
      v.originalLevel = v.level;  // remember tree assignment
      v.level = Math.max(v.level, v.node.data.band);  // shift down to meet band requirement
    }
  };

  BandedTreeLayout.prototype.commitLayers = function(layerRects, offset) {
    // for debugging:
    //for (var i = 0; i < layerRects.length; i++) {
    //  if (window.console) window.console.log(layerRects[i].toString());
    //}

    for (var it = this.network.vertexes.iterator; it.next(); ) {
      var v = it.value;
      var n = v.node;
      if (n && v.originalLevel) {
        // the band specifies the horizontal position
        var diff = n.data.band - v.originalLevel;
        if (diff > 0) {
          var pos = v.bounds.position;
          // this assumes that the angle is zero: rightward growth
          pos.x = layerRects[v.level].x;
          n.move(pos);
        }
      }
    }

    // update the background object holding the visual "bands"
    var bands = this.diagram.findPartForKey("_BANDS");
    if (bands) {
      bands.layerRects = layerRects;  // remember the Array of Rect

      var model = this.diagram.model;
      for (var it = this.network.vertexes.iterator; it.next(); ) {
        var v = it.value;
        if (!v.node) continue;
        model.setDataProperty(v.node.data, "band", v.level);
      }

      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) {
          model.setDataProperty(itemdata, "bounds", layerRects[i]);
        }
      }
    }
  };


  function addLayer() {
    if (myDiagram.findPartForKey("fiveA") !== null) return;
    myDiagram.startTransaction("addLayer");
    var bands = myDiagram.model.findNodeDataForKey("_BANDS");
    if (bands) myDiagram.model.addArrayItem(bands.itemArray, { text: "Five" });
    myDiagram.model.addNodeData({ key: "fiveA", parent: "fourB", band: 5 });
    myDiagram.commitTransaction("addLayer");
  }
  </script>
</head>
<body onload="init()">
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px blue; width:100%; height:600px;"></div>
  <button onclick="addLayer()">Add Layer 5</button>
</div>
</body>
</html>

Hi Walter! Thank you so much for your help!

two questions:

  1. when I did a invisible band as you did I got this graph:

image

as you can see there is an empty band at the left side - how can I remove him from the graph?

  1. I try to do the vertical layout:

sometimes it’s work:

image

and sometimes nodes cover each other:

(there are more instead Q2):

image

here some code of my trying to do the vertical feature:

 const dia = $(go.Diagram, {
            // @ts-ignore
            layout: $(BandedTreeLayout,  // custom layout is defined above
                {
                    angle: HORIZONTAL ? 0 : 90,
                    arrangement: HORIZONTAL ? go.TreeLayout.ArrangementVertical : go.TreeLayout.ArrangementHorizontal
                }),
            'initialContentAlignment': go.Spot.Center,
            'undoManager.isEnabled': true,
            model: $(go.GraphLinksModel,
                {
                    linkToPortIdProperty: 'toPort',
                    linkFromPortIdProperty: 'fromPort',
                    linkKeyProperty: 'key' // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel
                }
            ),

        });

and this is my old layout that support vertical:

dia.nodeTemplateMap.add("Bands",
            $(go.Part, "Position",
                new go.Binding("itemArray"),
                {
                    isLayoutPositioned: false,  // but still in document bounds
                    locationSpot: new go.Spot(0, 0, HORIZONTAL ? 0 : 16, HORIZONTAL ? 16 : 0),  // account for header height
                    layerName: "Background",
                    pickable: false,
                    selectable: false,
                    itemTemplate:
                        $(go.Panel, HORIZONTAL ? "Vertical" : "Horizontal",
                            new go.Binding("position", "bounds", function (b) {
                                return b.position;
                            }),
                            $(go.TextBlock,
                                {
                                    angle: HORIZONTAL ? 0 : 270,
                                    textAlign: "center",
                                    wrap: go.TextBlock.None,
                                    font: "bold 11pt sans-serif",
                                    background: $(go.Brush, "Linear", {0: "aqua", 1: go.Brush.darken("aqua")})
                                },
                                new go.Binding("text"),
                                // always bind "width" because the angle does the rotation
                                new go.Binding("width", "bounds", function (r) {
                                    return HORIZONTAL ? r.width : r.height;
                                })
                            ),
                            // option 1: rectangular bands:
                            $(go.Shape,
                                {stroke: null, strokeWidth: 0},
                                new go.Binding("desiredSize", "bounds", function (r) {
                                    return r.size;
                                }),
                                new go.Binding("fill", "itemIndex", function (i) {
                                    return i % 2 == 0 ? "whitesmoke" : go.Brush.darken("whitesmoke");
                                }).ofObject())
                            // option 2: separator lines:
                            //(HORIZONTAL
                            //  ? $(go.Shape, "LineV",
                            //      { stroke: "gray", alignment: go.Spot.TopLeft, width: 1 },
                            //      new go.Binding("height", "bounds", function(r) { return r.height; }),
                            //      new go.Binding("visible", "itemIndex", function(i) { return i > 0; }).ofObject())
                            //  : $(go.Shape, "LineH",
                            //      { stroke: "gray", alignment: go.Spot.TopLeft, height: 1 },
                            //      new go.Binding("width", "bounds", function(r) { return r.width; }),
                            //      new go.Binding("visible", "itemIndex", function(i) { return i > 0; }).ofObject())
                            //)
                        )
                }
            ));

Thank you so much!

Have a nice day,

BR,

Amit.

I made band 0 not-visible.

You’ll need to swap x with y, and width with height, in the layout.

Yeah but still I can see that band:

image

There is any change to remove that line?

and about the vertical: I did it but some nodes located one each other - do you know a way to rearrange them correctly?

Thanks!

  1. My code shows how to do that.

  2. You’ll need to debug that.

Thanks man!!!

I figure it out (for who that see the post :) )

for vertical mode:

let HORIZONTAL = true;
BandedTreeLayout.prototype.commitLayers = function(layerRects, offset) {
            // for debugging:
            //for (var i = 0; i < layerRects.length; i++) {
            //  if (window.console) window.console.log(layerRects[i].toString());
            //}

            for (var it = this.network.vertexes.iterator; it.next(); ) {
                var v = it.value;
                var n = v.node;
                if (n && v.originalLevel) {
                    // the band specifies the horizontal position
                    var diff = n.data.band - v.originalLevel;
                    if (diff > 0) {
                        var pos = v.bounds.position;
                        // this assumes that the angle is zero: rightward growth
                        HORIZONTAL ? pos.x = layerRects[v.level].x : pos.y = layerRects[v.level].y;
                        n.move(pos);
                    }
                }
            }

            // update the background object holding the visual "bands"
            var bands = this.diagram.findPartForKey("_BANDS");
            if (bands) {
                bands.layerRects = layerRects;  // remember the Array of Rect

                var model = this.diagram.model;
                for (var it = this.network.vertexes.iterator; it.next(); ) {
                    var v = it.value;
                    if (!v.node) continue;
                    model.setDataProperty(v.node.data, "band", v.level);
                }

                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) {
                        model.setDataProperty(itemdata, "bounds", layerRects[i]);
                    }
                }
            }
        };