vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1129 lines
37 KiB

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