/** * Ontology Visualizer Component * * Renders RDF ontologies as Mermaid diagrams (class diagrams and ER diagrams). * Supports loading from .mmd files or generating from RDF data. */ import React, { useEffect, useRef, useState } from 'react'; import type { SparqlClient } from '../../lib/sparql/client'; import './OntologyVisualizer.css'; // Lazy load mermaid to avoid bundling issues let mermaidInstance: typeof import('mermaid').default | null = null; const getMermaid = async () => { if (!mermaidInstance) { const mod = await import('mermaid'); mermaidInstance = mod.default; mermaidInstance.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose', fontFamily: 'Arial, sans-serif', logLevel: 'error', }); } return mermaidInstance; }; export interface OntologyVisualizerProps { /** Pre-loaded Mermaid diagram source */ mermaidSource?: string; /** SPARQL client for querying RDF data */ sparqlClient?: SparqlClient; /** Diagram type to generate from RDF */ diagramType?: 'class' | 'er'; /** Max height of diagram container */ maxHeight?: string; /** Show toolbar with zoom/download controls */ showToolbar?: boolean; } export const OntologyVisualizer: React.FC = ({ mermaidSource, sparqlClient, diagramType = 'class', maxHeight = '800px', showToolbar = true, }) => { const containerRef = useRef(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [zoom, setZoom] = useState(1); const [generatedSource, setGeneratedSource] = useState(null); const [mermaidReady, setMermaidReady] = useState(false); // Initialize Mermaid (lazy loaded) useEffect(() => { getMermaid().then(() => setMermaidReady(true)).catch(console.error); }, []); // Generate Mermaid diagram from RDF data const generateFromRdf = async () => { if (!sparqlClient) return; setIsLoading(true); setError(null); try { if (diagramType === 'class') { const source = await generateClassDiagram(sparqlClient); setGeneratedSource(source); } else { const source = await generateErDiagram(sparqlClient); setGeneratedSource(source); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to generate diagram'); } finally { setIsLoading(false); } }; // Render Mermaid diagram useEffect(() => { const source = mermaidSource || generatedSource; if (!source || !containerRef.current || !mermaidReady) return; const renderDiagram = async () => { try { const mermaid = await getMermaid(); const { svg } = await mermaid.render('mermaid-diagram', source); if (containerRef.current) { containerRef.current.innerHTML = svg; } } catch (err) { console.error('Mermaid render error:', err); setError('Failed to render diagram. Check Mermaid syntax.'); } }; renderDiagram(); }, [mermaidSource, generatedSource, mermaidReady]); // Generate diagram when sparqlClient is provided useEffect(() => { if (sparqlClient && !mermaidSource) { generateFromRdf(); } }, [sparqlClient, diagramType]); // Download diagram as SVG const handleDownload = () => { if (!containerRef.current) return; const svg = containerRef.current.querySelector('svg'); if (!svg) return; const svgData = new XMLSerializer().serializeToString(svg); const blob = new Blob([svgData], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `ontology-diagram-${Date.now()}.svg`; link.click(); URL.revokeObjectURL(url); }; // Zoom controls const handleZoomIn = () => setZoom((z) => Math.min(z + 0.1, 3)); const handleZoomOut = () => setZoom((z) => Math.max(z - 0.1, 0.3)); const handleZoomReset = () => setZoom(1); if (isLoading) { return (

Generating ontology diagram...

); } if (error) { return (

Failed to load diagram

{error}

{sparqlClient && ( )}
); } return (
{showToolbar && (
{Math.round(zoom * 100)}%
{sparqlClient && ( )}
)}
); }; /** * Generate Mermaid class diagram from RDF data */ async function generateClassDiagram(client: SparqlClient): Promise { // Query for classes and their properties const query = ` PREFIX rdfs: PREFIX owl: SELECT DISTINCT ?class ?label ?superclass WHERE { ?class a owl:Class . OPTIONAL { ?class rdfs:label ?label } OPTIONAL { ?class rdfs:subClassOf ?superclass } FILTER(isIRI(?class)) } ORDER BY ?class LIMIT 50 `; const results = await client.executeSelect(query); const classes = new Map(); // Build class hierarchy for (const binding of results.results.bindings) { const classUri = binding.class?.value; if (!classUri) continue; const className = extractLocalName(classUri); const label = binding.label?.value || className; const superclass = binding.superclass?.value; if (!classes.has(className)) { classes.set(className, { label, superclasses: [] }); } if (superclass) { const superclassName = extractLocalName(superclass); classes.get(className)!.superclasses.push(superclassName); } } // Generate Mermaid syntax let mermaid = 'classDiagram\n'; mermaid += ' %% Heritage Custodian Ontology - Class Diagram\n'; mermaid += ' %% Auto-generated from RDF data\n\n'; // Define classes for (const [className] of classes) { mermaid += ` class ${className} {\n`; mermaid += ` }\n\n`; } // Add inheritance relationships mermaid += ' %% Inheritance relationships\n'; for (const [className, data] of classes) { for (const superclass of data.superclasses) { if (classes.has(superclass)) { mermaid += ` ${superclass} <|-- ${className}\n`; } } } return mermaid; } /** * Generate Mermaid ER diagram from RDF data */ async function generateErDiagram(client: SparqlClient): Promise { // Query for classes and properties const query = ` PREFIX rdfs: PREFIX owl: SELECT DISTINCT ?class ?property ?range WHERE { ?class a owl:Class . ?property rdfs:domain ?class . OPTIONAL { ?property rdfs:range ?range } FILTER(isIRI(?class)) } ORDER BY ?class ?property LIMIT 100 `; const results = await client.executeSelect(query); const entities = new Map>(); const relationships = new Set(); // Build entity structure for (const binding of results.results.bindings) { const classUri = binding.class?.value; const propertyUri = binding.property?.value; const rangeUri = binding.range?.value; if (!classUri) continue; const className = extractLocalName(classUri); if (!entities.has(className)) { entities.set(className, new Set()); } if (propertyUri) { const propName = extractLocalName(propertyUri); entities.get(className)!.add(propName); // Add relationship if range is another class if (rangeUri && entities.has(extractLocalName(rangeUri))) { const rangeName = extractLocalName(rangeUri); relationships.add(`${className} ||--o{ ${rangeName} : "${propName}"`); } } } // Generate Mermaid syntax let mermaid = 'erDiagram\n'; // Define entities for (const [entityName, properties] of entities) { mermaid += `${entityName} {\n`; for (const prop of properties) { mermaid += ` string ${prop}\n`; } mermaid += `}\n\n`; } // Add relationships for (const rel of relationships) { mermaid += `${rel}\n`; } return mermaid; } /** * Extract local name from URI */ function extractLocalName(uri: string): string { const lastSlash = Math.max(uri.lastIndexOf('/'), uri.lastIndexOf('#')); return lastSlash > 0 ? uri.substring(lastSlash + 1) : uri; }