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.

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