234 lines
6.6 KiB
TypeScript
234 lines
6.6 KiB
TypeScript
/**
|
|
* 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<string>;
|
|
predicates: Set<string>;
|
|
searchTerm: string;
|
|
/** Hide literal nodes (strings, numbers, dates) from the graph */
|
|
hideLiterals: boolean;
|
|
/** Semantic categories to show (empty = show all) */
|
|
semanticCategories: Set<SemanticCategory>;
|
|
}
|
|
|
|
interface UseGraphDataReturn {
|
|
// Data
|
|
nodes: GraphNode[];
|
|
links: GraphLink[];
|
|
filteredNodes: GraphNode[];
|
|
filteredLinks: GraphLink[];
|
|
|
|
// Metadata
|
|
nodeTypes: string[];
|
|
predicates: string[];
|
|
linkCounts: Record<string, number>;
|
|
nodeCounts: Record<string, number>;
|
|
|
|
// Filters
|
|
filters: GraphFilters;
|
|
setFilters: (filters: Partial<GraphFilters>) => 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<GraphData | null>(null);
|
|
const [filters, setFiltersState] = useState<GraphFilters>(DEFAULT_FILTERS);
|
|
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(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<GraphFilters>) => {
|
|
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,
|
|
};
|
|
}
|