需求描述
關系圖分層級展示,做一個類似樹結構的展示界面,每一層級節點按照權重計算坐標位置,父節點的位置放在在下層子節點中間。
需求分析
-
關系圖不是真正的樹結構,所以目標節點只有在其下一層的才是計算的‘子節點’,如果兄弟節點有共同的下層‘子節點’,按照從左到右的順序優先排列(也就是說,若節點
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來排序。