Custom Layout

Hi

I need to create a custom layout for Go JS. First, I’ll give you some background on what we’re trying to achieve.

We have a Silverlight application that makes use of GoXam. One of the diagram types that we support is traditional tree-based diagrams. See:

Our user’s find that when the tree becomes very large it’s difficult for them to navigate the tree or to understand where they are located within a tree.

So we came up with an alternative view which we called a “Stack View”. The stack view allows the user to drill down into a particular branch of the tree without being visually distracted by the rest of tree.

The stack view behaves as follows:

  1. Initially, only the root nodes of the tree are visible and are located in the first level of the stack. See:

  2. The user then selects one of the root nodes. This will load that the selected node’s children into the second level of the stack.

  3. The user then might select a node in the second level stack which will loads it’s children into the third level of the stack.

  4. If the user selects a different node, it will remove all stacks levels below and then load it’s child nodes it to the next stack level.

  5. At any time the user can visually see which nodes were selected to generate the current stack.

It was relatively easy to implement in Silverlight using Stack Panels and Data Templates.

I now need to implement the same behaviour in GoJS and I’m guessing that a custom Layout is the way to go. Would it be possible to provide me some guidance or code on how to implement this layout?

Thanks
Justin

This reminds me a lot of the Local View sample, http://gojs.net/latest/samples/localView.html. (The same sample also exists in GoXam.)

But you don’t have to implement your Stack View using a separate Model that holds a subset of the whole model. You could just iterate through all of the nodes and make not visible all of those that are not on the path from the selected node to the root node or are not immediate siblings of such nodes. The TreeLayout would need to change its TreeLayout.alignment to be go.TreeLayout.AlignmentStart.

And of course you would want to either replace the linkTemplate with an empty $(go.Link) template, or you could change the opacity of all Links to be 0. This is trivial to do if you put all Links into the “Background” Layer and then change the value of that Layer.opacity from 1 to 0 or vice-versa as the user switches views.

Oh, you would also need to select or highlight all of the nodes on the path from the selected node to the root. For the difference between selecting and highlighting:
http://gojs.net/latest/intro/selection.html
http://gojs.net/latest/intro/highlighting.html

Oops – actually, setting TreeLayout.alignment only aligns the children of that node, not all nodes for the whole diagram regardless of parent.

Other than that discrepancy, try this code:

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

      myDiagram =
        $(go.Diagram, "myDiagram",
          {
            contentAlignment: go.Spot.Center,  // center the tree in the viewport
            isReadOnly: true,  // don't allow user to change the diagram
            "animationManager.isEnabled": false,
            maxSelectionCount: 1,  // only one node may be selected at a time in each diagram
            // when the selection changes, highlight the path to the root and collapse all nodes away from path
            "ChangedSelection": function(e) {
              highlightPath(e.diagram.selection.first());
            }
          });

      // Define a node template that is shared by both diagrams
      myDiagram.nodeTemplate = 
        $(go.Node, "Auto",
          { locationSpot: go.Spot.Center },
          new go.Binding("text", "key", go.Binding.toString),  // for sorting
          $(go.Shape, "RoundedRectangle",
            new go.Binding("fill", "color"),
            { stroke: "gray", strokeWidth: 2 },
            new go.Binding("stroke", "isHighlighted", function(h) { return h ? "red" : "gray"; }).ofObject()),
          $(go.TextBlock,
            { margin: 1 },
            new go.Binding("text", "key", function(k) { return "node" + k; }))
        );

      // Define a basic link template, not selectable, shared by both diagrams
      myDiagram.linkTemplate =
        $(go.Link,
          { routing: go.Link.Normal, selectable: false, layerName: "Background" },
          $(go.Shape,
            { strokeWidth: 1 })
        );

      // Create the full tree diagram
      setupDiagram();

      layout();
    }

    // Create the tree model containing TOTAL nodes, with each node having a variable number of children
    function setupDiagram(total) {
      if (total === undefined) total = 100;  // default to 100 nodes
      var nodeDataArray = [];
      for (var i = 0; i < total; i++) {
        nodeDataArray.push({
          key: nodeDataArray.length,
          color: go.Brush.randomColor()
        });
      }
      var j = 0;
      for (var i = 1; i < total; i++) {
        var data = nodeDataArray[i];
        data.parent = j;
        if (Math.random() < 0.3) j++;  // this controls the likelihood that there are enough children
      }
      myDiagram.model = new go.TreeModel(nodeDataArray);
    }

    function layout() {
      var radio = document.getElementsByName("view");
      var view = "Tree";
      for (var i = 0; i < radio.length; i++) {
        if (radio[i].checked) view = radio[i].value;
      }
      var linklayer = myDiagram.findLayer("Background");
      if (view === "Stack") {
        myDiagram.layout = go.GraphObject.make(go.TreeLayout,
            {
              angle: 90, sorting: go.TreeLayout.SortingAscending,
              layerSpacing: 10, alignment: go.TreeLayout.AlignmentStart
            });
        linklayer.opacity = 0;
      } else {  // "Tree"
        myDiagram.layout = go.GraphObject.make(go.TreeLayout,
            {
              angle: 90, sorting: go.TreeLayout.SortingAscending
            });
        linklayer.opacity = 1;
      }
    }

    function highlightPath(node) {
      if (node === null) return;
      myDiagram.startTransaction("highlight");

      node.findTreeRoot().collapseTree();
      myDiagram.nodes.each(function(n) { n.wasTreeExpanded = false; });

      myDiagram.clearHighlighteds();
      var n = node;
      while (n !== null) {
        n.isHighlighted = true;
        n.expandTree(1);
        n = n.findTreeParentNode();
      }

      myDiagram.commitTransaction("highlight");
    }

The HTML:

  <div id="myDiagram" style="height:500px;width:100%;border:1px solid black;margin:2px"></div>
  <input type="radio" name="view" onclick="layout()" value="Tree" checked="checked" />Tree View
  <input type="radio" name="view" onclick="layout()" value="Stack" />Stack View

Thanks very much, Walter. This will help a lot.