Layered Digraph with Custom Layers and Bands

I have a layered digraph with ~350 nodes. It was taking about 3 secs to load previously. However when I overwrote assignLayers and commitLayers my performance dropped to more like 20 secs. I am not sure if this is because of added complexity to the internal layout algorithms, or if I inadvertently caused the computations to run multiple times. Any ideas?

MyLayout.prototype.assignLayers = function() {

	go.LayeredDigraphLayout.prototype.assignLayers.call(this);
	
	var it = this.network.vertexes.iterator;
	while (it.next()) {
		var v = it.value;
		if (v.node !== null) {
			var lay = v.node.data.level;			
			if (typeof lay === "number" && lay >= 0) {
				v.layer = lay;
			}
		}
	}
};

MyLayout.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]);
          }
        }
      }
    };

To check on multiple calls – set a breakpoint or do some console.log calls to find out.

Why did you override those methods?

Looks like each method is only called once as intended. commitLayers is happening very quickly so it looks like assignLayers is the culprit for whatever reason.

Essentially the diagram we are trying to create (see screenshot) has nodes that need to be on specific floors of a building so we could not just use the default layer assignments. Then each layer is a band, but we are changing the color of bands to give some visual continuity of “floors” (i.e. bands are not just alternating colors). It was laying out fairly quick until we added the overridden layers functions. All of the layer logic on our end is happening server side so I can see that is still returning quick.

Screenshot%20(Custom)

Thanks!

Why are you callling the super method and assigning layers from the model data? That doesn’t make sense to me. Either you don’t have the layer assignments, so you call the super method and then save the results, or you do have the layer information in the data and you just use them without calling the super method.

That was just an over site and should have been commented out when I pasted the code snippet.

Any other suggestions or hints as to what might have caused the runtime to increase by so much?

No, sorry. I suppose you could profile it to see if the additional time actually is occurring in your override methods.

I think what we may end up doing is have an “admin” load the diagram and then save the layout back to the server so it does not need to be recomputed in subsequent loads by other users (our graph will not be changing all that often). Are there any examples you would point to that have best practice for that? I know the mindmap sample shows saving out the node locations. Can you do something comparable for links as well so that the location of nodes and links is all pre-computed and stored as JSON and just gets quickly loaded?

Many of the samples do that. Just look for calls to Model.toJson.

If your nodes are all the same size, or at least easily calculated from the data, you could do the layout on the server.

I think I am really close but missing one last thing. I have all the data for the layout exporting as json to our server and downloading ok. But the diagram undergoes a full layout again.I use a diagram.model = go.Model.fromJson(data); and have isInitial and isOngoing set to false. But it still undergoes a full rebuild of the graph. Any idea what I am missing?

Json:

>     {
>    "class":"GraphLinksModel",
>    "nodeDataArray":[
>       {
>          "key":"188",
>          "name":"1-CD-21",
>          "source":"image.png",
>          "level":20,
>          "branch":"5",
>          "color":"#FD7C2B",
>          "bgColor":"#FD7C2B",
>          "visible":true,
>          "currentNode":false,
>          "loc":"6092.0048828125 2614.797017478002"
>       },
>       {
>          "key":"189",
>          "name":"1-CD-22",
>          "source":"image.png",
>          "level":20,
>          "branch":"5",
>          "color":"#FD7C2B",
>          "bgColor":"#FD7C2B",
>          "visible":true,
>          "currentNode":true,
>          "loc":"6282.5048828125 2614.797017478002"
>       },
>       {
>          "key":"178",
>          "name":"1-CD-23",
>          "source":"image.png",
>          "level":21,
>          "branch":"5",
>          "color":"#FD7C2B",
>          "bgColor":"#FD7C2B",
>          "visible":true,
>          "currentNode":false,
>          "loc":"6017.0048828125 2751.31986497631"
>       }, ...
>       
> 	  
> 	  
>       {
>          "key":"_BANDS",
>          "category":"Bands",
>          "itemArray":[
>             {
>                "text":"LL",
>                "floor":"0",
>                "bounds":{
>                   "class":"go.Rect",
>                   "x":0,
>                   "y":3811.9797974644644,
>                   "width":16125,
>                   "height":121.52284749830778
>                }
>             },
>             {
>                "text":"",
>                "floor":"0",
>                "bounds":{
>                   "class":"go.Rect",
>                   "x":0,
>                   "y":3675.4569499661566,
>                   "width":16125,
>                   "height":136.52284749830778
>                }
>             },
>             {
>                "text":"",
>                "floor":"0",
>                "bounds":{
>                   "class":"go.Rect",
>                   "x":0,
>                   "y":3538.934102467849,
>                   "width":16125,
>                   "height":136.52284749830778
>                }
>             },
>             {
>                "text":"",
>                "floor":"0",
>                "bounds":{
>                   "class":"go.Rect",
>                   "x":0,
>                   "y":3418.934102467849,
>                   "width":16125,
>                   "height":120
>                }
>             },
>             {
>                "text":"First Floor",
>                "floor":"1",
>                "bounds":{
>                   "class":"go.Rect",
>                   "x":0,
>                   "y":3282.411254969541,
>                   "width":16125,
>                   "height":136.52284749830778
>                }
>             },
>             {
>                "text":"",
>                "floor":"1",
>                "bounds":{
>                   "class":"go.Rect",
>                   "x":0,
>                   "y":3145.888407471233,
>                   "width":16125,
>                   "height":136.52284749830778
>                }
>             }, ...
> 			
>          ]
>       }
>    ],
>    "linkDataArray":[
>       {
>          "from":"102",
>          "to":"101",
>          "color":"#027e54",
>          "points":[
>             1536.677734375,
>             3553.934102467849,
>             1536.677734375,
>             3543.934102467849,
>             1536.677734375,
>             3528.934102467849,
>             1536.677734375,
>             3528.934102467849,
>             1536.677734375,
>             3513.934102467849,
>             1536.677734375,
>             3503.934102467849
>          ]
>       },
>       {
>          "from":"103",
>          "to":"102",
>          "color":"#027e54",
>          "points":[
>             1536.677734375,
>             3690.4569499661566,
>             1536.677734375,
>             3680.4569499661566,
>             1536.677734375,
>             3665.4569499661566,
>             1536.677734375,
>             3665.4569499661566,
>             1536.677734375,
>             3650.4569499661566,
>             1536.677734375,
>             3640.4569499661566
>          ]
>       }, ...
> 	  
> 	  
>    ]
> }

So you set Diagram.layout to some instance of a Layout that has Layout.isInitial set to false, before you set Diagram.model? That sounds correct.

Presumably you are not calling Diagram.layoutDiagram. So I don’t know why it’s not behaving as we’d expect.

Ah that is what it is! I was calling:

diagram.isInitial = false;
diagram.isOngoing = false;

when it should be:

diagram.layout.isInitial = false;
diagram.layout.isOngoing = false;

Only issue I am still having is the bands are not reappearing.

In the Swim Bands sample, Layer Bands using a Background Part, there is a special Part that holds the “bands” that are the background for the “layers” of the laid-out diagram. Take a look at the how the model is defined in that sample. Might your loaded model not include that part whose key is “_BANDS”? If not, that’s OK, you can just add it yourself from the real model data. You would just need to figure out what information you want to show for each band – name, color, etc.

Yeah I am returning the “_BANDS” back with the json. You can see in the json snippet from the post above a few of them are there. I am guessing I am missing a binding somewhere.

diagram.nodeTemplateMap.add(
			"Bands",
			GO(	go.Part,
				"Position",
				new go.Binding("itemArray").makeTwoWay(),
				{
					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(	go.Panel,
										HORIZONTAL ? "Vertical"	: "Horizontal",
										new go.Binding("position", "bounds", function(b) {
												return b.position;
											}
										),
										GO(go.TextBlock,
												{
													angle : HORIZONTAL ? 0 : 270,
													textAlign : "center",
													wrap : go.TextBlock.None,
													font : "bold 11pt sans-serif",
												},
					new go.Binding("text"),
					new go.Binding("background", "floor", function(i) {
								return i % 2 == 0 ? "whitesmoke" : go.Brush
										.darken("whitesmoke");
							}),
					new go.Binding("width", "bounds", function(r) {
							return HORIZONTAL ? r.width : r.height;
						})
					),
					
					GO(	go.Shape,
						{
							stroke : null,
							strokeWidth : 0
						},
						new go.Binding("desiredSize", "bounds", function(r) {
								return r.size;
							}
						),
						new go.Binding("fill", "floor", function(i) {
								return i % 2 == 0 ? "whitesmoke" : go.Brush.darken("whitesmoke");
							}
						)
					)
					
					)
				}
			)
		);

Ah, you did include it in your data. Sorry, I missed that.

I’m not sure what’s wrong. First, you could check the “_BANDS” data. Is this true?

  myDiagram.model.findNodeDataForKey("_BANDS").itemArray[0].bounds instanceof go.Rect

That ought to be true if you have loaded it via the static function Model.fromJson. But if you have loaded it some other way, perhaps the step to convert plain Objects to instances of the “class” was skipped.

You should be using the debug version of the library, go-debug.js, which does more error checking and is more likely to warn you about problems.

Yeah that is returning true as expected and the debug.js is not logging any issues to the console. Hmm. Would there be any way to just force just those parts to layout again and not recompute everything?

No, I don’t see that helping.

Does Diagram | GoJS API return that special Part? If so, is it visible, is its location real (Point.isReal), is its layer the “Background” Layer?

Sure enough that was it!!! The part location for the _Bands was nan. Just needed to add a simple binding for location to the nodeTemplateMap for the Bands so it got output in the JSON and now everything works great. And the strategy of just pre-computing the complicated LayeredDigraph and storing the results yields a very fast reload as hoped!

Thank you so much for your help! This software is truly a work of art and arguably the most powerful piece of software I have ever seen. I feel I only understand 3% of its true power but am looking forward to learning much more and putting it to good use! Thanks again!