Diagram layout horizontal

Hi,

I would like to split my Diagram into multiple pieces to establish a layout for my application.
The data flow is from left to right and I got 5 different diagram “container”. The container are fully stretched in height and got a background color and they are aligned horizontally to each other. Each container should have its own layout algorithm. Each container can be zoomed independently, like hovering the mouse over container1 and wheeling does zoom in or out in container1 but does not have effect on any other container (2-4). Likewise panning.
Nodes within each container can be connected. So nodes from container1 should be connectable to nodes from container2, nodes from container2 to nodes from container3 and so forth.

The containers themselves cannot be resized manually by the user (but should be resizable like when the window’s size changes).

Is this possible to achieve? How would you do that? Using groups? Using subgraphs? Using panels?
Is it possible to apply logic (layout, zooming, panning) only to specific areas (groups, subgraphs, panels)?

Thanks in advance.

If you allow scrolling and/or zooming in one container that should not affect the other containers, and if you have links from nodes in one container to nodes in another container, what should happen when the user scrolls nodes far enough not to fit within the container’s area?

It would help if you had a sketch or screenshot of what you wanted in this case.

Hi,

thanks for you quick response. I created a small diagram, maybe it helps you understanding what I’m trying to build.

Nodes that are not in the viewport of a container (or another container is laid over) should be set to unvisible. Whenever they come back to the viewport, they should be made visible again.

Hmmm, let us think about this later today.

Got any hints on that?

Here’s what I’ve got so far:

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scrolling Groups</title>
<!-- Copyright 1998-2018 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<script src="../assets/js/goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
<script src="../extensions/ScrollingTable.js"></script>  <!-- to define AutoRepeatButton -->

<script id="code">
  function ScrollingGroupLayout() {
    go.Layout.call(this);
    this._topIndex = 0;
    this._spacing = 10;
    this._count = 0;
    this._lastIndex = 0;
    this._padding = 30;
  }
  go.Diagram.inherit(ScrollingGroupLayout, go.Layout);

  ScrollingGroupLayout.prototype.cloneProtected = function(copy) {
    go.Layout.prototype.cloneProtected.call(this, copy);
    copy._topIndex = this._topIndex;
    copy._spacing = this._spacing;
  };

  Object.defineProperty(ScrollingGroupLayout.prototype, "topIndex", {
    get: function() { return this._topIndex; },
    set: function(val) {
      if (typeof val !== "number") throw new Error("new value for ScrollingGroupLayout.topIndex is not a number: " + val);
      if (this._topIndex !== val) {
        this._topIndex = val;
        this.invalidateLayout();
      }
    }
  });

  Object.defineProperty(ScrollingGroupLayout.prototype, "spacing", {
    get: function() { return this._spacing; },
    set: function(val) {
      if (typeof val !== "number") throw new Error("new value for ScrollingGroupLayout.spacing is not a number: " + val);
      if (this._spacing !== val) {
        this._spacing = val;
        this.invalidateLayout();
      }
    }
  });

  Object.defineProperty(ScrollingGroupLayout.prototype, "count", {
    get: function() { return this._count; }
  });

  Object.defineProperty(ScrollingGroupLayout.prototype, "lastIndex", {
    get: function() { return this._lastIndex; }
  });

  ScrollingGroupLayout.prototype.doLayout = function(coll) {
    var diagram = this.diagram;
    var group = this.group;
    if (group === null) throw new Error("ScrollingGroupLayout must be a Group.layout, not a Diagram.layout");

    if (diagram !== null) diagram.startTransaction("Scrolling Group Layout");
    this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
    var arr = [];
    // can't use Layout.collectParts here, because we're intentionally making some
    // member nodes not visible, which would normally prevent them from being laid out
    var it = group.memberParts.iterator;
    while (it.next()) {
      var part = it.value;
      if (part instanceof go.Link) continue;
      part.ensureBounds();
      arr.push(part);
    }
    this._count = arr.length;
    // sort by their initial Y position, allowing users to drag nodes for re-ordering them
    var comparer = function(a, b) {
      return a.actualBounds.y - b.actualBounds.y;
    };
    arr.sort(comparer);
    var body = group.findObject("BODY");
    if (!body) body = group.placeholder;
    if (!body) throw new Error("no BODY element or Placeholder in Group");
    var x = this.arrangementOrigin.x + body.actualBounds.width/2;
    var y = this.arrangementOrigin.y + this._padding;
    var maxy = y + body.height - this._padding;
    var i = 0;
    var last = -1;
    while (i < arr.length && i < this.topIndex) {
      var part = arr[i++];
      part.visible = false;
      part.location = new go.Point(x, y);
    }
    while (i < arr.length && y < maxy) {
      var part = arr[i++];
      part.location = new go.Point(x, y + part.actualBounds.height/2);
      if (y + part.actualBounds.height < maxy) {
        part.visible = true;
        y += part.actualBounds.height + this.spacing;
        last = i - 1;
      } else {
        part.visible = false;
        break;
      }
    }
    while (i < arr.length) {
      var part = arr[i++];
      part.visible = false;
      part.location = new go.Point(x, y);
    }
    this._lastIndex = last;
    var up = group.findObject("UP");
    if (up !== null) up.visible = this.lastIndex < this.count - 1;
    var down = group.findObject("DOWN");
    if (down !== null) down.visible = this.topIndex > 0;
    if (diagram !== null) diagram.commitTransaction("Scrolling Group Layout");
  };
  // end of ScrollingGroupLayout


  function init() {
    if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",  // create a Diagram for the DIV HTML element
        {
          layout: $(go.GridLayout, { wrappingWidth: Infinity, spacing: new go.Size(0, 0), cellSize: new go.Size(1, 1) }),
          "SelectionMoved": function(e) {
            e.subject.each(function(p) {
              if (p.containingGroup !== null) p.containingGroup.layout.invalidateLayout();
            });
          },
          "PartResized": function(e) {
            var part = e.subject.part;
            if (part instanceof go.Group && part.layout instanceof ScrollingGroupLayout) {
              part.layout.invalidateLayout();
            }
          },
          "toolManager.doMouseWheel": function() {
            var grp = null;
            this.diagram.findTopLevelGroups().each(function(g) {
              if (g.actualBounds.containsPoint(g.diagram.lastInput.documentPoint)) grp = g;
            });
            if (grp !== null) {
              var lay = grp.layout;
              if (lay instanceof ScrollingGroupLayout) {
                if (this.diagram.lastInput.delta > 0) {
                  if (lay.lastIndex < lay.count - 1) {
                    lay.topIndex++;
                  }
                } else {
                  if (lay.topIndex > 0) {
                    lay.topIndex--;
                  }
                }
              }
            } else {
              this.standardMouseWheel();
            }
            this.diagram.lastInput.bubbles = false;
          }
        });

    myDiagram.nodeTemplate =
      $(go.Node, go.Panel.Auto,
        {
          locationSpot: go.Spot.Center,
          minLocation: new go.Point(NaN, -Infinity),
          maxLocation: new go.Point(NaN, Infinity),
          containingGroupChanged: function(n) {
            if (n.containingGroup.key === "GROUP 3") {
              n.minLocation = new go.Point(-Infinity, -Infinity);
              n.maxLocation = new go.Point(Infinity, Infinity);
              n.layerName = "Background";
            } else {
              n.minLocation = new go.Point(NaN, -Infinity);
              n.maxLocation = new go.Point(NaN, Infinity);
              n.layerName = "";
            }
          }
        },
        new go.Binding("location", "loc", go.Point.parse),
        $(go.Shape, { fill: "white" },
          new go.Binding("fill", "color")),
        $(go.TextBlock, { margin: 6 },
          new go.Binding("text", "key")));

    myDiagram.groupTemplate =
      $(go.Group, "Vertical",
        {
          selectable: false,
          resizeObjectName: "BODY",  // also used by ScrollingGroupLayout
          computesBoundsIncludingLocation: true,
          layout: $(ScrollingGroupLayout)
        },
        $(go.Panel, "Auto",
          { name: "BODY", height: 310, width: 120 },
          $(go.Shape, { fill: "lightyellow", stroke: "yellow" }),
          $("AutoRepeatButton",
            { name: "UP", alignment: go.Spot.TopRight },
            $(go.Shape, "TriangleUp", { width: 6, height: 6 }),
            {
              click: function(e, button) {
                var group = button.part;
                var lay = group.layout;
                if (lay.lastIndex < lay.count - 1) {
                  lay.topIndex++;
                }
              }
            }),
          $("AutoRepeatButton",
            { name: "DOWN", alignment: go.Spot.BottomRight },
            $(go.Shape, "TriangleDown", { width: 6, height: 6 }),
            {
              click: function(e, button) {
                var group = button.part;
                var lay = group.layout;
                if (lay.topIndex > 0) {
                  lay.topIndex--;
                }
              }
            })
        ),
        $(go.TextBlock, new go.Binding("text", "key"))
      );

    myDiagram.groupTemplateMap.add("Main",
      $(go.Group, "Vertical",
        {
          selectable: false,
          computesBoundsIncludingLocation: true,
          layerName: "Background"
        },
        $(go.Shape, { name: "BODY", height: 310, width: 300, fill: "lightpink", strokeWidth: 0.5 }),
        $(go.TextBlock, new go.Binding("text", "key"))
      ));

    myDiagram.linkTemplate =
      $(go.Link,
        {
          layerName: "Foreground",
          routing: go.Link.Orthogonal, corner: 10,
          fromSpot: go.Spot.Right, toSpot: go.Spot.Left
        },
        $(go.Shape),
        $(go.Shape, { toArrow: "Standard"})
      );

    var model = new go.GraphLinksModel();
    model.nodeDataArray = [
      { key: "GROUP", isGroup: true },
      { key: "Alpha", color: "coral", group: "GROUP" },
      { key: "Beta", color: "tomato", group: "GROUP" },
      { key: "Gamma", color: "goldenrod", group: "GROUP" },
      { key: "Delta", color: "orange", group: "GROUP" },
      { key: "Epsilon", color: "coral", group: "GROUP" },
      { key: "Zeta", color: "tomato", group: "GROUP" },
      { key: "Eta", color: "goldenrod", group: "GROUP" },
      { key: "Theta", color: "orange", group: "GROUP" },
      { key: "Iota", color: "coral", group: "GROUP" },
      { key: "Kappa", color: "tomato", group: "GROUP" },
      { key: "Lambda", color: "goldenrod", group: "GROUP" },
      { key: "Mu", color: "orange", group: "GROUP" },
      { key: "Nu", color: "coral", group: "GROUP" },
      { key: "GROUP 2", isGroup: true },
      { key: "Xi", color: "tomato", group: "GROUP 2" },
      { key: "Omicron", color: "goldenrod", group: "GROUP 2" },
      { key: "Pi", color: "orange", group: "GROUP 2" },
      { key: "Rho", color: "coral", group: "GROUP 2" },
      { key: "Sigma", color: "tomato", group: "GROUP 2" },
      { key: "Tau", color: "goldenrod", group: "GROUP 2" },
      { key: "Upsilon", color: "orange", group: "GROUP 2" },
      { key: "Phi", color: "coral", group: "GROUP 2" },
      { key: "Chi", color: "tomato", group: "GROUP 2" },
      { key: "Psi", color: "goldenrod", group: "GROUP 2" },
      { key: "Omega", color: "orange", group: "GROUP 2" },
      { key: "GROUP 3", isGroup: true, category: "Main" },
      { key: "Main 1", color: "lightgreen", group: "GROUP 3", loc: "140 50" },
      { key: "Main 2", color: "lightgreen", group: "GROUP 3", loc: "70 150" },
      { key: "Main 3", color: "lightgreen", group: "GROUP 3", loc: "200 250" },
      { key: "Main 4", color: "lightgreen", group: "GROUP 3", loc: "160 100" },
      { key: "GROUP 4", isGroup: true },
      { key: "Alpha", color: "coral", group: "GROUP 4" },
      { key: "Beta", color: "tomato", group: "GROUP 4" },
      { key: "Gamma", color: "goldenrod", group: "GROUP 4" },
      { key: "Delta", color: "orange", group: "GROUP 4" },
      { key: "Epsilon", color: "coral", group: "GROUP 4" },
      { key: "Zeta", color: "tomato", group: "GROUP 4" },
      { key: "Eta", color: "goldenrod", group: "GROUP 4" },
      { key: "Theta", color: "orange", group: "GROUP 4" },
      { key: "Iota", color: "coral", group: "GROUP 4" },
      { key: "Kappa", color: "tomato", group: "GROUP 4" },
      { key: "Lambda", color: "goldenrod", group: "GROUP 4" },
      { key: "Mu", color: "orange", group: "GROUP 4" },
      { key: "Nu", color: "coral", group: "GROUP 4" },
      { key: "GROUP 5", isGroup: true },
      { key: "Xi", color: "tomato", group: "GROUP 5" },
      { key: "Omicron", color: "goldenrod", group: "GROUP 5" },
      { key: "Pi", color: "orange", group: "GROUP 5" },
      { key: "Rho", color: "coral", group: "GROUP 5" },
      { key: "Sigma", color: "tomato", group: "GROUP 5" },
      { key: "Tau", color: "goldenrod", group: "GROUP 5" },
      { key: "Upsilon", color: "orange", group: "GROUP 5" },
      { key: "Phi", color: "coral", group: "GROUP 5" },
      { key: "Chi", color: "tomato", group: "GROUP 5" },
      { key: "Psi", color: "goldenrod", group: "GROUP 5" },
      { key: "Omega", color: "orange", group: "GROUP 2" }
    ];
    model.linkDataArray = [
      { from: "Beta", to: "Pi" },
      { from: "Gamma", to: "Psi" },
      { from: "Psi", to: "Main 2" },
      { from: "Main 2", to: "Main 3" },
      { from: "Main 3", to: "Kappa2" },
      { from: "Kappa2", to: "Rho2" }
    ];
    myDiagram.model = model;
  }
</script>
</head>
<body onload="init()">
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:500px; min-width: 200px"></div>
</div>
</body>
</html>

The user can scroll each of the four "ScrollingGroup"s on the sides (i.e. not the middle group) either via the "AutoRepeatButton"s or by scrolling the mouse wheel while the mouse pointer is within the group.

The layout of those four groups is implemented by the custom ScrollingGroupLayout, which is like GridLayout in putting everything in a single column, but it acts like a view onto a list by keeping track of the first visible item and how many items fit into the “BODY” element of the Group. It hides the rest of the items. It also allows the user to change the order by drag-and-drop.

The whole Diagram will need a new custom replacement PanningTool that supports scrolling individually in those "ScrollingGroup"s.

There also needs to be a “ViewportBoundsChanged” DiagramEvent listener that adjusts the height of each of the groups so that it matches the height of the viewport. The details for what you want to do as the viewport changes size or aspect ratio weren’t clear to me, but that is something you can do, along with customizing the templates.

That’s cool. Thanks for your effort and pointing me to the right direction.
Maybe one quick question: How would you do it do pass an indiviual width to the groups?

It works for the height but not for the width. All container have the same width afterwards, but I want to assign it inidivually. To do so, I manipulate the node data as follows:

e.diagram.model.nodeDataArray.forEach(x => {
        if (x['isGroup']) {
          console.log('old width', x['width']);
          x['height'] = e.diagram.viewportBounds.height;
          x['width'] = e.diagram.viewportBounds.width * (x['widthFactor']);
          console.log('new width', x['width']);
        }
      });

The numbers look fine, the diagram doesn’t. I use data binding for the height and width like

  $(go.Shape,
            new go.Binding("height", "height"),
            new go.Binding("width", "width"),
            {
              fill: "lightyellow", stroke: "yellow"
   }),

To change model data, you have to call Model.set, both to notify the Model (and thus any Diagrams) that some property value is changing as well as to record the previous value in the UndoManager.

Don’t forget to perform all of the changes within a Transaction. You can call Model.commit or Diagram.commit, or you can call startTransaction before and commitTransaction after.

https://gojs.net/latest/intro/usingModels.html#ModifyingModels
https://gojs.net/latest/intro/transactions.html

Thank you, Walter. Now it seems to work :)

Thanks for your quick response.

Is it possible to set the width of all nodes within a specified container (all except the middle one) to match its parent (Panel) width? Setting stretch: go.GraphObject.Horizontal (Vertical, Fill) within the Shape object of the groupTemplate doesn’t seem to have an effect.

Here’s the start of a substitute PanningTool:

  function ScrollingPanningTool() {
    go.PanningTool.call(this);
  }
  go.Diagram.inherit(ScrollingPanningTool, go.PanningTool);

  ScrollingPanningTool.prototype.doMouseMove = function() {
    this.scroll();
  };

  ScrollingPanningTool.prototype.doMouseUp = function() {
    this.scroll();
    this.stopTool();
  };

  ScrollingPanningTool.prototype.scroll = function() {
    var pt = this.diagram.lastInput.documentPoint;
    var grp = null;
    this.diagram.findTopLevelGroups().each(function(g) {
      if (g.actualBounds.containsPoint(pt)) grp = g;
    });
    if (grp !== null) {
      var lay = grp.layout;
      if (lay instanceof ScrollingGroupLayout && lay.lastIndex > 0) {
        var unit = grp.actualBounds.height / lay.lastIndex;
        var i = Math.floor(Math.abs(grp.actualBounds.bottom - pt.y) / unit);
        lay.topIndex = i;
      }
    }
  }

Install when you initialize the Diagram:

$(go.Diagram, . . .,
    { . . .,
        panningTool: new ScrollingPanningTool()
    })

Regarding your last question, you could customize the ScrollingGroupLayout to set the widths of the member Nodes the way that you want. It depends on the node template that you use – I just used something simple because the actual node template doesn’t matter when trying to achieve your other not-so-obvious goals.

Thanks for your effort :)

Honestly, I dont know how to set the width of the nodes to fill the containers width and I dont know why there are always two nodes position next to each other.

Is it possible to make containers fixed to the viewport (ignoring scrolling, zooming and panning)?

I assume you know about Diagram | GoJS API

Have you seen Absolute positioning within the viewport ?

Could you please update your profile with the company name either that you work for or for whom you are contracting?

Sure I’ve seen that.

Currently I’m working on a feasibility study and try to choose the best tool that fits to the requirements. As I experienced that theoretical stuff (documentation) are not always 100% what you may think of, I’m trying to implement the requirements to be sure.

I should mention that if you expect the HTML DIV element to change size dynamically, or even only once when first created, then it might be wise to implement a custom Layout as the value of Diagram.layout. If you set Layout.isViewportSized to true, then whenever GoJS recognizes that the Diagram.viewportBounds has changed size, it will run the layout again. Thus your custom layout code will have the opportunity to change the sizes of the groups, and apparently also it changes member node sizes.

https://gojs.net/latest/intro/layouts.html
https://gojs.net/latest/intro/extensions.html#OverridingMethodsBySubclassingLayout

Thanks for the hint. But I’m still trying to get the layout working. I guess that I might have messed up something because the doLayout function is not called (at least the breakpoint in the dev tools isn’t hit). This explains why changes that I make doesn’t appear to have an effect :D

By the way, I’m using typescript. So I created a class ScrollingGroupLayout extends go.Layout and call go.Diagram.inherit(ScrollingGroupLayout, go.Layout) in the constructor.

Have you set Diagram.layout to be an instance of your custom layout?

If you are using “class” in your TypeScript, you shouldn’t be calling Diagram.inherit; the latter is needed only when writing JavaScript without using “class”. There are example custom Layouts defined in TypeScript in the extensionsTS subdirectory.

Impressive. I removed the go.Diagram.inherit and now the diagram looks very nice. What a single line can cause an issue :D …

I’m still a little confused why doLayout isn’t hit but I’m trying to figure it out.

The next release will have Diagram.inherit throw an Error if it is called on an already defined class as its first argument.