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.

1159 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 = 2;
  31. var level = 0;
  32. // we first cluster the hubs, then we pull in the outliers, repeat
  33. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  34. console.log("Performing clustering:", level, numberOfNodes, this.clusterSession);
  35. if (level % 3 == 0.0) {
  36. //this.forceAggregateHubs(true);
  37. //this.normalizeClusterLevels();
  38. }
  39. else {
  40. //this.increaseClusterLevel(); // this also includes a cluster normalization
  41. }
  42. //this.forceAggregateHubs(true);
  43. numberOfNodes = this.nodeIndices.length;
  44. level += 1;
  45. }
  46. console.log("finished")
  47. // after the clustering we reposition the nodes to reduce the initial chaos
  48. if (level > 0 && reposition == true) {
  49. this.repositionNodes();
  50. }
  51. this._updateCalculationNodes();
  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. exports.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._updateCalculationNodes();
  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. exports.updateClustersDefault = function() {
  89. if (this.constants.clustering.enabled == true && this.constants.clustering.clusterByZoom == 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. exports.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. exports.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. * @param {Boolean} doNotStart | if true do not call start
  119. *
  120. */
  121. exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) {
  122. var isMovingBeforeClustering = this.moving;
  123. var amountOfNodes = this.nodeIndices.length;
  124. // on zoom out collapse the sector if the scale is at the level the sector was made
  125. if (this.previousScale > this.scale && zoomDirection == 0) {
  126. this._collapseSector();
  127. }
  128. // check if we zoom in or out
  129. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  130. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  131. // outer nodes determines if it is being clustered
  132. this._formClusters(force);
  133. }
  134. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  135. if (force == true) {
  136. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  137. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  138. this._openClusters(recursive,force);
  139. }
  140. else {
  141. // if a cluster takes up a set percentage of the active window
  142. this._openClustersBySize();
  143. }
  144. }
  145. this._updateNodeIndexList();
  146. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  147. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  148. this._aggregateHubs(force);
  149. this._updateNodeIndexList();
  150. }
  151. // we now reduce chains.
  152. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  153. this.handleChains();
  154. this._updateNodeIndexList();
  155. }
  156. this.previousScale = this.scale;
  157. // rest of the update the index list, dynamic edges and labels
  158. this._updateDynamicEdges();
  159. this.updateLabels();
  160. // if a cluster was formed, we increase the clusterSession
  161. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  162. this.clusterSession += 1;
  163. // if clusters have been made, we normalize the cluster level
  164. this.normalizeClusterLevels();
  165. }
  166. if (doNotStart == false || doNotStart === undefined) {
  167. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  168. if (this.moving != isMovingBeforeClustering) {
  169. this.start();
  170. }
  171. }
  172. this._updateCalculationNodes();
  173. };
  174. /**
  175. * This function handles the chains. It is called on every updateClusters().
  176. */
  177. exports.handleChains = function() {
  178. // after clustering we check how many chains there are
  179. var chainPercentage = this._getChainFraction();
  180. if (chainPercentage > this.constants.clustering.chainThreshold) {
  181. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  182. }
  183. };
  184. /**
  185. * this functions starts clustering by hubs
  186. * The minimum hub threshold is set globally
  187. *
  188. * @private
  189. */
  190. exports._aggregateHubs = function(force) {
  191. this._getHubSize();
  192. this._formClustersByHub(force,false);
  193. };
  194. /**
  195. * This function forces hubs to form.
  196. *
  197. */
  198. exports.forceAggregateHubs = function(doNotStart) {
  199. var isMovingBeforeClustering = this.moving;
  200. var amountOfNodes = this.nodeIndices.length;
  201. this._aggregateHubs(true);
  202. // update the index list, dynamic edges and labels
  203. this._updateNodeIndexList();
  204. this._updateCalculationNodes();
  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. exports._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. exports._openClusters = function(recursive,force) {
  243. for (var i = 0; i < this.nodeIndices.length; i++) {
  244. var node = this.nodes[this.nodeIndices[i]];
  245. this._expandClusterNode(node,recursive,force);
  246. this._updateCalculationNodes();
  247. }
  248. };
  249. /**
  250. * This function checks if a node has to be opened. This is done by checking the zoom level.
  251. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  252. * This recursive behaviour is optional and can be set by the recursive argument.
  253. *
  254. * @param {Node} parentNode | to check for cluster and expand
  255. * @param {Boolean} recursive | enabled or disable recursive calling
  256. * @param {Boolean} force | enabled or disable forcing
  257. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  258. * @private
  259. */
  260. exports._expandClusterNode = function(parentNode, recursive, force, openAll) {
  261. // first check if node is a cluster
  262. if (parentNode.clusterSize > 1) {
  263. // 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. exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) {
  308. var childNode = parentNode.containedNodes[containedNodeId]
  309. // if child node has been added on smaller scale than current, kick out
  310. if (childNode.formationScale < this.scale || force == true) {
  311. // unselect all selected items
  312. this._unselectAll();
  313. // put the child node back in the global nodes object
  314. this.nodes[containedNodeId] = childNode;
  315. // release the contained edges from this childNode back into the global edges
  316. this._releaseContainedEdges(parentNode,childNode);
  317. // reconnect rerouted edges to the childNode
  318. this._connectEdgeBackToChild(parentNode,childNode);
  319. // validate all edges in dynamicEdges
  320. this._validateEdges(parentNode);
  321. // undo the changes from the clustering operation on the parent node
  322. parentNode.options.mass -= childNode.options.mass;
  323. parentNode.clusterSize -= childNode.clusterSize;
  324. parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*(parentNode.clusterSize-1));
  325. 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. exports._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. exports._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. exports._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.options.mass > edge.from.options.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. exports._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.options.mass > childNode.options.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. exports._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. exports._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. exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  508. if (absorptionSizeOffset === undefined) {
  509. absorptionSizeOffset = 0;
  510. }
  511. if (hubNode.dynamicEdgesLength < 0) {
  512. console.error(hubNode.dynamicEdgesLength, this.hubThreshold, onlyEqual)
  513. }
  514. // we decide if the node is a hub
  515. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  516. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  517. // initialize variables
  518. var dx,dy,length;
  519. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  520. var allowCluster = false;
  521. // we create a list of edges because the dynamicEdges change over the course of this loop
  522. var edgesIdarray = [];
  523. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  524. for (var j = 0; j < amountOfInitialEdges; j++) {
  525. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  526. }
  527. // if the hub clustering is not forced, we check if one of the edges connected
  528. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  529. if (force == false) {
  530. allowCluster = false;
  531. for (j = 0; j < amountOfInitialEdges; j++) {
  532. var edge = this.edges[edgesIdarray[j]];
  533. if (edge !== undefined) {
  534. if (edge.connected) {
  535. if (edge.toId != edge.fromId) {
  536. dx = (edge.to.x - edge.from.x);
  537. dy = (edge.to.y - edge.from.y);
  538. length = Math.sqrt(dx * dx + dy * dy);
  539. if (length < minLength) {
  540. allowCluster = true;
  541. break;
  542. }
  543. }
  544. }
  545. }
  546. }
  547. }
  548. // start the clustering if allowed
  549. if ((!force && allowCluster) || force) {
  550. var children = [];
  551. var childrenIds = {};
  552. // we loop over all edges INITIALLY connected to this hub to get a list of the childNodes
  553. for (j = 0; j < amountOfInitialEdges; j++) {
  554. edge = this.edges[edgesIdarray[j]];
  555. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  556. if (childrenIds[childNode.id] === undefined) {
  557. childrenIds[childNode.id] = true;
  558. children.push(childNode);
  559. }
  560. }
  561. for (j = 0; j < children.length; j++) {
  562. var childNode = children[j];
  563. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  564. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  565. (childNode.id != hubNode.id)) {
  566. this._addToCluster(hubNode,childNode,force);
  567. }
  568. }
  569. }
  570. }
  571. };
  572. /**
  573. * This function adds the child node to the parent node, creating a cluster if it is not already.
  574. *
  575. * @param {Node} parentNode | this is the node that will house the child node
  576. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  577. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  578. * @private
  579. */
  580. exports._addToCluster = function(parentNode, childNode, force) {
  581. // join child node in the parent node
  582. parentNode.containedNodes[childNode.id] = childNode;
  583. //console.log(parentNode.id, childNode.id)
  584. // manage all the edges connected to the child and parent nodes
  585. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  586. var edge = childNode.dynamicEdges[i];
  587. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  588. //console.log("COLLECT",parentNode.id, childNode.id, edge.toId, edge.fromId)
  589. this._addToContainedEdges(parentNode,childNode,edge);
  590. }
  591. else {
  592. //console.log("REWIRE",parentNode.id, childNode.id, edge.toId, edge.fromId)
  593. this._connectEdgeToCluster(parentNode,childNode,edge);
  594. }
  595. }
  596. // a contained node has no dynamic edges.
  597. childNode.dynamicEdges = [];
  598. // remove circular edges from clusters
  599. this._containCircularEdgesFromNode(parentNode,childNode);
  600. // remove the childNode from the global nodes object
  601. delete this.nodes[childNode.id];
  602. // update the properties of the child and parent
  603. var massBefore = parentNode.options.mass;
  604. childNode.clusterSession = this.clusterSession;
  605. parentNode.options.mass += childNode.options.mass;
  606. parentNode.clusterSize += childNode.clusterSize;
  607. parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  608. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  609. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  610. parentNode.clusterSessions.push(this.clusterSession);
  611. }
  612. // forced clusters only open from screen size and double tap
  613. if (force == true) {
  614. parentNode.formationScale = 0;
  615. }
  616. else {
  617. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  618. }
  619. // recalculate the size of the node on the next time the node is rendered
  620. parentNode.clearSizeCache();
  621. // set the pop-out scale for the childnode
  622. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  623. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  624. childNode.clearVelocity();
  625. // the mass has altered, preservation of energy dictates the velocity to be updated
  626. parentNode.updateVelocity(massBefore);
  627. // restart the simulation to reorganise all nodes
  628. this.moving = true;
  629. };
  630. /**
  631. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  632. * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
  633. * It has to be called if a level is collapsed. It is called by _formClusters().
  634. * @private
  635. */
  636. exports._updateDynamicEdges = function() {
  637. for (var i = 0; i < this.nodeIndices.length; i++) {
  638. var node = this.nodes[this.nodeIndices[i]];
  639. node.dynamicEdgesLength = node.dynamicEdges.length;
  640. // this corrects for multiple edges pointing at the same other node
  641. var correction = 0;
  642. if (node.dynamicEdgesLength > 1) {
  643. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  644. var edgeToId = node.dynamicEdges[j].toId;
  645. var edgeFromId = node.dynamicEdges[j].fromId;
  646. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  647. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  648. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  649. correction += 1;
  650. }
  651. }
  652. }
  653. }
  654. if (node.dynamicEdgesLength < correction) {
  655. console.error("overshoot", node.dynamicEdgesLength, correction)
  656. }
  657. node.dynamicEdgesLength -= correction;
  658. }
  659. };
  660. /**
  661. * This adds an edge from the childNode to the contained edges of the parent node
  662. *
  663. * @param parentNode | Node object
  664. * @param childNode | Node object
  665. * @param edge | Edge object
  666. * @private
  667. */
  668. exports._addToContainedEdges = function(parentNode, childNode, edge) {
  669. // create an array object if it does not yet exist for this childNode
  670. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  671. parentNode.containedEdges[childNode.id] = []
  672. }
  673. // add this edge to the list
  674. parentNode.containedEdges[childNode.id].push(edge);
  675. // remove the edge from the global edges object
  676. delete this.edges[edge.id];
  677. // remove the edge from the parent object
  678. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  679. if (parentNode.dynamicEdges[i].id == edge.id) {
  680. parentNode.dynamicEdges.splice(i,1);
  681. break;
  682. }
  683. }
  684. };
  685. /**
  686. * This function connects an edge that was connected to a child node to the parent node.
  687. * It keeps track of which nodes it has been connected to with the originalId array.
  688. *
  689. * @param {Node} parentNode | Node object
  690. * @param {Node} childNode | Node object
  691. * @param {Edge} edge | Edge object
  692. * @private
  693. */
  694. exports._connectEdgeToCluster = function(parentNode, childNode, edge) {
  695. // handle circular edges
  696. if (edge.toId == edge.fromId) {
  697. this._addToContainedEdges(parentNode, childNode, edge);
  698. }
  699. else {
  700. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  701. edge.originalToId.push(childNode.id);
  702. edge.to = parentNode;
  703. edge.toId = parentNode.id;
  704. }
  705. else { // edge connected to other node with the "from" side
  706. edge.originalFromId.push(childNode.id);
  707. edge.from = parentNode;
  708. edge.fromId = parentNode.id;
  709. }
  710. this._addToReroutedEdges(parentNode,childNode,edge);
  711. }
  712. };
  713. /**
  714. * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
  715. * these edges inside of the cluster.
  716. *
  717. * @param parentNode
  718. * @param childNode
  719. * @private
  720. */
  721. exports._containCircularEdgesFromNode = function(parentNode, childNode) {
  722. // manage all the edges connected to the child and parent nodes
  723. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  724. var edge = parentNode.dynamicEdges[i];
  725. // handle circular edges
  726. if (edge.toId == edge.fromId) {
  727. this._addToContainedEdges(parentNode, childNode, edge);
  728. }
  729. }
  730. };
  731. /**
  732. * This adds an edge from the childNode to the rerouted edges of the parent node
  733. *
  734. * @param parentNode | Node object
  735. * @param childNode | Node object
  736. * @param edge | Edge object
  737. * @private
  738. */
  739. exports._addToReroutedEdges = function(parentNode, childNode, edge) {
  740. // create an array object if it does not yet exist for this childNode
  741. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  742. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  743. parentNode.reroutedEdges[childNode.id] = [];
  744. }
  745. parentNode.reroutedEdges[childNode.id].push(edge);
  746. // this edge becomes part of the dynamicEdges of the cluster node
  747. parentNode.dynamicEdges.push(edge);
  748. };
  749. /**
  750. * This function connects an edge that was connected to a cluster node back to the child node.
  751. *
  752. * @param parentNode | Node object
  753. * @param childNode | Node object
  754. * @private
  755. */
  756. exports._connectEdgeBackToChild = function(parentNode, childNode) {
  757. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  758. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  759. var edge = parentNode.reroutedEdges[childNode.id][i];
  760. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  761. edge.originalFromId.pop();
  762. edge.fromId = childNode.id;
  763. edge.from = childNode;
  764. }
  765. else {
  766. edge.originalToId.pop();
  767. edge.toId = childNode.id;
  768. edge.to = childNode;
  769. }
  770. // append this edge to the list of edges connecting to the childnode
  771. childNode.dynamicEdges.push(edge);
  772. // remove the edge from the parent object
  773. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  774. if (parentNode.dynamicEdges[j].id == edge.id) {
  775. parentNode.dynamicEdges.splice(j,1);
  776. break;
  777. }
  778. }
  779. }
  780. // remove the entry from the rerouted edges
  781. delete parentNode.reroutedEdges[childNode.id];
  782. }
  783. };
  784. /**
  785. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  786. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  787. * parentNode
  788. *
  789. * @param parentNode | Node object
  790. * @private
  791. */
  792. exports._validateEdges = function(parentNode) {
  793. var dynamicEdges = []
  794. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  795. var edge = parentNode.dynamicEdges[i];
  796. if (parentNode.id == edge.toId || parentNode.id == edge.fromId) {
  797. dynamicEdges.push(edge);
  798. }
  799. }
  800. parentNode.dynamicEdges = dynamicEdges;
  801. };
  802. /**
  803. * This function released the contained edges back into the global domain and puts them back into the
  804. * dynamic edges of both parent and child.
  805. *
  806. * @param {Node} parentNode |
  807. * @param {Node} childNode |
  808. * @private
  809. */
  810. exports._releaseContainedEdges = function(parentNode, childNode) {
  811. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  812. var edge = parentNode.containedEdges[childNode.id][i];
  813. // put the edge back in the global edges object
  814. this.edges[edge.id] = edge;
  815. // put the edge back in the dynamic edges of the child and parent
  816. childNode.dynamicEdges.push(edge);
  817. parentNode.dynamicEdges.push(edge);
  818. }
  819. // remove the entry from the contained edges
  820. delete parentNode.containedEdges[childNode.id];
  821. };
  822. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  823. /**
  824. * This updates the node labels for all nodes (for debugging purposes)
  825. */
  826. exports.updateLabels = function() {
  827. var nodeId;
  828. // update node labels
  829. for (nodeId in this.nodes) {
  830. if (this.nodes.hasOwnProperty(nodeId)) {
  831. var node = this.nodes[nodeId];
  832. if (node.clusterSize > 1) {
  833. node.label = "[".concat(String(node.clusterSize),"]");
  834. }
  835. }
  836. }
  837. // update node labels
  838. for (nodeId in this.nodes) {
  839. if (this.nodes.hasOwnProperty(nodeId)) {
  840. node = this.nodes[nodeId];
  841. if (node.clusterSize == 1) {
  842. if (node.originalLabel !== undefined) {
  843. node.label = node.originalLabel;
  844. }
  845. else {
  846. node.label = String(node.id);
  847. }
  848. }
  849. }
  850. }
  851. // /* Debug Override */
  852. // for (nodeId in this.nodes) {
  853. // if (this.nodes.hasOwnProperty(nodeId)) {
  854. // node = this.nodes[nodeId];
  855. // node.label = String(node.level);
  856. // }
  857. // }
  858. };
  859. /**
  860. * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
  861. * if the rest of the nodes are already a few cluster levels in.
  862. * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
  863. * clustered enough to the clusterToSmallestNeighbours function.
  864. */
  865. exports.normalizeClusterLevels = function() {
  866. var maxLevel = 0;
  867. var minLevel = 1e9;
  868. var clusterLevel = 0;
  869. var nodeId;
  870. // we loop over all nodes in the list
  871. for (nodeId in this.nodes) {
  872. if (this.nodes.hasOwnProperty(nodeId)) {
  873. clusterLevel = this.nodes[nodeId].clusterSessions.length;
  874. if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
  875. if (minLevel > clusterLevel) {minLevel = clusterLevel;}
  876. }
  877. }
  878. if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
  879. var amountOfNodes = this.nodeIndices.length;
  880. var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
  881. // we loop over all nodes in the list
  882. for (nodeId in this.nodes) {
  883. if (this.nodes.hasOwnProperty(nodeId)) {
  884. if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
  885. this._clusterToSmallestNeighbour(this.nodes[nodeId]);
  886. }
  887. }
  888. }
  889. this._updateNodeIndexList();
  890. this._updateDynamicEdges();
  891. // if a cluster was formed, we increase the clusterSession
  892. if (this.nodeIndices.length != amountOfNodes) {
  893. this.clusterSession += 1;
  894. }
  895. }
  896. };
  897. /**
  898. * This function determines if the cluster we want to decluster is in the active area
  899. * this means around the zoom center
  900. *
  901. * @param {Node} node
  902. * @returns {boolean}
  903. * @private
  904. */
  905. exports._nodeInActiveArea = function(node) {
  906. return (
  907. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  908. &&
  909. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  910. )
  911. };
  912. /**
  913. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  914. * It puts large clusters away from the center and randomizes the order.
  915. *
  916. */
  917. exports.repositionNodes = function() {
  918. for (var i = 0; i < this.nodeIndices.length; i++) {
  919. var node = this.nodes[this.nodeIndices[i]];
  920. if ((node.xFixed == false || node.yFixed == false)) {
  921. var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.options.mass);
  922. var angle = 2 * Math.PI * Math.random();
  923. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  924. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  925. this._repositionBezierNodes(node);
  926. }
  927. }
  928. };
  929. /**
  930. * We determine how many connections denote an important hub.
  931. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  932. *
  933. * @private
  934. */
  935. exports._getHubSize = function() {
  936. var average = 0;
  937. var averageSquared = 0;
  938. var hubCounter = 0;
  939. var largestHub = 0;
  940. for (var i = 0; i < this.nodeIndices.length; i++) {
  941. var node = this.nodes[this.nodeIndices[i]];
  942. if (node.dynamicEdgesLength > largestHub) {
  943. largestHub = node.dynamicEdgesLength;
  944. }
  945. average += node.dynamicEdgesLength;
  946. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  947. hubCounter += 1;
  948. }
  949. average = average / hubCounter;
  950. averageSquared = averageSquared / hubCounter;
  951. var variance = averageSquared - Math.pow(average,2);
  952. var standardDeviation = Math.sqrt(variance);
  953. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  954. // always have at least one to cluster
  955. if (this.hubThreshold > largestHub) {
  956. this.hubThreshold = largestHub;
  957. }
  958. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  959. // console.log("hubThreshold:",this.hubThreshold);
  960. };
  961. /**
  962. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  963. * with this amount we can cluster specifically on these chains.
  964. *
  965. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  966. * @private
  967. */
  968. exports._reduceAmountOfChains = function(fraction) {
  969. this.hubThreshold = 2;
  970. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  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. if (reduceAmount > 0) {
  975. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  976. reduceAmount -= 1;
  977. }
  978. }
  979. }
  980. }
  981. };
  982. /**
  983. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  984. * with this amount we can cluster specifically on these chains.
  985. *
  986. * @private
  987. */
  988. exports._getChainFraction = function() {
  989. var chains = 0;
  990. var total = 0;
  991. for (var nodeId in this.nodes) {
  992. if (this.nodes.hasOwnProperty(nodeId)) {
  993. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  994. chains += 1;
  995. }
  996. total += 1;
  997. }
  998. }
  999. return chains/total;
  1000. };