feat: dependency graph overhaul -- WebGL rendering + DSM matrix view#246
Conversation
- Remove reactflow, dagre, @types/dagre - Install sigma, graphology, graphology-layout-forceatlas2, graphology-communities-louvain, @react-sigma/core - Create new DependencyGraph structure: GraphView/, MatrixView/, types.ts - Build useGraphData hook (API -> graphology Graph + ForceAtlas2 + Louvain) - Build useMatrixData hook (API -> adjacency matrix + cycle detection) - Rewrite index.tsx with Graph/Matrix tab switcher + StatsBar - Wire existing ImpactPanel + useImpactAnalysis (unchanged) - Placeholder GraphView and MatrixView components (to be implemented) Closes OPE-41, OPE-42, OPE-43, OPE-44
GraphView (OPE-45): - WebGL rendering via Sigma.js with dark theme - ForceAtlas2 layout with Louvain community detection for cluster coloring - Hover highlights node neighborhood, fades rest - Click opens impact analysis, double-click zooms to node - Node tooltip showing file details on hover - Legend overlay explaining visual encoding MatrixView (OPE-46): - Dependency Structure Matrix showing file-to-file imports - Color intensity maps to coupling strength - Red cells highlight circular dependencies - Directory grouping with visual separators - Sticky row/column headers for scroll navigation - Row click triggers impact analysis - Stats bar showing file count, deps, and cycle count - Truncation for large matrices (150+ files)
… density - Set canvas background to zinc-950 (#09090b) matching our dark theme - Edges now nearly invisible (12% opacity) until hover reveals neighbors - Label density reduced so only important nodes show labels at default zoom - Hovered node gets white border glow, neighbors show labels - Non-neighbor nodes fade to 30% opacity on hover - Legend uses glass card style with backdrop blur - Added border around graph container for visual definition
- Add SearchBar: type to find files, fuzzy match, zoom-to-focus on select Keyboard shortcut: / to open, Escape to clear - Add GraphControls: zoom in/out, fit-to-screen, center buttons - Fix ImpactPanel positioning: now overlays as right sidebar on graph - Filter orphan nodes (0 connections) to reduce visual noise - Tune ForceAtlas2: lower gravity, higher scaling for spread-out layout linLogMode + outboundAttractionDistribution for better clustering - Increase iterations to 400 for more settled layout - hideEdgesOnMove for better pan/zoom performance
…nd hover Root cause: SearchBar and Interactions both set nodeReducer/edgeReducer independently. When search set reducers, hover's useEffect fired with hoveredNode=null and reset them to null, blanking the graph. Fix: lift highlightedNode state to GraphView parent. Both SearchBar and Interactions write to the same state. Single useEffect drives the reducers from that shared state. Also fixed: camera zoom using graph coords (node attributes x/y) instead of display coords (getNodeDisplayData) which were viewport- relative and caused zoom to wrong position. Also: clickStage clears highlight, keyboard handler skips when user is typing in other inputs.
Three bugs fixed: 1. Search blanking graph: search now sets pinnedNode which persists. Reducers use activeNode = hoveredNode || pinnedNode so the pinned highlight stays visible even when mouse moves away. 2. Click not persisting: clicking a node now pins it AND opens impact panel. The highlight stays until user clicks empty stage or closes the panel. 3. Search + hover fighting: hover takes visual priority (for exploring) but pinned state persists underneath. When hover ends, pinned node highlight comes back. Also: search zoom uses setTimeout(50ms) so reducer applies before camera animates -- prevents blank frame during transition.
…-down Search fix: - Use sigma.getNodeDisplayData() + viewportToGraph() for camera zoom instead of raw graph attributes. The ForceAtlas2 coordinates don't map directly to sigma's camera space. - Also fixed double-click zoom using same approach - 100ms delay before camera animate so reducer applies first Matrix rewrite: - Default view now shows directory-to-directory relationships (~15 dirs) instead of 239 files. Actually readable and useful. - Each cell shows the number of imports between directories - Click a directory row to drill into file-level within that directory - Back button to return to directory overview - Cell sizes scale with matrix size for readability - Numbers visible in cells for non-zero values - Hover tooltip shows full paths and import count - Circular dependency detection at both directory and file level
The camera zoom was blanking the graph because we were passing wrong coordinates. Sigma's camera.animate expects coordinates in its own normalized space, not raw graph coords or viewport pixels. New approach: use sigma.graphToViewport() to find where the target node is currently on screen, calculate the pixel offset from center, then adjust the camera state proportionally. This works regardless of the current zoom level or camera position. Same fix applied to double-click zoom.
Removed: - DependencyGraph/GraphNode.tsx (old ReactFlow node component) - DependencyGraph/DirectoryNode.tsx (old ReactFlow directory node) - DependencyGraph/GraphToolbar.tsx (old ReactFlow toolbar) - frontend/package-lock.json (Bun only policy, OPE-9) No remaining references to reactflow anywhere in the codebase. CSS bundle dropped 4.2kb from removing reactflow styles.
|
@DevanshuNEU is attempting to deploy a commit to the Dev's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThe PR replaces React Flow-based graph visualization with Sigma.js-based visualization and adds a dependency matrix view. Dependencies are updated to support graphology-based graph processing with Louvain community detection and ForceAtlas2 layout. Component architecture is simplified by removing node/edge state management and introducing new GraphView and MatrixView components driven by API data. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant SearchBar
participant Graph as GraphView/Sigma
participant Camera as Camera Animation
participant ImpactPanel
User->>SearchBar: Type query / Press "/"
SearchBar->>SearchBar: Search nodes by label<br/>(case-insensitive, top 8 results)
SearchBar->>User: Display results dropdown
User->>SearchBar: Select result
SearchBar->>Graph: onFocusNode(nodeId)
Graph->>Graph: Pin node, trigger highlight
Graph->>ImpactPanel: onSelectFile(nodeId)
ImpactPanel->>ImpactPanel: Render file impact analysis
SearchBar->>Camera: Compute viewport transform
Camera->>Camera: Animate camera focus<br/>towards selected node
SearchBar->>SearchBar: Reset search state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (12)
frontend/src/components/DependencyGraph/MatrixView/index.tsx (2)
109-113:onMouseEntersilently returnsundefinedfor zero-value non-cycle cells.The ternary
value > 0 || isCycle ? onCellHover({...}) : undefinedmeans hovering over an empty cell does nothing. This works becauseonMouseLeaveon the previous cell fires first. However, this is fragile — if the browser coalesces events (fast mouse movements), a stale tooltip could persist. CallingonCellHover(null)explicitly in the else branch would be safer.Proposed fix
onMouseEnter={(e) => - value > 0 || isCycle - ? onCellHover({ row: rowIdx, col: colIdx, x: e.clientX, y: e.clientY }) - : undefined + onCellHover( + value > 0 || isCycle + ? { row: rowIdx, col: colIdx, x: e.clientX, y: e.clientY } + : null + ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/MatrixView/index.tsx` around lines 109 - 113, The onMouseEnter handler currently returns undefined for cells where value === 0 and isCycle === false, which can leave a stale tooltip if events are coalesced; change the ternary so the else branch explicitly calls onCellHover(null) instead of returning undefined (update the JSX handler that references onCellHover, value, and isCycle in the MatrixView cell to call onCellHover(null) when the cell is empty), ensuring onMouseLeave and onMouseEnter both clear hovered state consistently.
41-48:cycleSetis computed twice — once inMatrixGrid(lines 41-48) and again inMatrixView(lines 190-197).Both
MatrixGridandMatrixViewindependently build acycleSetfrom the sameactiveCyclesarray. Pass theMatrixView-levelcycleSetas a prop toMatrixGridinstead of recomputing it.Proposed change
function MatrixGrid({ labels, matrix, - cycles, + cycleSet, onCellHover, onRowClick, highlightedIndex, setHighlightedIndex, }: { labels: string[] matrix: number[][] - cycles: [number, number][] + cycleSet: Set<string> onCellHover: (info: { row: number; col: number; x: number; y: number } | null) => void onRowClick: (index: number) => void highlightedIndex: number | null setHighlightedIndex: (i: number | null) => void }) { - const cycleSet = useMemo(() => { - const set = new Set<string>() - for (const [a, b] of cycles) { - set.add(`${a}-${b}`) - set.add(`${b}-${a}`) - } - return set - }, [cycles])Then in
MatrixView, passcycleSetdirectly:<MatrixGrid labels={activeLabels} matrix={activeMatrix} - cycles={activeCycles} + cycleSet={cycleSet} onCellHover={setHoveredCell}Also applies to: 190-197
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/MatrixView/index.tsx` around lines 41 - 48, MatrixGrid and MatrixView both compute the same cycleSet from activeCycles causing duplicate work; compute cycleSet once in MatrixView (using the existing useMemo that builds a Set<string> from activeCycles) and remove the duplicate computation inside MatrixGrid, then pass that cycleSet into MatrixGrid as a new prop (e.g., cycleSet) and update MatrixGrid to use the passed-in cycleSet instead of rebuilding it; ensure prop types/TSX signature for MatrixGrid are updated and all internal references to the local cycleSet are switched to the prop.frontend/src/components/DependencyGraph/GraphView/useGraphData.ts (3)
114-128: ForceAtlas2 with 400 iterations runs synchronously on the main thread.
forceAtlas2.assignis a blocking call. For graphs with hundreds of nodes, this will freeze the UI during initial render. ThebarnesHutOptimizeflag helps, but 400 iterations is still substantial.Consider either reducing iterations for large graphs or offloading to a Web Worker (graphology-layout-forceatlas2 provides a worker-based API). This aligns with the known limitation tracked in OPE-50 (bundle lazy loading), so flagging for awareness.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/GraphView/useGraphData.ts` around lines 114 - 128, forceAtlas2.assign is being called synchronously with iterations: 400 which blocks the main thread for medium/large graphs; update useGraphData to avoid UI freezes by adapting the layout strategy: detect large graphs via graph.order and either reduce iterations (e.g., lower iterations when graph.order > X) or switch to the worker-based API offered by graphology-layout-forceatlas2 (use the Web Worker wrapper instead of forceAtlas2.assign) and preserve barnesHutOptimize, barnesHutTheta, slowDown, and other settings when creating/dispatching the worker; ensure the code paths reference forceAtlas2.assign, iterations, and graph.order so reviewers can spot the conditional change.
24-27:getDirectoryis duplicated inuseMatrixData.ts.The same function appears at
useMatrixData.ts:7-10. Extract it into a shared utility (e.g.,utils.tsalongsidetypes.ts) to keep a single source of truth.Proposed shared utility
Create
frontend/src/components/DependencyGraph/utils.ts:export function getDirectory(filePath: string): string { const parts = filePath.split('/') return parts.length > 1 ? parts.slice(0, -1).join('/') : '.' }Then import from both hooks:
-function getDirectory(filePath: string): string { - const parts = filePath.split('/') - return parts.length > 1 ? parts.slice(0, -1).join('/') : '.' -} +import { getDirectory } from '../utils'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/GraphView/useGraphData.ts` around lines 24 - 27, Duplicate getDirectory logic in useGraphData and useMatrixData should be extracted into a single shared utility: create a new module (e.g., utils.ts) that exports a named function getDirectory(filePath: string): string with the existing implementation, then remove the local getDirectory definitions from both useGraphData and useMatrixData and replace them with imports (import { getDirectory } from './utils'). Ensure the exported function name matches the existing references so both hooks compile without other changes.
88-112: Louvain community color assignment usesindexOf— O(n) per node.Line 93 uses
communityIds.indexOf(communities[node])which is O(n) per lookup insideforEachNode, making the overall assignment O(n²). For typical graph sizes this is fine, but if scaling is a concern, use aMapinstead.Proposed optimization
const communities = louvain(graph) const communityIds = [...new Set(Object.values(communities))] + const communityMap = new Map(communityIds.map((id, idx) => [id, idx])) graph.forEachNode((node) => { - const communityIndex = communityIds.indexOf(communities[node]) + const communityIndex = communityMap.get(communities[node]) ?? 0 graph.setNodeAttribute( node, 'color', COMMUNITY_COLORS[communityIndex % COMMUNITY_COLORS.length] )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/GraphView/useGraphData.ts` around lines 88 - 112, The current Louvain color assignment uses communityIds.indexOf(communities[node]) inside graph.forEachNode which yields O(n²) behavior; replace this with a precomputed Map (e.g., build a Map from community value to index from communityIds) and use map.get(communities[node]) when setting color in the louvain block (symbols: louvain, communities, communityIds, COMMUNITY_COLORS, graph.forEachNode, graph.setNodeAttribute). Do the same optimization in the catch/fallback directory coloring: compute a Map from directory value to index from directories and use it instead of directories.indexOf(attrs.directory) when calling graph.setNodeAttribute. Ensure null/undefined guards if needed and preserve modulo coloring logic (index % COMMUNITY_COLORS.length).frontend/src/components/DependencyGraph/GraphView/NodeTooltip.tsx (1)
29-34: Tooltip can overflow the viewport when the cursor is near the right or bottom edge.The fixed positioning at
left: position.x + 12andtop: position.y - 10doesn't account for viewport bounds. When hovering nodes near the edges, the tooltip will be clipped or cause horizontal scrolling.Consider clamping or flipping the tooltip direction based on available viewport space. This could be deferred since it's tracked as UX polish.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/GraphView/NodeTooltip.tsx` around lines 29 - 34, The NodeTooltip positioning uses fixed left: position.x + 12 and top: position.y - 10 which can overflow the viewport; update NodeTooltip to compute clamped/flipped coordinates before applying the style: measure the tooltip element (via a ref, e.g., tooltipRef) after render to get its offsetWidth/offsetHeight, compare position.x/position.y plus tooltip size against window.innerWidth/innerHeight, and then choose left vs right (e.g., position.x + 12 or position.x - tooltipWidth - 12) and top vs bottom (position.y - 10 or position.y - tooltipHeight + 10) to keep it inside the viewport; apply those computed values to the style and update on window resize/mouse move to avoid clipping.frontend/src/components/DependencyGraph/GraphView/SearchBar.tsx (1)
19-43: Search callback runs on every keystroke without debouncing.
graph.forEachNodeiterates all nodes synchronously in auseEffectthat fires on everyquerychange. For large graphs this can cause noticeable input lag.Consider debouncing the query (e.g., 150–200ms) before running the search traversal, or switching to a pre-built index.
Debounce example
+import { useState, useRef, useEffect, useCallback, useDeferredValue } from 'react' ... export function SearchBar({ onFocusNode }: SearchBarProps) { const sigma = useSigma() const [query, setQuery] = useState('') + const deferredQuery = useDeferredValue(query) const [results, setResults] = useState<{ id: string; label: string }[]>([]) ... useEffect(() => { - if (!query.trim()) { + if (!deferredQuery.trim()) { setResults([]) return } const graph = sigma.getGraph() - const q = query.toLowerCase() + const q = deferredQuery.toLowerCase() ... - }, [query, sigma]) + }, [deferredQuery, sigma])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/GraphView/SearchBar.tsx` around lines 19 - 43, The search runs synchronously on every keystroke because the useEffect watching query calls sigma.getGraph() and graph.forEachNode immediately; add debouncing so the traversal only runs after the user stops typing (e.g., 150–200ms) or use a pre-built index. Implement this by wrapping the existing useEffect logic (the call to sigma.getGraph(), graph.forEachNode, sorting, and setResults) in a debounced handler: start/reset a timeout when query changes, run the traversal after the delay, and clear the timeout on cleanup; alternatively replace the inline traversal with a lookup against a precomputed index built outside the effect. Ensure you keep the same behavior for empty/trimmed query (clearing setResults) and reference the same symbols (useEffect, query, setResults, sigma.getGraph, graph.forEachNode) when making the change.frontend/src/components/DependencyGraph/GraphView/GraphControls.tsx (1)
14-21:fitToScreenandcenterGraphare functionally identical and likely confuse users.Both
reset()(fromuseCamera) andsigma.getCamera().animatedReset({ duration: 300 })call the same underlying Sigma camera function with identical animation options. Having two distinct buttons (Maximize2 and LocateFixed) that perform the same action is redundant. Consolidate them into a single button or differentiate their behavior (e.g., one focuses on zoom only, the other repositions the viewport).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/GraphView/GraphControls.tsx` around lines 14 - 21, fitToScreen and centerGraph are currently doing the same thing (reset() vs sigma.getCamera().animatedReset({ duration: 300 })) and create redundant UI actions; either consolidate them into a single control or change one to a different behavior. Locate the functions fitToScreen and centerGraph in GraphControls.tsx and either remove one and update the corresponding button (Maximize2 or LocateFixed) to call the remaining function, or modify one function to a distinct action (e.g., implement zoom-only by calling camera.setZoom(...) or implement reposition-only by animating camera.setState({...}) while keeping the other as full animatedReset). Ensure all button handlers and icons reference the updated function name so no orphan handlers remain.frontend/src/components/DependencyGraph/index.tsx (2)
131-143: No-oponFileHover— intentional stub?Line 140 passes an empty function for
onFileHover. IfImpactPaneluses this prop for highlighting behavior, this silently discards hover events. If intentionally deferred, a brief comment noting the intent (e.g.,// TODO: wire up hover highlight) would help future readers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/index.tsx` around lines 131 - 143, The empty onFileHover passed into ImpactPanel is a no-op and will drop hover events; either wire it to a proper handler or mark it explicitly as intentional. Replace onFileHover={() => {}} with a handler that forwards the hovered file id to your state (e.g., (fileId) => setSelectedFile(fileId) or a dedicated hover state setter) so hover highlights work, or if you purposely omitted behavior, add a short comment (e.g., // TODO: wire up hover highlight) next to the ImpactPanel prop to make the intent clear; reference symbols: ImpactPanel, onFileHover, setSelectedFile.
59-89: ViewToggle lacks accessible tab semantics.The toggle renders interactive
<button>elements that act as tabs, but there are no ARIA attributes (role="tablist",role="tab",aria-selected). Screen readers won't convey the toggle state.♿ Proposed accessibility improvement
- <div className="flex gap-1 mx-4 mb-4 p-1 bg-muted rounded-lg w-fit border border-border"> + <div role="tablist" className="flex gap-1 mx-4 mb-4 p-1 bg-muted rounded-lg w-fit border border-border"> {tabs.map((tab) => ( <button key={tab.id} + role="tab" + aria-selected={active === tab.id} onClick={() => onChange(tab.id)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/index.tsx` around lines 59 - 89, ViewToggle renders buttons that behave like tabs but lacks ARIA tab semantics; update the component so the container div has role="tablist" and each button uses role="tab", aria-selected={active === tab.id}, and tabIndex={active === tab.id ? 0 : -1} to expose state to assistive tech, and add an aria-controls attribute on each button (e.g., `${tab.id}-panel`) so the buttons can be associated with the corresponding panel; keep the existing onClick logic (onChange) and ensure the tab ids in the tabs array (used by active) match the panel ids.frontend/src/components/DependencyGraph/GraphView/index.tsx (2)
209-241: Consider making the graph container height responsive.The
h-[700px]hardcoded height may not suit all viewport sizes. Ah-[calc(100vh-<offset>)]or a prop-driven height would be more flexible. Low priority since this works for the current layout.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/GraphView/index.tsx` around lines 209 - 241, The outer container currently uses a hardcoded class "h-[700px]" which makes the graph non‑responsive; change the GraphView component to accept a height prop (e.g., containerHeight or graphHeight) and replace the fixed "h-[700px]" with a dynamic style/class that uses that prop (defaulting to a responsive fallback like "calc(100vh - <offset>)" if no prop provided), or compute the height and pass it into the SigmaContainer style (SigmaContainer's style prop is available) so the graph scales with viewport; update usages of GraphView to pass the prop or rely on the default.
26-46: RemovenodeReducerandedgeReducerfromSIGMA_SETTINGSto eliminate type-unsafeas anycasts.The
null as anyentries on lines 30–31 hide type contract issues. Since these are already set programmatically viasigma.setSetting()in theInteractionscomponent, they can be omitted from the static settings object entirely.Proposed fix
const SIGMA_SETTINGS = { defaultNodeColor: '#6366f1', defaultEdgeColor: 'rgba(75, 85, 99, 0.12)', defaultEdgeType: 'arrow' as const, - edgeReducer: null as any, - nodeReducer: null as any, renderEdgeLabels: false, labelFont: 'Inter, system-ui, sans-serif', // ... rest unchanged }The reducers are initialized and updated dynamically via
setSetting()calls, so the initial null entries are redundant.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/DependencyGraph/GraphView/index.tsx` around lines 26 - 46, Remove the type-unsafe null casts for nodeReducer and edgeReducer from the SIGMA_SETTINGS object: delete the nodeReducer and edgeReducer entries so the static settings no longer use "null as any". The reducers are set dynamically in the Interactions component via sigma.setSetting(), so keep those dynamic setSetting() calls and ensure no other code expects these keys to exist on initial SIGMA_SETTINGS.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/components/DependencyGraph/GraphView/index.tsx`:
- Around line 129-133: The clickStage handler is calling onSelectFile with an
unsafe undefined as any which violates the (filePath: string) => void contract;
update this by either 1) widening onSelectFile's prop type to accept string |
undefined (or string | null) and adjust its callers to handle undefined, then
call onSelectFile(undefined) from clickStage, or 2) keep onSelectFile as
(filePath: string) => void and add a new clear callback (e.g., onClearSelection)
that clickStage invokes after setPinnedNode(null); locate clickStage,
setPinnedNode and onSelectFile in the component to apply the chosen change and
update prop types and any parent usage accordingly.
- Around line 90-97: The tooltip position is computed using sigma's
container-relative coordinates in the registerEvents enterNode handler
(enterNode: ({ node, event }) => { ... }) but NodeTooltip is rendered with
position: fixed, causing misalignment when the sigma container is offset; fix by
reading the sigma container element via sigma.getContainer(), call
getBoundingClientRect() and add rect.left to event.x and rect.top to event.y
before calling setTooltip({ nodeId: node, position: { x, y } }), and apply the
same conversion when updating/clearing tooltip on leaveNode or move events to
ensure consistent viewport-relative coordinates.
In `@frontend/src/components/DependencyGraph/GraphView/NodeTooltip.tsx`:
- Around line 4-12: The NodeTooltipProps interface declares nodeId but the
NodeTooltip component does not use or destructure it, so either remove nodeId
from the NodeTooltipProps interface or wire it into the component (e.g., accept
nodeId in the component props and attach it as a data attribute or use it where
needed). Update the NodeTooltipProps definition and the NodeTooltip function
signature (or JSX attributes) accordingly—refer to NodeTooltipProps and the
NodeTooltip component to locate and apply the change.
In `@frontend/src/components/DependencyGraph/GraphView/SearchBar.tsx`:
- Around line 54-81: The setTimeout-based delay around the camera animation is
brittle; replace the setTimeout(...) wrapper with a reliable trigger such as
subscribing once to sigma's "afterRender" event (or using requestAnimationFrame
if you only need the next frame) so sigma.getDimensions() and
sigma.graphToViewport(...) return correct values before computing dx/dy and
calling camera.animate(...). Attach the listener before computing node attrs
(using getNodeAttributes(nodeId)) and ensure you remove the listener (or cancel
the rAF) immediately after the first invocation to avoid repeated animations.
In `@frontend/src/components/DependencyGraph/index.tsx`:
- Around line 1-13: The DependencyGraphProps interface and the DependencyGraph
component should not require the unused apiUrl prop; remove apiUrl from the
DependencyGraphProps declaration and from the DependencyGraph component's
parameter list (or props destructuring) so the component no longer expects that
prop, and update any usages/consumers to stop passing apiUrl; if you prefer
making the endpoint configurable instead, modify useDependencyGraph to accept an
apiUrl parameter and wire that through from DependencyGraph (change
useDependencyGraph(...) calls accordingly) but do not leave apiUrl declared on
DependencyGraphProps without being used.
In `@frontend/src/components/DependencyGraph/MatrixView/useMatrixData.ts`:
- Around line 81-89: The returned totalDeps currently uses apiData.edges.length
which counts intra-directory edges; change it to only count cross-directory
dependencies by summing off-diagonal entries (or filtering edges where sourceDir
!== targetDir) before returning. Update the return in useMatrixData (the object
containing directories, matrix, fileCounts, cycles, totalDeps, totalCycles) so
totalDeps = matrix off-diagonal sum (or edges.filter(e => sourceDir !==
targetDir).length) to reflect inter-directory dependencies correctly.
In `@frontend/src/components/DependencyGraph/types.ts`:
- Around line 37-46: Remove the unused MatrixData interface declaration (symbol:
MatrixData) from the file; instead rely on the existing DirectoryMatrixData and
FileMatrixData types produced by useMatrixData.ts—delete the entire MatrixData
block and any export for it, run a quick search for MatrixData to ensure there
are no remaining references, and if any code expects a general matrix type,
update those sites to use DirectoryMatrixData or FileMatrixData accordingly.
---
Nitpick comments:
In `@frontend/src/components/DependencyGraph/GraphView/GraphControls.tsx`:
- Around line 14-21: fitToScreen and centerGraph are currently doing the same
thing (reset() vs sigma.getCamera().animatedReset({ duration: 300 })) and create
redundant UI actions; either consolidate them into a single control or change
one to a different behavior. Locate the functions fitToScreen and centerGraph in
GraphControls.tsx and either remove one and update the corresponding button
(Maximize2 or LocateFixed) to call the remaining function, or modify one
function to a distinct action (e.g., implement zoom-only by calling
camera.setZoom(...) or implement reposition-only by animating
camera.setState({...}) while keeping the other as full animatedReset). Ensure
all button handlers and icons reference the updated function name so no orphan
handlers remain.
In `@frontend/src/components/DependencyGraph/GraphView/index.tsx`:
- Around line 209-241: The outer container currently uses a hardcoded class
"h-[700px]" which makes the graph non‑responsive; change the GraphView component
to accept a height prop (e.g., containerHeight or graphHeight) and replace the
fixed "h-[700px]" with a dynamic style/class that uses that prop (defaulting to
a responsive fallback like "calc(100vh - <offset>)" if no prop provided), or
compute the height and pass it into the SigmaContainer style (SigmaContainer's
style prop is available) so the graph scales with viewport; update usages of
GraphView to pass the prop or rely on the default.
- Around line 26-46: Remove the type-unsafe null casts for nodeReducer and
edgeReducer from the SIGMA_SETTINGS object: delete the nodeReducer and
edgeReducer entries so the static settings no longer use "null as any". The
reducers are set dynamically in the Interactions component via
sigma.setSetting(), so keep those dynamic setSetting() calls and ensure no other
code expects these keys to exist on initial SIGMA_SETTINGS.
In `@frontend/src/components/DependencyGraph/GraphView/NodeTooltip.tsx`:
- Around line 29-34: The NodeTooltip positioning uses fixed left: position.x +
12 and top: position.y - 10 which can overflow the viewport; update NodeTooltip
to compute clamped/flipped coordinates before applying the style: measure the
tooltip element (via a ref, e.g., tooltipRef) after render to get its
offsetWidth/offsetHeight, compare position.x/position.y plus tooltip size
against window.innerWidth/innerHeight, and then choose left vs right (e.g.,
position.x + 12 or position.x - tooltipWidth - 12) and top vs bottom (position.y
- 10 or position.y - tooltipHeight + 10) to keep it inside the viewport; apply
those computed values to the style and update on window resize/mouse move to
avoid clipping.
In `@frontend/src/components/DependencyGraph/GraphView/SearchBar.tsx`:
- Around line 19-43: The search runs synchronously on every keystroke because
the useEffect watching query calls sigma.getGraph() and graph.forEachNode
immediately; add debouncing so the traversal only runs after the user stops
typing (e.g., 150–200ms) or use a pre-built index. Implement this by wrapping
the existing useEffect logic (the call to sigma.getGraph(), graph.forEachNode,
sorting, and setResults) in a debounced handler: start/reset a timeout when
query changes, run the traversal after the delay, and clear the timeout on
cleanup; alternatively replace the inline traversal with a lookup against a
precomputed index built outside the effect. Ensure you keep the same behavior
for empty/trimmed query (clearing setResults) and reference the same symbols
(useEffect, query, setResults, sigma.getGraph, graph.forEachNode) when making
the change.
In `@frontend/src/components/DependencyGraph/GraphView/useGraphData.ts`:
- Around line 114-128: forceAtlas2.assign is being called synchronously with
iterations: 400 which blocks the main thread for medium/large graphs; update
useGraphData to avoid UI freezes by adapting the layout strategy: detect large
graphs via graph.order and either reduce iterations (e.g., lower iterations when
graph.order > X) or switch to the worker-based API offered by
graphology-layout-forceatlas2 (use the Web Worker wrapper instead of
forceAtlas2.assign) and preserve barnesHutOptimize, barnesHutTheta, slowDown,
and other settings when creating/dispatching the worker; ensure the code paths
reference forceAtlas2.assign, iterations, and graph.order so reviewers can spot
the conditional change.
- Around line 24-27: Duplicate getDirectory logic in useGraphData and
useMatrixData should be extracted into a single shared utility: create a new
module (e.g., utils.ts) that exports a named function getDirectory(filePath:
string): string with the existing implementation, then remove the local
getDirectory definitions from both useGraphData and useMatrixData and replace
them with imports (import { getDirectory } from './utils'). Ensure the exported
function name matches the existing references so both hooks compile without
other changes.
- Around line 88-112: The current Louvain color assignment uses
communityIds.indexOf(communities[node]) inside graph.forEachNode which yields
O(n²) behavior; replace this with a precomputed Map (e.g., build a Map from
community value to index from communityIds) and use map.get(communities[node])
when setting color in the louvain block (symbols: louvain, communities,
communityIds, COMMUNITY_COLORS, graph.forEachNode, graph.setNodeAttribute). Do
the same optimization in the catch/fallback directory coloring: compute a Map
from directory value to index from directories and use it instead of
directories.indexOf(attrs.directory) when calling graph.setNodeAttribute. Ensure
null/undefined guards if needed and preserve modulo coloring logic (index %
COMMUNITY_COLORS.length).
In `@frontend/src/components/DependencyGraph/index.tsx`:
- Around line 131-143: The empty onFileHover passed into ImpactPanel is a no-op
and will drop hover events; either wire it to a proper handler or mark it
explicitly as intentional. Replace onFileHover={() => {}} with a handler that
forwards the hovered file id to your state (e.g., (fileId) =>
setSelectedFile(fileId) or a dedicated hover state setter) so hover highlights
work, or if you purposely omitted behavior, add a short comment (e.g., // TODO:
wire up hover highlight) next to the ImpactPanel prop to make the intent clear;
reference symbols: ImpactPanel, onFileHover, setSelectedFile.
- Around line 59-89: ViewToggle renders buttons that behave like tabs but lacks
ARIA tab semantics; update the component so the container div has role="tablist"
and each button uses role="tab", aria-selected={active === tab.id}, and
tabIndex={active === tab.id ? 0 : -1} to expose state to assistive tech, and add
an aria-controls attribute on each button (e.g., `${tab.id}-panel`) so the
buttons can be associated with the corresponding panel; keep the existing
onClick logic (onChange) and ensure the tab ids in the tabs array (used by
active) match the panel ids.
In `@frontend/src/components/DependencyGraph/MatrixView/index.tsx`:
- Around line 109-113: The onMouseEnter handler currently returns undefined for
cells where value === 0 and isCycle === false, which can leave a stale tooltip
if events are coalesced; change the ternary so the else branch explicitly calls
onCellHover(null) instead of returning undefined (update the JSX handler that
references onCellHover, value, and isCycle in the MatrixView cell to call
onCellHover(null) when the cell is empty), ensuring onMouseLeave and
onMouseEnter both clear hovered state consistently.
- Around line 41-48: MatrixGrid and MatrixView both compute the same cycleSet
from activeCycles causing duplicate work; compute cycleSet once in MatrixView
(using the existing useMemo that builds a Set<string> from activeCycles) and
remove the duplicate computation inside MatrixGrid, then pass that cycleSet into
MatrixGrid as a new prop (e.g., cycleSet) and update MatrixGrid to use the
passed-in cycleSet instead of rebuilding it; ensure prop types/TSX signature for
MatrixGrid are updated and all internal references to the local cycleSet are
switched to the prop.
Bugs fixed: 1. clickStage: removed 'undefined as any' cast, widened onSelectFile type to accept string | null for proper type safety 2. Tooltip position: convert container-relative event coords to viewport-relative using getBoundingClientRect() for fixed tooltip 3. NodeTooltip: removed unused nodeId from props interface 4. DependencyGraph: removed unused apiUrl from props, updated caller in DashboardHome.tsx to stop passing it 5. types.ts: removed dead MatrixData interface (replaced by DirectoryMatrixData and FileMatrixData) 6. totalDeps: now counts only cross-directory deps (off-diagonal matrix sum) instead of all edges including intra-directory Improvements: 7. SIGMA_SETTINGS: removed null as any casts for nodeReducer and edgeReducer -- these are set dynamically via setSetting() 8. GraphControls: consolidated fitToScreen and centerGraph into single fit-to-screen button (both were calling animatedReset) 9. MatrixView: cycleSet now computed once in parent MatrixView and passed as prop to MatrixGrid, eliminating duplicate computation Also: onMouseEnter on empty cells now explicitly calls onCellHover(null) instead of returning undefined
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
What
Complete overhaul of the dependency graph visualization. Replaces ReactFlow (SVG) with Sigma.js (WebGL) for performance, adds ForceAtlas2 layout with Louvain community detection, and introduces a new Dependency Structure Matrix (DSM) view.
Why
The old ReactFlow-based graph couldn't handle 239 nodes -- it rendered as tiny unreadable rectangles. The new implementation renders the same data as an interactive, explorable WebGL graph with automatic clustering and a matrix view that surfaces circular dependencies.
Changes
Removed
Added
Testing
Known limitations (tracked)
Linear
Closes OPE-41 through OPE-51
Summary by CodeRabbit
Greptile Summary
Complete replacement of ReactFlow (SVG) with Sigma.js (WebGL) for the dependency graph visualization, plus a new Dependency Structure Matrix (DSM) view. The old 600+ line monolith
index.tsxis decomposed into a cleanGraphView/andMatrixView/module structure with shared types and data hooks.reactflow/dagre/@types/dagreforsigma/graphology/@react-sigma/core/graphology-layout-forceatlas2/graphology-communities-louvain.undefined as anyinclickStagehandler), unusedMatrixDatatype intypes.ts, unusedgetImportsdestructuring, and two new files exceeding the 200-line guideline fromCLAUDE.mdwithout being added to exceptions.Confidence Score: 4/5
undefined as anytype bypass in the clickStage handler, which works at runtime but violates the type contract. Remaining issues are style-level (file lengths, unused code). Build passes per the PR description.GraphView/index.tsx(type-safety issue on line 132) andMatrixView/index.tsx(exceeds 200-line limit).Important Files Changed
undefined as anyon clickStage) and exceeds 200-line limit at 241 lines.getDirectoryhelper fromuseGraphData.tsbut otherwise clean.getImportsdestructuring and no-oponFileHover.MatrixDatainterface that should be removed.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["DependencyGraph<br/>(index.tsx)"] -->|"viewMode = graph"| B["GraphView"] A -->|"viewMode = matrix"| C["MatrixView"] A -->|"selectedFile"| D["ImpactPanel"] A --> E["useImpactAnalysis"] A --> F["useDependencyGraph<br/>(API fetch)"] B --> G["SigmaContainer<br/>(WebGL)"] B --> H["useGraphData"] H --> I["graphology Graph"] H --> J["ForceAtlas2 layout"] H --> K["Louvain clustering"] G --> L["LoadAndDisplay"] G --> M["Interactions<br/>(hover/click/pin)"] G --> N["SearchBar"] G --> O["GraphControls"] C --> P["useDirectoryMatrix"] C --> Q["useFileMatrix<br/>(drill-down)"] C --> R["MatrixGrid"] C --> S["CellTooltip"] M -->|"onSelectFile"| A N -->|"onFocusNode"| A R -->|"onRowClick"| ALast reviewed commit: 4a2c00e
Context used:
dashboard- CLAUDE.md (source)