123 lines
3.0 KiB
TypeScript
123 lines
3.0 KiB
TypeScript
// @ts-nocheck
|
||
import * as d3 from 'd3';
|
||
import './d3.css';
|
||
|
||
export const drawGraph = (graphData) => {
|
||
// 初始配置:宽度和高度通过容器自适应
|
||
const svg = d3.select('.ai-graph');
|
||
|
||
const margin = { top: 20, right: 20, bottom: 20, left: 20 };
|
||
|
||
const updateChartSize = () => {
|
||
const width = svg.node().clientWidth - margin.left - margin.right;
|
||
const height = svg.node().clientHeight - margin.top - margin.bottom;
|
||
|
||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||
|
||
return { width, height };
|
||
};
|
||
|
||
let { width, height } = updateChartSize();
|
||
|
||
// 使用力导向布局
|
||
const simulation = d3
|
||
.forceSimulation(graphData.nodes)
|
||
.force(
|
||
'link',
|
||
d3
|
||
.forceLink(graphData.links)
|
||
.id((d) => d.id)
|
||
.distance(100)
|
||
)
|
||
.force('charge', d3.forceManyBody().strength(-300))
|
||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||
.force('collide', d3.forceCollide(20)); // 防止节点重叠,半径设置为20
|
||
|
||
// 绘制连线
|
||
const link = svg.append('g')
|
||
.selectAll('line')
|
||
.data(graphData.links)
|
||
.enter()
|
||
.append('line')
|
||
.attr('class', 'link');
|
||
|
||
// 绘制节点
|
||
const node = svg.append('g')
|
||
.selectAll('g')
|
||
.data(graphData.nodes)
|
||
.enter()
|
||
.append('g')
|
||
.attr('class', 'node');
|
||
|
||
node.append('circle').attr('r', 10);
|
||
|
||
// 添加节点标签
|
||
node
|
||
.append('text')
|
||
.attr('dx', 12)
|
||
.attr('dy', '.35em')
|
||
.text((d) => d.label);
|
||
|
||
// 限制节点在SVG范围内
|
||
const clampPosition = (d, width, height) => {
|
||
d.x = Math.max(10, Math.min(width - 10, d.x)); // 10为节点半径
|
||
d.y = Math.max(10, Math.min(height - 10, d.y));
|
||
};
|
||
|
||
// 更新节点和连线位置
|
||
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) => {
|
||
// 限制节点在SVG内部
|
||
clampPosition(d, width, height);
|
||
return `translate(${d.x},${d.y})`;
|
||
});
|
||
});
|
||
|
||
// 添加拖拽事件
|
||
node.call(
|
||
d3
|
||
.drag()
|
||
.on('start', (event, d) => {
|
||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||
d.fx = d.x;
|
||
d.fy = d.y;
|
||
})
|
||
.on('drag', (event, d) => {
|
||
d.fx = event.x;
|
||
d.fy = event.y;
|
||
})
|
||
.on('end', (event, d) => {
|
||
if (!event.active) simulation.alphaTarget(0);
|
||
d.fx = null;
|
||
d.fy = null;
|
||
})
|
||
);
|
||
|
||
// 添加双击事件
|
||
node.on('dblclick', (event, d) => {
|
||
d.fx = null;
|
||
d.fy = null;
|
||
console.log('dblclick', d);
|
||
});
|
||
|
||
// 监听窗口大小变化时更新图表
|
||
const resize = () => {
|
||
const { width, height } = updateChartSize();
|
||
simulation.force('center', d3.forceCenter(width / 2, height / 2));
|
||
simulation.alpha(1).restart(); // 重启仿真以更新布局
|
||
};
|
||
|
||
window.addEventListener('resize', resize);
|
||
|
||
// 在需要的时候手动调用,销毁图表时可以移除监听
|
||
return () => {
|
||
window.removeEventListener('resize', resize);
|
||
};
|
||
};
|