glam/frontend/NEXT_STEPS.md
kempersc 2761857b0d Add scripts for converting OWL/Turtle ontology to Mermaid and PlantUML diagrams
- 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.
2025-11-22 23:01:13 +01:00

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:

  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:

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

  1. D3 v7 Syntax Issues: Check D3 v7 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

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%)