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.

464 lines
15 KiB

  1. var util = require('../../util');
  2. class LayoutEngine {
  3. constructor(body) {
  4. this.body = body;
  5. this.initialRandomSeed = Math.round(Math.random() * 1000000);
  6. this.randomSeed = this.initialRandomSeed;
  7. this.options = {};
  8. this.optionsBackup = {};
  9. this.defaultOptions = {
  10. randomSeed: undefined,
  11. hierarchical: {
  12. enabled:false,
  13. levelSeparation: 150,
  14. direction: 'UD', // UD, DU, LR, RL
  15. sortMethod: 'hubsize' // hubsize, directed
  16. }
  17. }
  18. util.extend(this.options, this.defaultOptions);
  19. this.hierarchicalLevels = {};
  20. this.body.emitter.on('_dataChanged', () => {
  21. this.setupHierarchicalLayout();
  22. })
  23. this.body.emitter.on('_resetHierarchicalLayout', () => {
  24. this.setupHierarchicalLayout();
  25. this.body.emitter.emit('fit',{duration:0});
  26. })
  27. }
  28. setOptions(options, allOptions) {
  29. if (options !== undefined) {
  30. let prevHierarchicalState = this.options.hierarchical.enabled;
  31. util.mergeOptions(this.options, options, 'hierarchical');
  32. if (options.randomSeed !== undefined) {
  33. this.randomSeed = options.randomSeed;
  34. }
  35. if (this.options.hierarchical.enabled === true) {
  36. // make sure the level seperation is the right way up
  37. if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'DU') {
  38. if (this.options.hierarchical.levelSeparation > 0) {
  39. this.options.hierarchical.levelSeparation *= -1;
  40. }
  41. }
  42. else {
  43. if (this.options.hierarchical.levelSeparation < 0) {
  44. this.options.hierarchical.levelSeparation *= -1;
  45. }
  46. }
  47. this.body.emitter.emit('_resetHierarchicalLayout');
  48. // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed.
  49. return this.adaptAllOptions(allOptions);
  50. }
  51. else {
  52. if (prevHierarchicalState === true) {
  53. // refresh the overridden options for nodes and edges.
  54. this.body.emitter.emit('refresh');
  55. return util.deepExtend(allOptions,this.optionsBackup);
  56. }
  57. }
  58. }
  59. return allOptions;
  60. }
  61. adaptAllOptions(allOptions) {
  62. if (this.options.hierarchical.enabled === true) {
  63. // set the physics
  64. if (allOptions.physics === undefined || allOptions.physics === true) {
  65. allOptions.physics = {solver: 'hierarchicalRepulsion'};
  66. this.optionsBackup.physics = {solver:'barnesHut'};
  67. }
  68. else if (typeof options.physics === 'object') {
  69. this.optionsBackup.physics = {solver:'barnesHut'};
  70. if (options.physics.solver !== undefined) {
  71. this.optionsBackup.physics = {solver:options.physics.solver};
  72. }
  73. allOptions.physics['solver'] = 'hierarchicalRepulsion';
  74. }
  75. else if (options.physics !== false) {
  76. this.optionsBackup.physics = {solver:'barnesHut'};
  77. allOptions.physics['solver'] = 'hierarchicalRepulsion';
  78. }
  79. // get the type of static smooth curve in case it is required
  80. let type = 'horizontal';
  81. if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'LR') {
  82. type = 'vertical';
  83. }
  84. // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves.
  85. if (allOptions.edges === undefined) {
  86. this.optionsBackup.edges = {smooth:true, dynamic:true};
  87. allOptions.edges = {smooth: false};
  88. }
  89. else if (allOptions.edges.smooth === undefined) {
  90. this.optionsBackup.edges = {smooth:true, dynamic:true};
  91. allOptions.edges.smooth = false;
  92. }
  93. else {
  94. if (typeof allOptions.edges.smooth === 'boolean') {
  95. this.optionsBackup.edges = {smooth:allOptions.edges.smooth, dynamic:true};
  96. allOptions.edges.smooth = {enabled: allOptions.edges.smooth, dynamic: false, type:type}
  97. }
  98. else {
  99. this.optionsBackup.edges = {smooth: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, dynamic:true};
  100. allOptions.edges.smooth = {enabled: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, dynamic: false, type:type}
  101. }
  102. }
  103. // force all edges into static smooth curves. Only applies to edges that do not use the global options for smooth.
  104. this.body.emitter.emit('_forceDisableDynamicCurves', type);
  105. }
  106. return allOptions;
  107. }
  108. seededRandom() {
  109. var x = Math.sin(this.randomSeed++) * 10000;
  110. return x - Math.floor(x);
  111. }
  112. positionInitially(nodesArray) {
  113. if (this.options.hierarchical.enabled !== true) {
  114. for (let i = 0; i < nodesArray.length; i++) {
  115. let node = nodesArray[i];
  116. if ((!node.isFixed()) && (node.x === undefined || node.y === undefined)) {
  117. let radius = 10 * 0.1 * nodesArray.length + 10;
  118. let angle = 2 * Math.PI * this.seededRandom();
  119. if (node.options.fixed.x === false) {
  120. node.x = radius * Math.cos(angle);
  121. }
  122. if (node.options.fixed.x === false) {
  123. node.y = radius * Math.sin(angle);
  124. }
  125. }
  126. }
  127. }
  128. }
  129. getSeed() {
  130. return this.initialRandomSeed;
  131. }
  132. /**
  133. * This is the main function to layout the nodes in a hierarchical way.
  134. * It checks if the node details are supplied correctly
  135. *
  136. * @private
  137. */
  138. setupHierarchicalLayout() {
  139. if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) {
  140. // get the size of the largest hubs and check if the user has defined a level for a node.
  141. let node, nodeId;
  142. let definedLevel = false;
  143. let undefinedLevel = false;
  144. this.hierarchicalLevels = {};
  145. this.nodeSpacing = 100;
  146. for (nodeId in this.body.nodes) {
  147. if (this.body.nodes.hasOwnProperty(nodeId)) {
  148. node = this.body.nodes[nodeId];
  149. if (node.options.level !== undefined) {
  150. definedLevel = true;
  151. this.hierarchicalLevels[nodeId] = node.options.level;
  152. }
  153. else {
  154. undefinedLevel = true;
  155. }
  156. }
  157. }
  158. // if the user defined some levels but not all, alert and run without hierarchical layout
  159. if (undefinedLevel === true && definedLevel === true) {
  160. throw new Error('To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.');
  161. return;
  162. }
  163. else {
  164. // setup the system to use hierarchical method.
  165. //this._changeConstants();
  166. // define levels if undefined by the users. Based on hubsize
  167. if (undefinedLevel === true) {
  168. if (this.options.hierarchical.sortMethod === 'hubsize') {
  169. this._determineLevelsByHubsize();
  170. }
  171. else if (this.options.hierarchical.sortMethod === 'directed' || 'direction') {
  172. this._determineLevelsDirected();
  173. }
  174. }
  175. // check the distribution of the nodes per level.
  176. let distribution = this._getDistribution();
  177. // place the nodes on the canvas.
  178. this._placeNodesByHierarchy(distribution);
  179. }
  180. }
  181. }
  182. /**
  183. * This function places the nodes on the canvas based on the hierarchial distribution.
  184. *
  185. * @param {Object} distribution | obtained by the function this._getDistribution()
  186. * @private
  187. */
  188. _placeNodesByHierarchy(distribution) {
  189. let nodeId, node;
  190. this.positionedNodes = {};
  191. // start placing all the level 0 nodes first. Then recursively position their branches.
  192. for (let level in distribution) {
  193. if (distribution.hasOwnProperty(level)) {
  194. for (nodeId in distribution[level].nodes) {
  195. if (distribution[level].nodes.hasOwnProperty(nodeId)) {
  196. node = distribution[level].nodes[nodeId];
  197. if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
  198. if (node.x === undefined) {node.x = distribution[level].distance;}
  199. distribution[level].distance = node.x + this.nodeSpacing;
  200. }
  201. else {
  202. if (node.y === undefined) {node.y = distribution[level].distance;}
  203. distribution[level].distance = node.y + this.nodeSpacing;
  204. }
  205. this.positionedNodes[nodeId] = true;
  206. this._placeBranchNodes(node.edges,node.id,distribution,level);
  207. }
  208. }
  209. }
  210. }
  211. }
  212. /**
  213. * This function get the distribution of levels based on hubsize
  214. *
  215. * @returns {Object}
  216. * @private
  217. */
  218. _getDistribution() {
  219. let distribution = {};
  220. let nodeId, node;
  221. // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
  222. // the fix of X is removed after the x value has been set.
  223. for (nodeId in this.body.nodes) {
  224. if (this.body.nodes.hasOwnProperty(nodeId)) {
  225. node = this.body.nodes[nodeId];
  226. let level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId];
  227. if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
  228. node.y = this.options.hierarchical.levelSeparation * level;
  229. node.options.fixed.y = true;
  230. }
  231. else {
  232. node.x = this.options.hierarchical.levelSeparation * level;
  233. node.options.fixed.x = true;
  234. }
  235. if (distribution[level] === undefined) {
  236. distribution[level] = {amount: 0, nodes: {}, distance: 0};
  237. }
  238. distribution[level].amount += 1;
  239. distribution[level].nodes[nodeId] = node;
  240. }
  241. }
  242. return distribution;
  243. }
  244. /**
  245. * Get the hubsize from all remaining unlevelled nodes.
  246. *
  247. * @returns {number}
  248. * @private
  249. */
  250. _getHubSize() {
  251. let hubSize = 0;
  252. for (let nodeId in this.body.nodes) {
  253. if (this.body.nodes.hasOwnProperty(nodeId)) {
  254. let node = this.body.nodes[nodeId];
  255. if (this.hierarchicalLevels[nodeId] === undefined) {
  256. hubSize = node.edges.length < hubSize ? hubSize : node.edges.length;
  257. }
  258. }
  259. }
  260. return hubSize;
  261. }
  262. /**
  263. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  264. *
  265. * @param hubsize
  266. * @private
  267. */
  268. _determineLevelsByHubsize() {
  269. let nodeId, node;
  270. let hubSize = 1;
  271. while (hubSize > 0) {
  272. // determine hubs
  273. hubSize = this._getHubSize();
  274. if (hubSize === 0)
  275. break;
  276. for (nodeId in this.body.nodes) {
  277. if (this.body.nodes.hasOwnProperty(nodeId)) {
  278. node = this.body.nodes[nodeId];
  279. if (node.edges.length === hubSize) {
  280. this._setLevel(0, node);
  281. }
  282. }
  283. }
  284. }
  285. }
  286. /**
  287. * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
  288. *
  289. * @param level
  290. * @param edges
  291. * @param parentId
  292. * @private
  293. */
  294. _setLevel(level, node) {
  295. if (this.hierarchicalLevels[node.id] !== undefined)
  296. return;
  297. let childNode;
  298. this.hierarchicalLevels[node.id] = level;
  299. for (let i = 0; i < node.edges.length; i++) {
  300. if (node.edges[i].toId === node.id) {
  301. childNode = node.edges[i].from;
  302. }
  303. else {
  304. childNode = node.edges[i].to;
  305. }
  306. this._setLevel(level + 1, childNode);
  307. }
  308. }
  309. /**
  310. * this function allocates nodes in levels based on the direction of the edges
  311. *
  312. * @param hubsize
  313. * @private
  314. */
  315. _determineLevelsDirected() {
  316. let nodeId, node;
  317. let minLevel = 10000;
  318. // set first node to source
  319. for (nodeId in this.body.nodes) {
  320. if (this.body.nodes.hasOwnProperty(nodeId)) {
  321. node = this.body.nodes[nodeId];
  322. this._setLevelDirected(minLevel,node);
  323. }
  324. }
  325. // get the minimum level
  326. for (nodeId in this.body.nodes) {
  327. if (this.body.nodes.hasOwnProperty(nodeId)) {
  328. minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel;
  329. }
  330. }
  331. // subtract the minimum from the set so we have a range starting from 0
  332. for (nodeId in this.body.nodes) {
  333. if (this.body.nodes.hasOwnProperty(nodeId)) {
  334. this.hierarchicalLevels[nodeId] -= minLevel;
  335. }
  336. }
  337. }
  338. /**
  339. * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction
  340. *
  341. * @param level
  342. * @param edges
  343. * @param parentId
  344. * @private
  345. */
  346. _setLevelDirected(level, node) {
  347. if (this.hierarchicalLevels[node.id] !== undefined)
  348. return;
  349. let childNode;
  350. this.hierarchicalLevels[node.id] = level;
  351. for (let i = 0; i < node.edges.length; i++) {
  352. if (node.edges[i].toId === node.id) {
  353. childNode = node.edges[i].from;
  354. this._setLevelDirected(level - 1, childNode);
  355. }
  356. else {
  357. childNode = node.edges[i].to;
  358. this._setLevelDirected(level + 1, childNode);
  359. }
  360. }
  361. }
  362. /**
  363. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  364. * on a X position that ensures there will be no overlap.
  365. *
  366. * @param edges
  367. * @param parentId
  368. * @param distribution
  369. * @param parentLevel
  370. * @private
  371. */
  372. _placeBranchNodes(edges, parentId, distribution, parentLevel) {
  373. for (let i = 0; i < edges.length; i++) {
  374. let childNode = undefined;
  375. let parentNode = undefined;
  376. if (edges[i].toId === parentId) {
  377. childNode = edges[i].from;
  378. parentNode = edges[i].to;
  379. }
  380. else {
  381. childNode = edges[i].to;
  382. parentNode = edges[i].from;
  383. }
  384. let childNodeLevel = this.hierarchicalLevels[childNode.id];
  385. if (this.positionedNodes[childNode.id] === undefined) {
  386. // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
  387. if (childNodeLevel > parentLevel) {
  388. if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
  389. if (childNode.x === undefined) {
  390. childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x);
  391. }
  392. distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing;
  393. this.positionedNodes[childNode.id] = true;
  394. }
  395. else {
  396. if (childNode.y === undefined) {
  397. childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y)
  398. }
  399. distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing;
  400. }
  401. this.positionedNodes[childNode.id] = true;
  402. if (childNode.edges.length > 1) {
  403. this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel);
  404. }
  405. }
  406. }
  407. }
  408. }
  409. }
  410. export default LayoutEngine;