REACTFLOW_TEMPLATE

Constant REACTFLOW_TEMPLATE 

Source
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