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.

613 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. * get the position of the cluster node based on what's inside
  321. * @param {object} childNodesObj | object with node objects, id as keys
  322. * @returns {{x: number, y: number}}
  323. * @private
  324. */
  325. exports._getClusterPosition = function(childNodesObj) {
  326. var childKeys = Object.keys(childNodesObj);
  327. var minX = childNodesObj[childKeys[0]].x;
  328. var maxX = childNodesObj[childKeys[0]].x;
  329. var minY = childNodesObj[childKeys[0]].y;
  330. var maxY = childNodesObj[childKeys[0]].y;
  331. var node;
  332. for (var i = 0; i < childKeys.lenght; i++) {
  333. node = childNodesObj[childKeys[0]];
  334. minX = node.x < minX ? node.x : minX;
  335. maxX = node.x > maxX ? node.x : maxX;
  336. minY = node.y < minY ? node.y : minY;
  337. maxY = node.y > maxY ? node.y : maxY;
  338. }
  339. return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)};
  340. }
  341. /**
  342. * Open a cluster by calling this function.
  343. * @param {String} clusterNodeId | the ID of the cluster node
  344. * @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true
  345. */
  346. exports.openCluster = function(clusterNodeId, doNotUpdateCalculationNodes) {
  347. // kill conditions
  348. if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");}
  349. if (this.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
  350. if (this.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return};
  351. var node = this.nodes[clusterNodeId];
  352. var containedNodes = node.containedNodes;
  353. var containedEdges = node.containedEdges;
  354. // release nodes
  355. for (var nodeId in containedNodes) {
  356. if (containedNodes.hasOwnProperty(nodeId)) {
  357. this.nodes[nodeId] = containedNodes[nodeId];
  358. // inherit position
  359. this.nodes[nodeId].x = node.x;
  360. this.nodes[nodeId].y = node.y;
  361. // inherit speed
  362. this.nodes[nodeId].vx = node.vx;
  363. this.nodes[nodeId].vy = node.vy;
  364. delete this.clusteredNodes[nodeId];
  365. }
  366. }
  367. // release edges
  368. for (var edgeId in containedEdges) {
  369. if (containedEdges.hasOwnProperty(edgeId)) {
  370. this.edges[edgeId] = containedEdges[edgeId];
  371. this.edges[edgeId].connect();
  372. var edge = this.edges[edgeId];
  373. if (edge.connected === false) {
  374. if (this.clusteredNodes[edge.fromId] !== undefined) {
  375. this._connectEdge(edge, edge.fromId, true);
  376. }
  377. if (this.clusteredNodes[edge.toId] !== undefined) {
  378. this._connectEdge(edge, edge.toId, false);
  379. }
  380. }
  381. }
  382. }
  383. this._createBezierNodes(containedEdges);
  384. var edgeIds = [];
  385. for (var i = 0; i < node.edges.length; i++) {
  386. edgeIds.push(node.edges[i].id);
  387. }
  388. // remove edges in clusterNode
  389. for (var i = 0; i < edgeIds.length; i++) {
  390. var edge = this.edges[edgeIds[i]];
  391. // if the edge should have been connected to a contained node
  392. if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) {
  393. // the node in the from array was contained in the cluster
  394. if (this.nodes[edge.fromArray[0].id] !== undefined) {
  395. this._connectEdge(edge, edge.fromArray[0].id, true);
  396. }
  397. }
  398. else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) {
  399. // the node in the to array was contained in the cluster
  400. if (this.nodes[edge.toArray[0].id] !== undefined) {
  401. this._connectEdge(edge, edge.toArray[0].id, false);
  402. }
  403. }
  404. else {
  405. var edgeId = edgeIds[i];
  406. var viaId = this.edges[edgeId].via.id;
  407. if (viaId) {
  408. this.edges[edgeId].via = null
  409. delete this.sectors['support']['nodes'][viaId];
  410. }
  411. // this removes the edge from node.edges, which is why edgeIds is formed
  412. this.edges[edgeId].disconnect();
  413. delete this.edges[edgeId];
  414. }
  415. }
  416. // remove clusterNode
  417. delete this.nodes[clusterNodeId];
  418. if (doNotUpdateCalculationNodes !== true) {
  419. this._wrapUp();
  420. }
  421. }
  422. /**
  423. * Recalculate navigation nodes, color edges dirty, update nodes list etc.
  424. * @private
  425. */
  426. exports._wrapUp = function() {
  427. this._updateNodeIndexList();
  428. this._updateCalculationNodes();
  429. this._markAllEdgesAsDirty();
  430. if (this.initializing !== true) {
  431. this.moving = true;
  432. this.start();
  433. }
  434. }
  435. /**
  436. * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
  437. * is currently residing in cluster B
  438. * @param edge
  439. * @param nodeId
  440. * @param from
  441. * @private
  442. */
  443. exports._connectEdge = function(edge, nodeId, from) {
  444. var clusterStack = this._getClusterStack(nodeId);
  445. if (from == true) {
  446. edge.from = clusterStack[clusterStack.length - 1];
  447. edge.fromId = clusterStack[clusterStack.length - 1].id;
  448. clusterStack.pop()
  449. edge.fromArray = clusterStack;
  450. }
  451. else {
  452. edge.to = clusterStack[clusterStack.length - 1];
  453. edge.toId = clusterStack[clusterStack.length - 1].id;
  454. clusterStack.pop();
  455. edge.toArray = clusterStack;
  456. }
  457. edge.connect();
  458. }
  459. /**
  460. * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
  461. * @param nodeId
  462. * @returns {Array}
  463. * @private
  464. */
  465. exports._getClusterStack = function(nodeId) {
  466. var stack = [];
  467. var max = 100;
  468. var counter = 0;
  469. while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
  470. stack.push(this.clusteredNodes[nodeId].node);
  471. nodeId = this.clusteredNodes[nodeId].clusterId;
  472. counter++;
  473. }
  474. stack.push(this.nodes[nodeId]);
  475. return stack;
  476. }
  477. /**
  478. * Get the Id the node is connected to
  479. * @param edge
  480. * @param nodeId
  481. * @returns {*}
  482. * @private
  483. */
  484. exports._getConnectedId = function(edge, nodeId) {
  485. if (edge.toId != nodeId) {
  486. return edge.toId;
  487. }
  488. else if (edge.fromId != nodeId) {
  489. return edge.fromId;
  490. }
  491. else {
  492. return edge.fromId;
  493. }
  494. }
  495. /**
  496. * We determine how many connections denote an important hub.
  497. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  498. *
  499. * @private
  500. */
  501. exports._getHubSize = function() {
  502. var average = 0;
  503. var averageSquared = 0;
  504. var hubCounter = 0;
  505. var largestHub = 0;
  506. for (var i = 0; i < this.nodeIndices.length; i++) {
  507. var node = this.nodes[this.nodeIndices[i]];
  508. if (node.edges.length > largestHub) {
  509. largestHub = node.edges.length;
  510. }
  511. average += node.edges.length;
  512. averageSquared += Math.pow(node.edges.length,2);
  513. hubCounter += 1;
  514. }
  515. average = average / hubCounter;
  516. averageSquared = averageSquared / hubCounter;
  517. var variance = averageSquared - Math.pow(average,2);
  518. var standardDeviation = Math.sqrt(variance);
  519. var hubThreshold = Math.floor(average + 2*standardDeviation);
  520. // always have at least one to cluster
  521. if (hubThreshold > largestHub) {
  522. hubThreshold = largestHub;
  523. }
  524. return hubThreshold;
  525. };