echarts关系拓扑图一个demo
需求描述
关系图分层级展示,做一个类似树结构的展示界面,每一层级节点按照权重计算坐标位置,父节点的位置放在在下层子节点中间。
需求分析
-
关系图不是真正的树结构,所以目标节点只有在其下一层的才是计算的‘子节点’,如果兄弟节点有共同的下层‘子节点’,按照从左到右的顺序优先排列(也就是说,若节点
A
和节点B
是同一层的兄弟节点,他们有共同的下层节点C
,那么就把C
作为A
的‘子节点’,在计算B
节点坐标的时候就不在使用C
节点作为参照了),这样做的目的是为了避免两个兄弟节点具有相同的下层‘子节点’导致渲染节点重合的问题。 -
确定分层:关系层次是确定的,所以可以根据分类数组遍历创建一个收集每一层节点的数组。
-
节点排序:根据分层的节点,从上到下依次排出节点的顺序,为了计算节点坐标做准备。
-
计算所有叶子节点横坐标:根据所有叶子节点的数量平分区间。遍历分层数组,层从上到下,节点从左到右,如果当前节点是第一个叶子节点,设置权重比例为 w = 1,如果当前节点有子节点就去遍历其子节点,如果其子节点是叶子节点就设置权重 w = w + 1, 按照遍历顺序每一个叶子节点权重 + 1。如果一共有n 个叶子节点那么就把空间平均分成
n + 1
份。这个(n+1)
要记录下来,等分空间的每一份宽度就是boxWidth / (n + 1)
,用叶子节点的权重 * 每一份的宽度就是当前叶子节点的横坐标了。 -
计算除了叶子节点之外的节点横坐标:在上一步基础上,倒序遍历分类数组,比如只有3层,从第三层开始遍历,计算第二层节点横坐标,只要将第二层某一节点的下层所有子节点(第三层的叶子节点)中两头的节点权重相加除以2作为当前节点在本层的权重,其他节点类似求权重即可。
-
设置节点坐标:纵坐标可以设置成固定值,横坐标按照节点权重乘以每一份宽度即可
w * boxWidth / (n + 1)
问题解决
- 按分类排序节点
// 分类数组 before
categories: [
{name: '分类1'},
{name: '分类2'},
{name: '分类3'}
],
// 分类数组 after,增加了children,其中就是node节点
cateArr: [
name: '分类1',
children: [...]
},
{
name: '分类2',
children: [...]
},
{
name: '分类3',
children: [...]
}
],
// children 中的 node 节点
{
"id": "1",
"category": 0,
"name": "节点名称1",
"value": 10
}
/**
* @Description: 根据分层遍历计算每个节点的子节点数
* @param {*} links 节点关系数组(排序后)
* @param {*} arr 分类数组
* @param {*} lastIdx 数组最后一个index, 最后一层就是子节点,不用参与遍历
* @return {*}
*/
sortNodes(links, arr, lastIdx) {
const cArr = cloneDeep(arr)
let vm = this
cArr.forEach((item, idx) => {
if (idx !== lastIdx) {
let prev = []
let nodes = item.children
nodes.forEach((node) => {
let id = node.id
let link = vm.getLinkIds(links, id, prev, item.pIds)
node.ids = link.ids
node.cIds = link.cIds
prev = link.prev
})
}
})
this.setLeafWeight(cArr)
}
- 叶子节点设置权重
/**
* @Description: 设置叶子节点的权重
* 每个叶子节点权重为1,其余节点权重为其下层所有关联节点权重的中间值
* @param {*} arr 分类数组
* @return {*}
*/
setLeafWeight(arr) {
let vm = this
const cArr = cloneDeep(arr)
let len = cArr.length - 1
let len2 = len - 1 // 倒数第二层
let w = 1 // 节点权重初始值
vm.nodeMap.clear()
// 从上到下计算所有叶子节点权重
cArr.forEach((item, idx) => {
if (idx !== len) {
const nodes = item.children
nodes.forEach((node) => {
let ids = node.ids
if (!ids.length) {
vm.nodeMap.set(node.id, w)
w++
}
if (idx === len2 && ids.length) {
ids.forEach((id) => {
vm.nodeMap.set(id, w)
w++
})
}
})
}
})
this.leafGrad = vm.nodeMap.size + 1
this.setRestWeight(cArr, len2)
},
- 非叶子节点设置权重
/**
* @Description: 设置除叶子节点之外的节点权重
* @param {*} arr 分类数组
* @param {*} len 分类数组倒数第二层index
* @return {*}
*/
setRestWeight(arr, len) {
let map = this.nodeMap
for (let i = len; i > -1; i--) {
let item = arr[i].children
Array.isArray(item) &&
item.forEach((node) => {
if (!map.has(node.id)) {
if (node.ids.length === 1) { // 只有一个字节的的直接取子节点值
let mid = map.get(node.ids[0])
map.set(node.id, mid)
} else {
let [start, end] = node.cIds
let mid = (map.get(start) + map.get(end)) / 2
map.set(node.id, mid)
}
}
})
}
},
- 获取节点坐标值
/**
* @Description: 获取节点坐标值
* @param {*} cArr 大分类
* @param {*} len 分类数组长度
* @return {*}
*/
getNodePos(cArr, len) {
const boxDom = document.getElementById('myChart')
const boxWidth = boxDom.clientWidth - 100
const ySize = Math.ceil(boxDom.clientHeight / (len + 1))
const xGrad = boxWidth / this.leafGrad // 叶子节点分割区间宽度
let allNodes = []
cArr.forEach((c, idx) => {
let children = c.children
if (Array.isArray(children) && children.length) {
let nodes = cloneDeep(children)
const height = ySize * (idx + 1)
nodes.forEach((node) => {
node.x = this.nodeMap.get(node.id) * xGrad
node.y = height
})
allNodes.push(...nodes)
}
})
return allNodes
},
注意
- 关系数组中数据排序会影响节点的坐标,所以通过遍历分类数组,根据每一个节点的id在关系数组中按照当前节点的指向target来排序。