@principal-ai/principal-view-react 0.14.4 → 0.14.6

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 (41) hide show
  1. package/dist/components/GraphRenderer.d.ts +30 -7
  2. package/dist/components/GraphRenderer.d.ts.map +1 -1
  3. package/dist/components/GraphRenderer.js +29 -16
  4. package/dist/components/GraphRenderer.js.map +1 -1
  5. package/dist/components/NodeTooltip.js +1 -1
  6. package/dist/components/NodeTooltip.js.map +1 -1
  7. package/dist/contexts/TooltipPortalContext.d.ts +8 -0
  8. package/dist/contexts/TooltipPortalContext.d.ts.map +1 -0
  9. package/dist/contexts/TooltipPortalContext.js +8 -0
  10. package/dist/contexts/TooltipPortalContext.js.map +1 -0
  11. package/dist/edges/CustomEdge.d.ts +5 -0
  12. package/dist/edges/CustomEdge.d.ts.map +1 -1
  13. package/dist/edges/CustomEdge.js +7 -3
  14. package/dist/edges/CustomEdge.js.map +1 -1
  15. package/dist/hooks/useElkLayout.d.ts +66 -0
  16. package/dist/hooks/useElkLayout.d.ts.map +1 -0
  17. package/dist/hooks/useElkLayout.js +136 -0
  18. package/dist/hooks/useElkLayout.js.map +1 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/nodes/otel/OtelSpanConventionNode.js +3 -3
  24. package/dist/nodes/otel/OtelSpanConventionNode.js.map +1 -1
  25. package/dist/utils/elkLayout.d.ts +92 -0
  26. package/dist/utils/elkLayout.d.ts.map +1 -0
  27. package/dist/utils/elkLayout.js +281 -0
  28. package/dist/utils/elkLayout.js.map +1 -0
  29. package/package.json +4 -3
  30. package/src/components/GraphRenderer.tsx +70 -13
  31. package/src/components/NodeTooltip.tsx +1 -1
  32. package/src/contexts/TooltipPortalContext.ts +8 -0
  33. package/src/edges/CustomEdge.tsx +13 -2
  34. package/src/hooks/useElkLayout.test.ts +134 -0
  35. package/src/hooks/useElkLayout.ts +191 -0
  36. package/src/index.ts +6 -0
  37. package/src/nodes/otel/OtelSpanConventionNode.tsx +3 -3
  38. package/src/stories/ElkEdgeRouting.stories.tsx +415 -0
  39. package/src/stories/SpanBadges.stories.tsx +840 -0
  40. package/src/utils/elkLayout.test.ts +240 -0
  41. package/src/utils/elkLayout.ts +412 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * ELK (Eclipse Layout Kernel) Layout Utility
3
+ *
4
+ * Provides sophisticated edge routing with orthogonal (circuit-board style) paths
5
+ * that don't overlap and run parallel to each other.
6
+ */
7
+ import ELK from 'elkjs/lib/elk.bundled.js';
8
+ // Create ELK instance lazily to avoid issues in test environments
9
+ let elkInstance = null;
10
+ function getElkInstance() {
11
+ if (!elkInstance) {
12
+ elkInstance = new ELK();
13
+ }
14
+ return elkInstance;
15
+ }
16
+ /**
17
+ * Convert bend points to SVG path string
18
+ * @public Exported for testing
19
+ */
20
+ export function pointsToPath(points) {
21
+ if (points.length === 0)
22
+ return '';
23
+ if (points.length === 1)
24
+ return `M ${points[0].x} ${points[0].y}`;
25
+ let path = `M ${points[0].x} ${points[0].y}`;
26
+ for (let i = 1; i < points.length; i++) {
27
+ path += ` L ${points[i].x} ${points[i].y}`;
28
+ }
29
+ return path;
30
+ }
31
+ /**
32
+ * Convert bend points to smooth orthogonal path with rounded corners
33
+ * @public Exported for testing
34
+ */
35
+ export function pointsToSmoothPath(points, cornerRadius = 8) {
36
+ if (points.length === 0)
37
+ return '';
38
+ if (points.length === 1)
39
+ return `M ${points[0].x} ${points[0].y}`;
40
+ if (points.length === 2) {
41
+ return `M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y}`;
42
+ }
43
+ let path = `M ${points[0].x} ${points[0].y}`;
44
+ for (let i = 1; i < points.length - 1; i++) {
45
+ const prev = points[i - 1];
46
+ const curr = points[i];
47
+ const next = points[i + 1];
48
+ // Calculate distances
49
+ const d1 = Math.sqrt(Math.pow(curr.x - prev.x, 2) + Math.pow(curr.y - prev.y, 2));
50
+ const d2 = Math.sqrt(Math.pow(next.x - curr.x, 2) + Math.pow(next.y - curr.y, 2));
51
+ // Limit corner radius to half the shorter segment
52
+ const maxRadius = Math.min(d1, d2) / 2;
53
+ const radius = Math.min(cornerRadius, maxRadius);
54
+ if (radius < 1) {
55
+ // Too short for curve, just draw line
56
+ path += ` L ${curr.x} ${curr.y}`;
57
+ continue;
58
+ }
59
+ // Calculate direction vectors
60
+ const dir1 = { x: (curr.x - prev.x) / d1, y: (curr.y - prev.y) / d1 };
61
+ const dir2 = { x: (next.x - curr.x) / d2, y: (next.y - curr.y) / d2 };
62
+ // Calculate arc start and end points
63
+ const arcStart = {
64
+ x: curr.x - dir1.x * radius,
65
+ y: curr.y - dir1.y * radius,
66
+ };
67
+ const arcEnd = {
68
+ x: curr.x + dir2.x * radius,
69
+ y: curr.y + dir2.y * radius,
70
+ };
71
+ // Draw line to arc start, then quadratic curve to arc end
72
+ path += ` L ${arcStart.x} ${arcStart.y}`;
73
+ path += ` Q ${curr.x} ${curr.y} ${arcEnd.x} ${arcEnd.y}`;
74
+ }
75
+ // Final line to last point
76
+ const last = points[points.length - 1];
77
+ path += ` L ${last.x} ${last.y}`;
78
+ return path;
79
+ }
80
+ /**
81
+ * Calculate the midpoint of a path for label positioning
82
+ * @public Exported for testing
83
+ */
84
+ export function calculatePathMidpoint(points) {
85
+ if (points.length === 0)
86
+ return { x: 0, y: 0 };
87
+ if (points.length === 1)
88
+ return points[0];
89
+ // Calculate total path length
90
+ let totalLength = 0;
91
+ const segmentLengths = [];
92
+ for (let i = 1; i < points.length; i++) {
93
+ const len = Math.sqrt(Math.pow(points[i].x - points[i - 1].x, 2) + Math.pow(points[i].y - points[i - 1].y, 2));
94
+ segmentLengths.push(len);
95
+ totalLength += len;
96
+ }
97
+ // Find midpoint
98
+ const targetLength = totalLength / 2;
99
+ let currentLength = 0;
100
+ for (let i = 0; i < segmentLengths.length; i++) {
101
+ if (currentLength + segmentLengths[i] >= targetLength) {
102
+ // Midpoint is on this segment
103
+ const remaining = targetLength - currentLength;
104
+ const ratio = remaining / segmentLengths[i];
105
+ return {
106
+ x: points[i].x + (points[i + 1].x - points[i].x) * ratio,
107
+ y: points[i].y + (points[i + 1].y - points[i].y) * ratio,
108
+ };
109
+ }
110
+ currentLength += segmentLengths[i];
111
+ }
112
+ // Fallback to last point
113
+ return points[points.length - 1];
114
+ }
115
+ /**
116
+ * Get ELK layout options based on configuration
117
+ */
118
+ function getElkOptions(options) {
119
+ const { routingStyle = 'orthogonal', nodeSpacing = 50, edgeSpacing = 8, edgeNodeSpacing = 10, direction = 'RIGHT', } = options;
120
+ const baseOptions = {
121
+ 'elk.algorithm': 'layered',
122
+ 'elk.direction': direction,
123
+ 'elk.spacing.nodeNode': String(nodeSpacing),
124
+ 'elk.spacing.edgeEdge': String(edgeSpacing),
125
+ 'elk.spacing.edgeNode': String(edgeNodeSpacing),
126
+ 'elk.layered.spacing.edgeEdgeBetweenLayers': String(edgeSpacing),
127
+ 'elk.layered.spacing.edgeNodeBetweenLayers': String(edgeNodeSpacing),
128
+ };
129
+ // Set edge routing style
130
+ switch (routingStyle) {
131
+ case 'orthogonal':
132
+ baseOptions['elk.edgeRouting'] = 'ORTHOGONAL';
133
+ break;
134
+ case 'splines':
135
+ baseOptions['elk.edgeRouting'] = 'SPLINES';
136
+ break;
137
+ case 'polyline':
138
+ baseOptions['elk.edgeRouting'] = 'POLYLINE';
139
+ break;
140
+ }
141
+ return baseOptions;
142
+ }
143
+ /**
144
+ * Compute ELK layout for nodes and edges
145
+ *
146
+ * @param nodes - xyflow nodes
147
+ * @param edges - xyflow edges
148
+ * @param options - Layout options
149
+ * @returns Layout result with edge paths
150
+ */
151
+ export async function computeElkLayout(nodes, edges, options = {}) {
152
+ const { preserveNodePositions = true } = options;
153
+ // Convert nodes to ELK format - simple, no ports
154
+ const elkNodes = nodes.map((node) => {
155
+ const width = node.measured?.width ?? node.width ?? 200;
156
+ const height = node.measured?.height ?? node.height ?? 100;
157
+ return {
158
+ id: node.id,
159
+ width,
160
+ height,
161
+ x: node.position.x,
162
+ y: node.position.y,
163
+ };
164
+ });
165
+ // Convert edges to ELK format - simple node references
166
+ const elkEdges = edges.map((edge) => ({
167
+ id: edge.id,
168
+ sources: [edge.source],
169
+ targets: [edge.target],
170
+ }));
171
+ // Create ELK graph
172
+ const elkGraph = {
173
+ id: 'root',
174
+ layoutOptions: getElkOptions(options),
175
+ children: elkNodes,
176
+ edges: elkEdges,
177
+ };
178
+ // Run ELK layout
179
+ const layoutedGraph = await getElkInstance().layout(elkGraph);
180
+ // Build a map of original node positions (what we passed in)
181
+ const originalPositions = new Map();
182
+ for (const node of elkNodes) {
183
+ originalPositions.set(node.id, { x: node.x ?? 0, y: node.y ?? 0 });
184
+ }
185
+ // Build a map of ELK-computed node positions
186
+ const elkPositions = new Map();
187
+ if (layoutedGraph.children) {
188
+ for (const child of layoutedGraph.children) {
189
+ elkPositions.set(child.id, { x: child.x ?? 0, y: child.y ?? 0 });
190
+ }
191
+ }
192
+ // Extract results
193
+ const edgePaths = new Map();
194
+ const edgeLabelPositions = new Map();
195
+ // Process edges
196
+ if (layoutedGraph.edges) {
197
+ for (const edge of layoutedGraph.edges) {
198
+ if (edge.sections && edge.sections.length > 0) {
199
+ // Get source and target nodes for this edge
200
+ const sourceId = edges.find(e => e.id === edge.id)?.source;
201
+ const targetId = edges.find(e => e.id === edge.id)?.target;
202
+ // Calculate offset needed to translate from ELK positions to original positions
203
+ // We need to figure out which node each point is closest to and offset accordingly
204
+ const sourceOriginal = sourceId ? originalPositions.get(sourceId) : null;
205
+ const sourceElk = sourceId ? elkPositions.get(sourceId) : null;
206
+ const targetOriginal = targetId ? originalPositions.get(targetId) : null;
207
+ const targetElk = targetId ? elkPositions.get(targetId) : null;
208
+ // Collect all points from sections
209
+ const allPoints = [];
210
+ for (const section of edge.sections) {
211
+ allPoints.push(section.startPoint);
212
+ if (section.bendPoints) {
213
+ allPoints.push(...section.bendPoints);
214
+ }
215
+ allPoints.push(section.endPoint);
216
+ }
217
+ // If preserving positions, we need to offset the edge points
218
+ // The edge path is relative to ELK's layout, so we translate it
219
+ if (preserveNodePositions && sourceOriginal && sourceElk && targetOriginal && targetElk) {
220
+ // Calculate the offset from ELK space to original space
221
+ // For the start point, use source node offset
222
+ // For the end point, use target node offset
223
+ // For middle points, interpolate based on x position
224
+ const sourceOffset = {
225
+ x: sourceOriginal.x - sourceElk.x,
226
+ y: sourceOriginal.y - sourceElk.y,
227
+ };
228
+ const targetOffset = {
229
+ x: targetOriginal.x - targetElk.x,
230
+ y: targetOriginal.y - targetElk.y,
231
+ };
232
+ // Get the x-range of the path for interpolation
233
+ const minX = Math.min(...allPoints.map(p => p.x));
234
+ const maxX = Math.max(...allPoints.map(p => p.x));
235
+ const xRange = maxX - minX || 1;
236
+ // Offset each point - interpolate between source and target offset based on x position
237
+ for (let i = 0; i < allPoints.length; i++) {
238
+ const t = (allPoints[i].x - minX) / xRange; // 0 at source, 1 at target
239
+ allPoints[i] = {
240
+ x: allPoints[i].x + sourceOffset.x + (targetOffset.x - sourceOffset.x) * t,
241
+ y: allPoints[i].y + sourceOffset.y + (targetOffset.y - sourceOffset.y) * t,
242
+ };
243
+ }
244
+ }
245
+ // Convert to path
246
+ const path = options.routingStyle === 'orthogonal'
247
+ ? pointsToSmoothPath(allPoints, 8)
248
+ : pointsToPath(allPoints);
249
+ edgePaths.set(edge.id, path);
250
+ // Calculate label position
251
+ const labelPos = calculatePathMidpoint(allPoints);
252
+ edgeLabelPositions.set(edge.id, labelPos);
253
+ }
254
+ }
255
+ }
256
+ // Process nodes (update positions if not preserving)
257
+ const resultNodes = preserveNodePositions
258
+ ? nodes
259
+ : nodes.map((node) => {
260
+ const elkNode = layoutedGraph.children?.find((n) => n.id === node.id);
261
+ if (elkNode && elkNode.x !== undefined && elkNode.y !== undefined) {
262
+ return {
263
+ ...node,
264
+ position: { x: elkNode.x, y: elkNode.y },
265
+ };
266
+ }
267
+ return node;
268
+ });
269
+ return {
270
+ nodes: resultNodes,
271
+ edgePaths,
272
+ edgeLabelPositions,
273
+ };
274
+ }
275
+ /**
276
+ * Hook-friendly version that returns a layout function
277
+ */
278
+ export function createElkLayouter(options = {}) {
279
+ return (nodes, edges) => computeElkLayout(nodes, edges, options);
280
+ }
281
+ //# sourceMappingURL=elkLayout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elkLayout.js","sourceRoot":"","sources":["../../src/utils/elkLayout.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/principal-view-react",
3
- "version": "0.14.4",
3
+ "version": "0.14.6",
4
4
  "description": "React components for graph-based principal view framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@xyflow/react": "12.0.0",
18
+ "elkjs": "^0.11.1",
18
19
  "hast-util-sanitize": "^5.0.2",
19
20
  "highlight.js": "^11.11.1",
20
21
  "js-yaml": "4.1.1",
@@ -31,12 +32,12 @@
31
32
  },
32
33
  "peerDependencies": {
33
34
  "@principal-ade/industry-theme": "^0.1.7",
34
- "@principal-ai/principal-view-core": "^0.25.0",
35
+ "@principal-ai/principal-view-core": "^0.26.5",
35
36
  "react": "^18.0.0 || ^19.0.0",
36
37
  "react-dom": "^18.0.0 || ^19.0.0"
37
38
  },
38
39
  "devDependencies": {
39
- "@principal-ai/principal-view-core": "0.25.0",
40
+ "@principal-ai/principal-view-core": "0.26.5",
40
41
  "@principal-ade/industry-theme": "0.1.7",
41
42
  "@storybook/addon-docs": "10.1.2",
42
43
  "@storybook/addon-links": "10.1.2",
@@ -7,7 +7,6 @@ import React, {
7
7
  useRef,
8
8
  useImperativeHandle,
9
9
  forwardRef,
10
- createContext,
11
10
  } from 'react';
12
11
  import {
13
12
  ReactFlow,
@@ -59,6 +58,7 @@ import {
59
58
  convertToXYFlowNodes,
60
59
  convertToXYFlowEdges,
61
60
  } from '../utils/graphConverter';
61
+ import { useElkLayout, applyElkPathsToEdges } from '../hooks/useElkLayout';
62
62
  import {
63
63
  getCanvasBounds,
64
64
  calculateInitialViewport,
@@ -66,13 +66,10 @@ import {
66
66
  } from '../utils/canvasBounds';
67
67
  import { GraphEditProvider } from '../contexts/GraphEditContext';
68
68
  import { useUndoRedo, type HistoryEntry } from '../hooks/useUndoRedo';
69
+ import { TooltipPortalContext } from '../contexts/TooltipPortalContext';
69
70
 
70
- /**
71
- * Context for providing a portal target for tooltips.
72
- * This allows tooltips to be rendered within the graph container
73
- * instead of document.body, so they hide properly when tabs switch.
74
- */
75
- export const TooltipPortalContext = createContext<HTMLElement | null>(null);
71
+ // Re-export for backwards compatibility
72
+ export { TooltipPortalContext };
76
73
 
77
74
  /** Position change event for tracking node movements */
78
75
  export interface NodePositionChange {
@@ -277,6 +274,35 @@ interface GraphRendererBaseProps {
277
274
  */
278
275
  containerHeight?: number;
279
276
 
277
+ /**
278
+ * ELK layout configuration for circuit-board style edge routing.
279
+ * When enabled, edges are routed with orthogonal paths that don't overlap.
280
+ */
281
+ elkLayout?: {
282
+ /**
283
+ * Whether ELK layout is enabled
284
+ * @default false
285
+ */
286
+ enabled: boolean;
287
+ /**
288
+ * Edge routing style
289
+ * - 'orthogonal': Circuit-board style with 90-degree angles (default)
290
+ * - 'splines': Smooth curved edges
291
+ * - 'polyline': Straight line segments
292
+ */
293
+ routingStyle?: 'orthogonal' | 'splines' | 'polyline';
294
+ /**
295
+ * Minimum spacing between parallel edges in pixels
296
+ * @default 15
297
+ */
298
+ edgeSpacing?: number;
299
+ /**
300
+ * Spacing between edges and nodes in pixels
301
+ * @default 20
302
+ */
303
+ edgeNodeSpacing?: number;
304
+ };
305
+
280
306
  }
281
307
 
282
308
  /** GraphRenderer props - canvas format only */
@@ -535,6 +561,13 @@ interface GraphRendererInnerProps {
535
561
  onCopy?: (selectedNodeIds: string[]) => void;
536
562
  /** Pre-calculated initial viewport to avoid zoom animation on mount */
537
563
  initialViewport?: Viewport;
564
+ /** ELK layout configuration for circuit-board style edge routing */
565
+ elkLayout?: {
566
+ enabled: boolean;
567
+ routingStyle?: 'orthogonal' | 'splines' | 'polyline';
568
+ edgeSpacing?: number;
569
+ edgeNodeSpacing?: number;
570
+ };
538
571
  }
539
572
 
540
573
  /**
@@ -575,6 +608,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
575
608
  onNodeDragStop: onNodeDragStopProp,
576
609
  onCopy,
577
610
  initialViewport,
611
+ elkLayout,
578
612
  }) => {
579
613
  const { fitView, fitBounds, getNodes } = useReactFlow();
580
614
  const updateNodeInternals = useUpdateNodeInternals();
@@ -2151,31 +2185,52 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
2151
2185
  });
2152
2186
  }, [edges, configuration, violations, animationState.edgeAnimations, showTooltips, selectedEdgeIds, shiftKeyPressed, activeNodeIds, hiddenNodeIds]);
2153
2187
 
2188
+ // ELK layout for circuit-board style edge routing
2189
+ const { edgePaths: elkEdgePaths, edgeLabelPositions: elkLabelPositions } = useElkLayout(
2190
+ xyflowNodesBase,
2191
+ xyflowEdgesBase,
2192
+ {
2193
+ enabled: elkLayout?.enabled ?? false,
2194
+ routingStyle: elkLayout?.routingStyle ?? 'orthogonal',
2195
+ edgeSpacing: elkLayout?.edgeSpacing ?? 5,
2196
+ edgeNodeSpacing: elkLayout?.edgeNodeSpacing ?? 10,
2197
+ preserveNodePositions: true,
2198
+ }
2199
+ );
2200
+
2201
+ // Apply ELK paths to edges when ELK layout is enabled
2202
+ const xyflowEdgesWithElk = useMemo(() => {
2203
+ if (!elkLayout?.enabled || elkEdgePaths.size === 0) {
2204
+ return xyflowEdgesBase;
2205
+ }
2206
+ return applyElkPathsToEdges(xyflowEdgesBase, elkEdgePaths, elkLabelPositions);
2207
+ }, [elkLayout?.enabled, xyflowEdgesBase, elkEdgePaths, elkLabelPositions]);
2208
+
2154
2209
  // Local xyflow edges state for reconnection
2155
- const [xyflowLocalEdges, setXyflowLocalEdges] = useState<Edge<CustomEdgeData>[]>(xyflowEdgesBase);
2210
+ const [xyflowLocalEdges, setXyflowLocalEdges] = useState<Edge<CustomEdgeData>[]>(xyflowEdgesWithElk);
2156
2211
 
2157
2212
  // Sync when base edges change (structure changes like add/remove)
2158
2213
  const prevBaseEdgesKeyRef2 = useRef(baseEdgesKey);
2159
2214
  useEffect(() => {
2160
2215
  if (prevBaseEdgesKeyRef2.current !== baseEdgesKey) {
2161
2216
  prevBaseEdgesKeyRef2.current = baseEdgesKey;
2162
- setXyflowLocalEdges(xyflowEdgesBase);
2217
+ setXyflowLocalEdges(xyflowEdgesWithElk);
2163
2218
  }
2164
- }, [baseEdgesKey, xyflowEdgesBase]);
2219
+ }, [baseEdgesKey, xyflowEdgesWithElk]);
2165
2220
 
2166
2221
  // Set the reset visual state function for use by resetEditState
2167
2222
  // This resets both nodes and edges to their original state
2168
2223
  useEffect(() => {
2169
2224
  resetVisualStateRef.current = () => {
2170
2225
  setXyflowLocalNodes(xyflowNodesBase);
2171
- setXyflowLocalEdges(xyflowEdgesBase);
2226
+ setXyflowLocalEdges(xyflowEdgesWithElk);
2172
2227
  // Notify parent that changes have been cleared
2173
2228
  onPendingChangesChange?.(false);
2174
2229
  };
2175
- }, [xyflowNodesBase, xyflowEdgesBase, onPendingChangesChange]);
2230
+ }, [xyflowNodesBase, xyflowEdgesWithElk, onPendingChangesChange]);
2176
2231
 
2177
2232
  // Use local edges in edit mode, base edges otherwise
2178
- const xyflowEdges = editable ? xyflowLocalEdges : xyflowEdgesBase;
2233
+ const xyflowEdges = editable ? xyflowLocalEdges : xyflowEdgesWithElk;
2179
2234
 
2180
2235
  // Handle edge changes (selection, reconnection, etc.)
2181
2236
  const handleEdgesChange = useCallback(
@@ -2645,6 +2700,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2645
2700
  height = '100%',
2646
2701
  containerWidth,
2647
2702
  containerHeight,
2703
+ elkLayout,
2648
2704
  } = props;
2649
2705
  const { theme } = useTheme();
2650
2706
  const containerRef = useRef<HTMLDivElement>(null);
@@ -2867,6 +2923,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2867
2923
  onNodeDragStop={onNodeDragStop}
2868
2924
  onCopy={onCopy}
2869
2925
  initialViewport={initialViewport}
2926
+ elkLayout={elkLayout}
2870
2927
  />
2871
2928
  </ReactFlowProvider>
2872
2929
  </TooltipPortalContext.Provider>
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useContext } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import { useTheme } from '@principal-ade/industry-theme';
4
4
  import { IndustryMarkdownSlide } from 'themed-markdown';
5
- import { TooltipPortalContext } from './GraphRenderer';
5
+ import { TooltipPortalContext } from '../contexts/TooltipPortalContext';
6
6
 
7
7
  export interface OtelInfo {
8
8
  kind: 'type' | 'service' | 'instance';
@@ -0,0 +1,8 @@
1
+ import { createContext } from 'react';
2
+
3
+ /**
4
+ * Context for providing a portal target for tooltips.
5
+ * This allows tooltips to be rendered within the graph container
6
+ * instead of document.body, so they hide properly when tabs switch.
7
+ */
8
+ export const TooltipPortalContext = createContext<HTMLElement | null>(null);
@@ -17,6 +17,10 @@ export interface CustomEdgeData extends Record<string, unknown> {
17
17
  tooltipsEnabled?: boolean;
18
18
  // Whether shift key is currently pressed (for tooltip control)
19
19
  shiftKeyPressed?: boolean;
20
+ // ELK-computed path (circuit-board style routing)
21
+ elkPath?: string;
22
+ // ELK-computed label position
23
+ elkLabelPosition?: { x: number; y: number };
20
24
  }
21
25
 
22
26
  /**
@@ -46,6 +50,8 @@ export const CustomEdge: React.FC<EdgeProps<Edge<CustomEdgeData>>> = ({
46
50
  animationDirection = 'forward',
47
51
  tooltipsEnabled = true,
48
52
  shiftKeyPressed = false,
53
+ elkPath,
54
+ elkLabelPosition,
49
55
  } = edgeProps || ({} as CustomEdgeData);
50
56
 
51
57
  const [particlePosition, setParticlePosition] = useState(0);
@@ -77,8 +83,8 @@ export const CustomEdge: React.FC<EdgeProps<Edge<CustomEdgeData>>> = ({
77
83
  const color = hasViolations ? '#D0021B' : edgeColor || typeDefinition.color || '#888';
78
84
  const width = typeDefinition.width || 2;
79
85
 
80
- // Get SmoothStep path (orthogonal routing with rounded corners)
81
- const [edgePath, labelX, labelY] = getSmoothStepPath({
86
+ // Get edge path - use ELK path if available, otherwise fall back to SmoothStep
87
+ const [defaultPath, defaultLabelX, defaultLabelY] = getSmoothStepPath({
82
88
  sourceX,
83
89
  sourceY,
84
90
  sourcePosition,
@@ -89,6 +95,11 @@ export const CustomEdge: React.FC<EdgeProps<Edge<CustomEdgeData>>> = ({
89
95
  offset: 20,
90
96
  });
91
97
 
98
+ // Use ELK-computed path for circuit-board style routing when available
99
+ const edgePath = elkPath || defaultPath;
100
+ const labelX = elkLabelPosition?.x ?? defaultLabelX;
101
+ const labelY = elkLabelPosition?.y ?? defaultLabelY;
102
+
92
103
  // Style based on edge type
93
104
  const getStrokeStyle = () => {
94
105
  switch (typeDefinition.style) {