├── .gitignore ├── .npmignore ├── README.md ├── bower.json ├── cytoscape-hierarchical.js ├── demo-img.png ├── demo.html ├── gulpfile.js ├── package.json └── test ├── hierarchical-tests.js └── run-tests.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cytoscape-hierarchical 2 | ================================================================================ 3 | 4 |  5 | 6 | ## Description 7 | 8 | A hierarchical clustering algorithm for Cytoscape.js. 9 | 10 | ## Dependencies 11 | 12 | * Cytoscape.js >= 2.6.12 13 | 14 | 15 | ## Usage instructions 16 | 17 | Download the library: 18 | * via npm: `npm install cytoscape-hierarchical`, 19 | * via bower: `bower install cytoscape-hierarchical`, or 20 | * via direct download in the repository. 21 | 22 | `require()` the library as appropriate for your project: 23 | 24 | CommonJS: 25 | ```js 26 | var cytoscape = require('cytoscape'); 27 | var hierarchical = require('cytoscape-hierarchical'); 28 | 29 | hierarchical( cytoscape ); // register extension 30 | ``` 31 | 32 | AMD: 33 | ```js 34 | require(['cytoscape', 'cytoscape-hierarchical'], function( cytoscape, hierarchical ){ 35 | hierarchical( cytoscape ); // register extension 36 | }); 37 | ``` 38 | 39 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. 40 | 41 | 42 | ## API 43 | 44 | #### Mode 1: "regular" 45 | Under the ```regular``` mode, the algorithm returns an array of clusters generated from the data set. 46 | One may set the ```threshold``` option to specify a stopping point for the algorithm. 47 | When every cluster is more than ```threshold``` distance apart, clustering is stopped and the current set of hierarchies is returned. 48 | 49 | ```js 50 | cy.elements().hierarchical({ 51 | mode: "regular", // extension mode 52 | threshold: 25, // stopping criterion that affects granularity (#) of clusters 53 | 54 | distance: "euclidean", // distance metric for measuring the distance between two nodes 55 | linkage: "single", // linkage criteria for determining the distance between two clusters 56 | attributes: [ // attributes/features used to group nodes 57 | function(node) { 58 | return node.position('x'); 59 | }, 60 | function(node) { 61 | return node.position('y'); 62 | } 63 | ] 64 | }); 65 | ``` 66 | 67 | #### Mode 2: "dendrogram" 68 | Under the ```dendrogram``` mode, the algorithm returns an array of clusters generated from the data set, and generates a dendrogram of the clusters. 69 | One may set the ```cutoff``` option to specify the level at which the tree is cut. This option partitions clusters at different precisions. 70 | For example, in the demo img above, setting ```cutoff = 2``` will return the clusters {D,F,E}, {C}, {A}, {B}. Setting ```cutoff = 1``` will return the clusters {D,F,E,C}, {A,B}. Setting ```cutoff = 0``` will return a single cluster containing all the nodes. 71 | 72 | Since the ```dendrogram``` mode generates many additional nodes and edges in order to render the tree, it might not be performant for large data sets. Thus it is recommended to use ```regular``` mode for clustering instead. 73 | 74 | ```js 75 | cy.elements().hierarchical({ 76 | mode: "dendrogram", // extension mode 77 | cutoff: 2, // stopping criterion that affects granularity (#) of clusters 78 | 79 | distance: "euclidean", // distance metric for measuring the distance between two nodes 80 | linkage: "single", // linkage criteria for determining the distance between two clusters 81 | attributes: [ // attributes/features used to group nodes 82 | function(node) { 83 | return node.position('x'); 84 | }, 85 | function(node) { 86 | return node.position('y'); 87 | } 88 | ] 89 | }); 90 | ``` 91 | 92 | ```demo.html``` provides working examples of the 2 different modes using separate data sets. 93 | 94 | ##### Linkage Types 95 | ```average``` - the distance between two clusters is an average of the differences between the nodes in the clusters. 96 | 97 | ```single``` - the distance between clusters is the smallest distance between a node from each cluster. 98 | 99 | ```complete``` - the distance between clusters is the largest distance between two nodes in the clusters. 100 | 101 | 102 | ## Publishing instructions 103 | 104 | This project is set up to automatically be published to npm and bower. To publish: 105 | 106 | 1. Set the version number environment variable: `export VERSION=1.2.3` 107 | 1. Publish: `gulp publish` 108 | 1. If publishing to bower for the first time, you'll need to run `bower register cytoscape-hierarchical https://github.com/cytoscape.js-hierarchical.git` 109 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-hierarchical", 3 | "description": "A Cytoscape.js extension for the hierarchical clustering algorithm", 4 | "main": "cytoscape-hierarchical.js", 5 | "dependencies": { 6 | "cytoscape": "^2.7.0" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/cytoscape.js-hierarchical.git" 11 | }, 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "bower_components", 16 | "test", 17 | "tests" 18 | ], 19 | "keywords": [ 20 | "cytoscape", 21 | "cyext" 22 | ], 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /cytoscape-hierarchical.js: -------------------------------------------------------------------------------- 1 | ;(function(){ 'use strict'; 2 | 3 | // Implemented from the reference library: https://harthur.github.io/clusterfck/ 4 | 5 | var defaults = { 6 | distance: 'euclidean', 7 | linkage: 'single', 8 | threshold: 10, 9 | mode: 'regular', 10 | cutoff: 0, 11 | attributes: [ 12 | function(node) { 13 | return node.position('x'); 14 | }, 15 | function(node) { 16 | return node.position('y'); 17 | } 18 | ], 19 | testMode: false 20 | }; 21 | 22 | var setOptions = function( opts, options ) { 23 | for (var i in defaults) { opts[i] = defaults[i]; } 24 | for (var i in options) { opts[i] = options[i]; } 25 | }; 26 | 27 | var distances = { 28 | euclidean: function ( n1, n2, attributes ) { 29 | var total = 0; 30 | for ( var dim = 0; dim < attributes.length; dim++ ) { 31 | total += Math.pow( attributes[dim](n1) - attributes[dim](n2), 2 ); 32 | } 33 | return Math.sqrt(total); 34 | }, 35 | manhattan: function ( n1, n2, attributes ) { 36 | var total = 0; 37 | for ( var dim = 0; dim < attributes.length; dim++ ) { 38 | total += Math.abs( attributes[dim](n1) - attributes[dim](n2) ); 39 | } 40 | return total; 41 | }, 42 | max: function ( n1, n2, attributes ) { 43 | var max = 0; 44 | for ( var dim = 0; dim < attributes.length; dim++ ) { 45 | max = Math.max( max, Math.abs( attributes[dim](n1) - attributes[dim](n2) ) ); 46 | } 47 | return max; 48 | } 49 | }; 50 | 51 | var mergeClosest = function( clusters, index, dists, mins, opts ) { 52 | // Find two closest clusters from cached mins 53 | var minKey = 0; 54 | var min = Infinity; 55 | 56 | for ( var i = 0; i < clusters.length; i++ ) { 57 | var key = clusters[i].key; 58 | var dist = dists[key][mins[key]]; 59 | if ( dist < min ) { 60 | minKey = key; 61 | min = dist; 62 | } 63 | } 64 | if ( (opts.mode === 'regular' && min >= opts.threshold) || 65 | (opts.mode === 'dendrogram' && clusters.length === 1) ) { 66 | return false; 67 | } 68 | 69 | var c1 = index[minKey]; 70 | var c2 = index[mins[minKey]]; 71 | 72 | // Merge two closest clusters 73 | if ( opts.mode === 'dendrogram' ) { 74 | var merged = { 75 | left: c1, 76 | right: c2, 77 | key: c1.key 78 | }; 79 | } 80 | else { 81 | var merged = { 82 | value: c1.value.concat(c2.value), 83 | key: c1.key 84 | }; 85 | } 86 | 87 | clusters[c1.index] = merged; 88 | clusters.splice(c2.index, 1); 89 | 90 | index[c1.key] = merged; 91 | 92 | // Update distances with new merged cluster 93 | for ( var i = 0; i < clusters.length; i++ ) { 94 | var cur = clusters[i]; 95 | 96 | if ( c1.key === cur.key ) { 97 | dist = Infinity; 98 | } 99 | else if ( opts.linkage === 'single' ) { 100 | dist = dists[c1.key][cur.key]; 101 | if ( dists[c1.key][cur.key] > dists[c2.key][cur.key] ) { 102 | dist = dists[c2.key][cur.key]; 103 | } 104 | } 105 | else if ( opts.linkage === 'complete' ) { 106 | dist = dists[c1.key][cur.key]; 107 | if ( dists[c1.key][cur.key] < dists[c2.key][cur.key] ) { 108 | dist = dists[c2.key][cur.key]; 109 | } 110 | } 111 | else if ( opts.linkage === 'average' ) { 112 | dist = (dists[c1.key][cur.key] * c1.size + dists[c2.key][cur.key] * c2.size) / (c1.size + c2.size); 113 | } 114 | else { 115 | if ( opts.mode === 'dendrogram' ) 116 | dist = distances[opts.distance]( cur.value, c1.value, opts.attributes ); 117 | else 118 | dist = distances[opts.distance]( cur.value[0], c1.value[0], opts.attributes ); 119 | } 120 | 121 | dists[c1.key][cur.key] = dists[cur.key][c1.key] = dist; // distance matrix is symmetric 122 | } 123 | 124 | // Update cached mins 125 | for ( var i = 0; i < clusters.length; i++ ) { 126 | var key1 = clusters[i].key; 127 | if ( mins[key1] === c1.key || mins[key1] === c2.key ) { 128 | var min = key1; 129 | for ( var j = 0; j < clusters.length; j++ ) { 130 | var key2 = clusters[j].key; 131 | if ( dists[key1][key2] < dists[key1][min] ) { 132 | min = key2; 133 | } 134 | } 135 | mins[key1] = min; 136 | } 137 | clusters[i].index = i; 138 | } 139 | 140 | // Clean up meta data used for clustering 141 | delete c1.key; delete c2.key; 142 | delete c1.index; delete c2.index; 143 | 144 | return true; 145 | }; 146 | 147 | var getAllChildren = function( root, arr, cy ) { 148 | 149 | if ( !root ) 150 | return; 151 | 152 | if ( root.value ) { 153 | arr.push( root.value ); 154 | } 155 | else { 156 | if ( root.left ) 157 | getAllChildren( root.left, arr, cy ); 158 | if ( root.right ) 159 | getAllChildren( root.right, arr, cy ); 160 | } 161 | }; 162 | 163 | var buildDendrogram = function ( root, cy ) { 164 | 165 | if ( !root ) 166 | return ''; 167 | 168 | if ( root.left && root.right ) { 169 | 170 | var leftStr = buildDendrogram( root.left, cy ); 171 | var rightStr = buildDendrogram( root.right, cy ); 172 | 173 | var node = cy.add({group:'nodes', data: {id: leftStr + ',' + rightStr}}); 174 | 175 | cy.add({group:'edges', data: { source: leftStr, target: node.id() }}); 176 | cy.add({group:'edges', data: { source: rightStr, target: node.id() }}); 177 | 178 | return node.id(); 179 | } 180 | else if ( root.value ) { 181 | return root.value.id(); 182 | } 183 | 184 | }; 185 | 186 | var buildClustersFromTree = function( root, k, cy ) { 187 | 188 | if ( !root ) 189 | return []; 190 | 191 | var left = [], right = [], leaves = []; 192 | 193 | if ( k === 0 ) { // don't cut tree, simply return all nodes as 1 single cluster 194 | if ( root.left ) 195 | getAllChildren( root.left, left, cy ); 196 | if ( root.right ) 197 | getAllChildren( root.right, right, cy ); 198 | 199 | leaves = left.concat(right); 200 | return [ cy.collection(leaves) ]; 201 | } 202 | else if ( k === 1 ) { // cut at root 203 | 204 | if ( root.value ) { // leaf node 205 | return [ cy.collection( root.value ) ]; 206 | } 207 | else { 208 | if ( root.left ) 209 | getAllChildren( root.left, left, cy ); 210 | if ( root.right ) 211 | getAllChildren( root.right, right, cy ); 212 | 213 | return [ cy.collection(left), cy.collection(right) ]; 214 | } 215 | } 216 | else { 217 | if ( root.value ) { 218 | return [ cy.collection(root.value) ]; 219 | } 220 | else { 221 | if ( root.left ) 222 | left = buildClustersFromTree( root.left, k - 1, cy ); 223 | if ( root.right ) 224 | right = buildClustersFromTree( root.right, k - 1, cy ); 225 | 226 | return left.concat(right); 227 | } 228 | } 229 | }; 230 | 231 | var printMatrix = function( M ) { // used for debugging purposes only 232 | var n = M.length; 233 | for(var i = 0; i < n; i++ ) { 234 | var row = ''; 235 | for ( var j = 0; j < n; j++ ) { 236 | row += Math.round(M[i][j]*100)/100 + ' '; 237 | } 238 | console.log(row); 239 | } 240 | console.log(''); 241 | }; 242 | 243 | var hierarchical = function( options ){ 244 | var cy = this.cy(); 245 | var nodes = this.nodes(); 246 | var opts = {}; 247 | 248 | // Set parameters of algorithm: linkage type, distance metric, etc. 249 | setOptions( opts, options ); 250 | 251 | // Begin hierarchical algorithm 252 | var clusters = []; 253 | var dists = []; // distances between each pair of clusters 254 | var mins = []; // closest cluster for each cluster 255 | var index = []; // hash of all clusters by key 256 | 257 | // In agglomerative (bottom-up) clustering, each node starts as its own cluster 258 | for ( var n = 0; n < nodes.length; n++ ) { 259 | var cluster = { 260 | value: (opts.mode === 'dendrogram') ? nodes[n] : [ nodes[n] ], 261 | key: n, 262 | index: n 263 | }; 264 | clusters[n] = cluster; 265 | index[n] = cluster; 266 | dists[n] = []; 267 | mins[n] = 0; 268 | } 269 | 270 | // Calculate the distance between each pair of clusters 271 | for ( var i = 0; i < clusters.length; i++ ) { 272 | for ( var j = 0; j <= i; j++ ) { 273 | if ( opts.mode === 'dendrogram' ) // modes store cluster values differently 274 | var dist = (i === j) ? Infinity : distances[opts.distance]( clusters[i].value, clusters[j].value, opts.attributes ); 275 | else 276 | var dist = (i === j) ? Infinity : distances[opts.distance]( clusters[i].value[0], clusters[j].value[0], opts.attributes ); 277 | dists[i][j] = dist; 278 | dists[j][i] = dist; 279 | 280 | if ( dist < dists[i][mins[i]] ) { 281 | mins[i] = j; // Cache mins: closest cluster to cluster i is cluster j 282 | } 283 | } 284 | } 285 | 286 | // Find the closest pair of clusters and merge them into a single cluster. 287 | // Update distances between new cluster and each of the old clusters, and loop until threshold reached. 288 | var merged = mergeClosest( clusters, index, dists, mins, opts ); 289 | while ( merged ) { 290 | merged = mergeClosest( clusters, index, dists, mins, opts ); 291 | } 292 | 293 | // Dendrogram mode builds the hierarchy and adds intermediary nodes + edges 294 | // in addition to returning the clusters. 295 | if ( opts.mode === 'dendrogram') { 296 | var retClusters = buildClustersFromTree( clusters[0], opts.cutoff, cy ); 297 | 298 | if ( !opts.testMode ) 299 | buildDendrogram( clusters[0], cy ); 300 | } 301 | else { // Regular mode simply returns the clusters 302 | 303 | var retClusters = new Array(clusters.length); 304 | clusters.forEach( function( cluster, i ) { 305 | // Clean up meta data used for clustering 306 | delete cluster.key; 307 | delete cluster.index; 308 | 309 | retClusters[i] = cy.collection( cluster.value ); 310 | }); 311 | } 312 | 313 | return retClusters; 314 | }; 315 | 316 | // registers the extension on a cytoscape lib ref 317 | var register = function( cytoscape ){ 318 | 319 | if( !cytoscape ){ return; } // can't register if cytoscape unspecified 320 | 321 | // main entry point 322 | cytoscape( 'collection', 'hierarchical', hierarchical ); 323 | 324 | }; 325 | 326 | if( typeof module !== 'undefined' && module.exports ){ // expose as a commonjs module 327 | module.exports = register; 328 | } 329 | 330 | if( typeof define !== 'undefined' && define.amd ){ // expose as an amd/requirejs module 331 | define('cytoscape-hierarchical', function(){ 332 | return register; 333 | }); 334 | } 335 | 336 | if( typeof cytoscape !== 'undefined' ){ // expose to global cytoscape (i.e. window.cytoscape) 337 | register( cytoscape ); 338 | } 339 | 340 | })(); 341 | -------------------------------------------------------------------------------- /demo-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cytoscape/cytoscape.js-hierarchical/7af700fdadd63bd85720eed4db60874440428b4b/demo-img.png -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |