vis-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ };