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.

620 lines
20 KiB

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