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.

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