vis-rails 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ };