graph.php
- Pfad:
src/View/semantic-explorer/graph.php - Namespace: -
- Zeilen: 306 | Größe: 12,955 Bytes
- Geändert: 2025-12-23 16:41:42 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 93
- Dependencies: 100 (25%)
- LOC: 64 (20%)
- Methods: 100 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 100 (10%)
Keine Issues gefunden.
Code
<?php
declare(strict_types=1);
// @responsibility: View für Entity-Relations Graph
ob_start();
?>
<link rel="stylesheet" href="/css/graph.css">
<nav class="breadcrumb">
<a href="/semantic-explorer">Semantic Explorer</a> »
<span>Entity Graph</span>
</nav>
<h1>Entity Relations Graph</h1>
<p class="graph-stats">Alle Entitäten und ihre Beziehungen</p>
<div class="graph-controls">
<div class="graph-legend">
<span class="graph-legend-item"><span class="graph-legend-node graph-node-person"></span> Person</span>
<span class="graph-legend-item"><span class="graph-legend-node graph-node-organization"></span> Organization</span>
<span class="graph-legend-item"><span class="graph-legend-node graph-node-location"></span> Location</span>
<span class="graph-legend-item"><span class="graph-legend-node graph-node-concept"></span> Concept</span>
<span class="graph-legend-item"><span class="graph-legend-node graph-node-method"></span> Method</span>
<span class="graph-legend-item"><span class="graph-legend-node graph-node-tool"></span> Tool</span>
<span class="graph-legend-item"><span class="graph-legend-node graph-node-event"></span> Event</span>
<span class="graph-legend-item graph-legend-separator"><span class="graph-legend-link graph-link-related_to"></span> RELATED_TO</span>
<span class="graph-legend-item"><span class="graph-legend-link graph-link-part_of"></span> PART_OF</span>
<span class="graph-legend-item"><span class="graph-legend-link graph-link-used_in"></span> USED_IN</span>
</div>
<div class="graph-filters">
<label for="entity-type-filter" class="graph-filter-label">Entity-Typ:</label>
<select id="entity-type-filter" class="graph-filter-select">
<option value="">Alle</option>
<?php foreach ($entityTypes as $type): ?>
<option value="<?= htmlspecialchars(strtoupper($type['type'])) ?>"><?= htmlspecialchars($type['type']) ?> (<?= $type['count'] ?>)</option>
<?php endforeach; ?>
</select>
<label for="relation-type-filter" class="graph-filter-label">Relation:</label>
<select id="relation-type-filter" class="graph-filter-select">
<option value="">Alle</option>
<?php foreach ($relationTypes as $type): ?>
<option value="<?= htmlspecialchars($type['relation_type']) ?>"><?= htmlspecialchars($type['relation_type']) ?> (<?= $type['count'] ?>)</option>
<?php endforeach; ?>
</select>
<button id="reset-zoom" class="btn btn--secondary graph-reset-btn">Reset Zoom</button>
</div>
</div>
<div id="graph-stats" class="graph-stats"></div>
<div id="graph-container" class="graph-container"></div>
<div class="graph-back-link">
<a href="/semantic-explorer" class="btn btn--secondary">← Zurück zur Übersicht</a>
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
(function() {
const container = document.getElementById('graph-container');
const width = container.clientWidth;
const height = container.clientHeight;
const nodeColors = {
PERSON: '#6366f1',
ORGANIZATION: '#f59e0b',
LOCATION: '#10b981',
CONCEPT: '#8b5cf6',
METHOD: '#3b82f6',
TOOL: '#14b8a6',
EVENT: '#f43f5e',
OTHER: '#94a3b8'
};
const linkColors = {
RELATED_TO: '#94a3b8',
PART_OF: '#3b82f6',
USED_IN: '#10b981',
DEVELOPED_BY: '#8b5cf6',
TAUGHT_BY: '#f59e0b',
BASED_ON: '#14b8a6',
INFLUENCED_BY: '#f43f5e',
WORKS_WITH: '#6366f1',
TEACHES: '#f59e0b',
CREATED: '#8b5cf6'
};
container.innerHTML = '<div class="graph-loading">Lade Graph-Daten...</div>';
fetch('/semantic-explorer/graph-data')
.then(r => r.json())
.then(data => {
container.innerHTML = '';
document.getElementById('graph-stats').textContent =
`${data.stats.nodes} Entitäten | ${data.stats.links} Relationen | ${data.stats.entityTypes} Entity-Typen | ${data.stats.relationTypes} Relation-Typen`;
// Group nodes by type for static layout
const typeGroups = {};
data.nodes.forEach((node, i) => {
const type = node.type || 'OTHER';
if (!typeGroups[type]) typeGroups[type] = [];
typeGroups[type].push(node);
});
// Compute static positions (circular layout by type)
const typeKeys = Object.keys(typeGroups).sort();
const typeCount = typeKeys.length;
const centerX = width / 2;
const centerY = height / 2;
const typeRadius = Math.min(width, height) * 0.35;
typeKeys.forEach((type, typeIndex) => {
const nodes = typeGroups[type];
const typeAngle = (2 * Math.PI * typeIndex) / typeCount - Math.PI / 2;
const typeCenterX = centerX + typeRadius * Math.cos(typeAngle);
const typeCenterY = centerY + typeRadius * Math.sin(typeAngle);
const nodeCount = nodes.length;
const nodeRadius = Math.min(120, 30 + nodeCount * 3);
nodes.forEach((node, nodeIndex) => {
if (nodeCount === 1) {
node.x = typeCenterX;
node.y = typeCenterY;
} else {
const nodeAngle = (2 * Math.PI * nodeIndex) / nodeCount;
node.x = typeCenterX + nodeRadius * Math.cos(nodeAngle);
node.y = typeCenterY + nodeRadius * Math.sin(nodeAngle);
}
});
});
// Create SVG with zoom
const svg = d3.select('#graph-container')
.append('svg')
.attr('width', width)
.attr('height', height);
const g = svg.append('g');
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoom);
document.getElementById('reset-zoom').addEventListener('click', () => {
svg.transition().duration(300).call(zoom.transform, d3.zoomIdentity);
});
// Arrow markers
const linkTypes = [...new Set(data.links.map(l => l.type))];
svg.append('defs').selectAll('marker')
.data(linkTypes)
.enter().append('marker')
.attr('id', d => 'arrow-semantic-' + d)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 15)
.attr('refY', 0)
.attr('markerWidth', 5)
.attr('markerHeight', 5)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', d => linkColors[d] || '#94a3b8');
// Draw type labels
const typeLabels = g.append('g').attr('class', 'type-labels');
typeKeys.forEach((type, typeIndex) => {
const typeAngle = (2 * Math.PI * typeIndex) / typeCount - Math.PI / 2;
const labelRadius = typeRadius + 140;
const labelX = centerX + labelRadius * Math.cos(typeAngle);
const labelY = centerY + labelRadius * Math.sin(typeAngle);
typeLabels.append('text')
.attr('x', labelX)
.attr('y', labelY)
.attr('text-anchor', 'middle')
.attr('fill', nodeColors[type] || nodeColors.OTHER)
.attr('font-size', '11px')
.attr('font-weight', 'bold')
.text(type);
});
// Draw links
const link = g.append('g')
.selectAll('line')
.data(data.links)
.enter().append('line')
.attr('class', 'graph-link')
.attr('data-type', d => d.type)
.attr('stroke', d => linkColors[d.type] || '#94a3b8')
.attr('stroke-width', d => 1 + (d.strength || 1) * 0.5)
.attr('stroke-opacity', 0.4)
.attr('marker-end', d => 'url(#arrow-semantic-' + d.type + ')')
.attr('x1', d => data.nodes[d.source].x)
.attr('y1', d => data.nodes[d.source].y)
.attr('x2', d => data.nodes[d.target].x)
.attr('y2', d => data.nodes[d.target].y);
// Draw nodes
const node = g.append('g')
.selectAll('g')
.data(data.nodes)
.enter().append('g')
.attr('class', 'graph-node')
.attr('data-type', d => d.type)
.attr('transform', d => `translate(${d.x},${d.y})`)
.attr('cursor', 'pointer')
.on('click', (event, d) => {
if (d.entityId) {
window.location.href = '/semantic-explorer/entitaeten/' + d.entityId;
}
});
node.append('circle')
.attr('r', 8)
.attr('fill', d => nodeColors[d.type] || nodeColors.OTHER)
.attr('stroke', '#fff')
.attr('stroke-width', 1.5);
node.append('text')
.text(d => d.label.length > 15 ? d.label.substring(0, 15) + '...' : d.label)
.attr('x', 0)
.attr('y', 20)
.attr('text-anchor', 'middle')
.attr('fill', 'var(--text-primary)')
.attr('font-size', '8px');
node.append('title')
.text(d => d.label + ' (' + d.type + ')');
// Filter logic
const entityTypeFilter = document.getElementById('entity-type-filter');
const relationTypeFilter = document.getElementById('relation-type-filter');
function applyFilters() {
const selectedEntityType = entityTypeFilter.value;
const selectedRelationType = relationTypeFilter.value;
// Find connected nodes for relation type filter
const connectedNodes = new Set();
if (selectedRelationType) {
data.links.forEach(link => {
if (link.type === selectedRelationType) {
connectedNodes.add(data.nodes[link.source].id);
connectedNodes.add(data.nodes[link.target].id);
}
});
}
// Filter links
d3.selectAll('.graph-link')
.style('opacity', d => {
const typeMatch = !selectedRelationType || d.type === selectedRelationType;
const srcType = data.nodes[d.source].type;
const tgtType = data.nodes[d.target].type;
const entityMatch = !selectedEntityType || srcType === selectedEntityType || tgtType === selectedEntityType;
if (!typeMatch) return 0;
if (!entityMatch) return 0.05;
return 0.6;
});
// Filter nodes
d3.selectAll('.graph-node')
.style('opacity', d => {
const relationMatch = !selectedRelationType || connectedNodes.has(d.id);
const entityMatch = !selectedEntityType || d.type === selectedEntityType;
if (selectedRelationType && !relationMatch) return 0.1;
if (selectedEntityType && !entityMatch) return 0.1;
return 1;
});
}
entityTypeFilter.addEventListener('change', applyFilters);
relationTypeFilter.addEventListener('change', applyFilters);
// Initial zoom to fit
const bounds = g.node().getBBox();
const dx = bounds.width;
const dy = bounds.height;
const x = bounds.x + dx / 2;
const y = bounds.y + dy / 2;
const scale = 0.8 / Math.max(dx / width, dy / height);
const translate = [width / 2 - scale * x, height / 2 - scale * y];
svg.call(zoom.transform, d3.zoomIdentity
.translate(translate[0], translate[1])
.scale(scale));
})
.catch(err => {
container.innerHTML = '<p class="graph-error">Fehler beim Laden des Graphen: ' + err.message + '</p>';
});
})();
</script>
<?php $content = ob_get_clean(); ?>
<?php require VIEW_PATH . '/layout.php'; ?>