Source: layered_layout.js

/**
 * `dc_graph.layered_layout` produces 3D layered layouts, utilizing another layout
 * that supports fixed nodes and position hints for the layers
 * @class layered_layout
 * @memberof dc_graph
 * @param {String} [id=uuid()] - Unique identifier
 * @return {dc_graph.layered_layout}
 **/
dc_graph.layered_layout = function(id) {
    var _layoutId = id || uuid();
    var _dispatch = d3.dispatch('tick', 'start', 'end');
    var _supergraph, _subgraphs;
    var _layers;
    var _options = null;

    function init(options) {
        _options = options;

    }

    function data(nodes, edges, constraints) {
        _supergraph = dc_graph.supergraph({nodes: nodes, edges: edges}, {
            nodeKey: function(n) { return n.dcg_nodeKey; },
            edgeKey: function(n) { return n.dcg_edgeKey; },
            nodeValue: function(n) { return n; },
            edgeValue: function(e) { return e; },
            edgeSource: function(e) { return e.dcg_edgeSource; },
            edgeTarget: function(e) { return e.dcg_edgeTarget; }
        });

        // every node belongs natively in one rank
        var nranks = _supergraph.nodes().reduce(function(p, n) {
            var rank = engine.layerAccessor()(n.value());
            p[rank] = p[rank] || [];
            p[rank].push(n);
            return p;
        }, {});
        var eranks = Object.keys(nranks).reduce(function(p, r) {
            p[r] = [];
            return p;
        }, {});

        // nodes are shadowed into any layers to which they are adjacent
        // edges are induced from the native&shadow nodes in each layer
        _supergraph.edges().forEach(function(e) {
            var srank = engine.layerAccessor()(e.source().value()),
                trank = engine.layerAccessor()(e.target().value());
            if(srank == trank) {
                eranks[srank].push(e);
                return;
            }
            nranks[trank].push(e.source());
            eranks[trank].push(e);
            nranks[srank].push(e.target());
            eranks[srank].push(e);
        });

        // produce a subgraph for each layer
        _subgraphs = Object.keys(nranks).reduce(function(p, r) {
            p[r] = _supergraph.subgraph(
                nranks[r].map(function(n) { return n.key(); }),
                eranks[r].map(function(e) { return e.key(); }));
            return p;
        }, {});

        // start from the most populous layer
        var max = null;
        Object.keys(nranks).forEach(function(r) {
            if(max === null ||
               _subgraphs[r].nodes().length > _subgraphs[max].nodes().length)
                max = +r;
        });

        // travel up and down from there, each time fixing the nodes from the last layer
        var ranks = Object.keys(nranks).map(function(r) { return +r; }).sort();
        _layers = ranks.map(function(r) {
            return {
                rank: r,
                z: -r * engine.layerSeparationZ()
            };
        });
        var mi = ranks.indexOf(max);
        var ups = ranks.slice(mi+1), downs = ranks.slice(0, mi).reverse();
        layout_layer(max, -1).then(function(layout) {
            Promise.all([
                layout_layers(layout, max, ups),
                layout_layers(layout, max, downs)
            ]).then(function() {
                _dispatch.end(
                    _supergraph.nodes().map(function(n) { return n.value(); }),
                    _supergraph.edges().map(function(e) { return e.value(); }));
            });
        });
    }

    function layout_layers(layout, last, layers) {
        if(layers.length === 0)
            return Promise.resolve(layout);
        var curr = layers.shift();
        return layout_layer(curr, last).then(function(layout) {
            return layout_layers(layout, curr, layers);
        });
    }

    function layout_layer(r, last) {
        _subgraphs[r].nodes().forEach(function(n) {
            if(engine.layerAccessor()(n.value()) !== r &&
               n.value().x !== undefined &&
               n.value().y !== undefined)
                n.value().dcg_nodeFixed = {
                    x: n.value().x,
                    y: n.value().y
                };
            else n.value().dcg_nodeFixed = null;
        });
        var subengine = engine.engineFactory()();
        subengine.init(_options);
        subengine.data(
            {},
            _subgraphs[r].nodes().map(function(n) {
                return n.value();
            }),
            _subgraphs[r].edges().map(function(e) {
                return e.value();
            }));
        return promise_layout(r, subengine);
    }

    function promise_layout(r, subengine) {
        // stopgap - engine.start() should return a promise
        return new Promise(function(resolve, reject) {
            subengine.on('end', function(nodes, edges) {
                resolve({nodes: nodes, edges: edges});
            });
            subengine.start();
        }).then(function(layout) {
            // copy positions back into the subgraph (and hence supergraph)
            layout.nodes.forEach(function(ln) {
                var n = _subgraphs[r].node(ln.dcg_nodeKey);
                // do not copy positions for shadow nodes
                if(engine.layerAccessor()(n.value()) !== r)
                    return;
                n.value().x = ln.x;
                n.value().y = ln.y;
                n.value().z = -r * engine.layerSeparationZ(); // lowest rank at top
            });
            return layout;
        });
    }

    function start() {
        _dispatch.start();
    }

    function stop() {
    }

    var graphviz = dc_graph.graphviz_attrs(), graphviz_keys = Object.keys(graphviz);

    var engine = Object.assign(graphviz, {
        layoutAlgorithm: function() {
            return 'layered';
        },
        layoutId: function() {
            return _layoutId;
        },
        supportsWebworker: function() {
            return false;
        },
        parent: property(null),
        on: function(event, f) {
            if(arguments.length === 1)
                return _dispatch.on(event);
            _dispatch.on(event, f);
            return this;
        },
        init: function(options) {
            this.optionNames().forEach(function(option) {
                options[option] = options[option] || this[option]();
            }.bind(this));
            init(options);
            return this;
        },
        data: function(graph, nodes, edges, constraints) {
            data(nodes, edges, constraints);
        },
        start: function() {
            start();
        },
        stop: function() {
            stop();
        },
        optionNames: function() {
            return []
                .concat(graphviz_keys);
        },
        engineFactory: property(null),
        layerAccessor: property(null),
        layerSeparationZ: property(50),
        layers: function() {
            return _layers;
        },
        populateLayoutNode: function() {},
        populateLayoutEdge: function() {},
        extractNodeAttrs: property({}), // {attr: function(node)}
        extractEdgeAttrs: property({})
    });
    return engine;
};