Layouts.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. /* *
  2. *
  3. * Networkgraph series
  4. *
  5. * (c) 2010-2021 Paweł Fus
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import Chart from '../../Core/Chart/Chart.js';
  14. import A from '../../Core/Animation/AnimationUtilities.js';
  15. var setAnimation = A.setAnimation;
  16. import H from '../../Core/Globals.js';
  17. import U from '../../Core/Utilities.js';
  18. var addEvent = U.addEvent, clamp = U.clamp, defined = U.defined, extend = U.extend, isFunction = U.isFunction, pick = U.pick;
  19. import './Integrations.js';
  20. import './QuadTree.js';
  21. /* eslint-disable no-invalid-this, valid-jsdoc */
  22. H.layouts = {
  23. 'reingold-fruchterman': function () {
  24. }
  25. };
  26. extend(
  27. /**
  28. * Reingold-Fruchterman algorithm from
  29. * "Graph Drawing by Force-directed Placement" paper.
  30. * @private
  31. */
  32. H.layouts['reingold-fruchterman'].prototype, {
  33. init: function (options) {
  34. this.options = options;
  35. this.nodes = [];
  36. this.links = [];
  37. this.series = [];
  38. this.box = {
  39. x: 0,
  40. y: 0,
  41. width: 0,
  42. height: 0
  43. };
  44. this.setInitialRendering(true);
  45. this.integration =
  46. H.networkgraphIntegrations[options.integration];
  47. this.enableSimulation = options.enableSimulation;
  48. this.attractiveForce = pick(options.attractiveForce, this.integration.attractiveForceFunction);
  49. this.repulsiveForce = pick(options.repulsiveForce, this.integration.repulsiveForceFunction);
  50. this.approximation = options.approximation;
  51. },
  52. updateSimulation: function (enable) {
  53. this.enableSimulation = pick(enable, this.options.enableSimulation);
  54. },
  55. start: function () {
  56. var layout = this, series = this.series, options = this.options;
  57. layout.currentStep = 0;
  58. layout.forces = series[0] && series[0].forces || [];
  59. layout.chart = series[0] && series[0].chart;
  60. if (layout.initialRendering) {
  61. layout.initPositions();
  62. // Render elements in initial positions:
  63. series.forEach(function (s) {
  64. s.finishedAnimating = true; // #13169
  65. s.render();
  66. });
  67. }
  68. layout.setK();
  69. layout.resetSimulation(options);
  70. if (layout.enableSimulation) {
  71. layout.step();
  72. }
  73. },
  74. step: function () {
  75. var layout = this, series = this.series, options = this.options;
  76. // Algorithm:
  77. layout.currentStep++;
  78. if (layout.approximation === 'barnes-hut') {
  79. layout.createQuadTree();
  80. layout.quadTree.calculateMassAndCenter();
  81. }
  82. layout.forces.forEach(function (forceName) {
  83. layout[forceName + 'Forces'](layout.temperature);
  84. });
  85. // Limit to the plotting area and cool down:
  86. layout.applyLimits(layout.temperature);
  87. // Cool down the system:
  88. layout.temperature = layout.coolDown(layout.startTemperature, layout.diffTemperature, layout.currentStep);
  89. layout.prevSystemTemperature = layout.systemTemperature;
  90. layout.systemTemperature = layout.getSystemTemperature();
  91. if (layout.enableSimulation) {
  92. series.forEach(function (s) {
  93. // Chart could be destroyed during the simulation
  94. if (s.chart) {
  95. s.render();
  96. }
  97. });
  98. if (layout.maxIterations-- &&
  99. isFinite(layout.temperature) &&
  100. !layout.isStable()) {
  101. if (layout.simulation) {
  102. H.win.cancelAnimationFrame(layout.simulation);
  103. }
  104. layout.simulation = H.win.requestAnimationFrame(function () {
  105. layout.step();
  106. });
  107. }
  108. else {
  109. layout.simulation = false;
  110. }
  111. }
  112. },
  113. stop: function () {
  114. if (this.simulation) {
  115. H.win.cancelAnimationFrame(this.simulation);
  116. }
  117. },
  118. setArea: function (x, y, w, h) {
  119. this.box = {
  120. left: x,
  121. top: y,
  122. width: w,
  123. height: h
  124. };
  125. },
  126. setK: function () {
  127. // Optimal distance between nodes,
  128. // available space around the node:
  129. this.k = this.options.linkLength || this.integration.getK(this);
  130. },
  131. addElementsToCollection: function (elements, collection) {
  132. elements.forEach(function (elem) {
  133. if (collection.indexOf(elem) === -1) {
  134. collection.push(elem);
  135. }
  136. });
  137. },
  138. removeElementFromCollection: function (element, collection) {
  139. var index = collection.indexOf(element);
  140. if (index !== -1) {
  141. collection.splice(index, 1);
  142. }
  143. },
  144. clear: function () {
  145. this.nodes.length = 0;
  146. this.links.length = 0;
  147. this.series.length = 0;
  148. this.resetSimulation();
  149. },
  150. resetSimulation: function () {
  151. this.forcedStop = false;
  152. this.systemTemperature = 0;
  153. this.setMaxIterations();
  154. this.setTemperature();
  155. this.setDiffTemperature();
  156. },
  157. restartSimulation: function () {
  158. if (!this.simulation) {
  159. // When dragging nodes, we don't need to calculate
  160. // initial positions and rendering nodes:
  161. this.setInitialRendering(false);
  162. // Start new simulation:
  163. if (!this.enableSimulation) {
  164. // Run only one iteration to speed things up:
  165. this.setMaxIterations(1);
  166. }
  167. else {
  168. this.start();
  169. }
  170. if (this.chart) {
  171. this.chart.redraw();
  172. }
  173. // Restore defaults:
  174. this.setInitialRendering(true);
  175. }
  176. else {
  177. // Extend current simulation:
  178. this.resetSimulation();
  179. }
  180. },
  181. setMaxIterations: function (maxIterations) {
  182. this.maxIterations = pick(maxIterations, this.options.maxIterations);
  183. },
  184. setTemperature: function () {
  185. this.temperature = this.startTemperature =
  186. Math.sqrt(this.nodes.length);
  187. },
  188. setDiffTemperature: function () {
  189. this.diffTemperature = this.startTemperature /
  190. (this.options.maxIterations + 1);
  191. },
  192. setInitialRendering: function (enable) {
  193. this.initialRendering = enable;
  194. },
  195. createQuadTree: function () {
  196. this.quadTree = new H.QuadTree(this.box.left, this.box.top, this.box.width, this.box.height);
  197. this.quadTree.insertNodes(this.nodes);
  198. },
  199. initPositions: function () {
  200. var initialPositions = this.options.initialPositions;
  201. if (isFunction(initialPositions)) {
  202. initialPositions.call(this);
  203. this.nodes.forEach(function (node) {
  204. if (!defined(node.prevX)) {
  205. node.prevX = node.plotX;
  206. }
  207. if (!defined(node.prevY)) {
  208. node.prevY = node.plotY;
  209. }
  210. node.dispX = 0;
  211. node.dispY = 0;
  212. });
  213. }
  214. else if (initialPositions === 'circle') {
  215. this.setCircularPositions();
  216. }
  217. else {
  218. this.setRandomPositions();
  219. }
  220. },
  221. setCircularPositions: function () {
  222. var box = this.box, nodes = this.nodes, nodesLength = nodes.length + 1, angle = 2 * Math.PI / nodesLength, rootNodes = nodes.filter(function (node) {
  223. return node.linksTo.length === 0;
  224. }), sortedNodes = [], visitedNodes = {}, radius = this.options.initialPositionRadius;
  225. /**
  226. * @private
  227. */
  228. function addToNodes(node) {
  229. node.linksFrom.forEach(function (link) {
  230. if (!visitedNodes[link.toNode.id]) {
  231. visitedNodes[link.toNode.id] = true;
  232. sortedNodes.push(link.toNode);
  233. addToNodes(link.toNode);
  234. }
  235. });
  236. }
  237. // Start with identified root nodes an sort the nodes by their
  238. // hierarchy. In trees, this ensures that branches don't cross
  239. // eachother.
  240. rootNodes.forEach(function (rootNode) {
  241. sortedNodes.push(rootNode);
  242. addToNodes(rootNode);
  243. });
  244. // Cyclic tree, no root node found
  245. if (!sortedNodes.length) {
  246. sortedNodes = nodes;
  247. // Dangling, cyclic trees
  248. }
  249. else {
  250. nodes.forEach(function (node) {
  251. if (sortedNodes.indexOf(node) === -1) {
  252. sortedNodes.push(node);
  253. }
  254. });
  255. }
  256. // Initial positions are laid out along a small circle, appearing
  257. // as a cluster in the middle
  258. sortedNodes.forEach(function (node, index) {
  259. node.plotX = node.prevX = pick(node.plotX, box.width / 2 + radius * Math.cos(index * angle));
  260. node.plotY = node.prevY = pick(node.plotY, box.height / 2 + radius * Math.sin(index * angle));
  261. node.dispX = 0;
  262. node.dispY = 0;
  263. });
  264. },
  265. setRandomPositions: function () {
  266. var box = this.box, nodes = this.nodes, nodesLength = nodes.length + 1;
  267. /**
  268. * Return a repeatable, quasi-random number based on an integer
  269. * input. For the initial positions
  270. * @private
  271. */
  272. function unrandom(n) {
  273. var rand = n * n / Math.PI;
  274. rand = rand - Math.floor(rand);
  275. return rand;
  276. }
  277. // Initial positions:
  278. nodes.forEach(function (node, index) {
  279. node.plotX = node.prevX = pick(node.plotX, box.width * unrandom(index));
  280. node.plotY = node.prevY = pick(node.plotY, box.height * unrandom(nodesLength + index));
  281. node.dispX = 0;
  282. node.dispY = 0;
  283. });
  284. },
  285. force: function (name) {
  286. this.integration[name].apply(this, Array.prototype.slice.call(arguments, 1));
  287. },
  288. barycenterForces: function () {
  289. this.getBarycenter();
  290. this.force('barycenter');
  291. },
  292. getBarycenter: function () {
  293. var systemMass = 0, cx = 0, cy = 0;
  294. this.nodes.forEach(function (node) {
  295. cx += node.plotX * node.mass;
  296. cy += node.plotY * node.mass;
  297. systemMass += node.mass;
  298. });
  299. this.barycenter = {
  300. x: cx,
  301. y: cy,
  302. xFactor: cx / systemMass,
  303. yFactor: cy / systemMass
  304. };
  305. return this.barycenter;
  306. },
  307. barnesHutApproximation: function (node, quadNode) {
  308. var layout = this, distanceXY = layout.getDistXY(node, quadNode), distanceR = layout.vectorLength(distanceXY), goDeeper, force;
  309. if (node !== quadNode && distanceR !== 0) {
  310. if (quadNode.isInternal) {
  311. // Internal node:
  312. if (quadNode.boxSize / distanceR <
  313. layout.options.theta &&
  314. distanceR !== 0) {
  315. // Treat as an external node:
  316. force = layout.repulsiveForce(distanceR, layout.k);
  317. layout.force('repulsive', node, force * quadNode.mass, distanceXY, distanceR);
  318. goDeeper = false;
  319. }
  320. else {
  321. // Go deeper:
  322. goDeeper = true;
  323. }
  324. }
  325. else {
  326. // External node, direct force:
  327. force = layout.repulsiveForce(distanceR, layout.k);
  328. layout.force('repulsive', node, force * quadNode.mass, distanceXY, distanceR);
  329. }
  330. }
  331. return goDeeper;
  332. },
  333. repulsiveForces: function () {
  334. var layout = this;
  335. if (layout.approximation === 'barnes-hut') {
  336. layout.nodes.forEach(function (node) {
  337. layout.quadTree.visitNodeRecursive(null, function (quadNode) {
  338. return layout.barnesHutApproximation(node, quadNode);
  339. });
  340. });
  341. }
  342. else {
  343. layout.nodes.forEach(function (node) {
  344. layout.nodes.forEach(function (repNode) {
  345. var force, distanceR, distanceXY;
  346. if (
  347. // Node can not repulse itself:
  348. node !== repNode &&
  349. // Only close nodes affect each other:
  350. // layout.getDistR(node, repNode) < 2 * k &&
  351. // Not dragged:
  352. !node.fixedPosition) {
  353. distanceXY = layout.getDistXY(node, repNode);
  354. distanceR = layout.vectorLength(distanceXY);
  355. if (distanceR !== 0) {
  356. force = layout.repulsiveForce(distanceR, layout.k);
  357. layout.force('repulsive', node, force * repNode.mass, distanceXY, distanceR);
  358. }
  359. }
  360. });
  361. });
  362. }
  363. },
  364. attractiveForces: function () {
  365. var layout = this, distanceXY, distanceR, force;
  366. layout.links.forEach(function (link) {
  367. if (link.fromNode && link.toNode) {
  368. distanceXY = layout.getDistXY(link.fromNode, link.toNode);
  369. distanceR = layout.vectorLength(distanceXY);
  370. if (distanceR !== 0) {
  371. force = layout.attractiveForce(distanceR, layout.k);
  372. layout.force('attractive', link, force, distanceXY, distanceR);
  373. }
  374. }
  375. });
  376. },
  377. applyLimits: function () {
  378. var layout = this, nodes = layout.nodes;
  379. nodes.forEach(function (node) {
  380. if (node.fixedPosition) {
  381. return;
  382. }
  383. layout.integration.integrate(layout, node);
  384. layout.applyLimitBox(node, layout.box);
  385. // Reset displacement:
  386. node.dispX = 0;
  387. node.dispY = 0;
  388. });
  389. },
  390. /**
  391. * External box that nodes should fall. When hitting an edge, node
  392. * should stop or bounce.
  393. * @private
  394. */
  395. applyLimitBox: function (node, box) {
  396. var radius = node.radius;
  397. /*
  398. TO DO: Consider elastic collision instead of stopping.
  399. o' means end position when hitting plotting area edge:
  400. - "inelastic":
  401. o
  402. \
  403. ______
  404. | o'
  405. | \
  406. | \
  407. - "elastic"/"bounced":
  408. o
  409. \
  410. ______
  411. | ^
  412. | / \
  413. |o' \
  414. Euler sample:
  415. if (plotX < 0) {
  416. plotX = 0;
  417. dispX *= -1;
  418. }
  419. if (plotX > box.width) {
  420. plotX = box.width;
  421. dispX *= -1;
  422. }
  423. */
  424. // Limit X-coordinates:
  425. node.plotX = clamp(node.plotX, box.left + radius, box.width - radius);
  426. // Limit Y-coordinates:
  427. node.plotY = clamp(node.plotY, box.top + radius, box.height - radius);
  428. },
  429. /**
  430. * From "A comparison of simulated annealing cooling strategies" by
  431. * Nourani and Andresen work.
  432. * @private
  433. */
  434. coolDown: function (temperature, temperatureStep, currentStep) {
  435. // Logarithmic:
  436. /*
  437. return Math.sqrt(this.nodes.length) -
  438. Math.log(
  439. currentStep * layout.diffTemperature
  440. );
  441. */
  442. // Exponential:
  443. /*
  444. var alpha = 0.1;
  445. layout.temperature = Math.sqrt(layout.nodes.length) *
  446. Math.pow(alpha, layout.diffTemperature);
  447. */
  448. // Linear:
  449. return temperature - temperatureStep * currentStep;
  450. },
  451. isStable: function () {
  452. return Math.abs(this.systemTemperature -
  453. this.prevSystemTemperature) < 0.00001 || this.temperature <= 0;
  454. },
  455. getSystemTemperature: function () {
  456. return this.nodes.reduce(function (value, node) {
  457. return value + node.temperature;
  458. }, 0);
  459. },
  460. vectorLength: function (vector) {
  461. return Math.sqrt(vector.x * vector.x + vector.y * vector.y);
  462. },
  463. getDistR: function (nodeA, nodeB) {
  464. var distance = this.getDistXY(nodeA, nodeB);
  465. return this.vectorLength(distance);
  466. },
  467. getDistXY: function (nodeA, nodeB) {
  468. var xDist = nodeA.plotX - nodeB.plotX, yDist = nodeA.plotY - nodeB.plotY;
  469. return {
  470. x: xDist,
  471. y: yDist,
  472. absX: Math.abs(xDist),
  473. absY: Math.abs(yDist)
  474. };
  475. }
  476. });
  477. /* ************************************************************************** *
  478. * Multiple series support:
  479. * ************************************************************************** */
  480. // Clear previous layouts
  481. addEvent(Chart, 'predraw', function () {
  482. if (this.graphLayoutsLookup) {
  483. this.graphLayoutsLookup.forEach(function (layout) {
  484. layout.stop();
  485. });
  486. }
  487. });
  488. addEvent(Chart, 'render', function () {
  489. var systemsStable, afterRender = false;
  490. /**
  491. * @private
  492. */
  493. function layoutStep(layout) {
  494. if (layout.maxIterations-- &&
  495. isFinite(layout.temperature) &&
  496. !layout.isStable() &&
  497. !layout.enableSimulation) {
  498. // Hook similar to build-in addEvent, but instead of
  499. // creating whole events logic, use just a function.
  500. // It's faster which is important for rAF code.
  501. // Used e.g. in packed-bubble series for bubble radius
  502. // calculations
  503. if (layout.beforeStep) {
  504. layout.beforeStep();
  505. }
  506. layout.step();
  507. systemsStable = false;
  508. afterRender = true;
  509. }
  510. }
  511. if (this.graphLayoutsLookup) {
  512. setAnimation(false, this);
  513. // Start simulation
  514. this.graphLayoutsLookup.forEach(function (layout) {
  515. layout.start();
  516. });
  517. // Just one sync step, to run different layouts similar to
  518. // async mode.
  519. while (!systemsStable) {
  520. systemsStable = true;
  521. this.graphLayoutsLookup.forEach(layoutStep);
  522. }
  523. if (afterRender) {
  524. this.series.forEach(function (s) {
  525. if (s && s.layout) {
  526. s.render();
  527. }
  528. });
  529. }
  530. }
  531. });
  532. // disable simulation before print if enabled
  533. addEvent(Chart, 'beforePrint', function () {
  534. if (this.graphLayoutsLookup) {
  535. this.graphLayoutsLookup.forEach(function (layout) {
  536. layout.updateSimulation(false);
  537. });
  538. this.redraw();
  539. }
  540. });
  541. // re-enable simulation after print
  542. addEvent(Chart, 'afterPrint', function () {
  543. if (this.graphLayoutsLookup) {
  544. this.graphLayoutsLookup.forEach(function (layout) {
  545. // return to default simulation
  546. layout.updateSimulation();
  547. });
  548. }
  549. this.redraw();
  550. });