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.

634 lines
20 KiB

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