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.

552 lines
17 KiB

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