|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8"> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | + <title>{html_page_title}</title> |
| 7 | + <script type="text/javascript" src="{cdn_js_url}"></script> |
| 8 | + <link href="{cdn_css_url}" rel="stylesheet" type="text/css" /> |
| 9 | + <style type="text/css"> |
| 10 | + body, html {{ |
| 11 | + margin: 0; |
| 12 | + padding: 0; |
| 13 | + width: 100%; |
| 14 | + height: 100%; |
| 15 | + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; |
| 16 | + background-color: #f4f6f8; /* Light background for the page */ |
| 17 | + display: flex; |
| 18 | + flex-direction: column; |
| 19 | + overflow: hidden; /* Prevent body scrollbars */ |
| 20 | + }} |
| 21 | + |
| 22 | + .config-panel-wrapper {{ |
| 23 | + width: 100%; |
| 24 | + background-color: #ffffff; |
| 25 | + box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| 26 | + z-index: 10; /* Ensure it's above the graph if any overlap issues */ |
| 27 | + flex-shrink: 0; /* Prevent panel from shrinking */ |
| 28 | + }} |
| 29 | + |
| 30 | + .config-panel-header {{ |
| 31 | + display: flex; |
| 32 | + justify-content: space-between; |
| 33 | + align-items: center; |
| 34 | + padding: 10px 15px; |
| 35 | + border-bottom: 1px solid #e0e0e0; |
| 36 | + cursor: pointer; |
| 37 | + background-color: #f9f9f9; |
| 38 | + }} |
| 39 | + |
| 40 | + .config-panel-header h3 {{ |
| 41 | + margin: 0; |
| 42 | + font-size: 16px; |
| 43 | + font-weight: 600; |
| 44 | + color: #333; |
| 45 | + }} |
| 46 | + |
| 47 | + .config-toggle-btn {{ |
| 48 | + background: none; |
| 49 | + border: none; |
| 50 | + font-size: 18px; |
| 51 | + cursor: pointer; |
| 52 | + padding: 5px; |
| 53 | + color: #555; |
| 54 | + }} |
| 55 | + .config-toggle-btn::after {{ |
| 56 | + content: '\25BC'; /* Down arrow ▼ */ |
| 57 | + display: inline-block; |
| 58 | + transition: transform 0.2s ease-in-out; |
| 59 | + }} |
| 60 | + .collapsed .config-toggle-btn::after {{ |
| 61 | + transform: rotate(-90deg); /* Right arrow for collapsed state */ |
| 62 | + }} |
| 63 | + |
| 64 | + #config-container-content-{div_id_suffix} {{ |
| 65 | + max-height: 40vh; /* Default expanded max height */ |
| 66 | + overflow-y: auto; |
| 67 | + padding: 15px; |
| 68 | + box-sizing: border-box; |
| 69 | + background-color: #ffffff; |
| 70 | + transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; |
| 71 | + }} |
| 72 | + |
| 73 | + .collapsed #config-container-content-{div_id_suffix} {{ |
| 74 | + max-height: 0; |
| 75 | + padding-top: 0; |
| 76 | + padding-bottom: 0; |
| 77 | + overflow: hidden; |
| 78 | + border-bottom: none; /* Hide border when collapsed */ |
| 79 | + }} |
| 80 | + |
| 81 | + #mynetwork-{div_id_suffix} {{ |
| 82 | + width: 100%; |
| 83 | + flex-grow: 1; /* Graph takes remaining vertical space */ |
| 84 | + min-height: 0; /* Important for flex children to shrink */ |
| 85 | + background-color: #ffffff; /* Graph background */ |
| 86 | + }} |
| 87 | + |
| 88 | + .filter-panel {{ |
| 89 | + display: flex; |
| 90 | + align-items: center; |
| 91 | + gap: 10px; |
| 92 | + padding: 8px 15px; |
| 93 | + background-color: #e9ecef; |
| 94 | + border-bottom: 1px solid #dee2e6; |
| 95 | + flex-shrink: 0; |
| 96 | + }} |
| 97 | + .filter-panel label {{ |
| 98 | + font-size: 13px; |
| 99 | + font-weight: 500; |
| 100 | + color: #495057; |
| 101 | + }} |
| 102 | + .filter-panel input[type=text] {{ |
| 103 | + flex-grow: 1; |
| 104 | + padding: 6px 8px; |
| 105 | + border: 1px solid #ced4da; |
| 106 | + border-radius: 4px; |
| 107 | + font-size: 13px; |
| 108 | + }} |
| 109 | + .filter-panel button {{ |
| 110 | + padding: 6px 12px; |
| 111 | + font-size: 13px; |
| 112 | + border-radius: 4px; |
| 113 | + border: 1px solid #6c757d; |
| 114 | + background-color: #6c757d; |
| 115 | + color: white; |
| 116 | + cursor: pointer; |
| 117 | + }} |
| 118 | + .filter-panel button:hover {{ |
| 119 | + background-color: #5a6268; |
| 120 | + }} |
| 121 | + |
| 122 | + .info-panel {{ |
| 123 | + padding: 8px 15px; |
| 124 | + background-color: #f8f9fa; |
| 125 | + font-size: 13px; |
| 126 | + color: #495057; |
| 127 | + text-align: center; |
| 128 | + border-bottom: 1px solid #dee2e6; |
| 129 | + flex-shrink: 0; |
| 130 | + }} |
| 131 | + |
| 132 | + /* Basic styling for vis.js config elements to blend better */ |
| 133 | + div.vis-configuration-wrapper {{ padding: 0; }} |
| 134 | + div.vis-configuration-wrapper table {{ width: 100%; }} |
| 135 | + div.vis-configuration-wrapper table tr td:first-child {{ width: 30%; font-size: 13px; }} |
| 136 | + div.vis-configuration-wrapper input[type=text], |
| 137 | + div.vis-configuration-wrapper select {{ width: 95%; padding: 6px; margin: 2px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 13px; }} |
| 138 | + div.vis-configuration-wrapper input[type=range] {{ width: 60%; }} |
| 139 | + div.vis-configuration-wrapper .vis-label {{ font-size: 13px; color: #333; }} |
| 140 | + |
| 141 | + </style> |
| 142 | +</head> |
| 143 | +<body> |
| 144 | + <div class="config-panel-wrapper" id="config-panel-wrapper-{div_id_suffix}"> |
| 145 | + <div class="config-panel-header" id="config-panel-header-{div_id_suffix}" role="button" tabindex="0" aria-expanded="true" aria-controls="config-container-content-{div_id_suffix}"> |
| 146 | + <h3>Configuration</h3> |
| 147 | + <button class="config-toggle-btn" id="config-toggle-btn-{div_id_suffix}" aria-label="Toggle configuration panel"></button> |
| 148 | + </div> |
| 149 | + <div id="config-container-content-{div_id_suffix}"> |
| 150 | + <!-- Vis.js configuration UI will be injected here --> |
| 151 | + </div> |
| 152 | + </div> |
| 153 | + |
| 154 | + <div class="filter-panel"> |
| 155 | + <label for="search-input-{div_id_suffix}">Filter nodes (regex):</label> |
| 156 | + <input type="text" id="search-input-{div_id_suffix}" placeholder="e.g., ^IMP_ or ELEC$"> |
| 157 | + <button id="clear-btn-{div_id_suffix}">Clear</button> |
| 158 | + </div> |
| 159 | + |
| 160 | + <div class="info-panel"> |
| 161 | + <strong>Tip:</strong> Click a node to isolate it and its neighbors. Click the background to reset the view. |
| 162 | + </div> |
| 163 | + |
| 164 | + <div id="mynetwork-{div_id_suffix}"></div> |
| 165 | + |
| 166 | + <script type="text/javascript"> |
| 167 | + (function() {{ |
| 168 | + // These arrays are the master dataset |
| 169 | + var allNodesArray = {nodes_json_str}; |
| 170 | + var allEdgesArray = {edges_json_str}; |
| 171 | + var optionsObject = {options_json_str}; |
| 172 | + |
| 173 | + // Get DOM elements |
| 174 | + var configWrapper = document.getElementById('config-panel-wrapper-{div_id_suffix}'); |
| 175 | + var configHeader = document.getElementById('config-panel-header-{div_id_suffix}'); |
| 176 | + var configContent = document.getElementById('config-container-content-{div_id_suffix}'); |
| 177 | + var toggleButton = document.getElementById('config-toggle-btn-{div_id_suffix}'); |
| 178 | + var searchInput = document.getElementById('search-input-{div_id_suffix}'); |
| 179 | + var clearButton = document.getElementById('clear-btn-{div_id_suffix}'); |
| 180 | + |
| 181 | + // Handle config panel toggle |
| 182 | + if (optionsObject.configure && optionsObject.configure.enabled) {{ |
| 183 | + if (!optionsObject.configure.container) {{ |
| 184 | + optionsObject.configure.container = configContent; |
| 185 | + }} |
| 186 | + configHeader.addEventListener('click', function() {{ |
| 187 | + configWrapper.classList.toggle('collapsed'); |
| 188 | + var isExpanded = !configWrapper.classList.contains('collapsed'); |
| 189 | + configHeader.setAttribute('aria-expanded', isExpanded); |
| 190 | + toggleButton.setAttribute('aria-expanded', isExpanded); |
| 191 | + }}); |
| 192 | + configHeader.addEventListener('keydown', function(event) {{ |
| 193 | + if (event.key === 'Enter' || event.key === ' ') {{ |
| 194 | + configWrapper.classList.toggle('collapsed'); |
| 195 | + var isExpanded = !configWrapper.classList.contains('collapsed'); |
| 196 | + configHeader.setAttribute('aria-expanded', isExpanded); |
| 197 | + toggleButton.setAttribute('aria-expanded', isExpanded); |
| 198 | + event.preventDefault(); |
| 199 | + }} |
| 200 | + }}); |
| 201 | + }} else {{ |
| 202 | + if (configWrapper) {{ configWrapper.style.display = 'none'; }} |
| 203 | + }} |
| 204 | + |
| 205 | + // These DataSets are the "active" data being displayed |
| 206 | + var nodes = new vis.DataSet(allNodesArray); |
| 207 | + var edges = new vis.DataSet(allEdgesArray); |
| 208 | + var graphContainer = document.getElementById('mynetwork-{div_id_suffix}'); |
| 209 | + var data = {{ nodes: nodes, edges: edges }}; |
| 210 | + var network = new vis.Network(graphContainer, data, optionsObject); |
| 211 | + |
| 212 | + // Function to restore the full graph view |
| 213 | + function resetView() {{ |
| 214 | + searchInput.value = ""; // Clear search input |
| 215 | + nodes.clear(); |
| 216 | + edges.clear(); |
| 217 | + nodes.add(allNodesArray); |
| 218 | + edges.add(allEdgesArray); |
| 219 | + network.fit(); |
| 220 | + }} |
| 221 | + |
| 222 | + // Function to filter graph based on search query |
| 223 | + function filterBySearch(query) {{ |
| 224 | + if (!query) {{ |
| 225 | + resetView(); |
| 226 | + return; |
| 227 | + }} |
| 228 | + var regex; |
| 229 | + try {{ |
| 230 | + regex = new RegExp(query, 'i'); // Case-insensitive regex |
| 231 | + }} catch (e) {{ |
| 232 | + console.error("Invalid Regex:", e); |
| 233 | + return; // Don't filter if regex is invalid |
| 234 | + }} |
| 235 | + |
| 236 | + var matchingNodeIds = new Set(); |
| 237 | + allNodesArray.forEach(function(node) {{ |
| 238 | + var textToSearch = node.label || node.id; |
| 239 | + if (regex.test(textToSearch)) {{ |
| 240 | + matchingNodeIds.add(node.id); |
| 241 | + }} |
| 242 | + }}); |
| 243 | + |
| 244 | + var filteredNodes = allNodesArray.filter(function(node) {{ |
| 245 | + return matchingNodeIds.has(node.id); |
| 246 | + }}); |
| 247 | + |
| 248 | + var filteredEdges = allEdgesArray.filter(function(edge) {{ |
| 249 | + return matchingNodeIds.has(edge.from) && matchingNodeIds.has(edge.to); |
| 250 | + }}); |
| 251 | + |
| 252 | + nodes.clear(); |
| 253 | + edges.clear(); |
| 254 | + nodes.add(filteredNodes); |
| 255 | + edges.add(filteredEdges); |
| 256 | + network.fit(); |
| 257 | + }} |
| 258 | + |
| 259 | + // Function to isolate a node and its neighbors |
| 260 | + function showNeighborhood(nodeId) {{ |
| 261 | + searchInput.value = ""; // Clear search when isolating |
| 262 | + var nodesToShow = new Set([nodeId]); |
| 263 | + var edgesToShow = []; |
| 264 | + |
| 265 | + allEdgesArray.forEach(function(edge) {{ |
| 266 | + if (edge.from === nodeId) {{ |
| 267 | + nodesToShow.add(edge.to); |
| 268 | + edgesToShow.push(edge); |
| 269 | + }} else if (edge.to === nodeId) {{ |
| 270 | + nodesToShow.add(edge.from); |
| 271 | + edgesToShow.push(edge); |
| 272 | + }} |
| 273 | + }}); |
| 274 | + |
| 275 | + var filteredNodes = allNodesArray.filter(function(node) {{ |
| 276 | + return nodesToShow.has(node.id); |
| 277 | + }}); |
| 278 | + |
| 279 | + nodes.clear(); |
| 280 | + edges.clear(); |
| 281 | + nodes.add(filteredNodes); |
| 282 | + edges.add(edgesToShow); |
| 283 | + network.fit(); |
| 284 | + }} |
| 285 | + |
| 286 | + // --- Event Listeners --- |
| 287 | + |
| 288 | + // Filter as user types in the search box |
| 289 | + searchInput.addEventListener('input', function() {{ |
| 290 | + filterBySearch(this.value); |
| 291 | + }}); |
| 292 | + |
| 293 | + // Clear button resets the view |
| 294 | + clearButton.addEventListener('click', resetView); |
| 295 | + |
| 296 | + // Handle clicks on the network |
| 297 | + network.on("click", function (params) {{ |
| 298 | + if (params.nodes.length > 0) {{ |
| 299 | + // A node was clicked, show its neighborhood |
| 300 | + var clickedNodeId = params.nodes[0]; |
| 301 | + showNeighborhood(clickedNodeId); |
| 302 | + }} else {{ |
| 303 | + // The background was clicked, reset everything |
| 304 | + resetView(); |
| 305 | + }} |
| 306 | + }}); |
| 307 | + }})(); |
| 308 | + </script> |
| 309 | +</body> |
| 310 | +</html> |
0 commit comments