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.

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