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.

1140 lines
39 KiB

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