16 |
17 | fCoSE layout algorithm combines the speed of spectral layout with the aesthetics of force-directed layout. fCoSE runs up to 2 times as fast as CoSE while achieving similar aesthetics.
18 |
19 |
20 |
21 | Furthermore, fCoSE also supports a fairly rich set of constraint types (i.e., fixed position, vertical/horizontal alignment and relative placement).
22 |
23 |
24 |
25 | You can see constraint support in action in the following videos: [fixed node](https://youtu.be/vRZVlwntzGY), [alignment](https://youtu.be/O5rddJ7DteU), [relative placement](https://youtu.be/Xcm87bT50RA), [hybrid](https://youtu.be/KRAQHmnTvUA), [real life graphs](https://youtu.be/vTPy9G2ALcI). Constraints can also be added [incrementally](https://youtu.be/DTm2WmzwP4k) on a given layout.
26 |
27 | Please cite the following when you use this layout:
28 |
29 | H. Balci and U. Dogrusoz, "[fCoSE: A Fast Compound Graph Layout Algorithm with Constraint Support](https://doi.org/10.1109/TVCG.2021.3095303)," in IEEE Transactions on Visualization and Computer Graphics, 28(12), pp. 4582-4593, 2022.
30 |
31 | U. Dogrusoz, E. Giral, A. Cetintas, A. Civril and E. Demir, "[A Layout Algorithm For Undirected Compound Graphs](http://www.sciencedirect.com/science/article/pii/S0020025508004799)", Information Sciences, 179, pp. 980-994, 2009.
32 |
33 | ## Dependencies
34 |
35 | * Cytoscape.js ^3.2.0
36 | * cose-base ^2.0.0
37 | * cytoscape-layout-utilities.js (optional for packing disconnected components) ^1.0.0
38 |
39 | ## Documentation
40 |
41 | fCoSE supports user-defined placement constraints as well as its full support for compound graphs. These constraints may be defined for simple nodes. Supported constraint types are:
42 |
43 | * **Fixed node constraint:** The user may provide *exact* desired positions for a set of nodes called *fixed nodes*. For example, in order to position node *n1* to *(x: 100, y: 200)* and node *n2* to *(x: 200, y: -300)* as a result of the layout, ```fixedNodeConstraint``` option should be set as follows:
44 |
45 | ```js
46 | fixedNodeConstraint: [{nodeId: 'n1', position: {x: 100, y: 200}},
47 | {nodeId: 'n2', position: {x: 200, y: -300}}],
48 | ```
49 |
50 | * **Alignment constraint:** This constraint aims to align two or more nodes (with respect to their centers) vertically or horizontally. For example, for the vertical alignment of nodes {*n1, n2, n3*} and {*n4, n5*}, and horizontal alignment of nodes {*n2, n4*} as a result of the layout, ```alignmentConstraint``` option should be set as follows:
51 | ```js
52 | alignmentConstraint: {vertical: [['n1', 'n2', 'n3'], ['n4', 'n5']], horizontal: [['n2', 'n4']]},
53 | ```
54 | ***Note:** Alignment constraints in a direction must be given in most compact form. Example: ```['n1', 'n2', 'n3']``` instead of ```['n1', 'n2'], ['n1', 'n3']```.*
55 |
56 | * **Relative placement constraint:** The user may constrain the position of a node relative to another node in either vertical or horizontal direction. For example, in order to position node *n1* to be above of node *n2* by at least 100 pixels and position node *n3* to be on the left of node *n4* by at least 75 pixels as a result of the layout, ```relativePlacementConstraint``` option should be set as follows:
57 |
58 | ```js
59 | relativePlacementConstraint: [{top: 'n1', bottom: 'n2', gap: 100},
60 | {left: 'n3', right: 'n4', gap: 75}],
61 | ```
62 | The `gap` property is optional. If it is omitted, average `idealEdgeLength` is used as the gap value.
63 |
64 | ## Usage instructions
65 |
66 | Download the library:
67 | * via npm: `npm install cytoscape-fcose`,
68 | * via bower: `bower install cytoscape-fcose`, or
69 | * via direct download in the repository (probably from a tag).
70 |
71 | Import the library as appropriate for your project:
72 |
73 | ES import:
74 |
75 | ```js
76 | import cytoscape from 'cytoscape';
77 | import fcose from 'cytoscape-fcose';
78 |
79 | cytoscape.use( fcose );
80 | ```
81 |
82 | CommonJS require:
83 |
84 | ```js
85 | let cytoscape = require('cytoscape');
86 | let fcose = require('cytoscape-fcose');
87 |
88 | cytoscape.use( fcose ); // register extension
89 | ```
90 |
91 | AMD:
92 |
93 | ```js
94 | require(['cytoscape', 'cytoscape-fcose'], function( cytoscape, fcose ){
95 | fcose( cytoscape ); // register extension
96 | });
97 | ```
98 |
99 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. Just add the following files:
100 |
101 | ```
102 |
103 |
104 |
105 | ```
106 |
107 |
108 | ## API
109 |
110 | When calling the layout, e.g. `cy.layout({ name: 'fcose', ... })`, the following options are supported:
111 |
112 | ```js
113 | var defaultOptions = {
114 |
115 | // 'draft', 'default' or 'proof'
116 | // - "draft" only applies spectral layout
117 | // - "default" improves the quality with incremental layout (fast cooling rate)
118 | // - "proof" improves the quality with incremental layout (slow cooling rate)
119 | quality: "default",
120 | // Use random node positions at beginning of layout
121 | // if this is set to false, then quality option must be "proof"
122 | randomize: true,
123 | // Whether or not to animate the layout
124 | animate: true,
125 | // Duration of animation in ms, if enabled
126 | animationDuration: 1000,
127 | // Easing of animation, if enabled
128 | animationEasing: undefined,
129 | // Fit the viewport to the repositioned nodes
130 | fit: true,
131 | // Padding around layout
132 | padding: 30,
133 | // Whether to include labels in node dimensions. Valid in "proof" quality
134 | nodeDimensionsIncludeLabels: false,
135 | // Whether or not simple nodes (non-compound nodes) are of uniform dimensions
136 | uniformNodeDimensions: false,
137 | // Whether to pack disconnected components - cytoscape-layout-utilities extension should be registered and initialized
138 | packComponents: true,
139 | // Layout step - all, transformed, enforced, cose - for debug purpose only
140 | step: "all",
141 |
142 | /* spectral layout options */
143 |
144 | // False for random, true for greedy sampling
145 | samplingType: true,
146 | // Sample size to construct distance matrix
147 | sampleSize: 25,
148 | // Separation amount between nodes
149 | nodeSeparation: 75,
150 | // Power iteration tolerance
151 | piTol: 0.0000001,
152 |
153 | /* incremental layout options */
154 |
155 | // Node repulsion (non overlapping) multiplier
156 | nodeRepulsion: node => 4500,
157 | // Ideal edge (non nested) length
158 | idealEdgeLength: edge => 50,
159 | // Divisor to compute edge forces
160 | edgeElasticity: edge => 0.45,
161 | // Nesting factor (multiplier) to compute ideal edge length for nested edges
162 | nestingFactor: 0.1,
163 | // Maximum number of iterations to perform - this is a suggested value and might be adjusted by the algorithm as required
164 | numIter: 2500,
165 | // For enabling tiling
166 | tile: true,
167 | // The comparison function to be used while sorting nodes during tiling operation.
168 | // Takes the ids of 2 nodes that will be compared as a parameter and the default tiling operation is performed when this option is not set.
169 | // It works similar to ``compareFunction`` parameter of ``Array.prototype.sort()``
170 | // If node1 is less then node2 by some ordering criterion ``tilingCompareBy(nodeId1, nodeId2)`` must return a negative value
171 | // If node1 is greater then node2 by some ordering criterion ``tilingCompareBy(nodeId1, nodeId2)`` must return a positive value
172 | // If node1 is equal to node2 by some ordering criterion ``tilingCompareBy(nodeId1, nodeId2)`` must return 0
173 | tilingCompareBy: undefined,
174 | // Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function)
175 | tilingPaddingVertical: 10,
176 | // Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function)
177 | tilingPaddingHorizontal: 10,
178 | // Gravity force (constant)
179 | gravity: 0.25,
180 | // Gravity range (constant) for compounds
181 | gravityRangeCompound: 1.5,
182 | // Gravity force (constant) for compounds
183 | gravityCompound: 1.0,
184 | // Gravity range (constant)
185 | gravityRange: 3.8,
186 | // Initial cooling factor for incremental layout
187 | initialEnergyOnIncremental: 0.3,
188 |
189 | /* constraint options */
190 |
191 | // Fix desired nodes to predefined positions
192 | // [{nodeId: 'n1', position: {x: 100, y: 200}}, {...}]
193 | fixedNodeConstraint: undefined,
194 | // Align desired nodes in vertical/horizontal direction
195 | // {vertical: [['n1', 'n2'], [...]], horizontal: [['n2', 'n4'], [...]]}
196 | alignmentConstraint: undefined,
197 | // Place two nodes relatively in vertical/horizontal direction
198 | // [{top: 'n1', bottom: 'n2', gap: 100}, {left: 'n3', right: 'n4', gap: 75}, {...}]
199 | relativePlacementConstraint: undefined,
200 |
201 | /* layout event callbacks */
202 | ready: () => {}, // on layoutready
203 | stop: () => {} // on layoutstop
204 | };
205 | ```
206 | To be able to use `packComponents` option, `cytoscape-layout-utilities` extension should also be registered in the application.
207 | Packing related [options](https://github.com/iVis-at-Bilkent/cytoscape.js-layout-utilities#default-options) should be set via `cytoscape-layout-utilities` extension.
208 | If they are not set, fCoSE uses default options.
209 |
210 |
211 | ## Build targets
212 |
213 | * `npm run test` : Run Mocha tests in `./test`
214 | * `npm run build` : Build `./src/**` into `cytoscape-fcose.js`
215 | * `npm run watch` : Automatically build on changes with live reloading (N.b. you must already have an HTTP server running)
216 | * `npm run dev` : Automatically build on changes with live reloading with webpack dev server
217 | * `npm run lint` : Run eslint on the source
218 |
219 | N.b. all builds use babel, so modern ES features can be used in the `src`.
220 |
221 |
222 | ## Publishing instructions
223 |
224 | This project is set up to automatically be published to npm and bower. To publish:
225 |
226 | 1. Build the extension : `npm run build:release`
227 | 1. Commit the build : `git commit -am "Build for release"`
228 | 1. Bump the version number and tag: `npm version major|minor|patch`
229 | 1. Push to origin: `git push && git push --tags`
230 | 1. Publish to npm: `npm publish .`
231 | 1. If publishing to bower for the first time, you'll need to run `bower register cytoscape-fcose https://github.com/iVis-at-Bilkent/cytoscape.js-fcose.git`
232 | 1. [Make a new release](https://github.com/iVis-at-Bilkent/cytoscape.js-fcose/releases/new) for Zenodo.
233 |
234 | ## Team
235 |
236 | * [Hasan Balcı](https://github.com/hasanbalci) and [Ugur Dogrusoz](https://github.com/ugurdogrusoz) of [i-Vis at Bilkent University](http://www.cs.bilkent.edu.tr/~ivis)
237 |
--------------------------------------------------------------------------------
/src/fcose/cose.js:
--------------------------------------------------------------------------------
1 | /**
2 | The implementation of the postprocessing part that applies CoSE layout over the spectral layout
3 | */
4 |
5 | const aux = require('./auxiliary');
6 | const CoSELayout = require('cose-base').CoSELayout;
7 | const CoSENode = require('cose-base').CoSENode;
8 | const PointD = require('cose-base').layoutBase.PointD;
9 | const DimensionD = require('cose-base').layoutBase.DimensionD;
10 | const LayoutConstants = require('cose-base').layoutBase.LayoutConstants;
11 | const FDLayoutConstants = require('cose-base').layoutBase.FDLayoutConstants;
12 | const CoSEConstants = require('cose-base').CoSEConstants;
13 |
14 | // main function that cose layout is processed
15 | let coseLayout = function(options, spectralResult){
16 |
17 | let cy = options.cy;
18 | let eles = options.eles;
19 | let nodes = eles.nodes();
20 | let edges = eles.edges();
21 |
22 | let nodeIndexes;
23 | let xCoords;
24 | let yCoords;
25 | let idToLNode = {};
26 |
27 | if(options.randomize){
28 | nodeIndexes = spectralResult["nodeIndexes"];
29 | xCoords = spectralResult["xCoords"];
30 | yCoords = spectralResult["yCoords"];
31 | }
32 |
33 | const isFn = fn => typeof fn === 'function';
34 |
35 | const optFn = ( opt, ele ) => {
36 | if( isFn( opt ) ){
37 | return opt( ele );
38 | } else {
39 | return opt;
40 | }
41 | };
42 |
43 | /**** Postprocessing functions ****/
44 |
45 | let parentsWithoutChildren = aux.calcParentsWithoutChildren(cy, eles);
46 |
47 | // transfer cytoscape nodes to cose nodes
48 | let processChildrenList = function (parent, children, layout, options) {
49 | let size = children.length;
50 | for (let i = 0; i < size; i++) {
51 | let theChild = children[i];
52 | let children_of_children = null;
53 | if(theChild.intersection(parentsWithoutChildren).length == 0) {
54 | children_of_children = theChild.children();
55 | }
56 | let theNode;
57 |
58 | let dimensions = theChild.layoutDimensions({
59 | nodeDimensionsIncludeLabels: options.nodeDimensionsIncludeLabels
60 | });
61 |
62 | if (theChild.outerWidth() != null
63 | && theChild.outerHeight() != null) {
64 | if(options.randomize){
65 | if(!theChild.isParent()){
66 | theNode = parent.add(new CoSENode(layout.graphManager,
67 | new PointD(xCoords[nodeIndexes.get(theChild.id())] - dimensions.w / 2, yCoords[nodeIndexes.get(theChild.id())] - dimensions.h / 2),
68 | new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h))));
69 | }
70 | else{
71 | let parentInfo = aux.calcBoundingBox(theChild, xCoords, yCoords, nodeIndexes);
72 | if(theChild.intersection(parentsWithoutChildren).length == 0) {
73 | theNode = parent.add(new CoSENode(layout.graphManager,
74 | new PointD(parentInfo.topLeftX, parentInfo.topLeftY),
75 | new DimensionD(parentInfo.width, parentInfo.height)));
76 | }
77 | else { // for the parentsWithoutChildren
78 | theNode = parent.add(new CoSENode(layout.graphManager,
79 | new PointD(parentInfo.topLeftX, parentInfo.topLeftY),
80 | new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h))));
81 | }
82 | }
83 | }
84 | else{
85 | theNode = parent.add(new CoSENode(layout.graphManager,
86 | new PointD(theChild.position('x') - dimensions.w / 2, theChild.position('y') - dimensions.h / 2),
87 | new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h))));
88 | }
89 | }
90 | else {
91 | theNode = parent.add(new CoSENode(this.graphManager));
92 | }
93 | // Attach id to the layout node and repulsion value
94 | theNode.id = theChild.data("id");
95 | theNode.nodeRepulsion = optFn( options.nodeRepulsion, theChild );
96 | // Attach the paddings of cy node to layout node
97 | theNode.paddingLeft = parseInt( theChild.css('padding') );
98 | theNode.paddingTop = parseInt( theChild.css('padding') );
99 | theNode.paddingRight = parseInt( theChild.css('padding') );
100 | theNode.paddingBottom = parseInt( theChild.css('padding') );
101 |
102 | //Attach the label properties to both compound and simple nodes if labels will be included in node dimensions
103 | //These properties will be used while updating bounds of compounds during iterations or tiling
104 | //and will be used for simple nodes while transferring final positions to cytoscape
105 | if(options.nodeDimensionsIncludeLabels){
106 | theNode.labelWidth = theChild.boundingBox({ includeLabels: true, includeNodes: false, includeOverlays: false }).w;
107 | theNode.labelHeight = theChild.boundingBox({ includeLabels: true, includeNodes: false, includeOverlays: false }).h;
108 | theNode.labelPosVertical = theChild.css("text-valign");
109 | theNode.labelPosHorizontal = theChild.css("text-halign");
110 | }
111 |
112 | // Map the layout node
113 | idToLNode[theChild.data("id")] = theNode;
114 |
115 | if (isNaN(theNode.rect.x)) {
116 | theNode.rect.x = 0;
117 | }
118 |
119 | if (isNaN(theNode.rect.y)) {
120 | theNode.rect.y = 0;
121 | }
122 |
123 | if (children_of_children != null && children_of_children.length > 0) {
124 | let theNewGraph;
125 | theNewGraph = layout.getGraphManager().add(layout.newGraph(), theNode);
126 | processChildrenList(theNewGraph, children_of_children, layout, options);
127 | }
128 | }
129 | };
130 |
131 | // transfer cytoscape edges to cose edges
132 | let processEdges = function(layout, gm, edges){
133 | let idealLengthTotal = 0;
134 | let edgeCount = 0;
135 | for (let i = 0; i < edges.length; i++) {
136 | let edge = edges[i];
137 | let sourceNode = idToLNode[edge.data("source")];
138 | let targetNode = idToLNode[edge.data("target")];
139 | if(sourceNode && targetNode && sourceNode !== targetNode && sourceNode.getEdgesBetween(targetNode).length == 0){
140 | let e1 = gm.add(layout.newEdge(), sourceNode, targetNode);
141 | e1.id = edge.id();
142 | e1.idealLength = optFn( options.idealEdgeLength, edge );
143 | e1.edgeElasticity = optFn( options.edgeElasticity, edge );
144 | idealLengthTotal += e1.idealLength;
145 | edgeCount++;
146 | }
147 | }
148 | // we need to update the ideal edge length constant with the avg. ideal length value after processing edges
149 | // in case there is no edge, use other options
150 | if (options.idealEdgeLength != null){
151 | if (edgeCount > 0)
152 | CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = idealLengthTotal / edgeCount;
153 | else if(!isFn(options.idealEdgeLength)) // in case there is no edge, but option gives a value to use
154 | CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = options.idealEdgeLength;
155 | else // in case there is no edge and we cannot get a value from option (because it's a function)
156 | CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = 50;
157 | // we need to update these constant values based on the ideal edge length constant
158 | CoSEConstants.MIN_REPULSION_DIST = FDLayoutConstants.MIN_REPULSION_DIST = FDLayoutConstants.DEFAULT_EDGE_LENGTH / 10.0;
159 | CoSEConstants.DEFAULT_RADIAL_SEPARATION = FDLayoutConstants.DEFAULT_EDGE_LENGTH;
160 | }
161 | };
162 |
163 | // transfer cytoscape constraints to cose layout
164 | let processConstraints = function(layout, options){
165 | // get nodes to be fixed
166 | if(options.fixedNodeConstraint){
167 | layout.constraints["fixedNodeConstraint"] = options.fixedNodeConstraint;
168 | }
169 | // get nodes to be aligned
170 | if(options.alignmentConstraint){
171 | layout.constraints["alignmentConstraint"] = options.alignmentConstraint;
172 | }
173 | // get nodes to be relatively placed
174 | if(options.relativePlacementConstraint){
175 | layout.constraints["relativePlacementConstraint"] = options.relativePlacementConstraint;
176 | }
177 | };
178 |
179 | /**** Apply postprocessing ****/
180 | if (options.nestingFactor != null)
181 | CoSEConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = FDLayoutConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = options.nestingFactor;
182 | if (options.gravity != null)
183 | CoSEConstants.DEFAULT_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_GRAVITY_STRENGTH = options.gravity;
184 | if (options.numIter != null)
185 | CoSEConstants.MAX_ITERATIONS = FDLayoutConstants.MAX_ITERATIONS = options.numIter;
186 | if (options.gravityRange != null)
187 | CoSEConstants.DEFAULT_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_GRAVITY_RANGE_FACTOR = options.gravityRange;
188 | if(options.gravityCompound != null)
189 | CoSEConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = options.gravityCompound;
190 | if(options.gravityRangeCompound != null)
191 | CoSEConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = options.gravityRangeCompound;
192 | if (options.initialEnergyOnIncremental != null)
193 | CoSEConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = options.initialEnergyOnIncremental;
194 |
195 | if (options.tilingCompareBy != null)
196 | CoSEConstants.TILING_COMPARE_BY = options.tilingCompareBy;
197 |
198 | if(options.quality == 'proof')
199 | LayoutConstants.QUALITY = 2;
200 | else
201 | LayoutConstants.QUALITY = 0;
202 |
203 | CoSEConstants.NODE_DIMENSIONS_INCLUDE_LABELS = FDLayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = LayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = options.nodeDimensionsIncludeLabels;
204 | CoSEConstants.DEFAULT_INCREMENTAL = FDLayoutConstants.DEFAULT_INCREMENTAL = LayoutConstants.DEFAULT_INCREMENTAL =
205 | !(options.randomize);
206 | CoSEConstants.ANIMATE = FDLayoutConstants.ANIMATE = LayoutConstants.ANIMATE = options.animate;
207 | CoSEConstants.TILE = options.tile;
208 | CoSEConstants.TILING_PADDING_VERTICAL =
209 | typeof options.tilingPaddingVertical === 'function' ? options.tilingPaddingVertical.call() : options.tilingPaddingVertical;
210 | CoSEConstants.TILING_PADDING_HORIZONTAL =
211 | typeof options.tilingPaddingHorizontal === 'function' ? options.tilingPaddingHorizontal.call() : options.tilingPaddingHorizontal;
212 |
213 | CoSEConstants.DEFAULT_INCREMENTAL = FDLayoutConstants.DEFAULT_INCREMENTAL = LayoutConstants.DEFAULT_INCREMENTAL = true;
214 | CoSEConstants.PURE_INCREMENTAL = !options.randomize;
215 | LayoutConstants.DEFAULT_UNIFORM_LEAF_NODE_SIZES = options.uniformNodeDimensions;
216 |
217 | // This part is for debug/demo purpose
218 | if(options.step == "transformed"){
219 | CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = true;
220 | CoSEConstants.ENFORCE_CONSTRAINTS = false;
221 | CoSEConstants.APPLY_LAYOUT = false;
222 | }
223 | if(options.step == "enforced"){
224 | CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = false;
225 | CoSEConstants.ENFORCE_CONSTRAINTS = true;
226 | CoSEConstants.APPLY_LAYOUT = false;
227 | }
228 | if(options.step == "cose"){
229 | CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = false;
230 | CoSEConstants.ENFORCE_CONSTRAINTS = false;
231 | CoSEConstants.APPLY_LAYOUT = true;
232 | }
233 | if(options.step == "all"){
234 | if(options.randomize)
235 | CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = true;
236 | else
237 | CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = false;
238 | CoSEConstants.ENFORCE_CONSTRAINTS = true;
239 | CoSEConstants.APPLY_LAYOUT = true;
240 | }
241 |
242 | if(options.fixedNodeConstraint || options.alignmentConstraint || options.relativePlacementConstraint){
243 | CoSEConstants.TREE_REDUCTION_ON_INCREMENTAL = false;
244 | }
245 | else{
246 | CoSEConstants.TREE_REDUCTION_ON_INCREMENTAL = true;
247 | }
248 |
249 | let coseLayout = new CoSELayout();
250 | let gm = coseLayout.newGraphManager();
251 |
252 | processChildrenList(gm.addRoot(), aux.getTopMostNodes(nodes), coseLayout, options);
253 | processEdges(coseLayout, gm, edges);
254 | processConstraints(coseLayout, options);
255 |
256 | coseLayout.runLayout();
257 |
258 | return idToLNode;
259 | };
260 |
261 | module.exports = { coseLayout };
262 |
--------------------------------------------------------------------------------
/src/fcose/spectral.js:
--------------------------------------------------------------------------------
1 | /**
2 | The implementation of the spectral layout that is the first part of the fcose layout algorithm
3 | */
4 |
5 | const aux = require('./auxiliary');
6 | const Matrix = require('cose-base').layoutBase.Matrix;
7 | const SVD = require('cose-base').layoutBase.SVD;
8 |
9 | // main function that spectral layout is processed
10 | let spectralLayout = function(options){
11 |
12 | let cy = options.cy;
13 | let eles = options.eles;
14 | let nodes = eles.nodes();
15 | let parentNodes = eles.nodes(":parent");
16 |
17 | let dummyNodes = new Map(); // map to keep dummy nodes and their neighbors
18 | let nodeIndexes = new Map(); // map to keep indexes to nodes
19 | let parentChildMap = new Map(); // mapping btw. compound and its representative node
20 | let allNodesNeighborhood = []; // array to keep neighborhood of all nodes
21 | let xCoords = [];
22 | let yCoords = [];
23 |
24 | let samplesColumn = []; // sampled vertices
25 | let minDistancesColumn = [];
26 | let C = []; // column sampling matrix
27 | let PHI = []; // intersection of column and row sampling matrices
28 | let INV = []; // inverse of PHI
29 |
30 | let firstSample; // the first sampled node
31 | let nodeSize;
32 |
33 | const infinity = 100000000;
34 | const small = 0.000000001;
35 |
36 | let piTol = options.piTol;
37 | let samplingType = options.samplingType; // false for random, true for greedy
38 | let nodeSeparation = options.nodeSeparation;
39 | let sampleSize;
40 |
41 | /**** Spectral-preprocessing functions ****/
42 |
43 | /**** Spectral layout functions ****/
44 |
45 | // determine which columns to be sampled
46 | let randomSampleCR = function() {
47 | let sample = 0;
48 | let count = 0;
49 | let flag = false;
50 |
51 | while(count < sampleSize){
52 | sample = Math.floor(Math.random() * nodeSize);
53 |
54 | flag = false;
55 | for(let i = 0; i < count; i++){
56 | if(samplesColumn[i] == sample){
57 | flag = true;
58 | break;
59 | }
60 | }
61 |
62 | if(!flag){
63 | samplesColumn[count] = sample;
64 | count++;
65 | }
66 | else{
67 | continue;
68 | }
69 | }
70 | };
71 |
72 | // takes the index of the node(pivot) to initiate BFS as a parameter
73 | let BFS = function(pivot, index, samplingMethod){
74 | let path = []; // the front of the path
75 | let front = 0; // the back of the path
76 | let back = 0;
77 | let current = 0;
78 | let temp;
79 | let distance = [];
80 |
81 | let max_dist = 0; // the furthest node to be returned
82 | let max_ind = 1;
83 |
84 | for(let i = 0; i < nodeSize; i++){
85 | distance[i] = infinity;
86 | }
87 |
88 | path[back] = pivot;
89 | distance[pivot] = 0;
90 |
91 | while(back >= front){
92 | current = path[front++];
93 | let neighbors = allNodesNeighborhood[current];
94 | for(let i = 0; i < neighbors.length; i++){
95 | temp = nodeIndexes.get(neighbors[i]);
96 | if(distance[temp] == infinity){
97 | distance[temp] = distance[current] + 1;
98 | path[++back] = temp;
99 | }
100 | }
101 | C[current][index] = distance[current] * nodeSeparation;
102 | }
103 |
104 | if(samplingMethod){
105 | for(let i = 0; i < nodeSize; i++){
106 | if(C[i][index] < minDistancesColumn[i])
107 | minDistancesColumn[i] = C[i][index];
108 | }
109 |
110 | for(let i = 0; i < nodeSize; i++){
111 | if(minDistancesColumn[i] > max_dist ){
112 | max_dist = minDistancesColumn[i];
113 | max_ind = i;
114 |
115 | }
116 | }
117 | }
118 | return max_ind;
119 | };
120 |
121 | // apply BFS to all nodes or selected samples
122 | let allBFS = function(samplingMethod){
123 |
124 | let sample;
125 |
126 | if(!samplingMethod){
127 | randomSampleCR();
128 |
129 | // call BFS
130 | for(let i = 0; i < sampleSize; i++){
131 | BFS(samplesColumn[i], i, samplingMethod, false);
132 | }
133 | }
134 | else{
135 | sample = Math.floor(Math.random() * nodeSize);
136 | firstSample = sample;
137 |
138 | for(let i = 0; i < nodeSize; i++){
139 | minDistancesColumn[i] = infinity;
140 | }
141 |
142 | for(let i = 0; i < sampleSize; i++){
143 | samplesColumn[i] = sample;
144 | sample = BFS(sample, i, samplingMethod);
145 | }
146 |
147 | }
148 |
149 | // form the squared distances for C
150 | for(let i = 0; i < nodeSize; i++){
151 | for(let j = 0; j < sampleSize; j++){
152 | C[i][j] *= C[i][j];
153 | }
154 | }
155 |
156 | // form PHI
157 | for(let i = 0; i < sampleSize; i++){
158 | PHI[i] = [];
159 | }
160 |
161 | for(let i = 0; i < sampleSize; i++){
162 | for(let j = 0; j < sampleSize; j++){
163 | PHI[i][j] = C[samplesColumn[j]][i];
164 | }
165 | }
166 | };
167 |
168 | // perform the SVD algorithm and apply a regularization step
169 | let sample = function(){
170 |
171 | let SVDResult = SVD.svd(PHI);
172 |
173 | let a_q = SVDResult.S;
174 | let a_u = SVDResult.U;
175 | let a_v = SVDResult.V;
176 |
177 | let max_s = a_q[0]*a_q[0]*a_q[0];
178 |
179 | let a_Sig = [];
180 |
181 | // regularization
182 | for(let i = 0; i < sampleSize; i++){
183 | a_Sig[i] = [];
184 | for(let j = 0; j < sampleSize; j++){
185 | a_Sig[i][j] = 0;
186 | if(i == j){
187 | a_Sig[i][j] = a_q[i]/(a_q[i]*a_q[i] + max_s/(a_q[i]*a_q[i]));
188 | }
189 | }
190 | }
191 |
192 | INV = Matrix.multMat(Matrix.multMat(a_v, a_Sig), Matrix.transpose(a_u));
193 |
194 | };
195 |
196 | // calculate final coordinates
197 | let powerIteration = function(){
198 | // two largest eigenvalues
199 | let theta1;
200 | let theta2;
201 |
202 | // initial guesses for eigenvectors
203 | let Y1 = [];
204 | let Y2 = [];
205 |
206 | let V1 = [];
207 | let V2 = [];
208 |
209 | for(let i = 0; i < nodeSize; i++){
210 | Y1[i] = Math.random();
211 | Y2[i] = Math.random();
212 | }
213 |
214 | Y1 = Matrix.normalize(Y1);
215 | Y2 = Matrix.normalize(Y2);
216 |
217 | let count = 0;
218 | // to keep track of the improvement ratio in power iteration
219 | let current = small;
220 | let previous = small;
221 |
222 | let temp;
223 |
224 | while(true){
225 | count++;
226 |
227 | for(let i = 0; i < nodeSize; i++){
228 | V1[i] = Y1[i];
229 | }
230 |
231 | Y1 = Matrix.multGamma(Matrix.multL(Matrix.multGamma(V1), C, INV));
232 | theta1 = Matrix.dotProduct(V1, Y1);
233 | Y1 = Matrix.normalize(Y1);
234 |
235 | current = Matrix.dotProduct(V1, Y1);
236 |
237 | temp = Math.abs(current/previous);
238 |
239 | if(temp <= 1 + piTol && temp >= 1){
240 | break;
241 | }
242 |
243 | previous = current;
244 | }
245 |
246 | for(let i = 0; i < nodeSize; i++){
247 | V1[i] = Y1[i];
248 | }
249 |
250 | count = 0;
251 | previous = small;
252 | while(true){
253 | count++;
254 |
255 | for(let i = 0; i < nodeSize; i++){
256 | V2[i] = Y2[i];
257 | }
258 |
259 | V2 = Matrix.minusOp(V2, Matrix.multCons(V1, (Matrix.dotProduct(V1, V2))));
260 | Y2 = Matrix.multGamma(Matrix.multL(Matrix.multGamma(V2), C, INV));
261 | theta2 = Matrix.dotProduct(V2, Y2);
262 | Y2 = Matrix.normalize(Y2);
263 |
264 | current = Matrix.dotProduct(V2, Y2);
265 |
266 | temp = Math.abs(current/previous);
267 |
268 | if(temp <= 1 + piTol && temp >= 1){
269 | break;
270 | }
271 |
272 | previous = current;
273 | }
274 |
275 | for(let i = 0; i < nodeSize; i++){
276 | V2[i] = Y2[i];
277 | }
278 |
279 | // theta1 now contains dominant eigenvalue
280 | // theta2 now contains the second-largest eigenvalue
281 | // V1 now contains theta1's eigenvector
282 | // V2 now contains theta2's eigenvector
283 |
284 | //populate the two vectors
285 | xCoords = Matrix.multCons(V1, Math.sqrt(Math.abs(theta1)));
286 | yCoords = Matrix.multCons(V2, Math.sqrt(Math.abs(theta2)));
287 |
288 | };
289 |
290 | /**** Preparation for spectral layout (Preprocessing) ****/
291 |
292 | // connect disconnected components (first top level, then inside of each compound node)
293 | aux.connectComponents(cy, eles, aux.getTopMostNodes(nodes), dummyNodes);
294 |
295 | parentNodes.forEach(function( ele ){
296 | aux.connectComponents(cy, eles, aux.getTopMostNodes(ele.descendants().intersection(eles)), dummyNodes);
297 | });
298 |
299 | // assign indexes to nodes (first real, then dummy nodes)
300 | let index = 0;
301 | for(let i = 0; i < nodes.length; i++){
302 | if(!nodes[i].isParent()){
303 | nodeIndexes.set(nodes[i].id(), index++);
304 | }
305 | }
306 |
307 | for (let key of dummyNodes.keys()) {
308 | nodeIndexes.set(key, index++);
309 | }
310 |
311 | // instantiate the neighborhood matrix
312 | for(let i = 0; i < nodeIndexes.size; i++){
313 | allNodesNeighborhood[i] = [];
314 | }
315 |
316 | // form a parent-child map to keep representative node of each compound node
317 | parentNodes.forEach(function( ele ){
318 | let children = ele.children().intersection(eles);
319 |
320 | // let random = 0;
321 | while(children.nodes(":childless").length == 0){
322 | // random = Math.floor(Math.random() * children.nodes().length); // if all children are compound then proceed randomly
323 | children = children.nodes()[0].children().intersection(eles);
324 | }
325 | // select the representative node - we can apply different methods here
326 | // random = Math.floor(Math.random() * children.nodes(":childless").length);
327 | let index = 0;
328 | let min = children.nodes(":childless")[0].connectedEdges().length;
329 | children.nodes(":childless").forEach(function(ele2, i){
330 | if(ele2.connectedEdges().length < min){
331 | min = ele2.connectedEdges().length;
332 | index = i;
333 | }
334 | });
335 | parentChildMap.set(ele.id(), children.nodes(":childless")[index].id());
336 | });
337 |
338 | // add neighborhood relations (first real, then dummy nodes)
339 | nodes.forEach(function( ele ){
340 | let eleIndex;
341 |
342 | if(ele.isParent())
343 | eleIndex = nodeIndexes.get(parentChildMap.get(ele.id()));
344 | else
345 | eleIndex = nodeIndexes.get(ele.id());
346 |
347 | ele.neighborhood().nodes().forEach(function(node){
348 | if(eles.intersection(ele.edgesWith(node)).length > 0){
349 | if(node.isParent())
350 | allNodesNeighborhood[eleIndex].push(parentChildMap.get(node.id()));
351 | else
352 | allNodesNeighborhood[eleIndex].push(node.id());
353 | }
354 | });
355 | });
356 |
357 | for (let key of dummyNodes.keys()) {
358 | let eleIndex = nodeIndexes.get(key);
359 | let disconnectedId;
360 | dummyNodes.get(key).forEach(function(id){
361 | if(cy.getElementById(id).isParent())
362 | disconnectedId = parentChildMap.get(id);
363 | else
364 | disconnectedId = id;
365 |
366 | allNodesNeighborhood[eleIndex].push(disconnectedId);
367 | allNodesNeighborhood[nodeIndexes.get(disconnectedId)].push(key);
368 | });
369 | }
370 |
371 | // nodeSize now only considers the size of transformed graph
372 | nodeSize = nodeIndexes.size;
373 |
374 | let spectralResult;
375 |
376 | // If number of nodes in transformed graph is 1 or 2, either SVD or powerIteration causes problem
377 | // So skip spectral and layout the graph with cose
378 | if(nodeSize > 2) {
379 | // if # of nodes in transformed graph is smaller than sample size,
380 | // then use # of nodes as sample size
381 | sampleSize = nodeSize < options.sampleSize ? nodeSize : options.sampleSize;
382 |
383 | // instantiates the partial matrices that will be used in spectral layout
384 | for(let i = 0; i < nodeSize; i++){
385 | C[i] = [];
386 | }
387 | for(let i = 0; i < sampleSize; i++){
388 | INV[i] = [];
389 | }
390 |
391 | /**** Apply spectral layout ****/
392 |
393 | if(options.quality == "draft" || options.step == "all"){
394 | allBFS(samplingType);
395 | sample();
396 | powerIteration();
397 |
398 | spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords };
399 | }
400 | else{
401 | nodeIndexes.forEach(function(value, key){
402 | xCoords.push(cy.getElementById(key).position("x"));
403 | yCoords.push(cy.getElementById(key).position("y"));
404 | });
405 | spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords };
406 | }
407 | return spectralResult;
408 | }
409 | else {
410 | let iterator = nodeIndexes.keys();
411 | let firstNode = cy.getElementById(iterator.next().value);
412 | let firstNodePos = firstNode.position();
413 | let firstNodeWidth = firstNode.outerWidth();
414 | xCoords.push(firstNodePos.x);
415 | yCoords.push(firstNodePos.y);
416 | if(nodeSize == 2){
417 | let secondNode = cy.getElementById(iterator.next().value);
418 | let secondNodeWidth = secondNode.outerWidth();
419 | xCoords.push(firstNodePos.x + firstNodeWidth / 2 + secondNodeWidth / 2 + options.idealEdgeLength);
420 | yCoords.push(firstNodePos.y);
421 | }
422 |
423 | spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords };
424 | return spectralResult;
425 | }
426 | };
427 |
428 | module.exports = { spectralLayout };
--------------------------------------------------------------------------------
/demo/demo-constraint.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | cytoscape-fcose.js demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
187 |
188 |
189 |