pub const REACTFLOW_TEMPLATE: &str = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Hydro IR Graph - ReactFlow.js</title>\n <script crossorigin src=\"https://unpkg.com/react@17/umd/react.production.min.js\"></script>\n <script crossorigin src=\"https://unpkg.com/react-dom@17/umd/react-dom.production.min.js\"></script>\n <script src=\"https://unpkg.com/@babel/standalone/babel.min.js\"></script>\n <script src=\"https://unpkg.com/[email protected]/dist/umd/index.js\"></script>\n <script src=\"https://unpkg.com/[email protected]/lib/elk.bundled.js\"></script>\n <link rel=\"stylesheet\" href=\"https://unpkg.com/[email protected]/dist/style.css\" />\n <style>\n body {\n margin: 0;\n padding: 0;\n font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', \'Roboto\', \'Oxygen\',\n \'Ubuntu\', \'Cantarell\', \'Fira Sans\', \'Droid Sans\', \'Helvetica Neue\',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n .reactflow-wrapper {\n width: 100vw;\n height: 100vh;\n }\n /* Compact unified legend in upper right */\n .unified-legend {\n position: absolute;\n top: 90px;\n right: 10px;\n z-index: 10;\n background: rgba(255, 255, 255, 0.95);\n backdrop-filter: blur(10px);\n padding: 8px;\n border-radius: 6px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n max-width: 220px;\n font-size: 11px;\n }\n /* Layout controls above legend */\n .layout-controls {\n position: absolute;\n top: 40px;\n right: 10px;\n z-index: 10;\n background: rgba(255, 255, 255, 0.95);\n backdrop-filter: blur(10px);\n border-radius: 6px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n padding: 6px;\n display: flex;\n align-items: center;\n gap: 4px;\n }\n .unified-legend h4 {\n margin: 0 0 6px 0;\n font-size: 12px;\n font-weight: 600;\n color: #333;\n border-bottom: 1px solid #eee;\n padding-bottom: 3px;\n }\n .legend-section {\n margin-bottom: 8px;\n }\n .legend-section:last-child {\n margin-bottom: 0;\n }\n .legend-item {\n display: flex;\n align-items: center;\n margin: 3px 0;\n font-size: 10px;\n }\n .legend-color {\n width: 12px;\n height: 12px;\n border-radius: 2px;\n margin-right: 6px;\n border: 1px solid #666;\n flex-shrink: 0;\n }\n .location-legend-color {\n width: 16px;\n height: 10px;\n border-radius: 2px;\n margin-right: 6px;\n border: 1px solid;\n flex-shrink: 0;\n }\n .icon-button {\n width: 28px;\n height: 28px;\n border: none;\n border-radius: 4px;\n background: #f8f9fa;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n color: #495057;\n transition: all 0.2s ease;\n position: relative;\n }\n .icon-button:hover {\n background: #e9ecef;\n color: #212529;\n transform: translateY(-1px);\n }\n .icon-button:active {\n transform: translateY(0);\n }\n .layout-select, .palette-select {\n background: #f8f9fa;\n border: 1px solid #dee2e6;\n border-radius: 4px;\n font-size: 11px;\n padding: 4px 6px;\n width: 80px;\n color: #495057;\n }\n .layout-select:hover, .palette-select:hover {\n border-color: #adb5bd;\n }\n /* Tooltip styles */\n .tooltip {\n position: absolute;\n bottom: 100%;\n left: 50%;\n transform: translateX(-50%);\n background: #333;\n color: white;\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 10px;\n white-space: nowrap;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.2s ease;\n margin-bottom: 4px;\n }\n .tooltip::after {\n content: \'\';\n position: absolute;\n top: 100%;\n left: 50%;\n transform: translateX(-50%);\n border: 4px solid transparent;\n border-top-color: #333;\n }\n /* Special positioning for rightmost button tooltip to avoid cutoff */\n .layout-controls .icon-button:last-child .tooltip {\n left: auto;\n right: 0;\n transform: none;\n }\n .layout-controls .icon-button:last-child .tooltip::after {\n left: auto;\n right: 16px;\n transform: none;\n }\n .icon-button:hover .tooltip {\n opacity: 1;\n }\n .react-flow__node {\n cursor: pointer;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n transition: all 0.2s ease;\n }\n .react-flow__node:hover {\n transform: scale(1.02);\n box-shadow: 0 4px 8px rgba(0,0,0,0.15);\n }\n /* Container node specific styles */\n .react-flow__node.container-node {\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n font-size: 12px;\n font-weight: 500;\n color: #333;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n padding: 4px 8px;\n box-sizing: border-box;\n }\n /* Ensure container labels are readable when collapsed */\n .react-flow__node.container-node .react-flow__node-default {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n width: 100%;\n }\n </style>\n</head>\n<body>\n <div id=\"root\"></div>\n <script type=\"text/babel\">\n // @ts-nocheck\n // eslint-disable-next-line\n const graphData = {{GRAPH_DATA}};\n const { useState, useCallback, useRef, useEffect } = React;\n // ReactFlow v11 components via reactflow\n const ReactFlowLib = window.ReactFlow;\n const { default: ReactFlow, Controls, MiniMap, Background, useNodesState, useEdgesState, addEdge, applyNodeChanges, applyEdgeChanges } = ReactFlowLib;\n \n // ColorBrewer palettes - expanded collection with various aesthetics\n const colorPalettes = {\n // Qualitative palettes (great for categorical data)\n \'Set3\': [\'#8dd3c7\', \'#ffffb3\', \'#bebada\', \'#fb8072\', \'#80b1d3\', \'#fdb462\', \'#b3de69\'],\n \'Pastel1\': [\'#fbb4ae\', \'#b3cde3\', \'#ccebc5\', \'#decbe4\', \'#fed9a6\', \'#ffffcc\', \'#e5d8bd\'],\n \'Pastel2\': [\'#b3e2cd\', \'#fdcdac\', \'#cbd5e8\', \'#f4cae4\', \'#e6f5c9\', \'#fff2ae\', \'#f1e2cc\'],\n \'Set1\': [\'#e41a1c\', \'#377eb8\', \'#4daf4a\', \'#984ea3\', \'#ff7f00\', \'#ffff33\', \'#a65628\'],\n \'Set2\': [\'#66c2a5\', \'#fc8d62\', \'#8da0cb\', \'#e78ac3\', \'#a6d854\', \'#ffd92f\', \'#e5c494\'],\n \'Dark2\': [\'#1b9e77\', \'#d95f02\', \'#7570b3\', \'#e7298a\', \'#66a61e\', \'#e6ab02\', \'#a6761d\'],\n \'Accent\': [\'#7fc97f\', \'#beaed4\', \'#fdc086\', \'#ffff99\', \'#386cb0\', \'#f0027f\', \'#bf5b17\'],\n \'Paired\': [\'#a6cee3\', \'#1f78b4\', \'#b2df8a\', \'#33a02c\', \'#fb9a99\', \'#e31a1c\', \'#fdbf6f\'],\n \n // Sequential palettes (good for intensity/hierarchy)\n \'Blues\': [\'#f7fbff\', \'#deebf7\', \'#c6dbef\', \'#9ecae1\', \'#6baed6\', \'#4292c6\', \'#2171b5\'],\n \'Greens\': [\'#f7fcf5\', \'#e5f5e0\', \'#c7e9c0\', \'#a1d99b\', \'#74c476\', \'#41ab5d\', \'#238b45\'],\n \'Oranges\': [\'#fff5eb\', \'#fee6ce\', \'#fdd0a2\', \'#fdae6b\', \'#fd8d3c\', \'#f16913\', \'#d94801\'],\n \'Purples\': [\'#fcfbfd\', \'#efedf5\', \'#dadaeb\', \'#bcbddc\', \'#9e9ac8\', \'#807dba\', \'#6a51a3\'],\n \'Reds\': [\'#fff5f0\', \'#fee0d2\', \'#fcbba1\', \'#fc9272\', \'#fb6a4a\', \'#ef3b2c\', \'#cb181d\'],\n \n // Diverging palettes (great for showing contrasts)\n \'Spectral\': [\'#9e0142\', \'#d53e4f\', \'#f46d43\', \'#fdae61\', \'#fee08b\', \'#e6f598\', \'#abdda4\'],\n \'RdYlBu\': [\'#d73027\', \'#f46d43\', \'#fdae61\', \'#fee090\', \'#e0f3f8\', \'#abd9e9\', \'#74add1\'],\n \'RdYlGn\': [\'#d73027\', \'#f46d43\', \'#fdae61\', \'#fee08b\', \'#d9ef8b\', \'#a6d96a\', \'#66bd63\'],\n \'PiYG\': [\'#d01c8b\', \'#f1b6da\', \'#fde0ef\', \'#f7f7f7\', \'#e6f5d0\', \'#b8e186\', \'#4d9221\'],\n \'BrBG\': [\'#8c510a\', \'#bf812d\', \'#dfc27d\', \'#f6e8c3\', \'#c7eae5\', \'#80cdc1\', \'#35978f\'],\n \n // Modern/trendy palettes\n \'Viridis\': [\'#440154\', \'#482777\', \'#3f4a8a\', \'#31678e\', \'#26838f\', \'#1f9d8a\', \'#6cce5a\'],\n \'Plasma\': [\'#0d0887\', \'#6a00a8\', \'#b12a90\', \'#e16462\', \'#fca636\', \'#f0f921\', \'#fcffa4\'],\n \'Warm\': [\'#375a7f\', \'#5bc0de\', \'#5cb85c\', \'#f0ad4e\', \'#d9534f\', \'#ad4e92\', \'#6f5499\'],\n \'Cool\': [\'#2c3e50\', \'#3498db\', \'#1abc9c\', \'#16a085\', \'#27ae60\', \'#2980b9\', \'#8e44ad\'],\n \'Earth\': [\'#8b4513\', \'#a0522d\', \'#cd853f\', \'#daa520\', \'#b8860b\', \'#228b22\', \'#006400\']\n };\n \n // Remove sourcePosition/targetPosition from nodes for flexible edge attachment\n const initialNodes = (graphData.nodes || []).map(node => {\n const { sourcePosition, targetPosition, ...rest } = node;\n return rest;\n });\n const initialEdges = (graphData.edges || []).map(edge => {\n // Use \'bezier\' edge type for flexible routing to all sides of nodes (left, right, top, bottom)\n const processedEdge = {\n id: edge.id,\n source: edge.source,\n target: edge.target,\n type: \'bezier\',\n zIndex: 1000,\n markerEnd: {\n type: \'arrowclosed\',\n width: 20,\n height: 20,\n color: edge.style?.stroke || \'#666666\'\n },\n style: {\n strokeWidth: edge.style?.strokeWidth || 2,\n stroke: edge.style?.stroke || \'#666666\',\n strokeDasharray: edge.style?.strokeDasharray\n },\n animated: edge.animated || false,\n interactionWidth: 20\n };\n if (edge.label) processedEdge.label = edge.label;\n if (edge.labelStyle) processedEdge.labelStyle = edge.labelStyle;\n if (edge.labelShowBg) processedEdge.labelShowBg = edge.labelShowBg;\n if (edge.labelBgStyle) processedEdge.labelBgStyle = edge.labelBgStyle;\n return processedEdge;\n });\n\n // elk.js layout configuration with hierarchical support\n const elkLayouts = {\n layered: {\n \'elk.algorithm\': \'layered\',\n \'elk.layered.spacing.nodeNodeBetweenLayers\': 150,\n \'elk.spacing.nodeNode\': 120,\n \'elk.spacing.componentComponent\': 80,\n \'elk.direction\': \'RIGHT\',\n \'elk.layered.thoroughness\': 7,\n \'elk.hierarchyHandling\': \'SEPARATE_CHILDREN\'\n },\n force: {\n \'elk.algorithm\': \'force\',\n \'elk.force.repulsivePower\': 0.5,\n \'elk.spacing.nodeNode\': 150,\n \'elk.spacing.componentComponent\': 100,\n \'elk.hierarchyHandling\': \'SEPARATE_CHILDREN\'\n },\n stress: {\n \'elk.algorithm\': \'stress\',\n \'elk.stress.desiredEdgeLength\': 150,\n \'elk.spacing.nodeNode\': 120,\n \'elk.spacing.componentComponent\': 80,\n \'elk.hierarchyHandling\': \'SEPARATE_CHILDREN\'\n },\n mrtree: {\n \'elk.algorithm\': \'mrtree\',\n \'elk.mrtree.searchOrder\': \'DFS\',\n \'elk.spacing.nodeNode\': 120,\n \'elk.spacing.componentComponent\': 80,\n \'elk.hierarchyHandling\': \'SEPARATE_CHILDREN\'\n },\n radial: {\n \'elk.algorithm\': \'radial\',\n \'elk.radial.radius\': 250,\n \'elk.spacing.nodeNode\': 120,\n \'elk.spacing.componentComponent\': 80,\n \'elk.hierarchyHandling\': \'SEPARATE_CHILDREN\'\n },\n disco: {\n \'elk.algorithm\': \'disco\',\n \'elk.disco.componentCompaction.strategy\': \'POLYOMINO\',\n \'elk.spacing.nodeNode\': 80,\n \'elk.hierarchyHandling\': \'INCLUDE_CHILDREN\'\n }\n };\n\n // Generate colors based on selected palette\n const generateNodeColors = (nodeType, palette = \'Set3\') => {\n const colors = colorPalettes[palette];\n const typeMap = {\n \'Source\': 0,\n \'Transform\': 1,\n \'Join\': 2,\n \'Aggregation\': 3,\n \'Network\': 4,\n \'Sink\': 5,\n \'Tee\': 6\n };\n \n const baseColor = colors[typeMap[nodeType] || 0];\n \n // Create gradient colors\n const primary = baseColor;\n const secondary = lightenColor(baseColor, 10);\n const tertiary = lightenColor(baseColor, 25);\n const border = darkenColor(baseColor, 5);\n \n // Create a gentle linear gradient\n const gradient = `linear-gradient(0deg, ${tertiary} 0%, ${secondary} 80%, ${primary} 100%)`;\n \n return { primary, secondary, tertiary, border, gradient };\n };\n \n // Simplified color manipulation using CSS color-mix\n const lightenColor = (color, percent) => `color-mix(in srgb, ${color} ${100-percent}%, white)`;\n const darkenColor = (color, percent) => `color-mix(in srgb, ${color} ${100-percent}%, black)`;\n\n // Helper function to truncate long labels for collapsed containers\n const leftTruncateRustPath = (label, isCollapsed) => {\n if (!isCollapsed) return label;\n if (!label || typeof label !== \'string\') return \'Unknown\';\n \n // For collapsed containers, we have about 180px width (200px - padding)\n // Assuming average character width of ~8px, we can fit about 22 characters\n let maxLength = 22;\n \n if (label.length <= maxLength) return label;\n \n // If it contains \"::\" separators, left-truncate with ellipses on the left\n if (label.includes(\'::\')) {\n const segments = label.split(\'::\');\n const lastComponent = segments[segments.length - 1];\n \n // Always show at least \"...::lastComponent\"\n const minTruncated = \'...\' + \'::\' + lastComponent;\n \n // If the minimum required format is longer than our standard width,\n // we\'ll need a wider container - return the minimum format\n if (minTruncated.length > maxLength) {\n return minTruncated;\n }\n \n // Try to include more segments while keeping ellipses on the left\n let truncated = lastComponent;\n for (let i = segments.length - 2; i >= 0; i--) {\n const withNext = segments[i] + \'::\' + truncated;\n const withEllipsis = \'...\' + \'::\' + truncated;\n \n if (withNext.length <= maxLength) {\n // Can fit without ellipses\n truncated = withNext;\n } else if (withEllipsis.length <= maxLength) {\n // Need ellipses but it fits\n truncated = withEllipsis;\n break;\n } else {\n // Even with ellipses it doesn\'t fit, use previous iteration result\n truncated = \'...\' + \'::\' + truncated;\n break;\n }\n }\n \n return truncated;\n } else {\n // No \"::\" separators, just truncate from the left with ellipses\n return \'...\' + label.slice(-(maxLength - 3));\n }\n };\n\n // Helper function to calculate minimum width needed for a collapsed container\n const getMinCollapsedWidth = (label) => {\n if (!label || typeof label !== \'string\') label = \'Unknown\';\n const truncated = leftTruncateRustPath(label, true);\n // Assuming 8px per character + 16px padding\n const minWidth = Math.max(200, truncated.length * 8 + 16);\n return minWidth;\n };\n\n\n\n // Simplified location color generation\n const generateLocationColor = (locationId, totalLocations, palette = \'Set3\') => {\n const colors = colorPalettes[palette];\n const color = colors[locationId % colors.length];\n return `${color}40`; // Add transparency\n };\n\n const generateLocationBorderColor = (locationId, totalLocations, palette = \'Set3\') => {\n const colors = colorPalettes[palette];\n return colors[locationId % colors.length];\n };\n\n const elk = new ELK();\n\n // Helper function to generate hyperedges between containers\n const generateHyperedges = (nodes, edges) => {\n const hyperedges = [];\n const containerPairs = new Set();\n \n // Find all edges that cross container boundaries\n edges.forEach(edge => {\n const sourceNode = nodes.find(n => n.id === edge.source);\n const targetNode = nodes.find(n => n.id === edge.target);\n \n if (sourceNode && targetNode) {\n const sourceLocationId = sourceNode.data?.locationId;\n const targetLocationId = targetNode.data?.locationId;\n \n // Only create hyperedges between different locations (containers)\n if (sourceLocationId !== undefined && targetLocationId !== undefined && \n sourceLocationId !== targetLocationId) {\n \n const sourceContainerId = `container_${sourceLocationId}`;\n const targetContainerId = `container_${targetLocationId}`;\n const pairKey = `${sourceContainerId}->${targetContainerId}`;\n \n // Avoid duplicate hyperedges between the same container pair\n if (!containerPairs.has(pairKey)) {\n containerPairs.add(pairKey);\n hyperedges.push({\n id: `hyperedge_${sourceLocationId}_to_${targetLocationId}`,\n sources: [sourceContainerId],\n targets: [targetContainerId],\n });\n }\n }\n }\n });\n \n return hyperedges;\n };\n\n // Shared edge routing logic for collapsed containers\n const routeEdgesForCollapsedContainers = (edges, collapsedLocations, childNodeIdsByParent) => {\n return edges.map(edge => {\n let newEdge = { ...edge };\n \n // Reset any previous modifications\n if (newEdge.data?.originalSource) {\n newEdge.source = newEdge.data.originalSource;\n newEdge.data = { ...newEdge.data };\n delete newEdge.data.originalSource;\n }\n if (newEdge.data?.originalTarget) {\n newEdge.target = newEdge.data.originalTarget;\n newEdge.data = { ...newEdge.data };\n delete newEdge.data.originalTarget;\n }\n newEdge.hidden = false;\n\n // Find collapsed containers containing source/target\n let sourceInCollapsedContainer = null;\n let targetInCollapsedContainer = null;\n \n for (const locationId in collapsedLocations) {\n if (collapsedLocations[locationId]) {\n const containerId = `container_${locationId}`;\n const childIds = childNodeIdsByParent[containerId] || new Set();\n\n if (childIds.has(newEdge.source)) {\n sourceInCollapsedContainer = containerId;\n }\n if (childIds.has(newEdge.target)) {\n targetInCollapsedContainer = containerId;\n }\n }\n }\n \n // Apply routing based on container states\n if (sourceInCollapsedContainer && targetInCollapsedContainer) {\n if (sourceInCollapsedContainer === targetInCollapsedContainer) {\n newEdge.hidden = true; // Hide internal edges\n } else {\n // Route container to container\n newEdge.data = { ...newEdge.data, originalSource: newEdge.source, originalTarget: newEdge.target };\n newEdge.source = sourceInCollapsedContainer;\n newEdge.target = targetInCollapsedContainer;\n }\n } else if (sourceInCollapsedContainer) {\n newEdge.data = { ...newEdge.data, originalSource: newEdge.source };\n newEdge.source = sourceInCollapsedContainer;\n } else if (targetInCollapsedContainer) {\n newEdge.data = { ...newEdge.data, originalTarget: newEdge.target };\n newEdge.target = targetInCollapsedContainer;\n }\n \n return newEdge;\n });\n };\n\n // Function to apply ELK layout with hierarchical grouping and hyperedges\n const applyElkLayout = async (nodes, edges, layoutType = \'layered\', precomputedHyperedges = []) => {\n const elkOptions = elkLayouts[layoutType] || elkLayouts.layered;\n \n // Group nodes by location first\n const locationGroups = new Map();\n const orphanNodes = [];\n \n nodes.forEach(node => {\n const nodeLocationId = node.data?.locationId;\n if (nodeLocationId !== null && nodeLocationId !== undefined) {\n if (!locationGroups.has(nodeLocationId)) {\n locationGroups.set(nodeLocationId, []);\n }\n locationGroups.get(nodeLocationId).push(node);\n } else {\n orphanNodes.push(node);\n }\n });\n \n // Create hierarchical ELK structure with proper container spacing\n const elkChildren = [];\n \n // Process each location as a separate container\n for (const [locationId, locationNodes] of locationGroups) {\n const elkNodes = locationNodes.map(node => {\n const actualNode = nodes.find(n => n.id === node.id);\n const nodeWidth = actualNode?.style?.width ? \n parseFloat(actualNode.style.width.toString().replace(\'px\', \'\')) : 200;\n const nodeHeight = actualNode?.style?.height ? \n parseFloat(actualNode.style.height.toString().replace(\'px\', \'\')) : 60;\n \n return {\n id: node.id,\n width: nodeWidth,\n height: nodeHeight,\n };\n });\n\n const elkEdgesInLocation = edges.filter(edge => {\n const sourceInLocation = locationNodes.some(n => n.id === edge.source);\n const targetInLocation = locationNodes.some(n => n.id === edge.target);\n return sourceInLocation && targetInLocation;\n }).map(edge => ({\n id: edge.id,\n sources: [edge.source],\n targets: [edge.target],\n }));\n\n // Use default container dimensions for ELK layout\n const containerWidth = 400;\n const containerHeight = 300;\n\n elkChildren.push({\n id: `container_${locationId}`,\n width: containerWidth,\n height: containerHeight,\n layoutOptions: {\n ...elkOptions,\n \'elk.padding\': \'[top=40,left=20,bottom=20,right=20]\',\n \'elk.spacing.nodeNode\': 60,\n },\n children: elkNodes,\n edges: elkEdgesInLocation,\n });\n }\n \n // Add orphan nodes as top-level nodes with actual dimensions\n orphanNodes.forEach(node => {\n const actualNode = nodes.find(n => n.id === node.id);\n const nodeWidth = actualNode?.style?.width ? \n parseFloat(actualNode.style.width.toString().replace(\'px\', \'\')) : 200;\n const nodeHeight = actualNode?.style?.height ? \n parseFloat(actualNode.style.height.toString().replace(\'px\', \'\')) : 60;\n \n elkChildren.push({\n id: node.id,\n width: nodeWidth,\n height: nodeHeight,\n });\n });\n\n // Use precomputed hyperedges if available, otherwise generate them\n const hyperedges = precomputedHyperedges.length > 0 ? precomputedHyperedges : generateHyperedges(nodes, edges);\n \n const elkGraph = {\n id: \'root\',\n layoutOptions: {\n ...elkOptions,\n \'elk.spacing.nodeNode\': 150,\n \'elk.spacing.componentComponent\': 100,\n \'elk.layered.spacing.nodeNodeBetweenLayers\': 150,\n },\n children: elkChildren,\n edges: hyperedges, // Use hyperedges for container layout\n };\n\n try {\n const layoutedGraph = await elk.layout(elkGraph);\n \n // Apply positions from ELK layout\n const layoutedNodes = nodes.map((node) => {\n // Find the node in the layout result\n let elkNode = null;\n let containerOffset = { x: 0, y: 0 };\n \n // Look for the node in containers first\n for (const container of layoutedGraph.children || []) {\n if (container.children) {\n const foundNode = container.children.find(n => n.id === node.id);\n if (foundNode) {\n elkNode = foundNode;\n containerOffset = { x: container.x || 0, y: container.y || 0 };\n break;\n }\n }\n }\n \n // If not found in containers, look at top level\n if (!elkNode) {\n elkNode = layoutedGraph.children?.find(n => n.id === node.id);\n }\n \n return {\n ...node,\n position: {\n x: elkNode ? (elkNode.x || 0) + containerOffset.x : Math.random() * 500,\n y: elkNode ? (elkNode.y || 0) + containerOffset.y : Math.random() * 500,\n },\n };\n });\n\n return layoutedNodes;\n } catch (error) {\n console.error(\'ELK layout failed:\', error);\n return nodes; // Fallback to original positions\n }\n };\n\n // Helper function to create a container for a location\n const createLocationContainer = (location, locationNodes, currentPalette) => {\n // Calculate actual bounds based on real node dimensions\n const bounds = locationNodes.reduce((acc, node) => {\n // Get actual node dimensions from style or use defaults\n const nodeWidth = node.style?.width ? \n parseFloat(node.style.width.toString().replace(\'px\', \'\')) : 200;\n const nodeHeight = node.style?.height ? \n parseFloat(node.style.height.toString().replace(\'px\', \'\')) : 60;\n \n const nodeRight = node.position.x + nodeWidth;\n const nodeBottom = node.position.y + nodeHeight;\n \n return {\n minX: Math.min(acc.minX, node.position.x),\n minY: Math.min(acc.minY, node.position.y),\n maxX: Math.max(acc.maxX, nodeRight),\n maxY: Math.max(acc.maxY, nodeBottom)\n };\n }, {\n minX: locationNodes[0]?.position.x || 0,\n minY: locationNodes[0]?.position.y || 0,\n maxX: locationNodes[0]?.position.x || 0,\n maxY: locationNodes[0]?.position.y || 0\n });\n \n const padding = 30;\n const containerX = bounds.minX - padding;\n const containerY = bounds.minY - padding - 30; // Extra space for label\n \n const backgroundColor = generateLocationColor(location.id, 1, currentPalette);\n const borderColor = generateLocationBorderColor(location.id, 1, currentPalette);\n \n return {\n id: `container_${location.id}`,\n type: \'default\',\n position: { x: containerX, y: containerY },\n className: \'container-node\',\n style: {\n width: bounds.maxX - bounds.minX + 2 * padding,\n height: bounds.maxY - bounds.minY + 2 * padding + 30, // Extra 30px for label\n backgroundColor: backgroundColor,\n border: `2px solid ${borderColor}`,\n borderRadius: \'8px\',\n cursor: \'pointer\',\n zIndex: 1,\n },\n data: { \n label: location.label,\n isContainer: true\n },\n draggable: true,\n };\n };\n\n // Helper function to create child nodes for a container\n const createChildNodes = (locationNodes, containerId, containerPosition) => {\n return locationNodes.map(node => ({\n ...node,\n parentNode: containerId,\n extent: \'parent\',\n position: {\n x: node.position.x - containerPosition.x,\n y: node.position.y - containerPosition.y\n }\n }));\n };\n\n // STEP 2: Smart container layout using ELK with position preservation and hyperedges\n const layoutContainersWithELK = async (containerNodes, allNodes, allEdges, changedContainerId = null, precomputedHyperedges = []) => {\n if (containerNodes.length === 0) return containerNodes;\n \n // Use precomputed hyperedges if available, otherwise generate them\n const hyperedges = precomputedHyperedges.length > 0 ? precomputedHyperedges : generateHyperedges(allNodes, allEdges);\n \n // Prepare ELK layout for containers only\n const elkContainers = containerNodes.map(container => {\n const width = parseFloat(container.style?.width?.toString().replace(\'px\', \'\')) || 400;\n const height = parseFloat(container.style?.height?.toString().replace(\'px\', \'\')) || 300;\n \n const elkContainer = {\n id: container.id,\n width: width,\n height: height,\n };\n \n // If this container hasn\'t changed, COMPLETELY FIX its position\n if (changedContainerId && container.id !== changedContainerId) {\n elkContainer.x = container.position.x;\n elkContainer.y = container.position.y;\n // Use ELK\'s position fixing to completely lock this container in place\n elkContainer.layoutOptions = {\n \'elk.position.x\': container.position.x.toString(),\n \'elk.position.y\': container.position.y.toString(),\n \'elk.nodeSize.constraints\': \'FIXED_POS\', // This fixes the position completely\n \'elk.nodeSize.options\': \'FIXED_POS\'\n };\n } else {\n // For the changed container, allow ELK to position it freely\n elkContainer.layoutOptions = {\n \'elk.nodeSize.constraints\': \'\',\n \'elk.nodeSize.options\': \'\'\n };\n }\n \n return elkContainer;\n });\n \n // Create a simple ELK graph for container layout with hyperedges\n const elkGraph = {\n id: \'container_root\',\n layoutOptions: {\n \'elk.algorithm\': \'org.eclipse.elk.layered\', // Use layered for better hyperedge handling\n \'elk.direction\': \'RIGHT\',\n \'elk.spacing.nodeNode\': 100,\n \'elk.spacing.componentComponent\': 100,\n \'elk.layered.spacing.nodeNodeBetweenLayers\': 150,\n // More iterations for the changed container to find a good position\n \'elk.layered.thoroughness\': changedContainerId ? 10 : 5,\n // Respect fixed positions\n \'elk.partitioning.activate\': \'false\'\n },\n children: elkContainers,\n edges: hyperedges // Use hyperedges for better container layout\n };\n \n try {\n const layoutedGraph = await elk.layout(elkGraph);\n \n // Apply the new positions back to containers\n return containerNodes.map(container => {\n const elkContainer = layoutedGraph.children?.find(c => c.id === container.id);\n if (elkContainer) {\n // Only update position if this was the changed container OR if no specific container was changed\n if (!changedContainerId || container.id === changedContainerId) {\n return {\n ...container,\n position: {\n x: elkContainer.x || container.position.x,\n y: elkContainer.y || container.position.y\n }\n };\n } else {\n // Keep the original position for unchanged containers\n return container;\n }\n }\n return container;\n });\n } catch (error) {\n console.error(\'Container layout with ELK failed:\', error);\n return containerNodes; // Fallback to original positions\n }\n };\n\n\n // Helper function to determine if graph should be auto-collapsed\n const shouldAutoCollapse = (locationContainers) => {\n if (locationContainers.length === 0) return false;\n \n const minLegibleZoom = 0.5;\n const graphWidth = Math.max(...locationContainers.map(c => c.position.x + parseFloat(c.style.width.toString().replace(\'px\', \'\')) || 400)) - \n Math.min(...locationContainers.map(c => c.position.x));\n const graphHeight = Math.max(...locationContainers.map(c => c.position.y + parseFloat(c.style.height.toString().replace(\'px\', \'\')) || 300)) - \n Math.min(...locationContainers.map(c => c.position.y));\n \n const wouldBeZoomX = (window.innerWidth * 0.85) / graphWidth;\n const wouldBeZoomY = (window.innerHeight * 0.85) / graphHeight;\n const wouldBeZoom = Math.min(wouldBeZoomX, wouldBeZoomY);\n \n return wouldBeZoom < minLegibleZoom;\n };\n\n // Greedy expansion algorithm for small graphs\n const determineInitialContainerStates = (allContainers, locationNodes) => {\n const minLegibleZoom = 0.4; // Relaxed threshold to allow more expansion\n const viewWidth = window.innerWidth * 0.85;\n const viewHeight = window.innerHeight * 0.85;\n \n // Create container info with areas for sorting\n const containerInfo = allContainers.map(container => {\n const locationId = container.id.replace(\'container_\', \'\');\n const nodes = locationNodes[locationId] || [];\n \n // Calculate area based on number of nodes and their layout\n const nodeCount = nodes.length;\n const estimatedArea = nodeCount * 200 * 60; // rough node area estimate\n \n const expandedWidth = parseFloat(container.style.width.toString().replace(\'px\', \'\')) || 400;\n const expandedHeight = parseFloat(container.style.height.toString().replace(\'px\', \'\')) || 300;\n \n return {\n container,\n locationId,\n nodeCount,\n estimatedArea,\n expandedWidth,\n expandedHeight,\n collapsedWidth: getMinCollapsedWidth(container.data.label),\n collapsedHeight: 50\n };\n });\n \n // Sort by area (smallest first for greedy expansion)\n containerInfo.sort((a, b) => a.estimatedArea - b.estimatedArea);\n \n const finalStates = {};\n const expandedContainers = [];\n \n // Greedy expansion algorithm\n for (const info of containerInfo) {\n // Create a test scenario where this container is expanded\n const testContainers = allContainers.map(c => {\n const cInfo = containerInfo.find(ci => ci.container.id === c.id);\n const isExpanded = expandedContainers.includes(cInfo.locationId) || \n cInfo.locationId === info.locationId;\n \n return {\n ...c,\n style: {\n ...c.style,\n width: isExpanded ? cInfo.expandedWidth : cInfo.collapsedWidth,\n height: isExpanded ? cInfo.expandedHeight : cInfo.collapsedHeight\n }\n };\n });\n \n // Calculate bounds with this expansion\n const bounds = testContainers.reduce((acc, c) => {\n const width = parseFloat(c.style.width.toString().replace(\'px\', \'\')) || 400;\n const height = parseFloat(c.style.height.toString().replace(\'px\', \'\')) || 300;\n const right = c.position.x + width;\n const bottom = c.position.y + height;\n \n return {\n minX: Math.min(acc.minX, c.position.x),\n minY: Math.min(acc.minY, c.position.y),\n maxX: Math.max(acc.maxX, right),\n maxY: Math.max(acc.maxY, bottom)\n };\n }, {\n minX: testContainers[0]?.position.x || 0,\n minY: testContainers[0]?.position.y || 0,\n maxX: testContainers[0]?.position.x || 0,\n maxY: testContainers[0]?.position.y || 0\n });\n \n const graphWidth = bounds.maxX - bounds.minX;\n const graphHeight = bounds.maxY - bounds.minY;\n \n const wouldBeZoomX = viewWidth / graphWidth;\n const wouldBeZoomY = viewHeight / graphHeight;\n const wouldBeZoom = Math.min(wouldBeZoomX, wouldBeZoomY);\n \n if (wouldBeZoom >= minLegibleZoom) {\n // Safe to expand this container\n expandedContainers.push(info.locationId);\n finalStates[info.locationId] = false; // false = expanded\n } else {\n // Would make zoom too small, keep collapsed\n finalStates[info.locationId] = true; // true = collapsed\n }\n }\n \n return finalStates;\n };\n\n function UnifiedLegend({ palette }) {\n const nodeTypes = [\'Source\', \'Transform\', \'Join\', \'Aggregation\', \'Network\', \'Sink\', \'Tee\'];\n const locations = graphData.locations || [];\n\n return (\n <div className=\"unified-legend\">\n <div className=\"legend-section\">\n <h4>Node Types</h4>\n {nodeTypes.map(type => {\n const colors = generateNodeColors(type, palette);\n return (\n <div key={type} className=\"legend-item\">\n <div className=\"legend-color\" style={{ background: colors.gradient, borderColor: colors.border }}></div>\n {type}\n </div>\n );\n })}\n </div>\n {locations.length > 0 && (\n <div className=\"legend-section\">\n <h4>Locations</h4>\n {locations.map(location => {\n const backgroundColor = generateLocationColor(location.id, locations.length, palette);\n const borderColor = generateLocationBorderColor(location.id, locations.length, palette);\n return (\n <div key={location.id} className=\"legend-item\">\n <div className=\"location-legend-color\" style={{ backgroundColor, borderColor }}></div>\n {location.label}\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n }\n \n function HydroGraph() {\n const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);\n const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);\n const [currentLayout, setCurrentLayout] = React.useState(\'mrtree\');\n const [currentPalette, setCurrentPalette] = React.useState(\'Set3\');\n const [useShortLabels, setUseShortLabels] = React.useState(true);\n const [allContainersCollapsed, setAllContainersCollapsed] = React.useState(false);\n \n const [collapsedLocations, setCollapsedLocations] = React.useState({});\n const [originalNodeDimensions, setOriginalNodeDimensions] = React.useState({});\n const [lastChangedContainer, setLastChangedContainer] = React.useState(null);\n const [hyperedges, setHyperedges] = React.useState([]);\n\n const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []);\n \n const fitView = useCallback(() => {\n const reactFlowInstance = window.reactFlowInstance;\n if (reactFlowInstance) {\n // Check if most containers are currently collapsed\n const currentNodes = reactFlowInstance.getNodes();\n const containerNodes = currentNodes.filter(node => node.data?.isContainer);\n const collapsedContainers = containerNodes.filter(node => {\n const width = parseFloat(node.style?.width?.toString().replace(\'px\', \'\')) || 400;\n // A container is considered collapsed if it\'s smaller than the normal expanded size\n // Collapsed containers are typically 200-300px wide, expanded are 400px+\n return width <= 350; \n });\n \n const mostlyCollapsed = collapsedContainers.length > containerNodes.length / 2;\n \n if (mostlyCollapsed) {\n // Most containers collapsed - use wider zoom range\n reactFlowInstance.fitView({ \n padding: 0.2, \n maxZoom: 0.5, \n minZoom: 0.02 \n });\n } else {\n // Mostly expanded - use closer zoom\n reactFlowInstance.fitView({ \n padding: 0.1, \n maxZoom: 1.0, \n minZoom: 0.2 \n });\n }\n }\n }, []);\n\n const toggleAllContainers = useCallback(() => {\n const reactFlowInstance = window.reactFlowInstance;\n if (!reactFlowInstance) return;\n \n const currentNodes = reactFlowInstance.getNodes();\n const containerNodes = currentNodes.filter(node => node.data?.isContainer);\n const newCollapsedState = {};\n const newOriginalDimensions = {};\n \n const shouldCollapse = !allContainersCollapsed;\n \n if (shouldCollapse) {\n // Collapse all containers\n containerNodes.forEach(container => {\n const locationId = container.id.replace(\'container_\', \'\');\n \n // Store original dimensions if not already stored\n if (!originalNodeDimensions[locationId]) {\n const width = parseFloat(container.style?.width?.toString().replace(\'px\', \'\')) || 400;\n const height = parseFloat(container.style?.height?.toString().replace(\'px\', \'\')) || 300;\n newOriginalDimensions[locationId] = { width, height };\n } else {\n newOriginalDimensions[locationId] = originalNodeDimensions[locationId];\n }\n \n newCollapsedState[locationId] = true;\n });\n \n const updatedNodes = currentNodes.map(node => {\n if (node.data?.isContainer) {\n // Store original label and set truncated label for collapsed state\n const originalLabel = node.data.originalLabel || node.data.label;\n const collapsedWidth = getMinCollapsedWidth(originalLabel);\n return { \n ...node, \n style: { ...node.style, width: collapsedWidth + \'px\', height: \'50px\' },\n data: {\n ...node.data,\n originalLabel: originalLabel,\n label: leftTruncateRustPath(originalLabel, true)\n }\n };\n } else if (node.parentNode) {\n return { ...node, hidden: true };\n }\n return node;\n });\n \n setNodes(updatedNodes);\n \n // Fit view after collapse\n setTimeout(() => {\n reactFlowInstance.fitView({ padding: 0.2, maxZoom: 0.5, minZoom: 0.02 });\n }, 100);\n } else {\n // Expand all containers\n containerNodes.forEach(container => {\n const locationId = container.id.replace(\'container_\', \'\');\n newCollapsedState[locationId] = false;\n \n // Keep existing original dimensions\n if (originalNodeDimensions[locationId]) {\n newOriginalDimensions[locationId] = originalNodeDimensions[locationId];\n }\n });\n \n const updatedNodes = currentNodes.map(node => {\n if (node.data?.isContainer) {\n const locationId = node.id.replace(\'container_\', \'\');\n const originalDims = originalNodeDimensions[locationId];\n const originalLabel = node.data.originalLabel || node.data.label;\n \n if (originalDims) {\n return { \n ...node, \n style: { ...node.style, width: originalDims.width, height: originalDims.height },\n data: { \n ...node.data, \n label: originalLabel\n }\n };\n } else {\n // Default expanded size if no stored dimensions\n return { \n ...node, \n style: { ...node.style, width: 400, height: 300 },\n data: { \n ...node.data, \n label: originalLabel\n }\n };\n }\n } else if (node.parentNode) {\n return { ...node, hidden: false };\n }\n return node;\n });\n \n setNodes(updatedNodes);\n \n // Fit view after expand\n setTimeout(() => {\n reactFlowInstance.fitView({ padding: 0.1, maxZoom: 1.0, minZoom: 0.2 });\n }, 100);\n }\n \n // Update states\n setCollapsedLocations(newCollapsedState);\n setOriginalNodeDimensions(prev => ({ ...prev, ...newOriginalDimensions }));\n setAllContainersCollapsed(shouldCollapse);\n }, [allContainersCollapsed, originalNodeDimensions, setCollapsedLocations, setOriginalNodeDimensions, setNodes]);\n\n const onNodeClick = useCallback((event, node) => {\n if (node.data?.isContainer) {\n const locationId = node.id.replace(\'container_\', \'\');\n \n // Track which container is being changed\n setLastChangedContainer(node.id);\n \n setCollapsedLocations(prev => {\n const isCollapsing = !prev[locationId];\n if (isCollapsing) {\n setOriginalNodeDimensions(dims => ({\n ...dims,\n [locationId]: { width: node.style.width, height: node.style.height }\n }));\n }\n \n const newState = { ...prev, [locationId]: isCollapsing };\n \n // Update allContainersCollapsed state\n setTimeout(() => {\n const allCollapsed = Object.values(newState).every(collapsed => collapsed);\n setAllContainersCollapsed(allCollapsed);\n }, 0);\n \n return newState;\n });\n } else if (node.data?.shortLabel && node.data?.fullLabel) {\n // Handle regular node click to toggle between short and full label\n setNodes(currentNodes => {\n return currentNodes.map(n => {\n if (n.id === node.id) {\n const isCurrentlyExpanded = n.data.expanded || false;\n const newLabel = isCurrentlyExpanded ? n.data.shortLabel : n.data.fullLabel;\n \n return {\n ...n,\n data: {\n ...n.data,\n label: newLabel,\n expanded: !isCurrentlyExpanded\n }\n };\n }\n return n;\n });\n });\n }\n }, [collapsedLocations, setNodes]);\n\n React.useEffect(() => {\n // Update node visibility and container sizes based on collapsed state\n setNodes(currentNodes => {\n const updatedNodes = currentNodes.map(n => {\n if (n.data?.isContainer) {\n const locationId = n.id.replace(\'container_\', \'\');\n const isCollapsed = collapsedLocations[locationId];\n if (typeof isCollapsed !== \'boolean\') return n;\n\n // Get the original label for this container\n let originalLabel = n.data.label;\n \n // If this container has a stored original label, use that\n if (n.data.originalLabel) {\n originalLabel = n.data.originalLabel;\n }\n\n if (isCollapsed) {\n return { \n ...n, \n style: { \n ...n.style, \n width: getMinCollapsedWidth(originalLabel), \n height: 50 \n },\n data: { \n ...n.data, \n originalLabel: originalLabel, // Store original label\n label: leftTruncateRustPath(originalLabel, true) \n }\n };\n } else {\n // When expanding, use stored dimensions if available, otherwise use default size\n const originalDims = originalNodeDimensions[locationId];\n if (originalDims) {\n return { \n ...n, \n style: { ...n.style, width: originalDims.width, height: originalDims.height },\n data: { \n ...n.data, \n label: originalLabel // Restore original label\n }\n };\n } else {\n // Default expanded size if no stored dimensions\n return { \n ...n, \n style: { ...n.style, width: 400, height: 300 },\n data: { \n ...n.data, \n label: originalLabel // Restore original label\n }\n };\n }\n }\n } else if (n.parentNode) {\n const parentLocationId = n.parentNode.replace(\'container_\', \'\');\n const isParentCollapsed = collapsedLocations[parentLocationId];\n if (typeof isParentCollapsed === \'boolean\') {\n return { ...n, hidden: isParentCollapsed };\n }\n }\n return n;\n });\n\n // Update edges using the shared routing function\n setEdges(currentEdges => {\n const childNodeIdsByParent = {};\n updatedNodes.forEach(n => {\n if (n.parentNode) {\n if (!childNodeIdsByParent[n.parentNode]) {\n childNodeIdsByParent[n.parentNode] = new Set();\n }\n childNodeIdsByParent[n.parentNode].add(n.id);\n }\n });\n\n return routeEdgesForCollapsedContainers(currentEdges, collapsedLocations, childNodeIdsByParent);\n });\n\n return updatedNodes;\n });\n \n // STEP 2: Use ELK to intelligently reposition containers after expand/collapse\n setTimeout(async () => {\n const reactFlowInstance = window.reactFlowInstance;\n if (reactFlowInstance) {\n try {\n const currentNodes = reactFlowInstance.getNodes();\n const containerNodes = currentNodes.filter(node => node.data?.isContainer);\n \n if (containerNodes.length > 0) {\n // Use the tracked changed container ID for better layout preservation\n const changedContainerId = lastChangedContainer;\n \n // Reconstruct original internal nodes for hyperedge generation\n // Get all child nodes (both hidden and visible) and treat them as the internal nodes\n const internalNodes = currentNodes.filter(node => \n !node.data?.isContainer && node.parentNode\n );\n \n // Use original edges (not the routed container edges) for hyperedge generation\n const originalEdges = initialEdges;\n \n // Use ELK to layout containers while preserving positions of unchanged ones\n const layoutedContainers = await layoutContainersWithELK(containerNodes, internalNodes, originalEdges, changedContainerId, hyperedges);\n \n // Update all nodes with the new container positions\n const updatedNodes = currentNodes.map(node => {\n if (node.data?.isContainer) {\n const layoutedContainer = layoutedContainers.find(c => c.id === node.id);\n return layoutedContainer || node;\n }\n return node;\n });\n \n setNodes(updatedNodes);\n \n // Clear the changed container tracking\n setLastChangedContainer(null);\n \n // Re-fit view after repositioning\n setTimeout(() => {\n reactFlowInstance.fitView({ padding: 0.1, maxZoom: 1.0, minZoom: 0.01 });\n }, 100);\n }\n } catch (error) {\n console.error(\'Error re-layouting containers with ELK:\', error);\n }\n }\n }, 300);\n }, [collapsedLocations, originalNodeDimensions, lastChangedContainer]);\n \n // Apply node colors based on palette\n const applyNodePalette = useCallback((nodes, palette) => {\n return nodes.map(node => {\n if (node.data?.isContainer) return node;\n \n const nodeType = node.data?.nodeType || \'Transform\';\n const colors = generateNodeColors(nodeType, palette);\n \n return {\n ...node,\n style: {\n ...node.style,\n background: colors.gradient,\n border: `1px solid ${colors.border}`,\n // Remove the fixed gradients that were in the data\n \'--node-color-primary\': colors.primary,\n \'--node-color-secondary\': colors.secondary,\n \'--node-border-color\': colors.border,\n }\n };\n });\n }, []);\n \n const onInit = useCallback(async (reactFlowInstance) => {\n window.reactFlowInstance = reactFlowInstance;\n \n // Compute hyperedges once from the initial graph structure\n const computedHyperedges = generateHyperedges(initialNodes, initialEdges);\n setHyperedges(computedHyperedges);\n \n // Apply ELK layout to get proper positions\n const layoutedNodes = await applyElkLayout(initialNodes, initialEdges, currentLayout, computedHyperedges);\n \n // Group nodes by location for hierarchical display\n const locationContainers = [];\n const childNodes = [];\n \n if (graphData.locations && graphData.locations.length > 0) {\n graphData.locations.forEach(location => {\n // Find nodes in this location\n const locationNodes = layoutedNodes.filter(node => {\n const nodeLocationId = node.data?.locationId;\n return nodeLocationId !== null && \n nodeLocationId !== undefined && \n nodeLocationId.toString() === location.id.toString();\n });\n \n if (locationNodes.length > 0) {\n // Create container using helper function\n const container = createLocationContainer(location, locationNodes, currentPalette);\n locationContainers.push(container);\n \n // Create child nodes using helper function\n const children = createChildNodes(locationNodes, container.id, container.position);\n childNodes.push(...children);\n }\n });\n \n // Handle orphan nodes (not in any location) - group them into a grey container\n const orphanNodes = layoutedNodes.filter(node => {\n const nodeLocationId = node.data?.locationId;\n if (nodeLocationId === null || nodeLocationId === undefined) return true;\n \n return !graphData.locations.some(loc => \n loc.id.toString() === nodeLocationId.toString()\n );\n });\n \n if (orphanNodes.length > 0) {\n // Create orphan container using similar logic\n const orphanLocation = { id: \'null\', label: \'Internal/Unassigned\' };\n const orphanContainer = createLocationContainer(orphanLocation, orphanNodes, currentPalette);\n \n // Override styles for orphan container\n orphanContainer.id = \'container_null\';\n orphanContainer.style.backgroundColor = \'rgba(200, 200, 200, 0.2)\';\n orphanContainer.style.border = \'2px solid #999999\';\n \n locationContainers.push(orphanContainer);\n \n const orphanChildren = createChildNodes(orphanNodes, orphanContainer.id, orphanContainer.position);\n childNodes.push(...orphanChildren);\n }\n } else {\n // No locations defined, use all nodes as-is\n childNodes.push(...layoutedNodes);\n }\n \n \n // Apply palette colors to nodes\n const coloredChildNodes = applyNodePalette(childNodes, currentPalette);\n \n // Combine containers and child nodes\n const allElements = [...locationContainers, ...coloredChildNodes];\n \n // STEP 1: Use greedy expansion algorithm to determine initial container states\n const locationNodesByLocationId = {};\n graphData.locations?.forEach(location => {\n const locationNodes = layoutedNodes.filter(node => {\n const nodeLocationId = node.data?.locationId;\n return nodeLocationId !== null && \n nodeLocationId !== undefined && \n nodeLocationId.toString() === location.id.toString();\n });\n if (locationNodes.length > 0) {\n locationNodesByLocationId[location.id.toString()] = locationNodes;\n }\n });\n \n // Add orphan nodes to location mapping\n const orphanNodes = layoutedNodes.filter(node => {\n const nodeLocationId = node.data?.locationId;\n if (nodeLocationId === null || nodeLocationId === undefined) return true;\n return !graphData.locations.some(loc => \n loc.id.toString() === nodeLocationId.toString()\n );\n });\n if (orphanNodes.length > 0) {\n locationNodesByLocationId[\'null\'] = orphanNodes;\n }\n \n // Determine initial states using greedy expansion\n const initialCollapsedState = determineInitialContainerStates(locationContainers, locationNodesByLocationId);\n const initialOriginalDimensions = {};\n \n // Apply the determined states to containers\n locationContainers.forEach(container => {\n const width = parseFloat(container.style.width.toString().replace(\'px\', \'\')) || 400;\n const height = parseFloat(container.style.height.toString().replace(\'px\', \'\')) || 300;\n const locationId = container.id.replace(\'container_\', \'\');\n \n // Store original dimensions\n initialOriginalDimensions[locationId] = { width, height };\n \n const isCollapsed = initialCollapsedState[locationId];\n \n if (isCollapsed) {\n // Store original label and modify container to collapsed state with truncated label\n const originalLabel = container.data.label;\n const collapsedWidth = getMinCollapsedWidth(originalLabel);\n container.style.width = collapsedWidth + \'px\';\n container.style.height = \'50px\';\n container.data = {\n ...container.data,\n originalLabel: originalLabel,\n label: leftTruncateRustPath(originalLabel, true)\n };\n \n // Hide child nodes for collapsed containers\n allElements.forEach(element => {\n if (element.parentNode === container.id) {\n element.hidden = true;\n }\n });\n }\n // If not collapsed, leave container and children as-is (expanded)\n });\n \n // Set initial states\n setCollapsedLocations(initialCollapsedState);\n setOriginalNodeDimensions(initialOriginalDimensions);\n \n // Update allContainersCollapsed state based on initial container states\n const allCollapsed = Object.values(initialCollapsedState).every(collapsed => collapsed);\n setAllContainersCollapsed(allCollapsed);\n \n setNodes(allElements);\n setEdges(initialEdges);\n \n // Apply initial label state to ensure consistency with useShortLabels\n setTimeout(() => {\n setNodes(currentNodes => {\n return currentNodes.map(node => {\n // For container nodes, handle label toggling\n if (node.data?.isContainer) {\n const originalLabel = node.data.originalLabel || node.data.label;\n const newLabel = useShortLabels \n ? leftTruncateRustPath(originalLabel, true)\n : originalLabel;\n \n return {\n ...node,\n data: {\n ...node.data,\n label: newLabel,\n originalLabel: originalLabel\n }\n };\n }\n \n // For regular nodes, use shortLabel/fullLabel\n if (node.data?.shortLabel && node.data?.fullLabel) {\n return {\n ...node,\n data: {\n ...node.data,\n label: useShortLabels ? node.data.shortLabel : node.data.fullLabel\n }\n };\n }\n \n return node;\n });\n });\n }, 10); // Small delay to ensure nodes are set\n \n // Apply initial edge routing based on determined container states\n setTimeout(() => {\n setEdges(currentEdges => {\n const childNodeIdsByParent = {};\n allElements.forEach(n => {\n if (n.parentNode) {\n if (!childNodeIdsByParent[n.parentNode]) {\n childNodeIdsByParent[n.parentNode] = new Set();\n }\n childNodeIdsByParent[n.parentNode].add(n.id);\n }\n });\n return routeEdgesForCollapsedContainers(currentEdges, initialCollapsedState, childNodeIdsByParent);\n });\n }, 50);\n \n // Adaptive fit view based on whether containers are mostly expanded or collapsed\n const expandedCount = Object.values(initialCollapsedState).filter(collapsed => !collapsed).length;\n const totalCount = Object.keys(initialCollapsedState).length;\n const mostlyExpanded = expandedCount > totalCount / 2;\n \n if (mostlyExpanded) {\n // Most containers expanded - use closer zoom for details\n reactFlowInstance.fitView({ padding: 0.1, maxZoom: 1.0, minZoom: 0.2, duration: 300 });\n } else {\n // Most containers collapsed - use wider zoom\n reactFlowInstance.fitView({ padding: 0.2, maxZoom: 0.5, minZoom: 0.02, duration: 300 });\n }\n }, [setNodes, setEdges, currentLayout, currentPalette, applyNodePalette, setHyperedges, useShortLabels]);\n\n // Apply elk layout with selected algorithm\n const applyLayout = useCallback(async (layoutType = currentLayout) => {\n // Only layout the actual graph nodes, not containers\n const actualNodes = nodes.filter(node => !node.data?.isContainer);\n \n if (actualNodes.length === 0) return;\n \n const layoutedNodes = await applyElkLayout(actualNodes, edges, layoutType, hyperedges);\n \n // Create new containers and child relationships using helper functions\n const locationContainers = [];\n const childNodes = [];\n \n if (graphData.locations && graphData.locations.length > 0) {\n graphData.locations.forEach(location => {\n const locationNodes = layoutedNodes.filter(node => {\n const nodeLocationId = node.data?.locationId;\n return nodeLocationId !== null && \n nodeLocationId !== undefined && \n nodeLocationId.toString() === location.id.toString();\n });\n \n if (locationNodes.length > 0) {\n const container = createLocationContainer(location, locationNodes, currentPalette);\n \n // Check if this container is currently collapsed\n const locationId = location.id.toString();\n const isCollapsed = collapsedLocations[locationId];\n \n // ALWAYS update the original dimensions with the new layout\'s correct size\n // This ensures that when collapsed containers are later expanded, they use\n // the correct dimensions for the current layout, not the old layout\n setOriginalNodeDimensions(prev => ({\n ...prev,\n [locationId]: { \n width: container.style.width, \n height: container.style.height \n }\n }));\n \n if (isCollapsed) {\n // If collapsed, preserve the collapsed dimensions and label for display\n const existingContainer = nodes.find(n => n.id === container.id);\n if (existingContainer) {\n container.style.width = existingContainer.style.width;\n container.style.height = existingContainer.style.height;\n container.data = {\n ...container.data,\n originalLabel: existingContainer.data.originalLabel || container.data.label,\n label: existingContainer.data.label\n };\n } else {\n // Fallback to standard collapsed sizing\n const originalLabel = container.data.label;\n const collapsedWidth = getMinCollapsedWidth(originalLabel);\n container.style.width = collapsedWidth + \'px\';\n container.style.height = \'50px\';\n container.data = {\n ...container.data,\n originalLabel: originalLabel,\n label: leftTruncateRustPath(originalLabel, true)\n };\n }\n }\n \n locationContainers.push(container);\n \n const children = createChildNodes(locationNodes, container.id, container.position);\n // Hide children if container is collapsed\n const visibleChildren = children.map(child => ({\n ...child,\n hidden: isCollapsed\n }));\n childNodes.push(...visibleChildren);\n }\n });\n \n // Handle orphan nodes\n const orphanNodes = layoutedNodes.filter(node => {\n const nodeLocationId = node.data?.locationId;\n if (nodeLocationId === null || nodeLocationId === undefined) return true;\n return !graphData.locations.some(loc => \n loc.id.toString() === nodeLocationId.toString()\n );\n });\n \n if (orphanNodes.length > 0) {\n const orphanLocation = { id: \'null\', label: \'Internal/Unassigned\' };\n const orphanContainer = createLocationContainer(orphanLocation, orphanNodes, currentPalette);\n \n // Override styles for orphan container\n orphanContainer.id = \'container_null\';\n orphanContainer.style.backgroundColor = \'rgba(200, 200, 200, 0.2)\';\n orphanContainer.style.border = \'2px solid #999999\';\n \n // ALWAYS update the original dimensions for the orphan container\n // This ensures correct sizing when expanded after layout changes\n setOriginalNodeDimensions(prev => ({\n ...prev,\n \'null\': { \n width: orphanContainer.style.width, \n height: orphanContainer.style.height \n }\n }));\n \n // Check if orphan container is collapsed\n const isOrphanCollapsed = collapsedLocations[\'null\'];\n if (isOrphanCollapsed) {\n const existingOrphanContainer = nodes.find(n => n.id === \'container_null\');\n if (existingOrphanContainer) {\n orphanContainer.style.width = existingOrphanContainer.style.width;\n orphanContainer.style.height = existingOrphanContainer.style.height;\n orphanContainer.data = {\n ...orphanContainer.data,\n originalLabel: existingOrphanContainer.data.originalLabel || orphanContainer.data.label,\n label: existingOrphanContainer.data.label\n };\n } else {\n const originalLabel = orphanContainer.data.label;\n const collapsedWidth = getMinCollapsedWidth(originalLabel);\n orphanContainer.style.width = collapsedWidth + \'px\';\n orphanContainer.style.height = \'50px\';\n orphanContainer.data = {\n ...orphanContainer.data,\n originalLabel: originalLabel,\n label: leftTruncateRustPath(originalLabel, true)\n };\n }\n }\n \n locationContainers.push(orphanContainer);\n \n const orphanChildren = createChildNodes(orphanNodes, orphanContainer.id, orphanContainer.position);\n // Hide orphan children if container is collapsed\n const visibleOrphanChildren = orphanChildren.map(child => ({\n ...child,\n hidden: isOrphanCollapsed\n }));\n childNodes.push(...visibleOrphanChildren);\n }\n } else {\n childNodes.push(...layoutedNodes);\n }\n \n // Apply palette colors and update nodes\n const coloredChildNodes = applyNodePalette(childNodes, currentPalette);\n \n // Apply global label state to all nodes\n const labelAdjustedNodes = coloredChildNodes.map(node => {\n if (node.data?.shortLabel && node.data?.fullLabel) {\n return {\n ...node,\n data: {\n ...node.data,\n label: useShortLabels ? node.data.shortLabel : node.data.fullLabel\n }\n };\n }\n return node;\n });\n \n // Also apply label state to containers\n const labelAdjustedContainers = locationContainers.map(container => {\n if (container.data?.isContainer) {\n const originalLabel = container.data.originalLabel || container.data.label;\n const newLabel = useShortLabels \n ? leftTruncateRustPath(originalLabel, true)\n : originalLabel;\n \n return {\n ...container,\n data: {\n ...container.data,\n label: newLabel,\n originalLabel: originalLabel\n }\n };\n }\n return container;\n });\n \n const allElements = [...labelAdjustedContainers, ...labelAdjustedNodes];\n setNodes(allElements);\n \n // Update edges to respect current collapsed state\n setTimeout(() => {\n setEdges(currentEdges => {\n const childNodeIdsByParent = {};\n allElements.forEach(n => {\n if (n.parentNode) {\n if (!childNodeIdsByParent[n.parentNode]) {\n childNodeIdsByParent[n.parentNode] = new Set();\n }\n childNodeIdsByParent[n.parentNode].add(n.id);\n }\n });\n return routeEdgesForCollapsedContainers(currentEdges, collapsedLocations, childNodeIdsByParent);\n });\n }, 50);\n \n // Fit view after layout change\n setTimeout(() => {\n const reactFlowInstance = window.reactFlowInstance;\n if (reactFlowInstance) {\n reactFlowInstance.fitView({ padding: 0.1, maxZoom: 1.0, minZoom: 0.01 });\n }\n }, 100);\n }, [nodes, edges, currentLayout, currentPalette, collapsedLocations, setNodes, setOriginalNodeDimensions, applyNodePalette, hyperedges, useShortLabels]);\n\n const toggleLabelMode = useCallback(() => {\n setUseShortLabels(prev => {\n const newUseShortLabels = !prev;\n \n // Update all nodes with appropriate labels\n setNodes(currentNodes => {\n return currentNodes.map(node => {\n // For container nodes, handle label toggling\n if (node.data?.isContainer) {\n const originalLabel = node.data.originalLabel || node.data.label;\n const newLabel = newUseShortLabels \n ? leftTruncateRustPath(originalLabel, true)\n : originalLabel;\n \n return {\n ...node,\n data: {\n ...node.data,\n label: newLabel,\n originalLabel: originalLabel\n }\n };\n }\n \n // For regular nodes, use shortLabel/fullLabel\n if (node.data?.shortLabel && node.data?.fullLabel) {\n return {\n ...node,\n data: {\n ...node.data,\n label: newUseShortLabels ? node.data.shortLabel : node.data.fullLabel\n }\n };\n }\n \n return node;\n });\n });\n \n return newUseShortLabels;\n });\n }, [setNodes]);\n\n const onLayoutChange = (event) => {\n const newLayout = event.target.value;\n setCurrentLayout(newLayout);\n applyLayout(newLayout);\n };\n\n const onPaletteChange = (event) => {\n const newPalette = event.target.value;\n setCurrentPalette(newPalette);\n \n // Re-color nodes\n const recoloredNodes = applyNodePalette(nodes, newPalette);\n \n // Re-color location containers\n const finalNodes = recoloredNodes.map(node => {\n if (node.data?.isContainer) {\n const locationId = node.id.split(\'_\')[1];\n if (locationId === \'null\') {\n return {\n ...node,\n style: {\n ...node.style,\n backgroundColor: \'rgba(200, 200, 200, 0.2)\',\n border: \'2px solid #999999\',\n }\n };\n }\n \n const loc = graphData.locations.find(l => l.id.toString() === locationId);\n if (loc) {\n const backgroundColor = generateLocationColor(loc.id, graphData.locations.length, newPalette);\n const borderColor = generateLocationBorderColor(loc.id, graphData.locations.length, newPalette);\n return {\n ...node,\n style: {\n ...node.style,\n backgroundColor: backgroundColor,\n border: `2px solid ${borderColor}`,\n }\n };\n }\n }\n return node;\n });\n \n setNodes(finalNodes);\n \n // Re-color edges\n const recoloredEdges = edges.map(edge => {\n const sourceNode = nodes.find(n => n.id === edge.source);\n if (sourceNode) {\n const nodeType = sourceNode.data?.nodeType || \'Transform\';\n const colors = generateNodeColors(nodeType, newPalette);\n return {\n ...edge,\n style: { ...edge.style, stroke: colors.border },\n markerEnd: { ...edge.markerEnd, color: colors.border }\n };\n }\n return edge;\n });\n setEdges(recoloredEdges);\n };\n\n return (\n <div className=\"reactflow-wrapper\">\n <div className=\"layout-controls\">\n <select className=\"layout-select\" value={currentLayout} onChange={onLayoutChange}>\n {Object.keys(elkLayouts).map(key => (\n <option key={key} value={key}>{key.charAt(0).toUpperCase() + key.slice(1)}</option>\n ))}\n </select>\n <select className=\"palette-select\" value={currentPalette} onChange={onPaletteChange}>\n {Object.keys(colorPalettes).map(key => (\n <option key={key} value={key}>{key}</option>\n ))}\n </select>\n <button className=\"icon-button\" onClick={() => applyLayout(currentLayout)}>\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z\"/>\n <path d=\"M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z\"/>\n </svg>\n <span className=\"tooltip\">Refresh Layout</span>\n </button>\n <button className=\"icon-button\" onClick={fitView}>\n <span style={{fontSize: \'16px\', fontWeight: \'bold\', display: \'flex\', alignItems: \'center\', justifyContent: \'center\'}}>\n \u{26f6}\n </span>\n <span className=\"tooltip\">Fit to View</span>\n </button>\n <button className=\"icon-button\" onClick={toggleAllContainers}>\n <span style={{fontSize: \'16px\', fontWeight: \'bold\', display: \'flex\', alignItems: \'center\', justifyContent: \'center\'}}>\n {allContainersCollapsed ? \'\u{2295}\' : \'\u{2296}\'}\n </span>\n <span className=\"tooltip\">{allContainersCollapsed ? \'Expand All Containers\' : \'Collapse All Containers\'}</span>\n </button>\n <button className=\"icon-button\" onClick={toggleLabelMode}>\n <span style={{fontSize: \'14px\', fontWeight: \'bold\'}}>\n {useShortLabels ? \'\u{2194}\' : \'\u{2192}\u{2190}\'}\n </span>\n <span className=\"tooltip\">{useShortLabels ? \'Show Full Labels\' : \'Show Short Labels\'}</span>\n </button>\n </div>\n <UnifiedLegend palette={currentPalette} />\n <ReactFlow\n nodes={nodes}\n edges={edges}\n onNodesChange={onNodesChange}\n onEdgesChange={onEdgesChange}\n onConnect={onConnect}\n onInit={onInit}\n onNodeClick={onNodeClick}\n fitView\n attributionPosition=\"bottom-left\"\n >\n <Controls />\n <MiniMap />\n <Background />\n </ReactFlow>\n </div>\n );\n }\n\n ReactDOM.render(<HydroGraph />, document.getElementById(\'root\'));\n </script>\n</body>\n</html>\n";
Expand description
HTML template for ReactFlow visualization