layout.worker.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { forceSimulation, forceLink, forceCollide, forceX } from 'd3-force';
  2. addEventListener('message', (event) => {
  3. const { nodes, edges, config } = event.data;
  4. layout(nodes, edges, config);
  5. postMessage({ nodes, edges });
  6. });
  7. /**
  8. * Use d3 force layout to lay the nodes in a sensible way. This function modifies the nodes adding the x,y positions
  9. * and also fills in node references in edges instead of node ids.
  10. */
  11. export function layout(nodes, edges, config) {
  12. // Start with some hardcoded positions so it starts laid out from left to right
  13. let { roots, secondLevelRoots } = initializePositions(nodes, edges);
  14. // There always seems to be one or more root nodes each with single edge and we want to have them static on the
  15. // left neatly in something like grid layout
  16. [...roots, ...secondLevelRoots].forEach((n, index) => {
  17. n.fx = n.x;
  18. });
  19. const simulation = forceSimulation(nodes)
  20. .force(
  21. 'link',
  22. forceLink(edges)
  23. .id((d) => d.id)
  24. .distance(config.linkDistance)
  25. .strength(config.linkStrength)
  26. )
  27. // to keep the left to right layout we add force that pulls all nodes to right but because roots are fixed it will
  28. // apply only to non root nodes
  29. .force('x', forceX(config.forceX).strength(config.forceXStrength))
  30. // Make sure nodes don't overlap
  31. .force('collide', forceCollide(config.forceCollide));
  32. // 300 ticks for the simulation are recommended but less would probably work too, most movement is done in first
  33. // few iterations and then all the forces gets smaller https://github.com/d3/d3-force#simulation_alphaDecay
  34. simulation.tick(config.tick);
  35. simulation.stop();
  36. // We do centering here instead of using centering force to keep this more stable
  37. centerNodes(nodes);
  38. }
  39. /**
  40. * This initializes positions of the graph by going from the root to it's children and laying it out in a grid from left
  41. * to right. This works only so, so because service map graphs can have cycles and children levels are not ordered in a
  42. * way to minimize the edge lengths. Nevertheless this seems to make the graph easier to nudge with the forces later on
  43. * than with the d3 default initial positioning. Also we can fix the root positions later on for a bit more neat
  44. * organisation.
  45. *
  46. * This function directly modifies the nodes given and only returns references to root nodes so they do not have to be
  47. * found again later on.
  48. *
  49. * How the spacing could look like approximately:
  50. * 0 - 0 - 0 - 0
  51. * \- 0 - 0 |
  52. * \- 0 -/
  53. * 0 - 0 -/
  54. */
  55. function initializePositions(nodes, edges) {
  56. // To prevent going in cycles
  57. const alreadyPositioned = {};
  58. const nodesMap = nodes.reduce((acc, node) => {
  59. acc[node.id] = node;
  60. return acc;
  61. }, {});
  62. const edgesMap = edges.reduce((acc, edge) => {
  63. const sourceId = edge.source;
  64. acc[sourceId] = [...(acc[sourceId] || []), edge];
  65. return acc;
  66. }, {});
  67. let roots = nodes.filter((n) => n.incoming === 0);
  68. // For things like service maps we assume there is some root (client) node but if there is none then selecting
  69. // any node as a starting point should work the same.
  70. if (!roots.length) {
  71. roots = [nodes[0]];
  72. }
  73. let secondLevelRoots = roots.reduce((acc, r) => {
  74. acc.push(...(edgesMap[r.id] ? edgesMap[r.id].map((e) => nodesMap[e.target]) : []));
  75. return acc;
  76. }, []);
  77. const rootYSpacing = 300;
  78. const nodeYSpacing = 200;
  79. const nodeXSpacing = 200;
  80. let rootY = 0;
  81. for (const root of roots) {
  82. let graphLevel = [root];
  83. let x = 0;
  84. while (graphLevel.length > 0) {
  85. const nextGraphLevel = [];
  86. let y = rootY;
  87. for (const node of graphLevel) {
  88. if (alreadyPositioned[node.id]) {
  89. continue;
  90. }
  91. // Initialize positions based on the spacing in the grid
  92. node.x = x;
  93. node.y = y;
  94. alreadyPositioned[node.id] = true;
  95. // Move to next Y position for next node
  96. y += nodeYSpacing;
  97. if (edgesMap[node.id]) {
  98. nextGraphLevel.push(...edgesMap[node.id].map((edge) => nodesMap[edge.target]));
  99. }
  100. }
  101. graphLevel = nextGraphLevel;
  102. // Move to next X position for next level
  103. x += nodeXSpacing;
  104. // Reset Y back to baseline for this root
  105. y = rootY;
  106. }
  107. rootY += rootYSpacing;
  108. }
  109. return { roots, secondLevelRoots };
  110. }
  111. /**
  112. * Makes sure that the center of the graph based on it's bound is in 0, 0 coordinates.
  113. * Modifies the nodes directly.
  114. */
  115. function centerNodes(nodes) {
  116. const bounds = graphBounds(nodes);
  117. for (let node of nodes) {
  118. node.x = node.x - bounds.center.x;
  119. node.y = node.y - bounds.center.y;
  120. }
  121. }
  122. /**
  123. * Get bounds of the graph meaning the extent of the nodes in all directions.
  124. */
  125. function graphBounds(nodes) {
  126. if (nodes.length === 0) {
  127. return { top: 0, right: 0, bottom: 0, left: 0, center: { x: 0, y: 0 } };
  128. }
  129. const bounds = nodes.reduce(
  130. (acc, node) => {
  131. if (node.x > acc.right) {
  132. acc.right = node.x;
  133. }
  134. if (node.x < acc.left) {
  135. acc.left = node.x;
  136. }
  137. if (node.y > acc.bottom) {
  138. acc.bottom = node.y;
  139. }
  140. if (node.y < acc.top) {
  141. acc.top = node.y;
  142. }
  143. return acc;
  144. },
  145. { top: Infinity, right: -Infinity, bottom: -Infinity, left: Infinity }
  146. );
  147. const y = bounds.top + (bounds.bottom - bounds.top) / 2;
  148. const x = bounds.left + (bounds.right - bounds.left) / 2;
  149. return {
  150. ...bounds,
  151. center: {
  152. x,
  153. y,
  154. },
  155. };
  156. }