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.

592 lines
18 KiB

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