useNodeLimit.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import { fromPairs, uniq } from 'lodash';
  2. import { useMemo } from 'react';
  3. import { Config } from './layout';
  4. import { EdgeDatumLayout, NodeDatum, NodesMarker } from './types';
  5. type NodesMap = Record<string, NodeDatum>;
  6. type EdgesMap = Record<string, EdgeDatumLayout[]>;
  7. /**
  8. * Limits the number of nodes by going from the roots breadth first until we have desired number of nodes.
  9. */
  10. export function useNodeLimit(
  11. nodes: NodeDatum[],
  12. edges: EdgeDatumLayout[],
  13. limit: number,
  14. config: Config,
  15. rootId?: string
  16. ): { nodes: NodeDatum[]; edges: EdgeDatumLayout[]; markers?: NodesMarker[] } {
  17. // This is pretty expensive also this happens once in the layout code when initializing position but it's a bit
  18. // tricky to do it only once and reuse the results because layout directly modifies the nodes.
  19. const [edgesMap, nodesMap] = useMemo(() => {
  20. // Make sure we don't compute this until we have all the data.
  21. if (!(nodes.length && edges.length)) {
  22. return [{}, {}];
  23. }
  24. const edgesMap = edges.reduce<EdgesMap>((acc, e) => {
  25. acc[e.source.id] = [...(acc[e.source.id] ?? []), e];
  26. acc[e.target.id] = [...(acc[e.target.id] ?? []), e];
  27. return acc;
  28. }, {});
  29. const nodesMap = nodes.reduce<NodesMap>((acc, node) => {
  30. acc[node.id] = node;
  31. return acc;
  32. }, {});
  33. return [edgesMap, nodesMap];
  34. }, [edges, nodes]);
  35. return useMemo(() => {
  36. if (nodes.length <= limit) {
  37. return { nodes, edges };
  38. }
  39. if (config.gridLayout) {
  40. return limitGridLayout(nodes, limit, rootId);
  41. }
  42. return limitGraphLayout(nodes, edges, nodesMap, edgesMap, limit, rootId);
  43. }, [edges, edgesMap, limit, nodes, nodesMap, rootId, config.gridLayout]);
  44. }
  45. export function limitGraphLayout(
  46. nodes: NodeDatum[],
  47. edges: EdgeDatumLayout[],
  48. nodesMap: NodesMap,
  49. edgesMap: EdgesMap,
  50. limit: number,
  51. rootId?: string
  52. ) {
  53. let roots;
  54. if (rootId) {
  55. roots = [nodesMap[rootId]];
  56. } else {
  57. roots = nodes.filter((n) => n.incoming === 0);
  58. // TODO: same code as layout
  59. if (!roots.length) {
  60. roots = [nodes[0]];
  61. }
  62. }
  63. const { visibleNodes, markers } = collectVisibleNodes(limit, roots, nodesMap, edgesMap);
  64. const markersWithStats = collectMarkerStats(markers, visibleNodes, nodesMap, edgesMap);
  65. const markersMap = fromPairs(markersWithStats.map((m) => [m.node.id, m]));
  66. for (const marker of markersWithStats) {
  67. if (marker.count === 1) {
  68. delete markersMap[marker.node.id];
  69. visibleNodes[marker.node.id] = marker.node;
  70. }
  71. }
  72. // Show all edges between visible nodes or placeholder markers
  73. const visibleEdges = edges.filter(
  74. (e) =>
  75. (visibleNodes[e.source.id] || markersMap[e.source.id]) && (visibleNodes[e.target.id] || markersMap[e.target.id])
  76. );
  77. return {
  78. nodes: Object.values(visibleNodes),
  79. edges: visibleEdges,
  80. markers: Object.values(markersMap),
  81. };
  82. }
  83. export function limitGridLayout(nodes: NodeDatum[], limit: number, rootId?: string) {
  84. let start = 0;
  85. let stop = limit;
  86. let markers: NodesMarker[] = [];
  87. if (rootId) {
  88. const index = nodes.findIndex((node) => node.id === rootId);
  89. const prevLimit = Math.floor(limit / 2);
  90. let afterLimit = prevLimit;
  91. start = index - prevLimit;
  92. if (start < 0) {
  93. afterLimit += Math.abs(start);
  94. start = 0;
  95. }
  96. stop = index + afterLimit + 1;
  97. if (stop > nodes.length) {
  98. if (start > 0) {
  99. start = Math.max(0, start - (stop - nodes.length));
  100. }
  101. stop = nodes.length;
  102. }
  103. if (start > 1) {
  104. markers.push({ node: nodes[start - 1], count: start });
  105. }
  106. if (nodes.length - stop > 1) {
  107. markers.push({ node: nodes[stop], count: nodes.length - stop });
  108. }
  109. } else {
  110. if (nodes.length - limit > 1) {
  111. markers = [{ node: nodes[limit], count: nodes.length - limit }];
  112. }
  113. }
  114. return {
  115. nodes: nodes.slice(start, stop),
  116. edges: [],
  117. markers,
  118. };
  119. }
  120. /**
  121. * Breath first traverse of the graph collecting all the nodes until we reach the limit. It also returns markers which
  122. * are nodes on the edges which did not make it into the limit but can be used as clickable markers for manually
  123. * expanding the graph.
  124. * @param limit
  125. * @param roots - Nodes where to start the traversal. In case of exploration this can be any node that user clicked on.
  126. * @param nodesMap - Node id to node
  127. * @param edgesMap - This is a map of node id to a list of edges (both ingoing and outgoing)
  128. */
  129. function collectVisibleNodes(
  130. limit: number,
  131. roots: NodeDatum[],
  132. nodesMap: Record<string, NodeDatum>,
  133. edgesMap: Record<string, EdgeDatumLayout[]>
  134. ): { visibleNodes: Record<string, NodeDatum>; markers: NodeDatum[] } {
  135. const visibleNodes: Record<string, NodeDatum> = {};
  136. let stack = [...roots];
  137. while (Object.keys(visibleNodes).length < limit && stack.length > 0) {
  138. let current = stack.shift()!;
  139. // We are already showing this node. This can happen because graphs can be cyclic
  140. if (visibleNodes[current!.id]) {
  141. continue;
  142. }
  143. // Show this node
  144. visibleNodes[current.id] = current;
  145. const edges = edgesMap[current.id] || [];
  146. // Add any nodes that are connected to it on top of the stack to be considered in the next pass
  147. const connectedNodes = edges.map((e) => {
  148. // We don't care about direction here. Should not make much difference but argument could be made that with
  149. // directed graphs it should walk the graph directionally. Problem is when we focus on a node in the middle of
  150. // graph (not going from the "natural" root) we also want to show what was "before".
  151. const id = e.source.id === current.id ? e.target.id : e.source.id;
  152. return nodesMap[id];
  153. });
  154. stack = stack.concat(connectedNodes);
  155. }
  156. // Right now our stack contains all the nodes which are directly connected to the graph but did not make the cut.
  157. // Some of them though can be nodes we already are showing so we have to filter them and then use them as markers.
  158. const markers = uniq(stack.filter((n) => !visibleNodes[n.id]));
  159. return { visibleNodes, markers };
  160. }
  161. function collectMarkerStats(
  162. markers: NodeDatum[],
  163. visibleNodes: Record<string, NodeDatum>,
  164. nodesMap: Record<string, NodeDatum>,
  165. edgesMap: Record<string, EdgeDatumLayout[]>
  166. ): NodesMarker[] {
  167. return markers.map((marker) => {
  168. const nodesToCount: Record<string, NodeDatum> = {};
  169. let count = 0;
  170. let stack = [marker];
  171. while (stack.length > 0 && count <= 101) {
  172. let current = stack.shift()!;
  173. // We are showing this node so not going to count it as hidden.
  174. if (visibleNodes[current.id] || nodesToCount[current.id]) {
  175. continue;
  176. }
  177. if (!nodesToCount[current.id]) {
  178. count++;
  179. }
  180. nodesToCount[current.id] = current;
  181. const edges = edgesMap[current.id] || [];
  182. const connectedNodes = edges.map((e) => {
  183. const id = e.source.id === current.id ? e.target.id : e.source.id;
  184. return nodesMap[id];
  185. });
  186. stack = stack.concat(connectedNodes);
  187. }
  188. return {
  189. node: marker,
  190. count: count,
  191. };
  192. });
  193. }