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 += '
';
+ filterHTML += '
';
- let edgeCheckboxes = '
Edges
';
+ // Edges section
+ filterHTML += '
';
+ 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}`);
}