/**
* `dc_graph.cola_layout` is an adaptor for cola.js layouts in dc.graph.js
* @class cola_layout
* @memberof dc_graph
* @param {String} [id=uuid()] - Unique identifier
* @return {dc_graph.cola_layout}
**/
dc_graph.cola_layout = function(id) {
var _layoutId = id || uuid();
var _d3cola = null;
var _setcola_nodes;
var _dispatch = d3.dispatch('tick', 'start', 'end');
var _flowLayout;
// node and edge objects shared with cola.js, preserved from one iteration
// to the next (as long as the object is still in the layout)
var _nodes = {}, _edges = {};
var _options;
function init(options) {
_options = options;
_d3cola = cola.d3adaptor()
.avoidOverlaps(true)
.size([options.width, options.height])
.handleDisconnected(options.handleDisconnected);
if(_d3cola.tickSize) // non-standard
_d3cola.tickSize(options.tickSize);
switch(options.lengthStrategy) {
case 'symmetric':
_d3cola.symmetricDiffLinkLengths(options.baseLength);
break;
case 'jaccard':
_d3cola.jaccardLinkLengths(options.baseLength);
break;
case 'individual':
_d3cola.linkDistance(function(e) {
return e.dcg_edgeLength || options.baseLength;
});
break;
case 'none':
default:
}
if(options.flowLayout) {
_d3cola.flowLayout(options.flowLayout.axis, options.flowLayout.minSeparation);
}
}
function data(nodes, edges, clusters, constraints) {
var wnodes = regenerate_objects(_nodes, nodes, null, function(v) {
return v.dcg_nodeKey;
}, function(v1, v) {
v1.dcg_nodeKey = v.dcg_nodeKey;
v1.dcg_nodeParentCluster = v.dcg_nodeParentCluster;
v1.width = v.width;
v1.height = v.height;
v1.fixed = !!v.dcg_nodeFixed;
_options.nodeAttrs.forEach(function(key) {
v1[key] = v[key];
});
if(v1.fixed && typeof v.dcg_nodeFixed === 'object') {
v1.x = v.dcg_nodeFixed.x;
v1.y = v.dcg_nodeFixed.y;
}
else {
// should we support e.g. null to unset x,y?
if(v.x !== undefined)
v1.x = v.x;
if(v.y !== undefined)
v1.y = v.y;
}
});
var wedges = regenerate_objects(_edges, edges, null, function(e) {
return e.dcg_edgeKey;
}, function(e1, e) {
e1.dcg_edgeKey = e.dcg_edgeKey;
// cola edges can work with indices or with object references
// but it will replace indices with object references
e1.source = _nodes[e.dcg_edgeSource];
e1.target = _nodes[e.dcg_edgeTarget];
e1.dcg_edgeLength = e.dcg_edgeLength;
_options.edgeAttrs.forEach(function(key) {
e1[key] = e[key];
});
});
// cola needs each node object to have an index property
wnodes.forEach(function(v, i) {
v.index = i;
});
var groups = null;
if(engine.groupConnected()) {
var components = cola.separateGraphs(wnodes, wedges);
groups = components.map(function(g) {
return {
dcg_autoGroup: true,
leaves: g.array.map(function(n) { return n.index; })
};
});
} else if(clusters) {
var G = {};
groups = clusters.filter(function(c) {
return /^cluster/.test(c.dcg_clusterKey);
}).map(function(c, i) {
return G[c.dcg_clusterKey] = {
dcg_clusterKey: c.dcg_clusterKey,
index: i,
groups: [],
leaves: []
};
});
clusters.forEach(function(c) {
if(c.dcg_clusterParent && G[c.dcg_clusterParent])
G[c.dcg_clusterParent].groups.push(G[c.dcg_clusterKey].index);
});
wnodes.forEach(function(n, i) {
if(n.dcg_nodeParentCluster && G[n.dcg_nodeParentCluster])
G[n.dcg_nodeParentCluster].leaves.push(i);
});
}
function dispatchState(event) {
// clean up extra setcola annotations
wnodes.forEach(function(n) {
Object.keys(n).forEach(function(key) {
if(/^get/.test(key) && typeof n[key] === 'function')
delete n[key];
});
});
_dispatch[event](
wnodes,
wedges.map(function(e) {
return {dcg_edgeKey: e.dcg_edgeKey};
}),
groups.filter(function(g) {
return !g.dcg_autoGroup;
}).map(function(g) {
g = Object.assign({}, g);
g.bounds = {
left: g.bounds.x,
top: g.bounds.y,
right: g.bounds.X,
bottom: g.bounds.Y
};
return g;
}),
_setcola_nodes
);
}
_d3cola.on('tick', /* _tick = */ function() {
dispatchState('tick');
}).on('start', function() {
_dispatch.start();
}).on('end', /* _done = */ function() {
dispatchState('end');
});
if(_options.setcolaSpec && typeof setcola !== 'undefined') {
console.log('generating setcola constrains');
var setcola_result = setcola
.nodes(wnodes)
.links(wedges)
.constraints(_options.setcolaSpec)
.gap(10) //default value is 10, can be customized in setcolaSpec
.layout();
_setcola_nodes = setcola_result.nodes.filter(function(n) { return n._cid; });
_d3cola.nodes(setcola_result.nodes)
.links(setcola_result.links)
.constraints(setcola_result.constraints)
.groups(groups);
} else {
_d3cola.nodes(wnodes)
.links(wedges)
.constraints(constraints)
.groups(groups);
}
}
function start() {
_d3cola.start(engine.unconstrainedIterations(),
engine.userConstraintIterations(),
engine.allConstraintsIterations(),
engine.gridSnapIterations());
}
function stop() {
if(_d3cola)
_d3cola.stop();
}
var graphviz = dc_graph.graphviz_attrs(), graphviz_keys = Object.keys(graphviz);
graphviz.rankdir(null);
var engine = Object.assign(graphviz, {
layoutAlgorithm: function() {
return 'cola';
},
layoutId: function() {
return _layoutId;
},
supportsWebworker: function() {
return true;
},
supportsMoving: function() {
return true;
},
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));
this.propagateOptions(options);
init(options);
return this;
},
data: function(graph, nodes, edges, clusters, constraints) {
data(nodes, edges, clusters, constraints);
},
start: function() {
start();
},
stop: function() {
stop();
},
optionNames: function() {
return ['handleDisconnected', 'lengthStrategy', 'baseLength', 'flowLayout',
'tickSize', 'groupConnected', 'setcolaSpec', 'setcolaNodes']
.concat(graphviz_keys);
},
passThru: function() {
return ['extractNodeAttrs', 'extractEdgeAttrs'];
},
propagateOptions: function(options) {
if(!options.nodeAttrs)
options.nodeAttrs = Object.keys(engine.extractNodeAttrs());
if(!options.edgeAttrs)
options.edgeAttrs = Object.keys(engine.extractEdgeAttrs());
},
populateLayoutNode: function() {},
populateLayoutEdge: function() {},
/**
* Instructs cola.js to fit the connected components.
* @method handleDisconnected
* @memberof dc_graph.cola_layout
* @instance
* @param {Boolean} [handleDisconnected=true]
* @return {Boolean}
* @return {dc_graph.cola_layout}
**/
handleDisconnected: property(true),
/**
* Currently, three strategies are supported for specifying the lengths of edges:
* * 'individual' - uses the `edgeLength` for each edge. If it returns falsy, uses the
* `baseLength`
* * 'symmetric', 'jaccard' - compute the edge length based on the graph structure around
* the edge. See
* {@link https://github.com/tgdwyer/WebCola/wiki/link-lengths the cola.js wiki}
* for more details.
* 'none' - no edge lengths will be specified
* @method lengthStrategy
* @memberof dc_graph.cola_layout
* @instance
* @param {Function|String} [lengthStrategy='symmetric']
* @return {Function|String}
* @return {dc_graph.cola_layout}
**/
lengthStrategy: property('symmetric'),
/**
* Gets or sets the default edge length (in pixels) when the `.lengthStrategy` is
* 'individual', and the base value to be multiplied for 'symmetric' and 'jaccard' edge
* lengths.
* @method baseLength
* @memberof dc_graph.cola_layout
* @instance
* @param {Number} [baseLength=30]
* @return {Number}
* @return {dc_graph.cola_layout}
**/
baseLength: property(30),
/**
* If `flowLayout` is set, it determines the axis and separation for
* {@link http://marvl.infotech.monash.edu/webcola/doc/classes/cola.layout.html#flowlayout cola flow layout}.
* If it is not set, `flowLayout` will be calculated from the {@link dc_graph.graphviz_attrs#rankdir rankdir}
* and {@link dc_graph.graphviz_attrs#ranksep ranksep}; if `rankdir` is also null (the
* default for cola layout), then there will be no flow.
* @method flowLayout
* @memberof dc_graph.cola_layout
* @instance
* @param {Object} [flowLayout=null]
* @example
* // No flow (default)
* diagram.flowLayout(null)
* // flow in x with min separation 200
* diagram.flowLayout({axis: 'x', minSeparation: 200})
**/
flowLayout: function(flow) {
if(!arguments.length) {
if(_flowLayout)
return _flowLayout;
var dir = engine.rankdir();
switch(dir) {
case 'LR': return {axis: 'x', minSeparation: engine.ranksep() + engine.parent().nodeRadius()*2};
case 'TB': return {axis: 'y', minSeparation: engine.ranksep() + engine.parent().nodeRadius()*2};
default: return null; // RL, BT do not appear to be possible (negative separation) (?)
}
}
_flowLayout = flow;
return this;
},
unconstrainedIterations: property(10),
userConstraintIterations: property(20),
allConstraintsIterations: property(20),
gridSnapIterations: property(0),
tickSize: property(1),
groupConnected: property(false),
setcolaSpec: property(null),
setcolaNodes: function() {
return _setcola_nodes;
},
extractNodeAttrs: property({}), // {attr: function(node)}
extractEdgeAttrs: property({}),
processExtraWorkerResults: function(setcolaNodes) {
_setcola_nodes = setcolaNodes;
}
});
return engine;
};
dc_graph.cola_layout.scripts = ['d3.js', 'cola.js'];
dc_graph.cola_layout.optional_scripts = ['setcola.js'];