- Implemented `owl_to_mermaid.py` to convert OWL/Turtle files into Mermaid class diagrams. - Implemented `owl_to_plantuml.py` to convert OWL/Turtle files into PlantUML class diagrams. - Added two new PlantUML files for custodian multi-aspect diagrams.
13 KiB
🎯 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:
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:
/**
* 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<GraphData>;
isLoading: boolean;
error: Error | null;
graphData: GraphData | null;
}
export function useRdfParser(): UseRdfParserReturn {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [graphData, setGraphData] = useState<GraphData | null>(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
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 = `
<http://example.org/record1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://www.ica.org/standards/RiC/ontology#Record> .
`;
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:
# 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:
/**
* 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<typeof calculateGraphStats>;
}
export function useGraphData(
options: UseGraphDataOptions = {}
): UseGraphDataReturn {
const [graphData] = useState<GraphData>(
options.initialData || { nodes: [], links: [] }
);
const [filterTypes, setFilterTypes] = useState<NodeType[]>([]);
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:
# 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:
- D3 v7 force simulation
- SVG rendering with React refs
- Node rendering (colored circles)
- Link rendering (curved paths)
- Zoom and pan
- Drag behavior
- Bidirectional edge clicking
Example Structure:
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<SVGSVGElement>(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<SVGSVGElement, unknown>()
.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<SVGCircleElement, GraphNode>()
.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 (
<svg
ref={svgRef}
width={width}
height={height}
style={{ border: '1px solid #ccc' }}
/>
);
}
Commands:
# 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:
- useRdfParser hook working with tests
- useGraphData hook working with tests
- ForceDirectedGraph component rendering
- Bidirectional edge clicking working
- Zoom, pan, drag working
- GraphControls component functional
- Integration tests passing
- Test coverage maintained at 80%+
🎯 Final Verification
# 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
🆘 If You Get Stuck
- D3 v7 Syntax Issues: Check D3 v7 Migration Guide
- TypeScript Errors: Add
as anytemporarily, fix types later - React + D3 Issues: Use refs, avoid mixing React and D3 DOM manipulation
- Test Failures: Start with
npm run test:uifor debugging
💾 Commit After Session
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?
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%)