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.

587 lines
18 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. let 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. 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 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. let clusters = [];
  70. // collect the nodes that will be in the cluster
  71. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  72. let childNodesObj = {};
  73. let childEdgesObj = {};
  74. let nodeId = this.body.nodeIndices[i];
  75. if (this.body.nodes[nodeId].edges.length === 1) {
  76. let edge = this.body.nodes[nodeId].edges[0];
  77. let 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. let 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 (let 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. let 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. let childNodesObj = {};
  124. let childEdgesObj = {}
  125. let parentNodeId = node.id;
  126. let parentClonedOptions = this._cloneOptions(parentNodeId);
  127. childNodesObj[parentNodeId] = node;
  128. // collect the nodes that will be in the cluster
  129. for (let i = 0; i < node.edges.length; i++) {
  130. let edge = node.edges[i];
  131. let 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. let 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. let clonedOptions = {};
  161. if (type === undefined || type === 'node') {
  162. util.deepExtend(clonedOptions, this.body.nodes[objId].options, true);
  163. clonedOptions.x = this.body.nodes[objId].x;
  164. clonedOptions.y = this.body.nodes[objId].y;
  165. clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length;
  166. }
  167. else {
  168. util.deepExtend(clonedOptions, this.body.edges[objId].options, true);
  169. }
  170. return clonedOptions;
  171. }
  172. /**
  173. * This function creates the edges that will be attached to the cluster.
  174. *
  175. * @param childNodesObj
  176. * @param childEdgesObj
  177. * @param newEdges
  178. * @param options
  179. * @private
  180. */
  181. _createClusterEdges (childNodesObj, childEdgesObj, newEdges, options) {
  182. let edge, childNodeId, childNode;
  183. let childKeys = Object.keys(childNodesObj);
  184. for (let i = 0; i < childKeys.length; i++) {
  185. childNodeId = childKeys[i];
  186. childNode = childNodesObj[childNodeId];
  187. // mark all edges for removal from global and construct new edges from the cluster to others
  188. for (let j = 0; j < childNode.edges.length; j++) {
  189. edge = childNode.edges[j];
  190. childEdgesObj[edge.id] = edge;
  191. let otherNodeId = edge.toId;
  192. let otherOnTo = true;
  193. if (edge.toId != childNodeId) {
  194. otherNodeId = edge.toId;
  195. otherOnTo = true;
  196. }
  197. else if (edge.fromId != childNodeId) {
  198. otherNodeId = edge.fromId;
  199. otherOnTo = false;
  200. }
  201. if (childNodesObj[otherNodeId] === undefined) {
  202. let clonedOptions = this._cloneOptions(edge.id, 'edge');
  203. util.deepExtend(clonedOptions, options.clusterEdgeProperties);
  204. if (otherOnTo === true) {
  205. clonedOptions.from = options.clusterNodeProperties.id;
  206. clonedOptions.to = otherNodeId;
  207. }
  208. else {
  209. clonedOptions.from = otherNodeId;
  210. clonedOptions.to = options.clusterNodeProperties.id;
  211. }
  212. clonedOptions.id = 'clusterEdge:' + util.randomUUID();
  213. newEdges.push(this.body.functions.createEdge(clonedOptions))
  214. }
  215. }
  216. }
  217. }
  218. /**
  219. * This function checks the options that can be supplied to the different cluster functions
  220. * for certain fields and inserts defaults if needed
  221. * @param options
  222. * @returns {*}
  223. * @private
  224. */
  225. _checkOptions(options = {}) {
  226. if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};}
  227. if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};}
  228. return options;
  229. }
  230. /**
  231. *
  232. * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node
  233. * @param {Object} childEdgesObj | object with edge objects, id as keys
  234. * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
  235. * @param {Boolean} refreshData | when true, do not wrap up
  236. * @private
  237. */
  238. _cluster(childNodesObj, childEdgesObj, options, refreshData = true) {
  239. // kill condition: no children so cant cluster
  240. if (Object.keys(childNodesObj).length === 0) {return;}
  241. // check if we have an unique id;
  242. if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();}
  243. let clusterId = options.clusterNodeProperties.id;
  244. // construct the clusterNodeProperties
  245. let clusterNodeProperties = options.clusterNodeProperties;
  246. if (options.processProperties !== undefined) {
  247. // get the childNode options
  248. let childNodesOptions = [];
  249. for (let nodeId in childNodesObj) {
  250. let clonedOptions = this._cloneOptions(nodeId);
  251. childNodesOptions.push(clonedOptions);
  252. }
  253. // get clusterproperties based on childNodes
  254. let childEdgesOptions = [];
  255. for (let edgeId in childEdgesObj) {
  256. let 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. let pos = undefined;
  269. if (clusterNodeProperties.x === undefined) {
  270. pos = this._getClusterPosition(childNodesObj);
  271. clusterNodeProperties.x = pos.x;
  272. }
  273. if (clusterNodeProperties.y === undefined) {
  274. if (pos === undefined) {
  275. pos = this._getClusterPosition(childNodesObj);
  276. }
  277. clusterNodeProperties.y = pos.y;
  278. }
  279. // force the ID to remain the same
  280. clusterNodeProperties.id = clusterId;
  281. // create the clusterNode
  282. let clusterNode = this.body.functions.createNode(clusterNodeProperties, Cluster);
  283. clusterNode.isCluster = true;
  284. clusterNode.containedNodes = childNodesObj;
  285. clusterNode.containedEdges = childEdgesObj;
  286. // finally put the cluster node into global
  287. this.body.nodes[clusterNodeProperties.id] = clusterNode;
  288. // create the new edges that will connect to the cluster
  289. let newEdges = [];
  290. this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options);
  291. // disable the childEdges
  292. for (let edgeId in childEdgesObj) {
  293. if (childEdgesObj.hasOwnProperty(edgeId)) {
  294. if (this.body.edges[edgeId] !== undefined) {
  295. let edge = this.body.edges[edgeId];
  296. edge.togglePhysics(false);
  297. edge.options.hidden = true;
  298. }
  299. }
  300. }
  301. // disable the childNodes
  302. for (let nodeId in childNodesObj) {
  303. if (childNodesObj.hasOwnProperty(nodeId)) {
  304. this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.body.nodes[nodeId]};
  305. this.body.nodes[nodeId].togglePhysics(false);
  306. this.body.nodes[nodeId].options.hidden = true;
  307. }
  308. }
  309. // push new edges to global
  310. for (let i = 0; i < newEdges.length; i++) {
  311. this.body.edges[newEdges[i].id] = newEdges[i];
  312. this.body.edges[newEdges[i].id].connect();
  313. }
  314. // set ID to undefined so no duplicates arise
  315. clusterNodeProperties.id = undefined;
  316. // wrap up
  317. if (refreshData === true) {
  318. this.body.emitter.emit('_dataChanged');
  319. }
  320. }
  321. /**
  322. * Check if a node is a cluster.
  323. * @param nodeId
  324. * @returns {*}
  325. */
  326. isCluster(nodeId) {
  327. if (this.body.nodes[nodeId] !== undefined) {
  328. return this.body.nodes[nodeId].isCluster === true;
  329. }
  330. else {
  331. console.log("Node does not exist.")
  332. return false;
  333. }
  334. }
  335. /**
  336. * get the position of the cluster node based on what's inside
  337. * @param {object} childNodesObj | object with node objects, id as keys
  338. * @returns {{x: number, y: number}}
  339. * @private
  340. */
  341. _getClusterPosition(childNodesObj) {
  342. let childKeys = Object.keys(childNodesObj);
  343. let minX = childNodesObj[childKeys[0]].x;
  344. let maxX = childNodesObj[childKeys[0]].x;
  345. let minY = childNodesObj[childKeys[0]].y;
  346. let maxY = childNodesObj[childKeys[0]].y;
  347. let node;
  348. for (let i = 0; i < childKeys.lenght; i++) {
  349. node = childNodesObj[childKeys[0]];
  350. minX = node.x < minX ? node.x : minX;
  351. maxX = node.x > maxX ? node.x : maxX;
  352. minY = node.y < minY ? node.y : minY;
  353. maxY = node.y > maxY ? node.y : maxY;
  354. }
  355. return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)};
  356. }
  357. /**
  358. * Open a cluster by calling this function.
  359. * @param {String} clusterNodeId | the ID of the cluster node
  360. * @param {Boolean} refreshData | wrap up afterwards if not true
  361. */
  362. openCluster(clusterNodeId, refreshData = true) {
  363. // kill conditions
  364. if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");}
  365. if (this.body.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
  366. if (this.body.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return};
  367. let clusterNode = this.body.nodes[clusterNodeId];
  368. let containedNodes = clusterNode.containedNodes;
  369. let containedEdges = clusterNode.containedEdges;
  370. // release nodes
  371. for (let nodeId in containedNodes) {
  372. if (containedNodes.hasOwnProperty(nodeId)) {
  373. let containedNode = this.body.nodes[nodeId];
  374. containedNode = containedNodes[nodeId];
  375. // inherit position
  376. containedNode.x = clusterNode.x;
  377. containedNode.y = clusterNode.y;
  378. // inherit speed
  379. containedNode.vx = clusterNode.vx;
  380. containedNode.vy = clusterNode.vy;
  381. containedNode.options.hidden = false;
  382. containedNode.togglePhysics(true);
  383. delete this.clusteredNodes[nodeId];
  384. }
  385. }
  386. // release edges
  387. for (let edgeId in containedEdges) {
  388. if (containedEdges.hasOwnProperty(edgeId)) {
  389. let edge = this.body.edges[edgeId];
  390. edge.options.hidden = false;
  391. edge.togglePhysics(true);
  392. }
  393. }
  394. // remove all temporary edges
  395. for (let i = 0; i < clusterNode.edges.length; i++) {
  396. let edgeId = clusterNode.edges[i].id;
  397. this.body.edges[edgeId].edgeType.cleanup();
  398. // this removes the edge from node.edges, which is why edgeIds is formed
  399. this.body.edges[edgeId].disconnect();
  400. delete this.body.edges[edgeId];
  401. }
  402. // remove clusterNode
  403. delete this.body.nodes[clusterNodeId];
  404. if (refreshData === true) {
  405. this.body.emitter.emit('_dataChanged');
  406. }
  407. }
  408. /**
  409. * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
  410. * is currently residing in cluster B
  411. * @param edge
  412. * @param nodeId
  413. * @param from
  414. * @private
  415. */
  416. _connectEdge(edge, nodeId, from) {
  417. let clusterStack = this.findNode(nodeId);
  418. if (from === true) {
  419. edge.from = clusterStack[clusterStack.length - 1];
  420. edge.fromId = clusterStack[clusterStack.length - 1].id;
  421. clusterStack.pop()
  422. edge.fromArray = clusterStack;
  423. }
  424. else {
  425. edge.to = clusterStack[clusterStack.length - 1];
  426. edge.toId = clusterStack[clusterStack.length - 1].id;
  427. clusterStack.pop();
  428. edge.toArray = clusterStack;
  429. }
  430. edge.connect();
  431. }
  432. /**
  433. * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
  434. * @param nodeId
  435. * @returns {Array}
  436. * @private
  437. */
  438. findNode(nodeId) {
  439. let stack = [];
  440. let max = 100;
  441. let counter = 0;
  442. while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
  443. stack.push(this.clusteredNodes[nodeId].node);
  444. nodeId = this.clusteredNodes[nodeId].clusterId;
  445. counter++;
  446. }
  447. stack.push(this.body.nodes[nodeId]);
  448. return stack;
  449. }
  450. /**
  451. * Get the Id the node is connected to
  452. * @param edge
  453. * @param nodeId
  454. * @returns {*}
  455. * @private
  456. */
  457. _getConnectedId(edge, nodeId) {
  458. if (edge.toId != nodeId) {
  459. return edge.toId;
  460. }
  461. else if (edge.fromId != nodeId) {
  462. return edge.fromId;
  463. }
  464. else {
  465. return edge.fromId;
  466. }
  467. }
  468. /**
  469. * We determine how many connections denote an important hub.
  470. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  471. *
  472. * @private
  473. */
  474. _getHubSize() {
  475. let average = 0;
  476. let averageSquared = 0;
  477. let hubCounter = 0;
  478. let largestHub = 0;
  479. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  480. let node = this.body.nodes[this.body.nodeIndices[i]];
  481. if (node.edges.length > largestHub) {
  482. largestHub = node.edges.length;
  483. }
  484. average += node.edges.length;
  485. averageSquared += Math.pow(node.edges.length,2);
  486. hubCounter += 1;
  487. }
  488. average = average / hubCounter;
  489. averageSquared = averageSquared / hubCounter;
  490. let letiance = averageSquared - Math.pow(average,2);
  491. let standardDeviation = Math.sqrt(letiance);
  492. let hubThreshold = Math.floor(average + 2*standardDeviation);
  493. // always have at least one to cluster
  494. if (hubThreshold > largestHub) {
  495. hubThreshold = largestHub;
  496. }
  497. return hubThreshold;
  498. };
  499. }
  500. export default ClusterEngine;