First commit.

Signed-off-by: Chen Xiao <abigwc@gmail.com>
This commit is contained in:
Chen Xiao
2026-05-08 14:43:16 +08:00
commit 0b64e2de94
10989 changed files with 2253791 additions and 0 deletions
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;