@truedat/dd 8.11.6 → 8.11.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/package.json +4 -3
  2. package/src/components/Graphs.js +30 -4
  3. package/src/components/LineageContextMenu.js +185 -58
  4. package/src/components/LineageDownloadButton.js +16 -6
  5. package/src/components/LineageEventsPanel.js +80 -27
  6. package/src/components/LineageEventsTable.js +7 -3
  7. package/src/components/LineageGraph.js +5144 -306
  8. package/src/components/LineageGraphToolbar.js +896 -0
  9. package/src/components/LineageInspectorPanel.js +748 -0
  10. package/src/components/LineageResourceNode.js +501 -0
  11. package/src/components/LineageRoutes.js +1 -2
  12. package/src/components/LineageSelectionPanel.js +28 -12
  13. package/src/components/StructureGraph.js +8 -1
  14. package/src/components/StructureGraphView.js +14 -0
  15. package/src/components/StructureImpact.js +2 -22
  16. package/src/components/StructureLineage.js +2 -19
  17. package/src/components/StructuresUploadEventsTable.js +0 -3
  18. package/src/components/__tests__/LineageContextMenu.spec.js +42 -0
  19. package/src/components/__tests__/LineageGraph.spec.js +896 -0
  20. package/src/components/__tests__/LineageGraphInteractions.spec.js +660 -0
  21. package/src/components/__tests__/LineageGraphLayout.spec.js +620 -0
  22. package/src/components/__tests__/LineageGraphToolbar.spec.js +366 -0
  23. package/src/components/__tests__/LineageHierarchy.spec.js +116 -0
  24. package/src/components/__tests__/LineageInspectorPanel.spec.js +357 -0
  25. package/src/components/__tests__/LineageResourceNode.spec.js +338 -0
  26. package/src/components/__tests__/LineageRoutes.spec.js +27 -9
  27. package/src/components/__tests__/__snapshots__/LineageDownloadButton.spec.js.snap +23 -3
  28. package/src/components/__tests__/__snapshots__/LineageForm.spec.js.snap +23 -3
  29. package/src/components/__tests__/lineageEventHelpers.spec.js +60 -0
  30. package/src/components/lineageEventHelpers.js +53 -17
  31. package/src/reducers/__tests__/lineageEvents.spec.js +164 -0
  32. package/src/reducers/__tests__/lineageQuery.spec.js +11 -0
  33. package/src/reducers/lineageEvents.js +68 -2
  34. package/src/reducers/lineageQuery.js +2 -6
  35. package/src/styles/LineageGraphColors.less +105 -0
  36. package/src/styles/LineageGraphCore.less +20 -0
  37. package/src/styles/LineageResourceNode.less +711 -0
  38. package/src/styles/lineageGraph.less +2973 -84
  39. package/src/utils/__tests__/lineageGraphIndex.spec.js +52 -0
  40. package/src/utils/__tests__/lineageLayout.spec.js +234 -0
  41. package/src/utils/__tests__/lineageTopology.spec.js +256 -0
  42. package/src/utils/lineageGraphIndex.js +296 -0
  43. package/src/utils/lineageLayout.js +1047 -0
@@ -0,0 +1,1047 @@
1
+ const DEFAULT_NODE_WIDTH = 288;
2
+ const DEFAULT_NODE_HEIGHT = 82;
3
+ const COLUMN_GAP = 152;
4
+ const NODE_GAP = 44;
5
+ const FIELD_GAP = 4;
6
+ const ROOT_BRANCH_GAP = 40;
7
+ const GROUP_PADDING_X = 16;
8
+ const GROUP_PADDING_BOTTOM = 34;
9
+ const GROUP_HEADER_HEIGHT = 58;
10
+ const MIN_GROUP_WIDTH = 240;
11
+ const MIN_GROUP_HEIGHT = 82;
12
+ const MAX_SWEEPS = 6;
13
+ const MAX_CACHED_TOPOLOGIES = 4;
14
+
15
+ const topologyCache = new Map();
16
+
17
+ const finiteNumber = (value) => {
18
+ const number = Number(value);
19
+
20
+ return Number.isFinite(number) ? number : undefined;
21
+ };
22
+
23
+ const firstFiniteNumber = (values = [], fallback = 0) =>
24
+ values.map(finiteNumber).find((value) => value !== undefined) ?? fallback;
25
+
26
+ const nodeGeometry = (node = {}) => {
27
+ const geometry = node.data?.absoluteGeometry;
28
+
29
+ return {
30
+ x: firstFiniteNumber([geometry?.x, node.position?.x, node.x], 0),
31
+ y: firstFiniteNumber([geometry?.y, node.position?.y, node.y], 0),
32
+ w: firstFiniteNumber(
33
+ [geometry?.w, node.width, node.measured?.width],
34
+ DEFAULT_NODE_WIDTH,
35
+ ),
36
+ h: firstFiniteNumber(
37
+ [geometry?.h, node.height, node.measured?.height],
38
+ DEFAULT_NODE_HEIGHT,
39
+ ),
40
+ };
41
+ };
42
+
43
+ const nodeLabel = (node = {}) => `${node.data?.label || node.id || ""}`;
44
+
45
+ const nodeDepth = (node = {}) =>
46
+ firstFiniteNumber([node.data?.z, node.z], undefined);
47
+
48
+ const median = (values = []) => {
49
+ if (!values.length) return undefined;
50
+
51
+ const ordered = [...values].sort((valueA, valueB) => valueA - valueB);
52
+ const middle = Math.floor(ordered.length / 2);
53
+
54
+ return ordered.length % 2
55
+ ? ordered[middle]
56
+ : (ordered[middle - 1] + ordered[middle]) / 2;
57
+ };
58
+
59
+ const appendMapSet = (map, key, value) => {
60
+ const values = map.get(key) || new Set();
61
+
62
+ values.add(value);
63
+ map.set(key, values);
64
+ };
65
+
66
+ const appendWeightedNeighbour = (map, key, value, weight = 1) => {
67
+ const values = map.get(key) || [];
68
+
69
+ values.push(...Array.from({ length: weight }, () => value));
70
+ map.set(key, values);
71
+ };
72
+
73
+ const sortedIds = (ids = []) =>
74
+ [...ids].sort((idA, idB) => idA.localeCompare(idB));
75
+
76
+ const buildHierarchy = (nodes = []) => {
77
+ const nodeById = new Map(
78
+ nodes.filter(Boolean).map((node) => [node.id, node]),
79
+ );
80
+ const parentById = new Map();
81
+ const childIdsByParentId = new Map();
82
+
83
+ nodeById.forEach((node) => {
84
+ if (!node.parentId || !nodeById.has(node.parentId)) return;
85
+
86
+ parentById.set(node.id, node.parentId);
87
+ appendMapSet(childIdsByParentId, node.parentId, node.id);
88
+ });
89
+
90
+ const vertexIds = new Set(
91
+ [...nodeById.keys()].filter(
92
+ (id) => !(childIdsByParentId.get(id)?.size > 0),
93
+ ),
94
+ );
95
+
96
+ return { childIdsByParentId, nodeById, parentById, vertexIds };
97
+ };
98
+
99
+ const descendantVertexIds = (id, childIdsByParentId, vertexIds, cache) => {
100
+ if (cache.has(id)) return cache.get(id);
101
+ if (vertexIds.has(id)) {
102
+ const result = [id];
103
+
104
+ cache.set(id, result);
105
+ return result;
106
+ }
107
+
108
+ const result = sortedIds(childIdsByParentId.get(id) || []).flatMap(
109
+ (childId) =>
110
+ descendantVertexIds(childId, childIdsByParentId, vertexIds, cache),
111
+ );
112
+
113
+ cache.set(id, result);
114
+ return result;
115
+ };
116
+
117
+ const endpointVertexId = (id, hierarchy, descendantsCache) => {
118
+ if (hierarchy.vertexIds.has(id)) return id;
119
+
120
+ return descendantVertexIds(
121
+ id,
122
+ hierarchy.childIdsByParentId,
123
+ hierarchy.vertexIds,
124
+ descendantsCache,
125
+ )[0];
126
+ };
127
+
128
+ const normalizedRelationEdges = (edges = [], graphType, hierarchy) => {
129
+ const descendantsCache = new Map();
130
+ const relationByKey = new Map();
131
+
132
+ edges.forEach((edge) => {
133
+ if (!edge || edge.data?.containment) return;
134
+
135
+ const renderedSource = endpointVertexId(
136
+ edge.source,
137
+ hierarchy,
138
+ descendantsCache,
139
+ );
140
+ const renderedTarget = endpointVertexId(
141
+ edge.target,
142
+ hierarchy,
143
+ descendantsCache,
144
+ );
145
+
146
+ if (
147
+ !renderedSource ||
148
+ !renderedTarget ||
149
+ renderedSource === renderedTarget
150
+ ) {
151
+ return;
152
+ }
153
+
154
+ const source = renderedSource;
155
+ const target = renderedTarget;
156
+ const key = `${source}::${target}`;
157
+ const relation = relationByKey.get(key);
158
+ const weight = Math.max(
159
+ 1,
160
+ Math.min(
161
+ 12,
162
+ Math.round(
163
+ firstFiniteNumber(
164
+ [edge.data?.aggregatedCount, edge.data?.count, edge.count],
165
+ 1,
166
+ ),
167
+ ),
168
+ ),
169
+ );
170
+
171
+ if (relation) {
172
+ relationByKey.set(key, {
173
+ ...relation,
174
+ weight: Math.min(12, relation.weight + weight),
175
+ });
176
+ return;
177
+ }
178
+
179
+ relationByKey.set(key, { source, target, weight });
180
+ });
181
+
182
+ return [...relationByKey.values()].sort(
183
+ (relationA, relationB) =>
184
+ relationA.source.localeCompare(relationB.source) ||
185
+ relationA.target.localeCompare(relationB.target),
186
+ );
187
+ };
188
+
189
+ const stronglyConnectedComponents = (vertexIds, outgoing) => {
190
+ const nextIndex = { current: 0 };
191
+ const indexes = new Map();
192
+ const lowLinks = new Map();
193
+ const stack = [];
194
+ const onStack = new Set();
195
+ const components = [];
196
+
197
+ const visit = (id) => {
198
+ indexes.set(id, nextIndex.current);
199
+ lowLinks.set(id, nextIndex.current);
200
+ nextIndex.current += 1;
201
+ stack.push(id);
202
+ onStack.add(id);
203
+
204
+ sortedIds(outgoing.get(id) || []).forEach((targetId) => {
205
+ if (!indexes.has(targetId)) {
206
+ visit(targetId);
207
+ lowLinks.set(id, Math.min(lowLinks.get(id), lowLinks.get(targetId)));
208
+ } else if (onStack.has(targetId)) {
209
+ lowLinks.set(id, Math.min(lowLinks.get(id), indexes.get(targetId)));
210
+ }
211
+ });
212
+
213
+ if (lowLinks.get(id) !== indexes.get(id)) return;
214
+
215
+ const component = [];
216
+ const popUntilRoot = () => {
217
+ const memberId = stack.pop();
218
+ onStack.delete(memberId);
219
+ component.push(memberId);
220
+ if (memberId !== id) popUntilRoot();
221
+ };
222
+ popUntilRoot();
223
+
224
+ components.push(component.sort((idA, idB) => idA.localeCompare(idB)));
225
+ };
226
+
227
+ sortedIds(vertexIds).forEach((id) => {
228
+ if (!indexes.has(id)) visit(id);
229
+ });
230
+
231
+ return components;
232
+ };
233
+
234
+ const depthForVertex = (id, hierarchy) => {
235
+ if (!id) return undefined;
236
+ const depth = nodeDepth(hierarchy.nodeById.get(id));
237
+ return depth !== undefined
238
+ ? depth
239
+ : depthForVertex(hierarchy.parentById.get(id), hierarchy);
240
+ };
241
+
242
+ const componentRanks = (hierarchy, relations) => {
243
+ const outgoing = new Map(
244
+ [...hierarchy.vertexIds].map((id) => [id, new Set()]),
245
+ );
246
+
247
+ relations.forEach(({ source, target }) =>
248
+ appendMapSet(outgoing, source, target),
249
+ );
250
+
251
+ const components = stronglyConnectedComponents(hierarchy.vertexIds, outgoing);
252
+ const componentByVertexId = new Map();
253
+
254
+ components.forEach((component, componentIndex) => {
255
+ component.forEach((id) => componentByVertexId.set(id, componentIndex));
256
+ });
257
+
258
+ const componentOutgoing = new Map(
259
+ components.map((_, componentIndex) => [componentIndex, new Set()]),
260
+ );
261
+ const componentIncomingCount = new Map(
262
+ components.map((_, componentIndex) => [componentIndex, 0]),
263
+ );
264
+
265
+ relations.forEach(({ source, target }) => {
266
+ const sourceComponent = componentByVertexId.get(source);
267
+ const targetComponent = componentByVertexId.get(target);
268
+
269
+ if (
270
+ sourceComponent === targetComponent ||
271
+ componentOutgoing.get(sourceComponent).has(targetComponent)
272
+ ) {
273
+ return;
274
+ }
275
+
276
+ componentOutgoing.get(sourceComponent).add(targetComponent);
277
+ componentIncomingCount.set(
278
+ targetComponent,
279
+ componentIncomingCount.get(targetComponent) + 1,
280
+ );
281
+ });
282
+ const connectedComponentIds = new Set(
283
+ relations.flatMap(({ source, target }) => [
284
+ componentByVertexId.get(source),
285
+ componentByVertexId.get(target),
286
+ ]),
287
+ );
288
+
289
+ const componentLabel = (componentIndex) =>
290
+ components[componentIndex][0] || "";
291
+ const queue = [...componentIncomingCount]
292
+ .filter(([, count]) => count === 0)
293
+ .map(([componentIndex]) => componentIndex)
294
+ .sort((indexA, indexB) =>
295
+ componentLabel(indexA).localeCompare(componentLabel(indexB)),
296
+ );
297
+ const rankByComponent = new Map(
298
+ queue.map((componentIndex) => [componentIndex, 0]),
299
+ );
300
+
301
+ while (queue.length) {
302
+ const componentIndex = queue.shift();
303
+ const rank = rankByComponent.get(componentIndex) || 0;
304
+
305
+ [...(componentOutgoing.get(componentIndex) || [])]
306
+ .sort((indexA, indexB) => indexA - indexB)
307
+ .forEach((targetComponent) => {
308
+ rankByComponent.set(
309
+ targetComponent,
310
+ Math.max(rankByComponent.get(targetComponent) || 0, rank + 1),
311
+ );
312
+ componentIncomingCount.set(
313
+ targetComponent,
314
+ componentIncomingCount.get(targetComponent) - 1,
315
+ );
316
+
317
+ if (componentIncomingCount.get(targetComponent) === 0) {
318
+ queue.push(targetComponent);
319
+ queue.sort((indexA, indexB) =>
320
+ componentLabel(indexA).localeCompare(componentLabel(indexB)),
321
+ );
322
+ }
323
+ });
324
+ }
325
+
326
+ const isolatedComponents = components
327
+ .map((component, componentIndex) => ({
328
+ component,
329
+ componentIndex,
330
+ isolated: !connectedComponentIds.has(componentIndex),
331
+ depth: median(
332
+ component
333
+ .map((id) => depthForVertex(id, hierarchy))
334
+ .filter((value) => value !== undefined),
335
+ ),
336
+ }))
337
+ .filter(({ isolated }) => isolated);
338
+ const depthOrder = new Map(
339
+ [
340
+ ...new Set(
341
+ isolatedComponents
342
+ .map(({ depth }) => depth)
343
+ .filter((depth) => depth !== undefined),
344
+ ),
345
+ ]
346
+ .sort((depthA, depthB) => depthA - depthB)
347
+ .map((depth, index) => [depth, index]),
348
+ );
349
+
350
+ isolatedComponents.forEach(({ componentIndex, depth }) => {
351
+ if (depth !== undefined)
352
+ rankByComponent.set(componentIndex, depthOrder.get(depth));
353
+ });
354
+
355
+ return new Map(
356
+ [...hierarchy.vertexIds].map((id) => [
357
+ id,
358
+ rankByComponent.get(componentByVertexId.get(id)) || 0,
359
+ ]),
360
+ );
361
+ };
362
+
363
+ const stableVertexCompare = (hierarchy) => (idA, idB) => {
364
+ const nodeA = hierarchy.nodeById.get(idA);
365
+ const nodeB = hierarchy.nodeById.get(idB);
366
+ const yDiff = nodeGeometry(nodeA).y - nodeGeometry(nodeB).y;
367
+
368
+ if (yDiff !== 0) return yDiff;
369
+
370
+ const labelDiff = nodeLabel(nodeA).localeCompare(nodeLabel(nodeB));
371
+
372
+ return labelDiff || idA.localeCompare(idB);
373
+ };
374
+
375
+ const crossingMinimizedOrder = (hierarchy, relations, rankById) => {
376
+ const layerIds = new Map();
377
+ const previousIds = new Map();
378
+ const nextIds = new Map();
379
+ const incomingSegmentsById = new Map();
380
+ const outgoingSegmentsById = new Map();
381
+ const stableCompare = stableVertexCompare(hierarchy);
382
+
383
+ const appendLayerId = (rank, id) => {
384
+ const ids = layerIds.get(rank) || [];
385
+
386
+ ids.push(id);
387
+ layerIds.set(rank, ids);
388
+ };
389
+
390
+ [...hierarchy.vertexIds]
391
+ .sort(stableCompare)
392
+ .forEach((id) => appendLayerId(rankById.get(id) || 0, id));
393
+
394
+ const appendSegment = (source, target, weight) => {
395
+ const segment = { source, target, weight };
396
+ const incoming = incomingSegmentsById.get(target) || [];
397
+ const outgoing = outgoingSegmentsById.get(source) || [];
398
+
399
+ incoming.push(segment);
400
+ outgoing.push(segment);
401
+ incomingSegmentsById.set(target, incoming);
402
+ outgoingSegmentsById.set(source, outgoing);
403
+ appendWeightedNeighbour(previousIds, target, source, weight);
404
+ appendWeightedNeighbour(nextIds, source, target, weight);
405
+ };
406
+
407
+ relations.forEach(({ source, target, weight }, relationIndex) => {
408
+ const sourceRank = rankById.get(source) || 0;
409
+ const targetRank = rankById.get(target) || 0;
410
+
411
+ if (targetRank <= sourceRank) return;
412
+
413
+ const lastId = Array.from(
414
+ { length: targetRank - sourceRank - 1 },
415
+ (_, i) => sourceRank + 1 + i,
416
+ ).reduce((prevId, rank) => {
417
+ const virtualId = `@@lineage-layout:${relationIndex}:${rank}`;
418
+ appendLayerId(rank, virtualId);
419
+ appendSegment(prevId, virtualId, weight);
420
+ return virtualId;
421
+ }, source);
422
+
423
+ appendSegment(lastId, target, weight);
424
+ });
425
+
426
+ const ranks = [...layerIds.keys()].sort((rankA, rankB) => rankA - rankB);
427
+ const stableOrder = new Map();
428
+
429
+ ranks.forEach((rank) => {
430
+ layerIds.get(rank).forEach((id, index) => stableOrder.set(id, index));
431
+ });
432
+
433
+ const positions = () => {
434
+ const result = new Map();
435
+
436
+ ranks.forEach((rank) => {
437
+ layerIds.get(rank).forEach((id, index) => result.set(id, index));
438
+ });
439
+ return result;
440
+ };
441
+
442
+ const sweep = (orderedRanks, neighbourIds) => {
443
+ const orderById = positions();
444
+
445
+ orderedRanks.forEach((rank) => {
446
+ const ids = layerIds.get(rank);
447
+ const scoreById = new Map(
448
+ ids.map((id) => [
449
+ id,
450
+ median(
451
+ (neighbourIds.get(id) || [])
452
+ .map((neighbourId) => orderById.get(neighbourId))
453
+ .filter((value) => value !== undefined),
454
+ ),
455
+ ]),
456
+ );
457
+
458
+ ids.sort((idA, idB) => {
459
+ const scoreA = scoreById.get(idA);
460
+ const scoreB = scoreById.get(idB);
461
+
462
+ if (scoreA !== undefined && scoreB !== undefined) {
463
+ if (scoreA !== scoreB) return scoreA - scoreB;
464
+ } else {
465
+ if (scoreA !== undefined) return -1;
466
+ if (scoreB !== undefined) return 1;
467
+ }
468
+
469
+ return (
470
+ (stableOrder.get(idA) || 0) - (stableOrder.get(idB) || 0) ||
471
+ idA.localeCompare(idB)
472
+ );
473
+ });
474
+ ids.forEach((id, index) => orderById.set(id, index));
475
+ });
476
+ };
477
+
478
+ Array.from({ length: MAX_SWEEPS }).forEach((_, pass) => {
479
+ if (pass % 2 === 0) sweep(ranks.slice(1), previousIds);
480
+ else sweep([...ranks].reverse().slice(1), nextIds);
481
+ });
482
+
483
+ const swapDelta = (idA, idB, orderById) => {
484
+ const computeCrossings = (segmentsA, segmentsB, oppositeId) =>
485
+ segmentsA.reduce(
486
+ (outer, segA) =>
487
+ outer +
488
+ segmentsB.reduce((inner, segB) => {
489
+ const posA = orderById.get(segA[oppositeId]);
490
+ const posB = orderById.get(segB[oppositeId]);
491
+ if (posA === undefined || posB === undefined) return inner;
492
+ const w = segA.weight * segB.weight;
493
+ return inner + (posA > posB ? -w : posA < posB ? w : 0);
494
+ }, 0),
495
+ 0,
496
+ );
497
+
498
+ return (
499
+ computeCrossings(
500
+ incomingSegmentsById.get(idA) || [],
501
+ incomingSegmentsById.get(idB) || [],
502
+ "source",
503
+ ) +
504
+ computeCrossings(
505
+ outgoingSegmentsById.get(idA) || [],
506
+ outgoingSegmentsById.get(idB) || [],
507
+ "target",
508
+ )
509
+ );
510
+ };
511
+
512
+ ranks.forEach((rank) => {
513
+ const ids = layerIds.get(rank);
514
+ const directions = [1, -1];
515
+
516
+ directions.forEach((direction) => {
517
+ const orderById = positions();
518
+ const start = direction > 0 ? 0 : ids.length - 2;
519
+ const end = direction > 0 ? ids.length - 1 : -1;
520
+
521
+ const bubble = (index) => {
522
+ if (direction > 0 ? index >= end : index <= end) return;
523
+ const idA = ids[index];
524
+ const idB = ids[index + 1];
525
+ if (swapDelta(idA, idB, orderById) < 0) {
526
+ ids.splice(index, 2, idB, idA);
527
+ orderById.set(idA, index + 1);
528
+ orderById.set(idB, index);
529
+ }
530
+ bubble(index + direction);
531
+ };
532
+ bubble(start);
533
+ });
534
+ });
535
+
536
+ const orderById = new Map();
537
+
538
+ ranks.forEach((rank) => {
539
+ layerIds.get(rank).forEach((id, index) => {
540
+ if (hierarchy.vertexIds.has(id)) orderById.set(id, index);
541
+ });
542
+ });
543
+
544
+ return orderById;
545
+ };
546
+
547
+ const stringHash = (value) => {
548
+ const hash = [...value].reduce(
549
+ (h, ch) => Math.imul(h ^ ch.charCodeAt(0), 16777619) >>> 0,
550
+ 2166136261,
551
+ );
552
+ return hash >>> 0;
553
+ };
554
+
555
+ const unorderedSignature = (items, partFor) => {
556
+ const { count, sum, squaredSum, xor } = items.reduce(
557
+ (acc, item) => {
558
+ const part = partFor(item);
559
+ if (part === undefined) return acc;
560
+ const hash = stringHash(part);
561
+ return {
562
+ count: acc.count + 1,
563
+ sum: (acc.sum + hash) >>> 0,
564
+ squaredSum: (acc.squaredSum + Math.imul(hash, hash)) >>> 0,
565
+ xor: (acc.xor ^ hash) >>> 0,
566
+ };
567
+ },
568
+ { count: 0, sum: 0, squaredSum: 0, xor: 0 },
569
+ );
570
+ return `${count}:${sum.toString(36)}:${squaredSum.toString(36)}:${xor.toString(36)}`;
571
+ };
572
+
573
+ const topologySignature = (nodes, edges, graphType) => {
574
+ const nodeSignature = unorderedSignature(
575
+ nodes,
576
+ (node) => `${node.id}:${node.parentId || ""}`,
577
+ );
578
+ const edgeSignature = unorderedSignature(edges, (edge) => {
579
+ if (!edge || edge.data?.containment) return undefined;
580
+
581
+ return [
582
+ edge.source || "",
583
+ edge.target || "",
584
+ firstFiniteNumber(
585
+ [edge.data?.aggregatedCount, edge.data?.count, edge.count],
586
+ 1,
587
+ ),
588
+ ].join(":");
589
+ });
590
+
591
+ return `${graphType}::${nodeSignature}::${edgeSignature}`;
592
+ };
593
+
594
+ const buildTopologyPlan = (hierarchy, edges, graphType) => {
595
+ const relations = normalizedRelationEdges(edges, graphType, hierarchy);
596
+ const rankById = componentRanks(hierarchy, relations);
597
+ const orderById = crossingMinimizedOrder(hierarchy, relations, rankById);
598
+
599
+ return { orderById, rankById, relations };
600
+ };
601
+
602
+ const cachedTopologyPlan = (nodes, edges, graphType) => {
603
+ const signature = topologySignature(nodes, edges, graphType);
604
+ const hierarchy = buildHierarchy(nodes);
605
+
606
+ if (topologyCache.has(signature)) {
607
+ return { ...topologyCache.get(signature), hierarchy };
608
+ }
609
+
610
+ const plan = buildTopologyPlan(hierarchy, edges, graphType);
611
+
612
+ topologyCache.set(signature, plan);
613
+ if (topologyCache.size > MAX_CACHED_TOPOLOGIES) {
614
+ topologyCache.delete(topologyCache.keys().next().value);
615
+ }
616
+
617
+ return { ...plan, hierarchy };
618
+ };
619
+
620
+ const previousGeometryMap = (nodes = []) =>
621
+ new Map(nodes.filter(Boolean).map((node) => [node.id, nodeGeometry(node)]));
622
+
623
+ const translateGeometries = (geometries, deltaX, deltaY) => {
624
+ const translated = new Map();
625
+
626
+ geometries.forEach((geometry, id) => {
627
+ translated.set(id, {
628
+ ...geometry,
629
+ x: geometry.x + deltaX,
630
+ y: geometry.y + deltaY,
631
+ });
632
+ });
633
+
634
+ return translated;
635
+ };
636
+
637
+ const mergeGeometryMaps = (blocks = []) => {
638
+ const geometries = new Map();
639
+
640
+ blocks.forEach((block) => {
641
+ block.geometries.forEach((geometry, id) => geometries.set(id, geometry));
642
+ });
643
+ return geometries;
644
+ };
645
+
646
+ const shiftedBlock = (block, deltaY) => ({
647
+ ...block,
648
+ geometries: translateGeometries(block.geometries, 0, deltaY),
649
+ });
650
+
651
+ const blocksOverlapX = (blockA, blockB, gap = NODE_GAP) =>
652
+ blockA.left < blockB.right + gap && blockB.left < blockA.right + gap;
653
+
654
+ const createContainerBlock = ({
655
+ childBlocks,
656
+ id,
657
+ node,
658
+ previousById,
659
+ root = false,
660
+ }) => {
661
+ const stableYFor = (block) =>
662
+ previousById.get(block.id)?.y ?? block.stableY ?? 0;
663
+ const orderedBlocks = [...childBlocks].sort((blockA, blockB) => {
664
+ const rankDiff = blockA.minRank - blockB.minRank;
665
+
666
+ if (rankDiff !== 0) return rankDiff;
667
+
668
+ const scoreDiff = blockA.score - blockB.score;
669
+
670
+ if (scoreDiff !== 0) return scoreDiff;
671
+
672
+ return (
673
+ stableYFor(blockA) - stableYFor(blockB) ||
674
+ blockA.id.localeCompare(blockB.id)
675
+ );
676
+ });
677
+ const forceStack = Boolean(node?.data?.resourceContainer);
678
+ const lanes = [];
679
+
680
+ orderedBlocks.forEach((block) => {
681
+ const lane = forceStack
682
+ ? undefined
683
+ : lanes.find(
684
+ (current) =>
685
+ block.minRank > current.maxRank &&
686
+ current.blocks.every(
687
+ (existing) => !blocksOverlapX(existing, block),
688
+ ),
689
+ );
690
+
691
+ if (lane) {
692
+ lane.blocks.push(block);
693
+ const updatedLane = {
694
+ ...lane,
695
+ height: Math.max(lane.height, block.height),
696
+ maxRank: Math.max(lane.maxRank, block.maxRank),
697
+ };
698
+ lanes.splice(lanes.indexOf(lane), 1, updatedLane);
699
+ return;
700
+ }
701
+
702
+ lanes.push({
703
+ blocks: [block],
704
+ height: block.height,
705
+ maxRank: block.maxRank,
706
+ });
707
+ });
708
+
709
+ const headerHeight = root ? 0 : GROUP_HEADER_HEIGHT;
710
+ const laneGap = root
711
+ ? orderedBlocks.length > 4
712
+ ? ROOT_BRANCH_GAP * 1.5
713
+ : ROOT_BRANCH_GAP
714
+ : NODE_GAP;
715
+ const { nextLaneY, positionedBlocks } = lanes.reduce(
716
+ (acc, lane) => ({
717
+ nextLaneY: acc.nextLaneY + lane.height + laneGap,
718
+ positionedBlocks: [
719
+ ...acc.positionedBlocks,
720
+ ...lane.blocks.map((block) => shiftedBlock(block, acc.nextLaneY)),
721
+ ],
722
+ }),
723
+ { nextLaneY: headerHeight, positionedBlocks: [] },
724
+ );
725
+
726
+ const minChildX = positionedBlocks.length
727
+ ? Math.min(...positionedBlocks.map((block) => block.left))
728
+ : 0;
729
+ const maxChildX = positionedBlocks.length
730
+ ? Math.max(...positionedBlocks.map((block) => block.right))
731
+ : MIN_GROUP_WIDTH;
732
+ const paddingX = root ? 0 : GROUP_PADDING_X;
733
+ const bottomPadding = root ? 0 : GROUP_PADDING_BOTTOM;
734
+ const x = minChildX - paddingX;
735
+ const height = Math.max(
736
+ root ? 0 : MIN_GROUP_HEIGHT,
737
+ Math.max(0, nextLaneY - laneGap) + bottomPadding,
738
+ );
739
+ const width = Math.max(root ? 0 : MIN_GROUP_WIDTH, maxChildX - x + paddingX);
740
+ const geometries = mergeGeometryMaps(positionedBlocks);
741
+
742
+ if (!root) geometries.set(id, { x, y: 0, w: width, h: height });
743
+
744
+ return {
745
+ geometries,
746
+ height,
747
+ left: root ? minChildX : Math.min(minChildX, x),
748
+ right: root ? maxChildX : Math.max(maxChildX, x + width),
749
+ id: id || "@@lineage-layout-root",
750
+ maxRank: orderedBlocks.length
751
+ ? Math.max(...orderedBlocks.map((block) => block.maxRank))
752
+ : 0,
753
+ minRank: orderedBlocks.length
754
+ ? Math.min(...orderedBlocks.map((block) => block.minRank))
755
+ : 0,
756
+ score: median(orderedBlocks.map((block) => block.score)) || 0,
757
+ stableY: median(orderedBlocks.map(stableYFor)) || 0,
758
+ width,
759
+ };
760
+ };
761
+
762
+ const packedGeometry = (nodes, topologyPlan, previousLayout) => {
763
+ const { hierarchy, orderById, rankById } = topologyPlan;
764
+ const previousById = previousGeometryMap(previousLayout);
765
+ const ranks = [...new Set(rankById.values())].sort(
766
+ (rankA, rankB) => rankA - rankB,
767
+ );
768
+ const compactRank = new Map(ranks.map((rank, index) => [rank, index]));
769
+ const maxWidthByRank = new Map();
770
+
771
+ hierarchy.vertexIds.forEach((id) => {
772
+ const rank = compactRank.get(rankById.get(id)) || 0;
773
+ const width = nodeGeometry(hierarchy.nodeById.get(id)).w;
774
+
775
+ maxWidthByRank.set(rank, Math.max(maxWidthByRank.get(rank) || 0, width));
776
+ });
777
+
778
+ const xByRank = new Map();
779
+ [...maxWidthByRank.keys()]
780
+ .sort((rankA, rankB) => rankA - rankB)
781
+ .reduce((acc, rank) => {
782
+ xByRank.set(rank, acc);
783
+ return (
784
+ acc + (maxWidthByRank.get(rank) || DEFAULT_NODE_WIDTH) + COLUMN_GAP
785
+ );
786
+ }, 0);
787
+
788
+ const blockById = new Map();
789
+ const createBlock = (id) => {
790
+ if (blockById.has(id)) return blockById.get(id);
791
+
792
+ const node = hierarchy.nodeById.get(id);
793
+ const childIds = sortedIds(hierarchy.childIdsByParentId.get(id) || []);
794
+
795
+ if (!childIds.length) {
796
+ const geometry = nodeGeometry(node);
797
+ const rank = compactRank.get(rankById.get(id)) || 0;
798
+ const block = {
799
+ geometries: new Map([
800
+ [
801
+ id,
802
+ { x: xByRank.get(rank) || 0, y: 0, w: geometry.w, h: geometry.h },
803
+ ],
804
+ ]),
805
+ height: geometry.h,
806
+ left: xByRank.get(rank) || 0,
807
+ right: (xByRank.get(rank) || 0) + geometry.w,
808
+ id,
809
+ maxRank: rank,
810
+ minRank: rank,
811
+ score: orderById.get(id) || 0,
812
+ stableY: previousById.get(id)?.y ?? geometry.y,
813
+ width: geometry.w,
814
+ };
815
+
816
+ blockById.set(id, block);
817
+ return block;
818
+ }
819
+
820
+ const block = createContainerBlock({
821
+ childBlocks: childIds.map(createBlock),
822
+ id,
823
+ node,
824
+ previousById,
825
+ });
826
+
827
+ blockById.set(id, block);
828
+ return block;
829
+ };
830
+ const rootIds = sortedIds(
831
+ [...hierarchy.nodeById.keys()].filter(
832
+ (id) => !hierarchy.parentById.has(id),
833
+ ),
834
+ );
835
+ const rootBlock = createContainerBlock({
836
+ childBlocks: rootIds.map(createBlock),
837
+ previousById,
838
+ root: true,
839
+ });
840
+ const geometries = rootBlock.geometries;
841
+ const allGeometries = [...geometries.values()];
842
+ const minX = Math.min(...allGeometries.map(({ x }) => x), 0);
843
+ const minY = Math.min(...allGeometries.map(({ y }) => y), 0);
844
+
845
+ return translateGeometries(geometries, -minX, -minY);
846
+ };
847
+
848
+ const preserveAnchor = (geometries, previousLayout, anchorNodeId) => {
849
+ if (!anchorNodeId) return geometries;
850
+
851
+ const previousGeometry =
852
+ previousGeometryMap(previousLayout).get(anchorNodeId);
853
+ const nextGeometry = geometries.get(anchorNodeId);
854
+
855
+ if (!previousGeometry || !nextGeometry) return geometries;
856
+
857
+ return translateGeometries(
858
+ geometries,
859
+ previousGeometry.x - nextGeometry.x,
860
+ previousGeometry.y - nextGeometry.y,
861
+ );
862
+ };
863
+
864
+ const applyGeometries = (nodes, geometries) =>
865
+ nodes.map((node) => {
866
+ const geometry = geometries.get(node.id) || nodeGeometry(node);
867
+ const parentGeometry = node.parentId ? geometries.get(node.parentId) : null;
868
+ const position = parentGeometry
869
+ ? {
870
+ x: geometry.x - parentGeometry.x,
871
+ y: geometry.y - parentGeometry.y,
872
+ }
873
+ : { x: geometry.x, y: geometry.y };
874
+
875
+ return {
876
+ ...node,
877
+ height: geometry.h,
878
+ position,
879
+ width: geometry.w,
880
+ data: {
881
+ ...node.data,
882
+ absoluteGeometry: geometry,
883
+ },
884
+ style: {
885
+ ...node.style,
886
+ height: geometry.h,
887
+ width: geometry.w,
888
+ },
889
+ };
890
+ });
891
+
892
+ const compoundGeometry = (nodes, edges) => {
893
+ const hierarchy = buildHierarchy(nodes);
894
+ const { nodeById, parentById, childIdsByParentId } = hierarchy;
895
+ const ancestorsById = new Map();
896
+ const projectedByParent = new Map();
897
+ const ancestors = (id) => {
898
+ if (ancestorsById.has(id)) return ancestorsById.get(id);
899
+ const build = (current, visited, result) => {
900
+ if (!current || visited.has(current)) return result;
901
+ const nextVisited = new Set([...visited, current]);
902
+ const nextResult = new Map([
903
+ ...result,
904
+ [parentById.get(current), current],
905
+ ]);
906
+ return build(parentById.get(current), nextVisited, nextResult);
907
+ };
908
+ const result = build(id, new Set(), new Map());
909
+ ancestorsById.set(id, result);
910
+ return result;
911
+ };
912
+ edges.forEach((edge) => {
913
+ if (
914
+ edge.data?.containment ||
915
+ !nodeById.has(edge.source) ||
916
+ !nodeById.has(edge.target)
917
+ )
918
+ return;
919
+ const targetAncestors = ancestors(edge.target);
920
+ ancestors(edge.source).forEach((source, parent) => {
921
+ const target = targetAncestors.get(parent);
922
+ if (!target || source === target) return;
923
+ const relations = projectedByParent.get(parent) || [];
924
+ relations.push({ ...edge, source, target });
925
+ projectedByParent.set(parent, relations);
926
+ });
927
+ });
928
+ const rootIds = nodes
929
+ .filter(({ id }) => !parentById.has(id))
930
+ .map(({ id }) => id);
931
+ const blockFor = (id) => {
932
+ const root = id === undefined;
933
+ const node = nodeById.get(id);
934
+ const children = root ? rootIds : [...(childIdsByParentId.get(id) || [])];
935
+ if (!children.length && !root) {
936
+ const { w, h } = nodeGeometry(node);
937
+ return {
938
+ width: w,
939
+ height: h,
940
+ geometries: new Map([[id, { x: 0, y: 0, w, h }]]),
941
+ };
942
+ }
943
+ const fields =
944
+ !root &&
945
+ children.every(
946
+ (child) => nodeById.get(child).data?.variant === "lineage-field",
947
+ );
948
+ const childNodes = children.map((child) => nodeById.get(child));
949
+ const plan = cachedTopologyPlan(
950
+ childNodes,
951
+ fields ? [] : projectedByParent.get(id) || [],
952
+ "compound",
953
+ );
954
+ const ordered = [...children].sort((a, b) => {
955
+ if (fields)
956
+ return (
957
+ nodeLabel(nodeById.get(a)).localeCompare(
958
+ nodeLabel(nodeById.get(b)),
959
+ ) || a.localeCompare(b)
960
+ );
961
+ return (
962
+ (plan.rankById.get(a) || 0) - (plan.rankById.get(b) || 0) ||
963
+ (plan.orderById.get(a) || 0) - (plan.orderById.get(b) || 0) ||
964
+ a.localeCompare(b)
965
+ );
966
+ });
967
+ const columns = new Map();
968
+ ordered.forEach((child) => {
969
+ const rank = fields ? 0 : plan.rankById.get(child) || 0;
970
+ const column = columns.get(rank) || [];
971
+ column.push(blockFor(child));
972
+ columns.set(rank, column);
973
+ });
974
+ const padding = root ? 0 : GROUP_PADDING_X;
975
+ const header = root ? 0 : GROUP_HEADER_HEIGHT;
976
+ const {
977
+ geometries,
978
+ x: finalX,
979
+ height: finalHeight,
980
+ } = [...columns.keys()]
981
+ .sort((a, b) => a - b)
982
+ .reduce(
983
+ (outerAcc, rank) => {
984
+ const {
985
+ rankGeos,
986
+ y: endY,
987
+ maxWidth,
988
+ } = columns.get(rank).reduce(
989
+ (innerAcc, block) => {
990
+ translateGeometries(
991
+ block.geometries,
992
+ outerAcc.x,
993
+ innerAcc.y,
994
+ ).forEach((geometry, child) =>
995
+ innerAcc.rankGeos.set(child, geometry),
996
+ );
997
+ return {
998
+ rankGeos: innerAcc.rankGeos,
999
+ y: innerAcc.y + block.height + (fields ? FIELD_GAP : NODE_GAP),
1000
+ maxWidth: Math.max(innerAcc.maxWidth, block.width),
1001
+ };
1002
+ },
1003
+ { rankGeos: new Map(), y: header, maxWidth: 0 },
1004
+ );
1005
+ rankGeos.forEach((geometry, child) =>
1006
+ outerAcc.geometries.set(child, geometry),
1007
+ );
1008
+ return {
1009
+ geometries: outerAcc.geometries,
1010
+ x: outerAcc.x + maxWidth + COLUMN_GAP,
1011
+ height: Math.max(outerAcc.height, endY),
1012
+ };
1013
+ },
1014
+ { geometries: new Map(), x: padding, height: header },
1015
+ );
1016
+ const width = Math.max(MIN_GROUP_WIDTH, finalX - COLUMN_GAP + padding);
1017
+ const totalHeight = finalHeight + (root ? 0 : GROUP_PADDING_BOTTOM);
1018
+ if (!root) geometries.set(id, { x: 0, y: 0, w: width, h: totalHeight });
1019
+ return { width, height: totalHeight, geometries };
1020
+ };
1021
+ return blockFor(undefined).geometries;
1022
+ };
1023
+
1024
+ export const layoutVisibleLineageGraph = ({
1025
+ anchorNodeId,
1026
+ edges = [],
1027
+ graphType = "impact",
1028
+ nodes = [],
1029
+ previousLayout = [],
1030
+ }) => {
1031
+ if (!nodes.length) return [];
1032
+
1033
+ const packed = nodes.some(
1034
+ (node) =>
1035
+ node.data?.topology ||
1036
+ (node.parentId && node.data?.variant === "lineage-field"),
1037
+ )
1038
+ ? compoundGeometry(nodes, edges)
1039
+ : packedGeometry(
1040
+ nodes,
1041
+ cachedTopologyPlan(nodes, edges, graphType),
1042
+ previousLayout,
1043
+ );
1044
+ const anchored = preserveAnchor(packed, previousLayout, anchorNodeId);
1045
+
1046
+ return applyGeometries(nodes, anchored);
1047
+ };