├── .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 | ![Screenshot of clusters returned from hierarchical clustering algorithm](./demo-img.png?raw=true "Screenshot of clusters returned from hierarchical clustering algorithm") 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 | cytoscape-hierarchical.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 38 | 39 | 135 | 136 | 137 | 138 |

cytoscape-hierarchical demo

139 | 140 |
141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var path = require('path'); 3 | var replace = require('gulp-replace'); 4 | var child_process = require('child_process'); 5 | var fs = require('fs'); 6 | var shell = require('gulp-shell'); 7 | var jshint = require('gulp-jshint'); 8 | var jshStylish = require('jshint-stylish'); 9 | var exec = require('child_process').exec; 10 | var runSequence = require('run-sequence'); 11 | var prompt = require('gulp-prompt'); 12 | var version; 13 | 14 | gulp.task('default', [], function( next ){ 15 | console.log('You must explicitly call `gulp publish` to publish the extension'); 16 | next(); 17 | }); 18 | 19 | gulp.task('publish', [], function( next ){ 20 | runSequence('confver', 'lint', 'pkgver', 'push', 'tag', 'npm', next); 21 | }); 22 | 23 | gulp.task('confver', ['version'], function(){ 24 | return gulp.src('.') 25 | .pipe( prompt.confirm({ message: 'Are you sure version `' + version + '` is OK to publish?' }) ) 26 | ; 27 | }); 28 | 29 | gulp.task('version', function( next ){ 30 | var now = new Date(); 31 | version = process.env['VERSION']; 32 | 33 | if( version ){ 34 | done(); 35 | } else { 36 | exec('git rev-parse HEAD', function( error, stdout, stderr ){ 37 | var sha = stdout.substring(0, 10); // shorten so not huge filename 38 | 39 | version = [ 'snapshot', sha, +now ].join('-'); 40 | done(); 41 | }); 42 | } 43 | 44 | function done(){ 45 | console.log('Using version number `%s` for building', version); 46 | next(); 47 | } 48 | 49 | }); 50 | 51 | gulp.task('pkgver', ['version'], function(){ 52 | return gulp.src([ 53 | 'package.json', 54 | 'bower.json' 55 | ]) 56 | .pipe( replace(/\"version\"\:\s*\".*?\"/, '"version": "' + version + '"') ) 57 | 58 | .pipe( gulp.dest('./') ) 59 | ; 60 | }); 61 | 62 | gulp.task('push', shell.task([ 63 | 'git add -A', 64 | 'git commit -m "pushing changes for v$VERSION release" || echo Nothing to commit', 65 | 'git push || echo Nothing to push' 66 | ])); 67 | 68 | gulp.task('tag', shell.task([ 69 | 'git tag -a $VERSION -m "tagging v$VERSION"', 70 | 'git push origin $VERSION' 71 | ])); 72 | 73 | gulp.task('npm', shell.task([ 74 | 'npm publish .' 75 | ])); 76 | 77 | // http://www.jshint.com/docs/options/ 78 | gulp.task('lint', function(){ 79 | return gulp.src( 'cytoscape-*.js' ) 80 | .pipe( jshint({ 81 | funcscope: true, 82 | laxbreak: true, 83 | loopfunc: true, 84 | strict: true, 85 | unused: 'vars', 86 | eqnull: true, 87 | sub: true, 88 | shadow: true, 89 | laxcomma: true 90 | }) ) 91 | 92 | .pipe( jshint.reporter(jshStylish) ) 93 | 94 | .pipe( jshint.reporter('fail') ) 95 | ; 96 | }); 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-hierarchical", 3 | "version": "0.0.0", 4 | "description": "A Cytoscape.js extension for the hierarchical clustering algorithm", 5 | "main": "cytoscape-hierarchical.js", 6 | "spm": { 7 | "main": "cytoscape-hierarchical.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/cytoscape/cytoscape.js-hierarchical.git" 15 | }, 16 | "keywords": [ 17 | "cytoscape", 18 | "cyext" 19 | ], 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/cytoscape/cytoscape.js-hierarchical/issues" 23 | }, 24 | "homepage": "https://github.com/cytoscape/cytoscape.js-hierarchical", 25 | "devDependencies": { 26 | "chai": "^3.5.0", 27 | "gulp": "^3.8.8", 28 | "gulp-jshint": "^1.8.5", 29 | "gulp-prompt": "^0.1.1", 30 | "gulp-replace": "^0.4.0", 31 | "gulp-shell": "^0.2.9", 32 | "jshint-stylish": "^1.0.0", 33 | "mocha": "^3.0.0", 34 | "run-sequence": "^1.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/hierarchical-tests.js: -------------------------------------------------------------------------------- 1 | 2 | var expect = chai.expect; 3 | 4 | // Expected results generated from the numerical example found at: 5 | // http://people.revoledu.com/kardi/tutorial/Clustering/Numerical%20Example.htm 6 | 7 | describe('hierarchical clustering', function() { 8 | 9 | var cy; 10 | var nodes; 11 | var nA, nB, nC, nD, nE, nF; 12 | 13 | var options; 14 | var expectedClusters; 15 | var clusters; 16 | 17 | before(function(done) { 18 | cytoscape({ 19 | elements: { 20 | nodes: [ 21 | { data: { id: 'A', X1: 1, X2: 1 } }, 22 | { data: { id: 'B', X1: 1.5, X2: 1.5 } }, 23 | { data: { id: 'C', X1: 5, X2: 5 } }, 24 | { data: { id: 'D', X1: 3, X2: 4 } }, 25 | { data: { id: 'E', X1: 4, X2: 4 } }, 26 | { data: { id: 'F', X1: 3, X2: 3.5 } } 27 | ] 28 | }, 29 | ready: function() { 30 | cy = this; 31 | nodes = cy.nodes(); 32 | 33 | nA = cy.$('#A'); 34 | nB = cy.$('#B'); 35 | nC = cy.$('#C'); 36 | nD = cy.$('#D'); 37 | nE = cy.$('#E'); 38 | nF = cy.$('#F'); 39 | 40 | options = { 41 | distance: "euclidean", 42 | linkage: "single", 43 | attributes: [ 44 | function(node) { 45 | return node.data('X1'); 46 | }, 47 | function(node) { 48 | return node.data('X2'); 49 | } 50 | ], 51 | mode: 'dendrogram', 52 | cutoff: 2, 53 | threshold: 10, 54 | testMode: true, 55 | testCentroids: [ [1.0, 1.0], [5.0, 7.0] ] 56 | }; 57 | 58 | clusters = cy.elements().hierarchical( options ); 59 | 60 | done(); 61 | } 62 | }); 63 | }); 64 | 65 | function classify(node, clusters) { 66 | var found = null; 67 | 68 | for (var c = 0; clusters.length; c++) { 69 | var cluster = clusters[c]; 70 | for (var e = 0; e < cluster.length; e++) { 71 | if (node === cluster[e]) { 72 | found = c; 73 | return found; 74 | } 75 | } 76 | } 77 | } 78 | 79 | function found(node, cluster) { 80 | for (var n = 0; n < cluster.length; n++) { 81 | if (node === cluster[n]) { 82 | return true; 83 | } 84 | } 85 | return false; 86 | } 87 | 88 | 89 | it('clusters should be returned in an array', function() { 90 | expect(clusters).to.exist; 91 | expect(clusters.constructor === Array).to.be.true; 92 | }); 93 | 94 | it('all nodes should be assigned to a cluster', function() { 95 | var total = 0; 96 | for (var i = 0; i < clusters.length; i++) { 97 | total += clusters[i].length; 98 | } 99 | expect(total).to.equal(nodes.length); 100 | }); 101 | 102 | it('nodes cannot be assigned to more than one cluster', function() { 103 | for (var n = 0; n < nodes.length; n++) { 104 | var node = nodes[n]; 105 | 106 | // Find which cluster the node belongs to. 107 | var cluster = classify(node, clusters); 108 | expect(cluster).to.exist; 109 | 110 | // Iterate through all other clusters to make sure the node 111 | // is not found in any other cluster. 112 | for (var c = 0; c < clusters.length; c++) { 113 | if (cluster !== c) { 114 | var duplicate = found(node, clusters[c]); 115 | expect(duplicate).to.be.false; 116 | } 117 | } 118 | } 119 | }); 120 | 121 | it('should return the same clusters if we run the algorithm multiple times', function() { 122 | for (var i = 0; i < 10; i++) { 123 | var clusters2 = cy.elements().hierarchical( options ); 124 | 125 | expect(clusters2).to.exist; 126 | expect(clusters2.length).to.equal(clusters.length); 127 | expect(clusters2).to.deep.equal(clusters); 128 | } 129 | }); 130 | 131 | it('Check level 0 of dendrogram: should return the numerically correct clusters (expected results)', function() { 132 | // Set algorithm to cut the dendrogram tree at level 0 133 | options.cutoff = 0; 134 | var clustersAtLevel0 = cy.elements().hierarchical( options ); 135 | 136 | // At level 0, we expect the algorithm (for this exmaple) to return all nodes in one single cluster 137 | expect(clustersAtLevel0.length).to.equal(1); 138 | }); 139 | 140 | it('Check level 1 of dendrogram: should return the numerically correct clusters (expected results)', function() { 141 | // Set algorithm to cut the dendrogram tree at level 1 142 | options.cutoff = 1; 143 | var clustersAtLevel1 = cy.elements().hierarchical( options ); 144 | 145 | // At level 1, we expect the algorithm (for this example) to return 2 clusters 146 | expect(clustersAtLevel1.length).to.equal(2); 147 | 148 | expect(clustersAtLevel1[0][0].id()).to.equal('B'); 149 | expect(clustersAtLevel1[0][1].id()).to.equal('A'); 150 | 151 | expect(clustersAtLevel1[1][0].id()).to.equal('C'); 152 | expect(clustersAtLevel1[1][1].id()).to.equal('E'); 153 | expect(clustersAtLevel1[1][2].id()).to.equal('F'); 154 | expect(clustersAtLevel1[1][3].id()).to.equal('D'); 155 | }); 156 | 157 | it('Check level 2 of dendrogram: should return the numerically correct clusters (expected results)', function() { 158 | // Set algorithm to cut the dendrogram tree at level 2 159 | options.cutoff = 2; 160 | var clustersAtLevel2 = cy.elements().hierarchical( options ); 161 | 162 | // At level 2, we expect the algorithm (for this example) to return 4 clusters 163 | expect(clustersAtLevel2.length).to.equal(4); 164 | 165 | expect(clustersAtLevel2[0][0].id()).to.equal('B'); 166 | expect(clustersAtLevel2[1][0].id()).to.equal('A'); 167 | expect(clustersAtLevel2[2][0].id()).to.equal('C'); 168 | expect(clustersAtLevel2[3][0].id()).to.equal('E'); 169 | expect(clustersAtLevel2[3][1].id()).to.equal('F'); 170 | expect(clustersAtLevel2[3][2].id()).to.equal('D'); 171 | }); 172 | 173 | it('Check level 3 of dendrogram: should return the numerically correct clusters (expected results)', function() { 174 | // Set algorithm to cut the dendrogram tree at level 3 175 | options.cutoff = 3; 176 | var clustersAtLevel3 = cy.elements().hierarchical( options ); 177 | 178 | // At level 3, we expect the algorithm (for this example) to return 5 clusters 179 | expect(clustersAtLevel3.length).to.equal(5); 180 | 181 | expect(clustersAtLevel3[0][0].id()).to.equal('B'); 182 | expect(clustersAtLevel3[1][0].id()).to.equal('A'); 183 | expect(clustersAtLevel3[2][0].id()).to.equal('C'); 184 | expect(clustersAtLevel3[3][0].id()).to.equal('E'); 185 | expect(clustersAtLevel3[4][0].id()).to.equal('F'); 186 | expect(clustersAtLevel3[4][1].id()).to.equal('D'); 187 | 188 | }); 189 | 190 | it('Check level 4 of dendrogram: should have 1 node per cluster (expected results)', function() { 191 | // Set algorithm to cut the dendrogram tree at level 4 192 | options.cutoff = 4; 193 | var clustersAtLevel4 = cy.elements().hierarchical( options ); 194 | 195 | // At level 4, we expect the algorithm (for this example) to return 6 clusters 196 | expect(clustersAtLevel4.length).to.equal(6); 197 | 198 | expect(clustersAtLevel4[0][0].id()).to.equal('B'); 199 | expect(clustersAtLevel4[1][0].id()).to.equal('A'); 200 | expect(clustersAtLevel4[2][0].id()).to.equal('C'); 201 | expect(clustersAtLevel4[3][0].id()).to.equal('E'); 202 | expect(clustersAtLevel4[4][0].id()).to.equal('F'); 203 | expect(clustersAtLevel4[5][0].id()).to.equal('D'); 204 | 205 | }); 206 | 207 | it('Check level 5+ of dendrogram: should have 1 node per cluster (fail safely)', function() { 208 | // Set algorithm to cut the dendrogram tree at level 5 209 | options.cutoff = 5; 210 | var clustersAtLevel5 = cy.elements().hierarchical( options ); 211 | 212 | // At level 5, we expect the algorithm (for this example) to return 6 clusters 213 | expect(clustersAtLevel5.length).to.equal(6); 214 | 215 | expect(clustersAtLevel5[0][0].id()).to.equal('B'); 216 | expect(clustersAtLevel5[1][0].id()).to.equal('A'); 217 | expect(clustersAtLevel5[2][0].id()).to.equal('C'); 218 | expect(clustersAtLevel5[3][0].id()).to.equal('E'); 219 | expect(clustersAtLevel5[4][0].id()).to.equal('F'); 220 | expect(clustersAtLevel5[5][0].id()).to.equal('D'); 221 | 222 | // Set algorithm to cut the dendrogram tree at level 10 223 | options.cutoff = 10; 224 | var clustersAtLevel10 = cy.elements().hierarchical( options ); 225 | 226 | // At level 10, we expect the algorithm (for this example) to return 6 clusters 227 | expect(clustersAtLevel10.length).to.equal(6); 228 | 229 | expect(clustersAtLevel10[0][0].id()).to.equal('B'); 230 | expect(clustersAtLevel10[1][0].id()).to.equal('A'); 231 | expect(clustersAtLevel10[2][0].id()).to.equal('C'); 232 | expect(clustersAtLevel10[3][0].id()).to.equal('E'); 233 | expect(clustersAtLevel10[4][0].id()).to.equal('F'); 234 | expect(clustersAtLevel10[5][0].id()).to.equal('D'); 235 | }); 236 | 237 | }); -------------------------------------------------------------------------------- /test/run-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tests for hierarchical clustering 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | --------------------------------------------------------------------------------