+9
@@ -0,0 +1,9 @@
|
||||
// Simple, internal Object.assign() polyfill for options objects etc.
|
||||
|
||||
module.exports = Object.assign != null ? Object.assign.bind( Object ) : function( tgt, ...srcs ){
|
||||
srcs.forEach( src => {
|
||||
Object.keys( src ).forEach( k => tgt[k] = src[k] );
|
||||
} );
|
||||
|
||||
return tgt;
|
||||
};
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Auxiliary functions
|
||||
*/
|
||||
|
||||
const LinkedList = require('cose-base').layoutBase.LinkedList;
|
||||
|
||||
let auxiliary = {};
|
||||
|
||||
// get the top most nodes
|
||||
auxiliary.getTopMostNodes = function(nodes) {
|
||||
let nodesMap = {};
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
nodesMap[nodes[i].id()] = true;
|
||||
}
|
||||
let roots = nodes.filter(function (ele, i) {
|
||||
if(typeof ele === "number") {
|
||||
ele = i;
|
||||
}
|
||||
let parent = ele.parent()[0];
|
||||
while(parent != null){
|
||||
if(nodesMap[parent.id()]){
|
||||
return false;
|
||||
}
|
||||
parent = parent.parent()[0];
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return roots;
|
||||
};
|
||||
|
||||
// find disconnected components and create dummy nodes that connect them
|
||||
auxiliary.connectComponents = function(cy, eles, topMostNodes, dummyNodes){
|
||||
let queue = new LinkedList();
|
||||
let visited = new Set();
|
||||
let visitedTopMostNodes = [];
|
||||
let currentNeighbor;
|
||||
let minDegreeNode;
|
||||
let minDegree;
|
||||
|
||||
let isConnected = false;
|
||||
let count = 1;
|
||||
let nodesConnectedToDummy = [];
|
||||
let components = [];
|
||||
|
||||
do{
|
||||
let cmpt = cy.collection();
|
||||
components.push(cmpt);
|
||||
|
||||
let currentNode = topMostNodes[0];
|
||||
let childrenOfCurrentNode = cy.collection();
|
||||
childrenOfCurrentNode.merge(currentNode).merge(currentNode.descendants().intersection(eles));
|
||||
visitedTopMostNodes.push(currentNode);
|
||||
|
||||
childrenOfCurrentNode.forEach(function(node) {
|
||||
queue.push(node);
|
||||
visited.add(node);
|
||||
cmpt.merge(node);
|
||||
});
|
||||
|
||||
while(queue.length != 0){
|
||||
currentNode = queue.shift();
|
||||
|
||||
// Traverse all neighbors of this node
|
||||
let neighborNodes = cy.collection();
|
||||
currentNode.neighborhood().nodes().forEach(function(node){
|
||||
if(eles.intersection(currentNode.edgesWith(node)).length > 0){
|
||||
neighborNodes.merge(node);
|
||||
}
|
||||
});
|
||||
|
||||
for(let i = 0; i < neighborNodes.length; i++){
|
||||
let neighborNode = neighborNodes[i];
|
||||
currentNeighbor = topMostNodes.intersection(neighborNode.union(neighborNode.ancestors()));
|
||||
if(currentNeighbor != null && !visited.has(currentNeighbor[0])){
|
||||
let childrenOfNeighbor = currentNeighbor.union(currentNeighbor.descendants());
|
||||
|
||||
childrenOfNeighbor.forEach(function(node){
|
||||
queue.push(node);
|
||||
visited.add(node);
|
||||
cmpt.merge(node);
|
||||
if(topMostNodes.has(node)){
|
||||
visitedTopMostNodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmpt.forEach(node => {
|
||||
eles.intersection(node.connectedEdges()).forEach(e => { // connectedEdges() usually cached
|
||||
if( cmpt.has(e.source()) && cmpt.has(e.target()) ){ // has() is cheap
|
||||
cmpt.merge(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if(visitedTopMostNodes.length == topMostNodes.length){
|
||||
isConnected = true;
|
||||
}
|
||||
|
||||
if(!isConnected || (isConnected && count > 1)){
|
||||
minDegreeNode = visitedTopMostNodes[0];
|
||||
minDegree = minDegreeNode.connectedEdges().length;
|
||||
visitedTopMostNodes.forEach(function(node){
|
||||
if(node.connectedEdges().length < minDegree){
|
||||
minDegree = node.connectedEdges().length;
|
||||
minDegreeNode = node;
|
||||
}
|
||||
});
|
||||
nodesConnectedToDummy.push(minDegreeNode.id());
|
||||
// TO DO: Check efficiency of this part
|
||||
let temp = cy.collection();
|
||||
temp.merge(visitedTopMostNodes[0]);
|
||||
visitedTopMostNodes.forEach(function(node){
|
||||
temp.merge(node);
|
||||
});
|
||||
visitedTopMostNodes = [];
|
||||
topMostNodes = topMostNodes.difference(temp);
|
||||
count++;
|
||||
}
|
||||
|
||||
}
|
||||
while(!isConnected);
|
||||
|
||||
if(dummyNodes){
|
||||
if(nodesConnectedToDummy.length > 0 ){
|
||||
dummyNodes.set('dummy'+(dummyNodes.size+1), nodesConnectedToDummy);
|
||||
}
|
||||
}
|
||||
return components;
|
||||
};
|
||||
|
||||
// relocates componentResult to originalCenter if there is no fixedNodeConstraint
|
||||
auxiliary.relocateComponent = function(originalCenter, componentResult, options) {
|
||||
if (!options.fixedNodeConstraint) {
|
||||
let minXCoord = Number.POSITIVE_INFINITY;
|
||||
let maxXCoord = Number.NEGATIVE_INFINITY;
|
||||
let minYCoord = Number.POSITIVE_INFINITY;
|
||||
let maxYCoord = Number.NEGATIVE_INFINITY;
|
||||
if (options.quality == "draft") {
|
||||
// calculate current bounding box
|
||||
for (let [key, value] of componentResult.nodeIndexes) {
|
||||
let cyNode = options.cy.getElementById(key);
|
||||
if (cyNode) {
|
||||
let nodeBB = cyNode.boundingBox();
|
||||
let leftX = componentResult.xCoords[value] - nodeBB.w / 2;
|
||||
let rightX = componentResult.xCoords[value] + nodeBB.w / 2;
|
||||
let topY = componentResult.yCoords[value] - nodeBB.h / 2;
|
||||
let bottomY = componentResult.yCoords[value] + nodeBB.h / 2;
|
||||
|
||||
if (leftX < minXCoord)
|
||||
minXCoord = leftX;
|
||||
if (rightX > maxXCoord)
|
||||
maxXCoord = rightX;
|
||||
if (topY < minYCoord)
|
||||
minYCoord = topY;
|
||||
if (bottomY > maxYCoord)
|
||||
maxYCoord = bottomY;
|
||||
}
|
||||
}
|
||||
// find difference between current and original center
|
||||
let diffOnX = originalCenter.x - (maxXCoord + minXCoord) / 2;
|
||||
let diffOnY = originalCenter.y - (maxYCoord + minYCoord) / 2;
|
||||
// move component to original center
|
||||
componentResult.xCoords = componentResult.xCoords.map(x => x + diffOnX);
|
||||
componentResult.yCoords = componentResult.yCoords.map(y => y + diffOnY);
|
||||
}
|
||||
else {
|
||||
// calculate current bounding box
|
||||
Object.keys(componentResult).forEach(function (item) {
|
||||
let node = componentResult[item];
|
||||
let leftX = node.getRect().x;
|
||||
let rightX = node.getRect().x + node.getRect().width;
|
||||
let topY = node.getRect().y;
|
||||
let bottomY = node.getRect().y + node.getRect().height;
|
||||
|
||||
if (leftX < minXCoord)
|
||||
minXCoord = leftX;
|
||||
if (rightX > maxXCoord)
|
||||
maxXCoord = rightX;
|
||||
if (topY < minYCoord)
|
||||
minYCoord = topY;
|
||||
if (bottomY > maxYCoord)
|
||||
maxYCoord = bottomY;
|
||||
});
|
||||
// find difference between current and original center
|
||||
let diffOnX = originalCenter.x - (maxXCoord + minXCoord) / 2;
|
||||
let diffOnY = originalCenter.y - (maxYCoord + minYCoord) / 2;
|
||||
// move component to original center
|
||||
Object.keys(componentResult).forEach(function (item) {
|
||||
let node = componentResult[item];
|
||||
node.setCenter(node.getCenterX() + diffOnX, node.getCenterY() + diffOnY);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auxiliary.calcBoundingBox = function(parentNode, xCoords, yCoords, nodeIndexes){
|
||||
// calculate bounds
|
||||
let left = Number.MAX_SAFE_INTEGER;
|
||||
let right = Number.MIN_SAFE_INTEGER;
|
||||
let top = Number.MAX_SAFE_INTEGER;
|
||||
let bottom = Number.MIN_SAFE_INTEGER;
|
||||
let nodeLeft;
|
||||
let nodeRight;
|
||||
let nodeTop;
|
||||
let nodeBottom;
|
||||
|
||||
let nodes = parentNode.descendants().not(":parent");
|
||||
let s = nodes.length;
|
||||
for (let i = 0; i < s; i++)
|
||||
{
|
||||
let node = nodes[i];
|
||||
|
||||
nodeLeft = xCoords[nodeIndexes.get(node.id())] - node.width()/2;
|
||||
nodeRight = xCoords[nodeIndexes.get(node.id())] + node.width()/2;
|
||||
nodeTop = yCoords[nodeIndexes.get(node.id())] - node.height()/2;
|
||||
nodeBottom = yCoords[nodeIndexes.get(node.id())] + node.height()/2;
|
||||
|
||||
if (left > nodeLeft)
|
||||
{
|
||||
left = nodeLeft;
|
||||
}
|
||||
|
||||
if (right < nodeRight)
|
||||
{
|
||||
right = nodeRight;
|
||||
}
|
||||
|
||||
if (top > nodeTop)
|
||||
{
|
||||
top = nodeTop;
|
||||
}
|
||||
|
||||
if (bottom < nodeBottom)
|
||||
{
|
||||
bottom = nodeBottom;
|
||||
}
|
||||
}
|
||||
|
||||
let boundingBox = {};
|
||||
boundingBox.topLeftX = left;
|
||||
boundingBox.topLeftY = top;
|
||||
boundingBox.width = right - left;
|
||||
boundingBox.height = bottom - top;
|
||||
return boundingBox;
|
||||
};
|
||||
|
||||
// This function finds and returns parent nodes whose all children are hidden
|
||||
auxiliary.calcParentsWithoutChildren = function(cy, eles){
|
||||
let parentsWithoutChildren = cy.collection();
|
||||
eles.nodes(':parent').forEach((parent) => {
|
||||
let check = false;
|
||||
parent.children().forEach((child) => {
|
||||
if(child.css('display') != 'none') {
|
||||
check = true;
|
||||
}
|
||||
});
|
||||
if(!check) {
|
||||
parentsWithoutChildren.merge(parent);
|
||||
}
|
||||
});
|
||||
|
||||
return parentsWithoutChildren;
|
||||
}
|
||||
|
||||
module.exports = auxiliary;
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
The implementation of the postprocessing part that applies CoSE layout over the spectral layout
|
||||
*/
|
||||
|
||||
const aux = require('./auxiliary');
|
||||
const CoSELayout = require('cose-base').CoSELayout;
|
||||
const CoSENode = require('cose-base').CoSENode;
|
||||
const PointD = require('cose-base').layoutBase.PointD;
|
||||
const DimensionD = require('cose-base').layoutBase.DimensionD;
|
||||
const LayoutConstants = require('cose-base').layoutBase.LayoutConstants;
|
||||
const FDLayoutConstants = require('cose-base').layoutBase.FDLayoutConstants;
|
||||
const CoSEConstants = require('cose-base').CoSEConstants;
|
||||
|
||||
// main function that cose layout is processed
|
||||
let coseLayout = function(options, spectralResult){
|
||||
|
||||
let cy = options.cy;
|
||||
let eles = options.eles;
|
||||
let nodes = eles.nodes();
|
||||
let edges = eles.edges();
|
||||
|
||||
let nodeIndexes;
|
||||
let xCoords;
|
||||
let yCoords;
|
||||
let idToLNode = {};
|
||||
|
||||
if(options.randomize){
|
||||
nodeIndexes = spectralResult["nodeIndexes"];
|
||||
xCoords = spectralResult["xCoords"];
|
||||
yCoords = spectralResult["yCoords"];
|
||||
}
|
||||
|
||||
const isFn = fn => typeof fn === 'function';
|
||||
|
||||
const optFn = ( opt, ele ) => {
|
||||
if( isFn( opt ) ){
|
||||
return opt( ele );
|
||||
} else {
|
||||
return opt;
|
||||
}
|
||||
};
|
||||
|
||||
/**** Postprocessing functions ****/
|
||||
|
||||
let parentsWithoutChildren = aux.calcParentsWithoutChildren(cy, eles);
|
||||
|
||||
// transfer cytoscape nodes to cose nodes
|
||||
let processChildrenList = function (parent, children, layout, options) {
|
||||
let size = children.length;
|
||||
for (let i = 0; i < size; i++) {
|
||||
let theChild = children[i];
|
||||
let children_of_children = null;
|
||||
if(theChild.intersection(parentsWithoutChildren).length == 0) {
|
||||
children_of_children = theChild.children();
|
||||
}
|
||||
let theNode;
|
||||
|
||||
let dimensions = theChild.layoutDimensions({
|
||||
nodeDimensionsIncludeLabels: options.nodeDimensionsIncludeLabels
|
||||
});
|
||||
|
||||
if (theChild.outerWidth() != null
|
||||
&& theChild.outerHeight() != null) {
|
||||
if(options.randomize){
|
||||
if(!theChild.isParent()){
|
||||
theNode = parent.add(new CoSENode(layout.graphManager,
|
||||
new PointD(xCoords[nodeIndexes.get(theChild.id())] - dimensions.w / 2, yCoords[nodeIndexes.get(theChild.id())] - dimensions.h / 2),
|
||||
new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h))));
|
||||
}
|
||||
else{
|
||||
let parentInfo = aux.calcBoundingBox(theChild, xCoords, yCoords, nodeIndexes);
|
||||
if(theChild.intersection(parentsWithoutChildren).length == 0) {
|
||||
theNode = parent.add(new CoSENode(layout.graphManager,
|
||||
new PointD(parentInfo.topLeftX, parentInfo.topLeftY),
|
||||
new DimensionD(parentInfo.width, parentInfo.height)));
|
||||
}
|
||||
else { // for the parentsWithoutChildren
|
||||
theNode = parent.add(new CoSENode(layout.graphManager,
|
||||
new PointD(parentInfo.topLeftX, parentInfo.topLeftY),
|
||||
new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h))));
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
theNode = parent.add(new CoSENode(layout.graphManager,
|
||||
new PointD(theChild.position('x') - dimensions.w / 2, theChild.position('y') - dimensions.h / 2),
|
||||
new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h))));
|
||||
}
|
||||
}
|
||||
else {
|
||||
theNode = parent.add(new CoSENode(this.graphManager));
|
||||
}
|
||||
// Attach id to the layout node and repulsion value
|
||||
theNode.id = theChild.data("id");
|
||||
theNode.nodeRepulsion = optFn( options.nodeRepulsion, theChild );
|
||||
// Attach the paddings of cy node to layout node
|
||||
theNode.paddingLeft = parseInt( theChild.css('padding') );
|
||||
theNode.paddingTop = parseInt( theChild.css('padding') );
|
||||
theNode.paddingRight = parseInt( theChild.css('padding') );
|
||||
theNode.paddingBottom = parseInt( theChild.css('padding') );
|
||||
|
||||
//Attach the label properties to both compound and simple nodes if labels will be included in node dimensions
|
||||
//These properties will be used while updating bounds of compounds during iterations or tiling
|
||||
//and will be used for simple nodes while transferring final positions to cytoscape
|
||||
if(options.nodeDimensionsIncludeLabels){
|
||||
theNode.labelWidth = theChild.boundingBox({ includeLabels: true, includeNodes: false, includeOverlays: false }).w;
|
||||
theNode.labelHeight = theChild.boundingBox({ includeLabels: true, includeNodes: false, includeOverlays: false }).h;
|
||||
theNode.labelPosVertical = theChild.css("text-valign");
|
||||
theNode.labelPosHorizontal = theChild.css("text-halign");
|
||||
}
|
||||
|
||||
// Map the layout node
|
||||
idToLNode[theChild.data("id")] = theNode;
|
||||
|
||||
if (isNaN(theNode.rect.x)) {
|
||||
theNode.rect.x = 0;
|
||||
}
|
||||
|
||||
if (isNaN(theNode.rect.y)) {
|
||||
theNode.rect.y = 0;
|
||||
}
|
||||
|
||||
if (children_of_children != null && children_of_children.length > 0) {
|
||||
let theNewGraph;
|
||||
theNewGraph = layout.getGraphManager().add(layout.newGraph(), theNode);
|
||||
processChildrenList(theNewGraph, children_of_children, layout, options);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// transfer cytoscape edges to cose edges
|
||||
let processEdges = function(layout, gm, edges){
|
||||
let idealLengthTotal = 0;
|
||||
let edgeCount = 0;
|
||||
for (let i = 0; i < edges.length; i++) {
|
||||
let edge = edges[i];
|
||||
let sourceNode = idToLNode[edge.data("source")];
|
||||
let targetNode = idToLNode[edge.data("target")];
|
||||
if(sourceNode && targetNode && sourceNode !== targetNode && sourceNode.getEdgesBetween(targetNode).length == 0){
|
||||
let e1 = gm.add(layout.newEdge(), sourceNode, targetNode);
|
||||
e1.id = edge.id();
|
||||
e1.idealLength = optFn( options.idealEdgeLength, edge );
|
||||
e1.edgeElasticity = optFn( options.edgeElasticity, edge );
|
||||
idealLengthTotal += e1.idealLength;
|
||||
edgeCount++;
|
||||
}
|
||||
}
|
||||
// we need to update the ideal edge length constant with the avg. ideal length value after processing edges
|
||||
// in case there is no edge, use other options
|
||||
if (options.idealEdgeLength != null){
|
||||
if (edgeCount > 0)
|
||||
CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = idealLengthTotal / edgeCount;
|
||||
else if(!isFn(options.idealEdgeLength)) // in case there is no edge, but option gives a value to use
|
||||
CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = options.idealEdgeLength;
|
||||
else // in case there is no edge and we cannot get a value from option (because it's a function)
|
||||
CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = 50;
|
||||
// we need to update these constant values based on the ideal edge length constant
|
||||
CoSEConstants.MIN_REPULSION_DIST = FDLayoutConstants.MIN_REPULSION_DIST = FDLayoutConstants.DEFAULT_EDGE_LENGTH / 10.0;
|
||||
CoSEConstants.DEFAULT_RADIAL_SEPARATION = FDLayoutConstants.DEFAULT_EDGE_LENGTH;
|
||||
}
|
||||
};
|
||||
|
||||
// transfer cytoscape constraints to cose layout
|
||||
let processConstraints = function(layout, options){
|
||||
// get nodes to be fixed
|
||||
if(options.fixedNodeConstraint){
|
||||
layout.constraints["fixedNodeConstraint"] = options.fixedNodeConstraint;
|
||||
}
|
||||
// get nodes to be aligned
|
||||
if(options.alignmentConstraint){
|
||||
layout.constraints["alignmentConstraint"] = options.alignmentConstraint;
|
||||
}
|
||||
// get nodes to be relatively placed
|
||||
if(options.relativePlacementConstraint){
|
||||
layout.constraints["relativePlacementConstraint"] = options.relativePlacementConstraint;
|
||||
}
|
||||
};
|
||||
|
||||
/**** Apply postprocessing ****/
|
||||
if (options.nestingFactor != null)
|
||||
CoSEConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = FDLayoutConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = options.nestingFactor;
|
||||
if (options.gravity != null)
|
||||
CoSEConstants.DEFAULT_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_GRAVITY_STRENGTH = options.gravity;
|
||||
if (options.numIter != null)
|
||||
CoSEConstants.MAX_ITERATIONS = FDLayoutConstants.MAX_ITERATIONS = options.numIter;
|
||||
if (options.gravityRange != null)
|
||||
CoSEConstants.DEFAULT_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_GRAVITY_RANGE_FACTOR = options.gravityRange;
|
||||
if(options.gravityCompound != null)
|
||||
CoSEConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = options.gravityCompound;
|
||||
if(options.gravityRangeCompound != null)
|
||||
CoSEConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = options.gravityRangeCompound;
|
||||
if (options.initialEnergyOnIncremental != null)
|
||||
CoSEConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = options.initialEnergyOnIncremental;
|
||||
|
||||
if (options.tilingCompareBy != null)
|
||||
CoSEConstants.TILING_COMPARE_BY = options.tilingCompareBy;
|
||||
|
||||
if(options.quality == 'proof')
|
||||
LayoutConstants.QUALITY = 2;
|
||||
else
|
||||
LayoutConstants.QUALITY = 0;
|
||||
|
||||
CoSEConstants.NODE_DIMENSIONS_INCLUDE_LABELS = FDLayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = LayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = options.nodeDimensionsIncludeLabels;
|
||||
CoSEConstants.DEFAULT_INCREMENTAL = FDLayoutConstants.DEFAULT_INCREMENTAL = LayoutConstants.DEFAULT_INCREMENTAL =
|
||||
!(options.randomize);
|
||||
CoSEConstants.ANIMATE = FDLayoutConstants.ANIMATE = LayoutConstants.ANIMATE = options.animate;
|
||||
CoSEConstants.TILE = options.tile;
|
||||
CoSEConstants.TILING_PADDING_VERTICAL =
|
||||
typeof options.tilingPaddingVertical === 'function' ? options.tilingPaddingVertical.call() : options.tilingPaddingVertical;
|
||||
CoSEConstants.TILING_PADDING_HORIZONTAL =
|
||||
typeof options.tilingPaddingHorizontal === 'function' ? options.tilingPaddingHorizontal.call() : options.tilingPaddingHorizontal;
|
||||
|
||||
CoSEConstants.DEFAULT_INCREMENTAL = FDLayoutConstants.DEFAULT_INCREMENTAL = LayoutConstants.DEFAULT_INCREMENTAL = true;
|
||||
CoSEConstants.PURE_INCREMENTAL = !options.randomize;
|
||||
LayoutConstants.DEFAULT_UNIFORM_LEAF_NODE_SIZES = options.uniformNodeDimensions;
|
||||
|
||||
// This part is for debug/demo purpose
|
||||
if(options.step == "transformed"){
|
||||
CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = true;
|
||||
CoSEConstants.ENFORCE_CONSTRAINTS = false;
|
||||
CoSEConstants.APPLY_LAYOUT = false;
|
||||
}
|
||||
if(options.step == "enforced"){
|
||||
CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = false;
|
||||
CoSEConstants.ENFORCE_CONSTRAINTS = true;
|
||||
CoSEConstants.APPLY_LAYOUT = false;
|
||||
}
|
||||
if(options.step == "cose"){
|
||||
CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = false;
|
||||
CoSEConstants.ENFORCE_CONSTRAINTS = false;
|
||||
CoSEConstants.APPLY_LAYOUT = true;
|
||||
}
|
||||
if(options.step == "all"){
|
||||
if(options.randomize)
|
||||
CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = true;
|
||||
else
|
||||
CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = false;
|
||||
CoSEConstants.ENFORCE_CONSTRAINTS = true;
|
||||
CoSEConstants.APPLY_LAYOUT = true;
|
||||
}
|
||||
|
||||
if(options.fixedNodeConstraint || options.alignmentConstraint || options.relativePlacementConstraint){
|
||||
CoSEConstants.TREE_REDUCTION_ON_INCREMENTAL = false;
|
||||
}
|
||||
else{
|
||||
CoSEConstants.TREE_REDUCTION_ON_INCREMENTAL = true;
|
||||
}
|
||||
|
||||
let coseLayout = new CoSELayout();
|
||||
let gm = coseLayout.newGraphManager();
|
||||
|
||||
processChildrenList(gm.addRoot(), aux.getTopMostNodes(nodes), coseLayout, options);
|
||||
processEdges(coseLayout, gm, edges);
|
||||
processConstraints(coseLayout, options);
|
||||
|
||||
coseLayout.runLayout();
|
||||
|
||||
return idToLNode;
|
||||
};
|
||||
|
||||
module.exports = { coseLayout };
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
The implementation of the fcose layout algorithm
|
||||
*/
|
||||
|
||||
const assign = require('../assign');
|
||||
const aux = require('./auxiliary');
|
||||
const { spectralLayout } = require('./spectral');
|
||||
const { coseLayout } = require('./cose');
|
||||
|
||||
const defaults = Object.freeze({
|
||||
|
||||
// 'draft', 'default' or 'proof'
|
||||
// - 'draft' only applies spectral layout
|
||||
// - 'default' improves the quality with subsequent CoSE layout (fast cooling rate)
|
||||
// - 'proof' improves the quality with subsequent CoSE layout (slow cooling rate)
|
||||
quality: "default",
|
||||
// Use random node positions at beginning of layout
|
||||
// if this is set to false, then quality option must be "proof"
|
||||
randomize: true,
|
||||
// Whether or not to animate the layout
|
||||
animate: true,
|
||||
// Duration of animation in ms, if enabled
|
||||
animationDuration: 1000,
|
||||
// Easing of animation, if enabled
|
||||
animationEasing: undefined,
|
||||
// Fit the viewport to the repositioned nodes
|
||||
fit: true,
|
||||
// Padding around layout
|
||||
padding: 30,
|
||||
// Whether to include labels in node dimensions. Valid in "proof" quality
|
||||
nodeDimensionsIncludeLabels: false,
|
||||
// Whether or not simple nodes (non-compound nodes) are of uniform dimensions
|
||||
uniformNodeDimensions: false,
|
||||
// Whether to pack disconnected components - valid only if randomize: true
|
||||
packComponents: true,
|
||||
// Layout step - all, transformed, enforced, cose - for debug purpose only
|
||||
step: "all",
|
||||
|
||||
/* spectral layout options */
|
||||
|
||||
// False for random, true for greedy
|
||||
samplingType: true,
|
||||
// Sample size to construct distance matrix
|
||||
sampleSize: 25,
|
||||
// Separation amount between nodes
|
||||
nodeSeparation: 75,
|
||||
// Power iteration tolerance
|
||||
piTol: 0.0000001,
|
||||
|
||||
/* CoSE layout options */
|
||||
|
||||
// Node repulsion (non overlapping) multiplier
|
||||
nodeRepulsion: node => 4500,
|
||||
// Ideal edge (non nested) length
|
||||
idealEdgeLength: edge => 50,
|
||||
// Divisor to compute edge forces
|
||||
edgeElasticity: edge => 0.45,
|
||||
// Nesting factor (multiplier) to compute ideal edge length for nested edges
|
||||
nestingFactor: 0.1,
|
||||
// Gravity force (constant)
|
||||
gravity: 0.25,
|
||||
// Maximum number of iterations to perform
|
||||
numIter: 2500,
|
||||
// For enabling tiling
|
||||
tile: true,
|
||||
// The function that specifies the criteria for comparing nodes while sorting them during tiling operation.
|
||||
// Takes the node id as a parameter and the default tiling operation is perfomed when this option is not set.
|
||||
tilingCompareBy: undefined,
|
||||
// Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function)
|
||||
tilingPaddingVertical: 10,
|
||||
// Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function)
|
||||
tilingPaddingHorizontal: 10,
|
||||
// Gravity range (constant) for compounds
|
||||
gravityRangeCompound: 1.5,
|
||||
// Gravity force (constant) for compounds
|
||||
gravityCompound: 1.0,
|
||||
// Gravity range (constant)
|
||||
gravityRange: 3.8,
|
||||
// Initial cooling factor for incremental layout
|
||||
initialEnergyOnIncremental: 0.3,
|
||||
|
||||
/* constraint options */
|
||||
|
||||
// Fix required nodes to predefined positions
|
||||
// [{nodeId: 'n1', position: {x: 100, y: 200}, {...}]
|
||||
fixedNodeConstraint: undefined,
|
||||
// Align required nodes in vertical/horizontal direction
|
||||
// {vertical: [['n1', 'n2')], ['n3', 'n4']], horizontal: ['n2', 'n4']}
|
||||
alignmentConstraint: undefined,
|
||||
// Place two nodes relatively in vertical/horizontal direction
|
||||
// [{top: 'n1', bottom: 'n2', gap: 100}, {left: 'n3', right: 'n4', gap: 75}]
|
||||
relativePlacementConstraint: undefined,
|
||||
|
||||
/* layout event callbacks */
|
||||
ready: () => {}, // on layoutready
|
||||
stop: () => {} // on layoutstop
|
||||
});
|
||||
|
||||
class Layout {
|
||||
constructor( options ){
|
||||
this.options = assign( {}, defaults, options );
|
||||
}
|
||||
|
||||
run(){
|
||||
let layout = this;
|
||||
let options = this.options;
|
||||
let cy = options.cy;
|
||||
let eles = options.eles;
|
||||
|
||||
let spectralResult = [];
|
||||
let xCoords;
|
||||
let yCoords;
|
||||
let coseResult = [];
|
||||
let components;
|
||||
let componentCenters = [];
|
||||
|
||||
// basic validity check for constraint inputs
|
||||
if(options.fixedNodeConstraint && (!Array.isArray(options.fixedNodeConstraint) || options.fixedNodeConstraint.length == 0)){
|
||||
options.fixedNodeConstraint = undefined;
|
||||
}
|
||||
|
||||
if(options.alignmentConstraint){
|
||||
if(options.alignmentConstraint.vertical && (!Array.isArray(options.alignmentConstraint.vertical) || options.alignmentConstraint.vertical.length == 0)){
|
||||
options.alignmentConstraint.vertical = undefined;
|
||||
}
|
||||
if(options.alignmentConstraint.horizontal && (!Array.isArray(options.alignmentConstraint.horizontal) || options.alignmentConstraint.horizontal.length == 0)){
|
||||
options.alignmentConstraint.horizontal = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(options.relativePlacementConstraint && (!Array.isArray(options.relativePlacementConstraint) || options.relativePlacementConstraint.length == 0)){
|
||||
options.relativePlacementConstraint = undefined;
|
||||
}
|
||||
|
||||
// if any constraint exists, set some options
|
||||
let constraintExist = options.fixedNodeConstraint || options.alignmentConstraint || options.relativePlacementConstraint;
|
||||
if(constraintExist){
|
||||
// constraints work with these options
|
||||
options.tile = false;
|
||||
options.packComponents = false;
|
||||
}
|
||||
|
||||
// decide component packing is enabled or not
|
||||
let layUtil;
|
||||
let packingEnabled = false;
|
||||
if(cy.layoutUtilities && options.packComponents){
|
||||
layUtil = cy.layoutUtilities("get");
|
||||
if(!layUtil)
|
||||
layUtil = cy.layoutUtilities();
|
||||
packingEnabled = true;
|
||||
}
|
||||
|
||||
if(eles.nodes().length > 0) {
|
||||
// if packing is not enabled, perform layout on the whole graph
|
||||
if(!packingEnabled){
|
||||
// store component center
|
||||
let boundingBox = options.eles.boundingBox();
|
||||
componentCenters.push({x: boundingBox.x1 + boundingBox.w / 2, y: boundingBox.y1 + boundingBox.h / 2});
|
||||
// apply spectral layout
|
||||
if(options.randomize){
|
||||
let result = spectralLayout(options);
|
||||
spectralResult.push(result);
|
||||
}
|
||||
// apply cose layout as postprocessing
|
||||
if(options.quality == "default" || options.quality == "proof"){
|
||||
coseResult.push(coseLayout(options, spectralResult[0]));
|
||||
aux.relocateComponent(componentCenters[0], coseResult[0], options); // relocate center to original position
|
||||
}
|
||||
else{
|
||||
aux.relocateComponent(componentCenters[0], spectralResult[0], options); // relocate center to original position
|
||||
}
|
||||
}
|
||||
else{ // packing is enabled
|
||||
let topMostNodes = aux.getTopMostNodes(options.eles.nodes());
|
||||
components = aux.connectComponents(cy, options.eles, topMostNodes);
|
||||
// store component centers
|
||||
components.forEach(function(component){
|
||||
let boundingBox = component.boundingBox();
|
||||
componentCenters.push({x: boundingBox.x1 + boundingBox.w / 2, y: boundingBox.y1 + boundingBox.h / 2});
|
||||
});
|
||||
|
||||
//send each component to spectral layout if randomized
|
||||
if(options.randomize){
|
||||
components.forEach(function(component){
|
||||
options.eles = component;
|
||||
spectralResult.push(spectralLayout(options));
|
||||
});
|
||||
}
|
||||
|
||||
if(options.quality == "default" || options.quality == "proof"){
|
||||
let toBeTiledNodes = cy.collection();
|
||||
if(options.tile){ // behave nodes to be tiled as one component
|
||||
let nodeIndexes = new Map();
|
||||
let xCoords = [];
|
||||
let yCoords = [];
|
||||
let count = 0;
|
||||
let tempSpectralResult = {nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords};
|
||||
let indexesToBeDeleted = [];
|
||||
components.forEach(function(component, index){
|
||||
if(component.edges().length == 0){
|
||||
component.nodes().forEach(function(node, i){
|
||||
toBeTiledNodes.merge(component.nodes()[i]);
|
||||
if(!node.isParent()){
|
||||
tempSpectralResult.nodeIndexes.set(component.nodes()[i].id(), count++);
|
||||
tempSpectralResult.xCoords.push(component.nodes()[0].position().x);
|
||||
tempSpectralResult.yCoords.push(component.nodes()[0].position().y);
|
||||
}
|
||||
});
|
||||
indexesToBeDeleted.push(index);
|
||||
}
|
||||
});
|
||||
if(toBeTiledNodes.length > 1){
|
||||
let boundingBox = toBeTiledNodes.boundingBox();
|
||||
componentCenters.push({x: boundingBox.x1 + boundingBox.w / 2, y: boundingBox.y1 + boundingBox.h / 2});
|
||||
components.push(toBeTiledNodes);
|
||||
spectralResult.push(tempSpectralResult);
|
||||
for(let i = indexesToBeDeleted.length-1; i >= 0; i--){
|
||||
components.splice(indexesToBeDeleted[i], 1);
|
||||
spectralResult.splice(indexesToBeDeleted[i], 1);
|
||||
componentCenters.splice(indexesToBeDeleted[i], 1);
|
||||
};
|
||||
}
|
||||
}
|
||||
components.forEach(function(component, index){ // send each component to cose layout
|
||||
options.eles = component;
|
||||
coseResult.push(coseLayout(options, spectralResult[index]));
|
||||
aux.relocateComponent(componentCenters[index], coseResult[index], options); // relocate center to original position
|
||||
});
|
||||
}
|
||||
else {
|
||||
components.forEach(function(component, index){
|
||||
aux.relocateComponent(componentCenters[index], spectralResult[index], options); // relocate center to original position
|
||||
});
|
||||
}
|
||||
|
||||
// packing
|
||||
let componentsEvaluated = new Set();
|
||||
if(components.length > 1){
|
||||
let subgraphs = [];
|
||||
let hiddenEles = eles.filter((ele) => {return ele.css('display') == 'none'});
|
||||
components.forEach(function(component, index){
|
||||
let nodeIndexes;
|
||||
if(options.quality == "draft"){
|
||||
nodeIndexes = spectralResult[index].nodeIndexes;
|
||||
}
|
||||
|
||||
if(component.nodes().not(hiddenEles).length > 0) {
|
||||
let subgraph = {};
|
||||
subgraph.edges = [];
|
||||
subgraph.nodes = [];
|
||||
let nodeIndex;
|
||||
component.nodes().not(hiddenEles).forEach(function (node) {
|
||||
if(options.quality == "draft"){
|
||||
if(!node.isParent()){
|
||||
nodeIndex = nodeIndexes.get(node.id());
|
||||
subgraph.nodes.push({x: spectralResult[index].xCoords[nodeIndex] - node.boundingbox().w/2, y: spectralResult[index].yCoords[nodeIndex] - node.boundingbox().h/2, width: node.boundingbox().w, height: node.boundingbox().h});
|
||||
}
|
||||
else{
|
||||
let parentInfo = aux.calcBoundingBox(node, spectralResult[index].xCoords, spectralResult[index].yCoords, nodeIndexes);
|
||||
subgraph.nodes.push({x: parentInfo.topLeftX, y: parentInfo.topLeftY, width: parentInfo.width, height: parentInfo.height});
|
||||
}
|
||||
}
|
||||
else{
|
||||
if(coseResult[index][node.id()]) {
|
||||
subgraph.nodes.push({x: coseResult[index][node.id()].getLeft(), y: coseResult[index][node.id()].getTop(), width: coseResult[index][node.id()].getWidth(), height: coseResult[index][node.id()].getHeight()});
|
||||
}
|
||||
}
|
||||
});
|
||||
component.edges().forEach(function (edge) {
|
||||
let source = edge.source();
|
||||
let target = edge.target();
|
||||
if(source.css("display") != "none" && target.css("display") != "none") {
|
||||
if(options.quality == "draft"){
|
||||
let sourceNodeIndex = nodeIndexes.get(source.id());
|
||||
let targetNodeIndex = nodeIndexes.get(target.id());
|
||||
let sourceCenter = [];
|
||||
let targetCenter = [];
|
||||
if(source.isParent()){
|
||||
let parentInfo = aux.calcBoundingBox(source, spectralResult[index].xCoords, spectralResult[index].yCoords, nodeIndexes);
|
||||
sourceCenter.push(parentInfo.topLeftX + parentInfo.width / 2);
|
||||
sourceCenter.push(parentInfo.topLeftY + parentInfo.height / 2);
|
||||
}
|
||||
else{
|
||||
sourceCenter.push(spectralResult[index].xCoords[sourceNodeIndex]);
|
||||
sourceCenter.push(spectralResult[index].yCoords[sourceNodeIndex]);
|
||||
}
|
||||
if(target.isParent()){
|
||||
let parentInfo = aux.calcBoundingBox(target, spectralResult[index].xCoords, spectralResult[index].yCoords, nodeIndexes);
|
||||
targetCenter.push(parentInfo.topLeftX + parentInfo.width / 2);
|
||||
targetCenter.push(parentInfo.topLeftY + parentInfo.height / 2);
|
||||
}
|
||||
else{
|
||||
targetCenter.push(spectralResult[index].xCoords[targetNodeIndex]);
|
||||
targetCenter.push(spectralResult[index].yCoords[targetNodeIndex]);
|
||||
}
|
||||
subgraph.edges.push({startX: sourceCenter[0], startY: sourceCenter[1], endX: targetCenter[0], endY: targetCenter[1]});
|
||||
}
|
||||
else{
|
||||
if(coseResult[index][source.id()] && coseResult[index][target.id()]) {
|
||||
subgraph.edges.push({startX: coseResult[index][source.id()].getCenterX(), startY: coseResult[index][source.id()].getCenterY(), endX: coseResult[index][target.id()].getCenterX(), endY: coseResult[index][target.id()].getCenterY()});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if(subgraph.nodes.length > 0) {
|
||||
subgraphs.push(subgraph);
|
||||
componentsEvaluated.add(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
let shiftResult = layUtil.packComponents(subgraphs, options.randomize).shifts;
|
||||
if(options.quality == "draft"){
|
||||
spectralResult.forEach(function(result, index){
|
||||
let newXCoords = result.xCoords.map(x => x + shiftResult[index].dx);
|
||||
let newYCoords = result.yCoords.map(y => y + shiftResult[index].dy);
|
||||
result.xCoords = newXCoords;
|
||||
result.yCoords = newYCoords;
|
||||
});
|
||||
}
|
||||
else{
|
||||
let count = 0;
|
||||
componentsEvaluated.forEach((index) => {
|
||||
Object.keys(coseResult[index]).forEach(function (item) {
|
||||
let nodeRectangle = coseResult[index][item];
|
||||
nodeRectangle.setCenter(nodeRectangle.getCenterX() + shiftResult[count].dx, nodeRectangle.getCenterY() + shiftResult[count].dy);
|
||||
});
|
||||
count++;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get each element's calculated position
|
||||
let getPositions = function(ele, i ){
|
||||
if(options.quality == "default" || options.quality == "proof") {
|
||||
if(typeof ele === "number") {
|
||||
ele = i;
|
||||
}
|
||||
let pos;
|
||||
let node;
|
||||
let theId = ele.data('id');
|
||||
coseResult.forEach(function(result){
|
||||
if (theId in result){
|
||||
pos = {x: result[theId].getRect().getCenterX(), y: result[theId].getRect().getCenterY()};
|
||||
node = result[theId];
|
||||
}
|
||||
});
|
||||
if(options.nodeDimensionsIncludeLabels){
|
||||
if(node.labelWidth){
|
||||
if(node.labelPosHorizontal == "left"){
|
||||
pos.x += node.labelWidth/2;
|
||||
}
|
||||
else if(node.labelPosHorizontal == "right"){
|
||||
pos.x -= node.labelWidth/2;
|
||||
}
|
||||
}
|
||||
if(node.labelHeight){
|
||||
if(node.labelPosVertical == "top"){
|
||||
pos.y += node.labelHeight/2;
|
||||
}
|
||||
else if(node.labelPosVertical == "bottom"){
|
||||
pos.y -= node.labelHeight/2;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(pos == undefined)
|
||||
pos = {x: ele.position("x"), y: ele.position("y")};
|
||||
return {
|
||||
x: pos.x,
|
||||
y: pos.y
|
||||
};
|
||||
}
|
||||
else{
|
||||
let pos;
|
||||
spectralResult.forEach(function(result){
|
||||
let index = result.nodeIndexes.get(ele.id());
|
||||
if(index != undefined){
|
||||
pos = {x: result.xCoords[index], y: result.yCoords[index]};
|
||||
}
|
||||
});
|
||||
if(pos == undefined)
|
||||
pos = {x: ele.position("x"), y: ele.position("y")};
|
||||
return {
|
||||
x: pos.x,
|
||||
y: pos.y
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// quality = "draft" and randomize = false are contradictive so in that case positions don't change
|
||||
if(options.quality == "default" || options.quality == "proof" || options.randomize) {
|
||||
// transfer calculated positions to nodes (positions of only simple nodes are evaluated, compounds are positioned automatically)
|
||||
let parentsWithoutChildren = aux.calcParentsWithoutChildren(cy, eles);
|
||||
let hiddenEles = eles.filter((ele) => {return ele.css('display') == 'none'});
|
||||
options.eles = eles.not(hiddenEles);
|
||||
|
||||
eles.nodes().not(":parent").not(hiddenEles).layoutPositions(layout, options, getPositions);
|
||||
|
||||
if(parentsWithoutChildren.length > 0){
|
||||
parentsWithoutChildren.forEach((ele) => {
|
||||
ele.position(getPositions(ele));
|
||||
});
|
||||
}
|
||||
}
|
||||
else{
|
||||
console.log("If randomize option is set to false, then quality option must be 'default' or 'proof'.");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Layout;
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
The implementation of the spectral layout that is the first part of the fcose layout algorithm
|
||||
*/
|
||||
|
||||
const aux = require('./auxiliary');
|
||||
const Matrix = require('cose-base').layoutBase.Matrix;
|
||||
const SVD = require('cose-base').layoutBase.SVD;
|
||||
|
||||
// main function that spectral layout is processed
|
||||
let spectralLayout = function(options){
|
||||
|
||||
let cy = options.cy;
|
||||
let eles = options.eles;
|
||||
let nodes = eles.nodes();
|
||||
let parentNodes = eles.nodes(":parent");
|
||||
|
||||
let dummyNodes = new Map(); // map to keep dummy nodes and their neighbors
|
||||
let nodeIndexes = new Map(); // map to keep indexes to nodes
|
||||
let parentChildMap = new Map(); // mapping btw. compound and its representative node
|
||||
let allNodesNeighborhood = []; // array to keep neighborhood of all nodes
|
||||
let xCoords = [];
|
||||
let yCoords = [];
|
||||
|
||||
let samplesColumn = []; // sampled vertices
|
||||
let minDistancesColumn = [];
|
||||
let C = []; // column sampling matrix
|
||||
let PHI = []; // intersection of column and row sampling matrices
|
||||
let INV = []; // inverse of PHI
|
||||
|
||||
let firstSample; // the first sampled node
|
||||
let nodeSize;
|
||||
|
||||
const infinity = 100000000;
|
||||
const small = 0.000000001;
|
||||
|
||||
let piTol = options.piTol;
|
||||
let samplingType = options.samplingType; // false for random, true for greedy
|
||||
let nodeSeparation = options.nodeSeparation;
|
||||
let sampleSize;
|
||||
|
||||
/**** Spectral-preprocessing functions ****/
|
||||
|
||||
/**** Spectral layout functions ****/
|
||||
|
||||
// determine which columns to be sampled
|
||||
let randomSampleCR = function() {
|
||||
let sample = 0;
|
||||
let count = 0;
|
||||
let flag = false;
|
||||
|
||||
while(count < sampleSize){
|
||||
sample = Math.floor(Math.random() * nodeSize);
|
||||
|
||||
flag = false;
|
||||
for(let i = 0; i < count; i++){
|
||||
if(samplesColumn[i] == sample){
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flag){
|
||||
samplesColumn[count] = sample;
|
||||
count++;
|
||||
}
|
||||
else{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// takes the index of the node(pivot) to initiate BFS as a parameter
|
||||
let BFS = function(pivot, index, samplingMethod){
|
||||
let path = []; // the front of the path
|
||||
let front = 0; // the back of the path
|
||||
let back = 0;
|
||||
let current = 0;
|
||||
let temp;
|
||||
let distance = [];
|
||||
|
||||
let max_dist = 0; // the furthest node to be returned
|
||||
let max_ind = 1;
|
||||
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
distance[i] = infinity;
|
||||
}
|
||||
|
||||
path[back] = pivot;
|
||||
distance[pivot] = 0;
|
||||
|
||||
while(back >= front){
|
||||
current = path[front++];
|
||||
let neighbors = allNodesNeighborhood[current];
|
||||
for(let i = 0; i < neighbors.length; i++){
|
||||
temp = nodeIndexes.get(neighbors[i]);
|
||||
if(distance[temp] == infinity){
|
||||
distance[temp] = distance[current] + 1;
|
||||
path[++back] = temp;
|
||||
}
|
||||
}
|
||||
C[current][index] = distance[current] * nodeSeparation;
|
||||
}
|
||||
|
||||
if(samplingMethod){
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
if(C[i][index] < minDistancesColumn[i])
|
||||
minDistancesColumn[i] = C[i][index];
|
||||
}
|
||||
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
if(minDistancesColumn[i] > max_dist ){
|
||||
max_dist = minDistancesColumn[i];
|
||||
max_ind = i;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return max_ind;
|
||||
};
|
||||
|
||||
// apply BFS to all nodes or selected samples
|
||||
let allBFS = function(samplingMethod){
|
||||
|
||||
let sample;
|
||||
|
||||
if(!samplingMethod){
|
||||
randomSampleCR();
|
||||
|
||||
// call BFS
|
||||
for(let i = 0; i < sampleSize; i++){
|
||||
BFS(samplesColumn[i], i, samplingMethod, false);
|
||||
}
|
||||
}
|
||||
else{
|
||||
sample = Math.floor(Math.random() * nodeSize);
|
||||
firstSample = sample;
|
||||
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
minDistancesColumn[i] = infinity;
|
||||
}
|
||||
|
||||
for(let i = 0; i < sampleSize; i++){
|
||||
samplesColumn[i] = sample;
|
||||
sample = BFS(sample, i, samplingMethod);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// form the squared distances for C
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
for(let j = 0; j < sampleSize; j++){
|
||||
C[i][j] *= C[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
// form PHI
|
||||
for(let i = 0; i < sampleSize; i++){
|
||||
PHI[i] = [];
|
||||
}
|
||||
|
||||
for(let i = 0; i < sampleSize; i++){
|
||||
for(let j = 0; j < sampleSize; j++){
|
||||
PHI[i][j] = C[samplesColumn[j]][i];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// perform the SVD algorithm and apply a regularization step
|
||||
let sample = function(){
|
||||
|
||||
let SVDResult = SVD.svd(PHI);
|
||||
|
||||
let a_q = SVDResult.S;
|
||||
let a_u = SVDResult.U;
|
||||
let a_v = SVDResult.V;
|
||||
|
||||
let max_s = a_q[0]*a_q[0]*a_q[0];
|
||||
|
||||
let a_Sig = [];
|
||||
|
||||
// regularization
|
||||
for(let i = 0; i < sampleSize; i++){
|
||||
a_Sig[i] = [];
|
||||
for(let j = 0; j < sampleSize; j++){
|
||||
a_Sig[i][j] = 0;
|
||||
if(i == j){
|
||||
a_Sig[i][j] = a_q[i]/(a_q[i]*a_q[i] + max_s/(a_q[i]*a_q[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
INV = Matrix.multMat(Matrix.multMat(a_v, a_Sig), Matrix.transpose(a_u));
|
||||
|
||||
};
|
||||
|
||||
// calculate final coordinates
|
||||
let powerIteration = function(){
|
||||
// two largest eigenvalues
|
||||
let theta1;
|
||||
let theta2;
|
||||
|
||||
// initial guesses for eigenvectors
|
||||
let Y1 = [];
|
||||
let Y2 = [];
|
||||
|
||||
let V1 = [];
|
||||
let V2 = [];
|
||||
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
Y1[i] = Math.random();
|
||||
Y2[i] = Math.random();
|
||||
}
|
||||
|
||||
Y1 = Matrix.normalize(Y1);
|
||||
Y2 = Matrix.normalize(Y2);
|
||||
|
||||
let count = 0;
|
||||
// to keep track of the improvement ratio in power iteration
|
||||
let current = small;
|
||||
let previous = small;
|
||||
|
||||
let temp;
|
||||
|
||||
while(true){
|
||||
count++;
|
||||
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
V1[i] = Y1[i];
|
||||
}
|
||||
|
||||
Y1 = Matrix.multGamma(Matrix.multL(Matrix.multGamma(V1), C, INV));
|
||||
theta1 = Matrix.dotProduct(V1, Y1);
|
||||
Y1 = Matrix.normalize(Y1);
|
||||
|
||||
current = Matrix.dotProduct(V1, Y1);
|
||||
|
||||
temp = Math.abs(current/previous);
|
||||
|
||||
if(temp <= 1 + piTol && temp >= 1){
|
||||
break;
|
||||
}
|
||||
|
||||
previous = current;
|
||||
}
|
||||
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
V1[i] = Y1[i];
|
||||
}
|
||||
|
||||
count = 0;
|
||||
previous = small;
|
||||
while(true){
|
||||
count++;
|
||||
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
V2[i] = Y2[i];
|
||||
}
|
||||
|
||||
V2 = Matrix.minusOp(V2, Matrix.multCons(V1, (Matrix.dotProduct(V1, V2))));
|
||||
Y2 = Matrix.multGamma(Matrix.multL(Matrix.multGamma(V2), C, INV));
|
||||
theta2 = Matrix.dotProduct(V2, Y2);
|
||||
Y2 = Matrix.normalize(Y2);
|
||||
|
||||
current = Matrix.dotProduct(V2, Y2);
|
||||
|
||||
temp = Math.abs(current/previous);
|
||||
|
||||
if(temp <= 1 + piTol && temp >= 1){
|
||||
break;
|
||||
}
|
||||
|
||||
previous = current;
|
||||
}
|
||||
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
V2[i] = Y2[i];
|
||||
}
|
||||
|
||||
// theta1 now contains dominant eigenvalue
|
||||
// theta2 now contains the second-largest eigenvalue
|
||||
// V1 now contains theta1's eigenvector
|
||||
// V2 now contains theta2's eigenvector
|
||||
|
||||
//populate the two vectors
|
||||
xCoords = Matrix.multCons(V1, Math.sqrt(Math.abs(theta1)));
|
||||
yCoords = Matrix.multCons(V2, Math.sqrt(Math.abs(theta2)));
|
||||
|
||||
};
|
||||
|
||||
/**** Preparation for spectral layout (Preprocessing) ****/
|
||||
|
||||
// connect disconnected components (first top level, then inside of each compound node)
|
||||
aux.connectComponents(cy, eles, aux.getTopMostNodes(nodes), dummyNodes);
|
||||
|
||||
parentNodes.forEach(function( ele ){
|
||||
aux.connectComponents(cy, eles, aux.getTopMostNodes(ele.descendants().intersection(eles)), dummyNodes);
|
||||
});
|
||||
|
||||
// assign indexes to nodes (first real, then dummy nodes)
|
||||
let index = 0;
|
||||
for(let i = 0; i < nodes.length; i++){
|
||||
if(!nodes[i].isParent()){
|
||||
nodeIndexes.set(nodes[i].id(), index++);
|
||||
}
|
||||
}
|
||||
|
||||
for (let key of dummyNodes.keys()) {
|
||||
nodeIndexes.set(key, index++);
|
||||
}
|
||||
|
||||
// instantiate the neighborhood matrix
|
||||
for(let i = 0; i < nodeIndexes.size; i++){
|
||||
allNodesNeighborhood[i] = [];
|
||||
}
|
||||
|
||||
// form a parent-child map to keep representative node of each compound node
|
||||
parentNodes.forEach(function( ele ){
|
||||
let children = ele.children().intersection(eles);
|
||||
|
||||
// let random = 0;
|
||||
while(children.nodes(":childless").length == 0){
|
||||
// random = Math.floor(Math.random() * children.nodes().length); // if all children are compound then proceed randomly
|
||||
children = children.nodes()[0].children().intersection(eles);
|
||||
}
|
||||
// select the representative node - we can apply different methods here
|
||||
// random = Math.floor(Math.random() * children.nodes(":childless").length);
|
||||
let index = 0;
|
||||
let min = children.nodes(":childless")[0].connectedEdges().length;
|
||||
children.nodes(":childless").forEach(function(ele2, i){
|
||||
if(ele2.connectedEdges().length < min){
|
||||
min = ele2.connectedEdges().length;
|
||||
index = i;
|
||||
}
|
||||
});
|
||||
parentChildMap.set(ele.id(), children.nodes(":childless")[index].id());
|
||||
});
|
||||
|
||||
// add neighborhood relations (first real, then dummy nodes)
|
||||
nodes.forEach(function( ele ){
|
||||
let eleIndex;
|
||||
|
||||
if(ele.isParent())
|
||||
eleIndex = nodeIndexes.get(parentChildMap.get(ele.id()));
|
||||
else
|
||||
eleIndex = nodeIndexes.get(ele.id());
|
||||
|
||||
ele.neighborhood().nodes().forEach(function(node){
|
||||
if(eles.intersection(ele.edgesWith(node)).length > 0){
|
||||
if(node.isParent())
|
||||
allNodesNeighborhood[eleIndex].push(parentChildMap.get(node.id()));
|
||||
else
|
||||
allNodesNeighborhood[eleIndex].push(node.id());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
for (let key of dummyNodes.keys()) {
|
||||
let eleIndex = nodeIndexes.get(key);
|
||||
let disconnectedId;
|
||||
dummyNodes.get(key).forEach(function(id){
|
||||
if(cy.getElementById(id).isParent())
|
||||
disconnectedId = parentChildMap.get(id);
|
||||
else
|
||||
disconnectedId = id;
|
||||
|
||||
allNodesNeighborhood[eleIndex].push(disconnectedId);
|
||||
allNodesNeighborhood[nodeIndexes.get(disconnectedId)].push(key);
|
||||
});
|
||||
}
|
||||
|
||||
// nodeSize now only considers the size of transformed graph
|
||||
nodeSize = nodeIndexes.size;
|
||||
|
||||
let spectralResult;
|
||||
|
||||
// If number of nodes in transformed graph is 1 or 2, either SVD or powerIteration causes problem
|
||||
// So skip spectral and layout the graph with cose
|
||||
if(nodeSize > 2) {
|
||||
// if # of nodes in transformed graph is smaller than sample size,
|
||||
// then use # of nodes as sample size
|
||||
sampleSize = nodeSize < options.sampleSize ? nodeSize : options.sampleSize;
|
||||
|
||||
// instantiates the partial matrices that will be used in spectral layout
|
||||
for(let i = 0; i < nodeSize; i++){
|
||||
C[i] = [];
|
||||
}
|
||||
for(let i = 0; i < sampleSize; i++){
|
||||
INV[i] = [];
|
||||
}
|
||||
|
||||
/**** Apply spectral layout ****/
|
||||
|
||||
if(options.quality == "draft" || options.step == "all"){
|
||||
allBFS(samplingType);
|
||||
sample();
|
||||
powerIteration();
|
||||
|
||||
spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords };
|
||||
}
|
||||
else{
|
||||
nodeIndexes.forEach(function(value, key){
|
||||
xCoords.push(cy.getElementById(key).position("x"));
|
||||
yCoords.push(cy.getElementById(key).position("y"));
|
||||
});
|
||||
spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords };
|
||||
}
|
||||
return spectralResult;
|
||||
}
|
||||
else {
|
||||
let iterator = nodeIndexes.keys();
|
||||
let firstNode = cy.getElementById(iterator.next().value);
|
||||
let firstNodePos = firstNode.position();
|
||||
let firstNodeWidth = firstNode.outerWidth();
|
||||
xCoords.push(firstNodePos.x);
|
||||
yCoords.push(firstNodePos.y);
|
||||
if(nodeSize == 2){
|
||||
let secondNode = cy.getElementById(iterator.next().value);
|
||||
let secondNodeWidth = secondNode.outerWidth();
|
||||
xCoords.push(firstNodePos.x + firstNodeWidth / 2 + secondNodeWidth / 2 + options.idealEdgeLength);
|
||||
yCoords.push(firstNodePos.y);
|
||||
}
|
||||
|
||||
spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords };
|
||||
return spectralResult;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { spectralLayout };
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
const impl = require('./fcose');
|
||||
|
||||
// registers the extension on a cytoscape lib ref
|
||||
let register = function( cytoscape ){
|
||||
if( !cytoscape ){ return; } // can't register if cytoscape unspecified
|
||||
|
||||
cytoscape( 'layout', 'fcose', impl ); // register with cytoscape.js
|
||||
};
|
||||
|
||||
if( typeof cytoscape !== 'undefined' ){ // expose to global cytoscape (i.e. window.cytoscape)
|
||||
register( cytoscape );
|
||||
}
|
||||
|
||||
module.exports = register;
|
||||
Reference in New Issue
Block a user