Source: graphviz_layout.js

/**
 * `dc_graph.graphviz_layout` is an adaptor for viz.js (graphviz) layouts in dc.graph.js
 *
 * In addition to the below layout attributes, `graphviz_layout` also implements the attributes from
 * {@link dc_graph.graphviz_attrs graphviz_attrs}
 * @class graphviz_layout
 * @memberof dc_graph
 * @param {String} [id=uuid()] - Unique identifier
 * @return {dc_graph.graphviz_layout}
 **/
dc_graph.graphviz_layout = function(id, layout, server) {
    var _layoutId = id || uuid();
    var _dispatch = d3.dispatch('tick', 'start', 'end');
    var _dotInput, _dotString;

    function init(options) {
    }

    function encode_name(name) {
        return name.replace(/^%/, '%');
    }
    function decode_name(name) {
        return name.replace(/^%/, '%');
    }
    function stringize_property(prop, value) {
        return [prop, '"' + value + '"'].join('=');
    }
    function stringize_properties(props) {
        return '[' + props.join(', ') + ']';
    }
    function data(nodes, edges, clusters) {
        if(_dotInput) {
            _dotString = _dotInput;
            return;
        }
        var lines = [];
        var directed = layout !== 'neato';
        lines.push((directed ? 'digraph' : 'graph') + ' g {');
        lines.push('graph ' + stringize_properties([
            stringize_property('nodesep', graphviz.nodesep()/72),
            stringize_property('ranksep', graphviz.ranksep()/72),
            stringize_property('rankdir', graphviz.rankdir())
        ]));
        var cluster_nodes = {};
        nodes.forEach(function(n) {
            var cl = n.dcg_nodeParentCluster;
            if(cl) {
                cluster_nodes[cl] = cluster_nodes[cl] || [];
                cluster_nodes[cl].push(n.dcg_nodeKey);
            }
        });
        var cluster_children = {}, tops = [];
        clusters.forEach(function(c) {
            var p = c.dcg_clusterParent;
            if(p) {
                cluster_children[p] = cluster_children[p] || [];
                cluster_children[p].push(c.dcg_clusterKey);
            } else tops.push(c.dcg_clusterKey);
        });

        function print_subgraph(i, c) {
            var indent = ' '.repeat(i*2);
            lines.push(indent + 'subgraph "' + c + '" {');
            if(cluster_children[c])
                cluster_children[c].forEach(print_subgraph.bind(null, i+1));
            if(cluster_nodes[c])
                lines.push(indent + '  ' + cluster_nodes[c].map(function (s) { return JSON.stringify(s) }).join(' '));
            lines.push(indent + '}');
        }
        tops.forEach(print_subgraph.bind(null, 1));

        lines = lines.concat(nodes.map(function(v) {
            var props = [
                stringize_property('width', v.width/72),
                stringize_property('height', v.height/72),
                stringize_property('fixedsize', 'shape'),
                stringize_property('shape', v.abstract.shape)
            ];
            if(v.dcg_nodeFixed)
                props.push(stringize_property('pos', [
                    v.dcg_nodeFixed.x,
                    1000-v.dcg_nodeFixed.y
                ].join(',')));
            return '  "' + encode_name(v.dcg_nodeKey) + '" ' + stringize_properties(props);
        }));
        lines = lines.concat(edges.map(function(e) {
            return '  "' + encode_name(e.dcg_edgeSource) + (directed ? '" -> "' : '" -- "') +
                encode_name(e.dcg_edgeTarget) + '" ' + stringize_properties([
                    stringize_property('id', encode_name(e.dcg_edgeKey)),
                stringize_property('arrowhead', 'none'),
                stringize_property('arrowtail', 'none')
                ]);
        }));
        lines.push('}');
        lines.push('');
        _dotString = lines.join('\n');
    }

    function process_response(error, result) {
        if(error) {
            console.warn("graphviz layout failed: ", error);
            return;
        }
        _dispatch.start();
        var bb = result.bb.split(',').map(function(x) { return +x; });
        var nodes = (result.objects || []).filter(function(n) {
            return n.pos; // remove non-nodes like clusters
        }).map(function(n) {
            var pos = n.pos.split(',');
            if(isNaN(pos[0]) || isNaN(pos[1])) {
                console.warn('got a NaN position from graphviz');
                pos[0] = pos[1] = 0;
            }
            return {
                dcg_nodeKey: decode_name(n.name),
                x: +pos[0],
                y: bb[3] - pos[1]
            };
        });
        var clusters = (result.objects || []).filter(function(n) {
            return /^cluster/.test(n.name) && n.bb;
        });
        clusters.forEach(function(c) {
            c.dcg_clusterKey = c.name;

            // gv: llx, lly, urx, ury, up-positive
            var cbb = c.bb.split(',').map(function(s) { return +s; });
            c.bounds = {left: cbb[0], top: bb[3] - cbb[3],
                        right: cbb[2], bottom: bb[3] - cbb[1]};
        });
        var edges = (result.edges || []).map(function(e) {
            var e2 = {
                dcg_edgeKey: decode_name(e.id || 'n' + e._gvid)
            };
            if(e._draw_) {
                var directive = e._draw_.find(function(d) { return d.op && d.points; });
                e2.points = directive.points.map(function(p) { return {x: p[0], y: bb[3] - p[1]}; });
            }
            return e2;
        });
        _dispatch.end(nodes, edges, clusters);
    }

    function start() {
        if(server) {
            d3.json(server)
                .header("Content-type", "application/x-www-form-urlencoded")
                .post('layouttool=' + layout + '&' + encodeURIComponent(_dotString), process_response);
        }
        else {
            var result = Viz(_dotString, {format: 'json', engine: layout, totalMemory: 1 << 25});
            result = JSON.parse(result);
            process_response(null, result);
        }
    }

    function stop() {
    }

    var graphviz = dc_graph.graphviz_attrs(), graphviz_keys = Object.keys(graphviz);
    return Object.assign(graphviz, {
        layoutAlgorithm: function() {
            return layout;
        },
        layoutId: function() {
            return _layoutId;
        },
        supportsWebworker: function() {
            return false;
        },
        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, clusters) {
            data(nodes, edges, clusters);
        },
        dotInput: function(text) {
            _dotInput = text;
            return this;
        },
        start: function() {
            start();
        },
        stop: function() {
            stop();
        },
        optionNames: function() {
            return graphviz_keys;
        },
        populateLayoutNode: function() {},
        populateLayoutEdge: function() {}
    });
}