/** * In cola.js there are three factors which influence the positions of nodes: * * *edge length* suggestions, controlled by the * {@link #dc_graph.diagram+lengthStrategy lengthStrategy}, * {@link #dc_graph.diagram+baseLength baseLength}, and * {@link #dc_graph.diagram+edgeLength edgeLength} parameters in dc.graph.js * * *automatic constraints* based on the global edge flow direction (`cola.flowLayout`) and overlap * avoidance parameters (`cola.avoidOverlaps`) * * *manual constraints* such as alignment, inequality and equality constraints in a dimension/axis. * * Generally when the * {@link https://github.com/tgdwyer/WebCola/wiki/Constraints cola.js documentation mentions constraints}, * it means the manual constraints. * * dc.graph.js allows generation of manual constraints using * {@link #dc_graph.diagram+constrain diagram.constrain} but it can be tedious to write these * functions because it usually means looping over the nodes and edges multiple times to * determine what classes or types of nodes to apply constraints to, and which edges should * take additional constraints. * * This utility creates a constraint generator function from a *pattern*, a graph where: * 1. Nodes represent *types* or classes of layout nodes, annotated with a specification * of how to match the nodes belonging each type. * 2. Edges represent *rules* to generate constraints. There are two kinds of rules: * <ol type='a'> * <li>To generate additional constraints on edges besides the built-in ones, create a rules * between two different types. The rule will apply to any edges in the layout which match the * source and target types, and generate simple "left/right" constraints. (Note that "left" and * "right" in this context refer to sides of an inequality constraint `left + gap <= right`) * <li>To generate constraints on a set of nodes, such as alignment, ordering, or circle * constraints, create a rule from a type to itself, a self edge. * </ol> * (It is also conceivable to want constraints between individual nodes which don't * have edges between them. This is not directly supported at this time; right now the workaround * is to create the edge but not draw it, e.g. by setting its {@link #dc_graph.diagram+edgeOpacity} * to zero. If you have a use-case for this, please * {@link https://github.com/dc-js/dc.graph.js/issues/new file an issue}. * * The pattern syntax is an embedded domain specific language designed to be terse without * restricting its power. As such, there are complicated rules for defaulting and inferring * parameters from other parameters. Since most users will want the simplest form, this document * will start from the highest level and then show how to use more complicated forms in order to * gain more control. * * Then we'll build back up from the ground up and show how inference works. * @class constraint_pattern * @memberof dc_graph * @param {dc_graph.diagram} diagram - the diagram to pull attributes from, mostly to determine * the keys of nodes and edge sources and targets * @param {Object} pattern - a graph which defines the constraints to be generated * @return {Function} */ dc_graph.constraint_pattern = function(pattern) { var types = {}, rules = []; pattern.nodes.forEach(function(n) { var id = n.id; var type = types[id] || (types[id] = {}); // partitions could be done more efficiently; this is POC if(n.partition) { var partition = n.partition; var value = n.value || n.id; if(n.all || n.typename) { type.match = n.extract ? function(n2) { return n.extract(n2.value[partition]); } : function(n2) { return n2.value[partition]; }; type.typename = n.typename || function(n2) { return partition + '=' + n2.value[partition]; }; } else type.match = function(n2) { return n2.value[partition] === value; }; } else if(n.match) type.match = n.match; else throw new Error("couldn't determine matcher for type " + JSON.stringify(n)); }); pattern.edges.forEach(function(e) { if(e.disable) return; var rule = {source: e.source, target: e.target}; rule.produce = typeof e.produce === 'function' ? e.produce : function() { return clone(e.produce); }; ['listname', 'wrap', 'reverse'].forEach(function(k) { if(e[k] !== undefined) rule[k] = e[k]; }); rules.push(rule); }); return function(diagram, nodes, edges) { var constraints = []; var members = {}; nodes.forEach(function(n) { var key = diagram.nodeKey.eval(n); for(var t in types) { var type = types[t], value = type.match(n.orig); if(value) { var tname = type.typename ? type.typename(t, value) : t; if(!members[tname]) members[tname] = { nodes: [], // original ordering whether: {} // boolean }; members[tname].nodes.push(key); members[tname].whether[key] = true; } } }); // traversal of rules could be more efficient, again POC var edge_rules = rules.filter(function(r) { return r.source !== r.target; }); var type_rules = rules.filter(function(r) { return r.source === r.target; }); edges.forEach(function(e) { var source = diagram.edgeSource.eval(e), target = diagram.edgeTarget.eval(e); edge_rules.forEach(function(r) { if(members[r.source] && members[r.source].whether[source] && members[r.target] && members[r.target].whether[target]) { var constraint = r.produce(members, nodes, edges); if(r.reverse) { constraint.left = target; constraint.right = source; } else { constraint.left = source; constraint.right = target; } constraints.push(constraint); } }); }); type_rules.forEach(function(r) { if(!members[r.source]) return; var constraint = r.produce(), listname = r.listname || r.produce.listname || 'nodes', wrap = r.wrap || r.produce.wrap || function(x) { return x; }; constraint[listname] = members[r.source].nodes.map(wrap); constraints.push(constraint); }); return constraints; }; }; // constraint generation convenience functions dc_graph.gap_y = function(gap, equality) { return { axis: 'y', gap: gap, equality: !!equality }; }; dc_graph.gap_x = function(gap, equality) { return { axis: 'x', gap: gap, equality: !!equality }; }; function align_f(axis) { var ret = function() { return { type: 'alignment', axis: axis }; }; ret.listname = 'offsets'; ret.wrap = function(x) { return {node: x, offset: 0}; }; return ret; } dc_graph.align_y = function() { return align_f('y'); }; dc_graph.align_x = function() { return align_f('x'); }; dc_graph.order_x = function(gap, ordering) { return { type: 'ordering', axis: 'x', gap: 60, ordering: ordering }; }; dc_graph.order_y = function(gap, ordering) { return { type: 'ordering', axis: 'y', gap: 60, ordering: ordering }; };