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.

594 lines
18 KiB

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