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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.gitmodules +3 -0
- data/.project +11 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/vis/rails/engine.rb +6 -0
- data/lib/vis/rails/version.rb +5 -0
- data/lib/vis/rails.rb +7 -0
- data/vendor/assets/javascripts/vis.js +1 -0
- data/vendor/assets/stylesheets/vis.css +3 -0
- data/vendor/assets/vis/DataSet.js +936 -0
- data/vendor/assets/vis/DataView.js +281 -0
- data/vendor/assets/vis/EventBus.js +89 -0
- data/vendor/assets/vis/events.js +116 -0
- data/vendor/assets/vis/graph/ClusterMixin.js +1019 -0
- data/vendor/assets/vis/graph/Edge.js +620 -0
- data/vendor/assets/vis/graph/Graph.js +2111 -0
- data/vendor/assets/vis/graph/Groups.js +80 -0
- data/vendor/assets/vis/graph/Images.js +41 -0
- data/vendor/assets/vis/graph/NavigationMixin.js +245 -0
- data/vendor/assets/vis/graph/Node.js +978 -0
- data/vendor/assets/vis/graph/Popup.js +105 -0
- data/vendor/assets/vis/graph/SectorsMixin.js +547 -0
- data/vendor/assets/vis/graph/SelectionMixin.js +515 -0
- data/vendor/assets/vis/graph/dotparser.js +829 -0
- data/vendor/assets/vis/graph/img/downarrow.png +0 -0
- data/vendor/assets/vis/graph/img/leftarrow.png +0 -0
- data/vendor/assets/vis/graph/img/minus.png +0 -0
- data/vendor/assets/vis/graph/img/plus.png +0 -0
- data/vendor/assets/vis/graph/img/rightarrow.png +0 -0
- data/vendor/assets/vis/graph/img/uparrow.png +0 -0
- data/vendor/assets/vis/graph/img/zoomExtends.png +0 -0
- data/vendor/assets/vis/graph/shapes.js +225 -0
- data/vendor/assets/vis/module/exports.js +68 -0
- data/vendor/assets/vis/module/header.js +24 -0
- data/vendor/assets/vis/module/imports.js +32 -0
- data/vendor/assets/vis/shim.js +252 -0
- data/vendor/assets/vis/timeline/Controller.js +172 -0
- data/vendor/assets/vis/timeline/Range.js +553 -0
- data/vendor/assets/vis/timeline/Stack.js +192 -0
- data/vendor/assets/vis/timeline/TimeStep.js +449 -0
- data/vendor/assets/vis/timeline/Timeline.js +476 -0
- data/vendor/assets/vis/timeline/component/Component.js +148 -0
- data/vendor/assets/vis/timeline/component/ContentPanel.js +113 -0
- data/vendor/assets/vis/timeline/component/CurrentTime.js +101 -0
- data/vendor/assets/vis/timeline/component/CustomTime.js +255 -0
- data/vendor/assets/vis/timeline/component/Group.js +129 -0
- data/vendor/assets/vis/timeline/component/GroupSet.js +546 -0
- data/vendor/assets/vis/timeline/component/ItemSet.js +612 -0
- data/vendor/assets/vis/timeline/component/Panel.js +112 -0
- data/vendor/assets/vis/timeline/component/RootPanel.js +215 -0
- data/vendor/assets/vis/timeline/component/TimeAxis.js +522 -0
- data/vendor/assets/vis/timeline/component/css/currenttime.css +5 -0
- data/vendor/assets/vis/timeline/component/css/customtime.css +6 -0
- data/vendor/assets/vis/timeline/component/css/groupset.css +59 -0
- data/vendor/assets/vis/timeline/component/css/item.css +93 -0
- data/vendor/assets/vis/timeline/component/css/itemset.css +17 -0
- data/vendor/assets/vis/timeline/component/css/panel.css +14 -0
- data/vendor/assets/vis/timeline/component/css/timeaxis.css +41 -0
- data/vendor/assets/vis/timeline/component/css/timeline.css +2 -0
- data/vendor/assets/vis/timeline/component/item/Item.js +81 -0
- data/vendor/assets/vis/timeline/component/item/ItemBox.js +302 -0
- data/vendor/assets/vis/timeline/component/item/ItemPoint.js +237 -0
- data/vendor/assets/vis/timeline/component/item/ItemRange.js +251 -0
- data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +91 -0
- data/vendor/assets/vis/util.js +673 -0
- data/vis-rails.gemspec +47 -0
- 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
|
+
};
|