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.

646 lines
20 KiB

  1. /**
  2. * Created by Alex on 2/20/2015.
  3. */
  4. var Node = require('../Node');
  5. var Edge = require('../Edge');
  6. var util = require('../../util');
  7. function ClusterEngine(data,options) {
  8. this.nodes = data.nodes;
  9. this.edges = data.edges;
  10. this.nodeIndices = data.nodeIndices;
  11. this.emitter = data.emitter;
  12. this.clusteredNodes = {};
  13. }
  14. /**
  15. *
  16. * @param hubsize
  17. * @param options
  18. */
  19. ClusterEngine.prototype.clusterByConnectionCount = function(hubsize, options) {
  20. if (hubsize === undefined) {
  21. hubsize = this._getHubSize();
  22. }
  23. else if (tyepof(hubsize) == "object") {
  24. options = this._checkOptions(hubsize);
  25. hubsize = this._getHubSize();
  26. }
  27. var nodesToCluster = [];
  28. for (var i = 0; i < this.nodeIndices.length; i++) {
  29. var node = this.nodes[this.nodeIndices[i]];
  30. if (node.edges.length >= hubsize) {
  31. nodesToCluster.push(node.id);
  32. }
  33. }
  34. for (var i = 0; i < nodesToCluster.length; i++) {
  35. var node = this.nodes[nodesToCluster[i]];
  36. this.clusterByConnection(node,options,{},{},true);
  37. }
  38. this.emitter.emit('dataChanged');
  39. }
  40. /**
  41. * loop over all nodes, check if they adhere to the condition and cluster if needed.
  42. * @param options
  43. * @param doNotUpdateCalculationNodes
  44. */
  45. ClusterEngine.prototype.clusterByNodeData = function(options, doNotUpdateCalculationNodes) {
  46. if (options === undefined) {throw new Error("Cannot call clusterByNodeData without options.");}
  47. if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");}
  48. // check if the options object is fine, append if needed
  49. options = this._checkOptions(options);
  50. var childNodesObj = {};
  51. var childEdgesObj = {}
  52. // collect the nodes that will be in the cluster
  53. for (var i = 0; i < this.nodeIndices.length; i++) {
  54. var nodeId = this.nodeIndices[i];
  55. var clonedOptions = this._cloneOptions(nodeId);
  56. if (options.joinCondition(clonedOptions) == true) {
  57. childNodesObj[nodeId] = this.nodes[nodeId];
  58. }
  59. }
  60. this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
  61. }
  62. /**
  63. * Cluster all nodes in the network that have only 1 edge
  64. * @param options
  65. * @param doNotUpdateCalculationNodes
  66. */
  67. ClusterEngine.prototype.clusterOutliers = function(options, doNotUpdateCalculationNodes) {
  68. options = this._checkOptions(options);
  69. var clusters = []
  70. // collect the nodes that will be in the cluster
  71. for (var i = 0; i < this.nodeIndices.length; i++) {
  72. var childNodesObj = {};
  73. var childEdgesObj = {};
  74. var nodeId = this.nodeIndices[i];
  75. if (this.nodes[nodeId].edges.length == 1) {
  76. var edge = this.nodes[nodeId].edges[0];
  77. var childNodeId = this._getConnectedId(edge, nodeId);
  78. if (childNodeId != nodeId) {
  79. if (options.joinCondition === undefined) {
  80. childNodesObj[nodeId] = this.nodes[nodeId];
  81. childNodesObj[childNodeId] = this.nodes[childNodeId];
  82. }
  83. else {
  84. var clonedOptions = this._cloneOptions(nodeId);
  85. if (options.joinCondition(clonedOptions) == true) {
  86. childNodesObj[nodeId] = this.nodes[nodeId];
  87. }
  88. clonedOptions = this._cloneOptions(childNodeId);
  89. if (options.joinCondition(clonedOptions) == true) {
  90. childNodesObj[childNodeId] = this.nodes[childNodeId];
  91. }
  92. }
  93. clusters.push({nodes:childNodesObj, edges:childEdgesObj})
  94. }
  95. }
  96. }
  97. for (var i = 0; i < clusters.length; i++) {
  98. this._cluster(clusters[i].nodes, clusters[i].edges, options, true)
  99. }
  100. if (doNotUpdateCalculationNodes !== true) {
  101. this.emitter.emit('dataChanged');
  102. }
  103. }
  104. /**
  105. *
  106. * @param nodeId
  107. * @param options
  108. * @param doNotUpdateCalculationNodes
  109. */
  110. ClusterEngine.prototype.clusterByConnection = function(nodeId, options, doNotUpdateCalculationNodes) {
  111. // kill conditions
  112. if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");}
  113. if (this.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");}
  114. var node = this.nodes[nodeId];
  115. options = this._checkOptions(options, node);
  116. if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;}
  117. if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;}
  118. var childNodesObj = {};
  119. var childEdgesObj = {}
  120. var parentNodeId = node.id;
  121. var parentClonedOptions = this._cloneOptions(parentNodeId);
  122. childNodesObj[parentNodeId] = node;
  123. // collect the nodes that will be in the cluster
  124. for (var i = 0; i < node.edges.length; i++) {
  125. var edge = node.edges[i];
  126. var childNodeId = this._getConnectedId(edge, parentNodeId);
  127. if (childNodeId !== parentNodeId) {
  128. if (options.joinCondition === undefined) {
  129. childEdgesObj[edge.id] = edge;
  130. childNodesObj[childNodeId] = this.nodes[childNodeId];
  131. }
  132. else {
  133. // clone the options and insert some additional parameters that could be interesting.
  134. var childClonedOptions = this._cloneOptions(childNodeId);
  135. if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) {
  136. childEdgesObj[edge.id] = edge;
  137. childNodesObj[childNodeId] = this.nodes[childNodeId];
  138. }
  139. }
  140. }
  141. else {
  142. childEdgesObj[edge.id] = edge;
  143. }
  144. }
  145. this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
  146. }
  147. /**
  148. * This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes.
  149. * @param objId
  150. * @param type
  151. * @returns {{}}
  152. * @private
  153. */
  154. ClusterEngine.prototype._cloneOptions = function(objId, type) {
  155. var clonedOptions = {};
  156. if (type === undefined || type == 'node') {
  157. util.deepExtend(clonedOptions, this.nodes[objId].options, true);
  158. util.deepExtend(clonedOptions, this.nodes[objId].properties, true);
  159. clonedOptions.amountOfConnections = this.nodes[objId].edges.length;
  160. }
  161. else {
  162. util.deepExtend(clonedOptions, this.edges[objId].properties, true);
  163. }
  164. return clonedOptions;
  165. }
  166. /**
  167. * This function creates the edges that will be attached to the cluster.
  168. *
  169. * @param childNodesObj
  170. * @param childEdgesObj
  171. * @param newEdges
  172. * @param options
  173. * @private
  174. */
  175. ClusterEngine.prototype._createClusterEdges = function (childNodesObj, childEdgesObj, newEdges, options) {
  176. var edge, childNodeId, childNode;
  177. var childKeys = Object.keys(childNodesObj);
  178. for (var i = 0; i < childKeys.length; i++) {
  179. childNodeId = childKeys[i];
  180. childNode = childNodesObj[childNodeId];
  181. // mark all edges for removal from global and construct new edges from the cluster to others
  182. for (var j = 0; j < childNode.edges.length; j++) {
  183. edge = childNode.edges[j];
  184. childEdgesObj[edge.id] = edge;
  185. var otherNodeId = edge.toId;
  186. var otherOnTo = true;
  187. if (edge.toId != childNodeId) {
  188. otherNodeId = edge.toId;
  189. otherOnTo = true;
  190. }
  191. else if (edge.fromId != childNodeId) {
  192. otherNodeId = edge.fromId;
  193. otherOnTo = false;
  194. }
  195. if (childNodesObj[otherNodeId] === undefined) {
  196. var clonedOptions = this._cloneOptions(edge.id, 'edge');
  197. util.deepExtend(clonedOptions, options.clusterEdgeProperties);
  198. // avoid forcing the default color on edges that inherit color
  199. if (edge.properties.color === undefined) {
  200. delete clonedOptions.color;
  201. }
  202. if (otherOnTo === true) {
  203. clonedOptions.from = options.clusterNodeProperties.id;
  204. clonedOptions.to = otherNodeId;
  205. }
  206. else {
  207. clonedOptions.from = otherNodeId;
  208. clonedOptions.to = options.clusterNodeProperties.id;
  209. }
  210. clonedOptions.id = 'clusterEdge:' + util.randomUUID();
  211. newEdges.push(new Edge(clonedOptions,this,this.constants))
  212. }
  213. }
  214. }
  215. }
  216. /**
  217. * This function checks the options that can be supplied to the different cluster functions
  218. * for certain fields and inserts defaults if needed
  219. * @param options
  220. * @returns {*}
  221. * @private
  222. */
  223. ClusterEngine.prototype._checkOptions = function(options) {
  224. if (options === undefined) {options = {};}
  225. if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};}
  226. if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};}
  227. return options;
  228. }
  229. /**
  230. *
  231. * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node
  232. * @param {Object} childEdgesObj | object with edge objects, id as keys
  233. * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
  234. * @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up
  235. * @private
  236. */
  237. ClusterEngine.prototype._cluster = function(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes) {
  238. // kill condition: no children so cant cluster
  239. if (Object.keys(childNodesObj).length == 0) {return;}
  240. // check if we have an unique id;
  241. if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();}
  242. var clusterId = options.clusterNodeProperties.id;
  243. // create the new edges that will connect to the cluster
  244. var newEdges = [];
  245. this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options);
  246. // construct the clusterNodeProperties
  247. var clusterNodeProperties = options.clusterNodeProperties;
  248. if (options.processProperties !== undefined) {
  249. // get the childNode options
  250. var childNodesOptions = [];
  251. for (var nodeId in childNodesObj) {
  252. var clonedOptions = this._cloneOptions(nodeId);
  253. childNodesOptions.push(clonedOptions);
  254. }
  255. // get clusterproperties based on childNodes
  256. var childEdgesOptions = [];
  257. for (var edgeId in childEdgesObj) {
  258. var clonedOptions = this._cloneOptions(edgeId, 'edge');
  259. childEdgesOptions.push(clonedOptions);
  260. }
  261. clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions);
  262. if (!clusterNodeProperties) {
  263. throw new Error("The processClusterProperties function does not return properties!");
  264. }
  265. }
  266. if (clusterNodeProperties.label === undefined) {
  267. clusterNodeProperties.label = 'cluster';
  268. }
  269. // give the clusterNode a postion if it does not have one.
  270. var pos = undefined
  271. if (clusterNodeProperties.x === undefined) {
  272. pos = this._getClusterPosition(childNodesObj);
  273. clusterNodeProperties.x = pos.x;
  274. clusterNodeProperties.allowedToMoveX = true;
  275. }
  276. if (clusterNodeProperties.x === undefined) {
  277. if (pos === undefined) {
  278. pos = this._getClusterPosition(childNodesObj);
  279. }
  280. clusterNodeProperties.y = pos.y;
  281. clusterNodeProperties.allowedToMoveY = true;
  282. }
  283. // force the ID to remain the same
  284. clusterNodeProperties.id = clusterId;
  285. // create the clusterNode
  286. var clusterNode = new Node(clusterNodeProperties, this.images, this.groups, this.constants);
  287. clusterNode.isCluster = true;
  288. clusterNode.containedNodes = childNodesObj;
  289. clusterNode.containedEdges = childEdgesObj;
  290. // delete contained edges from global
  291. for (var edgeId in childEdgesObj) {
  292. if (childEdgesObj.hasOwnProperty(edgeId)) {
  293. if (this.edges[edgeId] !== undefined) {
  294. if (this.edges[edgeId].via !== null) {
  295. var viaId = this.edges[edgeId].via.id;
  296. if (viaId) {
  297. this.edges[edgeId].via = null
  298. delete this.sectors['support']['nodes'][viaId];
  299. }
  300. }
  301. this.edges[edgeId].disconnect();
  302. delete this.edges[edgeId];
  303. }
  304. }
  305. }
  306. // remove contained nodes from global
  307. for (var nodeId in childNodesObj) {
  308. if (childNodesObj.hasOwnProperty(nodeId)) {
  309. this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.nodes[nodeId]};
  310. delete this.nodes[nodeId];
  311. }
  312. }
  313. // finally put the cluster node into global
  314. this.nodes[clusterNodeProperties.id] = clusterNode;
  315. // push new edges to global
  316. for (var i = 0; i < newEdges.length; i++) {
  317. this.edges[newEdges[i].id] = newEdges[i];
  318. this.edges[newEdges[i].id].connect();
  319. }
  320. // create bezier nodes for smooth curves if needed
  321. this._createBezierNodes(newEdges);
  322. // set ID to undefined so no duplicates arise
  323. clusterNodeProperties.id = undefined;
  324. // wrap up
  325. if (doNotUpdateCalculationNodes !== true) {
  326. this.emitter.emit('dataChanged');
  327. }
  328. }
  329. /**
  330. * Check if a node is a cluster.
  331. * @param nodeId
  332. * @returns {*}
  333. */
  334. ClusterEngine.prototype.isCluster = function(nodeId) {
  335. if (this.nodes[nodeId] !== undefined) {
  336. return this.nodes[nodeId].isCluster;
  337. }
  338. else {
  339. console.log("Node does not exist.")
  340. return false;
  341. }
  342. }
  343. /**
  344. * get the position of the cluster node based on what's inside
  345. * @param {object} childNodesObj | object with node objects, id as keys
  346. * @returns {{x: number, y: number}}
  347. * @private
  348. */
  349. ClusterEngine.prototype._getClusterPosition = function(childNodesObj) {
  350. var childKeys = Object.keys(childNodesObj);
  351. var minX = childNodesObj[childKeys[0]].x;
  352. var maxX = childNodesObj[childKeys[0]].x;
  353. var minY = childNodesObj[childKeys[0]].y;
  354. var maxY = childNodesObj[childKeys[0]].y;
  355. var node;
  356. for (var i = 0; i < childKeys.lenght; i++) {
  357. node = childNodesObj[childKeys[0]];
  358. minX = node.x < minX ? node.x : minX;
  359. maxX = node.x > maxX ? node.x : maxX;
  360. minY = node.y < minY ? node.y : minY;
  361. maxY = node.y > maxY ? node.y : maxY;
  362. }
  363. return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)};
  364. }
  365. /**
  366. * Open a cluster by calling this function.
  367. * @param {String} clusterNodeId | the ID of the cluster node
  368. * @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true
  369. */
  370. ClusterEngine.prototype.openCluster = function(clusterNodeId, doNotUpdateCalculationNodes) {
  371. // kill conditions
  372. if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");}
  373. if (this.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
  374. if (this.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return};
  375. var node = this.nodes[clusterNodeId];
  376. var containedNodes = node.containedNodes;
  377. var containedEdges = node.containedEdges;
  378. // release nodes
  379. for (var nodeId in containedNodes) {
  380. if (containedNodes.hasOwnProperty(nodeId)) {
  381. this.nodes[nodeId] = containedNodes[nodeId];
  382. // inherit position
  383. this.nodes[nodeId].x = node.x;
  384. this.nodes[nodeId].y = node.y;
  385. // inherit speed
  386. this.nodes[nodeId].vx = node.vx;
  387. this.nodes[nodeId].vy = node.vy;
  388. delete this.clusteredNodes[nodeId];
  389. }
  390. }
  391. // release edges
  392. for (var edgeId in containedEdges) {
  393. if (containedEdges.hasOwnProperty(edgeId)) {
  394. this.edges[edgeId] = containedEdges[edgeId];
  395. this.edges[edgeId].connect();
  396. var edge = this.edges[edgeId];
  397. if (edge.connected === false) {
  398. if (this.clusteredNodes[edge.fromId] !== undefined) {
  399. this._connectEdge(edge, edge.fromId, true);
  400. }
  401. if (this.clusteredNodes[edge.toId] !== undefined) {
  402. this._connectEdge(edge, edge.toId, false);
  403. }
  404. }
  405. }
  406. }
  407. this._createBezierNodes(containedEdges);
  408. var edgeIds = [];
  409. for (var i = 0; i < node.edges.length; i++) {
  410. edgeIds.push(node.edges[i].id);
  411. }
  412. // remove edges in clusterNode
  413. for (var i = 0; i < edgeIds.length; i++) {
  414. var edge = this.edges[edgeIds[i]];
  415. // if the edge should have been connected to a contained node
  416. if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) {
  417. // the node in the from array was contained in the cluster
  418. if (this.nodes[edge.fromArray[0].id] !== undefined) {
  419. this._connectEdge(edge, edge.fromArray[0].id, true);
  420. }
  421. }
  422. else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) {
  423. // the node in the to array was contained in the cluster
  424. if (this.nodes[edge.toArray[0].id] !== undefined) {
  425. this._connectEdge(edge, edge.toArray[0].id, false);
  426. }
  427. }
  428. else {
  429. var edgeId = edgeIds[i];
  430. var viaId = this.edges[edgeId].via.id;
  431. if (viaId) {
  432. this.edges[edgeId].via = null
  433. delete this.sectors['support']['nodes'][viaId];
  434. }
  435. // this removes the edge from node.edges, which is why edgeIds is formed
  436. this.edges[edgeId].disconnect();
  437. delete this.edges[edgeId];
  438. }
  439. }
  440. // remove clusterNode
  441. delete this.nodes[clusterNodeId];
  442. if (doNotUpdateCalculationNodes !== true) {
  443. this.emitter.emit('dataChanged');
  444. }
  445. }
  446. /**
  447. * Recalculate navigation nodes, color edges dirty, update nodes list etc.
  448. * @private
  449. */
  450. ClusterEngine.prototype._wrapUp = function() {
  451. this._updateNodeIndexList();
  452. this._updateCalculationNodes();
  453. this._markAllEdgesAsDirty();
  454. if (this.initializing !== true) {
  455. this.moving = true;
  456. this.start();
  457. }
  458. }
  459. /**
  460. * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
  461. * is currently residing in cluster B
  462. * @param edge
  463. * @param nodeId
  464. * @param from
  465. * @private
  466. */
  467. ClusterEngine.prototype._connectEdge = function(edge, nodeId, from) {
  468. var clusterStack = this._getClusterStack(nodeId);
  469. if (from == true) {
  470. edge.from = clusterStack[clusterStack.length - 1];
  471. edge.fromId = clusterStack[clusterStack.length - 1].id;
  472. clusterStack.pop()
  473. edge.fromArray = clusterStack;
  474. }
  475. else {
  476. edge.to = clusterStack[clusterStack.length - 1];
  477. edge.toId = clusterStack[clusterStack.length - 1].id;
  478. clusterStack.pop();
  479. edge.toArray = clusterStack;
  480. }
  481. edge.connect();
  482. }
  483. /**
  484. * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
  485. * @param nodeId
  486. * @returns {Array}
  487. * @private
  488. */
  489. ClusterEngine.prototype._getClusterStack = function(nodeId) {
  490. var stack = [];
  491. var max = 100;
  492. var counter = 0;
  493. while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
  494. stack.push(this.clusteredNodes[nodeId].node);
  495. nodeId = this.clusteredNodes[nodeId].clusterId;
  496. counter++;
  497. }
  498. stack.push(this.nodes[nodeId]);
  499. return stack;
  500. }
  501. /**
  502. * Get the Id the node is connected to
  503. * @param edge
  504. * @param nodeId
  505. * @returns {*}
  506. * @private
  507. */
  508. ClusterEngine.prototype._getConnectedId = function(edge, nodeId) {
  509. if (edge.toId != nodeId) {
  510. return edge.toId;
  511. }
  512. else if (edge.fromId != nodeId) {
  513. return edge.fromId;
  514. }
  515. else {
  516. return edge.fromId;
  517. }
  518. }
  519. /**
  520. * We determine how many connections denote an important hub.
  521. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  522. *
  523. * @private
  524. */
  525. ClusterEngine.prototype._getHubSize = function() {
  526. var average = 0;
  527. var averageSquared = 0;
  528. var hubCounter = 0;
  529. var largestHub = 0;
  530. for (var i = 0; i < this.nodeIndices.length; i++) {
  531. var node = this.nodes[this.nodeIndices[i]];
  532. if (node.edges.length > largestHub) {
  533. largestHub = node.edges.length;
  534. }
  535. average += node.edges.length;
  536. averageSquared += Math.pow(node.edges.length,2);
  537. hubCounter += 1;
  538. }
  539. average = average / hubCounter;
  540. averageSquared = averageSquared / hubCounter;
  541. var variance = averageSquared - Math.pow(average,2);
  542. var standardDeviation = Math.sqrt(variance);
  543. var hubThreshold = Math.floor(average + 2*standardDeviation);
  544. // always have at least one to cluster
  545. if (hubThreshold > largestHub) {
  546. hubThreshold = largestHub;
  547. }
  548. return hubThreshold;
  549. };
  550. module.exports = clusterEngine