// 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;
}
}
}