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.

1180 lines
40 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. 'use strict';
  2. let util = require('../../util');
  3. import NetworkUtil from '../NetworkUtil';
  4. class LayoutEngine {
  5. constructor(body) {
  6. this.body = body;
  7. this.initialRandomSeed = Math.round(Math.random() * 1000000);
  8. this.randomSeed = this.initialRandomSeed;
  9. this.options = {};
  10. this.optionsBackup = {};
  11. this.defaultOptions = {
  12. randomSeed: undefined,
  13. improvedLayout: true,
  14. hierarchical: {
  15. enabled:false,
  16. levelSeparation: 150,
  17. direction: 'UD', // UD, DU, LR, RL
  18. sortMethod: 'hubsize' // hubsize, directed
  19. }
  20. };
  21. util.extend(this.options, this.defaultOptions);
  22. this.bindEventListeners();
  23. }
  24. bindEventListeners() {
  25. this.body.emitter.on('_dataChanged', () => {
  26. this.setupHierarchicalLayout();
  27. });
  28. this.body.emitter.on('_dataLoaded', () => {
  29. this.layoutNetwork();
  30. });
  31. this.body.emitter.on('_resetHierarchicalLayout', () => {
  32. this.setupHierarchicalLayout();
  33. });
  34. }
  35. setOptions(options, allOptions) {
  36. if (options !== undefined) {
  37. let prevHierarchicalState = this.options.hierarchical.enabled;
  38. util.selectiveDeepExtend(["randomSeed", "improvedLayout"],this.options, options);
  39. util.mergeOptions(this.options, options, 'hierarchical');
  40. if (options.randomSeed !== undefined) {this.initialRandomSeed = options.randomSeed;}
  41. if (this.options.hierarchical.enabled === true) {
  42. if (prevHierarchicalState === true) {
  43. // refresh the overridden options for nodes and edges.
  44. this.body.emitter.emit('refresh', true);
  45. }
  46. // make sure the level separation is the right way up
  47. if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'DU') {
  48. if (this.options.hierarchical.levelSeparation > 0) {
  49. this.options.hierarchical.levelSeparation *= -1;
  50. }
  51. }
  52. else {
  53. if (this.options.hierarchical.levelSeparation < 0) {
  54. this.options.hierarchical.levelSeparation *= -1;
  55. }
  56. }
  57. this.body.emitter.emit('_resetHierarchicalLayout');
  58. // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed.
  59. return this.adaptAllOptionsForHierarchicalLayout(allOptions);
  60. }
  61. else {
  62. if (prevHierarchicalState === true) {
  63. // refresh the overridden options for nodes and edges.
  64. this.body.emitter.emit('refresh');
  65. return util.deepExtend(allOptions,this.optionsBackup);
  66. }
  67. }
  68. }
  69. return allOptions;
  70. }
  71. adaptAllOptionsForHierarchicalLayout(allOptions) {
  72. if (this.options.hierarchical.enabled === true) {
  73. // set the physics
  74. if (allOptions.physics === undefined || allOptions.physics === true) {
  75. allOptions.physics = {solver: 'hierarchicalRepulsion'};
  76. this.optionsBackup.physics = {solver:'barnesHut'};
  77. }
  78. else if (typeof allOptions.physics === 'object') {
  79. this.optionsBackup.physics = {solver:'barnesHut'};
  80. if (allOptions.physics.solver !== undefined) {
  81. this.optionsBackup.physics = {solver:allOptions.physics.solver};
  82. }
  83. allOptions.physics['solver'] = 'hierarchicalRepulsion';
  84. }
  85. else if (allOptions.physics !== false) {
  86. this.optionsBackup.physics = {solver:'barnesHut'};
  87. allOptions.physics['solver'] = 'hierarchicalRepulsion';
  88. }
  89. // get the type of static smooth curve in case it is required
  90. let type = 'horizontal';
  91. if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'LR') {
  92. type = 'vertical';
  93. }
  94. // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves.
  95. if (allOptions.edges === undefined) {
  96. this.optionsBackup.edges = {smooth:{enabled:true, type:'dynamic'}};
  97. allOptions.edges = {smooth: false};
  98. }
  99. else if (allOptions.edges.smooth === undefined) {
  100. this.optionsBackup.edges = {smooth:{enabled:true, type:'dynamic'}};
  101. allOptions.edges.smooth = false;
  102. }
  103. else {
  104. if (typeof allOptions.edges.smooth === 'boolean') {
  105. this.optionsBackup.edges = {smooth:allOptions.edges.smooth};
  106. allOptions.edges.smooth = {enabled: allOptions.edges.smooth, type:type}
  107. }
  108. else {
  109. // allow custom types except for dynamic
  110. if (allOptions.edges.smooth.type !== undefined && allOptions.edges.smooth.type !== 'dynamic') {
  111. type = allOptions.edges.smooth.type;
  112. }
  113. this.optionsBackup.edges = {
  114. smooth: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled,
  115. type:allOptions.edges.smooth.type === undefined ? 'dynamic' : allOptions.edges.smooth.type,
  116. roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness,
  117. forceDirection: allOptions.edges.smooth.forceDirection === undefined ? false : allOptions.edges.smooth.forceDirection
  118. };
  119. allOptions.edges.smooth = {
  120. enabled: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled,
  121. type:type,
  122. roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness,
  123. forceDirection: allOptions.edges.smooth.forceDirection === undefined ? false : allOptions.edges.smooth.forceDirection
  124. }
  125. }
  126. }
  127. // force all edges into static smooth curves. Only applies to edges that do not use the global options for smooth.
  128. this.body.emitter.emit('_forceDisableDynamicCurves', type);
  129. }
  130. return allOptions;
  131. }
  132. seededRandom() {
  133. let x = Math.sin(this.randomSeed++) * 10000;
  134. return x - Math.floor(x);
  135. }
  136. positionInitially(nodesArray) {
  137. if (this.options.hierarchical.enabled !== true) {
  138. this.randomSeed = this.initialRandomSeed;
  139. for (let i = 0; i < nodesArray.length; i++) {
  140. let node = nodesArray[i];
  141. let radius = 10 * 0.1 * nodesArray.length + 10;
  142. let angle = 2 * Math.PI * this.seededRandom();
  143. if (node.x === undefined) {
  144. node.x = radius * Math.cos(angle);
  145. }
  146. if (node.y === undefined) {
  147. node.y = radius * Math.sin(angle);
  148. }
  149. }
  150. }
  151. }
  152. /**
  153. * Use Kamada Kawai to position nodes. This is quite a heavy algorithm so if there are a lot of nodes we
  154. * cluster them first to reduce the amount.
  155. */
  156. layoutNetwork() {
  157. if (this.options.hierarchical.enabled !== true && this.options.improvedLayout === true) {
  158. // first check if we should Kamada Kawai to layout. The threshold is if less than half of the visible
  159. // nodes have predefined positions we use this.
  160. let positionDefined = 0;
  161. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  162. let node = this.body.nodes[this.body.nodeIndices[i]];
  163. if (node.predefinedPosition === true) {
  164. positionDefined += 1;
  165. }
  166. }
  167. // if less than half of the nodes have a predefined position we continue
  168. if (positionDefined < 0.5 * this.body.nodeIndices.length) {
  169. let MAX_LEVELS = 10;
  170. let level = 0;
  171. let clusterThreshold = 100;
  172. // if there are a lot of nodes, we cluster before we run the algorithm.
  173. if (this.body.nodeIndices.length > clusterThreshold) {
  174. let startLength = this.body.nodeIndices.length;
  175. while (this.body.nodeIndices.length > clusterThreshold) {
  176. //console.time("clustering")
  177. level += 1;
  178. let before = this.body.nodeIndices.length;
  179. // if there are many nodes we do a hubsize cluster
  180. if (level % 3 === 0) {
  181. this.body.modules.clustering.clusterBridges();
  182. }
  183. else {
  184. this.body.modules.clustering.clusterOutliers();
  185. }
  186. let after = this.body.nodeIndices.length;
  187. if ((before == after && level % 3 !== 0) || level > MAX_LEVELS) {
  188. this._declusterAll();
  189. this.body.emitter.emit("_layoutFailed");
  190. console.info("This network could not be positioned by this version of the improved layout algorithm. Please disable improvedLayout for better performance.");
  191. return;
  192. }
  193. //console.timeEnd("clustering")
  194. //console.log(level,after)
  195. }
  196. // increase the size of the edges
  197. this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150, 2 * startLength)})
  198. }
  199. // position the system for these nodes and edges
  200. this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true);
  201. // shift to center point
  202. this._shiftToCenter();
  203. // perturb the nodes a little bit to force the physics to kick in
  204. let offset = 70;
  205. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  206. this.body.nodes[this.body.nodeIndices[i]].x += (0.5 - this.seededRandom())*offset;
  207. this.body.nodes[this.body.nodeIndices[i]].y += (0.5 - this.seededRandom())*offset;
  208. }
  209. // uncluster all clusters
  210. this._declusterAll();
  211. // reposition all bezier nodes.
  212. this.body.emitter.emit("_repositionBezierNodes");
  213. }
  214. }
  215. }
  216. /**
  217. * Move all the nodes towards to the center so gravitational pull wil not move the nodes away from view
  218. * @private
  219. */
  220. _shiftToCenter() {
  221. let range = NetworkUtil.getRangeCore(this.body.nodes, this.body.nodeIndices);
  222. let center = NetworkUtil.findCenter(range);
  223. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  224. this.body.nodes[this.body.nodeIndices[i]].x -= center.x;
  225. this.body.nodes[this.body.nodeIndices[i]].y -= center.y;
  226. }
  227. }
  228. _declusterAll() {
  229. let clustersPresent = true;
  230. while (clustersPresent === true) {
  231. clustersPresent = false;
  232. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  233. if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) {
  234. clustersPresent = true;
  235. this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false);
  236. }
  237. }
  238. if (clustersPresent === true) {
  239. this.body.emitter.emit('_dataChanged');
  240. }
  241. }
  242. }
  243. getSeed() {
  244. return this.initialRandomSeed;
  245. }
  246. /**
  247. * This is the main function to layout the nodes in a hierarchical way.
  248. * It checks if the node details are supplied correctly
  249. *
  250. * @private
  251. */
  252. setupHierarchicalLayout() {
  253. if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) {
  254. // get the size of the largest hubs and check if the user has defined a level for a node.
  255. let node, nodeId;
  256. let definedLevel = false;
  257. let undefinedLevel = false;
  258. this.hierarchicalLevels = {};
  259. this.lastNodeOnLevel = {};
  260. this.hierarchicalParents = {};
  261. this.hierarchicalChildren = {};
  262. this.hierarchicalTrees = {};
  263. this.treeIndex = -1;
  264. this.whiteSpaceReductionFactor = 0.5;
  265. this.nodeSpacing = 100;
  266. this.treeSpacing = 2 * this.nodeSpacing;
  267. this.distributionOrdering = {};
  268. this.distributionIndex = {};
  269. this.distributionOrderingPresence = {};
  270. for (nodeId in this.body.nodes) {
  271. if (this.body.nodes.hasOwnProperty(nodeId)) {
  272. node = this.body.nodes[nodeId];
  273. if (node.options.level !== undefined) {
  274. definedLevel = true;
  275. this.hierarchicalLevels[nodeId] = node.options.level;
  276. }
  277. else {
  278. undefinedLevel = true;
  279. }
  280. }
  281. }
  282. // if the user defined some levels but not all, alert and run without hierarchical layout
  283. if (undefinedLevel === true && definedLevel === true) {
  284. throw new Error('To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.');
  285. return;
  286. }
  287. else {
  288. // define levels if undefined by the users. Based on hubsize.
  289. if (undefinedLevel === true) {
  290. if (this.options.hierarchical.sortMethod === 'hubsize') {
  291. this._determineLevelsByHubsize();
  292. }
  293. else if (this.options.hierarchical.sortMethod === 'directed') {
  294. this._determineLevelsDirected();
  295. }
  296. else if (this.options.hierarchical.sortMethod === 'custom') {
  297. this._determineLevelsCustomCallback();
  298. }
  299. }
  300. // check the distribution of the nodes per level.
  301. let distribution = this._getDistribution();
  302. // get the parent children relations.
  303. this._generateMap();
  304. // place the nodes on the canvas.
  305. this._placeNodesByHierarchy(distribution);
  306. // condense the whitespace.
  307. this._condenseHierarchy(distribution);
  308. // shift to center so gravity does not have to do much
  309. this._shiftToCenter();
  310. }
  311. }
  312. }
  313. /**
  314. * @private
  315. */
  316. _condenseHierarchy(distribution) {
  317. // first we have some methods to help shifting trees around.
  318. // the main method to shift the trees
  319. let shiftTrees = () => {
  320. let treeSizes = getTreeSizes();
  321. for (let i = 0; i < treeSizes.length - 1; i++) {
  322. let diff = treeSizes[i].max - treeSizes[i+1].min;
  323. if (diff !== this.treeSpacing) {
  324. shiftTree(i + 1, diff - this.treeSpacing);
  325. }
  326. }
  327. };
  328. // shift a single tree by an offset
  329. let shiftTree = (index, offset) => {
  330. for (let nodeId in this.hierarchicalTrees) {
  331. if (this.hierarchicalTrees.hasOwnProperty(nodeId)) {
  332. if (this.hierarchicalTrees[nodeId] === index) {
  333. this._setPositionForHierarchy(this.body.nodes[nodeId], offset, undefined, true);
  334. }
  335. }
  336. }
  337. };
  338. // get the width of a tree
  339. let getTreeSize = (index) => {
  340. let min = 1e9;
  341. let max = -1e9;
  342. for (let nodeId in this.hierarchicalTrees) {
  343. if (this.hierarchicalTrees.hasOwnProperty(nodeId)) {
  344. if (this.hierarchicalTrees[nodeId] === index) {
  345. let pos = this._getPositionForHierarchy(this.body.nodes[nodeId]);
  346. min = Math.min(pos, min);
  347. max = Math.max(pos, max);
  348. }
  349. }
  350. }
  351. return [min, max];
  352. };
  353. // get the width of all trees
  354. let getTreeSizes = () => {
  355. let treeWidths = [];
  356. for (let i = 0; i < this.treeIndex; i++) {
  357. treeWidths.push(getTreeSize(i));
  358. }
  359. return treeWidths;
  360. };
  361. // get a map of all nodes in this branch
  362. let getBranchNodes = (source, map) => {
  363. map[source.id] = true;
  364. if (this.hierarchicalParents[source.id]) {
  365. let children = this.hierarchicalParents[source.id].children;
  366. if (children.length > 0) {
  367. for (let i = 0; i < children.length; i++) {
  368. getBranchNodes(this.body.nodes[children[i]], map);
  369. }
  370. }
  371. }
  372. };
  373. // get a min max width as well as the maximum movement space it has on either sides
  374. // we use min max terminology because width and height can interchange depending on the direction of the layout
  375. let getBranchBoundary = (branchMap, maxLevel = 1e9) => {
  376. let minSpace = 1e9;
  377. let maxSpace = -1e9;
  378. let min = 1e9;
  379. let max = -1e9;
  380. for (let branchNode in branchMap) {
  381. if (branchMap.hasOwnProperty(branchNode)) {
  382. let node = this.body.nodes[branchNode];
  383. let level = this.hierarchicalLevels[node.id];
  384. let index = this.distributionIndex[node.id];
  385. let position = this._getPositionForHierarchy(this.body.nodes[node.id]);
  386. // if this is the node at the side, there is no previous node
  387. if (index != 0) {
  388. let prevNode = this.distributionOrdering[level][index - 1];
  389. if (branchMap[prevNode.id] === undefined) {
  390. let prevPos = this._getPositionForHierarchy(prevNode);
  391. minSpace = Math.min(minSpace, position - prevPos);
  392. }
  393. }
  394. // if this is the node at the end there is no next node
  395. if (index != this.distributionOrdering[level].length - 1) {
  396. let nextNode = this.distributionOrdering[level][index + 1];
  397. if (branchMap[nextNode.id] === undefined) {
  398. let nextPos = this._getPositionForHierarchy(nextNode);
  399. maxSpace = Math.max(maxSpace, nextPos - position);
  400. }
  401. }
  402. // the width is only relevant for the levels two nodes have in common. This is why we filter on this.
  403. if (level <= maxLevel) {
  404. min = Math.min(position, min);
  405. max = Math.max(position, max);
  406. }
  407. }
  408. }
  409. // if there was no next node, the max space is infinite (1e9 ~ close enough)
  410. maxSpace = maxSpace < 0 ? 1e9 : maxSpace;
  411. return [min, max, minSpace, maxSpace];
  412. };
  413. // get the maximum level of a branch.
  414. let getMaxLevel = (nodeId) => {
  415. let level = this.hierarchicalLevels[nodeId];
  416. if (this.hierarchicalParents[nodeId]) {
  417. let children = this.hierarchicalParents[nodeId].children;
  418. if (children.length > 0) {
  419. for (let i = 0; i < children.length; i++) {
  420. level = Math.max(level,getMaxLevel(children[i]));
  421. }
  422. }
  423. }
  424. return level;
  425. };
  426. // check what the maximum level is these nodes have in common.
  427. let getCollisionLevel = (node1, node2) => {
  428. let maxLevel1 = getMaxLevel(node1.id);
  429. let maxLevel2 = getMaxLevel(node2.id);
  430. return Math.min(maxLevel1, maxLevel2);
  431. };
  432. // check if two nodes have the same parent(s)
  433. let hasSameParent = (node1, node2) => {
  434. let parents1 = this.hierarchicalChildren[node1.id];
  435. let parents2 = this.hierarchicalChildren[node2.id];
  436. if (parents1 === undefined || parents2 === undefined) {
  437. return false;
  438. }
  439. parents1 = parents1.parents;
  440. parents2 = parents2.parents;
  441. for (let i = 0; i < parents1.length; i++) {
  442. for (let j = 0; j < parents2.length; j++) {
  443. if (parents1[i] == parents2[j]) {
  444. return true;
  445. }
  446. }
  447. }
  448. return false;
  449. };
  450. // condense elements. These can be nodes or branches depending on the callback.
  451. let shiftElementsCloser = (callback, levels, centerParents) => {
  452. for (let i = 0; i < levels.length; i++) {
  453. let level = levels[i];
  454. let levelNodes = this.distributionOrdering[level];
  455. if (levelNodes.length > 1) {
  456. for (let i = 0; i < levelNodes.length - 1; i++) {
  457. if (hasSameParent(levelNodes[i],levelNodes[i+1]) === true) {
  458. if (this.hierarchicalTrees[levelNodes[i].id] === this.hierarchicalTrees[levelNodes[i+1].id]) {
  459. callback(levelNodes[i],levelNodes[i+1], centerParents);
  460. }
  461. }}
  462. }
  463. }
  464. };
  465. // Global var in this scope to define when the movement has stopped.
  466. let stillShifting = false;
  467. // callback for shifting branches
  468. let branchShiftCallback = (node1, node2, centerParent = false) => {
  469. //window.CALLBACKS.push(() => {
  470. let pos1 = this._getPositionForHierarchy(node1);
  471. let pos2 = this._getPositionForHierarchy(node2);
  472. let diffAbs = Math.abs(pos2 - pos1);
  473. //console.log("NOW CHEcKING:", node1.id, node2.id, diffAbs);
  474. if (diffAbs > this.nodeSpacing) {
  475. let branchNodes1 = {}; branchNodes1[node1.id] = true;
  476. let branchNodes2 = {}; branchNodes2[node2.id] = true;
  477. getBranchNodes(node1, branchNodes1);
  478. getBranchNodes(node2, branchNodes2);
  479. // check the largest distance between the branches
  480. let maxLevel = getCollisionLevel(node1, node2);
  481. let [min1,max1, minSpace1, maxSpace1] = getBranchBoundary(branchNodes1, maxLevel);
  482. let [min2,max2, minSpace2, maxSpace2] = getBranchBoundary(branchNodes2, maxLevel);
  483. //console.log(node1.id, getBranchBoundary(branchNodes1, maxLevel), node2.id, getBranchBoundary(branchNodes2, maxLevel), maxLevel);
  484. let diffBranch = Math.abs(max1 - min2);
  485. if (diffBranch > this.nodeSpacing) {
  486. let offset = max1 - min2 + this.nodeSpacing;
  487. if (offset < -minSpace2 + this.nodeSpacing) {
  488. offset = -minSpace2 + this.nodeSpacing;
  489. //console.log("RESETTING OFFSET", max1 - min2 + this.nodeSpacing, -minSpace2, offset);
  490. }
  491. if (offset < 0) {
  492. //console.log("SHIFTING", node2.id, offset);
  493. this._shiftBlock(node2.id, offset);
  494. stillShifting = true;
  495. if (centerParent === true)
  496. this._centerParent(node2);
  497. }
  498. }
  499. }
  500. //this.body.emitter.emit("_redraw");})
  501. };
  502. // callback for shifting individual nodes
  503. let unitShiftCallback = (node1, node2, centerParent) => {
  504. //window.CALLBACKS.push(() => {
  505. let pos1 = this._getPositionForHierarchy(node1);
  506. let pos2 = this._getPositionForHierarchy(node2);
  507. let diffAbs = Math.abs(pos2 - pos1);
  508. //console.log("NOW CHEcKING:", node1.id, node2.id, diffAbs);
  509. if (diffAbs > this.nodeSpacing) {
  510. let diff = (pos1 + this.nodeSpacing - pos2) * this.whiteSpaceReductionFactor;
  511. if (diff != 0) {
  512. stillShifting = true;
  513. }
  514. let factor = node2.edges.length / (node1.edges.length + node2.edges.length);
  515. this._setPositionForHierarchy(node2, pos2 + factor * diff, undefined, true);
  516. this._setPositionForHierarchy(node1, pos1 - (1-factor) * diff, undefined, true);
  517. if (centerParent === true) {
  518. this._centerParent(node2);
  519. }
  520. }
  521. //this.body.emitter.emit("_redraw");})
  522. };
  523. // method to shift all nodes closer together iteratively
  524. let shiftUnitsCloser = (iterations) => {
  525. let levels = Object.keys(this.distributionOrdering);
  526. for (let i = 0; i < iterations; i++) {
  527. stillShifting = false;
  528. shiftElementsCloser(unitShiftCallback, levels, false);
  529. if (stillShifting !== true) {
  530. //console.log("FINISHED shiftUnitsCloser IN " + i);
  531. break;
  532. }
  533. }
  534. //console.log("FINISHED shiftUnitsCloser IN " + iterations);
  535. };
  536. // method to remove whitespace between branches. Because we do bottom up, we can center the parents.
  537. let shiftBranchesCloserBottomUp = (iterations) => {
  538. let levels = Object.keys(this.distributionOrdering);
  539. levels = levels.reverse();
  540. for (let i = 0; i < iterations; i++) {
  541. stillShifting = false;
  542. shiftElementsCloser(branchShiftCallback, levels, true);
  543. if (stillShifting !== true) {
  544. //console.log("FINISHED shiftBranchesCloserBottomUp IN " + i);
  545. break;
  546. }
  547. }
  548. };
  549. // center all parents
  550. let centerAllParents = () => {
  551. for (let node in this.body.nodes) {
  552. this._centerParent(this.body.nodes[node]);
  553. }
  554. };
  555. // the actual work is done here.
  556. shiftBranchesCloserBottomUp(5);
  557. centerAllParents();
  558. shiftUnitsCloser(2);
  559. shiftTrees();
  560. }
  561. /**
  562. * We use this method to center a parent node and check if it does not cross other nodes when it does.
  563. * @param node
  564. * @private
  565. */
  566. _centerParent(node) {
  567. if (this.hierarchicalChildren[node.id]) {
  568. let parents = this.hierarchicalChildren[node.id].parents;
  569. for (var i = 0; i < parents.length; i++) {
  570. let parentId = parents[i];
  571. let parentNode = this.body.nodes[parentId];
  572. if (this.hierarchicalParents[parentId]) {
  573. // get the range of the children
  574. let minPos = 1e9;
  575. let maxPos = -1e9;
  576. let children = this.hierarchicalParents[parentId].children;
  577. if (children.length > 0) {
  578. for (let i = 0; i < children.length; i++) {
  579. let childNode = this.body.nodes[children[i]];
  580. minPos = Math.min(minPos, this._getPositionForHierarchy(childNode));
  581. maxPos = Math.max(maxPos, this._getPositionForHierarchy(childNode));
  582. }
  583. }
  584. let level = this.hierarchicalLevels[parentId];
  585. let index = this.distributionIndex[parentId];
  586. let position = this._getPositionForHierarchy(parentNode);
  587. let minSpace = 1e9;
  588. let maxSpace = 1e9;
  589. if (index != 0) {
  590. let prevNode = this.distributionOrdering[level][index - 1];
  591. let prevPos = this._getPositionForHierarchy(prevNode);
  592. minSpace = position - prevPos;
  593. }
  594. if (index != this.distributionOrdering[level].length - 1) {
  595. let nextNode = this.distributionOrdering[level][index + 1];
  596. let nextPos = this._getPositionForHierarchy(nextNode);
  597. maxSpace = Math.min(maxSpace, nextPos - position);
  598. }
  599. let newPosition = 0.5 * (minPos + maxPos);
  600. if (newPosition < position + maxSpace && newPosition > position - minSpace) {
  601. this._setPositionForHierarchy(parentNode, newPosition, undefined, true);
  602. }
  603. }
  604. }
  605. }
  606. }
  607. /**
  608. * This function places the nodes on the canvas based on the hierarchial distribution.
  609. *
  610. * @param {Object} distribution | obtained by the function this._getDistribution()
  611. * @private
  612. */
  613. _placeNodesByHierarchy(distribution) {
  614. this.positionedNodes = {};
  615. // start placing all the level 0 nodes first. Then recursively position their branches.
  616. for (let level in distribution) {
  617. if (distribution.hasOwnProperty(level)) {
  618. // sort nodes in level by position:
  619. let nodeArray = Object.keys(distribution[level]);
  620. nodeArray = this._indexArrayToNodes(nodeArray);
  621. this._sortNodeArray(nodeArray);
  622. for (let i = 0; i < nodeArray.length; i++) {
  623. let node = nodeArray[i];
  624. if (this.positionedNodes[node.id] === undefined) {
  625. this._setPositionForHierarchy(node, this.nodeSpacing * i, level);
  626. this.positionedNodes[node.id] = true;
  627. this._placeBranchNodes(node.id, level);
  628. }
  629. }
  630. }
  631. }
  632. }
  633. /**
  634. * Receives an array with node indices and returns an array with the actual node references. Used for sorting based on
  635. * node properties.
  636. * @param idArray
  637. */
  638. _indexArrayToNodes(idArray) {
  639. let array = [];
  640. for (let i = 0; i < idArray.length; i++) {
  641. array.push(this.body.nodes[idArray[i]])
  642. }
  643. return array;
  644. }
  645. /**
  646. * This function get the distribution of levels based on hubsize
  647. *
  648. * @returns {Object}
  649. * @private
  650. */
  651. _getDistribution() {
  652. let distribution = {};
  653. let nodeId, node;
  654. // 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.
  655. // the fix of X is removed after the x value has been set.
  656. for (nodeId in this.body.nodes) {
  657. if (this.body.nodes.hasOwnProperty(nodeId)) {
  658. node = this.body.nodes[nodeId];
  659. let level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId];
  660. if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
  661. node.y = this.options.hierarchical.levelSeparation * level;
  662. node.options.fixed.y = true;
  663. }
  664. else {
  665. node.x = this.options.hierarchical.levelSeparation * level;
  666. node.options.fixed.x = true;
  667. }
  668. if (distribution[level] === undefined) {
  669. distribution[level] = {};
  670. }
  671. distribution[level][nodeId] = node;
  672. }
  673. }
  674. return distribution;
  675. }
  676. /**
  677. * Get the hubsize from all remaining unlevelled nodes.
  678. *
  679. * @returns {number}
  680. * @private
  681. */
  682. _getHubSize() {
  683. let hubSize = 0;
  684. for (let nodeId in this.body.nodes) {
  685. if (this.body.nodes.hasOwnProperty(nodeId)) {
  686. let node = this.body.nodes[nodeId];
  687. if (this.hierarchicalLevels[nodeId] === undefined) {
  688. hubSize = node.edges.length < hubSize ? hubSize : node.edges.length;
  689. }
  690. }
  691. }
  692. return hubSize;
  693. }
  694. /**
  695. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  696. *
  697. * @param hubsize
  698. * @private
  699. */
  700. _determineLevelsByHubsize() {
  701. let hubSize = 1;
  702. let levelDownstream = (nodeA, nodeB) => {
  703. if (this.hierarchicalLevels[nodeB.id] === undefined) {
  704. // set initial level
  705. if (this.hierarchicalLevels[nodeA.id] === undefined) {
  706. this.hierarchicalLevels[nodeA.id] = 0;
  707. }
  708. // set level
  709. this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + 1;
  710. }
  711. };
  712. while (hubSize > 0) {
  713. // determine hubs
  714. hubSize = this._getHubSize();
  715. if (hubSize === 0)
  716. break;
  717. for (let nodeId in this.body.nodes) {
  718. if (this.body.nodes.hasOwnProperty(nodeId)) {
  719. let node = this.body.nodes[nodeId];
  720. if (node.edges.length === hubSize) {
  721. this._crawlNetwork(levelDownstream,nodeId);
  722. }
  723. }
  724. }
  725. }
  726. }
  727. /**
  728. * TODO: release feature
  729. * @private
  730. */
  731. _determineLevelsCustomCallback() {
  732. let minLevel = 100000;
  733. // TODO: this should come from options.
  734. let customCallback = function(nodeA, nodeB, edge) {
  735. };
  736. let levelByDirection = (nodeA, nodeB, edge) => {
  737. let levelA = this.hierarchicalLevels[nodeA.id];
  738. // set initial level
  739. if (levelA === undefined) {this.hierarchicalLevels[nodeA.id] = minLevel;}
  740. let diff = customCallback(
  741. NetworkUtil.cloneOptions(nodeA,'node'),
  742. NetworkUtil.cloneOptions(nodeB,'node'),
  743. NetworkUtil.cloneOptions(edge,'edge')
  744. );
  745. this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + diff;
  746. };
  747. this._crawlNetwork(levelByDirection);
  748. this._setMinLevelToZero();
  749. }
  750. /**
  751. * this function allocates nodes in levels based on the direction of the edges
  752. *
  753. * @param hubsize
  754. * @private
  755. */
  756. _determineLevelsDirected() {
  757. let minLevel = 10000;
  758. let levelByDirection = (nodeA, nodeB, edge) => {
  759. let levelA = this.hierarchicalLevels[nodeA.id];
  760. // set initial level
  761. if (levelA === undefined) {this.hierarchicalLevels[nodeA.id] = minLevel;}
  762. if (edge.toId == nodeB.id) {
  763. this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + 1;
  764. }
  765. else {
  766. this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] - 1;
  767. }
  768. };
  769. this._crawlNetwork(levelByDirection);
  770. this._setMinLevelToZero();
  771. }
  772. /**
  773. * Small util method to set the minimum levels of the nodes to zero.
  774. * @private
  775. */
  776. _setMinLevelToZero() {
  777. let minLevel = 1e9;
  778. // get the minimum level
  779. for (let nodeId in this.body.nodes) {
  780. if (this.body.nodes.hasOwnProperty(nodeId)) {
  781. minLevel = Math.min(this.hierarchicalLevels[nodeId], minLevel);
  782. }
  783. }
  784. // subtract the minimum from the set so we have a range starting from 0
  785. for (let nodeId in this.body.nodes) {
  786. if (this.body.nodes.hasOwnProperty(nodeId)) {
  787. this.hierarchicalLevels[nodeId] -= minLevel;
  788. }
  789. }
  790. }
  791. /**
  792. * Update the bookkeeping of parent and child.
  793. * @param parentNodeId
  794. * @param childNodeId
  795. * @private
  796. */
  797. _generateMap() {
  798. let fillInRelations = (parentNode, childNode) => {
  799. if (this.hierarchicalLevels[childNode.id] > this.hierarchicalLevels[parentNode.id]) {
  800. let parentNodeId = parentNode.id;
  801. let childNodeId = childNode.id;
  802. if (this.hierarchicalParents[parentNodeId] === undefined) {
  803. this.hierarchicalParents[parentNodeId] = {children: [], amount: 0};
  804. }
  805. this.hierarchicalParents[parentNodeId].children.push(childNodeId);
  806. if (this.hierarchicalChildren[childNodeId] === undefined) {
  807. this.hierarchicalChildren[childNodeId] = {parents: [], amount: 0};
  808. }
  809. this.hierarchicalChildren[childNodeId].parents.push(parentNodeId);
  810. }
  811. };
  812. this._crawlNetwork(fillInRelations);
  813. }
  814. /**
  815. * Crawl over the entire network and use a callback on each node couple that is connected to each other.
  816. * @param callback | will receive nodeA nodeB and the connecting edge. A and B are unique.
  817. * @param startingNodeId
  818. * @private
  819. */
  820. _crawlNetwork(callback = function() {}, startingNodeId) {
  821. let progress = {};
  822. let crawler = (node) => {
  823. if (progress[node.id] === undefined) {
  824. progress[node.id] = true;
  825. let childNode;
  826. for (let i = 0; i < node.edges.length; i++) {
  827. if (node.edges[i].toId === node.id) {childNode = node.edges[i].from;}
  828. else {childNode = node.edges[i].to;}
  829. if (node.id !== childNode.id) {
  830. callback(node, childNode, node.edges[i]);
  831. crawler(childNode);
  832. }
  833. }
  834. }
  835. };
  836. // we can crawl from a specific node or over all nodes.
  837. if (startingNodeId === undefined) {
  838. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  839. let node = this.body.nodes[this.body.nodeIndices[i]];
  840. crawler(node);
  841. }
  842. }
  843. else {
  844. let node = this.body.nodes[startingNodeId];
  845. if (node === undefined) {
  846. console.error("Node not found:", startingNodeId);
  847. return;
  848. }
  849. crawler(node);
  850. }
  851. }
  852. /**
  853. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  854. * on a X position that ensures there will be no overlap.
  855. *
  856. * @param parentId
  857. * @param parentLevel
  858. * @private
  859. */
  860. _placeBranchNodes(parentId, parentLevel) {
  861. // if this is not a parent, cancel the placing. This can happen with multiple parents to one child.
  862. if (this.hierarchicalParents[parentId] === undefined) {
  863. return;
  864. }
  865. // get a list of childNodes
  866. let childNodes = [];
  867. for (let i = 0; i < this.hierarchicalParents[parentId].children.length; i++) {
  868. childNodes.push(this.body.nodes[this.hierarchicalParents[parentId].children[i]]);
  869. }
  870. // use the positions to order the nodes.
  871. this._sortNodeArray(childNodes);
  872. // position the childNodes
  873. for (let i = 0; i < childNodes.length; i++) {
  874. let childNode = childNodes[i];
  875. let childNodeLevel = this.hierarchicalLevels[childNode.id];
  876. // check if the child node is below the parent node and if it has already been positioned.
  877. if (childNodeLevel > parentLevel && this.positionedNodes[childNode.id] === undefined) {
  878. // get the amount of space required for this node. If parent the width is based on the amount of children.
  879. let pos;
  880. // we get the X or Y values we need and store them in pos and previousPos. The get and set make sure we get X or Y
  881. if (i === 0) {pos = this._getPositionForHierarchy(this.body.nodes[parentId]);}
  882. else {pos = this._getPositionForHierarchy(childNodes[i-1]) + this.nodeSpacing;}
  883. this._setPositionForHierarchy(childNode, pos, childNodeLevel);
  884. // if overlap has been detected, we shift the branch
  885. if (this.lastNodeOnLevel[childNodeLevel] !== undefined) {
  886. let previousPos = this._getPositionForHierarchy(this.body.nodes[this.lastNodeOnLevel[childNodeLevel]]);
  887. if (pos - previousPos < this.nodeSpacing) {
  888. let diff = (previousPos + this.nodeSpacing) - pos;
  889. let sharedParent = this._findCommonParent(this.lastNodeOnLevel[childNodeLevel], childNode.id);
  890. this._shiftBlock(sharedParent.withChild, diff);
  891. }
  892. }
  893. // store change in position.
  894. this.lastNodeOnLevel[childNodeLevel] = childNode.id;
  895. this.positionedNodes[childNode.id] = true;
  896. this._placeBranchNodes(childNode.id, childNodeLevel);
  897. }
  898. else {
  899. return;
  900. }
  901. }
  902. // center the parent nodes.
  903. let minPos = 1e9;
  904. let maxPos = -1e9;
  905. for (let i = 0; i < childNodes.length; i++) {
  906. let childNodeId = childNodes[i].id;
  907. minPos = Math.min(minPos, this._getPositionForHierarchy(this.body.nodes[childNodeId]));
  908. maxPos = Math.max(maxPos, this._getPositionForHierarchy(this.body.nodes[childNodeId]));
  909. }
  910. this._setPositionForHierarchy(this.body.nodes[parentId], 0.5 * (minPos + maxPos), parentLevel);
  911. }
  912. /**
  913. * Shift a branch a certain distance
  914. * @param parentId
  915. * @param diff
  916. * @private
  917. */
  918. _shiftBlock(parentId, diff) {
  919. if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
  920. this.body.nodes[parentId].x += diff;
  921. }
  922. else {
  923. this.body.nodes[parentId].y += diff;
  924. }
  925. if (this.hierarchicalParents[parentId] !== undefined) {
  926. for (let i = 0; i < this.hierarchicalParents[parentId].children.length; i++) {
  927. this._shiftBlock(this.hierarchicalParents[parentId].children[i], diff);
  928. }
  929. }
  930. }
  931. /**
  932. * Find a common parent between branches.
  933. * @param childA
  934. * @param childB
  935. * @returns {{foundParent, withChild}}
  936. * @private
  937. */
  938. _findCommonParent(childA,childB) {
  939. let parents = {};
  940. let iterateParents = (parents,child) => {
  941. if (this.hierarchicalChildren[child] !== undefined) {
  942. for (let i = 0; i < this.hierarchicalChildren[child].parents.length; i++) {
  943. let parent = this.hierarchicalChildren[child].parents[i];
  944. parents[parent] = true;
  945. iterateParents(parents, parent)
  946. }
  947. }
  948. };
  949. let findParent = (parents, child) => {
  950. if (this.hierarchicalChildren[child] !== undefined) {
  951. for (let i = 0; i < this.hierarchicalChildren[child].parents.length; i++) {
  952. let parent = this.hierarchicalChildren[child].parents[i];
  953. if (parents[parent] !== undefined) {
  954. return {foundParent:parent, withChild:child};
  955. }
  956. let branch = findParent(parents, parent);
  957. if (branch.foundParent !== null) {
  958. return branch;
  959. }
  960. }
  961. }
  962. return {foundParent:null, withChild:child};
  963. };
  964. iterateParents(parents, childA);
  965. return findParent(parents, childB);
  966. }
  967. /**
  968. * Abstract the getting of the position so we won't have to repeat the check for direction all the time
  969. * @param node
  970. * @param position
  971. * @param level
  972. * @private
  973. */
  974. _setPositionForHierarchy(node, position, level, doNotUpdate = false) {
  975. if (doNotUpdate !== true) {
  976. if (this.distributionOrdering[level] === undefined) {
  977. this.distributionOrdering[level] = [];
  978. this.distributionOrderingPresence[level] = {};
  979. }
  980. if (this.distributionOrderingPresence[level][node.id] === undefined) {
  981. this.distributionOrdering[level].push(node);
  982. this.distributionIndex[node.id] = this.distributionOrdering[level].length - 1;
  983. }
  984. this.distributionOrderingPresence[level][node.id] = true;
  985. if (this.hierarchicalTrees[node.id] === undefined) {
  986. if (this.hierarchicalChildren[node.id] !== undefined) {
  987. let tree = 1;
  988. // get the lowest tree denominator.
  989. for (let i = 0; i < this.hierarchicalChildren[node.id].parents.length; i++) {
  990. let parentId = this.hierarchicalChildren[node.id].parents[i];
  991. if (this.hierarchicalTrees[parentId] !== undefined) {
  992. //tree = Math.min(tree,this.hierarchicalTrees[parentId]);
  993. tree = this.hierarchicalTrees[parentId];
  994. }
  995. }
  996. //for (let i = 0; i < this.hierarchicalChildren.parents.length; i++) {
  997. // let parentId = this.hierarchicalChildren.parents[i];
  998. // this.hierarchicalTrees[parentId] = tree;
  999. //}
  1000. this.hierarchicalTrees[node.id] = tree;
  1001. }
  1002. else {
  1003. this.hierarchicalTrees[node.id] = ++this.treeIndex;
  1004. }
  1005. }
  1006. }
  1007. if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
  1008. node.x = position;
  1009. }
  1010. else {
  1011. node.y = position;
  1012. }
  1013. }
  1014. /**
  1015. * Abstract the getting of the position of a node so we do not have to repeat the direction check all the time.
  1016. * @param node
  1017. * @returns {number|*}
  1018. * @private
  1019. */
  1020. _getPositionForHierarchy(node) {
  1021. if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
  1022. return node.x;
  1023. }
  1024. else {
  1025. return node.y;
  1026. }
  1027. }
  1028. /**
  1029. * Use the x or y value to sort the array, allowing users to specify order.
  1030. * @param nodeArray
  1031. * @private
  1032. */
  1033. _sortNodeArray(nodeArray) {
  1034. if (nodeArray.length > 1) {
  1035. if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
  1036. nodeArray.sort(function (a, b) {
  1037. return a.x - b.x;
  1038. })
  1039. }
  1040. else {
  1041. nodeArray.sort(function (a, b) {
  1042. return a.y - b.y;
  1043. })
  1044. }
  1045. }
  1046. }
  1047. }
  1048. export default LayoutEngine;