// Graph visualization class class DirectedGraph { constructor(canvasId) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.nodes = []; this.edges = []; this.selectedNode = null; this.hoveredNode = null; this.offset = { x: 0, y: 0 }; this.scale = 1; this.isDragging = false; this.dragStart = { x: 0, y: 0 }; this.editMode = false; this.connectingMode = false; this.connectingFrom = null; this.tempConnection = null; this.nodeIdCounter = 1; this.setupCanvas(); this.createNodes(); this.createEdges(); this.setupEventListeners(); this.animate(); } setupCanvas() { const rect = this.canvas.parentElement.getBoundingClientRect(); this.canvas.width = rect.width; this.canvas.height = rect.height; } createNodes() { // Document nodes this.nodes.push({ id: 'doc1', x: 100, y: 100, radius: 40, color: '#3b82f6', label: 'Tree123.json', data: { person: 'John Dow', type: 'document' } }); this.nodes.push({ id: 'doc2', x: 100, y: 250, radius: 40, color: '#a855f7', label: 'Tree456.json', data: { person: 'John Dowe', type: 'document' } }); // Extracted nodes this.nodes.push({ id: 'extracted1', x: 300, y: 100, radius: 35, color: '#60a5fa', label: 'Extracted 1', data: { operation: 'extract_person', source: 'doc1' } }); this.nodes.push({ id: 'extracted2', x: 300, y: 250, radius: 35, color: '#c084fc', label: 'Extracted 2', data: { operation: 'extract_person', source: 'doc2' } }); // Comparison node this.nodes.push({ id: 'comparison', x: 500, y: 175, radius: 45, color: '#34d399', label: 'Comparison', data: { operation: 'compare_names', threshold: 0.8, result: 0.85 } }); // Classification node this.nodes.push({ id: 'classification', x: 700, y: 175, radius: 50, color: '#fbbf24', label: 'Classification', data: { operation: 'classify_same_person', method: 'fact_based', result: 'High probability match', confidence: 85 } }); } createEdges() { // Document to extraction this.edges.push({ from: 'doc1', to: 'extracted1', label: 'extract' }); this.edges.push({ from: 'doc2', to: 'extracted2', label: 'extract' }); // Extraction to comparison this.edges.push({ from: 'extracted1', to: 'comparison', label: 'input' }); this.edges.push({ from: 'extracted2', to: 'comparison', label: 'input' }); // Comparison to classification this.edges.push({ from: 'comparison', to: 'classification', label: 'result' }); } setupEventListeners() { this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e)); this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e)); this.canvas.addEventListener('mouseup', () => this.handleMouseUp()); this.canvas.addEventListener('wheel', (e) => this.handleWheel(e)); this.canvas.addEventListener('click', (e) => this.handleClick(e)); window.addEventListener('resize', () => { this.setupCanvas(); }); } handleMouseDown(e) { const rect = this.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; if (this.editMode && this.hoveredNode) { if (this.connectingMode) { if (this.connectingFrom && this.connectingFrom !== this.hoveredNode) { // Create connection this.edges.push({ from: this.connectingFrom.id, to: this.hoveredNode.id, label: 'connection' }); this.connectingFrom = null; this.connectingMode = false; this.canvas.classList.remove('edit-mode-cursor'); } } else { this.selectedNode = this.hoveredNode; this.showEditPanel(this.selectedNode); } } else if (!this.editMode) { this.isDragging = true; this.dragStart = { x: x - this.offset.x, y: y - this.offset.y }; this.canvas.style.cursor = 'grabbing'; } else if (this.editMode && !this.hoveredNode) { // Add new node at click position const worldX = (x - this.offset.x) / this.scale; const worldY = (y - this.offset.y) / this.scale; this.addNodeAt(worldX, worldY); } } handleMouseMove(e) { const rect = this.canvas.getBoundingClientRect(); const x = (e.clientX - rect.left - this.offset.x) / this.scale; const y = (e.clientY - rect.top - this.offset.y) / this.scale; if (this.isDragging && !this.editMode) { this.offset.x = e.clientX - rect.left - this.dragStart.x; this.offset.y = e.clientY - rect.top - this.dragStart.y; } else if (this.connectingMode && this.connectingFrom) { this.tempConnection = { x: (e.clientX - rect.left - this.offset.x) / this.scale, y: (e.clientY - rect.top - this.offset.y) / this.scale }; } else { // Check for hover over nodes this.hoveredNode = null; for (const node of this.nodes) { const dist = Math.sqrt((x - node.x) ** 2 + (y - node.y) ** 2); if (dist <= node.radius) { this.hoveredNode = node; if (this.editMode) { if (this.connectingMode) { this.canvas.style.cursor = 'crosshair'; } else { this.canvas.style.cursor = 'pointer'; } } else { this.canvas.style.cursor = 'pointer'; } break; } } if (!this.hoveredNode) { this.canvas.style.cursor = this.editMode ? (this.connectingMode ? 'crosshair' : 'crosshair') : 'move'; } } } handleMouseUp() { this.isDragging = false; this.canvas.style.cursor = this.editMode ? 'crosshair' : 'move'; } handleWheel(e) { e.preventDefault(); const delta = e.deltaY > 0 ? 0.9 : 1.1; this.scale *= delta; this.scale = Math.max(0.5, Math.min(2, this.scale)); } handleClick(e) { if (this.hoveredNode) { this.selectedNode = this.hoveredNode; this.showNodeInfo(this.selectedNode); } } showNodeInfo(node) { const infoPanel = document.getElementById('infoPanel'); const nodeInfo = document.getElementById('nodeInfo'); infoPanel.classList.remove('hidden'); document.getElementById('editPanel').classList.add('hidden'); let html = `

ID: ${node.id}

Label: ${node.label}

Type: ${node.data.type || 'custom'}

`; if (node.data.person) { html += `

Person: ${node.data.person}

`; } if (node.data.operation) { html += `

Operation: ${node.data.operation}

`; } if (node.data.result) { html += `

Result: ${node.data.result}

`; } if (node.data.confidence) { html += `

Confidence: ${node.data.confidence}%

`; } html += `
`; nodeInfo.innerHTML = html; } showEditPanel(node) { const editPanel = document.getElementById('editPanel'); document.getElementById('infoPanel').classList.add('hidden'); editPanel.classList.remove('hidden'); document.getElementById('nodeLabel').value = node.label; document.getElementById('nodeType').value = node.data.type || 'custom'; document.getElementById('nodeColor').value = node.color; } addNodeAt(x, y) { const newNode = { id: 'node_' + this.nodeIdCounter++, x: x, y: y, radius: 35, color: '#' + Math.floor(Math.random()*16777215).toString(16), label: 'New Node', data: { type: 'custom' } }; this.nodes.push(newNode); this.selectedNode = newNode; this.showEditPanel(newNode); } deleteNode(nodeId) { this.nodes = this.nodes.filter(n => n.id !== nodeId); this.edges = this.edges.filter(e => e.from !== nodeId && e.to !== nodeId); if (this.selectedNode && this.selectedNode.id === nodeId) { this.selectedNode = null; document.getElementById('editPanel').classList.add('hidden'); } } draw() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.save(); this.ctx.translate(this.offset.x, this.offset.y); this.ctx.scale(this.scale, this.scale); // Draw edges this.ctx.strokeStyle = 'rgba(168, 85, 247, 0.3)'; this.ctx.lineWidth = 2; for (const edge of this.edges) { const fromNode = this.nodes.find(n => n.id === edge.from); const toNode = this.nodes.find(n => n.id === edge.to); if (fromNode && toNode) { this.drawArrow(fromNode, toNode, edge.label); } } // Draw temporary connection line if (this.connectingMode && this.connectingFrom && this.tempConnection) { this.ctx.strokeStyle = '#10b981'; this.ctx.lineWidth = 3; this.ctx.setLineDash([5, 5]); this.ctx.beginPath(); this.ctx.moveTo(this.connectingFrom.x, this.connectingFrom.y); this.ctx.lineTo(this.tempConnection.x, this.tempConnection.y); this.ctx.stroke(); this.ctx.setLineDash([]); } // Draw nodes for (const node of this.nodes) { this.drawNode(node); } this.ctx.restore(); } drawNode(node) { const isHovered = this.hoveredNode === node; const isSelected = this.selectedNode === node; const isConnectingFrom = this.connectingFrom === node; // Node glow effect if (isHovered || isSelected || isConnectingFrom) { const gradient = this.ctx.createRadialGradient(node.x, node.y, 0, node.x, node.y, node.radius * 2); gradient.addColorStop(0, (isConnectingFrom ? '#10b981' : node.color) + '60'); gradient.addColorStop(1, 'transparent'); this.ctx.fillStyle = gradient; this.ctx.beginPath(); this.ctx.arc(node.x, node.y, node.radius * 2, 0, Math.PI * 2); this.ctx.fill(); } // Node circle this.ctx.fillStyle = node.color; this.ctx.beginPath(); this.ctx.arc(node.x, node.y, isHovered ? node.radius * 1.1 : node.radius, 0, Math.PI * 2); this.ctx.fill(); // Node border if (isConnectingFrom) { this.ctx.strokeStyle = '#10b981'; this.ctx.lineWidth = 4; } else if (isSelected) { this.ctx.strokeStyle = '#fff'; this.ctx.lineWidth = 3; } else { this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; this.ctx.lineWidth = 2; } this.ctx.stroke(); // Node label this.ctx.fillStyle = '#fff'; this.ctx.font = 'bold 12px system-ui'; this.ctx.textAlign = 'center'; this.ctx.textBaseline = 'middle'; this.ctx.fillText(node.label, node.x, node.y); } drawArrow(fromNode, toNode, label) { const dx = toNode.x - fromNode.x; const dy = toNode.y - fromNode.y; const angle = Math.atan2(dy, dx); const startX = fromNode.x + fromNode.radius * Math.cos(angle); const startY = fromNode.y + fromNode.radius * Math.sin(angle); const endX = toNode.x - toNode.radius * Math.cos(angle); const endY = toNode.y - toNode.radius * Math.sin(angle); // Draw line this.ctx.beginPath(); this.ctx.moveTo(startX, startY); this.ctx.lineTo(endX, endY); this.ctx.stroke(); // Draw arrowhead const arrowLength = 15; const arrowAngle = Math.PI / 6; this.ctx.beginPath(); this.ctx.moveTo(endX, endY); this.ctx.lineTo( endX - arrowLength * Math.cos(angle - arrowAngle), endY - arrowLength * Math.sin(angle - arrowAngle) ); this.ctx.moveTo(endX, endY); this.ctx.lineTo( endX - arrowLength * Math.cos(angle + arrowAngle), endY - arrowLength * Math.sin(angle + arrowAngle) ); this.ctx.stroke(); // Draw label const midX = (startX + endX) / 2; const midY = (startY + endY) / 2; this.ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; this.ctx.font = '10px system-ui'; this.ctx.textAlign = 'center'; this.ctx.fillText(label, midX, midY - 10); } animate() { this.draw(); requestAnimationFrame(() => this.animate()); } } // Initialize graph let graph; document.addEventListener('DOMContentLoaded', () => { graph = new DirectedGraph('graphCanvas'); }); // Control functions function zoomIn() { if (graph) { graph.scale *= 1.2; graph.scale = Math.min(2, graph.scale); } } function zoomOut() { if (graph) { graph.scale *= 0.8; graph.scale = Math.max(0.5, graph.scale); } } function fitToScreen() { if (graph) { graph.scale = 1; graph.offset = { x: 0, y: 0 }; } } function resetGraph() { if (graph) { graph.selectedNode = null; graph.hoveredNode = null; graph.connectingMode = false; graph.connectingFrom = null; document.getElementById('infoPanel').classList.add('hidden'); document.getElementById('editPanel').classList.add('hidden'); fitToScreen(); } } function toggleFullscreen() { const graphContainer = document.getElementById('graphCanvas').parentElement; if (!document.fullscreenElement) { graphContainer.requestFullscreen(); } else { document.exitFullscreen(); } } function toggleEditMode() { if (!graph) return; graph.editMode = !graph.editMode; const editBtn = document.getElementById('editModeBtn'); const editIndicator = document.getElementById('editIndicator'); if (graph.editMode) { editBtn.classList.remove('bg-purple-600', 'hover:bg-purple-700'); editBtn.classList.add('bg-orange-600', 'hover:bg-orange-700'); editBtn.querySelector('span').textContent = 'Exit Edit'; editIndicator.classList.remove('hidden'); graph.canvas.classList.add('edit-mode-cursor'); } else { editBtn.classList.remove('bg-orange-600', 'hover:bg-orange-700'); editBtn.classList.add('bg-purple-600', 'hover:bg-purple-700'); editBtn.querySelector('span').textContent = 'Edit Mode'; editIndicator.classList.add('hidden'); graph.canvas.classList.remove('edit-mode-cursor'); graph.connectingMode = false; graph.connectingFrom = null; document.getElementById('editPanel').classList.add('hidden'); } } function addNode() { if (graph && graph.editMode) { const centerX = (graph.canvas.width / 2 - graph.offset.x) / graph.scale; const centerY = (graph.canvas.height / 2 - graph.offset.y) / graph.scale; graph.addNodeAt(centerX, centerY); } } function deleteSelectedNode() { if (graph && graph.selectedNode && graph.editMode) { graph.deleteNode(graph.selectedNode.id); } } function connectNodes() { if (graph && graph.editMode) { graph.connectingMode = !graph.connectingMode; if (graph.connectingMode) { graph.canvas.style.cursor = 'crosshair'; } else { graph.connectingFrom = null; graph.canvas.style.cursor = 'crosshair'; } } } function saveNodeChanges() { if (graph && graph.selectedNode) { graph.selectedNode.label = document.getElementById('nodeLabel').value; graph.selectedNode.data.type = document.getElementById('nodeType').value; graph.selectedNode.color = document.getElementById('nodeColor').value; document.getElementById('editPanel').classList.add('hidden'); } } function cancelEdit() { document.getElementById('editPanel').classList.add('hidden'); if (graph) { graph.selectedNode = null; } } function highlightNode(nodeId) { if (graph) { const node = graph.nodes.find(n => n.id === nodeId || (n.data && n.data.source === nodeId) || (n.id === 'extracted1' && nodeId === 'doc1') || (n.id === 'extracted2' && nodeId === 'doc2')); if (node) { graph.selectedNode = node; graph.showNodeInfo(node); // Pan to node graph.offset.x = -node.x * graph.scale + graph.canvas.width / 2; graph.offset.y = -node.y * graph.scale + graph.canvas.height / 2; } } }