- 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.
1051 lines
32 KiB
Markdown
1051 lines
32 KiB
Markdown
# Graph Visualization Feature Checklist
|
|
|
|
**Source**: `/Users/kempersc/apps/example_ld` → GLAM Frontend Integration
|
|
**Date**: 2025-11-22
|
|
**Purpose**: Ensure all D3.js visualization features from example_ld are properly implemented in GLAM
|
|
|
|
---
|
|
|
|
## 🎯 Executive Summary
|
|
|
|
The example_ld project contains a **highly sophisticated D3.js force-directed graph visualization** with the following key features:
|
|
|
|
1. **Bidirectional edge switching** - Click edges to reverse direction
|
|
2. **Node pop-up metadata** - Double-click nodes for detailed modal
|
|
3. **Edge highlighting** - Hover to see edge labels and highlighting
|
|
4. **Label collision avoidance** - Intelligent positioning of edge labels
|
|
5. **Multi-degree connection analysis** - Streamgraph showing 1st, 2nd, 3rd degree connections
|
|
6. **RDF data extraction** - Click nodes to extract RDF triples in multiple formats
|
|
7. **Source XML tracing** - View original source XML for nodes
|
|
|
|
This document maps these features to GLAM's visualization needs and provides implementation guidance.
|
|
|
|
---
|
|
|
|
## 📋 Feature Comparison Matrix
|
|
|
|
| Feature | example_ld Status | GLAM Current Status | GLAM Priority |
|
|
|---------|------------------|---------------------|---------------|
|
|
| **Core Graph Visualization** |
|
|
| Force-directed graph | ✅ Implemented | ❌ Basic (Task 7) | 🔴 CRITICAL |
|
|
| Node type coloring | ✅ 12 node types | ⚠️ Schema.org only | 🟡 HIGH |
|
|
| Zoom/pan controls | ✅ D3 zoom | ⚠️ Basic | 🟢 MEDIUM |
|
|
| Drag-and-drop nodes | ✅ With simulation | ❌ Not impl | 🟡 HIGH |
|
|
| **Edge Features** |
|
|
| Bidirectional edges | ✅ Click to switch | ❌ Not impl | 🔴 CRITICAL |
|
|
| Edge highlighting | ✅ Hover + arrows | ❌ Not impl | 🔴 CRITICAL |
|
|
| Edge labels | ✅ With collision avoidance | ❌ Not impl | 🔴 CRITICAL |
|
|
| Curved edge paths | ✅ Quadratic bezier | ❌ Straight lines | 🟡 HIGH |
|
|
| Arrow markers | ✅ Per node type | ❌ Basic | 🟡 HIGH |
|
|
| **Node Interaction** |
|
|
| Click selection | ✅ Highlights node | ⚠️ Basic | 🟡 HIGH |
|
|
| Double-click modal | ✅ Full metadata | ❌ Not impl | 🔴 CRITICAL |
|
|
| Hover tooltips | ✅ Rich metadata | ⚠️ Basic | 🟡 HIGH |
|
|
| Node labels | ✅ Positioned | ⚠️ Basic | 🟢 MEDIUM |
|
|
| **Advanced Features** |
|
|
| Streamgraph | ✅ Multi-degree BFS | ❌ Not impl | 🟢 MEDIUM |
|
|
| Label collision avoidance | ✅ Physics-based | ❌ Not impl | 🟡 HIGH |
|
|
| RDF extraction per node | ✅ All formats | ❌ Not impl | 🔴 CRITICAL |
|
|
| Source data tracing | ✅ XML context | ❌ Not impl | 🟡 HIGH |
|
|
|
|
**Legend**:
|
|
- 🔴 CRITICAL - Must have for MVP
|
|
- 🟡 HIGH - Important for usability
|
|
- 🟢 MEDIUM - Nice to have
|
|
|
|
---
|
|
|
|
## 🔍 Detailed Feature Analysis
|
|
|
|
### 1. Bidirectional Edge Switching ✅→❌
|
|
|
|
**example_ld Implementation**:
|
|
```javascript
|
|
// File: /Users/kempersc/apps/example_ld/static/js/graph.js
|
|
// Lines: 30-84, 327-382
|
|
|
|
// Bidirectional relationship mappings
|
|
const BIDIRECTIONAL_MAPPINGS = {
|
|
'describesOrDescribed': 'isOrWasDescribedBy',
|
|
'isOrWasDescribedBy': 'describesOrDescribed',
|
|
'includes': 'isIncludedIn',
|
|
'isIncludedIn': 'includes',
|
|
'hasOrHadHolder': 'isOrWasHolderOf',
|
|
'isOrWasHolderOf': 'hasOrHadHolder',
|
|
// ... 20+ more mappings
|
|
};
|
|
|
|
// Click handler on edges
|
|
link.on('click', function(event, d) {
|
|
if (!d.isBidirectional) return;
|
|
|
|
event.stopPropagation();
|
|
d.isReversed = !d.isReversed;
|
|
|
|
// Swap source and target
|
|
const temp = d.source;
|
|
d.source = d.target;
|
|
d.target = temp;
|
|
|
|
// Update predicate to inverse
|
|
const inversePredicate = getInversePredicate(d.predicate);
|
|
if (inversePredicate) {
|
|
d.predicate = inversePredicate;
|
|
}
|
|
|
|
// Update arrow marker and label
|
|
// ... (lines 348-369)
|
|
});
|
|
```
|
|
|
|
**GLAM Requirement**:
|
|
Heritage ontology relationships are inherently bidirectional:
|
|
- `hasCreator` ↔ `isCreatorOf`
|
|
- `hasLocation` ↔ `isLocationOf`
|
|
- `hasCollection` ↔ `isPartOfCollection`
|
|
- `hasCustodian` ↔ `isCustodianOf`
|
|
|
|
**Implementation Plan**:
|
|
1. Create `HERITAGE_BIDIRECTIONAL_MAPPINGS` for GLAM ontology
|
|
2. Add click handler to edges in `OntologyVisualizer.tsx`
|
|
3. Update Mermaid diagram syntax to show direction changes
|
|
4. Add visual feedback (flash animation on direction change)
|
|
|
|
**Files to Create/Modify**:
|
|
- `frontend/src/lib/graph/bidirectional-mappings.ts` (NEW)
|
|
- `frontend/src/components/query/OntologyVisualizer.tsx` (MODIFY)
|
|
- `frontend/src/lib/graph/ontology-parser.ts` (MODIFY)
|
|
|
|
---
|
|
|
|
### 2. Node Pop-up Metadata Modal ✅→❌
|
|
|
|
**example_ld Implementation**:
|
|
```javascript
|
|
// File: /Users/kempersc/apps/example_ld/static/js/graph.js
|
|
// Lines: 696-716
|
|
|
|
function handleNodeDoubleClick(event, d) {
|
|
event.stopPropagation();
|
|
|
|
// Store selected node and edges globally
|
|
window.selectedGraphNode = d;
|
|
window.selectedGraphLinks = graphData.links.filter(link =>
|
|
link.source.id === d.id || link.target.id === d.id
|
|
);
|
|
|
|
// Open Bootstrap modal
|
|
const modal = new bootstrap.Modal(document.getElementById('nodeDetailsModal'));
|
|
modal.show();
|
|
|
|
// Populate modal with node data
|
|
populateNodeModal(d, window.selectedGraphLinks);
|
|
}
|
|
|
|
// Modal content includes:
|
|
// 1. Node metadata (label, type, URI, title, identifier, etc.)
|
|
// 2. RDF preview in multiple formats (Turtle, JSON-LD, N-Triples, XML)
|
|
// 3. Source XML with ancestor/descendant context controls
|
|
// 4. Copy to clipboard functionality
|
|
```
|
|
|
|
**GLAM Requirement**:
|
|
Heritage institutions have rich metadata that should be accessible via node interaction:
|
|
- Institution name, type, city, country
|
|
- ISIL codes, Wikidata IDs, VIAF IDs
|
|
- Collections held
|
|
- Digital platforms used
|
|
- Historical change events (mergers, relocations)
|
|
|
|
**Implementation Plan**:
|
|
1. Create React modal component for node details
|
|
2. Double-click handler on nodes
|
|
3. Fetch full institution record from SPARQL endpoint
|
|
4. Display metadata in organized sections
|
|
5. Include SPARQL query to retrieve this node's data
|
|
6. Add "Explore in Query Builder" button
|
|
|
|
**Files to Create/Modify**:
|
|
- `frontend/src/components/graph/NodeDetailsModal.tsx` (NEW)
|
|
- `frontend/src/components/query/OntologyVisualizer.tsx` (MODIFY)
|
|
- `frontend/src/lib/sparql/node-extraction.ts` (NEW)
|
|
|
|
---
|
|
|
|
### 3. Edge Highlighting with Labels ✅→❌
|
|
|
|
**example_ld Implementation**:
|
|
```javascript
|
|
// File: /Users/kempersc/apps/example_ld/static/js/graph.js
|
|
// Lines: 383-426
|
|
|
|
link.on('mouseenter', function(event, d) {
|
|
// Show label and background for this edge
|
|
const linkIndex = graphData.links.indexOf(d);
|
|
linkLabel.filter((ld, i) => i === linkIndex)
|
|
.transition()
|
|
.duration(200)
|
|
.style('opacity', 1);
|
|
linkLabelBg.filter((ld, i) => i === linkIndex)
|
|
.transition()
|
|
.duration(200)
|
|
.style('opacity', 1);
|
|
|
|
// Highlight the edge and switch to highlighted arrow
|
|
const targetType = (typeof d.target === 'object') ? d.target.type : 'Resource';
|
|
d3.select(this)
|
|
.transition()
|
|
.duration(200)
|
|
.attr('stroke-opacity', 1)
|
|
.attr('stroke-width', Math.sqrt(d.value || 1) * 1.5)
|
|
.attr('marker-end', `url(#arrow-${targetType}-highlight)`);
|
|
});
|
|
|
|
link.on('mouseleave', function(event, d) {
|
|
// Hide label and restore normal appearance
|
|
// ... (lines 405-425)
|
|
});
|
|
```
|
|
|
|
**Key Features**:
|
|
- Edge labels hidden by default (reduced visual clutter)
|
|
- Hover shows label with white background for readability
|
|
- Edge thickness increases on hover
|
|
- Arrow marker switches to highlighted version (larger, fully opaque)
|
|
- Smooth 200ms transitions
|
|
|
|
**GLAM Requirement**:
|
|
Heritage relationships should be discoverable without overwhelming the user:
|
|
- Show property names on hover (e.g., "has creator", "is located in")
|
|
- Highlight relationship path when hovering edges
|
|
- Distinguish between different relationship types by color
|
|
|
|
**Implementation Plan**:
|
|
1. Add hidden label layer in Mermaid diagram
|
|
2. Implement hover detection with D3 overlay
|
|
3. Create arrow marker definitions for each property type
|
|
4. Add transition animations for smooth UX
|
|
|
|
**Files to Create/Modify**:
|
|
- `frontend/src/components/graph/EdgeHighlighter.tsx` (NEW)
|
|
- `frontend/src/lib/graph/arrow-markers.ts` (NEW)
|
|
- `frontend/src/components/query/OntologyVisualizer.css` (MODIFY - add hover styles)
|
|
|
|
---
|
|
|
|
### 4. Label Collision Avoidance ✅→❌
|
|
|
|
**example_ld Implementation**:
|
|
```javascript
|
|
// File: /Users/kempersc/apps/example_ld/static/js/graph.js
|
|
// Lines: 718-877
|
|
|
|
/**
|
|
* Custom force for label collision avoidance
|
|
* Physics-based simulation that:
|
|
* 1. Detects bounding box overlaps between edge labels
|
|
* 2. Applies repulsive forces to separate colliding labels
|
|
* 3. Maintains spring force to keep labels near their edges
|
|
* 4. Freezes labels when simulation cools down
|
|
*/
|
|
function labelCollisionForce(links) {
|
|
let strength = 0.5;
|
|
let iterations = 1;
|
|
|
|
function force(alpha) {
|
|
// Only apply when simulation active
|
|
if (alpha > 0.05) {
|
|
for (let k = 0; k < iterations; k++) {
|
|
resolveLabelCollisions(links, alpha);
|
|
}
|
|
} else {
|
|
// Freeze labels when cooled
|
|
links.forEach(link => {
|
|
link.labelFixed = true;
|
|
link.labelVx = 0;
|
|
link.labelVy = 0;
|
|
});
|
|
}
|
|
}
|
|
|
|
return force;
|
|
}
|
|
|
|
function resolveLabelCollisions(links, alpha) {
|
|
const padding = 8;
|
|
const damping = 0.7;
|
|
|
|
// Check all pairs for collisions
|
|
for (let i = 0; i < links.length; i++) {
|
|
for (let j = i + 1; j < links.length; j++) {
|
|
const linkA = links[i];
|
|
const linkB = links[j];
|
|
|
|
// Calculate bounding boxes
|
|
const boxA = {
|
|
left: linkA.labelX - linkA.labelWidth / 2 - padding,
|
|
right: linkA.labelX + linkA.labelWidth / 2 + padding,
|
|
top: linkA.labelY - linkA.labelHeight / 2 - padding,
|
|
bottom: linkA.labelY + linkA.labelHeight / 2 + padding
|
|
};
|
|
|
|
// ... check overlap and apply forces
|
|
}
|
|
}
|
|
|
|
// Apply spring force to pull labels back to edges
|
|
// ... (lines 847-876)
|
|
}
|
|
```
|
|
|
|
**Key Insights**:
|
|
- Labels have bounding boxes tracked in link data
|
|
- Collision detection uses AABB (axis-aligned bounding box) algorithm
|
|
- Repulsive forces push labels apart
|
|
- Spring forces pull labels back to edge midpoints
|
|
- Velocity damping prevents oscillation
|
|
- Labels freeze when simulation cools (performance optimization)
|
|
|
|
**GLAM Requirement**:
|
|
Ontology diagrams can have many relationships, causing label overlap:
|
|
- Property names (e.g., "hasCreator", "isLocationOf")
|
|
- Cardinality constraints (e.g., "0..*", "1..1")
|
|
- Domain/range annotations
|
|
|
|
**Implementation Plan**:
|
|
1. Implement bounding box tracking for Mermaid edge labels
|
|
2. Add physics simulation for label positioning
|
|
3. Use D3 force simulation or custom collision detection
|
|
4. Ensure labels stay readable even in dense graphs
|
|
|
|
**Files to Create/Modify**:
|
|
- `frontend/src/lib/graph/label-collision.ts` (NEW)
|
|
- `frontend/src/lib/graph/physics-sim.ts` (NEW)
|
|
- `frontend/src/components/query/OntologyVisualizer.tsx` (MODIFY - integrate physics)
|
|
|
|
---
|
|
|
|
### 5. Multi-Degree Connection Analysis (Streamgraph) ✅→❌
|
|
|
|
**example_ld Implementation**:
|
|
```javascript
|
|
// File: /Users/kempersc/apps/example_ld/static/js/streamgraph.js
|
|
// Lines: 45-52, 187-300
|
|
|
|
const STREAMGRAPH_CONFIG = {
|
|
MAX_DEGREE_LEVELS: 3, // Traverse up to 3 degrees
|
|
MAX_NODES_PER_DEGREE: 10, // Limit per degree
|
|
MAX_TOTAL_NODES: 30, // Absolute max
|
|
ENABLE_MULTI_DEGREE: true
|
|
};
|
|
|
|
// BFS traversal for multi-degree connections
|
|
function analyzeNodeConnections(node, links, nodes) {
|
|
// Build adjacency list
|
|
const adjacencyList = {};
|
|
links.forEach(link => {
|
|
const sourceId = (typeof link.source === 'object') ? link.source.id : link.source;
|
|
const targetId = (typeof link.target === 'object') ? link.target.id : link.target;
|
|
|
|
if (!adjacencyList[sourceId]) adjacencyList[sourceId] = [];
|
|
if (!adjacencyList[targetId]) adjacencyList[targetId] = [];
|
|
|
|
adjacencyList[sourceId].push({ connectedNodeId: targetId, predicate: link.predicate });
|
|
adjacencyList[targetId].push({ connectedNodeId: sourceId, predicate: link.predicate });
|
|
});
|
|
|
|
// BFS queue: { nodeId, degreeLevel, pathFromRoot }
|
|
const queue = [{ nodeId: node.id, degreeLevel: 0, pathFromRoot: [node.id] }];
|
|
const visited = new Set([node.id]);
|
|
const connectedNodes = {};
|
|
|
|
while (queue.length > 0) {
|
|
const current = queue.shift();
|
|
|
|
if (current.degreeLevel >= STREAMGRAPH_CONFIG.MAX_DEGREE_LEVELS) continue;
|
|
|
|
const neighbors = adjacencyList[current.nodeId] || [];
|
|
|
|
for (const neighbor of neighbors) {
|
|
const nextNodeId = neighbor.connectedNodeId;
|
|
const predicate = neighbor.predicate;
|
|
const nextDegreeLevel = current.degreeLevel + 1;
|
|
|
|
if (visited.has(nextNodeId)) continue;
|
|
if (Object.keys(connectedNodes).length >= STREAMGRAPH_CONFIG.MAX_TOTAL_NODES) {
|
|
return connectedNodes;
|
|
}
|
|
|
|
visited.add(nextNodeId);
|
|
queue.push({
|
|
nodeId: nextNodeId,
|
|
degreeLevel: nextDegreeLevel,
|
|
pathFromRoot: [...current.pathFromRoot, nextNodeId]
|
|
});
|
|
|
|
connectedNodes[nextNodeId] = {
|
|
nodeId: nextNodeId,
|
|
degreeLevel: nextDegreeLevel,
|
|
relationships: {},
|
|
pathFromRoot: current.pathFromRoot
|
|
};
|
|
|
|
// Track relationship types
|
|
if (!connectedNodes[nextNodeId].relationships[predicate]) {
|
|
connectedNodes[nextNodeId].relationships[predicate] = [];
|
|
}
|
|
connectedNodes[nextNodeId].relationships[predicate].push(current.nodeId);
|
|
}
|
|
}
|
|
|
|
return connectedNodes;
|
|
}
|
|
```
|
|
|
|
**Visualization Output**:
|
|
- Streamgraph shows connections grouped by property type
|
|
- Different colors for different relationship types
|
|
- Degree levels indicated by shading (darker = closer)
|
|
- Interactive hover shows specific connections
|
|
|
|
**GLAM Requirement**:
|
|
Heritage institutions have complex networks:
|
|
- Direct relationships: Museum → Collection
|
|
- 2nd degree: Museum → Collection → Creator
|
|
- 3rd degree: Museum → Collection → Creator → Place
|
|
|
|
Use cases:
|
|
- Find all institutions connected to a specific creator
|
|
- Discover institutions holding related collections
|
|
- Trace provenance chains through custody transfers
|
|
|
|
**Implementation Plan**:
|
|
1. Implement BFS graph traversal algorithm
|
|
2. Create streamgraph visualization component (D3.js or Chart.js)
|
|
3. Add degree level filtering UI
|
|
4. Integrate with node click events
|
|
5. Show relationship paths in tooltip
|
|
|
|
**Files to Create/Modify**:
|
|
- `frontend/src/lib/graph/bfs-traversal.ts` (NEW)
|
|
- `frontend/src/components/graph/Streamgraph.tsx` (NEW)
|
|
- `frontend/src/lib/graph/connection-analysis.ts` (NEW)
|
|
- `frontend/src/components/query/OntologyVisualizer.tsx` (MODIFY - add streamgraph panel)
|
|
|
|
---
|
|
|
|
### 6. RDF Data Extraction Per Node ✅→❌
|
|
|
|
**example_ld Implementation**:
|
|
```javascript
|
|
// File: /Users/kempersc/apps/example_ld/static/js/graph.js
|
|
// Lines: 1358-1587
|
|
|
|
// Modal shows RDF data in multiple formats:
|
|
// 1. Turtle (default)
|
|
// 2. JSON-LD
|
|
// 3. N-Triples
|
|
// 4. RDF/XML
|
|
|
|
async function extractNodeRDFData(node, links, format) {
|
|
// Get full RDF from cache
|
|
let data = await window.resultsDB.getResults({ validate: true });
|
|
if (!data) {
|
|
data = JSON.parse(sessionStorage.getItem('transformResults'));
|
|
}
|
|
|
|
const formatMap = {
|
|
'turtle': 'turtle',
|
|
'jsonld': 'json-ld',
|
|
'ntriples': 'nt',
|
|
'xml': 'xml'
|
|
};
|
|
|
|
const formatKey = formatMap[format] || 'turtle';
|
|
let fullRDF = data.formats[formatKey];
|
|
|
|
// Extract triples related to this node
|
|
const nodeURI = node.uri || node.id;
|
|
const relatedURIs = new Set([nodeURI]);
|
|
|
|
// Add all connected node URIs
|
|
links.forEach(link => {
|
|
relatedURIs.add(link.source.uri || link.source.id);
|
|
relatedURIs.add(link.target.uri || link.target.id);
|
|
});
|
|
|
|
// Filter triples by format
|
|
if (formatKey === 'nt') {
|
|
// N-Triples - filter line by line
|
|
const lines = fullRDF.split('\n');
|
|
const filtered = lines.filter(line => {
|
|
if (!line.trim() || line.startsWith('#')) return true;
|
|
for (const uri of relatedURIs) {
|
|
if (line.includes(uri)) return true;
|
|
}
|
|
return false;
|
|
});
|
|
return filtered.join('\n');
|
|
} else if (formatKey === 'turtle') {
|
|
return filterTurtleBySubjects(fullRDF, relatedURIs);
|
|
} else if (formatKey === 'json-ld') {
|
|
const jsonData = JSON.parse(fullRDF);
|
|
if (jsonData['@graph']) {
|
|
jsonData['@graph'] = jsonData['@graph'].filter(item => {
|
|
const itemId = item['@id'] || '';
|
|
return Array.from(relatedURIs).some(uri => itemId.includes(uri));
|
|
});
|
|
}
|
|
return JSON.stringify(jsonData, null, 2);
|
|
}
|
|
|
|
return fullRDF;
|
|
}
|
|
```
|
|
|
|
**Key Features**:
|
|
- Format selector dropdown (Turtle/JSON-LD/N-Triples/XML)
|
|
- Filters RDF to show only node + connected nodes
|
|
- Syntax highlighting via Prism.js
|
|
- Copy to clipboard button
|
|
- Shows raw triples that generated the visualization
|
|
|
|
**GLAM Requirement**:
|
|
Users should be able to:
|
|
- See the SPARQL-generated RDF for any institution
|
|
- Understand how the visualization was created
|
|
- Copy RDF for use in external tools (Protégé, TopBraid)
|
|
- Verify data accuracy by inspecting raw triples
|
|
|
|
**Implementation Plan**:
|
|
1. Add SPARQL DESCRIBE query for selected node
|
|
2. Create format conversion utilities (SPARQL JSON → Turtle/JSON-LD/N-Triples)
|
|
3. Implement syntax highlighting (Prism.js or Monaco)
|
|
4. Add RDF preview panel to node details modal
|
|
5. Include "Run in Query Builder" button
|
|
|
|
**Files to Create/Modify**:
|
|
- `frontend/src/lib/sparql/rdf-extraction.ts` (NEW)
|
|
- `frontend/src/lib/rdf/format-converter.ts` (NEW)
|
|
- `frontend/src/components/graph/RDFPreview.tsx` (NEW)
|
|
- `frontend/src/components/graph/NodeDetailsModal.tsx` (MODIFY - add RDF tab)
|
|
|
|
---
|
|
|
|
### 7. Source Data Tracing ✅→❌
|
|
|
|
**example_ld Implementation**:
|
|
```javascript
|
|
// File: /Users/kempersc/apps/example_ld/static/js/graph.js
|
|
// Lines: 1589-1744
|
|
|
|
async function extractSourceXMLWithContext(node, ancestorLevels, descendantLevels) {
|
|
// Get source XML from cache
|
|
let data = await window.resultsDB.getResults({ validate: true });
|
|
const sourceXML = data.source_xml;
|
|
|
|
// Parse XML
|
|
const parser = new DOMParser();
|
|
const xmlDoc = parser.parseFromString(sourceXML, 'text/xml');
|
|
|
|
// Find element by ID
|
|
const nodeId = extractIdFromURI(node.uri || node.id);
|
|
let element = xmlDoc.querySelector(`[id="${nodeId}"]`);
|
|
|
|
// Fallback searches if not found
|
|
if (!element) {
|
|
element = xmlDoc.querySelector(`[unitid="${nodeId}"]`);
|
|
}
|
|
// ... more fallback searches (lines 1638-1660)
|
|
|
|
// Navigate up ancestor levels
|
|
let contextElement = element;
|
|
for (let i = 0; i < ancestorLevels && contextElement.parentElement; i++) {
|
|
contextElement = contextElement.parentElement;
|
|
}
|
|
|
|
// Clone and prune descendants
|
|
const clonedElement = contextElement.cloneNode(true);
|
|
if (descendantLevels >= 0 && descendantLevels < 100) {
|
|
pruneDescendants(clonedElement, descendantLevels, 0);
|
|
}
|
|
|
|
// Format and return
|
|
const serializer = new XMLSerializer();
|
|
let xmlString = serializer.serializeToString(clonedElement);
|
|
return formatXML(xmlString);
|
|
}
|
|
```
|
|
|
|
**UI Controls**:
|
|
- Slider for ancestor levels (0-5)
|
|
- Slider for descendant levels (0-10)
|
|
- "Include Source XML" checkbox
|
|
- Shows hierarchical context around node
|
|
|
|
**GLAM Requirement**:
|
|
Heritage data comes from CSV files, not XML, but tracing is still valuable:
|
|
- Show original CSV row that generated this institution
|
|
- Display provenance: which file, line number, extraction date
|
|
- Link back to data tier (TIER_1_AUTHORITATIVE vs TIER_4_INFERRED)
|
|
- Show data quality confidence scores
|
|
|
|
**Implementation Plan**:
|
|
1. Store CSV provenance in RDF as PROV-O triples
|
|
2. SPARQL query to retrieve provenance for node
|
|
3. Display CSV row in modal with field highlighting
|
|
4. Add "View Full Record in CSV" link
|
|
5. Show data quality indicators (tier, confidence)
|
|
|
|
**Files to Create/Modify**:
|
|
- `frontend/src/lib/provenance/csv-tracer.ts` (NEW)
|
|
- `frontend/src/components/graph/ProvenanceView.tsx` (NEW)
|
|
- `frontend/src/components/graph/NodeDetailsModal.tsx` (MODIFY - add provenance tab)
|
|
|
|
---
|
|
|
|
## 🏗️ Architecture Integration
|
|
|
|
### Component Hierarchy (Proposed)
|
|
|
|
```
|
|
QueryBuilder (existing)
|
|
├── OntologyVisualizer (existing)
|
|
│ ├── MermaidRenderer (existing)
|
|
│ ├── D3Overlay (NEW) ← Intercepts interactions
|
|
│ │ ├── EdgeInteractionLayer (NEW)
|
|
│ │ │ ├── BidirectionalSwitcher
|
|
│ │ │ ├── EdgeHighlighter
|
|
│ │ │ └── LabelCollisionManager
|
|
│ │ ├── NodeInteractionLayer (NEW)
|
|
│ │ │ ├── HoverTooltip
|
|
│ │ │ ├── ClickHandler
|
|
│ │ │ └── DoubleClickHandler
|
|
│ │ └── ForceSimulation (NEW)
|
|
│ │ ├── LabelPhysics
|
|
│ │ └── NodePositioning
|
|
│ ├── ZoomControls (existing)
|
|
│ └── SVGExporter (existing)
|
|
├── NodeDetailsModal (NEW)
|
|
│ ├── MetadataView
|
|
│ ├── RDFPreview (NEW)
|
|
│ │ ├── FormatSelector
|
|
│ │ ├── SyntaxHighlighter
|
|
│ │ └── CopyButton
|
|
│ ├── ProvenanceView (NEW)
|
|
│ │ ├── CSVTracer
|
|
│ │ └── DataQualityIndicator
|
|
│ └── ConnectionAnalysis (NEW)
|
|
│ └── Streamgraph (NEW)
|
|
└── GraphToolbar (NEW)
|
|
├── LayoutSelector
|
|
├── FilterControls
|
|
└── ExportOptions
|
|
```
|
|
|
|
### Data Flow
|
|
|
|
```
|
|
SPARQL Endpoint (Oxigraph)
|
|
↓
|
|
SPARQL CONSTRUCT query (get ontology triples)
|
|
↓
|
|
RDF Parser (n3.js or rdflib)
|
|
↓
|
|
Graph Data Structure { nodes, edges }
|
|
↓
|
|
Mermaid Diagram Generation (existing)
|
|
↓
|
|
D3 Overlay (NEW) ← Adds interactivity
|
|
↓
|
|
User Interactions:
|
|
- Click edge → Switch direction
|
|
- Double-click node → Open modal
|
|
- Hover edge → Show label
|
|
- Single-click node → Highlight connections
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 Implementation Priorities
|
|
|
|
### Phase 1: Core Interactivity (Week 1)
|
|
**Goal**: Make the graph interactive with basic features
|
|
|
|
- [ ] **Task 1.1**: Add D3 overlay to OntologyVisualizer
|
|
- Detect Mermaid SVG elements
|
|
- Attach D3 selection to nodes and edges
|
|
- Implement basic click/hover detection
|
|
- **Estimated**: 4 hours
|
|
|
|
- [ ] **Task 1.2**: Implement edge highlighting
|
|
- Show edge labels on hover
|
|
- Highlight edges with thicker stroke
|
|
- Add transition animations
|
|
- **Estimated**: 3 hours
|
|
|
|
- [ ] **Task 1.3**: Add node hover tooltips
|
|
- Extract metadata from RDF
|
|
- Show tooltip with node info
|
|
- Position tooltip near cursor
|
|
- **Estimated**: 2 hours
|
|
|
|
- [ ] **Task 1.4**: Implement node click handler
|
|
- Highlight clicked node
|
|
- Show connected edges
|
|
- Update UI to show selection
|
|
- **Estimated**: 2 hours
|
|
|
|
**Phase 1 Total**: ~11 hours (1.5 days)
|
|
|
|
---
|
|
|
|
### Phase 2: Bidirectional Edges (Week 1-2)
|
|
**Goal**: Enable edge direction switching
|
|
|
|
- [ ] **Task 2.1**: Create bidirectional mappings for GLAM ontology
|
|
- Map heritage properties (hasCreator ↔ isCreatorOf)
|
|
- Include CPOV, TOOI, Schema.org mappings
|
|
- **Estimated**: 3 hours
|
|
|
|
- [ ] **Task 2.2**: Implement edge click handler
|
|
- Detect bidirectional relationships
|
|
- Swap source/target on click
|
|
- Update arrow direction
|
|
- **Estimated**: 4 hours
|
|
|
|
- [ ] **Task 2.3**: Update Mermaid diagram on direction change
|
|
- Regenerate Mermaid syntax
|
|
- Re-render diagram
|
|
- Maintain zoom/pan state
|
|
- **Estimated**: 5 hours
|
|
|
|
- [ ] **Task 2.4**: Add visual feedback
|
|
- Flash animation on direction change
|
|
- Update label text
|
|
- Show "bidirectional" indicator
|
|
- **Estimated**: 2 hours
|
|
|
|
**Phase 2 Total**: ~14 hours (2 days)
|
|
|
|
---
|
|
|
|
### Phase 3: Node Details Modal (Week 2)
|
|
**Goal**: Rich node metadata display
|
|
|
|
- [ ] **Task 3.1**: Create NodeDetailsModal component
|
|
- Bootstrap 5 modal with tabs
|
|
- Metadata tab layout
|
|
- Close/navigation controls
|
|
- **Estimated**: 3 hours
|
|
|
|
- [ ] **Task 3.2**: Implement double-click handler
|
|
- Detect double-click on nodes
|
|
- Open modal with node data
|
|
- Populate metadata fields
|
|
- **Estimated**: 2 hours
|
|
|
|
- [ ] **Task 3.3**: Add RDF preview tab
|
|
- SPARQL DESCRIBE query for node
|
|
- Format selector (Turtle/JSON-LD/N-Triples)
|
|
- Syntax highlighting with Prism.js
|
|
- **Estimated**: 5 hours
|
|
|
|
- [ ] **Task 3.4**: Add provenance view tab
|
|
- Show CSV source data
|
|
- Display data tier and confidence
|
|
- Link to full record
|
|
- **Estimated**: 4 hours
|
|
|
|
- [ ] **Task 3.5**: Add copy-to-clipboard
|
|
- Copy RDF data
|
|
- Copy metadata as JSON
|
|
- Copy SPARQL query
|
|
- **Estimated**: 2 hours
|
|
|
|
**Phase 3 Total**: ~16 hours (2 days)
|
|
|
|
---
|
|
|
|
### Phase 4: Label Collision Avoidance (Week 3)
|
|
**Goal**: Intelligent edge label positioning
|
|
|
|
- [ ] **Task 4.1**: Implement bounding box tracking
|
|
- Calculate label dimensions
|
|
- Store in edge data
|
|
- Update on diagram changes
|
|
- **Estimated**: 3 hours
|
|
|
|
- [ ] **Task 4.2**: Add collision detection
|
|
- AABB overlap algorithm
|
|
- Check all label pairs
|
|
- Identify collisions
|
|
- **Estimated**: 4 hours
|
|
|
|
- [ ] **Task 4.3**: Implement physics simulation
|
|
- Repulsive forces between labels
|
|
- Spring forces to edges
|
|
- Velocity damping
|
|
- **Estimated**: 6 hours
|
|
|
|
- [ ] **Task 4.4**: Integrate with Mermaid
|
|
- Position labels via CSS transforms
|
|
- Curve edges around labels
|
|
- Freeze labels when settled
|
|
- **Estimated**: 5 hours
|
|
|
|
**Phase 4 Total**: ~18 hours (2.5 days)
|
|
|
|
---
|
|
|
|
### Phase 5: Advanced Features (Week 4)
|
|
**Goal**: Streamgraph and connection analysis
|
|
|
|
- [ ] **Task 5.1**: Implement BFS graph traversal
|
|
- Build adjacency list from RDF
|
|
- Multi-degree traversal (1st, 2nd, 3rd)
|
|
- Track paths and degree levels
|
|
- **Estimated**: 5 hours
|
|
|
|
- [ ] **Task 5.2**: Create Streamgraph component
|
|
- D3.js streamgraph layout
|
|
- Color coding by property type
|
|
- Interactive hover
|
|
- **Estimated**: 8 hours
|
|
|
|
- [ ] **Task 5.3**: Integrate streamgraph with node selection
|
|
- Trigger on node click
|
|
- Update when selection changes
|
|
- Show connection statistics
|
|
- **Estimated**: 4 hours
|
|
|
|
- [ ] **Task 5.4**: Add connection filtering UI
|
|
- Degree level slider
|
|
- Property type checkboxes
|
|
- Max nodes limit
|
|
- **Estimated**: 3 hours
|
|
|
|
**Phase 5 Total**: ~20 hours (2.5 days)
|
|
|
|
---
|
|
|
|
## 📦 Dependencies
|
|
|
|
### NPM Packages to Install
|
|
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"d3": "^7.8.5", // D3.js for graph manipulation
|
|
"d3-force": "^3.0.0", // Force simulation for physics
|
|
"d3-zoom": "^3.0.0", // Zoom and pan controls
|
|
"n3": "^1.17.2", // RDF parsing (Turtle, N-Triples)
|
|
"rdflib": "^2.2.33", // Alternative RDF library
|
|
"prismjs": "^1.29.0" // Syntax highlighting for RDF
|
|
},
|
|
"devDependencies": {
|
|
"@types/d3": "^7.4.3",
|
|
"@types/prismjs": "^1.26.3"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Existing Dependencies (Reuse)
|
|
- ✅ `mermaid` (already installed for diagrams)
|
|
- ✅ `@monaco-editor/react` (syntax highlighting fallback)
|
|
- ✅ `react` and `react-dom` (UI framework)
|
|
|
|
---
|
|
|
|
## 🧪 Testing Strategy
|
|
|
|
### Unit Tests
|
|
|
|
**Edge Interaction**:
|
|
```typescript
|
|
// tests/unit/graph/BidirectionalEdges.test.ts
|
|
describe('BidirectionalEdges', () => {
|
|
it('should detect bidirectional relationships', () => {
|
|
const edge = { predicate: 'hasCreator' };
|
|
expect(isBidirectional(edge)).toBe(true);
|
|
});
|
|
|
|
it('should swap source and target on click', () => {
|
|
const edge = { source: 'A', target: 'B', predicate: 'hasCreator' };
|
|
const swapped = swapEdgeDirection(edge);
|
|
expect(swapped.source).toBe('B');
|
|
expect(swapped.target).toBe('A');
|
|
expect(swapped.predicate).toBe('isCreatorOf');
|
|
});
|
|
});
|
|
```
|
|
|
|
**Label Collision**:
|
|
```typescript
|
|
// tests/unit/graph/LabelCollision.test.ts
|
|
describe('LabelCollision', () => {
|
|
it('should detect overlapping labels', () => {
|
|
const labelA = { x: 100, y: 100, width: 50, height: 20 };
|
|
const labelB = { x: 110, y: 105, width: 50, height: 20 };
|
|
expect(detectCollision(labelA, labelB)).toBe(true);
|
|
});
|
|
|
|
it('should apply repulsive forces to colliding labels', () => {
|
|
const labelA = { x: 100, y: 100, vx: 0, vy: 0 };
|
|
const labelB = { x: 110, y: 105, vx: 0, vy: 0 };
|
|
applyCollisionForce(labelA, labelB);
|
|
expect(labelA.vx).toBeLessThan(0); // Pushed left
|
|
expect(labelB.vx).toBeGreaterThan(0); // Pushed right
|
|
});
|
|
});
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
**Node Modal Workflow**:
|
|
```typescript
|
|
// tests/integration/NodeModalFlow.test.tsx
|
|
describe('NodeModalFlow', () => {
|
|
it('should open modal on double-click and display RDF', async () => {
|
|
const { container } = render(<OntologyVisualizer {...props} />);
|
|
|
|
// Find a node in the SVG
|
|
const node = container.querySelector('circle.graph-node');
|
|
fireEvent.doubleClick(node);
|
|
|
|
// Wait for modal to open
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Node Details')).toBeInTheDocument();
|
|
});
|
|
|
|
// Check RDF preview tab
|
|
fireEvent.click(screen.getByText('RDF Preview'));
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Turtle')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### Visual Regression Tests
|
|
|
|
**Graph Rendering**:
|
|
```typescript
|
|
// tests/visual/GraphVisualization.visual.test.tsx
|
|
describe('GraphVisualization', () => {
|
|
it('should render graph with correct node positions', async () => {
|
|
const { container } = render(<OntologyVisualizer {...props} />);
|
|
|
|
// Take screenshot
|
|
const screenshot = await page.screenshot();
|
|
|
|
// Compare with baseline
|
|
expect(screenshot).toMatchImageSnapshot({
|
|
failureThreshold: 0.01,
|
|
failureThresholdType: 'percent'
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Documentation Requirements
|
|
|
|
### User Documentation
|
|
|
|
**Interaction Guide** (`frontend/docs/GRAPH_INTERACTIONS.md`):
|
|
```markdown
|
|
# Graph Visualization Interactions
|
|
|
|
## Basic Navigation
|
|
- **Zoom**: Mouse wheel or pinch gesture
|
|
- **Pan**: Click and drag background
|
|
- **Reset view**: Click "Reset Zoom" button
|
|
|
|
## Node Interactions
|
|
- **Hover**: Shows tooltip with node metadata
|
|
- **Single-click**: Highlights node and connected edges
|
|
- **Double-click**: Opens detailed modal with RDF data
|
|
|
|
## Edge Interactions
|
|
- **Hover**: Shows edge label (property name)
|
|
- **Click**: Switches direction (for bidirectional properties)
|
|
- **Highlighted edges**: Indicated connected nodes
|
|
|
|
## Advanced Features
|
|
- **Streamgraph**: Click node to see multi-degree connections
|
|
- **RDF Preview**: View raw triples in multiple formats
|
|
- **Provenance**: Trace data back to source CSV
|
|
```
|
|
|
|
### Developer Documentation
|
|
|
|
**Architecture Guide** (`frontend/docs/GRAPH_ARCHITECTURE.md`):
|
|
```markdown
|
|
# Graph Visualization Architecture
|
|
|
|
## Component Overview
|
|
- OntologyVisualizer: Main container
|
|
- D3Overlay: Interactive layer on top of Mermaid
|
|
- NodeDetailsModal: Rich metadata display
|
|
- Streamgraph: Connection analysis
|
|
|
|
## Data Flow
|
|
1. SPARQL query → RDF triples
|
|
2. RDF parser → Graph data structure
|
|
3. Mermaid → SVG diagram
|
|
4. D3 overlay → Interactive events
|
|
5. User interaction → State updates → UI changes
|
|
|
|
## Extending the Graph
|
|
- Add new node types: Update `getNodeColor()` function
|
|
- Add new edge properties: Update `BIDIRECTIONAL_MAPPINGS`
|
|
- Customize layout: Modify force simulation parameters
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 Success Metrics
|
|
|
|
### User Experience Metrics
|
|
- **Interaction Latency**: < 100ms for hover, < 200ms for click
|
|
- **Graph Load Time**: < 2s for 50 nodes, < 5s for 200 nodes
|
|
- **Label Readability**: Zero overlapping labels in settled state
|
|
- **Modal Load Time**: < 500ms for RDF preview
|
|
|
|
### Technical Metrics
|
|
- **Test Coverage**: > 90% for graph interaction code
|
|
- **Bundle Size**: < 500 KB added for D3 and graph utilities
|
|
- **Memory Usage**: < 50 MB for graph with 200 nodes
|
|
- **Frame Rate**: 60 FPS during interactions (no lag)
|
|
|
|
---
|
|
|
|
## 🔄 Maintenance Plan
|
|
|
|
### Regular Updates
|
|
- **Weekly**: Check for D3.js security updates
|
|
- **Monthly**: Review user feedback on graph interactions
|
|
- **Quarterly**: Optimize physics simulation performance
|
|
|
|
### Known Limitations
|
|
- **Large graphs** (>500 nodes): Consider implementing virtualization
|
|
- **Complex ontologies**: May need hierarchical layout (not force-directed)
|
|
- **Mobile**: Touch interactions may need refinement
|
|
|
|
---
|
|
|
|
## 📞 Support
|
|
|
|
### Questions or Issues
|
|
- Review example_ld source code: `/Users/kempersc/apps/example_ld/static/js/graph.js`
|
|
- Consult D3.js documentation: https://d3js.org/
|
|
- Check Mermaid docs: https://mermaid.js.org/
|
|
- Ask in project Slack: #frontend-dev
|
|
|
|
---
|
|
|
|
**Last Updated**: 2025-11-22
|
|
**Author**: AI Assistant (Claude)
|
|
**Status**: ✅ COMPLETE - Ready for Implementation
|