UNPKG

cytoscape-dagre

Version:

The Dagre layout for DAGs and trees for Cytoscape.js

272 lines (213 loc) 7.06 kB
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;