上期回顾:历程[一]描述了基本的树状图的绘制,默认节点类型defaultNode中的type是circle,下面这篇描述的是节点抽离自定义节点并做数据静态渲染。
官网地址:https://g6-next.antv.antgroup.com/manual/introduction
一、案例效果
二、自定义节点渲染
1. 主要通过G6.registerNode实现自定义节点的配置,官网地址:https://g6.antv.antgroup.com/manual/middle/elements/nodes/custom-node
G6.registerNode(
'nodeName',
{
options: {
style: {},
stateStyles: {
hover: {},
selected: {},
},
},
/**
* 绘制节点,包含文本
* @param {Object} cfg 节点的配置项
* @param {G.Group} group 图形分组,节点中图形对象的容器
* @return {G.Shape} 返回一个绘制的图形作为 keyShape,通过 node.get('keyShape') 可以获取。
* 关于 keyShape 可参考文档 核心概念-节点/边/Combo-图形 Shape 与 keyShape
*/
draw(cfg, group) {},
/**
* 绘制后的附加操作,默认没有任何操作
* @param {Object} cfg 节点的配置项
* @param {G.Group} group 图形分组,节点中图形对象的容器
*/
afterDraw(cfg, group) {},
/**
* 更新节点,包含文本
* @override
* @param {Object} cfg 节点的配置项
* @param {Node} node 节点
*/
update(cfg, node) {},
/**
* 更新节点后的操作,一般同 afterDraw 配合使用
* @override
* @param {Object} cfg 节点的配置项
* @param {Node} node 节点
*/
afterUpdate(cfg, node) {},
/**
* 响应节点的状态变化。
* 在需要使用动画来响应状态变化时需要被复写,其他样式的响应参见下文提及的 [配置状态样式] 文档
* @param {String} name 状态名称
* @param {Object} value 状态值
* @param {Node} node 节点
*/
setState(name, value, node) {},
/**
* 获取锚点(相关边的连入点)
* @param {Object} cfg 节点的配置项
* @return {Array|null} 锚点(相关边的连入点)的数组,如果为 null,则没有控制点
*/
getAnchorPoints(cfg) {},
},
// 继承内置节点类型的名字,例如基类 'single-node',或 'circle', 'rect' 等
// 当不指定该参数则代表不继承任何内置节点类型
extendedNodeType,
);
2. 将渲染的节点转换为组件渲染
在 Vue3 中,你可以通过 G6 的自定义节点功能,并在 draw 方法中创建一个新的 Vue 实例,然后将 Vue 组件的 HTML 内容添加到 G6 节点中。以下是一个基本的示例:
<script setup lang="ts">
import { createApp, nextTick } from 'vue'
import G6 from '@antv/g6'
import YourComponent from './YourComponent.vue'
G6.registerNode('vue-node', {
draw: (cfg, group) => {
const container = document.createElement('div')
const app = createApp(YourComponent, { ...cfg })
app.mount(container)
let shape
nextTick(() => {
shape = group.addShape('dom', {
attrs: {
width: cfg.size[0],
height: cfg.size[1],
html: container.innerHTML,
},
name: 'dom-shape',
})
})
return shape
},
})
</script>
在这个示例中,我们创建了一个新的 Vue 实例,并将 YourComponent 组件挂载到一个新创建的 div 元素上。然后,我们在 Vue 的 nextTick 中,将这个 div 的 HTML 内容添加到 G6 节点的 dom 形状中。
然后,你可以在你的图中使用这个新注册的 ‘vue-node’ 节点类型:
<script setup lang="ts">
const graph = new G6.Graph({
container: 'graph-container',
width: 800,
height: 600,
defaultNode: {
type: 'vue-node',
size: [100, 100],
},
})
graph.data({
nodes: [
{ id: 'node1', x: 100, y: 100, label: 'Node 1' },
{ id: 'node2', x: 200, y: 200, label: 'Node 2' },
],
edges: [
{ source: 'node1', target: 'node2' },
],
})
graph.render()
</script>
在这个示例中,我们创建了一个新的 G6 图表,并设置了默认节点类型为 ‘vue-node’。然后,我们定义了图表的数据,并调用 graph.render() 方法来渲染图表。
三、组件抽离及案例代码
1. 基本的绘制组件 【TopologyBase.vue】
<template>
<div :id="domId" class="w-full h-[95%]"></div>
</template>
<script setup lang="ts">
import { initData } from '@/common/constants/topologyData/initRender'
import G6 from '@antv/g6'
import { createApp, nextTick, onMounted, ref, watch } from 'vue'
import StatusNode from './StatusNode.vue'
const props = defineProps({
domId: {
type: String,
default: 'container',
},
treeData: {
type: Array,
},
})
const initTreeData = ref(props.treeData)
G6.registerNode(
'dom-node',
{
draw(cfg: any, group) {
let shape
const container = document.createElement('div')
const app = createApp(StatusNode, { domNodeMsg: { ...cfg } })
app.mount(container)
shape = group.addShape('dom', {
attrs: {
width: cfg.size[0],
height: cfg.size[1],
html: container.innerHTML,
},
name: 'dom-shape',
})
return shape
},
},
'single-node',
)
const initRender = () => {
const container = document.getElementById(props.domId)
const width = container?.scrollWidth
const height = container?.scrollHeight
const graph = new G6.TreeGraph({
container: props.domId,
width,
height,
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
},
defaultNode: {
type: 'dom-node', // 矩形 rect/ 默认circle
size: [80, 30],
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
},
fitCenter: true,
renderer: 'svg',
linkCenter: true,
defaultEdge: {
type: 'cubic-horizontal',
/* 通过配置边的 style 属性来设置弯曲半径和到端节点的最小距离 */
style: {
radius: 5,
offset: 10,
endArrow: true,
/* 设置其他样式 */
stroke: '#2c3e50',
},
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: function getId(d: { id: string }) {
// 节点 id 的回调函数
return d.id
},
getHeight: function getHeight() {
// 每个节点的高度
return 16
},
getWidth: function getWidth() {
// 每个节点的宽度
return 16
},
getVGap: function getVGap() {
// 每个节点的垂直间隙
return 30
},
getHGap: function getHGap() {
// 每个节点的水平间隙
return 50
},
},
})
graph.node(function (node) {
return {
label: node.id,
labelCfg: {
position: node.children && node.children.length > 0 ? 'left' : 'right',
offset: 5,
},
}
})
graph.data(initData)
graph.render()
graph.fitView()
if (typeof window !== 'undefined')
window.onresize = () => {
if (!graph || graph.get('destroyed')) return
if (!container || !container.scrollWidth || !container.scrollHeight) return
graph.changeSize(container.scrollWidth, container.scrollHeight)
}
}
watch(
() => props.treeData,
(newValue) => {
initTreeData.value = newValue
},
{ immediate: true, deep: true },
)
onMounted(() => {
nextTick(() => {
initRender()
})
})
</script>
2. 节点组件抽离【StatusNode.vue】
<template>
<div class="status-node">
<div :id="domNodeMsg.id" class="dom-node">
<span style="cursor: pointer">{{ domNodeMsg.name }}</span>
</div>
</div>
</template>
<script setup lang="ts">
defineProps(['domNodeMsg'])
</script>
<style scoped>
.status-node {
background-color: #fff;
border: 1px solid #ccc;
border-radius: 10px;
text-align: center;
font-size: 12px;
padding: 0px 5px;
color: #5b8ff9;
.dom-node {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
</style>
3. 组件引用
<TopologyBase domId="featureTreeInfoContainer" :treeData="tableData" />
4.数据格式
注意 id 必须为String格式
export const initData = {
id: '376',
name: '世界',
children: [{
id: '377',
name: '中国',
children: [{
id: '380',
name: '北方',
children: [],
}]
}]
}