/** * `dc_graph.tree_layout` is a very simple and not very bright tree layout. It can draw any DAG, but * tries to position the nodes as a tree. * @class tree_layout * @memberof dc_graph * @param {String} [id=uuid()] - Unique identifier * @return {dc_graph.tree_layout} **/ dc_graph.tree_layout = function(id) { var _layoutId = id || uuid(); var _dispatch = d3.dispatch('tick', 'start', 'end'); var _dfs; function init(options) { var x; var nodeWidth = d3.functor(options.nodeWidth); function best_dist(left, right) { return (nodeWidth(left) + nodeWidth(right)) / 2; } _dfs = dc_graph.depth_first_traversal({ nodeid: function(n) { return n.dcg_nodeKey; }, sourceid: function(n) { return n.dcg_edgeSource; }, targetid: function(n) { return n.dcg_edgeTarget; }, init: function() { x = options.offsetX; }, row: function(n) { return n.dcg_rank; }, place: function(n, r, row) { if(row.length) { var left = row[row.length-1]; var g = (nodeWidth(left) + nodeWidth(n)) / 2; x = Math.max(x, left.left_x + g); } n.left_x = x; n.hit_ins = 1; n.y = r*options.gapY + options.offsetY; }, sib: function(isroot, left, right) { var g = best_dist(left, right); if(isroot) g = g*1.5; x += g; }, pop: function(n) { n.x = (n.left_x + x)/2; }, skip: function(n, indegree) { // rolling average of in-neighbor x positions n.x = (n.hit_ins*n.x + x)/++n.hit_ins; if(n.hit_ins === indegree) delete n.hit_ins; }, finish: function(rows) { // this is disgusting. patch up any places where nodes overlap by scanning // right far enough to find the space, then fill from left to right at the // minimum gap rows.forEach(function(row) { var sort = row.sort(function(a, b) { return a.x - b.x; }); var badi = null, badl = null, want; for(var i=0; i<sort.length-1; ++i) { var left = sort[i], right = sort[i+1]; if(!badi) { if(right.x - left.x < best_dist(left, right)) { badi = i; badl = left.x; want = best_dist(left, right); } // else still not bad } else { want += best_dist(left, right); if(i < sort.length - 2 && right.x < badl + want) continue; // still bad else { if(badi>0) --badi; // might want to use more left var l, limit; if(i < sort.length - 2) { // found space before right var extra = right.x - (badl + want); l = sort[badi].x + extra/2; limit = i+1; } else { l = Math.max(sort[badi].x, badl - best_dist(sort[badi], sort[badi+1]) - (want - right.x + badl)/2); limit = sort.length; } for(var j = badi+1; j<limit; ++j) { l += best_dist(sort[j-1], sort[j]); sort[j].x = l; } badi = badl = want = null; } } } }); } }); } var _nodes, _edges; function data(nodes, edges) { _nodes = nodes; _edges = edges; } function start() { _dfs(_nodes, _edges); _dispatch.end(_nodes, _edges); } function stop() { } var layout = { layoutAlgorithm: function() { return 'tree'; }, 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) { data(nodes, edges); }, start: function() { start(); }, stop: function() { stop(); }, optionNames: function() { return ['nodeWidth', 'offsetX', 'offsetY', 'rowFunction', 'gapY']; }, populateLayoutNode: function(layout, node) { if(this.rowFunction()) layout.dcg_rank = this.rowFunction.eval(node); }, populateLayoutEdge: function() {}, nodeWidth: property(function(n) { return n.width; }), offsetX: property(30), offsetY: property(30), rowFunction: property(null), gapY: property(100) }; return layout; }; dc_graph.tree_layout.scripts = [];