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.

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