UNPKG

cytoscape-spread

Version:

The Spread physics simulation layout for Cytoscape.js

504 lines (411 loc) 13.2 kB
var Thread; var foograph = require('./foograph'); var Voronoi = require('./rhill-voronoi-core'); /* * This layout combines several algorithms: * * - It generates an initial position of the nodes by using the * Fruchterman-Reingold algorithm (doi:10.1002/spe.4380211102) * * - Finally it eliminates overlaps by using the method described by * Gansner and North (doi:10.1007/3-540-37623-2_28) */ var defaults = { animate: true, // whether to show the layout as it's running ready: undefined, // Callback on layoutready stop: undefined, // Callback on layoutstop fit: true, // Reset viewport to fit default simulationBounds minDist: 20, // Minimum distance between nodes padding: 20, // Padding expandingFactor: -1.0, // If the network does not satisfy the minDist // criterium then it expands the network of this amount // If it is set to -1.0 the amount of expansion is automatically // calculated based on the minDist, the aspect ratio and the // number of nodes maxFruchtermanReingoldIterations: 50, // Maximum number of initial force-directed iterations maxExpandIterations: 4, // Maximum number of expanding iterations boundingBox: undefined, // Constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h } randomize: false // uses random initial node positions on true }; function SpreadLayout( options ) { var opts = this.options = {}; for( var i in defaults ){ opts[i] = defaults[i]; } for( var i in options ){ opts[i] = options[i]; } } SpreadLayout.prototype.run = function() { var layout = this; var options = this.options; var cy = options.cy; var 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; } var nodes = cy.nodes(); var edges = cy.edges(); var cWidth = cy.width(); var cHeight = cy.height(); var simulationBounds = bb; var padding = options.padding; var simBBFactor = Math.max( 1, Math.log(nodes.length) * 0.8 ); if( nodes.length < 100 ){ simBBFactor /= 2; } layout.trigger( { type: 'layoutstart', layout: layout } ); var simBB = { x1: 0, y1: 0, x2: cWidth * simBBFactor, y2: cHeight * simBBFactor }; if( simulationBounds ) { simBB.x1 = simulationBounds.x1; simBB.y1 = simulationBounds.y1; simBB.x2 = simulationBounds.x2; simBB.y2 = simulationBounds.y2; } simBB.x1 += padding; simBB.y1 += padding; simBB.x2 -= padding; simBB.y2 -= padding; var width = simBB.x2 - simBB.x1; var height = simBB.y2 - simBB.y1; // Get start time var startTime = Date.now(); // layout doesn't work with just 1 node if( nodes.size() <= 1 ) { nodes.positions( { x: Math.round( ( simBB.x1 + simBB.x2 ) / 2 ), y: Math.round( ( simBB.y1 + simBB.y2 ) / 2 ) } ); if( options.fit ) { cy.fit( options.padding ); } // Get end time var endTime = Date.now(); console.info( "Layout on " + nodes.size() + " nodes took " + ( endTime - startTime ) + " ms" ); layout.one( "layoutready", options.ready ); layout.trigger( "layoutready" ); layout.one( "layoutstop", options.stop ); layout.trigger( "layoutstop" ); return; } // First I need to create the data structure to pass to the worker var pData = { 'width': width, 'height': height, 'minDist': options.minDist, 'expFact': options.expandingFactor, 'expIt': 0, 'maxExpIt': options.maxExpandIterations, 'vertices': [], 'edges': [], 'startTime': startTime, 'maxFruchtermanReingoldIterations': options.maxFruchtermanReingoldIterations }; nodes.each( function( i, node ) { var nodeId = node.id(); var pos = node.position(); if( options.randomize ){ pos = { x: Math.round( simBB.x1 + (simBB.x2 - simBB.x1) * Math.random() ), y: Math.round( simBB.y1 + (simBB.y2 - simBB.y1) * Math.random() ) }; } pData[ 'vertices' ].push( { id: nodeId, x: pos.x, y: pos.y } ); } ); edges.each( function() { var srcNodeId = this.source().id(); var tgtNodeId = this.target().id(); pData[ 'edges' ].push( { src: srcNodeId, tgt: tgtNodeId } ); } ); //Decleration var t1 = layout.thread; // reuse old thread if possible if( !t1 || t1.stopped() ){ t1 = layout.thread = Thread(); // And to add the required scripts //EXTERNAL 1 t1.require( foograph, 'foograph' ); //EXTERNAL 2 t1.require( Voronoi, 'Voronoi' ); } function setPositions( pData ){ //console.log('set posns') // First we retrieve the important data // var expandIteration = pData[ 'expIt' ]; var dataVertices = pData[ 'vertices' ]; var vertices = []; for( var i = 0; i < dataVertices.length; ++i ) { var dv = dataVertices[ i ]; vertices[ dv.id ] = { x: dv.x, y: dv.y }; } /* * FINALLY: * * We position the nodes based on the calculation */ nodes.positions( function( i, node ) { var id = node.id() var vertex = vertices[ id ]; return { x: Math.round( simBB.x1 + vertex.x ), y: Math.round( simBB.y1 + vertex.y ) }; } ); if( options.fit ) { cy.fit( options.padding ); } cy.nodes().rtrigger( "position" ); } var didLayoutReady = false; t1.on('message', function(e){ var pData = e.message; //console.log('message', e) if( !options.animate ){ return; } setPositions( pData ); if( !didLayoutReady ){ layout.trigger( "layoutready" ); didLayoutReady = true; } }); layout.one( "layoutready", options.ready ); t1.pass( pData ).run( function( pData ) { function cellCentroid( cell ) { var hes = cell.halfedges; var area = 0, x = 0, y = 0; var p1, p2, f; for( var i = 0; i < hes.length; ++i ) { p1 = hes[ i ].getEndpoint(); p2 = hes[ i ].getStartpoint(); area += p1.x * p2.y; area -= p1.y * p2.x; f = p1.x * p2.y - p2.x * p1.y; x += ( p1.x + p2.x ) * f; y += ( p1.y + p2.y ) * f; } area /= 2; f = area * 6; return { x: x / f, y: y / f }; } function sitesDistance( ls, rs ) { var dx = ls.x - rs.x; var dy = ls.y - rs.y; return Math.sqrt( dx * dx + dy * dy ); } foograph = _ref_('foograph'); Voronoi = _ref_('Voronoi'); // I need to retrieve the important data var lWidth = pData[ 'width' ]; var lHeight = pData[ 'height' ]; var lMinDist = pData[ 'minDist' ]; var lExpFact = pData[ 'expFact' ]; var lMaxExpIt = pData[ 'maxExpIt' ]; var lMaxFruchtermanReingoldIterations = pData[ 'maxFruchtermanReingoldIterations' ]; // Prepare the data to output var savePositions = function(){ pData[ 'width' ] = lWidth; pData[ 'height' ] = lHeight; pData[ 'expIt' ] = expandIteration; pData[ 'expFact' ] = lExpFact; pData[ 'vertices' ] = []; for( var i = 0; i < fv.length; ++i ) { pData[ 'vertices' ].push( { id: fv[ i ].label, x: fv[ i ].x, y: fv[ i ].y } ); } }; var messagePositions = function(){ broadcast( pData ); }; /* * FIRST STEP: Application of the Fruchterman-Reingold algorithm * * We use the version implemented by the foograph library * * Ref.: https://code.google.com/p/foograph/ */ // We need to create an instance of a graph compatible with the library var frg = new foograph.Graph( "FRgraph", false ); var frgNodes = {}; // Then we have to add the vertices var dataVertices = pData[ 'vertices' ]; for( var ni = 0; ni < dataVertices.length; ++ni ) { var id = dataVertices[ ni ][ 'id' ]; var v = new foograph.Vertex( id, Math.round( Math.random() * lHeight ), Math.round( Math.random() * lHeight ) ); frgNodes[ id ] = v; frg.insertVertex( v ); } var dataEdges = pData[ 'edges' ]; for( var ei = 0; ei < dataEdges.length; ++ei ) { var srcNodeId = dataEdges[ ei ][ 'src' ]; var tgtNodeId = dataEdges[ ei ][ 'tgt' ]; frg.insertEdge( "", 1, frgNodes[ srcNodeId ], frgNodes[ tgtNodeId ] ); } var fv = frg.vertices; // Then we apply the layout var iterations = lMaxFruchtermanReingoldIterations; var frLayoutManager = new foograph.ForceDirectedVertexLayout( lWidth, lHeight, iterations, false, lMinDist ); frLayoutManager.callback = function(){ savePositions(); messagePositions(); }; frLayoutManager.layout( frg ); savePositions(); messagePositions(); if( lMaxExpIt <= 0 ){ return pData; } /* * SECOND STEP: Tiding up of the graph. * * We use the method described by Gansner and North, based on Voronoi * diagrams. * * Ref: doi:10.1007/3-540-37623-2_28 */ // We calculate the Voronoi diagram dor the position of the nodes var voronoi = new Voronoi(); var bbox = { xl: 0, xr: lWidth, yt: 0, yb: lHeight }; var vSites = []; for( var i = 0; i < fv.length; ++i ) { vSites[ fv[ i ].label ] = fv[ i ]; } function checkMinDist( ee ) { var infractions = 0; // Then we check if the minimum distance is satisfied for( var eei = 0; eei < ee.length; ++eei ) { var e = ee[ eei ]; if( ( e.lSite != null ) && ( e.rSite != null ) && sitesDistance( e.lSite, e.rSite ) < lMinDist ) { ++infractions; } } return infractions; } var diagram = voronoi.compute( fv, bbox ); // Then we reposition the nodes at the centroid of their Voronoi cells var cells = diagram.cells; for( var i = 0; i < cells.length; ++i ) { var cell = cells[ i ]; var site = cell.site; var centroid = cellCentroid( cell ); var currv = vSites[ site.label ]; currv.x = centroid.x; currv.y = centroid.y; } if( lExpFact < 0.0 ) { // Calculates the expanding factor lExpFact = Math.max( 0.05, Math.min( 0.10, lMinDist / Math.sqrt( ( lWidth * lHeight ) / fv.length ) * 0.5 ) ); //console.info("Expanding factor is " + (options.expandingFactor * 100.0) + "%"); } var prevInfractions = checkMinDist( diagram.edges ); //console.info("Initial infractions " + prevInfractions); var bStop = ( prevInfractions <= 0 ) || lMaxExpIt <= 0; var voronoiIteration = 0; var expandIteration = 0; // var initWidth = lWidth; while( !bStop ) { ++voronoiIteration; for( var it = 0; it <= 4; ++it ) { voronoi.recycle( diagram ); diagram = voronoi.compute( fv, bbox ); // Then we reposition the nodes at the centroid of their Voronoi cells // cells = diagram.cells; for( var i = 0; i < cells.length; ++i ) { var cell = cells[ i ]; var site = cell.site; var centroid = cellCentroid( cell ); var currv = vSites[ site.label ]; currv.x = centroid.x; currv.y = centroid.y; } } var currInfractions = checkMinDist( diagram.edges ); //console.info("Current infractions " + currInfractions); if( currInfractions <= 0 ) { bStop = true; } else { if( currInfractions >= prevInfractions || voronoiIteration >= 4 ) { if( expandIteration >= lMaxExpIt ) { bStop = true; } else { lWidth += lWidth * lExpFact; lHeight += lHeight * lExpFact; bbox = { xl: 0, xr: lWidth, yt: 0, yb: lHeight }; ++expandIteration; voronoiIteration = 0; //console.info("Expanded to ("+width+","+height+")"); } } } prevInfractions = currInfractions; savePositions(); messagePositions(); } savePositions(); return pData; } ).then( function( pData ) { // var expandIteration = pData[ 'expIt' ]; var dataVertices = pData[ 'vertices' ]; setPositions( pData ); // Get end time var startTime = pData[ 'startTime' ]; var endTime = new Date(); console.info( "Layout on " + dataVertices.length + " nodes took " + ( endTime - startTime ) + " ms" ); layout.one( "layoutstop", options.stop ); if( !options.animate ){ layout.trigger( "layoutready" ); } layout.trigger( "layoutstop" ); t1.stop(); } ); return this; }; // run SpreadLayout.prototype.stop = function(){ if( this.thread ){ this.thread.stop(); } this.trigger('layoutstop'); }; SpreadLayout.prototype.destroy = function(){ if( this.thread ){ this.thread.stop(); } }; module.exports = function get( cytoscape ){ Thread = cytoscape.Thread; return SpreadLayout; };