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.

560 lines
17 KiB

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