@truedat/dd 8.14.0 → 8.14.1

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.
@@ -1002,6 +1002,21 @@ export const lineageNodeContentWidth = (resources = []) => {
1002
1002
  );
1003
1003
  };
1004
1004
 
1005
+ const lineageSiblingWidths = (nodes, parentFor) => {
1006
+ const widths = new Map();
1007
+ nodes.forEach((node) => {
1008
+ const parent = parentFor(node);
1009
+ widths.set(
1010
+ parent,
1011
+ Math.max(widths.get(parent) || 0, lineageNodeContentWidth([node])),
1012
+ );
1013
+ });
1014
+ return (node) => widths.get(parentFor(node)) || LINEAGE_NODE_WIDTH_MIN;
1015
+ };
1016
+
1017
+ const resolvedLineageNodeWidth = (nodeWidth, node) =>
1018
+ typeof nodeWidth === "function" ? nodeWidth(node) : nodeWidth;
1019
+
1005
1020
  const lineagePath = (node, system, schema) => {
1006
1021
  const path = node.path || node.ancestry || node.parents || node.parent_path;
1007
1022
  const pathItems = Array.isArray(path)
@@ -1090,7 +1105,9 @@ const lineageNode =
1090
1105
  const systemType = lineageSystemType(node);
1091
1106
  const entity = lineageEntityPresentation(node, resolvedKind);
1092
1107
  const path = redesigned ? lineagePath(node, system, schema) : [];
1093
- const width = redesigned ? nodeWidth || NODE_WIDTH_REDESIGNED : NODE_WIDTH;
1108
+ const width = redesigned
1109
+ ? resolvedLineageNodeWidth(nodeWidth, node) || NODE_WIDTH_REDESIGNED
1110
+ : NODE_WIDTH;
1094
1111
 
1095
1112
  return {
1096
1113
  id,
@@ -1334,8 +1351,16 @@ const lineagePositionedResourceNode =
1334
1351
  export const lineageTopologyNodes = (
1335
1352
  topology,
1336
1353
  nodeDesign = "redesigned",
1337
- nodeWidth = NODE_WIDTH_REDESIGNED,
1354
+ nodeWidth,
1338
1355
  ) => {
1356
+ const widthFor =
1357
+ nodeWidth ??
1358
+ (nodeDesign === "redesigned"
1359
+ ? lineageSiblingWidths(
1360
+ topology.nodes.filter((node) => !node.container),
1361
+ (node) => node.parentId,
1362
+ )
1363
+ : NODE_WIDTH_REDESIGNED);
1339
1364
  const parentByNode = new Map();
1340
1365
  const childCountsByCanonicalId = new Map();
1341
1366
  const nodeById = new Map(topology.nodes.map((node) => [node.id, node]));
@@ -1345,7 +1370,7 @@ export const lineageTopologyNodes = (
1345
1370
  geometryById.set(node.id, {
1346
1371
  x: 0,
1347
1372
  y: 0,
1348
- w: nodeWidth,
1373
+ w: resolvedLineageNodeWidth(widthFor, node),
1349
1374
  h: COLLAPSED_GROUP_HEIGHT,
1350
1375
  });
1351
1376
  if (!node.parentId) return;
@@ -1376,7 +1401,7 @@ export const lineageTopologyNodes = (
1376
1401
  resolveSystemType,
1377
1402
  new Set(),
1378
1403
  undefined,
1379
- nodeWidth,
1404
+ widthFor,
1380
1405
  );
1381
1406
  return [...topology.nodes]
1382
1407
  .sort((a, b) => a.z - b.z || a.id.localeCompare(b.id))
@@ -2487,13 +2512,18 @@ export const lineagePositionedLayoutNodes = ({
2487
2512
  preparedHierarchy = preparePositionedLineageHierarchy(groups, resources),
2488
2513
  }) => {
2489
2514
  const { containerGroups, leafGroups, parentByNode } = preparedHierarchy;
2515
+ const widthFor =
2516
+ nodeWidth ??
2517
+ lineageSiblingWidths([...resources, ...leafGroups], (node) =>
2518
+ parentByNode.get(node),
2519
+ );
2490
2520
  const leafGroupIds = new Set(leafGroups.map(nodeId));
2491
2521
  const positionedResourceGeometry = (node) =>
2492
2522
  positionedNodeGeometryForDesign(
2493
2523
  node,
2494
2524
  nodeDesign,
2495
2525
  leafGroupIds.has(nodeId(node)) ? "group" : "resource",
2496
- nodeWidth,
2526
+ widthFor,
2497
2527
  );
2498
2528
  const geometryById = buildPositionedGeometryMap(
2499
2529
  containerGroups,
@@ -2529,7 +2559,7 @@ export const lineagePositionedLayoutNodes = ({
2529
2559
  resolveSystemType,
2530
2560
  expandedFieldNodeIds,
2531
2561
  onToggleFields,
2532
- nodeWidth,
2562
+ widthFor,
2533
2563
  ),
2534
2564
  ),
2535
2565
  ...resources.map(
@@ -2543,7 +2573,7 @@ export const lineagePositionedLayoutNodes = ({
2543
2573
  resolveSystemType,
2544
2574
  expandedFieldNodeIds,
2545
2575
  onToggleFields,
2546
- nodeWidth,
2576
+ widthFor,
2547
2577
  ),
2548
2578
  ),
2549
2579
  ]),
@@ -2717,7 +2747,7 @@ export const bundleLineageEdges = (edges = []) => {
2717
2747
  const endpointKey = (nodeId, handleId) => `${nodeId}::${handleId || ""}`;
2718
2748
 
2719
2749
  edges.forEach((edge) => {
2720
- if (edge.data?.containment) return;
2750
+ if (edge.data?.containment || edge.data?.internalOwnerId) return;
2721
2751
 
2722
2752
  const sourceKey = endpointKey(edge.source, edge.sourceHandle);
2723
2753
  const targetKey = endpointKey(edge.target, edge.targetHandle);
@@ -2727,7 +2757,7 @@ export const bundleLineageEdges = (edges = []) => {
2727
2757
  });
2728
2758
 
2729
2759
  return edges.map((edge) => {
2730
- if (edge.data?.containment) return edge;
2760
+ if (edge.data?.containment || edge.data?.internalOwnerId) return edge;
2731
2761
 
2732
2762
  const sourceCount =
2733
2763
  sourceCounts.get(endpointKey(edge.source, edge.sourceHandle)) || 0;
@@ -2754,8 +2784,11 @@ export const redesignedLineageEdges = (edges = []) =>
2754
2784
  data: {
2755
2785
  ...edge.data,
2756
2786
  edgeCurvature: 0.32,
2757
- edgePath: "bezier",
2758
- routeGuide: edge.data?.topology ? edge.data.routeGuide : undefined,
2787
+ edgePath: edge.data?.internalOwnerId ? "guided-bezier" : "bezier",
2788
+ routeGuide:
2789
+ edge.data?.internalOwnerId || edge.data?.topology
2790
+ ? edge.data.routeGuide
2791
+ : undefined,
2759
2792
  },
2760
2793
  })),
2761
2794
  );
@@ -3114,6 +3147,18 @@ export const planLineageEdgeRoutes = (edges = [], nodes = []) => {
3114
3147
  }));
3115
3148
  };
3116
3149
 
3150
+ export const planExternalLineageEdgeRoutes = (edges = [], nodes = []) => {
3151
+ const plannedById = new Map(
3152
+ planLineageEdgeRoutes(
3153
+ edges.filter((edge) => !edge.data?.internalOwnerId),
3154
+ nodes,
3155
+ ).map((edge) => [edge.id, edge]),
3156
+ );
3157
+ return edges.map((edge) =>
3158
+ edge.data?.internalOwnerId ? edge : plannedById.get(edge.id),
3159
+ );
3160
+ };
3161
+
3117
3162
  export const focusedLineageGraph = (selectedNodeId, edges = []) => {
3118
3163
  const focusedNodeIds = new Set();
3119
3164
  const focusedEdgeIds = new Set();
@@ -0,0 +1,389 @@
1
+ const LINEAGE_PREVIEW_EDGE_Z_INDEX = 150;
2
+
3
+ const HEADER = 58;
4
+ const PADDING = 16;
5
+ const COLUMN_GAP = 152;
6
+ const PORT = 24;
7
+ const PITCH = 12;
8
+
9
+ const assignSlots = (items) => {
10
+ const ends = [];
11
+ const slots = new Map();
12
+ [...items]
13
+ .sort(
14
+ (a, b) => a.start - b.start || a.end - b.end || a.id.localeCompare(b.id),
15
+ )
16
+ .forEach(({ id, start, end }) => {
17
+ const available = ends.findIndex((last) => last < start);
18
+ const slot = available < 0 ? ends.length : available;
19
+ ends.splice(slot, 1, end);
20
+ slots.set(id, slot);
21
+ });
22
+ return { slots, count: ends.length };
23
+ };
24
+
25
+ export const internalRoleColumns = (children, edges) => {
26
+ const incoming = new Set(edges.map(({ target }) => target));
27
+ const outgoing = new Set(edges.map(({ source }) => source));
28
+ const groups = { entry: [], intermediate: [], exit: [], independent: [] };
29
+ children.forEach((node) => {
30
+ const role = incoming.has(node.id)
31
+ ? outgoing.has(node.id)
32
+ ? "intermediate"
33
+ : "exit"
34
+ : outgoing.has(node.id)
35
+ ? "entry"
36
+ : "independent";
37
+ groups[role].push({ ...node, role });
38
+ });
39
+ const columns = ["entry", "intermediate", "exit"]
40
+ .filter((key) => groups[key].length)
41
+ .map((key) => ({ key, nodes: groups[key] }));
42
+ if (groups.independent.length) {
43
+ if (!columns.length) columns.push({ key: "independent", nodes: [] });
44
+ columns[0].nodes.push(...groups.independent);
45
+ }
46
+ return columns;
47
+ };
48
+
49
+ export const reserveInternalGroupChannels = ({
50
+ columns,
51
+ edges,
52
+ nodeGap = 44,
53
+ columnGap = COLUMN_GAP,
54
+ columnLabelHeight = 0,
55
+ compact = false,
56
+ }) => {
57
+ const lanePitch = compact ? 4 : PITCH;
58
+ const laneOffset = compact ? 32 : 48;
59
+ const lanePadding = compact ? 48 : 72;
60
+ const labelTop = HEADER + (columnLabelHeight ? 8 : 0);
61
+ const indexed = new Map();
62
+ const widths = columns.map(({ nodes }) =>
63
+ Math.max(...nodes.map(({ width }) => width)),
64
+ );
65
+ const columnEnds = columns.map(({ nodes, sectioned }, column) => {
66
+ const end = nodes.reduce(
67
+ (y, node, index) => {
68
+ const sectionStart =
69
+ index > 0 &&
70
+ node.role !== nodes[index - 1].role &&
71
+ (sectioned || node.role === "independent");
72
+ const top = y + (sectionStart ? 32 : 0);
73
+ indexed.set(node.id, { ...node, column, y: top, sectionStart });
74
+ return top + node.height + nodeGap;
75
+ },
76
+ labelTop + columnLabelHeight + (sectioned ? 8 : 0),
77
+ );
78
+ return end - nodeGap;
79
+ });
80
+ const bodyBottom = Math.max(...columnEnds);
81
+ const validEdges = edges.filter(
82
+ (e) =>
83
+ !e.data?.containment &&
84
+ e.source !== e.target &&
85
+ indexed.has(e.source) &&
86
+ indexed.has(e.target),
87
+ );
88
+ const routes = [...validEdges]
89
+ .sort(
90
+ (a, b) =>
91
+ a.source.localeCompare(b.source) ||
92
+ a.target.localeCompare(b.target) ||
93
+ String(a.id || "").localeCompare(String(b.id || "")),
94
+ )
95
+ .map((edge, index) => {
96
+ const source = indexed.get(edge.source),
97
+ target = indexed.get(edge.target);
98
+ const sourceY = source.y + source.height / 2,
99
+ targetY = target.y + target.height / 2;
100
+ const sameColumn = source.column === target.column;
101
+ const corridor = sameColumn || target.column === source.column + 1;
102
+ const topDistance =
103
+ sourceY + targetY - 2 * (labelTop + columnLabelHeight);
104
+ const bottomDistance = 2 * bodyBottom - sourceY - targetY;
105
+ return {
106
+ edge,
107
+ id: edge.id || `${edge.source}->${edge.target}:${index}`,
108
+ source,
109
+ target,
110
+ sourceY,
111
+ targetY,
112
+ sameColumn,
113
+ corridor,
114
+ side: topDistance <= bottomDistance ? "top" : "bottom",
115
+ };
116
+ });
117
+ const boundaryItems = new Map();
118
+ const buses = { top: [], bottom: [] };
119
+ const addBoundary = (boundary, id, a, b) => {
120
+ const items = boundaryItems.get(boundary) || [];
121
+ items.push({ id, start: Math.min(a, b), end: Math.max(a, b) });
122
+ boundaryItems.set(boundary, items);
123
+ };
124
+ routes.forEach((r) => {
125
+ if (r.corridor) addBoundary(r.source.column, r.id, r.sourceY, r.targetY);
126
+ else {
127
+ const limit = r.side === "top" ? -Infinity : Infinity;
128
+ addBoundary(r.source.column, `${r.id}:source`, r.sourceY, limit);
129
+ addBoundary(r.target.column - 1, `${r.id}:target`, r.targetY, limit);
130
+ buses[r.side].push({
131
+ id: r.id,
132
+ start: Math.min(r.source.column, r.target.column - 1),
133
+ end: Math.max(r.source.column, r.target.column - 1),
134
+ });
135
+ }
136
+ });
137
+ const boundarySlots = new Map(
138
+ [...boundaryItems].map(([key, items]) => [key, assignSlots(items)]),
139
+ );
140
+ const busSlots = {
141
+ top: assignSlots(buses.top),
142
+ bottom: assignSlots(buses.bottom),
143
+ };
144
+ const topReserve = busSlots.top.count ? 36 + busSlots.top.count * PITCH : 0;
145
+ const bottomReserve = busSlots.bottom.count
146
+ ? 36 + busSlots.bottom.count * PITCH
147
+ : 12;
148
+ const boundaryWidth = (index) =>
149
+ Math.max(
150
+ columnGap,
151
+ lanePadding + (boundarySlots.get(index)?.count || 0) * lanePitch,
152
+ );
153
+ const outsideWidth = (index) =>
154
+ boundarySlots.has(index)
155
+ ? lanePadding + boundarySlots.get(index).count * lanePitch
156
+ : PADDING;
157
+ const positions = [];
158
+ columns.forEach((_column, index) => {
159
+ positions.push(
160
+ index
161
+ ? positions[index - 1] + widths[index - 1] + boundaryWidth(index - 1)
162
+ : outsideWidth(-1),
163
+ );
164
+ });
165
+ const geometries = new Map(
166
+ [...indexed].map(([id, n]) => [
167
+ id,
168
+ { x: positions[n.column], y: n.y + topReserve, w: n.width, h: n.height },
169
+ ]),
170
+ );
171
+ const boundaryX = (index, id) => {
172
+ const start = index < 0 ? 0 : positions[index] + widths[index];
173
+ return (
174
+ start + laneOffset + boundarySlots.get(index).slots.get(id) * lanePitch
175
+ );
176
+ };
177
+ const guidesByEdgeId = new Map();
178
+ routes.forEach((r) => {
179
+ const source = geometries.get(r.edge.source),
180
+ target = geometries.get(r.edge.target);
181
+ const sourceY = source.y + source.h / 2,
182
+ targetY = target.y + target.h / 2;
183
+ const sourceStubX =
184
+ positions[r.source.column] + widths[r.source.column] + PORT;
185
+ const targetStubX = r.sameColumn
186
+ ? positions[r.target.column] + widths[r.target.column] + PORT
187
+ : target.x - PORT;
188
+ const guide = {
189
+ sourceStubX,
190
+ targetStubX,
191
+ sourcePortPosition: "right",
192
+ targetPortPosition: r.sameColumn ? "right" : "left",
193
+ cornerRadius: 24,
194
+ };
195
+ if (r.corridor) {
196
+ const laneX = boundaryX(r.source.column, r.id);
197
+ guidesByEdgeId.set(r.id, {
198
+ ...guide,
199
+ kind: "corridor",
200
+ laneX,
201
+ labelPoint: {
202
+ x: (sourceStubX + 6 * laneX + targetStubX) / 8,
203
+ y: (sourceY + targetY) / 2,
204
+ },
205
+ });
206
+ } else {
207
+ const laneY =
208
+ r.side === "top"
209
+ ? labelTop +
210
+ columnLabelHeight +
211
+ 12 +
212
+ busSlots.top.slots.get(r.id) * PITCH
213
+ : bodyBottom +
214
+ topReserve +
215
+ 24 +
216
+ busSlots.bottom.slots.get(r.id) * PITCH;
217
+ const sourceLaneX = boundaryX(r.source.column, `${r.id}:source`);
218
+ const targetLaneX = boundaryX(r.target.column - 1, `${r.id}:target`);
219
+ guidesByEdgeId.set(r.id, {
220
+ ...guide,
221
+ kind: "bypass",
222
+ sourceLaneX,
223
+ targetLaneX,
224
+ laneY,
225
+ labelPoint: { x: (sourceLaneX + targetLaneX) / 2, y: laneY },
226
+ });
227
+ }
228
+ });
229
+ const labels = columnLabelHeight
230
+ ? columns.map((column, index) => ({
231
+ key: column.key,
232
+ roles: column.roles,
233
+ x: positions[index],
234
+ y: labelTop,
235
+ width: widths[index],
236
+ count: column.roles
237
+ ? column.nodes.length
238
+ : column.nodes.filter((n) => n.role === column.key).length,
239
+ }))
240
+ : [];
241
+ indexed.forEach((n) => {
242
+ if (n.sectionStart)
243
+ labels.push({
244
+ key: n.role,
245
+ x: positions[n.column],
246
+ y: n.y + topReserve - 32,
247
+ width: widths[n.column],
248
+ count: columns[n.column].nodes.filter((item) => item.role === n.role)
249
+ .length,
250
+ });
251
+ });
252
+ return {
253
+ width:
254
+ positions[positions.length - 1] +
255
+ widths[widths.length - 1] +
256
+ outsideWidth(columns.length - 1),
257
+ height: bodyBottom + topReserve + bottomReserve,
258
+ geometries,
259
+ guidesByEdgeId: Object.fromEntries(guidesByEdgeId),
260
+ columns: labels,
261
+ };
262
+ };
263
+
264
+ export const layoutInternalGroup = ({
265
+ children,
266
+ rankById,
267
+ edges,
268
+ dependencyLayout,
269
+ }) => {
270
+ const rankedColumns = new Map();
271
+ children.forEach((child) => {
272
+ const rank = rankById.get(child.id) || 0;
273
+ const column = rankedColumns.get(rank) || [];
274
+ column.push(child);
275
+ rankedColumns.set(rank, column);
276
+ });
277
+ const compact = dependencyLayout === "compact";
278
+ const roleColumns = internalRoleColumns(children, edges);
279
+ const rolesById = new Map(
280
+ roleColumns.flatMap(({ nodes }) => nodes.map(({ id, role }) => [id, role])),
281
+ );
282
+ const columns = compact
283
+ ? [
284
+ {
285
+ key: roleColumns[0].key,
286
+ sectioned: true,
287
+ nodes: roleColumns.flatMap(({ nodes }) => nodes),
288
+ },
289
+ ]
290
+ : [...rankedColumns].map(([rank, nodes]) => ({
291
+ rank,
292
+ key: `rank-${rank}`,
293
+ roles: [...new Set(nodes.map(({ id }) => rolesById.get(id)))],
294
+ nodes,
295
+ }));
296
+ return reserveInternalGroupChannels({
297
+ columns,
298
+ edges,
299
+ nodeGap: compact ? 12 : 8,
300
+ columnGap: compact ? 136 : 120,
301
+ columnLabelHeight: 24,
302
+ compact,
303
+ });
304
+ };
305
+
306
+ export const applyInternalNodeLayers = (nodes = []) => {
307
+ const byId = new Map(nodes.map((n) => [n.id, n]));
308
+ const depths = new Map();
309
+ const depth = (id, visited = new Set()) => {
310
+ if (depths.has(id)) return depths.get(id);
311
+ const node = byId.get(id);
312
+ if (!node?.parentId || !byId.has(node.parentId) || visited.has(id))
313
+ return 0;
314
+ const value = 1 + depth(node.parentId, new Set([...visited, id]));
315
+ depths.set(id, value);
316
+ return value;
317
+ };
318
+ return nodes.map((node) =>
319
+ node.type === "lineageGroup"
320
+ ? { ...node, zIndex: 10 + depth(node.id) }
321
+ : node.zIndex >= 200 ||
322
+ node.data?.internalEdgeGuides ||
323
+ byId.get(node.parentId)?.data?.internalEdgeGuides
324
+ ? { ...node, zIndex: 200 + 4 * depth(node.id) }
325
+ : node,
326
+ );
327
+ };
328
+
329
+ export const applyInternalEdgeRoutes = (edges = [], nodes = []) => {
330
+ const guides = new Map();
331
+ nodes.forEach((node) =>
332
+ Object.entries(node.data?.internalEdgeGuides || {}).forEach(([id, guide]) =>
333
+ guides.set(id, { node, guide }),
334
+ ),
335
+ );
336
+ return edges.map((edge) => {
337
+ const owned = guides.get(edge.id);
338
+ if (!owned) {
339
+ if (!edge.data?.internalOwnerId) return edge;
340
+ return {
341
+ ...edge,
342
+ zIndex: LINEAGE_PREVIEW_EDGE_Z_INDEX,
343
+ data: {
344
+ ...edge.data,
345
+ selectionBaseZIndex: 100,
346
+ internalOwnerId: undefined,
347
+ routeGuide: undefined,
348
+ edgePath: "bezier",
349
+ lockTargetPort: false,
350
+ },
351
+ };
352
+ }
353
+ const { node, guide } = owned;
354
+ const { x, y } = node.data.absoluteGeometry;
355
+ const absolute = {
356
+ ...guide,
357
+ ...Object.fromEntries(
358
+ ["laneX", "sourceLaneX", "targetLaneX", "sourceStubX", "targetStubX"]
359
+ .filter((key) => Number.isFinite(guide[key]))
360
+ .map((key) => [key, guide[key] + x]),
361
+ ),
362
+ ...(Number.isFinite(guide.laneY) ? { laneY: guide.laneY + y } : {}),
363
+ labelPoint: { x: guide.labelPoint.x + x, y: guide.labelPoint.y + y },
364
+ };
365
+ return {
366
+ ...edge,
367
+ zIndex: LINEAGE_PREVIEW_EDGE_Z_INDEX,
368
+ sourceHandle: "end-node",
369
+ targetHandle:
370
+ guide.targetPortPosition === "right"
371
+ ? "target-right-node"
372
+ : "start-node",
373
+ data: {
374
+ ...edge.data,
375
+ selectionBaseZIndex: node.zIndex + 2,
376
+ internalOwnerId: node.id,
377
+ edgePath: "guided-bezier",
378
+ routeGuide: absolute,
379
+ forceHorizontalPorts: true,
380
+ sourcePortPosition: guide.sourcePortPosition,
381
+ targetPortPosition: guide.targetPortPosition,
382
+ lockSourcePort: true,
383
+ lockTargetPort: true,
384
+ bundleSource: false,
385
+ bundleTarget: false,
386
+ },
387
+ };
388
+ });
389
+ };
@@ -1,7 +1,9 @@
1
+ import { layoutInternalGroup } from "./lineageInternalRoutes";
2
+
1
3
  const DEFAULT_NODE_WIDTH = 288;
2
4
  const DEFAULT_NODE_HEIGHT = 82;
3
- const COLUMN_GAP = 152;
4
- const NODE_GAP = 44;
5
+ const COLUMN_GAP = 160;
6
+ const NODE_GAP = 48;
5
7
  const FIELD_GAP = 4;
6
8
  const ROOT_BRANCH_GAP = 40;
7
9
  const GROUP_PADDING_X = 16;
@@ -861,7 +863,7 @@ const preserveAnchor = (geometries, previousLayout, anchorNodeId) => {
861
863
  );
862
864
  };
863
865
 
864
- const applyGeometries = (nodes, geometries) =>
866
+ const applyGeometries = (nodes, geometries, internalLayouts) =>
865
867
  nodes.map((node) => {
866
868
  const geometry = geometries.get(node.id) || nodeGeometry(node);
867
869
  const parentGeometry = node.parentId ? geometries.get(node.parentId) : null;
@@ -880,6 +882,8 @@ const applyGeometries = (nodes, geometries) =>
880
882
  data: {
881
883
  ...node.data,
882
884
  absoluteGeometry: geometry,
885
+ internalEdgeGuides: internalLayouts.get(node.id)?.guidesByEdgeId,
886
+ internalColumns: internalLayouts.get(node.id)?.columns,
883
887
  },
884
888
  style: {
885
889
  ...node.style,
@@ -889,7 +893,7 @@ const applyGeometries = (nodes, geometries) =>
889
893
  };
890
894
  });
891
895
 
892
- const compoundGeometry = (nodes, edges) => {
896
+ const compoundGeometry = (nodes, edges, dependencyLayout, internalLayouts) => {
893
897
  const hierarchy = buildHierarchy(nodes);
894
898
  const { nodeById, parentById, childIdsByParentId } = hierarchy;
895
899
  const ancestorsById = new Map();
@@ -946,13 +950,10 @@ const compoundGeometry = (nodes, edges) => {
946
950
  (child) => nodeById.get(child).data?.variant === "lineage-field",
947
951
  );
948
952
  const childNodes = children.map((child) => nodeById.get(child));
949
- const plan = cachedTopologyPlan(
950
- childNodes,
951
- fields ? [] : projectedByParent.get(id) || [],
952
- "compound",
953
- );
953
+ const relations = projectedByParent.get(id) || [];
954
+ const plan = cachedTopologyPlan(childNodes, relations, "compound");
954
955
  const ordered = [...children].sort((a, b) => {
955
- if (fields)
956
+ if (fields && !relations.length)
956
957
  return (
957
958
  nodeLabel(nodeById.get(a)).localeCompare(
958
959
  nodeLabel(nodeById.get(b)),
@@ -964,6 +965,23 @@ const compoundGeometry = (nodes, edges) => {
964
965
  a.localeCompare(b)
965
966
  );
966
967
  });
968
+ if (fields && relations.length) {
969
+ const block = layoutInternalGroup({
970
+ children: ordered.map((child) => {
971
+ const geometry = nodeGeometry(nodeById.get(child));
972
+ return { id: child, width: geometry.w, height: geometry.h };
973
+ }),
974
+ rankById: plan.rankById,
975
+ edges: relations,
976
+ dependencyLayout,
977
+ });
978
+ internalLayouts.set(id, {
979
+ guidesByEdgeId: block.guidesByEdgeId,
980
+ columns: block.columns,
981
+ });
982
+ block.geometries.set(id, { x: 0, y: 0, w: block.width, h: block.height });
983
+ return block;
984
+ }
967
985
  const columns = new Map();
968
986
  ordered.forEach((child) => {
969
987
  const rank = fields ? 0 : plan.rankById.get(child) || 0;
@@ -1030,15 +1048,17 @@ export const layoutVisibleLineageGraph = ({
1030
1048
  graphType = "impact",
1031
1049
  nodes = [],
1032
1050
  previousLayout = [],
1051
+ dependencyLayout = "compact",
1033
1052
  }) => {
1034
1053
  if (!nodes.length) return [];
1054
+ const internalLayouts = new Map();
1035
1055
 
1036
1056
  const packed = nodes.some(
1037
1057
  (node) =>
1038
1058
  node.data?.topology ||
1039
1059
  (node.parentId && node.data?.variant === "lineage-field"),
1040
1060
  )
1041
- ? compoundGeometry(nodes, edges)
1061
+ ? compoundGeometry(nodes, edges, dependencyLayout, internalLayouts)
1042
1062
  : packedGeometry(
1043
1063
  nodes,
1044
1064
  cachedTopologyPlan(nodes, edges, graphType),
@@ -1046,5 +1066,5 @@ export const layoutVisibleLineageGraph = ({
1046
1066
  );
1047
1067
  const anchored = preserveAnchor(packed, previousLayout, anchorNodeId);
1048
1068
 
1049
- return applyGeometries(nodes, anchored);
1069
+ return applyGeometries(nodes, anchored, internalLayouts);
1050
1070
  };
@@ -0,0 +1,21 @@
1
+ const KEY = "td.lineage.dependencyLayout.v1";
2
+ export const normalizeDependencyLayout = (value) =>
3
+ value === "expanded" ? "expanded" : "compact";
4
+ export const readDependencyLayout = () => {
5
+ try {
6
+ return normalizeDependencyLayout(globalThis.localStorage?.getItem(KEY));
7
+ } catch {
8
+ return "compact";
9
+ }
10
+ };
11
+ export const writeDependencyLayout = (value) => {
12
+ try {
13
+ const storage = globalThis.localStorage;
14
+ if (!storage) return false;
15
+
16
+ storage.setItem(KEY, normalizeDependencyLayout(value));
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ };