| |
| |
| |
| |
|
|
| function initializeGraphVisualization() { |
| const container = document.getElementById('graph-container'); |
| if (!container) return; |
| |
| const width = container.clientWidth; |
| const height = container.clientHeight || 384; |
| |
| |
| const svg = d3.select('#entity-graph'); |
| svg.selectAll('*').remove(); |
| svg.attr('viewBox', [0, 0, width, height]); |
| |
| |
| 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' } |
| ]; |
| |
| |
| 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)); |
| |
| |
| 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'); |
| |
| |
| 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); |
| |
| |
| 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.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.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'); |
| |
| |
| node.append('title') |
| .text(d => `${d.name}\nRisk: ${d.risk}%\nType: ${d.type}`); |
| |
| |
| node.on('click', (event, d) => { |
| viewEntity(d.id); |
| }); |
| |
| |
| 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; |
| } |
| |
| |
| 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); |
| }; |
| |
| |
| const zoom = d3.zoom() |
| .scaleExtent([0.5, 4]) |
| .on('zoom', (event) => { |
| svg.selectAll('g').attr('transform', event.transform); |
| }); |
| |
| svg.call(zoom); |
| } |
|
|
| |
| window.initializeGraphVisualization = initializeGraphVisualization; |