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.

612 lines
19 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,{},{},false);
  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 refreshData
  40. */
  41. clusterByNodeData(options = {}, refreshData = true) {
  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, refreshData);
  56. }
  57. /**
  58. * Cluster all nodes in the network that have only 1 edge
  59. * @param options
  60. * @param refreshData
  61. */
  62. clusterOutliers(options, refreshData = true) {
  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, false)
  94. }
  95. if (refreshData === true) {
  96. this.body.emitter.emit('_dataChanged');
  97. }
  98. }
  99. /**
  100. *
  101. * @param nodeId
  102. * @param options
  103. * @param refreshData
  104. */
  105. clusterByConnection(nodeId, options, refreshData = true) {
  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, refreshData);
  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} refreshData | when true, do not wrap up
  225. * @private
  226. */
  227. _cluster(childNodesObj, childEdgesObj, options, refreshData = true) {
  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. // set ID to undefined so no duplicates arise
  311. clusterNodeProperties.id = undefined;
  312. // wrap up
  313. if (refreshData === true) {
  314. this.body.emitter.emit('_dataChanged');
  315. }
  316. }
  317. /**
  318. * Check if a node is a cluster.
  319. * @param nodeId
  320. * @returns {*}
  321. */
  322. isCluster(nodeId) {
  323. if (this.body.nodes[nodeId] !== undefined) {
  324. return this.body.nodes[nodeId].isCluster;
  325. }
  326. else {
  327. console.log("Node does not exist.")
  328. return false;
  329. }
  330. }
  331. /**
  332. * get the position of the cluster node based on what's inside
  333. * @param {object} childNodesObj | object with node objects, id as keys
  334. * @returns {{x: number, y: number}}
  335. * @private
  336. */
  337. _getClusterPosition(childNodesObj) {
  338. var childKeys = Object.keys(childNodesObj);
  339. var minX = childNodesObj[childKeys[0]].x;
  340. var maxX = childNodesObj[childKeys[0]].x;
  341. var minY = childNodesObj[childKeys[0]].y;
  342. var maxY = childNodesObj[childKeys[0]].y;
  343. var node;
  344. for (var i = 0; i < childKeys.lenght; i++) {
  345. node = childNodesObj[childKeys[0]];
  346. minX = node.x < minX ? node.x : minX;
  347. maxX = node.x > maxX ? node.x : maxX;
  348. minY = node.y < minY ? node.y : minY;
  349. maxY = node.y > maxY ? node.y : maxY;
  350. }
  351. return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)};
  352. }
  353. /**
  354. * Open a cluster by calling this function.
  355. * @param {String} clusterNodeId | the ID of the cluster node
  356. * @param {Boolean} refreshData | wrap up afterwards if not true
  357. */
  358. openCluster(clusterNodeId, refreshData = true) {
  359. // kill conditions
  360. if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");}
  361. if (this.body.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
  362. if (this.body.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return};
  363. var node = this.body.nodes[clusterNodeId];
  364. var containedNodes = node.containedNodes;
  365. var containedEdges = node.containedEdges;
  366. // release nodes
  367. for (var nodeId in containedNodes) {
  368. if (containedNodes.hasOwnProperty(nodeId)) {
  369. this.body.nodes[nodeId] = containedNodes[nodeId];
  370. // inherit position
  371. this.body.nodes[nodeId].x = node.x;
  372. this.body.nodes[nodeId].y = node.y;
  373. // inherit speed
  374. this.body.nodes[nodeId].vx = node.vx;
  375. this.body.nodes[nodeId].vy = node.vy;
  376. delete this.clusteredNodes[nodeId];
  377. }
  378. }
  379. // release edges
  380. for (var edgeId in containedEdges) {
  381. if (containedEdges.hasOwnProperty(edgeId)) {
  382. this.body.edges[edgeId] = containedEdges[edgeId];
  383. this.body.edges[edgeId].connect();
  384. var edge = this.body.edges[edgeId];
  385. if (edge.connected === false) {
  386. if (this.clusteredNodes[edge.fromId] !== undefined) {
  387. this._connectEdge(edge, edge.fromId, true);
  388. }
  389. if (this.clusteredNodes[edge.toId] !== undefined) {
  390. this._connectEdge(edge, edge.toId, false);
  391. }
  392. }
  393. }
  394. }
  395. var edgeIds = [];
  396. for (var i = 0; i < node.edges.length; i++) {
  397. edgeIds.push(node.edges[i].id);
  398. }
  399. // remove edges in clusterNode
  400. for (var i = 0; i < edgeIds.length; i++) {
  401. var edge = this.body.edges[edgeIds[i]];
  402. // if the edge should have been connected to a contained node
  403. if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) {
  404. // the node in the from array was contained in the cluster
  405. if (this.body.nodes[edge.fromArray[0].id] !== undefined) {
  406. this._connectEdge(edge, edge.fromArray[0].id, true);
  407. }
  408. }
  409. else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) {
  410. // the node in the to array was contained in the cluster
  411. if (this.body.nodes[edge.toArray[0].id] !== undefined) {
  412. this._connectEdge(edge, edge.toArray[0].id, false);
  413. }
  414. }
  415. else {
  416. var edgeId = edgeIds[i];
  417. var viaId = this.body.edges[edgeId].via.id;
  418. if (viaId) {
  419. this.body.edges[edgeId].via = null
  420. delete this.body.supportNodes[viaId];
  421. }
  422. // this removes the edge from node.edges, which is why edgeIds is formed
  423. this.body.edges[edgeId].disconnect();
  424. delete this.body.edges[edgeId];
  425. }
  426. }
  427. // remove clusterNode
  428. delete this.body.nodes[clusterNodeId];
  429. if (refreshData === true) {
  430. this.body.emitter.emit('_dataChanged');
  431. }
  432. }
  433. /**
  434. * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
  435. * is currently residing in cluster B
  436. * @param edge
  437. * @param nodeId
  438. * @param from
  439. * @private
  440. */
  441. _connectEdge(edge, nodeId, from) {
  442. var clusterStack = this._getClusterStack(nodeId);
  443. if (from == true) {
  444. edge.from = clusterStack[clusterStack.length - 1];
  445. edge.fromId = clusterStack[clusterStack.length - 1].id;
  446. clusterStack.pop()
  447. edge.fromArray = clusterStack;
  448. }
  449. else {
  450. edge.to = clusterStack[clusterStack.length - 1];
  451. edge.toId = clusterStack[clusterStack.length - 1].id;
  452. clusterStack.pop();
  453. edge.toArray = clusterStack;
  454. }
  455. edge.connect();
  456. }
  457. /**
  458. * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
  459. * @param nodeId
  460. * @returns {Array}
  461. * @private
  462. */
  463. _getClusterStack(nodeId) {
  464. var stack = [];
  465. var max = 100;
  466. var counter = 0;
  467. while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
  468. stack.push(this.clusteredNodes[nodeId].node);
  469. nodeId = this.clusteredNodes[nodeId].clusterId;
  470. counter++;
  471. }
  472. stack.push(this.body.nodes[nodeId]);
  473. return stack;
  474. }
  475. /**
  476. * Get the Id the node is connected to
  477. * @param edge
  478. * @param nodeId
  479. * @returns {*}
  480. * @private
  481. */
  482. _getConnectedId(edge, nodeId) {
  483. if (edge.toId != nodeId) {
  484. return edge.toId;
  485. }
  486. else if (edge.fromId != nodeId) {
  487. return edge.fromId;
  488. }
  489. else {
  490. return edge.fromId;
  491. }
  492. }
  493. /**
  494. * We determine how many connections denote an important hub.
  495. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  496. *
  497. * @private
  498. */
  499. _getHubSize() {
  500. var average = 0;
  501. var averageSquared = 0;
  502. var hubCounter = 0;
  503. var largestHub = 0;
  504. for (var i = 0; i < this.body.nodeIndices.length; i++) {
  505. var node = this.body.nodes[this.body.nodeIndices[i]];
  506. if (node.edges.length > largestHub) {
  507. largestHub = node.edges.length;
  508. }
  509. average += node.edges.length;
  510. averageSquared += Math.pow(node.edges.length,2);
  511. hubCounter += 1;
  512. }
  513. average = average / hubCounter;
  514. averageSquared = averageSquared / hubCounter;
  515. var variance = averageSquared - Math.pow(average,2);
  516. var standardDeviation = Math.sqrt(variance);
  517. var hubThreshold = Math.floor(average + 2*standardDeviation);
  518. // always have at least one to cluster
  519. if (hubThreshold > largestHub) {
  520. hubThreshold = largestHub;
  521. }
  522. return hubThreshold;
  523. };
  524. }
  525. export default ClusterEngine;