LayeredDigraphLayout nodes rendering to wrong layer when layers contain no nodes

I have a diagram using a modified LayeredDigraphLayout. I need 5 layers to be displayed at all times, even when a layer does not contain any nodes.

This is the overridden assignLayers() function of the custom layout (there are no other modifications):

assignLayers = (): void => {
    const layout = this as DeviceEditWiringViewDiagramLayout;

    // call super for initial layer assignments
    DeviceEditWiringViewDiagramLayout.prototype.assignLayers.call(layout);

    // iterate through all the nodes
    layout.network.vertexes.each((v: go.LayeredDigraphVertex) => {
        // we need to check the node template to be "GeneralDevice", as only its data
        // model is the type of DeviceHeaderDiagramModel
        switch (v.node.category) {
            case "GeneralDevice":
                const model = v.node.data as DeviceHeaderDiagramModel;

                // nodes without input ports go to the left side
                if (model.ports.every(group => group.inputPorts.length === 0)) {
                    v.layer = 4;
                }

                // the only node with both input and output ports goes to the middle
                else if (model.ports.some(
                    group =>
                        group.inputPorts.length !== 0 &&
                        group.outputPorts.length !== 0
                )) {
                    v.layer = 2;
                }

                // nodes without output ports go to the right
                else if (model.ports.every(group => group.outputPorts.length === 0)) {
                    v.layer = 0;
                }

                break;

            case "GeneralDevicePlaceholder":
                const placeholderModel = v.node.data as DeviceHeaderPlaceholderDiagramModel;

                if (placeholderModel.inputPort) v.layer = 1;
                else if (placeholderModel.outputPort) v.layer = 3;

                break;

            default:
                break;
        }
    });
};

So I explicitly tell every node on the diagram which layer they should be in and this is working correctly. The issue is if one of the middle layers (second and fourth) is empty (does not contain any nodes), the outer layers (first and fifth) jump in their place (0->1 and 4-> 3).

This is how it looks like correctly with 5 layers:

And this is how it looks when the middle layers (1 and 3) do not contain any nodes:

Actually it starts from the left and layer 4 will stay in place. Layer 2 will jump in the place of 3, and 0 to 2.

When there are no vertexes in a layer, the layer takes zero space.

You can create a dummy vertex in such layers (or all layers, really), and override LayeredDigraphLayout.nodeMinLayerSpace to return whatever value you like.

  function LDL() {
    go.LayeredDigraphLayout.call(this);
  }
  go.Diagram.inherit(LDL, go.LayeredDigraphLayout);

  LDL.prototype.assignLayers = function() {
    go.LayeredDigraphLayout.prototype.assignLayers.call(this);
    . . .
    // if there aren't any vertexes in layer 1, create a dummy one:
    var v = this.network.createVertex();
    v.layer = 1;
    this.network.addVertex(v);
  }

  LDL.prototype.nodeMinLayerSpace = function(v, topleft) {
    if (v.layer === 1) return 50;
    return go.LayeredDigraphLayout.prototype.nodeMinLayerSpace.call(this, v, topleft);
  }

Wow, this is working perfectly except one edge case!

If the leftmost layer is empty, everything is shifting one to the left (or the grid is shifting to the right?):

Code for reference:

assignLayers = (): void => {
    const layout = this as DeviceEditWiringViewDiagramLayout;

    // call super for initial layer assignments
    DeviceEditWiringViewDiagramLayout.prototype.assignLayers.call(layout);

    // we will count the number of nodes in each layer here
    const layerNodeCounts = new Array<number>();

    // iterate through all the nodes
    layout.network.vertexes.each((v: go.LayeredDigraphVertex) => {
        // we need to check the node template to be "GeneralDevice", as only its data
        // model is the type of DeviceHeaderDiagramModel
        switch (v.node.category) {
            case "GeneralDevice":
                const model = v.node.data as DeviceHeaderDiagramModel;

                // nodes without ports go to the left side
                if (model.ports.every(group => group.inputPorts.length === 0)) {
                    v.layer = 4;
                }

                // the only node with both input and output ports goes to the middle
                else if (model.ports.some(
                    group =>
                        group.inputPorts.length !== 0 &&
                        group.outputPorts.length !== 0
                )) {
                    v.layer = 2;
                }

                // nodes without output ports go to the right
                else if (model.ports.every(group => group.outputPorts.length === 0)) {
                    v.layer = 0;
                }

                break;

            case "GeneralDevicePlaceholder":
                const placeholderModel = v.node.data as DeviceHeaderPlaceholderDiagramModel;

                if (placeholderModel.inputPort) v.layer = 1;
                else if (placeholderModel.outputPort) v.layer = 3;

                break;

            default:
                break;
        }

        // add 1 to the layer count or set it to 1 if it was undefined
        layerNodeCounts[v.layer] ? layerNodeCounts[v.layer]++ : layerNodeCounts[v.layer] = 1;
    });

    for (let i = 0; i < layerNodeCounts.length; i++) {
        // if there aren't any vertexes in a given layer, create a dummy one
        if (!layerNodeCounts[i]) {
            var v = layout.network.createVertex() as go.LayeredDigraphVertex;
            v.layer = i;
            layout.network.addVertex(v);
        }
    }
};

nodeMinLayerSpace = (v: go.LayeredDigraphVertex, topleft: boolean): number => {
    // this is needed for the dummy nodes
    // as nodeMinLayerSpace calculates from the center, we will take the half of the standard node width
    return Constants.diagram.nodeDefaultWidth / 2;
}

Thanks for the quick help!

I suggest that you blindly add a dummy vertex to each layer and mark them so that you can recognize them in your override of nodeMinLayerSpace to return what you want, or else return the super call.

Okey, fair, the leftmost layer is the last in the array. If it was empty, I wasn’t even getting there in the for loop to add a dummy vertex. I have fixed the number of layers (5) to a constant and it is working correctly now. Thank you a lot!

for (let i = 0; i < NUMBER_OF_LAYERS; i++) {
    // if there aren't any vertexes in a given layer, create a dummy one
    if (!layerNodeCounts[i]) {
        var v = layout.network.createVertex() as go.LayeredDigraphVertex;
        v.layer = i;
        layout.network.addVertex(v);
        console.log("dummy vertex added to layer", i);
    }
}