diff --git a/static/css/main.css b/static/css/main.css index 0084f89..888a566 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -373,8 +373,8 @@ input[type="text"]:focus, select:focus { padding: 0.75rem; font-family: 'Roboto Mono', monospace; font-size: 0.8rem; - max-height: 40%; - overflow-y: auto; + /*max-height: 40%; + overflow-y: auto;*/ display: flex; gap: 1.5rem; } diff --git a/static/js/graph.js b/static/js/graph.js index ae6d10f..e3fdcb1 100644 --- a/static/js/graph.js +++ b/static/js/graph.js @@ -15,6 +15,7 @@ class GraphManager { this.contextMenu = null; this.history = []; this.filterPanel = null; + this.trueRootIds = new Set(); this.options = { nodes: { @@ -362,7 +363,9 @@ class GraphManager { // Update existing data this.nodes.update(processedNodes); this.edges.update(processedEdges); - + + // After data is loaded, compute roots and apply filters + this.computeTrueRoots(); this.updateFilterControls(); this.applyAllFilters(); @@ -935,27 +938,55 @@ class GraphManager { }; } + computeTrueRoots() { + this.trueRootIds.clear(); + const allNodes = this.nodes.get({ returnType: 'Object' }); + const allEdges = this.edges.get(); + const inDegrees = {}; + + for (const nodeId in allNodes) { + inDegrees[nodeId] = 0; + } + allEdges.forEach(edge => { + if (inDegrees[edge.to] !== undefined) { + inDegrees[edge.to]++; + } + }); + + for (const nodeId in allNodes) { + if (inDegrees[nodeId] === 0) { + this.trueRootIds.add(nodeId); + } + } + console.log("Computed true roots:", this.trueRootIds); + } + updateFilterControls() { if (!this.filterPanel) return; - const nodeTypes = new Set(this.nodes.get().map(n => n.type)); const edgeTypes = new Set(this.edges.get().map(e => e.metadata.relationship_type)); - let nodeCheckboxes = '

Nodes

'; + // Wrap both columns in a single container with vertical layout + let filterHTML = '
'; + + // Nodes section + filterHTML += '

Nodes

'; nodeTypes.forEach(type => { const label = type === 'correlation_object' ? 'latent correlations' : type; const isChecked = type !== 'correlation_object'; - nodeCheckboxes += ``; + filterHTML += ``; }); - nodeCheckboxes += '
'; + filterHTML += '
'; - let edgeCheckboxes = '

Edges

'; + // Edges section + filterHTML += '

Edges

'; edgeTypes.forEach(type => { - edgeCheckboxes += ``; + filterHTML += ``; }); - edgeCheckboxes += '
'; + filterHTML += '
'; - this.filterPanel.innerHTML = nodeCheckboxes + edgeCheckboxes; + filterHTML += '
'; // Close filter-container + this.filterPanel.innerHTML = filterHTML; this.filterPanel.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { checkbox.addEventListener('change', () => this.applyAllFilters()); @@ -963,42 +994,74 @@ class GraphManager { } applyAllFilters() { + console.log("Applying all filters (robust orphan detection)..."); + if (this.nodes.length === 0) return; + + // 1. Get filter criteria from the UI const hiddenNodeTypes = new Set(); this.filterPanel.querySelectorAll('input[data-filter-type="node"]:not(:checked)').forEach(cb => { hiddenNodeTypes.add(cb.value); }); - + const hiddenEdgeTypes = new Set(); this.filterPanel.querySelectorAll('input[data-filter-type="edge"]:not(:checked)').forEach(cb => { hiddenEdgeTypes.add(cb.value); }); - - const nodeUpdates = []; - const edgeUpdates = []; - const visibleEdges = new Set(); - - this.edges.get().forEach(edge => { - const isVisible = !hiddenEdgeTypes.has(edge.metadata.relationship_type); - edgeUpdates.push({ id: edge.id, hidden: !isVisible }); - if (isVisible) { - visibleEdges.add(edge.id); + + // 2. Build adjacency list for the visible part of the graph + const adj = {}; + this.nodes.getIds().forEach(id => adj[id] = []); + this.edges.forEach(edge => { + if (!hiddenEdgeTypes.has(edge.metadata.relationship_type)) { + adj[edge.from].push(edge.to); } }); - - this.nodes.get().forEach(node => { - let isVisible = !hiddenNodeTypes.has(node.type); - if (isVisible) { - const connectedEdges = this.network.getConnectedEdges(node.id); - const hasVisibleConnection = connectedEdges.some(edgeId => visibleEdges.has(edgeId)); - if (!hasVisibleConnection && connectedEdges.length > 0) { - isVisible = false; + + // 3. Traverse from "true roots" to find all reachable nodes + const reachableNodes = new Set(); + const queue = []; + + // Start the traversal from true roots that aren't hidden by type + this.trueRootIds.forEach(rootId => { + const node = this.nodes.get(rootId); + if (node && !hiddenNodeTypes.has(node.type)) { + if (!reachableNodes.has(rootId)) { + queue.push(rootId); + reachableNodes.add(rootId); } } - nodeUpdates.push({ id: node.id, hidden: !isVisible }); }); + + let head = 0; + while (head < queue.length) { + const u = queue[head++]; + + for (const v of (adj[u] || [])) { + if (!reachableNodes.has(v)) { + const node = this.nodes.get(v); + if (node && !hiddenNodeTypes.has(node.type)) { + reachableNodes.add(v); + queue.push(v); + } + } + } + } + + // 4. Create final node and edge visibility updates + const nodeUpdates = this.nodes.map(node => ({ + id: node.id, + hidden: !reachableNodes.has(node.id) + })); - this.edges.update(edgeUpdates); + const edgeUpdates = this.edges.map(edge => ({ + id: edge.id, + hidden: hiddenEdgeTypes.has(edge.metadata.relationship_type) || !reachableNodes.has(edge.from) || !reachableNodes.has(edge.to) + })); + this.nodes.update(nodeUpdates); + this.edges.update(edgeUpdates); + + console.log(`Filters applied. Reachable nodes: ${reachableNodes.size}`); }