Banded LayeredDigraphLayout

Is it possible to modify this layout here to work with a LayeredDigraphLayout?

Well, in this forum there’s:

The basic idea is to override LayeredDigraphLayout.commitLayers to arrange the layers the way that you want.
LayeredDigraphLayout | GoJS API

Here’s the basic code:

<!DOCTYPE html>
<html>
<head>
  <title>Layer Bands using a Background Part</title>
  <!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
  <meta name="description" content="Showing bands for the layers in a diagram.">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="go.js"></script>
  <script id="code">

  // this controls whether the layout is horizontal and the layer bands are vertical, or vice-versa:
  var HORIZONTAL = true;  // this constant parameter can only be set here, not dynamically

  // Perform a LayeredDigraphLayout where commitLayers is overridden to modify the background Part whose key is "_BANDS".
  function BandedLDLayout() {
    go.LayeredDigraphLayout.call(this);
  }
  go.Diagram.inherit(BandedLDLayout, go.LayeredDigraphLayout);

  BandedLDLayout.prototype.assignLayers = function() {
    go.LayeredDigraphLayout.prototype.assignLayers.call(this);
    var maxlayer = this.maxLayer;
    // now assign specific layers
    var it = this.network.vertexes.iterator;
    while (it.next()) {
      var v = it.value;
      if (v.node !== null) {
        var lay = v.node.data.layer;
        if (typeof lay === "number" && lay >= 0 && lay <= maxlayer) {
          v.layer = lay;
        }
      }
    }
  };

  BandedLDLayout.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);

      // 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;
      }

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


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

    myDiagram = $(go.Diagram, "myDiagramDiv",
                  {
                    layout: $(BandedLDLayout,
                              {
                                direction: HORIZONTAL ? 0 : 90
                              }),  // custom layout is defined above
                    "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 at most a single object of this category.
    // This Part will be modified by BandedLDLayout.commitLayers to display visual "bands"
    // where each "layer" is a layer of the tree.
    // This template is parameterized at load time by the HORIZONTAL parameter.
    // You also have the option of showing rectangles for the layer bands or
    // of showing separator lines between the layers, but not both at the same time,
    // by commenting in/out the indicated code.
    myDiagram.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())
              //)
            )
        }
      ));

    myDiagram.linkTemplate =
      $(go.Link,
        $(go.Shape));  // simple black line, no arrowhead needed

    // 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: "root" },
      { key: "oneB", parent: "root" },
      { key: "twoA", parent: "oneB", layer: 0 },
      { key: "twoC", parent: "root" },
      { key: "threeC", parent: "twoC" },
      { key: "threeD", parent: "twoC" },
      { key: "fourB", parent: "threeD", layer: 2 },
      { key: "fourC", parent: "twoC", layer: 0 },
      { key: "fourD", parent: "fourB" },
      { key: "twoD", parent: "root", layer: 1 }
    ];

    myDiagram.model = new go.TreeModel(nodearray);
  }
  </script>
</head>
<body onload="init()">
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px;"></div>
</div>
</body>
</html>

Thanks for the links. I’m looking here at the example you send and not 100% sure that I have it (New to creating a custom layout).

I didn’t make any changes to the code yet, just using the example you posted here.

When I specify the layer on my data the item never shows up in the correct layer. Example here, Node3 should be on layer three and root3 should be on layer One

Also is there a way to always display all the band even if they don’t have any nodes on it?

Here is how I have my data setup.

      nodeDataArray = [
        {
          key: '_BANDS',
          category: 'VerticalBands',
          itemArray: [
            { text: "One" },
            { text: "Two" },
            { text: "Three" }
          ]
        },
        { key: "root", layer: 0 },
        { key: "root2", layer: 0 },
        { key: "root3", layer: 0 },
        { key: "node", layer: 1 },
        { key: "node2", layer: 1 },
        { key: "node3", layer: 3 }
      ];

      linkDataArray = [
        {from: 'root', to: 'node'},
        {from: 'root', to: 'node2'},
        {from: 'root2', to: 'node'},
      ];

and when I look at the diagram I see this.

image

my end goal is to make a diagram like this. Not worried about the styles now, just trying to get the rough diagram working and later I will work on the styles.

@walter I think I was able to achieve what I’m looking for using the previous example you had here Setting nodes to specific bands - #14

Here is how my data is structure.

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

      linkDataArray = [
        {from: 'Q0', to: 'Q2'},
        {from: 'Q0', to: 'Q1'},
        {from: 'Q1', to: 'Q3'},
        {from: 'Q5', to: 'Q4'},
      ];

      // this.myDiagram.model = new go.TreeModel(nodearray);
      this.myDiagram.model =
        $(go.GraphLinksModel,
          {
            nodeDataArray,
            linkDataArray
          }
        );

I just need to work on the linkTemplate and style the diagram.

image

How about this?

The complete source code:

<!DOCTYPE html>
<html>
<head>
  <title>Layer Bands using a Background Part</title>
  <!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
  <meta name="description" content="Showing bands for the layers in a diagram.">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">

  // this controls whether the layout is horizontal and the layer bands are vertical, or vice-versa:
  var HORIZONTAL = false;  // this constant parameter can only be set here, not dynamically

  // Perform a Layout where commitLayers is overridden to modify the background Part whose key is "_BANDS".
  function BandedLDLayout() {
    go.LayeredDigraphLayout.call(this);
  }
  go.Diagram.inherit(BandedLDLayout, go.LayeredDigraphLayout);

  BandedLDLayout.prototype.assignLayers = function() {
    go.LayeredDigraphLayout.prototype.assignLayers.call(this);
    var maxlayer = this.maxLayer;
    // now assign specific layers
    var it = this.network.vertexes.iterator;
    while (it.next()) {
      var v = it.value;
      if (v.node !== null) {
        var lay = v.node.data.layer;
        if (typeof lay === "number" && lay >= 0 && lay <= maxlayer) {
          v.layer = lay;
        }
      }
    }
  };

  BandedLDLayout.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);

      // 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;
      }

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


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

    myDiagram = $(go.Diagram, "myDiagramDiv",
                  {
                    layout: $(BandedLDLayout,  // custom layout is defined above
                              {
                                direction: HORIZONTAL ? 0 : 90,
                                columnSpacing: 5,
                                layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource,
                                setsPortSpots: false
                              }),
                    "undoManager.isEnabled": true
                  });

    // There should be at most a single object of this category.
    // This Part will be modified by BandedLDLayout.commitLayers to display visual "bands"
    // where each "layer" is a layer of the tree.
    // This template is parameterized at load time by the HORIZONTAL parameter.
    // You also have the option of showing rectangles for the layer bands or
    // of showing separator lines between the layers, but not both at the same time,
    // by commenting in/out the indicated code.
    myDiagram.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 : 80, HORIZONTAL ? 80 : 0),  // account for header
          layerName: "Background",
          pickable: false,
          selectable: false,
          itemTemplate:
            $(go.Panel, HORIZONTAL ? "Vertical" : "Horizontal",
              new go.Binding("position", "bounds", function(b) { return b.position; }),
              // the header (on top for HORIZONTAL, on the left side for !HORIZONTAL)
              $(go.Panel, HORIZONTAL ? "Vertical" : "Horizontal",
                { width: 80 },
                new go.Binding("opacity", "text", t => t ? 1 : 0),
                new go.Binding("pickable", "text", t => !!t),
                $("Button",
                  { "ButtonBorder.fill": "gold" },
                  $(go.Shape, "MinusLine", { width: 8, height: 8 }),
                  {
                    click: (e, button) => { }
                  }),
                $(go.TextBlock,
                  {
                    //wrap: go.TextBlock.None,
                    font: "11pt sans-serif",
                    margin: new go.Margin(0, 0, 0, 4)
                  },
                  new go.Binding("text"))
              ),
              // 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", strokeDashArray: [4, 4], 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", strokeDashArray: [4, 4], 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())
              )
            )
        }
      ));

    myDiagram.nodeTemplate =
      $(go.Node, go.Panel.Auto,
        { width: 60, height: 40 },
        $(go.Shape, "Rectangle",
          { fill: "cornflowerblue", strokeWidth: 0 }),
        $(go.TextBlock,
          new go.Binding("text", "key")));

    myDiagram.nodeTemplateMap.add("X",
      $(go.Node, "Spot",
        {
          linkConnected: (node, link, port) => {  // color the link paths
            if (node.category === "X") link.path.stroke = node.data.color;
          }
        },
        $(go.Panel, "Auto",
          { width: 80, height: 40 },
          $(go.Shape, "Ellipse",
            { fill: "white" },
            new go.Binding("fill", "color"))
        ),
        $(go.Panel, "Spot",
          { alignment: new go.Spot(0, 0, 6, 6) },
          $(go.Shape, "Circle",
            { width: 14, height: 14, stroke: "white", fill: "gray" }),
          $(go.TextBlock,
            { stroke: "white", font: "10px sans-serif", alignment: new go.Spot(0.5, 0.5, 1, 0.5) },
            new go.Binding("text", "tag"))
        )
      ))

    myDiagram.linkTemplate =
      $(go.Link,
        { layerName: "Background" },
        $(go.Shape, { strokeWidth: 1.5 }));


    var nodearray = [
      { // this is the information needed for the headers of the bands
        key: "_BANDS",
        category: "Bands",
        itemArray: [
          { text: "One" },
          { text: "" },
          { text: "Two" },
          { text: "" },
          { text: "Three" }
        ]
      },
      { key: "A", category: "X", tag: 3, color: "green", layer: 3 },
      { key: "B", category: "X", tag: 2, color: "red", layer: 3 },
      { key: "C", category: "X", tag: 2, color: "magenta", layer: 1 },
      { key: "D", category: "X", tag: 1, color: "cornflowerblue", layer: 1 },
      { key: 1, layer: 4 },
      { key: 2, layer: 2 },
      { key: 3, layer: 2 },
      { key: 4, layer: 2 },
      { key: 5, layer: 2 },
      { key: 6, layer: 2 },
      { key: 7, layer: 2 },
      { key: 8, layer: 0 },
      { key: 9, layer: 0 },
      { key: 10, layer: 0 },
    ];

    var linkarray = [
      { from: 1, to: "A" },
      { from: 1, to: "B" },
      { from: "A", to: 2 },
      { from: "A", to: 3 },
      { from: "A", to: 4 },
      { from: "A", to: 5 },
      { from: "A", to: 6 },
      { from: "A", to: 7 },
      { from: "B", to: 4 },
      { from: "B", to: 5 },
      { from: 4, to: "D" },
      { from: 5, to: "C" },
      { from: 7, to: "D" },
      { from: "C", to: 8 },
      { from: "C", to: 9 },
      { from: "D", to: 10 },
    ];

    myDiagram.model =
      $(go.GraphLinksModel,
        {
          nodeDataArray: nodearray,
          linkDataArray: linkarray
        });
  }
  </script>
</head>
<body onload="init()">
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px;"></div>
  <p>
    Unlike swim lane diagrams where the nodes are supposed to stay in their lanes,
    layer bands run perpendicular to the growth direction of the layout.
  </p>
  <p>
    This sample uses a custom <a>LayeredDigraphLayout</a> that overrides the <a>LayeredDigraphLayout.commitLayers</a> method
    in order to specify the position and size of each "band" that surrounds a layer of the graph.
    The "bands" are held in a single Part that is bound to a particular node data object whose key is "_BANDS".
    The headers, and potentially any other information that you might want to display in the headers,
    are stored in this "_BANDS" object in an Array.
  </p>
  <p>
    This sample can be adapted to use a <a>TreeModel</a> instead of a <a>GraphLinksModel</a>
    and a <a>TreeLayout</a> instead of a <a>LayeredDigraphLayout</a>:
    <a href="../samples/swimBands.html">Swim Bands</a>.
  </p>
</div>
</body>
</html>

Thank you for the help. this works the way I needed too.

@walter one quick question. Is there a way to display a Band if there is no item on it?

You can probably adapt the commitLayers override to do what you want, but I haven’t tried it.

I will play with it here see if I can make it work. When I look at the layerRects inside of the commitLayer why I don’t see some links on it.

example if I use the following data on the code you posted above

nodeDataArray = [
          { // this is the information needed for the headers of the bands
            key: "_BANDS",
            category: "Bands",
            itemArray: [
              { text: "One" },
              { text: "" },
              { text: "Two" },
              { text: "" },
              { text: "Three" }
            ]
          },
          { key: "Parent", category: "X", tag: 3, color: "green", layer: 2 },
          { key: 1, layer: 1 },
          { key: 2, layer: 0 }
        ];

        linkDataArray = [
          { from: 'Parent', to: 1 },
          { from: 'Parent', to: 2 }
        ];

My diagram looks like this

image

and when I look at the layerRects I only see this 2 entries and not 3. But if I change the **linkDataArray ** to look like this.

        linkDataArray = [
          { from: 'Parent', to: 1 },
          { from: 1, to: 2 }
        ];

now I see this.

image

Is it possible on the first example to place the node 2 on band two, but I need the link to come from the parent and not from the node 1

@walter so it’s the linkArray that defines the data in the layerRects in the commitLayers?

Yes, it does by default. But you could force there to be empty layers by putting in dummy vertexes and edges. You can fiddle with the 25 for the height of the dummy node to control the minimum height of layers, which normally would be zero if there were no nodes in them.

  // Perform a Layout where commitLayers is overridden to modify the background Part whose key is "_BANDS".
  function BandedLDLayout() {
    go.LayeredDigraphLayout.call(this);
  }
  go.Diagram.inherit(BandedLDLayout, go.LayeredDigraphLayout);

  BandedLDLayout.prototype.makeNetwork = function(coll) {
    var net = go.LayeredDigraphLayout.prototype.makeNetwork.call(this, coll);
    var max = 0;
    net.vertexes.each(function(v) {
      if (v.node && typeof v.node.data.layer === "number") {
        max = Math.max(max, v.node.data.layer);
      }
    });
    if (max > 0) {
      var v = net.createVertex();  // a dummy vertex
      v.height = 25;  // with a minimum height, in case there are no nodes in this layer
      net.addVertex(v);
      while (max > 0) {
        var next = net.createVertex();
        next.height = v.height;
        net.addVertex(next);
        net.linkVertexes(v, next, null);  // dummy edge too
        v = next;
        max--;
      }
    }
    return net; 
  }

  BandedLDLayout.prototype.nodeMinLayerSpace = function(v, topleft) {
    if (!v.node && v.height > 0) return v.height/2;
    return go.LayeredDigraphLayout.prototype.nodeMinLayerSpace.call(this, v, topleft);
  }

  BandedLDLayout.prototype.assignLayers = function() {
    go.LayeredDigraphLayout.prototype.assignLayers.call(this);
    var maxlayer = this.maxLayer;
    // now assign specific layers
    var it = this.network.vertexes.iterator;
    while (it.next()) {
      var v = it.value;
      if (v.node !== null) {
        var lay = v.node.data.layer;
        if (typeof lay === "number" && lay >= 0 && lay <= maxlayer) {
          v.layer = lay;
        }
      }
    }
  };

  BandedLDLayout.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);

      // 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;
      }

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