Antv G6 拖拽到已有节点上创建新节点,并避免节点重复
G6
使用 API
-
G6 坐标转换:
graph.getPointByClient(clientX, clientY)(x, y)
。
可以将屏幕/页面坐标转换为渲染也就是画布坐标。 -
HTML 拖放 API:通过设置
HTML
的draggable
属性为true
,使元素可以被拖拽。这里可使用该 API 的dragend
事件(当拖拽操作结束时触发),生成对应节点。例如,用户可使用鼠标选择可拖拽元素,将元素拖拽到可放置元素,并释放鼠标按钮以放置这些元素。拖拽操作期间,会有一个可拖拽元素的半透明快照跟随着鼠标指针。
-
G6 生成新节点:
graph.addItem(type, model, stack)
。
新增元素(节点和边)。
注意:将会直接使用model
对象作为新增元素的数据模型,G6 内部可能会对其增加或修改一些必要的字段。若不希望原始参数被修改,建议在使用深拷贝后的model
。 -
G6 查找节点:
graph.find(type, fn)
。
根据具体规则查找单个元素。如果有符合规则的元素实例,则返回第一个匹配的元素实例,否则返回undefined
。const findNode = graph.find('node', (node) => { return node.get('model').x === 100; });
步骤
- 初始化 G6;
- 拖拽按钮到已有节点上(
:draggable="true"
); - 监听拖拽结束事件(
@dragend="addNode(item.type, $event)"
); - 将页面坐标转换为拓扑区域坐标(
this.graph.getPointByClient(e.x, e.y)
); - 获取被挂载的节点数据;
- 获取新节点的坐标,并尽可能避免重复;
- 生成新节点(
this.graph.addItem('node', node); this.graph.addItem('edge', edge);
)
代码
<template>
<div class="page">
...
<!-- 图表区域 -->
<div class="Graph-wrapper">
<!-- 拖拽按钮 -->
<div
v-for="item in addNodeBtns"
:key="item.type"
class="item"
>
<div
:class="['item-movement', item]"
:draggable="true"
@dragend="addNode(item.type, $event)"
/>
<div>{{ item.name }}</div>
</div>
<!-- 拓扑图 -->
<div id="graphContainer" style="width: 100%; height: 100%; position: absolute" />
</div>
</div>
</tempalte>
<script>
import G6 from "@antv/g6";
export default {
name: 'Test',
data() {
return {
...
addNodeBtns: [
{ type: 'a', name: 'testA' },
{ type: 'b', name: 'testB' },
],
};
},
methods: {
initGraph(data) {
const container = document.getElementById('graphContainer');
const graph = new G6.Graph({
container: 'graphContainer', // 图的 DOM 容器,可以传入该 DOM 的 id 或者直接传入容器的 HTML 节点对象。
width: container.clientWidth,
height: container.clientHeight,
animate: true, // 是否启用全局动画。
fitCenter: true, // 开启后,图将会被平移,图的中心将对齐到画布中心,但不缩放。优先级低于 fitView。
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
},
layout: {
type: 'dagre', // 层次布局。
rankdir: 'LR', // 可选,布局的方向, 从左至右布局(默认为图的中心)。
align: 'DL', // 可选,节点对齐方式,对齐到左下角(默认为中间对齐)。
nodesep: 20, // 可选,节点间距(px)。在 rankdir 为 'TB' 或 'BT' 时是节点的水平间距;在rankdir 为 'LR' 或 'RL' 时代表节点的竖直方向间距。
ranksep: 50, // 可选,层间距(px)。在 rankdir 为 'TB' 或 'BT' 时是竖直方向相邻层间距;在rankdir 为 'LR' 或 'RL' 时代表水平方向相邻层间距。
controlPoints: true, // 可选, 是否保留布局连线的控制点。默认值:false。
},
});
graph.get('canvas').set('localRefresh', false); // 关掉局部渲染,防止在缩放和拖动中会出现轨迹的线条
graph.data(data);
graph.render();
...
this.graph = graph;
},
addNode(type, e) {
// 将屏幕坐标转为拓扑区域坐标
const point = this.graph.getPointByClient(e.x, e.y);
// 获取挂载的节点
const currentMountedNode = this.graph.find('node', (node) => {
return (node.get('model').x >= point.x - 30 && node.get('model').x <= point.x + 30) && (node.get('model').y >= point.y - 30 && node.get('model').y <= point.y + 30);
});
if(!currentMountedNode?.get('model')){ return; };
...
// 获取新节点的坐标
const coordinate = this.getNodeCoordinate(currentMountedNode.get('model').x + 150, currentMountedNode.get('model').y);
const node = {
id: uuid, // 建议使用 uuid,尽量避免重复。
label: type, // 元素的文本标签,有该字段时默认会渲染 label 。
type: type, // 元素的类型。
x: coordinate.x,
y: coordinate.y,
...
};
// 生成从挂载节点到新节点的连线
const edge = {
source: currentMountedNode.get('model').id,
target: node.id,
...
};
this.graph.addItem('node', node);
this.graph.addItem('edge', edge);
},
// 尽可能不生成重复位置的节点
getNodeCoordinate(x, y) {
let currentX = x;
let currentY = y;
// 获取当前节点位置附近是否已有节点,如果有,则重新生成坐标。
const node = this.findNodeByCoordinate(currentX, currentY);
if(!node?.get('model')){
return {x, y};
}
currentX = x + 70;
currentX = y + 50;
return this.getNodeCoordinate(currentX, currentY);
},
// 获取当前坐标附近的节点
findNodeByCoordinate(x, y) {
const node = this.graph.find('node', (node) => {
return (node.get('model').x >= point.x - 50 && node.get('model').x <= point.x + 50) && (node.get('model').y >= point.y - 30 && node.get('model').y <= point.y + 30);
});
return node;
},
};
...
}
</script>
END
- 参考文章:Antv G6 拖拽生成节点