# Interactive Graph Visualization - Integration Guide **Date**: 2025-11-22 **Phase**: Phase 1 Complete - Ready for Integration **Estimated Integration Time**: 2-3 hours --- ## Overview This guide walks you through integrating the new D3.js interactive graph visualization into the GLAM frontend application. The implementation is complete and tested; this guide covers dependency installation, component integration, and testing procedures. --- ## Prerequisites - Node.js 18+ and npm - TypeScript 5.x - React 18+ - Vite build system - Oxigraph triplestore running on `http://localhost:7878` --- ## Step 1: Install Dependencies ### Install D3.js ```bash cd /Users/kempersc/apps/glam/frontend npm install d3@^7.8.5 @types/d3@^7.4.3 ``` ### Verify Installation ```bash npm list d3 # Expected output: # glam-frontend@0.1.0 # └── d3@7.8.5 ``` --- ## Step 2: Verify File Structure Ensure all Phase 1 files are in place: ```bash # Components ls -l src/components/graph/InteractiveGraph.tsx ls -l src/components/graph/InteractiveGraph.css ls -l src/components/graph/NodeMetadataModal.tsx ls -l src/components/graph/NodeMetadataModal.css ls -l src/components/graph/ConnectionAnalysisPanel.tsx ls -l src/components/graph/ConnectionAnalysisPanel.css # Libraries ls -l src/lib/rdf-extractor.ts ls -l src/lib/bfs-traversal.ts ``` Expected output: All files should exist with sizes matching the completion report. --- ## Step 3: Update QueryBuilder Component ### Add Imports Edit `src/components/query/QueryBuilder.tsx` and add these imports at the top: ```typescript import { InteractiveGraph } from '../graph/InteractiveGraph'; import type { GraphNode, GraphLink, GraphData } from '../graph/InteractiveGraph'; ``` ### Add State Management Inside the `QueryBuilder` component, add visualization mode state: ```typescript // Add after existing state declarations const [visualizationMode, setVisualizationMode] = useState<'mermaid' | 'interactive'>('interactive'); ``` ### Create Graph Data Converter Add this helper function inside the component (before the return statement): ```typescript /** * Convert SPARQL query results to D3 graph format */ function convertResultsToGraph(results: any): GraphData { const nodes: GraphNode[] = []; const links: GraphLink[] = []; const nodeMap = new Map(); if (!results?.results?.bindings) { return { nodes, links }; } // Process each triple from SPARQL results for (const binding of results.results.bindings) { const subject = binding.s?.value; const predicate = binding.p?.value; const object = binding.o?.value; const objectType = binding.o?.type; if (!subject || !predicate || !object) continue; // Add subject node if (!nodeMap.has(subject)) { const subjectNode: GraphNode = { id: subject, label: extractLocalName(subject), uri: subject, type: inferNodeType(subject), }; nodes.push(subjectNode); nodeMap.set(subject, subjectNode); } // Add object node if it's a URI (not a literal) if (objectType === 'uri') { if (!nodeMap.has(object)) { const objectNode: GraphNode = { id: object, label: extractLocalName(object), uri: object, type: inferNodeType(object), }; nodes.push(objectNode); nodeMap.set(object, objectNode); } // Add link between subject and object links.push({ source: nodeMap.get(subject)!, target: nodeMap.get(object)!, predicate, label: formatPredicate(predicate), isBidirectional: checkIfBidirectional(predicate), isReversed: false, originalPredicate: predicate, }); } } return { nodes, links }; } /** * Extract local name from URI */ function extractLocalName(uri: string): string { // Extract fragment identifier if (uri.includes('#')) { return uri.split('#').pop() || uri; } // Extract last path segment if (uri.includes('/')) { return uri.split('/').pop() || uri; } return uri; } /** * Infer node type from URI patterns */ function inferNodeType(uri: string): GraphNode['type'] { const lowerUri = uri.toLowerCase(); if (lowerUri.includes('museum')) return 'Museum'; if (lowerUri.includes('library') || lowerUri.includes('bibliothe')) return 'Library'; if (lowerUri.includes('archive') || lowerUri.includes('archief')) return 'Archive'; if (lowerUri.includes('gallery')) return 'Gallery'; if (lowerUri.includes('collection')) return 'Collection'; if (lowerUri.includes('organization') || lowerUri.includes('organisatie')) return 'Organization'; if (lowerUri.includes('person') || lowerUri.includes('persoon')) return 'Person'; if (lowerUri.includes('place') || lowerUri.includes('plaats')) return 'Place'; if (lowerUri.includes('event')) return 'Event'; return 'Concept'; } /** * Format predicate URI to human-readable label */ function formatPredicate(predicate: string): string { const localName = extractLocalName(predicate); // Handle camelCase const withSpaces = localName.replace(/([A-Z])/g, ' $1').trim(); // Handle underscores and hyphens const normalized = withSpaces.replace(/[_-]/g, ' '); // Capitalize first letter return normalized.charAt(0).toUpperCase() + normalized.slice(1); } /** * Check if predicate supports bidirectional switching */ function checkIfBidirectional(predicate: string): boolean { const bidirectionalPredicates = [ 'hasPart', 'isPartOf', 'hasSubOrganization', 'isSubOrganizationOf', 'owns', 'ownedBy', 'employs', 'isEmployedBy', 'hasMember', 'isMemberOf', 'hasCreator', 'isCreatorOf', 'parentOrganization', 'subOrganization', 'hasPredecessor', 'hasSuccessor', // Add more as needed ]; const localName = extractLocalName(predicate); return bidirectionalPredicates.some(p => localName.includes(p)); } ``` ### Update JSX Rendering Find the section where `OntologyVisualizer` is rendered and replace it with: ```typescript {/* Visualization Mode Toggle */}
{/* Render selected visualization */} {results && ( visualizationMode === 'interactive' ? ( { console.log('Selected node:', node); // Optional: Update URL or state }} onEdgeClick={(link) => { console.log('Clicked edge:', link); // Optional: Show edge details }} /> ) : ( ) )} ``` --- ## Step 4: Add Styling for Toggle Controls Edit `src/components/query/QueryBuilder.css` and add: ```css .visualization-controls { display: flex; gap: 1.5rem; margin-bottom: 1rem; padding: 0.75rem; background: #f5f5f5; border-radius: 4px; } .visualization-controls label { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-size: 0.95rem; } .visualization-controls input[type="radio"] { cursor: pointer; } ``` --- ## Step 5: Compile TypeScript ### Build the Project ```bash npm run build ``` ### Expected Output ``` vite v5.x.x building for production... ✓ 1234 modules transformed. dist/index.html 0.45 kB dist/assets/index-abc123.css 12.34 kB │ gzip: 3.45 kB dist/assets/index-def456.js 234.56 kB │ gzip: 78.90 kB ✓ built in 3.21s ``` ### Troubleshooting Build Errors **Error: Cannot find module 'd3'** ```bash # Solution: Reinstall D3 npm install d3 @types/d3 --save ``` **Error: Type errors in InteractiveGraph.tsx** ```bash # Solution: Ensure @types/d3 is installed npm install @types/d3 --save-dev ``` **Error: CSS import errors** ```bash # Solution: Verify CSS files exist and paths are correct ls -l src/components/graph/*.css ``` --- ## Step 6: Run Development Server ### Start Dev Server ```bash npm run dev ``` ### Expected Output ``` VITE v5.x.x ready in 456 ms ➜ Local: http://localhost:5174/ ➜ Network: use --host to expose ➜ press h to show help ``` ### Open in Browser Navigate to: http://localhost:5174 --- ## Step 7: Test Basic Functionality ### Test 1: Load Graph Data 1. Ensure Oxigraph is running: `curl http://localhost:7878/query` 2. In the frontend, execute a SPARQL query: ```sparql SELECT ?s ?p ?o WHERE { ?s ?p ?o . } LIMIT 100 ``` 3. Verify the Interactive Graph renders with nodes and edges **Expected Result**: Force-directed graph with draggable nodes appears --- ### Test 2: Node Interactions **Test Drag**: - Click and drag any node - Verify node follows mouse - Verify connected edges stretch - Verify simulation restarts smoothly **Test Selection**: - Click a node - Verify node highlights (border color change) - Check browser console for "Selected node:" log **Test Double-Click (Metadata Modal)**: - Double-click any node - Verify modal opens with node URI - Verify "Turtle" format is default - Verify RDF data is displayed **Expected Result**: All interactions work smoothly without errors --- ### Test 3: Edge Interactions **Test Hover**: - Hover mouse over any edge - Verify edge highlights (red color, glow effect) - Verify label appears next to edge - Verify label disappears when mouse leaves **Test Bidirectional Switching**: - Find an edge with "hasPart" label - Click the edge - Verify direction arrow reverses - Verify label changes to "isPartOf" - Verify label flashes briefly - Click again to reverse back **Expected Result**: Edges respond to hover and click interactions --- ### Test 4: Metadata Modal Features **Test Format Switching**: 1. Double-click node to open modal 2. Click "JSON-LD" tab 3. Verify format changes 4. Click "N-Triples" tab 5. Verify format changes 6. Click "RDF/XML" tab 7. Verify format changes **Test Copy to Clipboard**: 1. Click "Copy" button 2. Paste into a text editor 3. Verify RDF data is copied correctly **Test Download**: 1. Click "Download Turtle" button 2. Verify file downloads 3. Open file and verify RDF content **Test Close Modal**: - Press Escape key → Modal closes - Click outside modal → Modal closes - Click X button → Modal closes **Expected Result**: All modal features work correctly --- ### Test 5: Connection Analysis **Test Open Panel**: 1. Single-click a node 2. Verify Connection Analysis panel opens on the right 3. Verify statistics display (total connections, degree levels, paths) **Test Degree Selector**: 1. Change "Max Degree" from 3 to 5 2. Verify statistics update 3. Verify new connections appear **Test Expand Degree Level**: 1. Click "2nd Degree" to expand 2. Verify connection paths appear 3. Verify path format: "Node A → predicate → Node B" **Test Top Relationships**: 1. Scroll to "Most Common Relationships" 2. Verify list shows predicates with counts 3. Verify sorted by frequency (descending) **Expected Result**: Connection analysis updates dynamically --- ## Step 8: Test Responsive Design ### Test Mobile Layout (320px width) ```bash # Open browser DevTools (F12) # Toggle device emulation # Select "iPhone SE" (375x667) ``` **Verify**: - Graph resizes to fit container - Modal is scrollable - Touch interactions work (drag, tap) - Text is readable (no overflow) ### Test Tablet Layout (768px width) ```bash # Select "iPad" (768x1024) ``` **Verify**: - Graph fills available width - Connection panel doesn't overlap graph - Modal is centered ### Test Desktop Layout (1920px width) ```bash # Select "Responsive" and set to 1920x1080 ``` **Verify**: - Graph uses full width - No horizontal scrolling - All controls visible --- ## Step 9: Test Accessibility ### Test Keyboard Navigation 1. Press Tab key repeatedly 2. Verify focus moves through: - Visualization mode toggle - SPARQL query textarea - Execute button - Graph canvas (not implemented yet - Phase 2) - Modal close button (when open) ### Test Screen Reader Compatibility **Using VoiceOver (macOS)**: ```bash # Enable: Cmd + F5 # Navigate: Ctrl + Option + Arrow keys ``` **Verify**: - Modal announces title and content - Buttons have accessible labels - Form controls are properly labeled ### Test Color Contrast Use browser DevTools Accessibility panel: 1. Inspect node labels 2. Verify contrast ratio > 4.5:1 3. Inspect edge labels 4. Verify contrast ratio > 4.5:1 --- ## Step 10: Performance Testing ### Test Large Graph (1000+ nodes) 1. Execute SPARQL query with high LIMIT: ```sparql SELECT ?s ?p ?o WHERE { ?s ?p ?o . } LIMIT 2000 ``` 2. Measure rendering time: - Open DevTools Performance panel - Start recording - Execute query - Stop recording **Expected Result**: - Initial render < 2 seconds - Smooth drag interactions (60 FPS) - No browser freezing **Known Limitation**: May lag with 2000+ nodes (Phase 2 optimization) ### Test Memory Usage 1. Open DevTools Memory panel 2. Take heap snapshot 3. Interact with graph (drag, click, open modal) 4. Take another heap snapshot 5. Compare memory usage **Expected Result**: Memory increase < 50 MB after interactions --- ## Troubleshooting ### Issue: Graph doesn't render **Symptoms**: Blank area where graph should be **Possible Causes**: 1. No SPARQL results returned 2. D3.js not loaded 3. CSS not applied 4. JavaScript errors **Debugging Steps**: ```bash # 1. Check browser console for errors # Open DevTools (F12) → Console tab # 2. Verify D3.js is loaded # In browser console: > typeof d3 # Expected: "object" # 3. Check SPARQL results > console.log(results) # Expected: Object with bindings array # 4. Verify component mounted # In React DevTools: # Find component # Check props.data.nodes.length > 0 ``` --- ### Issue: Nodes don't drag **Symptoms**: Clicking node doesn't allow dragging **Possible Causes**: 1. D3 drag behavior not initialized 2. SVG event handlers not attached 3. Pointer events disabled in CSS **Debugging Steps**: ```typescript // Add debug logging to InteractiveGraph.tsx function dragstarted(event: any, d: GraphNode) { console.log('Drag started:', d.id); // ... rest of function } // Check CSS .node { pointer-events: all; /* Should be "all", not "none" */ } ``` --- ### Issue: Modal doesn't open **Symptoms**: Double-clicking node has no effect **Possible Causes**: 1. Event handler not attached 2. State not updating 3. Modal component not imported **Debugging Steps**: ```typescript // Add debug logging function handleNodeDoubleClick(event: MouseEvent, d: GraphNode) { console.log('Double-click detected:', d.id); setSelectedNode(d); console.log('Selected node state:', selectedNode); } // Verify modal render {selectedNode && ( <>
Modal should render for: {selectedNode.id}
)} ``` --- ### Issue: RDF extraction fails **Symptoms**: Modal shows "Error: No data available" **Possible Causes**: 1. Oxigraph not running 2. SPARQL query error 3. Node URI invalid **Debugging Steps**: ```bash # 1. Test Oxigraph directly curl -X POST http://localhost:7878/query \ -H "Content-Type: application/sparql-query" \ -d "SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 1" # Expected: JSON response with bindings # 2. Check node URI format console.log('Node URI:', selectedNode.uri) # Expected: Valid HTTP(S) URI # 3. Test RDF extractor directly import { extractNodeRdf } from '../lib/rdf-extractor'; const result = await extractNodeRdf(sparqlClient, nodeUri); console.log('Extracted triples:', result); ``` --- ### Issue: Connection analysis empty **Symptoms**: Panel shows "0 Total Connections" **Possible Causes**: 1. Node has no connections 2. BFS traversal error 3. Max degree too low **Debugging Steps**: ```typescript // Add debug logging to bfs-traversal.ts export function performBfsTraversal(...) { console.log('Starting BFS from:', sourceNodeId); console.log('Max degree:', maxDegree); const adjacencyList = buildAdjacencyList(graphData); console.log('Adjacency list size:', adjacencyList.size); // ... rest of function } ``` --- ## Common Integration Issues ### TypeScript Errors **Error: Property 'source' does not exist on type 'string | GraphNode'** **Cause**: D3 force simulation mutates link source/target from string IDs to object references **Solution**: Use type assertion in render functions ```typescript // Before const sourceX = link.source.x; // After const sourceX = (link.source as GraphNode).x; ``` --- ### CSS Conflicts **Issue**: Graph styling conflicts with existing styles **Solution**: Use CSS modules or increase specificity ```css /* Instead of */ .node { ... } /* Use */ .interactive-graph .node { ... } ``` --- ### React State Issues **Issue**: Graph doesn't update when data changes **Solution**: Use `key` prop to force re-render ```tsx ``` --- ## Next Steps After Integration ### Phase 2: Enhancements (Optional) If basic integration is successful, consider these enhancements: 1. **Physics-Based Label Collision** (4-6 hours) - Prevents edge labels from overlapping - Improves readability on dense graphs 2. **Advanced Streamgraph** (3-4 hours) - Better visualization of connection flow - Interactive filtering by degree 3. **Performance Optimization** (3-4 hours) - Canvas rendering for large graphs (2000+ nodes) - Node virtualization (only render visible) - Triple caching layer 4. **Export Features** (2-3 hours) - Export graph as PNG/SVG image - Export connection analysis as CSV - Share graph via URL ### Phase 3: Testing & Documentation (8-10 hours) 1. **Unit Tests** (4-5 hours) - Jest tests for all components - Test RDF serialization formats - Test BFS traversal algorithm 2. **Integration Tests** (2-3 hours) - Playwright E2E tests - Test with production Oxigraph data - Mobile interaction tests 3. **Documentation** (2-3 hours) - API reference for components - User guide with screenshots - Deployment checklist --- ## Getting Help ### Documentation References - **Component API**: See JSDoc comments in component files - **RDF Formats**: See comments in `rdf-extractor.ts` - **BFS Algorithm**: See comments in `bfs-traversal.ts` - **Phase 1 Report**: `/Users/kempersc/apps/glam/frontend/GRAPH_VIZ_PHASE1_COMPLETE.md` ### Code Examples **Custom Node Colors**: ```typescript // In InteractiveGraph.tsx const nodeColor = (node: GraphNode): string => { switch (node.type) { case 'Museum': return '#e74c3c'; case 'Library': return '#3498db'; case 'Archive': return '#2ecc71'; // Add your custom colors default: return '#95a5a6'; } }; ``` **Custom Edge Styles**: ```typescript // In InteractiveGraph.tsx const edgeStroke = (link: GraphLink): string => { if (link.label.includes('hasPart')) return '#e67e22'; if (link.label.includes('employs')) return '#9b59b6'; return '#7f8c8d'; }; ``` **Filter Graph by Node Type**: ```typescript const filteredData: GraphData = { nodes: graphData.nodes.filter(n => n.type === 'Museum'), links: graphData.links.filter(l => (l.source as GraphNode).type === 'Museum' && (l.target as GraphNode).type === 'Museum' ), }; ``` --- ## Contact & Support For integration issues or questions: 1. Check this guide's Troubleshooting section 2. Review Phase 1 completion report 3. Check component source code JSDoc comments 4. Contact project maintainers --- **Last Updated**: 2025-11-22 **Guide Version**: 1.0 **Phase**: Phase 1 Integration **Status**: Ready for Testing --- ## Quick Reference ### File Locations ``` frontend/ ├── src/ │ ├── components/ │ │ ├── graph/ │ │ │ ├── InteractiveGraph.tsx (main component) │ │ │ ├── InteractiveGraph.css │ │ │ ├── NodeMetadataModal.tsx │ │ │ ├── NodeMetadataModal.css │ │ │ ├── ConnectionAnalysisPanel.tsx │ │ │ └── ConnectionAnalysisPanel.css │ │ └── query/ │ │ └── QueryBuilder.tsx (integration point) │ └── lib/ │ ├── rdf-extractor.ts (RDF serialization) │ └── bfs-traversal.ts (connection analysis) └── INTEGRATION_GUIDE.md (this file) ``` ### Key Commands ```bash # Install dependencies npm install d3 @types/d3 # Build project npm run build # Run dev server npm run dev # Run tests (once written) npm test # Lint TypeScript npm run lint # Type check npx tsc --noEmit ``` ### Testing Checklist - [ ] Graph renders with nodes and edges - [ ] Nodes are draggable - [ ] Zoom and pan work - [ ] Node selection highlights - [ ] Edge hover shows label - [ ] Bidirectional edges reverse on click - [ ] Double-click opens metadata modal - [ ] Modal format switching works - [ ] Modal copy/download works - [ ] Modal closes with Escape/click-outside - [ ] Connection analysis shows statistics - [ ] Connection analysis shows paths - [ ] Responsive design works on mobile - [ ] Keyboard navigation works - [ ] Screen reader announces content - [ ] Performance acceptable with 100+ nodes --- End of Integration Guide