graph.php

Code Hygiene Score: 93

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> &raquo;
    <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">&larr; 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'; ?>
← Übersicht