vis-rails 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +5 -13
  2. data/lib/vis/rails/version.rb +1 -1
  3. data/vendor/assets/component/emitter.js +162 -0
  4. data/vendor/assets/javascripts/vis.js +1 -0
  5. data/vendor/assets/vis/DataSet.js +8 -2
  6. data/vendor/assets/vis/DataView.js +8 -4
  7. data/vendor/assets/vis/graph/Edge.js +210 -78
  8. data/vendor/assets/vis/graph/Graph.js +474 -652
  9. data/vendor/assets/vis/graph/Node.js +119 -82
  10. data/vendor/assets/vis/graph/css/graph-manipulation.css +128 -0
  11. data/vendor/assets/vis/graph/css/graph-navigation.css +62 -0
  12. data/vendor/assets/vis/graph/graphMixins/ClusterMixin.js +1141 -0
  13. data/vendor/assets/vis/graph/graphMixins/HierarchicalLayoutMixin.js +296 -0
  14. data/vendor/assets/vis/graph/graphMixins/ManipulationMixin.js +433 -0
  15. data/vendor/assets/vis/graph/graphMixins/MixinLoader.js +201 -0
  16. data/vendor/assets/vis/graph/graphMixins/NavigationMixin.js +173 -0
  17. data/vendor/assets/vis/graph/graphMixins/SectorsMixin.js +552 -0
  18. data/vendor/assets/vis/graph/graphMixins/SelectionMixin.js +558 -0
  19. data/vendor/assets/vis/graph/graphMixins/physics/BarnesHut.js +373 -0
  20. data/vendor/assets/vis/graph/graphMixins/physics/HierarchialRepulsion.js +64 -0
  21. data/vendor/assets/vis/graph/graphMixins/physics/PhysicsMixin.js +513 -0
  22. data/vendor/assets/vis/graph/graphMixins/physics/Repulsion.js +66 -0
  23. data/vendor/assets/vis/graph/img/acceptDeleteIcon.png +0 -0
  24. data/vendor/assets/vis/graph/img/addNodeIcon.png +0 -0
  25. data/vendor/assets/vis/graph/img/backIcon.png +0 -0
  26. data/vendor/assets/vis/graph/img/connectIcon.png +0 -0
  27. data/vendor/assets/vis/graph/img/cross.png +0 -0
  28. data/vendor/assets/vis/graph/img/cross2.png +0 -0
  29. data/vendor/assets/vis/graph/img/deleteIcon.png +0 -0
  30. data/vendor/assets/vis/graph/img/downArrow.png +0 -0
  31. data/vendor/assets/vis/graph/img/editIcon.png +0 -0
  32. data/vendor/assets/vis/graph/img/leftArrow.png +0 -0
  33. data/vendor/assets/vis/graph/img/rightArrow.png +0 -0
  34. data/vendor/assets/vis/graph/img/upArrow.png +0 -0
  35. data/vendor/assets/vis/module/exports.js +0 -2
  36. data/vendor/assets/vis/module/header.js +2 -2
  37. data/vendor/assets/vis/module/imports.js +1 -2
  38. data/vendor/assets/vis/timeline/Controller.js +56 -45
  39. data/vendor/assets/vis/timeline/Range.js +68 -62
  40. data/vendor/assets/vis/timeline/Stack.js +11 -13
  41. data/vendor/assets/vis/timeline/TimeStep.js +43 -38
  42. data/vendor/assets/vis/timeline/Timeline.js +215 -93
  43. data/vendor/assets/vis/timeline/component/Component.js +19 -3
  44. data/vendor/assets/vis/timeline/component/CurrentTime.js +1 -1
  45. data/vendor/assets/vis/timeline/component/CustomTime.js +39 -120
  46. data/vendor/assets/vis/timeline/component/GroupSet.js +35 -1
  47. data/vendor/assets/vis/timeline/component/ItemSet.js +272 -9
  48. data/vendor/assets/vis/timeline/component/RootPanel.js +59 -47
  49. data/vendor/assets/vis/timeline/component/TimeAxis.js +10 -0
  50. data/vendor/assets/vis/timeline/component/css/item.css +53 -22
  51. data/vendor/assets/vis/timeline/component/item/Item.js +40 -5
  52. data/vendor/assets/vis/timeline/component/item/ItemBox.js +3 -1
  53. data/vendor/assets/vis/timeline/component/item/ItemPoint.js +3 -1
  54. data/vendor/assets/vis/timeline/component/item/ItemRange.js +67 -3
  55. data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +37 -9
  56. data/vendor/assets/vis/timeline/img/delete.png +0 -0
  57. data/vendor/assets/vis/util.js +169 -30
  58. metadata +39 -12
@@ -0,0 +1,62 @@
1
+ div.graph-navigation {
2
+ width:34px;
3
+ height:34px;
4
+ z-index:10;
5
+ -moz-border-radius: 17px;
6
+ border-radius: 17px;
7
+ position:absolute;
8
+ display:inline-block;
9
+ background-position: 2px 2px;
10
+ background-repeat:no-repeat;
11
+ cursor: pointer;
12
+ -webkit-touch-callout: none;
13
+ -webkit-user-select: none;
14
+ -khtml-user-select: none;
15
+ -moz-user-select: none;
16
+ -ms-user-select: none;
17
+ user-select: none;
18
+ }
19
+
20
+ div.graph-navigation:hover {
21
+ box-shadow: 0px 0px 3px 3px rgba(56, 207, 21, 0.30);
22
+ }
23
+
24
+ div.graph-navigation:active {
25
+ box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95);
26
+ }
27
+
28
+ div.graph-navigation.up {
29
+ background-image: url("img/graph/upArrow.png");
30
+ margin-top:520px;
31
+ margin-left:55px;
32
+ }
33
+ div.graph-navigation.down {
34
+ background-image: url("img/graph/downArrow.png");
35
+ margin-top:560px;
36
+ margin-left:55px;
37
+ }
38
+ div.graph-navigation.left {
39
+ background-image: url("img/graph/leftArrow.png");
40
+ margin-top:560px;
41
+ margin-left:15px;
42
+ }
43
+ div.graph-navigation.right {
44
+ background-image: url("img/graph/rightArrow.png");
45
+ margin-top:560px;
46
+ margin-left:95px;
47
+ }
48
+ div.graph-navigation.zoomIn {
49
+ background-image: url("img/graph/plus.png");
50
+ margin-top:560px;
51
+ margin-left:555px;
52
+ }
53
+ div.graph-navigation.zoomOut {
54
+ background-image: url("img/graph/minus.png");
55
+ margin-top:560px;
56
+ margin-left:515px;
57
+ }
58
+ div.graph-navigation.zoomExtends {
59
+ background-image: url("img/graph/zoomExtends.png");
60
+ margin-top:520px;
61
+ margin-left:555px;
62
+ }
@@ -0,0 +1,1141 @@
1
+ /**
2
+ * Creation of the ClusterMixin var.
3
+ *
4
+ * This contains all the functions the Graph object can use to employ clustering
5
+ *
6
+ * Alex de Mulder
7
+ * 21-01-2013
8
+ */
9
+ var ClusterMixin = {
10
+
11
+ /**
12
+ * This is only called in the constructor of the graph object
13
+ *
14
+ */
15
+ startWithClustering : function() {
16
+ // cluster if the data set is big
17
+ this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
18
+
19
+ // updates the lables after clustering
20
+ this.updateLabels();
21
+
22
+ // this is called here because if clusterin is disabled, the start and stabilize are called in
23
+ // the setData function.
24
+ if (this.stabilize) {
25
+ this._doStabilize();
26
+ }
27
+ this.start();
28
+ },
29
+
30
+ /**
31
+ * This function clusters until the initialMaxNodes has been reached
32
+ *
33
+ * @param {Number} maxNumberOfNodes
34
+ * @param {Boolean} reposition
35
+ */
36
+ clusterToFit : function(maxNumberOfNodes, reposition) {
37
+ var numberOfNodes = this.nodeIndices.length;
38
+
39
+ var maxLevels = 50;
40
+ var level = 0;
41
+
42
+ // we first cluster the hubs, then we pull in the outliers, repeat
43
+ while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
44
+ if (level % 3 == 0) {
45
+ this.forceAggregateHubs(true);
46
+ this.normalizeClusterLevels();
47
+ }
48
+ else {
49
+ this.increaseClusterLevel(); // this also includes a cluster normalization
50
+ }
51
+
52
+ numberOfNodes = this.nodeIndices.length;
53
+ level += 1;
54
+ }
55
+
56
+ // after the clustering we reposition the nodes to reduce the initial chaos
57
+ if (level > 0 && reposition == true) {
58
+ this.repositionNodes();
59
+ }
60
+ this._updateCalculationNodes();
61
+ },
62
+
63
+ /**
64
+ * This function can be called to open up a specific cluster. It is only called by
65
+ * It will unpack the cluster back one level.
66
+ *
67
+ * @param node | Node object: cluster to open.
68
+ */
69
+ openCluster : function(node) {
70
+ var isMovingBeforeClustering = this.moving;
71
+ if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
72
+ !(this._sector() == "default" && this.nodeIndices.length == 1)) {
73
+ // this loads a new sector, loads the nodes and edges and nodeIndices of it.
74
+ this._addSector(node);
75
+ var level = 0;
76
+
77
+ // we decluster until we reach a decent number of nodes
78
+ while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
79
+ this.decreaseClusterLevel();
80
+ level += 1;
81
+ }
82
+
83
+ }
84
+ else {
85
+ this._expandClusterNode(node,false,true);
86
+
87
+ // update the index list, dynamic edges and labels
88
+ this._updateNodeIndexList();
89
+ this._updateDynamicEdges();
90
+ this._updateCalculationNodes();
91
+ this.updateLabels();
92
+ }
93
+
94
+ // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
95
+ if (this.moving != isMovingBeforeClustering) {
96
+ this.start();
97
+ }
98
+ },
99
+
100
+
101
+ /**
102
+ * This calls the updateClustes with default arguments
103
+ */
104
+ updateClustersDefault : function() {
105
+ if (this.constants.clustering.enabled == true) {
106
+ this.updateClusters(0,false,false);
107
+ }
108
+ },
109
+
110
+
111
+ /**
112
+ * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
113
+ * be clustered with their connected node. This can be repeated as many times as needed.
114
+ * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
115
+ */
116
+ increaseClusterLevel : function() {
117
+ this.updateClusters(-1,false,true);
118
+ },
119
+
120
+
121
+ /**
122
+ * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
123
+ * be unpacked if they are a cluster. This can be repeated as many times as needed.
124
+ * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
125
+ */
126
+ decreaseClusterLevel : function() {
127
+ this.updateClusters(1,false,true);
128
+ },
129
+
130
+
131
+ /**
132
+ * This is the main clustering function. It clusters and declusters on zoom or forced
133
+ * This function clusters on zoom, it can be called with a predefined zoom direction
134
+ * If out, check if we can form clusters, if in, check if we can open clusters.
135
+ * This function is only called from _zoom()
136
+ *
137
+ * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
138
+ * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
139
+ * @param {Boolean} force | enabled or disable forcing
140
+ *
141
+ */
142
+ updateClusters : function(zoomDirection,recursive,force,doNotStart) {
143
+ var isMovingBeforeClustering = this.moving;
144
+ var amountOfNodes = this.nodeIndices.length;
145
+
146
+ // on zoom out collapse the sector if the scale is at the level the sector was made
147
+ if (this.previousScale > this.scale && zoomDirection == 0) {
148
+ this._collapseSector();
149
+ }
150
+
151
+ // check if we zoom in or out
152
+ if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
153
+ // forming clusters when forced pulls outliers in. When not forced, the edge length of the
154
+ // outer nodes determines if it is being clustered
155
+ this._formClusters(force);
156
+ }
157
+ else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
158
+ if (force == true) {
159
+ // _openClusters checks for each node if the formationScale of the cluster is smaller than
160
+ // the current scale and if so, declusters. When forced, all clusters are reduced by one step
161
+ this._openClusters(recursive,force);
162
+ }
163
+ else {
164
+ // if a cluster takes up a set percentage of the active window
165
+ this._openClustersBySize();
166
+ }
167
+ }
168
+ this._updateNodeIndexList();
169
+
170
+ // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
171
+ if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
172
+ this._aggregateHubs(force);
173
+ this._updateNodeIndexList();
174
+ }
175
+
176
+ // we now reduce chains.
177
+ if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
178
+ this.handleChains();
179
+ this._updateNodeIndexList();
180
+ }
181
+
182
+ this.previousScale = this.scale;
183
+
184
+ // rest of the update the index list, dynamic edges and labels
185
+ this._updateDynamicEdges();
186
+ this.updateLabels();
187
+
188
+ // if a cluster was formed, we increase the clusterSession
189
+ if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
190
+ this.clusterSession += 1;
191
+ // if clusters have been made, we normalize the cluster level
192
+ this.normalizeClusterLevels();
193
+ }
194
+
195
+ if (doNotStart == false || doNotStart === undefined) {
196
+ // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
197
+ if (this.moving != isMovingBeforeClustering) {
198
+ this.start();
199
+ }
200
+ }
201
+
202
+ this._updateCalculationNodes();
203
+ },
204
+
205
+ /**
206
+ * This function handles the chains. It is called on every updateClusters().
207
+ */
208
+ handleChains : function() {
209
+ // after clustering we check how many chains there are
210
+ var chainPercentage = this._getChainFraction();
211
+ if (chainPercentage > this.constants.clustering.chainThreshold) {
212
+ this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
213
+
214
+ }
215
+ },
216
+
217
+ /**
218
+ * this functions starts clustering by hubs
219
+ * The minimum hub threshold is set globally
220
+ *
221
+ * @private
222
+ */
223
+ _aggregateHubs : function(force) {
224
+ this._getHubSize();
225
+ this._formClustersByHub(force,false);
226
+ },
227
+
228
+
229
+ /**
230
+ * This function is fired by keypress. It forces hubs to form.
231
+ *
232
+ */
233
+ forceAggregateHubs : function(doNotStart) {
234
+ var isMovingBeforeClustering = this.moving;
235
+ var amountOfNodes = this.nodeIndices.length;
236
+
237
+ this._aggregateHubs(true);
238
+
239
+ // update the index list, dynamic edges and labels
240
+ this._updateNodeIndexList();
241
+ this._updateDynamicEdges();
242
+ this.updateLabels();
243
+
244
+ // if a cluster was formed, we increase the clusterSession
245
+ if (this.nodeIndices.length != amountOfNodes) {
246
+ this.clusterSession += 1;
247
+ }
248
+
249
+ if (doNotStart == false || doNotStart === undefined) {
250
+ // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
251
+ if (this.moving != isMovingBeforeClustering) {
252
+ this.start();
253
+ }
254
+ }
255
+ },
256
+
257
+ /**
258
+ * If a cluster takes up more than a set percentage of the screen, open the cluster
259
+ *
260
+ * @private
261
+ */
262
+ _openClustersBySize : function() {
263
+ for (var nodeId in this.nodes) {
264
+ if (this.nodes.hasOwnProperty(nodeId)) {
265
+ var node = this.nodes[nodeId];
266
+ if (node.inView() == true) {
267
+ if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
268
+ (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
269
+ this.openCluster(node);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ },
275
+
276
+
277
+ /**
278
+ * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
279
+ * has to be opened based on the current zoom level.
280
+ *
281
+ * @private
282
+ */
283
+ _openClusters : function(recursive,force) {
284
+ for (var i = 0; i < this.nodeIndices.length; i++) {
285
+ var node = this.nodes[this.nodeIndices[i]];
286
+ this._expandClusterNode(node,recursive,force);
287
+ this._updateCalculationNodes();
288
+ }
289
+ },
290
+
291
+ /**
292
+ * This function checks if a node has to be opened. This is done by checking the zoom level.
293
+ * If the node contains child nodes, this function is recursively called on the child nodes as well.
294
+ * This recursive behaviour is optional and can be set by the recursive argument.
295
+ *
296
+ * @param {Node} parentNode | to check for cluster and expand
297
+ * @param {Boolean} recursive | enabled or disable recursive calling
298
+ * @param {Boolean} force | enabled or disable forcing
299
+ * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
300
+ * @private
301
+ */
302
+ _expandClusterNode : function(parentNode, recursive, force, openAll) {
303
+ // first check if node is a cluster
304
+ if (parentNode.clusterSize > 1) {
305
+ // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
306
+ if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
307
+ openAll = true;
308
+ }
309
+ recursive = openAll ? true : recursive;
310
+
311
+ // if the last child has been added on a smaller scale than current scale decluster
312
+ if (parentNode.formationScale < this.scale || force == true) {
313
+ // we will check if any of the contained child nodes should be removed from the cluster
314
+ for (var containedNodeId in parentNode.containedNodes) {
315
+ if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
316
+ var childNode = parentNode.containedNodes[containedNodeId];
317
+
318
+ // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
319
+ // the largest cluster is the one that comes from outside
320
+ if (force == true) {
321
+ if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
322
+ || openAll) {
323
+ this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
324
+ }
325
+ }
326
+ else {
327
+ if (this._nodeInActiveArea(parentNode)) {
328
+ this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
329
+ }
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+ },
336
+
337
+ /**
338
+ * ONLY CALLED FROM _expandClusterNode
339
+ *
340
+ * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
341
+ * the child node from the parent contained_node object and put it back into the global nodes object.
342
+ * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
343
+ *
344
+ * @param {Node} parentNode | the parent node
345
+ * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
346
+ * @param {Boolean} recursive | This will also check if the child needs to be expanded.
347
+ * With force and recursive both true, the entire cluster is unpacked
348
+ * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
349
+ * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
350
+ * @private
351
+ */
352
+ _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
353
+ var childNode = parentNode.containedNodes[containedNodeId];
354
+
355
+ // if child node has been added on smaller scale than current, kick out
356
+ if (childNode.formationScale < this.scale || force == true) {
357
+ // unselect all selected items
358
+ this._unselectAll();
359
+
360
+ // put the child node back in the global nodes object
361
+ this.nodes[containedNodeId] = childNode;
362
+
363
+ // release the contained edges from this childNode back into the global edges
364
+ this._releaseContainedEdges(parentNode,childNode);
365
+
366
+ // reconnect rerouted edges to the childNode
367
+ this._connectEdgeBackToChild(parentNode,childNode);
368
+
369
+ // validate all edges in dynamicEdges
370
+ this._validateEdges(parentNode);
371
+
372
+ // undo the changes from the clustering operation on the parent node
373
+ parentNode.mass -= childNode.mass;
374
+ parentNode.clusterSize -= childNode.clusterSize;
375
+ parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
376
+ parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
377
+
378
+ // place the child node near the parent, not at the exact same location to avoid chaos in the system
379
+ childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
380
+ childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
381
+
382
+ // remove node from the list
383
+ delete parentNode.containedNodes[containedNodeId];
384
+
385
+ // check if there are other childs with this clusterSession in the parent.
386
+ var othersPresent = false;
387
+ for (var childNodeId in parentNode.containedNodes) {
388
+ if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
389
+ if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
390
+ othersPresent = true;
391
+ break;
392
+ }
393
+ }
394
+ }
395
+ // if there are no others, remove the cluster session from the list
396
+ if (othersPresent == false) {
397
+ parentNode.clusterSessions.pop();
398
+ }
399
+
400
+ this._repositionBezierNodes(childNode);
401
+ // this._repositionBezierNodes(parentNode);
402
+
403
+ // remove the clusterSession from the child node
404
+ childNode.clusterSession = 0;
405
+
406
+ // recalculate the size of the node on the next time the node is rendered
407
+ parentNode.clearSizeCache();
408
+
409
+ // restart the simulation to reorganise all nodes
410
+ this.moving = true;
411
+ }
412
+
413
+ // check if a further expansion step is possible if recursivity is enabled
414
+ if (recursive == true) {
415
+ this._expandClusterNode(childNode,recursive,force,openAll);
416
+ }
417
+ },
418
+
419
+
420
+ /**
421
+ * position the bezier nodes at the center of the edges
422
+ *
423
+ * @param node
424
+ * @private
425
+ */
426
+ _repositionBezierNodes : function(node) {
427
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
428
+ node.dynamicEdges[i].positionBezierNode();
429
+ }
430
+ },
431
+
432
+
433
+ /**
434
+ * This function checks if any nodes at the end of their trees have edges below a threshold length
435
+ * This function is called only from updateClusters()
436
+ * forceLevelCollapse ignores the length of the edge and collapses one level
437
+ * This means that a node with only one edge will be clustered with its connected node
438
+ *
439
+ * @private
440
+ * @param {Boolean} force
441
+ */
442
+ _formClusters : function(force) {
443
+ if (force == false) {
444
+ this._formClustersByZoom();
445
+ }
446
+ else {
447
+ this._forceClustersByZoom();
448
+ }
449
+ },
450
+
451
+
452
+ /**
453
+ * This function handles the clustering by zooming out, this is based on a minimum edge distance
454
+ *
455
+ * @private
456
+ */
457
+ _formClustersByZoom : function() {
458
+ var dx,dy,length,
459
+ minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
460
+
461
+ // check if any edges are shorter than minLength and start the clustering
462
+ // the clustering favours the node with the larger mass
463
+ for (var edgeId in this.edges) {
464
+ if (this.edges.hasOwnProperty(edgeId)) {
465
+ var edge = this.edges[edgeId];
466
+ if (edge.connected) {
467
+ if (edge.toId != edge.fromId) {
468
+ dx = (edge.to.x - edge.from.x);
469
+ dy = (edge.to.y - edge.from.y);
470
+ length = Math.sqrt(dx * dx + dy * dy);
471
+
472
+
473
+ if (length < minLength) {
474
+ // first check which node is larger
475
+ var parentNode = edge.from;
476
+ var childNode = edge.to;
477
+ if (edge.to.mass > edge.from.mass) {
478
+ parentNode = edge.to;
479
+ childNode = edge.from;
480
+ }
481
+
482
+ if (childNode.dynamicEdgesLength == 1) {
483
+ this._addToCluster(parentNode,childNode,false);
484
+ }
485
+ else if (parentNode.dynamicEdgesLength == 1) {
486
+ this._addToCluster(childNode,parentNode,false);
487
+ }
488
+ }
489
+ }
490
+ }
491
+ }
492
+ }
493
+ },
494
+
495
+ /**
496
+ * This function forces the graph to cluster all nodes with only one connecting edge to their
497
+ * connected node.
498
+ *
499
+ * @private
500
+ */
501
+ _forceClustersByZoom : function() {
502
+ for (var nodeId in this.nodes) {
503
+ // another node could have absorbed this child.
504
+ if (this.nodes.hasOwnProperty(nodeId)) {
505
+ var childNode = this.nodes[nodeId];
506
+
507
+ // the edges can be swallowed by another decrease
508
+ if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
509
+ var edge = childNode.dynamicEdges[0];
510
+ var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
511
+
512
+ // group to the largest node
513
+ if (childNode.id != parentNode.id) {
514
+ if (parentNode.mass > childNode.mass) {
515
+ this._addToCluster(parentNode,childNode,true);
516
+ }
517
+ else {
518
+ this._addToCluster(childNode,parentNode,true);
519
+ }
520
+ }
521
+ }
522
+ }
523
+ }
524
+ },
525
+
526
+
527
+ /**
528
+ * To keep the nodes of roughly equal size we normalize the cluster levels.
529
+ * This function clusters a node to its smallest connected neighbour.
530
+ *
531
+ * @param node
532
+ * @private
533
+ */
534
+ _clusterToSmallestNeighbour : function(node) {
535
+ var smallestNeighbour = -1;
536
+ var smallestNeighbourNode = null;
537
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
538
+ if (node.dynamicEdges[i] !== undefined) {
539
+ var neighbour = null;
540
+ if (node.dynamicEdges[i].fromId != node.id) {
541
+ neighbour = node.dynamicEdges[i].from;
542
+ }
543
+ else if (node.dynamicEdges[i].toId != node.id) {
544
+ neighbour = node.dynamicEdges[i].to;
545
+ }
546
+
547
+
548
+ if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
549
+ smallestNeighbour = neighbour.clusterSessions.length;
550
+ smallestNeighbourNode = neighbour;
551
+ }
552
+ }
553
+ }
554
+
555
+ if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
556
+ this._addToCluster(neighbour, node, true);
557
+ }
558
+ },
559
+
560
+
561
+ /**
562
+ * This function forms clusters from hubs, it loops over all nodes
563
+ *
564
+ * @param {Boolean} force | Disregard zoom level
565
+ * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
566
+ * @private
567
+ */
568
+ _formClustersByHub : function(force, onlyEqual) {
569
+ // we loop over all nodes in the list
570
+ for (var nodeId in this.nodes) {
571
+ // we check if it is still available since it can be used by the clustering in this loop
572
+ if (this.nodes.hasOwnProperty(nodeId)) {
573
+ this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
574
+ }
575
+ }
576
+ },
577
+
578
+ /**
579
+ * This function forms a cluster from a specific preselected hub node
580
+ *
581
+ * @param {Node} hubNode | the node we will cluster as a hub
582
+ * @param {Boolean} force | Disregard zoom level
583
+ * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
584
+ * @param {Number} [absorptionSizeOffset] |
585
+ * @private
586
+ */
587
+ _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
588
+ if (absorptionSizeOffset === undefined) {
589
+ absorptionSizeOffset = 0;
590
+ }
591
+ // we decide if the node is a hub
592
+ if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
593
+ (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
594
+ // initialize variables
595
+ var dx,dy,length;
596
+ var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
597
+ var allowCluster = false;
598
+
599
+ // we create a list of edges because the dynamicEdges change over the course of this loop
600
+ var edgesIdarray = [];
601
+ var amountOfInitialEdges = hubNode.dynamicEdges.length;
602
+ for (var j = 0; j < amountOfInitialEdges; j++) {
603
+ edgesIdarray.push(hubNode.dynamicEdges[j].id);
604
+ }
605
+
606
+ // if the hub clustering is not forces, we check if one of the edges connected
607
+ // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
608
+ if (force == false) {
609
+ allowCluster = false;
610
+ for (j = 0; j < amountOfInitialEdges; j++) {
611
+ var edge = this.edges[edgesIdarray[j]];
612
+ if (edge !== undefined) {
613
+ if (edge.connected) {
614
+ if (edge.toId != edge.fromId) {
615
+ dx = (edge.to.x - edge.from.x);
616
+ dy = (edge.to.y - edge.from.y);
617
+ length = Math.sqrt(dx * dx + dy * dy);
618
+
619
+ if (length < minLength) {
620
+ allowCluster = true;
621
+ break;
622
+ }
623
+ }
624
+ }
625
+ }
626
+ }
627
+ }
628
+
629
+ // start the clustering if allowed
630
+ if ((!force && allowCluster) || force) {
631
+ // we loop over all edges INITIALLY connected to this hub
632
+ for (j = 0; j < amountOfInitialEdges; j++) {
633
+ edge = this.edges[edgesIdarray[j]];
634
+ // the edge can be clustered by this function in a previous loop
635
+ if (edge !== undefined) {
636
+ var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
637
+ // we do not want hubs to merge with other hubs nor do we want to cluster itself.
638
+ if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
639
+ (childNode.id != hubNode.id)) {
640
+ this._addToCluster(hubNode,childNode,force);
641
+ }
642
+ }
643
+ }
644
+ }
645
+ }
646
+ },
647
+
648
+
649
+
650
+ /**
651
+ * This function adds the child node to the parent node, creating a cluster if it is not already.
652
+ *
653
+ * @param {Node} parentNode | this is the node that will house the child node
654
+ * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
655
+ * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
656
+ * @private
657
+ */
658
+ _addToCluster : function(parentNode, childNode, force) {
659
+ // join child node in the parent node
660
+ parentNode.containedNodes[childNode.id] = childNode;
661
+
662
+ // manage all the edges connected to the child and parent nodes
663
+ for (var i = 0; i < childNode.dynamicEdges.length; i++) {
664
+ var edge = childNode.dynamicEdges[i];
665
+ if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
666
+ this._addToContainedEdges(parentNode,childNode,edge);
667
+ }
668
+ else {
669
+ this._connectEdgeToCluster(parentNode,childNode,edge);
670
+ }
671
+ }
672
+ // a contained node has no dynamic edges.
673
+ childNode.dynamicEdges = [];
674
+
675
+ // remove circular edges from clusters
676
+ this._containCircularEdgesFromNode(parentNode,childNode);
677
+
678
+
679
+ // remove the childNode from the global nodes object
680
+ delete this.nodes[childNode.id];
681
+
682
+ // update the properties of the child and parent
683
+ var massBefore = parentNode.mass;
684
+ childNode.clusterSession = this.clusterSession;
685
+ parentNode.mass += childNode.mass;
686
+ parentNode.clusterSize += childNode.clusterSize;
687
+ parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
688
+
689
+ // keep track of the clustersessions so we can open the cluster up as it has been formed.
690
+ if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
691
+ parentNode.clusterSessions.push(this.clusterSession);
692
+ }
693
+
694
+ // forced clusters only open from screen size and double tap
695
+ if (force == true) {
696
+ // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
697
+ parentNode.formationScale = 0;
698
+ }
699
+ else {
700
+ parentNode.formationScale = this.scale; // The latest child has been added on this scale
701
+ }
702
+
703
+ // recalculate the size of the node on the next time the node is rendered
704
+ parentNode.clearSizeCache();
705
+
706
+ // set the pop-out scale for the childnode
707
+ parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
708
+
709
+ // nullify the movement velocity of the child, this is to avoid hectic behaviour
710
+ childNode.clearVelocity();
711
+
712
+ // the mass has altered, preservation of energy dictates the velocity to be updated
713
+ parentNode.updateVelocity(massBefore);
714
+
715
+ // restart the simulation to reorganise all nodes
716
+ this.moving = true;
717
+ },
718
+
719
+
720
+ /**
721
+ * This function will apply the changes made to the remainingEdges during the formation of the clusters.
722
+ * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
723
+ * It has to be called if a level is collapsed. It is called by _formClusters().
724
+ * @private
725
+ */
726
+ _updateDynamicEdges : function() {
727
+ for (var i = 0; i < this.nodeIndices.length; i++) {
728
+ var node = this.nodes[this.nodeIndices[i]];
729
+ node.dynamicEdgesLength = node.dynamicEdges.length;
730
+
731
+ // this corrects for multiple edges pointing at the same other node
732
+ var correction = 0;
733
+ if (node.dynamicEdgesLength > 1) {
734
+ for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
735
+ var edgeToId = node.dynamicEdges[j].toId;
736
+ var edgeFromId = node.dynamicEdges[j].fromId;
737
+ for (var k = j+1; k < node.dynamicEdgesLength; k++) {
738
+ if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
739
+ (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
740
+ correction += 1;
741
+ }
742
+ }
743
+ }
744
+ }
745
+ node.dynamicEdgesLength -= correction;
746
+ }
747
+ },
748
+
749
+
750
+ /**
751
+ * This adds an edge from the childNode to the contained edges of the parent node
752
+ *
753
+ * @param parentNode | Node object
754
+ * @param childNode | Node object
755
+ * @param edge | Edge object
756
+ * @private
757
+ */
758
+ _addToContainedEdges : function(parentNode, childNode, edge) {
759
+ // create an array object if it does not yet exist for this childNode
760
+ if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
761
+ parentNode.containedEdges[childNode.id] = []
762
+ }
763
+ // add this edge to the list
764
+ parentNode.containedEdges[childNode.id].push(edge);
765
+
766
+ // remove the edge from the global edges object
767
+ delete this.edges[edge.id];
768
+
769
+ // remove the edge from the parent object
770
+ for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
771
+ if (parentNode.dynamicEdges[i].id == edge.id) {
772
+ parentNode.dynamicEdges.splice(i,1);
773
+ break;
774
+ }
775
+ }
776
+ },
777
+
778
+ /**
779
+ * This function connects an edge that was connected to a child node to the parent node.
780
+ * It keeps track of which nodes it has been connected to with the originalId array.
781
+ *
782
+ * @param {Node} parentNode | Node object
783
+ * @param {Node} childNode | Node object
784
+ * @param {Edge} edge | Edge object
785
+ * @private
786
+ */
787
+ _connectEdgeToCluster : function(parentNode, childNode, edge) {
788
+ // handle circular edges
789
+ if (edge.toId == edge.fromId) {
790
+ this._addToContainedEdges(parentNode, childNode, edge);
791
+ }
792
+ else {
793
+ if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
794
+ edge.originalToId.push(childNode.id);
795
+ edge.to = parentNode;
796
+ edge.toId = parentNode.id;
797
+ }
798
+ else { // edge connected to other node with the "from" side
799
+
800
+ edge.originalFromId.push(childNode.id);
801
+ edge.from = parentNode;
802
+ edge.fromId = parentNode.id;
803
+ }
804
+
805
+ this._addToReroutedEdges(parentNode,childNode,edge);
806
+ }
807
+ },
808
+
809
+
810
+ /**
811
+ * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
812
+ * these edges inside of the cluster.
813
+ *
814
+ * @param parentNode
815
+ * @param childNode
816
+ * @private
817
+ */
818
+ _containCircularEdgesFromNode : function(parentNode, childNode) {
819
+ // manage all the edges connected to the child and parent nodes
820
+ for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
821
+ var edge = parentNode.dynamicEdges[i];
822
+ // handle circular edges
823
+ if (edge.toId == edge.fromId) {
824
+ this._addToContainedEdges(parentNode, childNode, edge);
825
+ }
826
+ }
827
+ },
828
+
829
+
830
+ /**
831
+ * This adds an edge from the childNode to the rerouted edges of the parent node
832
+ *
833
+ * @param parentNode | Node object
834
+ * @param childNode | Node object
835
+ * @param edge | Edge object
836
+ * @private
837
+ */
838
+ _addToReroutedEdges : function(parentNode, childNode, edge) {
839
+ // create an array object if it does not yet exist for this childNode
840
+ // we store the edge in the rerouted edges so we can restore it when the cluster pops open
841
+ if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
842
+ parentNode.reroutedEdges[childNode.id] = [];
843
+ }
844
+ parentNode.reroutedEdges[childNode.id].push(edge);
845
+
846
+ // this edge becomes part of the dynamicEdges of the cluster node
847
+ parentNode.dynamicEdges.push(edge);
848
+ },
849
+
850
+
851
+
852
+ /**
853
+ * This function connects an edge that was connected to a cluster node back to the child node.
854
+ *
855
+ * @param parentNode | Node object
856
+ * @param childNode | Node object
857
+ * @private
858
+ */
859
+ _connectEdgeBackToChild : function(parentNode, childNode) {
860
+ if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
861
+ for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
862
+ var edge = parentNode.reroutedEdges[childNode.id][i];
863
+ if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
864
+ edge.originalFromId.pop();
865
+ edge.fromId = childNode.id;
866
+ edge.from = childNode;
867
+ }
868
+ else {
869
+ edge.originalToId.pop();
870
+ edge.toId = childNode.id;
871
+ edge.to = childNode;
872
+ }
873
+
874
+ // append this edge to the list of edges connecting to the childnode
875
+ childNode.dynamicEdges.push(edge);
876
+
877
+ // remove the edge from the parent object
878
+ for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
879
+ if (parentNode.dynamicEdges[j].id == edge.id) {
880
+ parentNode.dynamicEdges.splice(j,1);
881
+ break;
882
+ }
883
+ }
884
+ }
885
+ // remove the entry from the rerouted edges
886
+ delete parentNode.reroutedEdges[childNode.id];
887
+ }
888
+ },
889
+
890
+
891
+ /**
892
+ * When loops are clustered, an edge can be both in the rerouted array and the contained array.
893
+ * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
894
+ * parentNode
895
+ *
896
+ * @param parentNode | Node object
897
+ * @private
898
+ */
899
+ _validateEdges : function(parentNode) {
900
+ for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
901
+ var edge = parentNode.dynamicEdges[i];
902
+ if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
903
+ parentNode.dynamicEdges.splice(i,1);
904
+ }
905
+ }
906
+ },
907
+
908
+
909
+ /**
910
+ * This function released the contained edges back into the global domain and puts them back into the
911
+ * dynamic edges of both parent and child.
912
+ *
913
+ * @param {Node} parentNode |
914
+ * @param {Node} childNode |
915
+ * @private
916
+ */
917
+ _releaseContainedEdges : function(parentNode, childNode) {
918
+ for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
919
+ var edge = parentNode.containedEdges[childNode.id][i];
920
+
921
+ // put the edge back in the global edges object
922
+ this.edges[edge.id] = edge;
923
+
924
+ // put the edge back in the dynamic edges of the child and parent
925
+ childNode.dynamicEdges.push(edge);
926
+ parentNode.dynamicEdges.push(edge);
927
+ }
928
+ // remove the entry from the contained edges
929
+ delete parentNode.containedEdges[childNode.id];
930
+
931
+ },
932
+
933
+
934
+
935
+
936
+ // ------------------- UTILITY FUNCTIONS ---------------------------- //
937
+
938
+
939
+ /**
940
+ * This updates the node labels for all nodes (for debugging purposes)
941
+ */
942
+ updateLabels : function() {
943
+ var nodeId;
944
+ // update node labels
945
+ for (nodeId in this.nodes) {
946
+ if (this.nodes.hasOwnProperty(nodeId)) {
947
+ var node = this.nodes[nodeId];
948
+ if (node.clusterSize > 1) {
949
+ node.label = "[".concat(String(node.clusterSize),"]");
950
+ }
951
+ }
952
+ }
953
+
954
+ // update node labels
955
+ for (nodeId in this.nodes) {
956
+ if (this.nodes.hasOwnProperty(nodeId)) {
957
+ node = this.nodes[nodeId];
958
+ if (node.clusterSize == 1) {
959
+ if (node.originalLabel !== undefined) {
960
+ node.label = node.originalLabel;
961
+ }
962
+ else {
963
+ node.label = String(node.id);
964
+ }
965
+ }
966
+ }
967
+ }
968
+
969
+ // /* Debug Override */
970
+ // for (nodeId in this.nodes) {
971
+ // if (this.nodes.hasOwnProperty(nodeId)) {
972
+ // node = this.nodes[nodeId];
973
+ // node.label = String(node.level);
974
+ // }
975
+ // }
976
+
977
+ },
978
+
979
+
980
+ /**
981
+ * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
982
+ * if the rest of the nodes are already a few cluster levels in.
983
+ * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
984
+ * clustered enough to the clusterToSmallestNeighbours function.
985
+ */
986
+ normalizeClusterLevels : function() {
987
+ var maxLevel = 0;
988
+ var minLevel = 1e9;
989
+ var clusterLevel = 0;
990
+
991
+ // we loop over all nodes in the list
992
+ for (var nodeId in this.nodes) {
993
+ if (this.nodes.hasOwnProperty(nodeId)) {
994
+ clusterLevel = this.nodes[nodeId].clusterSessions.length;
995
+ if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
996
+ if (minLevel > clusterLevel) {minLevel = clusterLevel;}
997
+ }
998
+ }
999
+
1000
+ if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
1001
+ var amountOfNodes = this.nodeIndices.length;
1002
+ var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
1003
+ // we loop over all nodes in the list
1004
+ for (var nodeId in this.nodes) {
1005
+ if (this.nodes.hasOwnProperty(nodeId)) {
1006
+ if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
1007
+ this._clusterToSmallestNeighbour(this.nodes[nodeId]);
1008
+ }
1009
+ }
1010
+ }
1011
+ this._updateNodeIndexList();
1012
+ this._updateDynamicEdges();
1013
+ // if a cluster was formed, we increase the clusterSession
1014
+ if (this.nodeIndices.length != amountOfNodes) {
1015
+ this.clusterSession += 1;
1016
+ }
1017
+ }
1018
+ },
1019
+
1020
+
1021
+
1022
+ /**
1023
+ * This function determines if the cluster we want to decluster is in the active area
1024
+ * this means around the zoom center
1025
+ *
1026
+ * @param {Node} node
1027
+ * @returns {boolean}
1028
+ * @private
1029
+ */
1030
+ _nodeInActiveArea : function(node) {
1031
+ return (
1032
+ Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
1033
+ &&
1034
+ Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
1035
+ )
1036
+ },
1037
+
1038
+
1039
+ /**
1040
+ * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
1041
+ * It puts large clusters away from the center and randomizes the order.
1042
+ *
1043
+ */
1044
+ repositionNodes : function() {
1045
+ for (var i = 0; i < this.nodeIndices.length; i++) {
1046
+ var node = this.nodes[this.nodeIndices[i]];
1047
+ if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) {
1048
+ var radius = this.constants.physics.springLength * Math.min(100,node.mass);
1049
+ var angle = 2 * Math.PI * Math.random();
1050
+ if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
1051
+ if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
1052
+ this._repositionBezierNodes(node);
1053
+ }
1054
+ }
1055
+ },
1056
+
1057
+
1058
+ /**
1059
+ * We determine how many connections denote an important hub.
1060
+ * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
1061
+ *
1062
+ * @private
1063
+ */
1064
+ _getHubSize : function() {
1065
+ var average = 0;
1066
+ var averageSquared = 0;
1067
+ var hubCounter = 0;
1068
+ var largestHub = 0;
1069
+
1070
+ for (var i = 0; i < this.nodeIndices.length; i++) {
1071
+
1072
+ var node = this.nodes[this.nodeIndices[i]];
1073
+ if (node.dynamicEdgesLength > largestHub) {
1074
+ largestHub = node.dynamicEdgesLength;
1075
+ }
1076
+ average += node.dynamicEdgesLength;
1077
+ averageSquared += Math.pow(node.dynamicEdgesLength,2);
1078
+ hubCounter += 1;
1079
+ }
1080
+ average = average / hubCounter;
1081
+ averageSquared = averageSquared / hubCounter;
1082
+
1083
+ var variance = averageSquared - Math.pow(average,2);
1084
+
1085
+ var standardDeviation = Math.sqrt(variance);
1086
+
1087
+ this.hubThreshold = Math.floor(average + 2*standardDeviation);
1088
+
1089
+ // always have at least one to cluster
1090
+ if (this.hubThreshold > largestHub) {
1091
+ this.hubThreshold = largestHub;
1092
+ }
1093
+
1094
+ // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
1095
+ // console.log("hubThreshold:",this.hubThreshold);
1096
+ },
1097
+
1098
+
1099
+ /**
1100
+ * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
1101
+ * with this amount we can cluster specifically on these chains.
1102
+ *
1103
+ * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
1104
+ * @private
1105
+ */
1106
+ _reduceAmountOfChains : function(fraction) {
1107
+ this.hubThreshold = 2;
1108
+ var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
1109
+ for (var nodeId in this.nodes) {
1110
+ if (this.nodes.hasOwnProperty(nodeId)) {
1111
+ if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
1112
+ if (reduceAmount > 0) {
1113
+ this._formClusterFromHub(this.nodes[nodeId],true,true,1);
1114
+ reduceAmount -= 1;
1115
+ }
1116
+ }
1117
+ }
1118
+ }
1119
+ },
1120
+
1121
+ /**
1122
+ * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
1123
+ * with this amount we can cluster specifically on these chains.
1124
+ *
1125
+ * @private
1126
+ */
1127
+ _getChainFraction : function() {
1128
+ var chains = 0;
1129
+ var total = 0;
1130
+ for (var nodeId in this.nodes) {
1131
+ if (this.nodes.hasOwnProperty(nodeId)) {
1132
+ if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
1133
+ chains += 1;
1134
+ }
1135
+ total += 1;
1136
+ }
1137
+ }
1138
+ return chains/total;
1139
+ }
1140
+
1141
+ };