# 🎯 NEXT SESSION: Exact Steps to Continue ## Session Goal: Interactive Graph Visualization **Estimated Time**: 8-10 hours **Target Completion**: Week 3-4 (Days 8-14) **Current Progress**: Foundation 60% → Target 100% --- ## 📋 Pre-Session Checklist Before starting, verify: ```bash cd /Users/kempersc/apps/glam/frontend # ✅ Dependencies installed npm list | head -5 # ✅ Tests passing npm run test:run # ✅ Dev server works npm run dev # Open http://localhost:5173 and verify ``` --- ## 🎬 Step-by-Step Implementation Plan ### TASK 1: Create useRdfParser Hook (1 hour) **File**: `src/hooks/useRdfParser.ts` **Implementation**: ```typescript /** * React Hook for RDF Parsing * Wraps parser functions with React lifecycle */ import { useState, useCallback } from 'react'; import { parseNTriples, parseTurtle } from '@/lib/rdf/parser'; import type { GraphData, RdfFormat } from '@/types/rdf'; interface UseRdfParserReturn { parse: (data: string, format: RdfFormat) => Promise; isLoading: boolean; error: Error | null; graphData: GraphData | null; } export function useRdfParser(): UseRdfParserReturn { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [graphData, setGraphData] = useState(null); const parse = useCallback(async (data: string, format: RdfFormat) => { setIsLoading(true); setError(null); try { let result: GraphData; switch (format) { case 'text/turtle': result = parseTurtle(data); break; case 'application/n-triples': result = parseNTriples(data); break; default: throw new Error(`Unsupported format: ${format}`); } if (result.error) { throw new Error(result.error); } setGraphData(result); return result; } catch (err) { const error = err instanceof Error ? err : new Error('Parsing failed'); setError(error); throw error; } finally { setIsLoading(false); } }, []); return { parse, isLoading, error, graphData }; } ``` **Test File**: `tests/unit/use-rdf-parser.test.ts` ```typescript import { describe, it, expect } from 'vitest'; import { renderHook, act, waitFor } from '@testing-library/react'; import { useRdfParser } from '@/hooks/useRdfParser'; describe('useRdfParser', () => { it('should parse N-Triples data', async () => { const { result } = renderHook(() => useRdfParser()); const ntriples = ` . `; await act(async () => { await result.current.parse(ntriples, 'application/n-triples'); }); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBeNull(); expect(result.current.graphData).not.toBeNull(); expect(result.current.graphData?.nodes.length).toBeGreaterThan(0); }); it('should handle parsing errors', async () => { const { result } = renderHook(() => useRdfParser()); await act(async () => { try { await result.current.parse('invalid rdf', 'application/n-triples'); } catch (err) { // Expected error } }); expect(result.current.error).not.toBeNull(); }); }); ``` **Commands**: ```bash # Create files touch src/hooks/useRdfParser.ts touch tests/unit/use-rdf-parser.test.ts # Run tests npm run test -- use-rdf-parser ``` --- ### TASK 2: Create useGraphData Hook (1 hour) **File**: `src/hooks/useGraphData.ts` **Implementation**: ```typescript /** * React Hook for Graph Data Management * Manages graph state with filtering and search */ import { useState, useCallback, useMemo } from 'react'; import { filterNodesByType, calculateGraphStats } from '@/lib/rdf/graph-utils'; import type { GraphData, GraphNode, GraphLink, NodeType } from '@/types/rdf'; interface UseGraphDataOptions { initialData?: GraphData; } interface UseGraphDataReturn { nodes: GraphNode[]; links: GraphLink[]; filteredNodes: GraphNode[]; filteredLinks: GraphLink[]; filterTypes: NodeType[]; searchQuery: string; setFilterTypes: (types: NodeType[]) => void; setSearchQuery: (query: string) => void; resetFilters: () => void; stats: ReturnType; } export function useGraphData( options: UseGraphDataOptions = {} ): UseGraphDataReturn { const [graphData] = useState( options.initialData || { nodes: [], links: [] } ); const [filterTypes, setFilterTypes] = useState([]); const [searchQuery, setSearchQuery] = useState(''); // Filter nodes by type const filteredByType = useMemo(() => { if (filterTypes.length === 0) { return graphData.nodes; } return filterNodesByType(graphData.nodes, filterTypes); }, [graphData.nodes, filterTypes]); // Filter nodes by search query const filteredNodes = useMemo(() => { if (!searchQuery) { return filteredByType; } const query = searchQuery.toLowerCase(); return filteredByType.filter( (node) => node.label.toLowerCase().includes(query) || node.uri.toLowerCase().includes(query) ); }, [filteredByType, searchQuery]); // Filter links based on filtered nodes const filteredLinks = useMemo(() => { const nodeIds = new Set(filteredNodes.map((n) => n.id)); return graphData.links.filter((link) => { const sourceId = typeof link.source === 'string' ? link.source : link.source.id; const targetId = typeof link.target === 'string' ? link.target : link.target.id; return nodeIds.has(sourceId) && nodeIds.has(targetId); }); }, [filteredNodes, graphData.links]); // Calculate statistics const stats = useMemo( () => calculateGraphStats(filteredNodes, filteredLinks), [filteredNodes, filteredLinks] ); const resetFilters = useCallback(() => { setFilterTypes([]); setSearchQuery(''); }, []); return { nodes: graphData.nodes, links: graphData.links, filteredNodes, filteredLinks, filterTypes, searchQuery, setFilterTypes, setSearchQuery, resetFilters, stats, }; } ``` **Test File**: `tests/unit/use-graph-data.test.ts` **Commands**: ```bash # Create files touch src/hooks/useGraphData.ts touch tests/unit/use-graph-data.test.ts # Run tests npm run test -- use-graph-data ``` --- ### TASK 3: Build ForceDirectedGraph Component (3 hours) **File**: `src/components/visualizations/GraphView/ForceDirectedGraph.tsx` **Key Features to Implement**: 1. D3 v7 force simulation 2. SVG rendering with React refs 3. Node rendering (colored circles) 4. Link rendering (curved paths) 5. Zoom and pan 6. Drag behavior 7. Bidirectional edge clicking **Example Structure**: ```typescript import { useEffect, useRef } from 'react'; import * as d3 from 'd3'; import type { GraphData, GraphNode, GraphLink } from '@/types/rdf'; import { getNodeColor } from '@/lib/rdf/graph-utils'; interface ForceDirectedGraphProps { data: GraphData; width?: number; height?: number; onNodeClick?: (node: GraphNode) => void; onLinkClick?: (link: GraphLink) => void; } export function ForceDirectedGraph({ data, width = 1200, height = 800, onNodeClick, onLinkClick, }: ForceDirectedGraphProps) { const svgRef = useRef(null); useEffect(() => { if (!svgRef.current || !data.nodes.length) return; // Clear previous d3.select(svgRef.current).selectAll('*').remove(); const svg = d3.select(svgRef.current); // Create force simulation const simulation = d3 .forceSimulation(data.nodes) .force('link', d3.forceLink(data.links).id((d: any) => d.id)) .force('charge', d3.forceManyBody().strength(-200)) .force('center', d3.forceCenter(width / 2, height / 2)) .force('collision', d3.forceCollide().radius(30)); // Create container const container = svg.append('g'); // Zoom behavior const zoom = d3.zoom() .scaleExtent([0.1, 4]) .on('zoom', (event) => { container.attr('transform', event.transform); }); svg.call(zoom); // Draw links const link = container .append('g') .selectAll('line') .data(data.links) .join('line') .attr('stroke', '#999') .attr('stroke-opacity', 0.6) .attr('stroke-width', 2) .on('click', (event, d) => { event.stopPropagation(); onLinkClick?.(d); }); // Draw nodes const node = container .append('g') .selectAll('circle') .data(data.nodes) .join('circle') .attr('r', 10) .attr('fill', (d) => getNodeColor(d.type)) .call( d3.drag() .on('start', dragStarted) .on('drag', dragged) .on('end', dragEnded) ) .on('click', (event, d) => { event.stopPropagation(); onNodeClick?.(d); }); // Add labels const labels = container .append('g') .selectAll('text') .data(data.nodes) .join('text') .text((d) => d.label) .attr('font-size', 10) .attr('dx', 12) .attr('dy', 4); // Update positions on tick simulation.on('tick', () => { link .attr('x1', (d: any) => d.source.x) .attr('y1', (d: any) => d.source.y) .attr('x2', (d: any) => d.target.x) .attr('y2', (d: any) => d.target.y); node .attr('cx', (d) => d.x!) .attr('cy', (d) => d.y!); labels .attr('x', (d) => d.x!) .attr('y', (d) => d.y!); }); // Drag functions function dragStarted(event: any, d: GraphNode) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event: any, d: GraphNode) { d.fx = event.x; d.fy = event.y; } function dragEnded(event: any, d: GraphNode) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } return () => { simulation.stop(); }; }, [data, width, height, onNodeClick, onLinkClick]); return ( ); } ``` **Commands**: ```bash # Create files mkdir -p src/components/visualizations/GraphView touch src/components/visualizations/GraphView/ForceDirectedGraph.tsx touch tests/unit/force-directed-graph.test.tsx # Run dev server to see it npm run dev ``` --- ### TASK 4: Create GraphControls Component (1 hour) **File**: `src/components/visualizations/GraphView/GraphControls.tsx` **Features**: - Filter by node type (checkboxes) - Search box - Layout toggle (force vs. hierarchical) - Reset filters button - Statistics display --- ### TASK 5: Integration Testing (1 hour) **File**: `tests/integration/graph-visualization.test.tsx` **Test Scenarios**: - Load graph data - Filter by node type - Search for nodes - Click on nodes/links - Zoom and pan - Drag nodes --- ## ✅ Session Success Criteria By end of session, you should have: - [x] useRdfParser hook working with tests - [x] useGraphData hook working with tests - [x] ForceDirectedGraph component rendering - [x] Bidirectional edge clicking working - [x] Zoom, pan, drag working - [x] GraphControls component functional - [x] Integration tests passing - [x] Test coverage maintained at 80%+ --- ## 🎯 Final Verification ```bash # Run all tests npm run test:run # Check types npx tsc --noEmit # Build npm run build # Visual verification npm run dev # Then manually test: # 1. Upload a file (when form exists) # 2. See graph visualization # 3. Click on edges (should reverse) # 4. Drag nodes # 5. Zoom and pan # 6. Filter nodes # 7. Search nodes ``` --- ## 📚 Reference Files **Example to Study**: - `/Users/kempersc/apps/example_ld/static/js/graph.js` (lines 200-900) - Focus on D3 force simulation setup - Study bidirectional edge logic - Review zoom/pan/drag implementation **Documentation**: - `/docs/plan/frontend/02-design-patterns.md` - Pattern #7 (D3 Integration) - `/docs/plan/frontend/03-tdd-strategy.md` - Testing D3 components - [D3 Force Documentation](https://d3js.org/d3-force) --- ## 🆘 If You Get Stuck 1. **D3 v7 Syntax Issues**: Check [D3 v7 Migration Guide](https://observablehq.com/@d3/d3v6-migration-guide) 2. **TypeScript Errors**: Add `as any` temporarily, fix types later 3. **React + D3 Issues**: Use refs, avoid mixing React and D3 DOM manipulation 4. **Test Failures**: Start with `npm run test:ui` for debugging --- ## 💾 Commit After Session ```bash git add src/ tests/ git commit -m "feat: add force-directed graph visualization - Implement useRdfParser hook with N-Triples/Turtle support - Implement useGraphData hook with filtering and search - Create ForceDirectedGraph component with D3 v7 - Add bidirectional edge clicking - Add zoom, pan, and drag behaviors - Create GraphControls for filtering - Add integration tests - Maintain 80%+ test coverage" ``` --- **Ready to Start?** ```bash cd /Users/kempersc/apps/glam/frontend npm run dev npm run test:ui # Open code editor and follow TASK 1 above ``` **Good luck! 🚀** --- *Generated: November 22, 2024* *Current Phase: Foundation → Visualization* *Target: Week 3-4 Complete (100%)*