cytoscape-dagre
Version:
The Dagre layout for DAGs and trees for Cytoscape.js
272 lines (213 loc) • 7.06 kB
JavaScript
import defaults from './defaults.mjs';
import assign from './assign.mjs';
import dagre from '@dagrejs/dagre';
const isFunction = function(o){ return typeof o === 'function'; };
const EPSILON = 0.001; // what does it mean to be too close to 0?
// constructor
// options : object containing layout options
function DagreLayout( options ) {
this.options = assign( {}, defaults, options );
}
function subtract(a, b) {
return { x: noZero(a.x - b.x), y: noZero(a.y - b.y) };
}
function product(a, b) {
return noZero(a.x * b.x) + noZero(a.y * b.y);
}
function norm(v) {
const len = Math.hypot(v.x, v.y) || 1;
return { x: v.x / len, y: v.y / len, len };
}
function perp(v) {
return { x: -v.y, y: v.x };
}
/* provides the context for mapping from dagre's x, y coordinate system
* for control points to cytoscapes coordinate system for control points
* which is relative to the straight vector from source to target node
*/
function buildEdgeFrame(src, tgt) {
const d = subtract(tgt, src);
const { x, y, len } = norm(d);
const dir = { x, y };
const normal = perp(dir);
return { src, tgt, dir, normal, len };
}
function noZero(x) {
if (Math.abs(x) < EPSILON) {
return x < 0 ? -EPSILON : EPSILON;
}
return x;
}
function toEdgeCoordinates(P, frame) {
const vector = subtract(P, frame.src);
const weight = noZero(product(vector, frame.dir) / frame.len);
const distance = noZero(product(vector, frame.normal));
return { weight, distance };
}
function normalizeWeight(coords) {
let min = Infinity;
let max = -Infinity;
for (const p of coords) {
if (p.weight < min) {
min = p.weight;
}
if (p.weight > max) {
max = p.weight;
}
}
const range = max - min || 1;
return coords.map(p => ({
distance: p.distance,
weight: (p.weight - min) / range
}));
}
/* First introduce new control points to bridge between the dagre list of
* points and the centres of cytoscape nodes.
* Then we sanitize any empty or non-existing or degenerate control points
* And finally we map the Dagre coordinates to the Cytoscape coordinated which
* are relative to the original direction vector from source to target.
* These final coordinates are stored pairwise in two arrays cpw and cpd
* which are picked up by the Bezier construction code in cytoscape.
*/
function dagreEdgeToCytoscapeEdge(dEdge, cEdge) {
const fromNode = cEdge.source().position();
const toNode = cEdge.target().position();
const frame = buildEdgeFrame(fromNode, toNode);
const coords = normalizeWeight(dEdge.points.map(p => toEdgeCoordinates(p, frame)));
const controlPointWeights = coords.slice(1,-1).map(c => c.weight);
const controlPointDistances = coords.slice(1,-1).map(c => c.distance);
const result = { controlPointWeights, controlPointDistances };
return result;
}
// runs the layout
DagreLayout.prototype.run = function(){
let options = this.options;
let layout = this;
let cy = options.cy; // cy is automatically populated for us in the constructor
let eles = options.eles;
let getVal = function( ele, val ){
return isFunction(val) ? val.apply( ele, [ ele ] ) : val;
};
let bb = options.boundingBox || { x1: 0, y1: 0, w: cy.width(), h: cy.height() };
if( bb.x2 === undefined ){ bb.x2 = bb.x1 + bb.w; }
if( bb.w === undefined ){ bb.w = bb.x2 - bb.x1; }
if( bb.y2 === undefined ){ bb.y2 = bb.y1 + bb.h; }
if( bb.h === undefined ){ bb.h = bb.y2 - bb.y1; }
let g = new dagre.graphlib.Graph({
multigraph: true,
compound: true
});
let gObj = {};
let setGObj = function( name, val ){
if( val != null ){
gObj[ name ] = val;
}
};
setGObj( 'nodesep', options.nodeSep );
setGObj( 'edgesep', options.edgeSep );
setGObj( 'ranksep', options.rankSep );
setGObj( 'rankdir', options.rankDir );
setGObj( 'align', options.align);
setGObj( 'ranker', options.ranker );
setGObj( 'acyclicer', options.acyclicer);
g.setGraph( gObj );
g.setDefaultEdgeLabel(function() { return {}; });
g.setDefaultNodeLabel(function() { return {}; });
// add nodes to dagre
let nodes = eles.nodes();
if ( isFunction(options.sort) ) {
nodes = nodes.sort( options.sort );
}
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[i];
let nbb = node.layoutDimensions( options );
g.setNode( node.id(), {
width: nbb.w,
height: nbb.h,
shape: 'ellipse',
name: node.id()
} );
}
// set compound parents
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[i];
if( node.isChild() ){
g.setParent( node.id(), node.parent().id() );
}
}
// add edges to dagre
let edges = eles.edges().stdFilter(function( edge ){
return !edge.source().isParent() && !edge.target().isParent(); // dagre can't handle edges on compound nodes
});
if ( isFunction(options.sort) ) {
edges = edges.sort( options.sort );
}
for( let i = 0; i < edges.length; i++ ){
let edge = edges[i];
g.setEdge( edge.source().id(), edge.target().id(), {
minlen: getVal( edge, options.minLen ),
weight: getVal( edge, options.edgeWeight ),
name: edge.id()
}, edge.id() );
}
dagre.layout( g );
let gNodeIds = g.nodes();
for( let i = 0; i < gNodeIds.length; i++ ){
let id = gNodeIds[i];
let n = g.node( id );
cy.getElementById(id).scratch().dagre = n;
}
let dagreBB;
if( options.boundingBox ){
dagreBB = { x1: Infinity, x2: -Infinity, y1: Infinity, y2: -Infinity };
nodes.forEach(function( node ){
let dModel = node.scratch().dagre;
dagreBB.x1 = Math.min( dagreBB.x1, dModel.x );
dagreBB.x2 = Math.max( dagreBB.x2, dModel.x );
dagreBB.y1 = Math.min( dagreBB.y1, dModel.y );
dagreBB.y2 = Math.max( dagreBB.y2, dModel.y );
});
dagreBB.w = dagreBB.x2 - dagreBB.x1;
dagreBB.h = dagreBB.y2 - dagreBB.y1;
} else {
dagreBB = bb;
}
let constrainPos = function( p ){
if( options.boundingBox ){
let xPct = dagreBB.w === 0 ? 0 : (p.x - dagreBB.x1) / dagreBB.w;
let yPct = dagreBB.h === 0 ? 0 : (p.y - dagreBB.y1) / dagreBB.h;
return {
x: bb.x1 + xPct * bb.w,
y: bb.y1 + yPct * bb.h
};
} else {
return p;
}
};
nodes.layoutPositions(layout, options, function( ele ){
ele = typeof ele === "object" ? ele : this;
let dModel = ele.scratch().dagre;
return constrainPos({
x: dModel.x,
y: dModel.y
});
});
if (options.useDagreEdgeControlPoints) {
if (options.automaticDagreEdgeStyle) {
cy.edges().addClass('useDagreEdgeControlPoints');
cy.style()
.selector('edge.useDagreEdgeControlPoints')
.style(options.dagreEdgeStyle)
.update();
}
g.edges().forEach(id => {
const cyEdge = cy.getElementById(id.name);
const dEdge = g.edge(id);
if (dEdge && dEdge.points) {
cyEdge.scratch(dagreEdgeToCytoscapeEdge(dEdge, cyEdge));
}
});
}
return this; // chaining
};
export default DagreLayout;