/** * Graph Visualization Component * D3.js Force-directed graph for entity relationships */ function initializeGraphVisualization() { const container = document.getElementById('graph-container'); if (!container) return; const width = container.clientWidth; const height = container.clientHeight || 384; // Clear existing const svg = d3.select('#entity-graph'); svg.selectAll('*').remove(); svg.attr('viewBox', [0, 0, width, height]); // Sample data based on entities const nodes = [ { id: 'QE001', name: 'Quantum Alpha LP', risk: 92, type: 'fund', group: 1 }, { id: 'SB002', name: 'Sigma Beta Fund', risk: 88, type: 'fund', group: 1 }, { id: 'AA003', name: 'AI Alpha Tech', risk: 65, type: 'manager', group: 2 }, { id: 'BS004', name: 'Beta Sigma Mgmt', risk: 58, type: 'manager', group: 2 }, { id: 'MT005', name: 'Mirror Trading', risk: 35, type: 'trading', group: 3 }, { id: 'SHELL1', name: 'Cayman Shell A', risk: 95, type: 'shell', group: 4 }, { id: 'SHELL2', name: 'BVI Holdings', risk: 87, type: 'shell', group: 4 } ]; const links = [ { source: 'AA003', target: 'QE001', value: 1, type: 'manages' }, { source: 'BS004', target: 'SB002', value: 1, type: 'manages' }, { source: 'QE001', target: 'SB002', value: 2, type: 'correlated' }, { source: 'SHELL1', target: 'AA003', value: 1, type: 'owns' }, { source: 'SHELL2', target: 'BS004', value: 1, type: 'owns' }, { source: 'MT005', target: 'QE001', value: 1, type: 'trades' }, { source: 'SHELL1', target: 'SHELL2', value: 3, type: 'entangled' } ]; // Simulation setup const simulation = d3.forceSimulation(nodes) .force('link', d3.forceLink(links).id(d => d.id).distance(100)) .force('charge', d3.forceManyBody().strength(-400)) .force('center', d3.forceCenter(width / 2, height / 2)) .force('collision', d3.forceCollide().radius(40)); // Arrow markers svg.append('defs').selectAll('marker') .data(['end']) .enter().append('marker') .attr('id', 'arrow') .attr('viewBox', '0 -5 10 10') .attr('refX', 25) .attr('refY', 0) .attr('markerWidth', 6) .attr('markerHeight', 6) .attr('orient', 'auto') .append('path') .attr('d', 'M0,-5L10,0L0,5') .attr('fill', '#2dd4bf'); // Links const link = svg.append('g') .selectAll('line') .data(links) .join('line') .attr('class', 'node-link') .attr('stroke-width', d => Math.sqrt(d.value) * 2) .attr('stroke', d => { if (d.type === 'entangled') return '#ef4444'; if (d.type === 'correlated') return '#f59e0b'; return '#2dd4bf'; }) .attr('stroke-opacity', 0.6); // Nodes const node = svg.append('g') .selectAll('g') .data(nodes) .join('g') .attr('class', 'node-circle') .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)); // Node circles node.append('circle') .attr('r', d => d.risk > 80 ? 20 : 15) .attr('fill', d => { if (d.risk > 80) return '#ef4444'; if (d.risk > 60) return '#f59e0b'; if (d.risk > 40) return '#14b8a6'; return '#22c55e'; }) .attr('stroke', '#0f172a') .attr('stroke-width', 2) .style('cursor', 'pointer'); // Node labels node.append('text') .text(d => d.name) .attr('x', 0) .attr('y', d => d.risk > 80 ? 28 : 23) .attr('text-anchor', 'middle') .attr('fill', '#94a3b8') .attr('font-size', '10px') .attr('font-family', 'Inter, sans-serif'); // Tooltips node.append('title') .text(d => `${d.name}\nRisk: ${d.risk}%\nType: ${d.type}`); // Click handlers node.on('click', (event, d) => { viewEntity(d.id); }); // Update positions simulation.on('tick', () => { link .attr('x1', d => d.source.x) .attr('y1', d => d.source.y) .attr('x2', d => d.target.x) .attr('y2', d => d.target.y); node.attr('transform', d => `translate(${d.x},${d.y})`); }); function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } // Zoom controls window.zoomGraph = (scale) => { const currentTransform = d3.zoomTransform(svg.node()); const newTransform = currentTransform.scale(scale); svg.transition().duration(750).call(d3.zoom().transform, newTransform); }; window.resetGraph = () => { svg.transition().duration(750).call(d3.zoom().transform, d3.zoomIdentity); }; // Add zoom behavior const zoom = d3.zoom() .scaleExtent([0.5, 4]) .on('zoom', (event) => { svg.selectAll('g').attr('transform', event.transform); }); svg.call(zoom); } // Expose to window window.initializeGraphVisualization = initializeGraphVisualization;