glam/frontend/src/components/query/OntologyVisualizer.tsx
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

344 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 mermaid from 'mermaid';
import type { SparqlClient } from '../../lib/sparql/client';
import './OntologyVisualizer.css';
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<OntologyVisualizerProps> = ({
mermaidSource,
sparqlClient,
diagramType = 'class',
maxHeight = '800px',
showToolbar = true,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [zoom, setZoom] = useState(1);
const [generatedSource, setGeneratedSource] = useState<string | null>(null);
// Initialize Mermaid
useEffect(() => {
mermaid.initialize({
startOnLoad: true,
theme: 'default',
securityLevel: 'loose',
fontFamily: 'Arial, sans-serif',
logLevel: '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) return;
const renderDiagram = async () => {
try {
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]);
// 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 (
<div className="ontology-visualizer">
<div className="visualizer-loading">
<div className="spinner" />
<p>Generating ontology diagram...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="ontology-visualizer">
<div className="visualizer-error">
<p className="error-title">Failed to load diagram</p>
<p className="error-message">{error}</p>
{sparqlClient && (
<button onClick={generateFromRdf} className="retry-btn">
Retry
</button>
)}
</div>
</div>
);
}
return (
<div className="ontology-visualizer">
{showToolbar && (
<div className="visualizer-toolbar">
<div className="zoom-controls">
<button
onClick={handleZoomOut}
className="toolbar-btn"
title="Zoom out"
disabled={zoom <= 0.3}
>
</button>
<span className="zoom-level">{Math.round(zoom * 100)}%</span>
<button
onClick={handleZoomIn}
className="toolbar-btn"
title="Zoom in"
disabled={zoom >= 3}
>
+
</button>
<button onClick={handleZoomReset} className="toolbar-btn" title="Reset zoom">
Reset
</button>
</div>
<div className="toolbar-actions">
{sparqlClient && (
<button onClick={generateFromRdf} className="toolbar-btn">
Regenerate
</button>
)}
<button onClick={handleDownload} className="toolbar-btn">
Download SVG
</button>
</div>
</div>
)}
<div
className="visualizer-container"
style={{
maxHeight,
transform: `scale(${zoom})`,
transformOrigin: 'top left',
}}
>
<div ref={containerRef} className="mermaid-diagram" />
</div>
</div>
);
};
/**
* Generate Mermaid class diagram from RDF data
*/
async function generateClassDiagram(client: SparqlClient): Promise<string> {
// Query for classes and their properties
const query = `
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX owl: <http://www.w3.org/2002/07/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<string, { label: string; superclasses: string[] }>();
// 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<string> {
// Query for classes and properties
const query = `
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX owl: <http://www.w3.org/2002/07/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<string, Set<string>>();
const relationships = new Set<string>();
// 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;
}