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.

628 lines
19 KiB

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