- 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.
561 lines
13 KiB
Markdown
561 lines
13 KiB
Markdown
# 🎯 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<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`
|
|
|
|
```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 = `
|
|
<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**:
|
|
```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<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**:
|
|
```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<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**:
|
|
```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%)*
|