Vue3+TypeScript完美实现AntVX6官方人工智能建模 DAG 图
简述:本文通过理解官方文档进行编写,实现官方的示例效果。
详情请见AntVX6官网:https://x6.antv.antgroup.com/
官方示例链接:https://x6.antv.antgroup.com/zh/examples/showcase/practices/#dag
实现结果如下:
代码解析:
代码解释为onMounted初始化页面顺序进行讲解
onMounted(() => {
buildAntvX6Container();
init(initData);
showNodeStatus(nodeStatusList);
// 将画布中元素缩小或者放大一定级别,让画布正好容纳所有元素,可以通过 maxScale 配置最大缩放级别
graph.value!.zoomToFit({ maxScale: 2 })
});
一、节点注册:
通过以下代码注册新的自定义节点dag-node,并使用组件化节点,组件节点代码请见后文。
register({
shape: 'dag-node', // 自定义节点的名称
width: 180, // 节点的宽度
height: 36, // 节点的高度
component: AlgoNode, // 与此节点关联的组件(通常用于渲染节点的内容)
ports: { // 定义节点的连接点(ports)
groups: {
left: { // 左侧连接点组
position: 'left', // 连接点的位置
attrs: {
circle: { // 连接点的形状和样式
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
right: { // 右侧连接点组
position: 'right', // 连接点的位置
attrs: {
circle: { // 连接点的形状和样式
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
},
}
});
二、边、连接器注册:
以下代码为注册自定义边与连接器的样式(此连接器注册参考官方数据加工 DAG 图)
// 注册自定义边样式
Graph.registerEdge(
"dag-edge",
{
inherit: "edge",
attrs: {
line: {
stroke: "#C2C8D5",
strokeWidth: 1,
targetMarker: null,
},
},
},
true
);
//注册连接器的样式
Graph.registerConnector(
'algo-connector',
(sourcePoint, targetPoint) => {
const hgap = Math.abs(targetPoint.x - sourcePoint.x)
const path = new Path()
path.appendSegment(
Path.createSegment('M', sourcePoint.x - 4, sourcePoint.y),
)
path.appendSegment(
Path.createSegment('L', sourcePoint.x + 12, sourcePoint.y),
)
// 水平三阶贝塞尔曲线
path.appendSegment(
Path.createSegment(
'C',
sourcePoint.x < targetPoint.x
? sourcePoint.x + hgap / 2
: sourcePoint.x - hgap / 2,
sourcePoint.y,
sourcePoint.x < targetPoint.x
? targetPoint.x - hgap / 2
: targetPoint.x + hgap / 2,
targetPoint.y,
targetPoint.x - 6,
targetPoint.y,
),
)
path.appendSegment(
Path.createSegment('L', targetPoint.x + 2, targetPoint.y),
)
return path.serialize()
},
true,
)
三、新建画布
设置新注册的节点、线、连接器样式,并开启数据监听!!!很重要!!!。
const buildAntvX6Container = () => {
const container = document.getElementById("antvcontainer");
if (!container) {
console.error("AntV X6 容器未找到");
return;
}
// 创建图形实例
graph.value = new Graph({
container: container,
// autoResize: true,
width: 2000,
height: 2000,
panning: {
enabled: true,
eventTypes: ["leftMouseDown", "mouseWheel"],
},
mousewheel: true,
background: {
color: "#d9e4f5",
},
grid: {
visible: true,
type: "doubleMesh",
args: [
{
color: "#eee",
thickness: 1,
},
{
color: "#ddd",
thickness: 1,
factor: 4,
},
],
},
connecting: {
connector: 'algo-connector',
snap: { radius: 50 },
allowBlank: false,
allowLoop: false,
allowNode: false,
allowEdge: false,
allowMulti: true,
highlight: true,
validateMagnet({ magnet }) {
return magnet.getAttribute("port-group") !== "left";
},
createEdge() {
return graph.value!.createEdge({
shape: "dag-edge",
attrs: {
line: {
strokeDasharray: "5 5",
},
},
zIndex: -1,
});
},
},
highlighting: {
magnetAvailable: {
name: "stroke",
args: {
attrs: {
fill: "#fff",
stroke: "#31d0c6",
strokeWidth: 4,
},
},
},
},
});
// 使用选择插件
graph.value.use(
new Selection ({
multiple: true,
rubberEdge: true,
rubberNode: true,
modifiers: 'shift',
rubberband: true,
})
)
// 监听边连接事件
graph.value.on('edge:connected', ({ edge }) => {
edge.attr({
line: {
strokeDasharray: '',
},
})
})
// 监听节点数据变化事件
graph.value.on('node:change:data', ({ node }) => {
const edges = graph.value!.getIncomingEdges(node) // 获取入边
const { status } = node.getData() as { status: string } // 获取节点状态
edges?.forEach((edge) => {
if (status === 'running') {
edge.attr('line/strokeDasharray', 5) // 设置虚线
edge.attr('line/style/animation', 'running-line 30s infinite linear') // 添加动画
} else {
edge.attr('line/strokeDasharray', '') // 清除虚线
edge.attr('line/style/animation', '') // 移除动画
}
})
})
};
四、初始化数据
初始化节点的连接关系、位置关系、id、形状,这个步骤结束后界面就会显示出节点信息,接下来就是模拟运行动效。
const init = (data: Cell.Metadata[]) => {
const cells = data.map((item) => {
if (item.shape === "dag-node") {
return graph.value!.createNode(item);
}
return graph.value!.createEdge(item);
});
graph.value!.resetCells(cells);
};
// 初始化节点和边
const initData: Cell.Metadata[] = [
{
id: "1",
shape: "dag-node",
x: 300,
y: 110,
data: { label: "读数据", status: "success" },
ports: [{ id: "1-1", group: "right" }],
},
{
id: "2",
shape: "dag-node",
x: 600,
y: 110,
data: { label: "逻辑回归", status: "success" },
ports: [
{ id: "2-1", group: "left" },
{ id: "2-2", group: "right" },
{ id: "2-3", group: "right" },
],
},
{
id: "3",
shape: "dag-node",
x: 900,
y: 0,
data: { label: "模型预测", status: "success" },
ports: [
{ id: "3-1", group: "left" },
{ id: "3-2", group: "right" },
],
},
{
id: "4",
shape: "dag-node",
x: 900,
y: 300,
data: { label: "读取参数", status: "success" },
ports: [
{ id: "4-1", group: "left" },
{ id: "4-2", group: "right" },
],
},
{
id: "5",
shape: "dag-edge",
source: { cell: "1", port: "1-1" },
target: { cell: "2", port: "2-1" },
zIndex: 0,
},
{
id: "6",
shape: "dag-edge",
source: { cell: "2", port: "2-2" },
target: { cell: "3", port: "3-1" },
zIndex: 0,
},
{
id: "7",
shape: "dag-edge",
source: { cell: "2", port: "2-3" },
target: { cell: "4", port: "4-1" },
zIndex: 0,
},
];
五、进行数据处理模拟
通过回调showNodeStatus方法进行数据处理模拟,每3s进行一次遍历nodeStatusList参数,并通过shift()函数进行数据优化,也就是每次执行完都把已经执行完的数据给删除。
const showNodeStatus = async (statusList: NodeStatus[][]) => {
const status = statusList.shift();
if (status) {
status.forEach(({ id, status }) => {
const node = graph.value!.getCellById(id);
const data = node.getData() as NodeStatus;
node.setData({
...data,
status,
});
});
setTimeout(() => {
showNodeStatus(statusList);
}, 3000 );
}
};
// 节点状态列表
const nodeStatusList: NodeStatus[][] = [
[
{ id: "1", status: "running" },
{ id: "2", status: "default" },
{ id: "3", status: "default" },
{ id: "4", status: "default" },
],
[
{ id: "1", status: "success" },
{ id: "2", status: "running" },
{ id: "3", status: "default" },
{ id: "4", status: "default" },
],
[
{ id: "1", status: "success" },
{ id: "2", status: "success" },
{ id: "3", status: "running" },
{ id: "4", status: "running" },
],
[
{ id: "1", status: "success" },
{ id: "2", status: "success" },
{ id: "3", status: "success" },
{ id: "4", status: "failed" },
],
];
线变化的在初始化画布时就已开启了数据监听,通过数据监听进行渲染。
// 监听节点数据变化事件
graph.value.on('node:change:data', ({ node }) => {
const edges = graph.value!.getIncomingEdges(node) // 获取入边
const { status } = node.getData() as { status: string } // 获取节点状态
edges?.forEach((edge) => {
if (status === 'running') {
edge.attr('line/strokeDasharray', 5) // 设置虚线
edge.attr('line/style/animation', 'running-line 30s infinite linear') // 添加动画
} else {
edge.attr('line/strokeDasharray', '') // 清除虚线
edge.attr('line/style/animation', '') // 移除动画
}
})
})
六、节点样式的动态变化
初始想法:通过watch去监听props,后发现props并不为Vue的响应数据,也就是此数据并不是通过响应式数据传输的,所以用watch监听不到props参数,这也就导致节点的渲染一直停留在第一次的状态。(但其实props的值是在变化的)
// 监听 node 数据变化
watch(
() => props,
(newData, oldData) => {
console.log("Data changed:", newData);
updateNodeData();
},
{ deep: true }
);
改进后的想法 :通过监听change:data进行节点动态渲染,通过监听chage:data可以发现其实数据是在动态变化的,然后就可以通过chage:data的监听实现节点的动态改变。
const props = defineProps<{
node: {
getData: () => { label: string; status: string };
on: (event: string, callback: Function) => void;
off: (event: string, callback: Function) => void;
};
}>();
// 监听节点数据变化
onMounted(() => {
updateNodeData(); // 初始化数据
props.node.on('change:data', updateNodeData);
});
// 清理监听器
onBeforeUnmount(() => {
props.node.off('change:data', updateNodeData);
});
记得要使用响应式数据哦~
<template>
<div :class="['node', status]">
<img :src="image.logo" alt="logo" />
<span class="label">{{ label }}</span>
<span class="status">
<img v-if="status === 'success'" :src="image.success" alt="success" />
<img v-if="status === 'failed'" :src="image.failed" alt="failed" />
<img v-if="status === 'running'" :src="image.running" alt="running" />
</span>
</div>
</template>
// 定义响应式变量
const label = ref('');
const status = ref<'default' | 'success' | 'failed' | 'running'>('default');
const updateNodeData = () => {
const data = props.node.getData();
label.value = data.label;
status.value = data.status as any;
};
成果的效果如下:
完整代码
一、画布代码如下:
<template>
<div style="width: 100%; height: 100vh;display: flex;justify-content: center;align-items: center">
<div id="antvcontainer"></div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import {Cell, Graph, Path} from "@antv/x6";
import AlgoNode from "@/views/AntVDag/node.vue";
import { register } from '@antv/x6-vue-shape'
// AlgoNode 组件
register({
shape: 'dag-node', // 自定义节点的名称
width: 180, // 节点的宽度
height: 36, // 节点的高度
component: AlgoNode, // 与此节点关联的组件(通常用于渲染节点的内容)
ports: { // 定义节点的连接点(ports)
groups: {
left: { // 左侧连接点组
position: 'left', // 连接点的位置
attrs: {
circle: { // 连接点的形状和样式
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
right: { // 右侧连接点组
position: 'right', // 连接点的位置
attrs: {
circle: { // 连接点的形状和样式
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
},
}
});
// 注册自定义边样式
Graph.registerEdge(
"dag-edge",
{
inherit: "edge",
attrs: {
line: {
stroke: "#C2C8D5",
strokeWidth: 1,
targetMarker: null,
},
},
},
true
);
//注册连接器的样式
Graph.registerConnector(
'algo-connector',
(sourcePoint, targetPoint) => {
const hgap = Math.abs(targetPoint.x - sourcePoint.x)
const path = new Path()
path.appendSegment(
Path.createSegment('M', sourcePoint.x - 4, sourcePoint.y),
)
path.appendSegment(
Path.createSegment('L', sourcePoint.x + 12, sourcePoint.y),
)
// 水平三阶贝塞尔曲线
path.appendSegment(
Path.createSegment(
'C',
sourcePoint.x < targetPoint.x
? sourcePoint.x + hgap / 2
: sourcePoint.x - hgap / 2,
sourcePoint.y,
sourcePoint.x < targetPoint.x
? targetPoint.x - hgap / 2
: targetPoint.x + hgap / 2,
targetPoint.y,
targetPoint.x - 6,
targetPoint.y,
),
)
path.appendSegment(
Path.createSegment('L', targetPoint.x + 2, targetPoint.y),
)
return path.serialize()
},
true,
)
// 定义 graph 的引用
const graph = ref<Graph | null>(null);
// 节点状态的类型定义
interface NodeStatus {
id: string;
status: "default" | "success" | "failed" | "running";
label?: string;
}
import { Selection } from '@antv/x6-plugin-selection'
const buildAntvX6Container = () => {
const container = document.getElementById("antvcontainer");
if (!container) {
console.error("AntV X6 容器未找到");
return;
}
// 创建图形实例
graph.value = new Graph({
container: container,
// autoResize: true,
width: 2000,
height: 2000,
panning: {
enabled: true,
eventTypes: ["leftMouseDown", "mouseWheel"],
},
mousewheel: true,
background: {
color: "#d9e4f5",
},
grid: {
visible: true,
type: "doubleMesh",
args: [
{
color: "#eee",
thickness: 1,
},
{
color: "#ddd",
thickness: 1,
factor: 4,
},
],
},
connecting: {
connector: 'algo-connector',
snap: { radius: 50 },
allowBlank: false,
allowLoop: false,
allowNode: false,
allowEdge: false,
allowMulti: true,
highlight: true,
validateMagnet({ magnet }) {
return magnet.getAttribute("port-group") !== "left";
},
createEdge() {
return graph.value!.createEdge({
shape: "dag-edge",
attrs: {
line: {
strokeDasharray: "5 5",
},
},
zIndex: -1,
});
},
},
highlighting: {
magnetAvailable: {
name: "stroke",
args: {
attrs: {
fill: "#fff",
stroke: "#31d0c6",
strokeWidth: 4,
},
},
},
},
});
// 使用选择插件
graph.value.use(
new Selection ({
multiple: true,
rubberEdge: true,
rubberNode: true,
modifiers: 'shift',
rubberband: true,
})
)
// 监听边连接事件
graph.value.on('edge:connected', ({ edge }) => {
edge.attr({
line: {
strokeDasharray: '',
},
})
})
// 监听节点数据变化事件
graph.value.on('node:change:data', ({ node }) => {
const edges = graph.value!.getIncomingEdges(node) // 获取入边
const { status } = node.getData() as { status: string } // 获取节点状态
edges?.forEach((edge) => {
if (status === 'running') {
edge.attr('line/strokeDasharray', 5) // 设置虚线
edge.attr('line/style/animation', 'running-line 30s infinite linear') // 添加动画
} else {
edge.attr('line/strokeDasharray', '') // 清除虚线
edge.attr('line/style/animation', '') // 移除动画
}
})
})
};
// 节点状态列表
const nodeStatusList: NodeStatus[][] = [
[
{ id: "1", status: "running" },
{ id: "2", status: "default" },
{ id: "3", status: "default" },
{ id: "4", status: "default" },
],
[
{ id: "1", status: "success" },
{ id: "2", status: "running" },
{ id: "3", status: "default" },
{ id: "4", status: "default" },
],
[
{ id: "1", status: "success" },
{ id: "2", status: "success" },
{ id: "3", status: "running" },
{ id: "4", status: "running" },
],
[
{ id: "1", status: "success" },
{ id: "2", status: "success" },
{ id: "3", status: "success" },
{ id: "4", status: "failed" },
],
];
const showNodeStatus = async (statusList: NodeStatus[][]) => {
const status = statusList.shift();
if (status) {
status.forEach(({ id, status }) => {
const node = graph.value!.getCellById(id);
const data = node.getData() as NodeStatus;
node.setData({
...data,
status,
});
});
setTimeout(() => {
showNodeStatus(statusList);
}, 3000 );
}
};
// 初始化节点和边
const initData: Cell.Metadata[] = [
{
id: "1",
shape: "dag-node",
x: 300,
y: 110,
data: { label: "读数据", status: "success" },
ports: [{ id: "1-1", group: "right" }],
},
{
id: "2",
shape: "dag-node",
x: 600,
y: 110,
data: { label: "逻辑回归", status: "success" },
ports: [
{ id: "2-1", group: "left" },
{ id: "2-2", group: "right" },
{ id: "2-3", group: "right" },
],
},
{
id: "3",
shape: "dag-node",
x: 900,
y: 0,
data: { label: "模型预测", status: "success" },
ports: [
{ id: "3-1", group: "left" },
{ id: "3-2", group: "right" },
],
},
{
id: "4",
shape: "dag-node",
x: 900,
y: 300,
data: { label: "读取参数", status: "success" },
ports: [
{ id: "4-1", group: "left" },
{ id: "4-2", group: "right" },
],
},
{
id: "5",
shape: "dag-edge",
source: { cell: "1", port: "1-1" },
target: { cell: "2", port: "2-1" },
zIndex: 0,
},
{
id: "6",
shape: "dag-edge",
source: { cell: "2", port: "2-2" },
target: { cell: "3", port: "3-1" },
zIndex: 0,
},
{
id: "7",
shape: "dag-edge",
source: { cell: "2", port: "2-3" },
target: { cell: "4", port: "4-1" },
zIndex: 0,
},
];
const init = (data: Cell.Metadata[]) => {
const cells = data.map((item) => {
if (item.shape === "dag-node") {
return graph.value!.createNode(item);
}
return graph.value!.createEdge(item);
});
graph.value!.resetCells(cells);
};
// 挂载后初始化
onMounted(() => {
buildAntvX6Container();
init(initData);
showNodeStatus(nodeStatusList);
// 将画布中元素缩小或者放大一定级别,让画布正好容纳所有元素,可以通过 maxScale 配置最大缩放级别
graph.value!.zoomToFit({ maxScale: 2 })
});
</script>
<style scoped>
</style>
二、节点组件代码如下:
<template>
<div :class="['node', status]">
<img :src="image.logo" alt="logo" />
<span class="label">{{ label }}</span>
<span class="status">
<img v-if="status === 'success'" :src="image.success" alt="success" />
<img v-if="status === 'failed'" :src="image.failed" alt="failed" />
<img v-if="status === 'running'" :src="image.running" alt="running" />
</span>
</div>
</template>
<script setup lang="ts">
import {ref, onMounted, onBeforeUnmount, watch} from 'vue';
// 定义响应式变量
const label = ref('');
const status = ref<'default' | 'success' | 'failed' | 'running'>('default');
// 图片路径
const image = {
logo: 'logo.png',
success: 'success.png',
failed: 'failed.png',
running: 'running.png',
};
const props = defineProps<{
node: {
getData: () => { label: string; status: string };
on: (event: string, callback: Function) => void;
off: (event: string, callback: Function) => void;
};
}>();
// 数据更新处理函数
const updateNodeData = () => {
const data = props.node.getData();
label.value = data.label;
status.value = data.status as any;
};
// 监听节点数据变化
onMounted(() => {
updateNodeData(); // 初始化数据
props.node.on('change:data', updateNodeData);
});
// 清理监听器
onBeforeUnmount(() => {
props.node.off('change:data', updateNodeData);
});
</script>
<style >
.node {
display: flex;
align-items: center;
width: 100%;
height: 100%;
background-color: #fff;
border: 1px solid #c2c8d5;
border-left: 4px solid #5F95FF;
border-radius: 4px;
box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06);
}
.node img {
width: 20px;
height: 20px;
flex-shrink: 0;
margin-left: 8px;
}
.node .label {
display: inline-block;
flex-shrink: 0;
width: 104px;
margin-left: 8px;
color: #666;
font-size: 12px;
}
.node .status {
flex-shrink: 0;
}
.node.success {
border-left: 4px solid #52c41a;
}
.node.failed {
border-left: 4px solid #ff4d4f;
}
.node.running .status img {
animation: spin 1s linear infinite;
}
.x6-node-selected .node {
border-color: #1890ff;
border-radius: 2px;
box-shadow: 0 0 0 4px #d4e8fe;
}
.x6-node-selected .node.success {
border-color: #52c41a;
border-radius: 2px;
box-shadow: 0 0 0 4px #ccecc0;
}
.x6-node-selected .node.failed {
border-color: #ff4d4f;
border-radius: 2px;
box-shadow: 0 0 0 4px #fedcdc;
}
.x6-edge:hover path:nth-child(2){
stroke: #1890ff;
stroke-width: 1px;
}
.x6-edge-selected path:nth-child(2){
stroke: #1890ff;
stroke-width: 1.5px !important;
}
@keyframes running-line {
to {
stroke-dashoffset: -1000;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
原文地址:https://blog.csdn.net/qq_58055766/article/details/145140207
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!