cytoscape
Advanced tools
Comparing version 3.6.2 to 3.6.3
{ | ||
"build/cytoscape.umd.js": { | ||
"bundled": 907018, | ||
"minified": 334561, | ||
"gzipped": 103964 | ||
"bundled": 902985, | ||
"minified": 333462, | ||
"gzipped": 103571 | ||
}, | ||
"build/cytoscape.cjs.js": { | ||
"bundled": 835450, | ||
"minified": 354810, | ||
"gzipped": 106789 | ||
"bundled": 831579, | ||
"minified": 354160, | ||
"gzipped": 106513 | ||
}, | ||
"build/cytoscape.esm.js": { | ||
"bundled": 835277, | ||
"minified": 354667, | ||
"gzipped": 106751, | ||
"bundled": 831406, | ||
"minified": 354017, | ||
"gzipped": 106479, | ||
"treeshaked": { | ||
"rollup": { | ||
"code": 329110, | ||
"code": 328009, | ||
"import_statements": 51 | ||
}, | ||
"webpack": { | ||
"code": 331118 | ||
"code": 330017 | ||
} | ||
@@ -24,0 +24,0 @@ } |
{ | ||
"name": "cytoscape", | ||
"version": "3.6.2", | ||
"version": "3.6.3", | ||
"license": "MIT", | ||
@@ -81,4 +81,3 @@ "description": "Graph theory (a.k.a. network) library for analysis and visualisation", | ||
"dist": "cross-env NODE_ENV=production run-s build dist:*", | ||
"release": "run-s copyright dist docs", | ||
"postpublish": "run-s docs:push", | ||
"release": "run-s copyright dist", | ||
"watch": "run-s watch:fast", | ||
@@ -85,0 +84,0 @@ "watch:sync": "livereload \"build, debug\"", |
@@ -160,2 +160,4 @@ import * as util from '../util'; | ||
elesfn.hasElementWithId = function( id ){ | ||
id = '' + id; // id must be string | ||
return this._private.map.has( id ); | ||
@@ -165,2 +167,4 @@ }; | ||
elesfn.getElementById = function( id ){ | ||
id = '' + id; // id must be string | ||
let cy = this._private.cy; | ||
@@ -189,2 +193,4 @@ let entry = this._private.map.get( id ); | ||
elesfn.indexOfId = function( id ){ | ||
id = '' + id; // id must be string | ||
return this._private.map.get( id ).index; | ||
@@ -218,9 +224,9 @@ }; | ||
if( src != null && src !== data.source ){ | ||
spec.source = src; | ||
if( src != null && src != data.source ){ | ||
spec.source = '' + src; // id must be string | ||
move = true; | ||
} | ||
if( tgt != null && tgt !== data.target ){ | ||
spec.target = tgt; | ||
if( tgt != null && tgt != data.target ){ | ||
spec.target = '' + tgt; // id must be string | ||
move = true; | ||
@@ -235,3 +241,3 @@ } | ||
if( (parent != null || data.parent != null) && parent !== data.parent ){ | ||
if( (parent != null || data.parent != null) && parent != data.parent ){ | ||
if( parent === undefined ){ // can't set undefined imperatively, so use null | ||
@@ -241,2 +247,6 @@ parent = null; | ||
if( parent != null ){ | ||
parent = '' + parent; // id must be string | ||
} | ||
ele = ele.move({ parent }); | ||
@@ -450,4 +460,9 @@ } | ||
src._private.edges.push( edge ); | ||
tgt._private.edges.push( edge ); | ||
// only one edge in node if loop | ||
if (src.same(tgt)) { | ||
src._private.edges.push( edge ); | ||
} else { | ||
src._private.edges.push( edge ); | ||
tgt._private.edges.push( edge ); | ||
} | ||
@@ -733,5 +748,7 @@ edge._private.source = src; | ||
let toString = id => id == null ? id : '' + id; // id must be string | ||
if( struct.source !== undefined || struct.target !== undefined ){ | ||
let srcId = struct.source; | ||
let tgtId = struct.target; | ||
let srcId = toString(struct.source); | ||
let tgtId = toString(struct.target); | ||
let srcExists = srcId != null && cy.hasElementWithId( srcId ); | ||
@@ -763,3 +780,3 @@ let tgtExists = tgtId != null && cy.hasElementWithId( tgtId ); | ||
} else if( struct.parent !== undefined ){ // move node to new parent | ||
let parentId = struct.parent; | ||
let parentId = toString(struct.parent); | ||
let parentExists = parentId === null || cy.hasElementWithId( parentId ); | ||
@@ -766,0 +783,0 @@ |
import easings from './easings'; | ||
import ease from './ease'; | ||
import * as is from '../../is'; | ||
import {bound} from '../../math'; | ||
@@ -113,3 +114,3 @@ function step( self, ani, now, isCore ){ | ||
if( valid( startZoom, endZoom ) ){ | ||
_p.zoom = ease( startZoom, endZoom, percent, easing ); | ||
_p.zoom = bound( _p.minZoom, ease( startZoom, endZoom, percent, easing ), _p.maxZoom ); | ||
} | ||
@@ -116,0 +117,0 @@ |
@@ -263,3 +263,3 @@ import window from '../window'; | ||
mount: function( container, rendererOptions ){ | ||
mount: function( container ){ | ||
if( container == null ){ return; } | ||
@@ -271,5 +271,2 @@ | ||
let rOpts = rendererOptions ? rendererOptions : { name: 'canvas' }; | ||
options.renderer = rOpts; | ||
if( !is.htmlElement( container ) && is.htmlElement( container[0] ) ){ | ||
@@ -288,3 +285,3 @@ container = container[0]; | ||
cy.initRenderer( rOpts ); | ||
cy.initRenderer( util.assign({}, options, options.renderer) ); | ||
@@ -337,3 +334,3 @@ cy.startAnimationLoop(); | ||
let json = jsons[ i ]; | ||
let id = json.data.id; | ||
let id = '' + json.data.id; // id must be string | ||
let ele = cy.getElementById( id ); | ||
@@ -340,0 +337,0 @@ |
@@ -14,3 +14,2 @@ /* | ||
import * as is from '../../is'; | ||
import Promise from '../../promise'; | ||
@@ -100,6 +99,3 @@ var DEBUG; | ||
// Lower temperature threshold (below this point the layout will end) | ||
minTemp: 1.0, | ||
// Pass a reference to weaver to use threads for calculations | ||
weaver: false | ||
minTemp: 1.0 | ||
}; | ||
@@ -125,54 +121,3 @@ | ||
var layout = this; | ||
var thread = this.thread; | ||
var Thread = options.weaver ? options.weaver.Thread : null; | ||
var falseThread = { // use false thread as polyfill | ||
listeners: [], | ||
on: function(e, cb){ | ||
this.listeners.push({ event: e, callback: cb }); | ||
return this; | ||
}, | ||
trigger: function(e){ | ||
if( is.string(e) ){ | ||
e = { type: e }; | ||
} | ||
var matchesEvent = function( l ){ return l.event === e.type; }; | ||
var trigger = function( l ){ l.callback(e); }; | ||
this.listeners.filter( matchesEvent ).forEach( trigger ); | ||
return this; | ||
}, | ||
pass: function( data ){ | ||
this.pass = data; | ||
return this; | ||
}, | ||
run: function( cb ){ | ||
var pass = this.pass; | ||
return new Promise(function( resolve ){ | ||
resolve( cb( pass ) ); | ||
}); | ||
}, | ||
stop: function(){ | ||
return this; | ||
}, | ||
stopped: function(){ | ||
return true; | ||
}, | ||
}; | ||
function broadcast( message ){ // for false thread | ||
var e = { type: 'message', message: message }; | ||
falseThread.trigger( e ); | ||
} | ||
if( !thread || thread.stopped() ){ | ||
thread = this.thread = Thread ? new Thread() : falseThread; | ||
} | ||
layout.stopped = false; | ||
@@ -204,763 +149,89 @@ | ||
var startTime = Date.now(); | ||
var refreshRequested = false; | ||
var refresh = function( rOpts ){ | ||
rOpts = rOpts || {}; | ||
var startTime = util.performanceNow(); | ||
if( refreshRequested && !rOpts.next ){ | ||
return; | ||
var refresh = function(){ | ||
refreshPositions( layoutInfo, cy, options ); | ||
// Fit the graph if necessary | ||
if( true === options.fit ){ | ||
cy.fit( options.padding ); | ||
} | ||
}; | ||
if( !rOpts.force && Date.now() - startTime < options.animationThreshold ){ | ||
return; | ||
var mainLoop = function( i ){ | ||
if( layout.stopped || i >= options.numIter ){ | ||
// logDebug("Layout manually stopped. Stopping computation in step " + i); | ||
return false; | ||
} | ||
refreshRequested = true; | ||
// Do one step in the phisical simulation | ||
step( layoutInfo, options, i ); | ||
util.requestAnimationFrame( function(){ | ||
refreshPositions( layoutInfo, cy, options ); | ||
// Update temperature | ||
layoutInfo.temperature = layoutInfo.temperature * options.coolingFactor; | ||
// logDebug("New temperature: " + layoutInfo.temperature); | ||
// Fit the graph if necessary | ||
if( true === options.fit ){ | ||
cy.fit( options.padding ); | ||
} | ||
if( layoutInfo.temperature < options.minTemp ){ | ||
// logDebug("Temperature drop below minimum threshold. Stopping computation in step " + i); | ||
return false; | ||
} | ||
refreshRequested = false; | ||
if( rOpts.next ){ rOpts.next(); } | ||
}); | ||
return true; | ||
}; | ||
thread.on( 'message', function( e ){ | ||
var layoutNodes = e.message; | ||
var done = function(){ | ||
if( options.animate === true || options.animate === false ){ | ||
refresh(); | ||
layoutInfo.layoutNodes = layoutNodes; | ||
refresh(); | ||
} ); | ||
// Layout has finished | ||
layout.one('layoutstop', options.stop); | ||
layout.emit({ type: 'layoutstop', layout: layout }); | ||
} else { | ||
var nodes = options.eles.nodes(); | ||
var getScaledPos = getScaleInBoundsFn(layoutInfo, options, nodes); | ||
thread.pass( { | ||
layoutInfo: layoutInfo, | ||
options: { | ||
animate: options.animate, | ||
refresh: options.refresh, | ||
componentSpacing: options.componentSpacing, | ||
nodeOverlap: options.nodeOverlap, | ||
nestingFactor: options.nestingFactor, | ||
gravity: options.gravity, | ||
numIter: options.numIter, | ||
initialTemp: options.initialTemp, | ||
coolingFactor: options.coolingFactor, | ||
minTemp: options.minTemp | ||
nodes.layoutPositions(layout, options, getScaledPos); | ||
} | ||
} ).run( function( pass ){ | ||
var layoutInfo = pass.layoutInfo; | ||
var options = pass.options; | ||
var stopped = false; | ||
}; | ||
/** | ||
* @brief : Performs one iteration of the physical simulation | ||
* @arg layoutInfo : LayoutInfo object already initialized | ||
* @arg cy : Cytoscape object | ||
* @arg options : Layout options | ||
*/ | ||
var step = function( layoutInfo, options, step ){ | ||
// var s = "\n\n###############################"; | ||
// s += "\nSTEP: " + step; | ||
// s += "\n###############################\n"; | ||
// logDebug(s); | ||
var i = 0; | ||
var loopRet = true; | ||
// Calculate node repulsions | ||
calculateNodeForces( layoutInfo, options ); | ||
// Calculate edge forces | ||
calculateEdgeForces( layoutInfo, options ); | ||
// Calculate gravity forces | ||
calculateGravityForces( layoutInfo, options ); | ||
// Propagate forces from parent to child | ||
propagateForces( layoutInfo, options ); | ||
// Update positions based on calculated forces | ||
updatePositions( layoutInfo, options ); | ||
}; | ||
if( options.animate === true ){ | ||
var frame = function(){ | ||
var f = 0; | ||
/** | ||
* @brief : Computes the node repulsion forces | ||
*/ | ||
var calculateNodeForces = function( layoutInfo, options ){ | ||
// Go through each of the graphs in graphSet | ||
// Nodes only repel each other if they belong to the same graph | ||
// var s = 'calculateNodeForces'; | ||
// logDebug(s); | ||
for( var i = 0; i < layoutInfo.graphSet.length; i ++ ){ | ||
var graph = layoutInfo.graphSet[ i ]; | ||
var numNodes = graph.length; | ||
while( loopRet && f < options.refresh ){ | ||
loopRet = mainLoop(i); | ||
// s = "Set: " + graph.toString(); | ||
// logDebug(s); | ||
// Now get all the pairs of nodes | ||
// Only get each pair once, (A, B) = (B, A) | ||
for( var j = 0; j < numNodes; j++ ){ | ||
var node1 = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[ j ] ] ]; | ||
for( var k = j + 1; k < numNodes; k++ ){ | ||
var node2 = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[ k ] ] ]; | ||
nodeRepulsion( node1, node2, layoutInfo, options ); | ||
} | ||
} | ||
i++; | ||
f++; | ||
} | ||
}; | ||
var randomDistance = function( max ){ | ||
return -max + 2 * max * Math.random(); | ||
}; | ||
/** | ||
* @brief : Compute the node repulsion forces between a pair of nodes | ||
*/ | ||
var nodeRepulsion = function( node1, node2, layoutInfo, options ){ | ||
// var s = "Node repulsion. Node1: " + node1.id + " Node2: " + node2.id; | ||
var cmptId1 = node1.cmptId; | ||
var cmptId2 = node2.cmptId; | ||
if( cmptId1 !== cmptId2 && !layoutInfo.isCompound ){ return; } | ||
// Get direction of line connecting both node centers | ||
var directionX = node2.positionX - node1.positionX; | ||
var directionY = node2.positionY - node1.positionY; | ||
var maxRandDist = 1; | ||
// s += "\ndirectionX: " + directionX + ", directionY: " + directionY; | ||
// If both centers are the same, apply a random force | ||
if( 0 === directionX && 0 === directionY ){ | ||
directionX = randomDistance( maxRandDist ); | ||
directionY = randomDistance( maxRandDist ); | ||
} | ||
var overlap = nodesOverlap( node1, node2, directionX, directionY ); | ||
if( overlap > 0 ){ | ||
// s += "\nNodes DO overlap."; | ||
// s += "\nOverlap: " + overlap; | ||
// If nodes overlap, repulsion force is proportional | ||
// to the overlap | ||
var force = options.nodeOverlap * overlap; | ||
// Compute the module and components of the force vector | ||
var distance = Math.sqrt( directionX * directionX + directionY * directionY ); | ||
// s += "\nDistance: " + distance; | ||
var forceX = force * directionX / distance; | ||
var forceY = force * directionY / distance; | ||
if( !loopRet ){ // it's done | ||
separateComponents( layoutInfo, options ); | ||
done(); | ||
} else { | ||
// s += "\nNodes do NOT overlap."; | ||
// If there's no overlap, force is inversely proportional | ||
// to squared distance | ||
var now = util.performanceNow(); | ||
// Get clipping points for both nodes | ||
var point1 = findClippingPoint( node1, directionX, directionY ); | ||
var point2 = findClippingPoint( node2, -1 * directionX, -1 * directionY ); | ||
// Use clipping points to compute distance | ||
var distanceX = point2.x - point1.x; | ||
var distanceY = point2.y - point1.y; | ||
var distanceSqr = distanceX * distanceX + distanceY * distanceY; | ||
var distance = Math.sqrt( distanceSqr ); | ||
// s += "\nDistance: " + distance; | ||
// Compute the module and components of the force vector | ||
var force = ( node1.nodeRepulsion + node2.nodeRepulsion ) / distanceSqr; | ||
var forceX = force * distanceX / distance; | ||
var forceY = force * distanceY / distance; | ||
} | ||
// Apply force | ||
if( !node1.isLocked ){ | ||
node1.offsetX -= forceX; | ||
node1.offsetY -= forceY; | ||
} | ||
if( !node2.isLocked ){ | ||
node2.offsetX += forceX; | ||
node2.offsetY += forceY; | ||
} | ||
// s += "\nForceX: " + forceX + " ForceY: " + forceY; | ||
// logDebug(s); | ||
return; | ||
}; | ||
/** | ||
* @brief : Determines whether two nodes overlap or not | ||
* @return : Amount of overlapping (0 => no overlap) | ||
*/ | ||
var nodesOverlap = function( node1, node2, dX, dY ){ | ||
if( dX > 0 ){ | ||
var overlapX = node1.maxX - node2.minX; | ||
} else { | ||
var overlapX = node2.maxX - node1.minX; | ||
} | ||
if( dY > 0 ){ | ||
var overlapY = node1.maxY - node2.minY; | ||
} else { | ||
var overlapY = node2.maxY - node1.minY; | ||
} | ||
if( overlapX >= 0 && overlapY >= 0 ){ | ||
return Math.sqrt( overlapX * overlapX + overlapY * overlapY ); | ||
} else { | ||
return 0; | ||
} | ||
}; | ||
/** | ||
* @brief : Finds the point in which an edge (direction dX, dY) intersects | ||
* the rectangular bounding box of it's source/target node | ||
*/ | ||
var findClippingPoint = function( node, dX, dY ){ | ||
// Shorcuts | ||
var X = node.positionX; | ||
var Y = node.positionY; | ||
var H = node.height || 1; | ||
var W = node.width || 1; | ||
var dirSlope = dY / dX; | ||
var nodeSlope = H / W; | ||
// var s = 'Computing clipping point of node ' + node.id + | ||
// " . Height: " + H + ", Width: " + W + | ||
// "\nDirection " + dX + ", " + dY; | ||
// | ||
// Compute intersection | ||
var res = {}; | ||
// Case: Vertical direction (up) | ||
if( 0 === dX && 0 < dY ){ | ||
res.x = X; | ||
// s += "\nUp direction"; | ||
res.y = Y + H / 2; | ||
return res; | ||
} | ||
// Case: Vertical direction (down) | ||
if( 0 === dX && 0 > dY ){ | ||
res.x = X; | ||
res.y = Y + H / 2; | ||
// s += "\nDown direction"; | ||
return res; | ||
} | ||
// Case: Intersects the right border | ||
if( 0 < dX && | ||
-1 * nodeSlope <= dirSlope && | ||
dirSlope <= nodeSlope ){ | ||
res.x = X + W / 2; | ||
res.y = Y + (W * dY / 2 / dX); | ||
// s += "\nRightborder"; | ||
return res; | ||
} | ||
// Case: Intersects the left border | ||
if( 0 > dX && | ||
-1 * nodeSlope <= dirSlope && | ||
dirSlope <= nodeSlope ){ | ||
res.x = X - W / 2; | ||
res.y = Y - (W * dY / 2 / dX); | ||
// s += "\nLeftborder"; | ||
return res; | ||
} | ||
// Case: Intersects the top border | ||
if( 0 < dY && | ||
( dirSlope <= -1 * nodeSlope || | ||
dirSlope >= nodeSlope ) ){ | ||
res.x = X + (H * dX / 2 / dY); | ||
res.y = Y + H / 2; | ||
// s += "\nTop border"; | ||
return res; | ||
} | ||
// Case: Intersects the bottom border | ||
if( 0 > dY && | ||
( dirSlope <= -1 * nodeSlope || | ||
dirSlope >= nodeSlope ) ){ | ||
res.x = X - (H * dX / 2 / dY); | ||
res.y = Y - H / 2; | ||
// s += "\nBottom border"; | ||
return res; | ||
} | ||
// s += "\nClipping point found at " + res.x + ", " + res.y; | ||
// logDebug(s); | ||
return res; | ||
}; | ||
/** | ||
* @brief : Calculates all edge forces | ||
*/ | ||
var calculateEdgeForces = function( layoutInfo, options ){ | ||
// Iterate over all edges | ||
for( var i = 0; i < layoutInfo.edgeSize; i++ ){ | ||
// Get edge, source & target nodes | ||
var edge = layoutInfo.layoutEdges[ i ]; | ||
var sourceIx = layoutInfo.idToIndex[ edge.sourceId ]; | ||
var source = layoutInfo.layoutNodes[ sourceIx ]; | ||
var targetIx = layoutInfo.idToIndex[ edge.targetId ]; | ||
var target = layoutInfo.layoutNodes[ targetIx ]; | ||
// Get direction of line connecting both node centers | ||
var directionX = target.positionX - source.positionX; | ||
var directionY = target.positionY - source.positionY; | ||
// If both centers are the same, do nothing. | ||
// A random force has already been applied as node repulsion | ||
if( 0 === directionX && 0 === directionY ){ | ||
continue; | ||
if( now - startTime >= options.animationThreshold ){ | ||
refresh(); | ||
} | ||
// Get clipping points for both nodes | ||
var point1 = findClippingPoint( source, directionX, directionY ); | ||
var point2 = findClippingPoint( target, -1 * directionX, -1 * directionY ); | ||
var lx = point2.x - point1.x; | ||
var ly = point2.y - point1.y; | ||
var l = Math.sqrt( lx * lx + ly * ly ); | ||
var force = Math.pow( edge.idealLength - l, 2 ) / edge.elasticity; | ||
if( 0 !== l ){ | ||
var forceX = force * lx / l; | ||
var forceY = force * ly / l; | ||
} else { | ||
var forceX = 0; | ||
var forceY = 0; | ||
} | ||
// Add this force to target and source nodes | ||
if( !source.isLocked ){ | ||
source.offsetX += forceX; | ||
source.offsetY += forceY; | ||
} | ||
if( !target.isLocked ){ | ||
target.offsetX -= forceX; | ||
target.offsetY -= forceY; | ||
} | ||
// var s = 'Edge force between nodes ' + source.id + ' and ' + target.id; | ||
// s += "\nDistance: " + l + " Force: (" + forceX + ", " + forceY + ")"; | ||
// logDebug(s); | ||
util.requestAnimationFrame(frame); | ||
} | ||
}; | ||
/** | ||
* @brief : Computes gravity forces for all nodes | ||
*/ | ||
var calculateGravityForces = function( layoutInfo, options ){ | ||
var distThreshold = 1; | ||
frame(); | ||
} else { | ||
while( loopRet ){ | ||
loopRet = mainLoop(i); | ||
// var s = 'calculateGravityForces'; | ||
// logDebug(s); | ||
for( var i = 0; i < layoutInfo.graphSet.length; i ++ ){ | ||
var graph = layoutInfo.graphSet[ i ]; | ||
var numNodes = graph.length; | ||
i++; | ||
} | ||
// s = "Set: " + graph.toString(); | ||
// logDebug(s); | ||
// Compute graph center | ||
if( 0 === i ){ | ||
var centerX = layoutInfo.clientHeight / 2; | ||
var centerY = layoutInfo.clientWidth / 2; | ||
} else { | ||
// Get Parent node for this graph, and use its position as center | ||
var temp = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[0] ] ]; | ||
var parent = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ temp.parentId ] ]; | ||
var centerX = parent.positionX; | ||
var centerY = parent.positionY; | ||
} | ||
// s = "Center found at: " + centerX + ", " + centerY; | ||
// logDebug(s); | ||
// Apply force to all nodes in graph | ||
for( var j = 0; j < numNodes; j++ ){ | ||
var node = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[ j ] ] ]; | ||
// s = "Node: " + node.id; | ||
if( node.isLocked ){ continue; } | ||
var dx = centerX - node.positionX; | ||
var dy = centerY - node.positionY; | ||
var d = Math.sqrt( dx * dx + dy * dy ); | ||
if( d > distThreshold ){ | ||
var fx = options.gravity * dx / d; | ||
var fy = options.gravity * dy / d; | ||
node.offsetX += fx; | ||
node.offsetY += fy; | ||
// s += ": Applied force: " + fx + ", " + fy; | ||
} else { | ||
// s += ": skypped since it's too close to center"; | ||
} | ||
// logDebug(s); | ||
} | ||
} | ||
}; | ||
/** | ||
* @brief : This function propagates the existing offsets from | ||
* parent nodes to its descendents. | ||
* @arg layoutInfo : layoutInfo Object | ||
* @arg cy : cytoscape Object | ||
* @arg options : Layout options | ||
*/ | ||
var propagateForces = function( layoutInfo, options ){ | ||
// Inline implementation of a queue, used for traversing the graph in BFS order | ||
var queue = []; | ||
var start = 0; // Points to the start the queue | ||
var end = -1; // Points to the end of the queue | ||
// logDebug('propagateForces'); | ||
// Start by visiting the nodes in the root graph | ||
queue.push.apply( queue, layoutInfo.graphSet[0] ); | ||
end += layoutInfo.graphSet[0].length; | ||
// Traverse the graph, level by level, | ||
while( start <= end ){ | ||
// Get the node to visit and remove it from queue | ||
var nodeId = queue[ start++ ]; | ||
var nodeIndex = layoutInfo.idToIndex[ nodeId ]; | ||
var node = layoutInfo.layoutNodes[ nodeIndex ]; | ||
var children = node.children; | ||
// We only need to process the node if it's compound | ||
if( 0 < children.length && !node.isLocked ){ | ||
var offX = node.offsetX; | ||
var offY = node.offsetY; | ||
// var s = "Propagating offset from parent node : " + node.id + | ||
// ". OffsetX: " + offX + ". OffsetY: " + offY; | ||
// s += "\n Children: " + children.toString(); | ||
// logDebug(s); | ||
for( var i = 0; i < children.length; i++ ){ | ||
var childNode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ children[ i ] ] ]; | ||
// Propagate offset | ||
childNode.offsetX += offX; | ||
childNode.offsetY += offY; | ||
// Add children to queue to be visited | ||
queue[ ++end ] = children[ i ]; | ||
} | ||
// Reset parent offsets | ||
node.offsetX = 0; | ||
node.offsetY = 0; | ||
} | ||
} | ||
}; | ||
/** | ||
* @brief : Updates the layout model positions, based on | ||
* the accumulated forces | ||
*/ | ||
var updatePositions = function( layoutInfo, options ){ | ||
// var s = 'Updating positions'; | ||
// logDebug(s); | ||
// Reset boundaries for compound nodes | ||
for( var i = 0; i < layoutInfo.nodeSize; i++ ){ | ||
var n = layoutInfo.layoutNodes[ i ]; | ||
if( 0 < n.children.length ){ | ||
// logDebug("Resetting boundaries of compound node: " + n.id); | ||
n.maxX = undefined; | ||
n.minX = undefined; | ||
n.maxY = undefined; | ||
n.minY = undefined; | ||
} | ||
} | ||
for( var i = 0; i < layoutInfo.nodeSize; i++ ){ | ||
var n = layoutInfo.layoutNodes[ i ]; | ||
if( 0 < n.children.length || n.isLocked ){ | ||
// No need to set compound or locked node position | ||
// logDebug("Skipping position update of node: " + n.id); | ||
continue; | ||
} | ||
// s = "Node: " + n.id + " Previous position: (" + | ||
// n.positionX + ", " + n.positionY + ")."; | ||
// Limit displacement in order to improve stability | ||
var tempForce = limitForce( n.offsetX, n.offsetY, layoutInfo.temperature ); | ||
n.positionX += tempForce.x; | ||
n.positionY += tempForce.y; | ||
n.offsetX = 0; | ||
n.offsetY = 0; | ||
n.minX = n.positionX - n.width; | ||
n.maxX = n.positionX + n.width; | ||
n.minY = n.positionY - n.height; | ||
n.maxY = n.positionY + n.height; | ||
// s += " New Position: (" + n.positionX + ", " + n.positionY + ")."; | ||
// logDebug(s); | ||
// Update ancestry boudaries | ||
updateAncestryBoundaries( n, layoutInfo ); | ||
} | ||
// Update size, position of compund nodes | ||
for( var i = 0; i < layoutInfo.nodeSize; i++ ){ | ||
var n = layoutInfo.layoutNodes[ i ]; | ||
if( 0 < n.children.length && !n.isLocked ){ | ||
n.positionX = (n.maxX + n.minX) / 2; | ||
n.positionY = (n.maxY + n.minY) / 2; | ||
n.width = n.maxX - n.minX; | ||
n.height = n.maxY - n.minY; | ||
// s = "Updating position, size of compound node " + n.id; | ||
// s += "\nPositionX: " + n.positionX + ", PositionY: " + n.positionY; | ||
// s += "\nWidth: " + n.width + ", Height: " + n.height; | ||
// logDebug(s); | ||
} | ||
} | ||
}; | ||
/** | ||
* @brief : Limits a force (forceX, forceY) to be not | ||
* greater (in modulo) than max. | ||
8 Preserves force direction. | ||
*/ | ||
var limitForce = function( forceX, forceY, max ){ | ||
// var s = "Limiting force: (" + forceX + ", " + forceY + "). Max: " + max; | ||
var force = Math.sqrt( forceX * forceX + forceY * forceY ); | ||
if( force > max ){ | ||
var res = { | ||
x: max * forceX / force, | ||
y: max * forceY / force | ||
}; | ||
} else { | ||
var res = { | ||
x: forceX, | ||
y: forceY | ||
}; | ||
} | ||
// s += ".\nResult: (" + res.x + ", " + res.y + ")"; | ||
// logDebug(s); | ||
return res; | ||
}; | ||
/** | ||
* @brief : Function used for keeping track of compound node | ||
* sizes, since they should bound all their subnodes. | ||
*/ | ||
var updateAncestryBoundaries = function( node, layoutInfo ){ | ||
// var s = "Propagating new position/size of node " + node.id; | ||
var parentId = node.parentId; | ||
if( null == parentId ){ | ||
// If there's no parent, we are done | ||
// s += ". No parent node."; | ||
// logDebug(s); | ||
return; | ||
} | ||
// Get Parent Node | ||
var p = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ parentId ] ]; | ||
var flag = false; | ||
// MaxX | ||
if( null == p.maxX || node.maxX + p.padRight > p.maxX ){ | ||
p.maxX = node.maxX + p.padRight; | ||
flag = true; | ||
// s += "\nNew maxX for parent node " + p.id + ": " + p.maxX; | ||
} | ||
// MinX | ||
if( null == p.minX || node.minX - p.padLeft < p.minX ){ | ||
p.minX = node.minX - p.padLeft; | ||
flag = true; | ||
// s += "\nNew minX for parent node " + p.id + ": " + p.minX; | ||
} | ||
// MaxY | ||
if( null == p.maxY || node.maxY + p.padBottom > p.maxY ){ | ||
p.maxY = node.maxY + p.padBottom; | ||
flag = true; | ||
// s += "\nNew maxY for parent node " + p.id + ": " + p.maxY; | ||
} | ||
// MinY | ||
if( null == p.minY || node.minY - p.padTop < p.minY ){ | ||
p.minY = node.minY - p.padTop; | ||
flag = true; | ||
// s += "\nNew minY for parent node " + p.id + ": " + p.minY; | ||
} | ||
// If updated boundaries, propagate changes upward | ||
if( flag ){ | ||
// logDebug(s); | ||
return updateAncestryBoundaries( p, layoutInfo ); | ||
} | ||
// s += ". No changes in boundaries/position of parent node " + p.id; | ||
// logDebug(s); | ||
return; | ||
}; | ||
var separateComponents = function( layutInfo, options ){ | ||
var nodes = layoutInfo.layoutNodes; | ||
var components = []; | ||
for( var i = 0; i < nodes.length; i++ ){ | ||
var node = nodes[ i ]; | ||
var cid = node.cmptId; | ||
var component = components[ cid ] = components[ cid ] || []; | ||
component.push( node ); | ||
} | ||
var totalA = 0; | ||
for( var i = 0; i < components.length; i++ ){ | ||
var c = components[ i ]; | ||
if( !c ){ continue; } | ||
c.x1 = Infinity; | ||
c.x2 = -Infinity; | ||
c.y1 = Infinity; | ||
c.y2 = -Infinity; | ||
for( var j = 0; j < c.length; j++ ){ | ||
var n = c[ j ]; | ||
c.x1 = Math.min( c.x1, n.positionX - n.width / 2 ); | ||
c.x2 = Math.max( c.x2, n.positionX + n.width / 2 ); | ||
c.y1 = Math.min( c.y1, n.positionY - n.height / 2 ); | ||
c.y2 = Math.max( c.y2, n.positionY + n.height / 2 ); | ||
} | ||
c.w = c.x2 - c.x1; | ||
c.h = c.y2 - c.y1; | ||
totalA += c.w * c.h; | ||
} | ||
components.sort( function( c1, c2 ){ | ||
return c2.w * c2.h - c1.w * c1.h; | ||
} ); | ||
var x = 0; | ||
var y = 0; | ||
var usedW = 0; | ||
var rowH = 0; | ||
var maxRowW = Math.sqrt( totalA ) * layoutInfo.clientWidth / layoutInfo.clientHeight; | ||
for( var i = 0; i < components.length; i++ ){ | ||
var c = components[ i ]; | ||
if( !c ){ continue; } | ||
for( var j = 0; j < c.length; j++ ){ | ||
var n = c[ j ]; | ||
if( !n.isLocked ){ | ||
n.positionX += x; | ||
n.positionY += y; | ||
} | ||
} | ||
x += c.w + options.componentSpacing; | ||
usedW += c.w + options.componentSpacing; | ||
rowH = Math.max( rowH, c.h ); | ||
if( usedW > maxRowW ){ | ||
y += rowH + options.componentSpacing; | ||
x = 0; | ||
usedW = 0; | ||
rowH = 0; | ||
} | ||
} | ||
}; | ||
var mainLoop = function( i ){ | ||
if( stopped ){ | ||
// logDebug("Layout manually stopped. Stopping computation in step " + i); | ||
return false; | ||
} | ||
// Do one step in the phisical simulation | ||
step( layoutInfo, options, i ); | ||
// Update temperature | ||
layoutInfo.temperature = layoutInfo.temperature * options.coolingFactor; | ||
// logDebug("New temperature: " + layoutInfo.temperature); | ||
if( layoutInfo.temperature < options.minTemp ){ | ||
// logDebug("Temperature drop below minimum threshold. Stopping computation in step " + i); | ||
return false; | ||
} | ||
return true; | ||
}; | ||
var i = 0; | ||
var loopRet; | ||
do { | ||
var f = 0; | ||
while( (f < options.refresh || options.refresh === 0) && i < options.numIter ){ | ||
var loopRet = mainLoop( i ); | ||
if( !loopRet ){ break; } | ||
f++; | ||
i++; | ||
} | ||
if( options.animate === true ){ | ||
broadcast( layoutInfo.layoutNodes ); // eslint-disable-line no-undef | ||
} | ||
} while( loopRet && i + 1 < options.numIter ); | ||
separateComponents( layoutInfo, options ); | ||
return layoutInfo; | ||
} ).then( function( layoutInfoUpdated ){ | ||
layoutInfo.layoutNodes = layoutInfoUpdated.layoutNodes; // get the positions | ||
thread.stop(); | ||
done(); | ||
} ); | ||
} | ||
var done = function(){ | ||
if( options.animate === true || options.animate === false ){ | ||
refresh({ | ||
force: true, | ||
next: function(){ | ||
// Layout has finished | ||
layout.one('layoutstop', options.stop); | ||
layout.emit({ type: 'layoutstop', layout: layout }); | ||
} | ||
}); | ||
} else { | ||
var nodes = options.eles.nodes(); | ||
var getScaledPos = getScaleInBoundsFn(layoutInfo, options, nodes); | ||
nodes.layoutPositions(layout, options, getScaledPos); | ||
} | ||
}; | ||
return this; // chaining | ||
@@ -1419,2 +690,639 @@ }; | ||
/** | ||
* @brief : Performs one iteration of the physical simulation | ||
* @arg layoutInfo : LayoutInfo object already initialized | ||
* @arg cy : Cytoscape object | ||
* @arg options : Layout options | ||
*/ | ||
var step = function( layoutInfo, options, step ){ | ||
// var s = "\n\n###############################"; | ||
// s += "\nSTEP: " + step; | ||
// s += "\n###############################\n"; | ||
// logDebug(s); | ||
// Calculate node repulsions | ||
calculateNodeForces( layoutInfo, options ); | ||
// Calculate edge forces | ||
calculateEdgeForces( layoutInfo, options ); | ||
// Calculate gravity forces | ||
calculateGravityForces( layoutInfo, options ); | ||
// Propagate forces from parent to child | ||
propagateForces( layoutInfo, options ); | ||
// Update positions based on calculated forces | ||
updatePositions( layoutInfo, options ); | ||
}; | ||
/** | ||
* @brief : Computes the node repulsion forces | ||
*/ | ||
var calculateNodeForces = function( layoutInfo, options ){ | ||
// Go through each of the graphs in graphSet | ||
// Nodes only repel each other if they belong to the same graph | ||
// var s = 'calculateNodeForces'; | ||
// logDebug(s); | ||
for( var i = 0; i < layoutInfo.graphSet.length; i ++ ){ | ||
var graph = layoutInfo.graphSet[ i ]; | ||
var numNodes = graph.length; | ||
// s = "Set: " + graph.toString(); | ||
// logDebug(s); | ||
// Now get all the pairs of nodes | ||
// Only get each pair once, (A, B) = (B, A) | ||
for( var j = 0; j < numNodes; j++ ){ | ||
var node1 = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[ j ] ] ]; | ||
for( var k = j + 1; k < numNodes; k++ ){ | ||
var node2 = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[ k ] ] ]; | ||
nodeRepulsion( node1, node2, layoutInfo, options ); | ||
} | ||
} | ||
} | ||
}; | ||
var randomDistance = function( max ){ | ||
return -max + 2 * max * Math.random(); | ||
}; | ||
/** | ||
* @brief : Compute the node repulsion forces between a pair of nodes | ||
*/ | ||
var nodeRepulsion = function( node1, node2, layoutInfo, options ){ | ||
// var s = "Node repulsion. Node1: " + node1.id + " Node2: " + node2.id; | ||
var cmptId1 = node1.cmptId; | ||
var cmptId2 = node2.cmptId; | ||
if( cmptId1 !== cmptId2 && !layoutInfo.isCompound ){ return; } | ||
// Get direction of line connecting both node centers | ||
var directionX = node2.positionX - node1.positionX; | ||
var directionY = node2.positionY - node1.positionY; | ||
var maxRandDist = 1; | ||
// s += "\ndirectionX: " + directionX + ", directionY: " + directionY; | ||
// If both centers are the same, apply a random force | ||
if( 0 === directionX && 0 === directionY ){ | ||
directionX = randomDistance( maxRandDist ); | ||
directionY = randomDistance( maxRandDist ); | ||
} | ||
var overlap = nodesOverlap( node1, node2, directionX, directionY ); | ||
if( overlap > 0 ){ | ||
// s += "\nNodes DO overlap."; | ||
// s += "\nOverlap: " + overlap; | ||
// If nodes overlap, repulsion force is proportional | ||
// to the overlap | ||
var force = options.nodeOverlap * overlap; | ||
// Compute the module and components of the force vector | ||
var distance = Math.sqrt( directionX * directionX + directionY * directionY ); | ||
// s += "\nDistance: " + distance; | ||
var forceX = force * directionX / distance; | ||
var forceY = force * directionY / distance; | ||
} else { | ||
// s += "\nNodes do NOT overlap."; | ||
// If there's no overlap, force is inversely proportional | ||
// to squared distance | ||
// Get clipping points for both nodes | ||
var point1 = findClippingPoint( node1, directionX, directionY ); | ||
var point2 = findClippingPoint( node2, -1 * directionX, -1 * directionY ); | ||
// Use clipping points to compute distance | ||
var distanceX = point2.x - point1.x; | ||
var distanceY = point2.y - point1.y; | ||
var distanceSqr = distanceX * distanceX + distanceY * distanceY; | ||
var distance = Math.sqrt( distanceSqr ); | ||
// s += "\nDistance: " + distance; | ||
// Compute the module and components of the force vector | ||
var force = ( node1.nodeRepulsion + node2.nodeRepulsion ) / distanceSqr; | ||
var forceX = force * distanceX / distance; | ||
var forceY = force * distanceY / distance; | ||
} | ||
// Apply force | ||
if( !node1.isLocked ){ | ||
node1.offsetX -= forceX; | ||
node1.offsetY -= forceY; | ||
} | ||
if( !node2.isLocked ){ | ||
node2.offsetX += forceX; | ||
node2.offsetY += forceY; | ||
} | ||
// s += "\nForceX: " + forceX + " ForceY: " + forceY; | ||
// logDebug(s); | ||
return; | ||
}; | ||
/** | ||
* @brief : Determines whether two nodes overlap or not | ||
* @return : Amount of overlapping (0 => no overlap) | ||
*/ | ||
var nodesOverlap = function( node1, node2, dX, dY ){ | ||
if( dX > 0 ){ | ||
var overlapX = node1.maxX - node2.minX; | ||
} else { | ||
var overlapX = node2.maxX - node1.minX; | ||
} | ||
if( dY > 0 ){ | ||
var overlapY = node1.maxY - node2.minY; | ||
} else { | ||
var overlapY = node2.maxY - node1.minY; | ||
} | ||
if( overlapX >= 0 && overlapY >= 0 ){ | ||
return Math.sqrt( overlapX * overlapX + overlapY * overlapY ); | ||
} else { | ||
return 0; | ||
} | ||
}; | ||
/** | ||
* @brief : Finds the point in which an edge (direction dX, dY) intersects | ||
* the rectangular bounding box of it's source/target node | ||
*/ | ||
var findClippingPoint = function( node, dX, dY ){ | ||
// Shorcuts | ||
var X = node.positionX; | ||
var Y = node.positionY; | ||
var H = node.height || 1; | ||
var W = node.width || 1; | ||
var dirSlope = dY / dX; | ||
var nodeSlope = H / W; | ||
// var s = 'Computing clipping point of node ' + node.id + | ||
// " . Height: " + H + ", Width: " + W + | ||
// "\nDirection " + dX + ", " + dY; | ||
// | ||
// Compute intersection | ||
var res = {}; | ||
// Case: Vertical direction (up) | ||
if( 0 === dX && 0 < dY ){ | ||
res.x = X; | ||
// s += "\nUp direction"; | ||
res.y = Y + H / 2; | ||
return res; | ||
} | ||
// Case: Vertical direction (down) | ||
if( 0 === dX && 0 > dY ){ | ||
res.x = X; | ||
res.y = Y + H / 2; | ||
// s += "\nDown direction"; | ||
return res; | ||
} | ||
// Case: Intersects the right border | ||
if( 0 < dX && | ||
-1 * nodeSlope <= dirSlope && | ||
dirSlope <= nodeSlope ){ | ||
res.x = X + W / 2; | ||
res.y = Y + (W * dY / 2 / dX); | ||
// s += "\nRightborder"; | ||
return res; | ||
} | ||
// Case: Intersects the left border | ||
if( 0 > dX && | ||
-1 * nodeSlope <= dirSlope && | ||
dirSlope <= nodeSlope ){ | ||
res.x = X - W / 2; | ||
res.y = Y - (W * dY / 2 / dX); | ||
// s += "\nLeftborder"; | ||
return res; | ||
} | ||
// Case: Intersects the top border | ||
if( 0 < dY && | ||
( dirSlope <= -1 * nodeSlope || | ||
dirSlope >= nodeSlope ) ){ | ||
res.x = X + (H * dX / 2 / dY); | ||
res.y = Y + H / 2; | ||
// s += "\nTop border"; | ||
return res; | ||
} | ||
// Case: Intersects the bottom border | ||
if( 0 > dY && | ||
( dirSlope <= -1 * nodeSlope || | ||
dirSlope >= nodeSlope ) ){ | ||
res.x = X - (H * dX / 2 / dY); | ||
res.y = Y - H / 2; | ||
// s += "\nBottom border"; | ||
return res; | ||
} | ||
// s += "\nClipping point found at " + res.x + ", " + res.y; | ||
// logDebug(s); | ||
return res; | ||
}; | ||
/** | ||
* @brief : Calculates all edge forces | ||
*/ | ||
var calculateEdgeForces = function( layoutInfo, options ){ | ||
// Iterate over all edges | ||
for( var i = 0; i < layoutInfo.edgeSize; i++ ){ | ||
// Get edge, source & target nodes | ||
var edge = layoutInfo.layoutEdges[ i ]; | ||
var sourceIx = layoutInfo.idToIndex[ edge.sourceId ]; | ||
var source = layoutInfo.layoutNodes[ sourceIx ]; | ||
var targetIx = layoutInfo.idToIndex[ edge.targetId ]; | ||
var target = layoutInfo.layoutNodes[ targetIx ]; | ||
// Get direction of line connecting both node centers | ||
var directionX = target.positionX - source.positionX; | ||
var directionY = target.positionY - source.positionY; | ||
// If both centers are the same, do nothing. | ||
// A random force has already been applied as node repulsion | ||
if( 0 === directionX && 0 === directionY ){ | ||
continue; | ||
} | ||
// Get clipping points for both nodes | ||
var point1 = findClippingPoint( source, directionX, directionY ); | ||
var point2 = findClippingPoint( target, -1 * directionX, -1 * directionY ); | ||
var lx = point2.x - point1.x; | ||
var ly = point2.y - point1.y; | ||
var l = Math.sqrt( lx * lx + ly * ly ); | ||
var force = Math.pow( edge.idealLength - l, 2 ) / edge.elasticity; | ||
if( 0 !== l ){ | ||
var forceX = force * lx / l; | ||
var forceY = force * ly / l; | ||
} else { | ||
var forceX = 0; | ||
var forceY = 0; | ||
} | ||
// Add this force to target and source nodes | ||
if( !source.isLocked ){ | ||
source.offsetX += forceX; | ||
source.offsetY += forceY; | ||
} | ||
if( !target.isLocked ){ | ||
target.offsetX -= forceX; | ||
target.offsetY -= forceY; | ||
} | ||
// var s = 'Edge force between nodes ' + source.id + ' and ' + target.id; | ||
// s += "\nDistance: " + l + " Force: (" + forceX + ", " + forceY + ")"; | ||
// logDebug(s); | ||
} | ||
}; | ||
/** | ||
* @brief : Computes gravity forces for all nodes | ||
*/ | ||
var calculateGravityForces = function( layoutInfo, options ){ | ||
var distThreshold = 1; | ||
// var s = 'calculateGravityForces'; | ||
// logDebug(s); | ||
for( var i = 0; i < layoutInfo.graphSet.length; i ++ ){ | ||
var graph = layoutInfo.graphSet[ i ]; | ||
var numNodes = graph.length; | ||
// s = "Set: " + graph.toString(); | ||
// logDebug(s); | ||
// Compute graph center | ||
if( 0 === i ){ | ||
var centerX = layoutInfo.clientHeight / 2; | ||
var centerY = layoutInfo.clientWidth / 2; | ||
} else { | ||
// Get Parent node for this graph, and use its position as center | ||
var temp = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[0] ] ]; | ||
var parent = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ temp.parentId ] ]; | ||
var centerX = parent.positionX; | ||
var centerY = parent.positionY; | ||
} | ||
// s = "Center found at: " + centerX + ", " + centerY; | ||
// logDebug(s); | ||
// Apply force to all nodes in graph | ||
for( var j = 0; j < numNodes; j++ ){ | ||
var node = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[ j ] ] ]; | ||
// s = "Node: " + node.id; | ||
if( node.isLocked ){ continue; } | ||
var dx = centerX - node.positionX; | ||
var dy = centerY - node.positionY; | ||
var d = Math.sqrt( dx * dx + dy * dy ); | ||
if( d > distThreshold ){ | ||
var fx = options.gravity * dx / d; | ||
var fy = options.gravity * dy / d; | ||
node.offsetX += fx; | ||
node.offsetY += fy; | ||
// s += ": Applied force: " + fx + ", " + fy; | ||
} else { | ||
// s += ": skypped since it's too close to center"; | ||
} | ||
// logDebug(s); | ||
} | ||
} | ||
}; | ||
/** | ||
* @brief : This function propagates the existing offsets from | ||
* parent nodes to its descendents. | ||
* @arg layoutInfo : layoutInfo Object | ||
* @arg cy : cytoscape Object | ||
* @arg options : Layout options | ||
*/ | ||
var propagateForces = function( layoutInfo, options ){ | ||
// Inline implementation of a queue, used for traversing the graph in BFS order | ||
var queue = []; | ||
var start = 0; // Points to the start the queue | ||
var end = -1; // Points to the end of the queue | ||
// logDebug('propagateForces'); | ||
// Start by visiting the nodes in the root graph | ||
queue.push.apply( queue, layoutInfo.graphSet[0] ); | ||
end += layoutInfo.graphSet[0].length; | ||
// Traverse the graph, level by level, | ||
while( start <= end ){ | ||
// Get the node to visit and remove it from queue | ||
var nodeId = queue[ start++ ]; | ||
var nodeIndex = layoutInfo.idToIndex[ nodeId ]; | ||
var node = layoutInfo.layoutNodes[ nodeIndex ]; | ||
var children = node.children; | ||
// We only need to process the node if it's compound | ||
if( 0 < children.length && !node.isLocked ){ | ||
var offX = node.offsetX; | ||
var offY = node.offsetY; | ||
// var s = "Propagating offset from parent node : " + node.id + | ||
// ". OffsetX: " + offX + ". OffsetY: " + offY; | ||
// s += "\n Children: " + children.toString(); | ||
// logDebug(s); | ||
for( var i = 0; i < children.length; i++ ){ | ||
var childNode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ children[ i ] ] ]; | ||
// Propagate offset | ||
childNode.offsetX += offX; | ||
childNode.offsetY += offY; | ||
// Add children to queue to be visited | ||
queue[ ++end ] = children[ i ]; | ||
} | ||
// Reset parent offsets | ||
node.offsetX = 0; | ||
node.offsetY = 0; | ||
} | ||
} | ||
}; | ||
/** | ||
* @brief : Updates the layout model positions, based on | ||
* the accumulated forces | ||
*/ | ||
var updatePositions = function( layoutInfo, options ){ | ||
// var s = 'Updating positions'; | ||
// logDebug(s); | ||
// Reset boundaries for compound nodes | ||
for( var i = 0; i < layoutInfo.nodeSize; i++ ){ | ||
var n = layoutInfo.layoutNodes[ i ]; | ||
if( 0 < n.children.length ){ | ||
// logDebug("Resetting boundaries of compound node: " + n.id); | ||
n.maxX = undefined; | ||
n.minX = undefined; | ||
n.maxY = undefined; | ||
n.minY = undefined; | ||
} | ||
} | ||
for( var i = 0; i < layoutInfo.nodeSize; i++ ){ | ||
var n = layoutInfo.layoutNodes[ i ]; | ||
if( 0 < n.children.length || n.isLocked ){ | ||
// No need to set compound or locked node position | ||
// logDebug("Skipping position update of node: " + n.id); | ||
continue; | ||
} | ||
// s = "Node: " + n.id + " Previous position: (" + | ||
// n.positionX + ", " + n.positionY + ")."; | ||
// Limit displacement in order to improve stability | ||
var tempForce = limitForce( n.offsetX, n.offsetY, layoutInfo.temperature ); | ||
n.positionX += tempForce.x; | ||
n.positionY += tempForce.y; | ||
n.offsetX = 0; | ||
n.offsetY = 0; | ||
n.minX = n.positionX - n.width; | ||
n.maxX = n.positionX + n.width; | ||
n.minY = n.positionY - n.height; | ||
n.maxY = n.positionY + n.height; | ||
// s += " New Position: (" + n.positionX + ", " + n.positionY + ")."; | ||
// logDebug(s); | ||
// Update ancestry boudaries | ||
updateAncestryBoundaries( n, layoutInfo ); | ||
} | ||
// Update size, position of compund nodes | ||
for( var i = 0; i < layoutInfo.nodeSize; i++ ){ | ||
var n = layoutInfo.layoutNodes[ i ]; | ||
if( 0 < n.children.length && !n.isLocked ){ | ||
n.positionX = (n.maxX + n.minX) / 2; | ||
n.positionY = (n.maxY + n.minY) / 2; | ||
n.width = n.maxX - n.minX; | ||
n.height = n.maxY - n.minY; | ||
// s = "Updating position, size of compound node " + n.id; | ||
// s += "\nPositionX: " + n.positionX + ", PositionY: " + n.positionY; | ||
// s += "\nWidth: " + n.width + ", Height: " + n.height; | ||
// logDebug(s); | ||
} | ||
} | ||
}; | ||
/** | ||
* @brief : Limits a force (forceX, forceY) to be not | ||
* greater (in modulo) than max. | ||
8 Preserves force direction. | ||
*/ | ||
var limitForce = function( forceX, forceY, max ){ | ||
// var s = "Limiting force: (" + forceX + ", " + forceY + "). Max: " + max; | ||
var force = Math.sqrt( forceX * forceX + forceY * forceY ); | ||
if( force > max ){ | ||
var res = { | ||
x: max * forceX / force, | ||
y: max * forceY / force | ||
}; | ||
} else { | ||
var res = { | ||
x: forceX, | ||
y: forceY | ||
}; | ||
} | ||
// s += ".\nResult: (" + res.x + ", " + res.y + ")"; | ||
// logDebug(s); | ||
return res; | ||
}; | ||
/** | ||
* @brief : Function used for keeping track of compound node | ||
* sizes, since they should bound all their subnodes. | ||
*/ | ||
var updateAncestryBoundaries = function( node, layoutInfo ){ | ||
// var s = "Propagating new position/size of node " + node.id; | ||
var parentId = node.parentId; | ||
if( null == parentId ){ | ||
// If there's no parent, we are done | ||
// s += ". No parent node."; | ||
// logDebug(s); | ||
return; | ||
} | ||
// Get Parent Node | ||
var p = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ parentId ] ]; | ||
var flag = false; | ||
// MaxX | ||
if( null == p.maxX || node.maxX + p.padRight > p.maxX ){ | ||
p.maxX = node.maxX + p.padRight; | ||
flag = true; | ||
// s += "\nNew maxX for parent node " + p.id + ": " + p.maxX; | ||
} | ||
// MinX | ||
if( null == p.minX || node.minX - p.padLeft < p.minX ){ | ||
p.minX = node.minX - p.padLeft; | ||
flag = true; | ||
// s += "\nNew minX for parent node " + p.id + ": " + p.minX; | ||
} | ||
// MaxY | ||
if( null == p.maxY || node.maxY + p.padBottom > p.maxY ){ | ||
p.maxY = node.maxY + p.padBottom; | ||
flag = true; | ||
// s += "\nNew maxY for parent node " + p.id + ": " + p.maxY; | ||
} | ||
// MinY | ||
if( null == p.minY || node.minY - p.padTop < p.minY ){ | ||
p.minY = node.minY - p.padTop; | ||
flag = true; | ||
// s += "\nNew minY for parent node " + p.id + ": " + p.minY; | ||
} | ||
// If updated boundaries, propagate changes upward | ||
if( flag ){ | ||
// logDebug(s); | ||
return updateAncestryBoundaries( p, layoutInfo ); | ||
} | ||
// s += ". No changes in boundaries/position of parent node " + p.id; | ||
// logDebug(s); | ||
return; | ||
}; | ||
var separateComponents = function( layoutInfo, options ){ | ||
var nodes = layoutInfo.layoutNodes; | ||
var components = []; | ||
for( var i = 0; i < nodes.length; i++ ){ | ||
var node = nodes[ i ]; | ||
var cid = node.cmptId; | ||
var component = components[ cid ] = components[ cid ] || []; | ||
component.push( node ); | ||
} | ||
var totalA = 0; | ||
for( var i = 0; i < components.length; i++ ){ | ||
var c = components[ i ]; | ||
if( !c ){ continue; } | ||
c.x1 = Infinity; | ||
c.x2 = -Infinity; | ||
c.y1 = Infinity; | ||
c.y2 = -Infinity; | ||
for( var j = 0; j < c.length; j++ ){ | ||
var n = c[ j ]; | ||
c.x1 = Math.min( c.x1, n.positionX - n.width / 2 ); | ||
c.x2 = Math.max( c.x2, n.positionX + n.width / 2 ); | ||
c.y1 = Math.min( c.y1, n.positionY - n.height / 2 ); | ||
c.y2 = Math.max( c.y2, n.positionY + n.height / 2 ); | ||
} | ||
c.w = c.x2 - c.x1; | ||
c.h = c.y2 - c.y1; | ||
totalA += c.w * c.h; | ||
} | ||
components.sort( function( c1, c2 ){ | ||
return c2.w * c2.h - c1.w * c1.h; | ||
} ); | ||
var x = 0; | ||
var y = 0; | ||
var usedW = 0; | ||
var rowH = 0; | ||
var maxRowW = Math.sqrt( totalA ) * layoutInfo.clientWidth / layoutInfo.clientHeight; | ||
for( var i = 0; i < components.length; i++ ){ | ||
var c = components[ i ]; | ||
if( !c ){ continue; } | ||
for( var j = 0; j < c.length; j++ ){ | ||
var n = c[ j ]; | ||
if( !n.isLocked ){ | ||
n.positionX += x; | ||
n.positionY += y; | ||
} | ||
} | ||
x += c.w + options.componentSpacing; | ||
usedW += c.w + options.componentSpacing; | ||
rowH = Math.max( rowH, c.h ); | ||
if( usedW > maxRowW ){ | ||
y += rowH + options.componentSpacing; | ||
x = 0; | ||
usedW = 0; | ||
rowH = 0; | ||
} | ||
} | ||
}; | ||
export default CoseLayout; |
@@ -247,2 +247,4 @@ import window from '../../../../window'; | ||
var bb = _p.labelBounds[prefix || 'main']; | ||
var text = ele.pstyle( prefixDash + 'label' ).value; | ||
@@ -254,6 +256,2 @@ var eventsEnabled = ele.pstyle( 'text-events' ).strValue === 'yes'; | ||
var rstyle = _p.rstyle; | ||
var bw = ele.pstyle('text-border-width').pfValue; | ||
var pw = ele.pstyle('text-background-padding').pfValue; | ||
var lw = preprop( rstyle, 'labelWidth', prefix ) + bw + 2*th + 2*pw; | ||
var lh = preprop( rstyle, 'labelHeight', prefix ) + bw + 2*th + 2*pw; | ||
var lx = preprop( rstyle, 'labelX', prefix ); | ||
@@ -264,6 +262,6 @@ var ly = preprop( rstyle, 'labelY', prefix ); | ||
var lx1 = lx - lw / 2; | ||
var lx2 = lx + lw / 2; | ||
var ly1 = ly - lh / 2; | ||
var ly2 = ly + lh / 2; | ||
var lx1 = bb.x1 - th; | ||
var lx2 = bb.x2 + th; | ||
var ly1 = bb.y1 - th; | ||
var ly2 = bb.y2 + th; | ||
@@ -301,11 +299,2 @@ if( theta ){ | ||
} else { // do a cheaper bb check | ||
var bb = { | ||
w: lw, | ||
h: lh, | ||
x1: lx1, | ||
x2: lx2, | ||
y1: ly1, | ||
y2: ly2 | ||
}; | ||
if( math.inBoundingBox( bb, x, y ) ){ | ||
@@ -312,0 +301,0 @@ addEle( ele ); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3760145
102287