vis-rails 0.0.1

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