/** * React Hook for Graph Data Management * Processes parsed RDF data for D3.js visualization */ import { useState, useCallback, useMemo } from 'react'; import { extractNodeTypes, extractPredicates, getLinkCounts, getNodeCounts, } from '@/lib/rdf/graph-utils'; import type { GraphData, GraphNode, GraphLink, SemanticCategory } from '@/types/rdf'; import { getSemanticCategory } from '@/types/rdf'; export interface GraphFilters { nodeTypes: Set; predicates: Set; searchTerm: string; /** Hide literal nodes (strings, numbers, dates) from the graph */ hideLiterals: boolean; /** Semantic categories to show (empty = show all) */ semanticCategories: Set; } interface UseGraphDataReturn { // Data nodes: GraphNode[]; links: GraphLink[]; filteredNodes: GraphNode[]; filteredLinks: GraphLink[]; // Metadata nodeTypes: string[]; predicates: string[]; linkCounts: Record; nodeCounts: Record; // Filters filters: GraphFilters; setFilters: (filters: Partial) => void; resetFilters: () => void; // Selection selectedNode: GraphNode | null; setSelectedNode: (node: GraphNode | null) => void; // Data loading loadGraphData: (data: GraphData) => void; clearGraphData: () => void; // Stats stats: { totalNodes: number; totalLinks: number; filteredNodeCount: number; filteredLinkCount: number; }; } const DEFAULT_FILTERS: GraphFilters = { nodeTypes: new Set(), predicates: new Set(), searchTerm: '', hideLiterals: true, // Default to hiding literals for cleaner graphs semanticCategories: new Set(), // Empty = show all categories }; export function useGraphData(): UseGraphDataReturn { const [graphData, setGraphData] = useState(null); const [filters, setFiltersState] = useState(DEFAULT_FILTERS); const [selectedNode, setSelectedNode] = useState(null); // Extract metadata from graph data const nodeTypes = useMemo( () => (graphData ? extractNodeTypes(graphData) : []), [graphData] ); const predicates = useMemo( () => (graphData ? extractPredicates(graphData) : []), [graphData] ); const linkCounts = useMemo( () => (graphData ? getLinkCounts(graphData) : {}), [graphData] ); const nodeCounts = useMemo( () => (graphData ? getNodeCounts(graphData) : {}), [graphData] ); // Get raw nodes and links const nodes = graphData?.nodes || []; const links = graphData?.links || []; // Apply filters to nodes const filteredNodes = useMemo(() => { if (!graphData) return []; let result = nodes; // Filter by semantic categories (if any selected) if (filters.semanticCategories.size > 0) { result = result.filter((node) => { const category = getSemanticCategory(node.type); return filters.semanticCategories.has(category); }); } // Filter out literals if hideLiterals is enabled if (filters.hideLiterals) { result = result.filter((node) => { const category = getSemanticCategory(node.type); return category !== 'literals'; }); } // Filter by node type if (filters.nodeTypes.size > 0) { result = result.filter((node) => filters.nodeTypes.has(node.type)); } // Filter by search term if (filters.searchTerm) { const term = filters.searchTerm.toLowerCase(); result = result.filter( (node) => node.id.toLowerCase().includes(term) || node.label?.toLowerCase().includes(term) ); } return result; }, [graphData, nodes, filters]); // Apply filters to links and expand to include connected nodes const { filteredLinks, expandedNodes } = useMemo(() => { if (!graphData) return { filteredLinks: [], expandedNodes: [] as GraphNode[] }; // Start with the filtered nodes based on type/search const primaryNodeIds = new Set(filteredNodes.map((n) => n.id)); // Find links where at least ONE end is a primary node // This ensures we see connections FROM the filtered nodes let relevantLinks = 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 primaryNodeIds.has(sourceId) || primaryNodeIds.has(targetId); }); // Filter by predicate if specified if (filters.predicates.size > 0) { relevantLinks = relevantLinks.filter((link) => filters.predicates.has(link.predicate)); } // Collect all nodes involved in the links const expandedNodeIds = new Set(primaryNodeIds); for (const link of relevantLinks) { const sourceId = typeof link.source === 'string' ? link.source : link.source.id; const targetId = typeof link.target === 'string' ? link.target : link.target.id; expandedNodeIds.add(sourceId); expandedNodeIds.add(targetId); } // Return expanded set of nodes (primary + connected) const expanded = nodes.filter((n) => expandedNodeIds.has(n.id)); return { filteredLinks: relevantLinks, expandedNodes: expanded }; }, [graphData, nodes, links, filteredNodes, filters.predicates]); // Use expandedNodes instead of filteredNodes for display const displayNodes = expandedNodes.length > 0 ? expandedNodes : filteredNodes; // Load graph data const loadGraphData = useCallback((data: GraphData) => { setGraphData(data); setSelectedNode(null); // Reset filters when loading new data setFiltersState(DEFAULT_FILTERS); }, []); // Clear graph data const clearGraphData = useCallback(() => { setGraphData(null); setSelectedNode(null); setFiltersState(DEFAULT_FILTERS); }, []); // Update filters const setFilters = useCallback((newFilters: Partial) => { setFiltersState((prev) => ({ ...prev, ...newFilters, })); }, []); // Reset filters const resetFilters = useCallback(() => { setFiltersState(DEFAULT_FILTERS); }, []); // Stats const stats = useMemo( () => ({ totalNodes: nodes.length, totalLinks: links.length, filteredNodeCount: displayNodes.length, filteredLinkCount: filteredLinks.length, }), [nodes.length, links.length, displayNodes.length, filteredLinks.length] ); return { nodes, links, filteredNodes: displayNodes, filteredLinks, nodeTypes, predicates, linkCounts, nodeCounts, filters, setFilters, resetFilters, selectedNode, setSelectedNode, loadGraphData, clearGraphData, stats, }; }