本文是關於如何使用可視化庫 gojs 完成節點分組關系展示的,從零基礎到實現最終效果。希望對使用 gojs 的小伙伴有幫助。
1. 節點分組需求及 demo 展示
需求
- 能正確展示組的層次,以及節點之間的關系。
- 單選節點、多選節點,獲取到節點信息
- 選中組,能選中組中的節點,能獲取到組中的節點信息
- 選中節點,當前節點視為根節點,能選中根節點連線下的所有節點,並獲取到節點信息
2. 准備
- 從后端獲取到的接口數據:
const data = {
"properties": [
{ "key": "t-2272", "parentKey": "j-1051", "name": "哈哈" },
{ "key": "p-344", "parentKey": "g--1586357764", "name": "test" },
{ "key": "t-2271", "parentKey": "j-1051", "name": "查詢" },
{ "key": "t-2275", "parentKey": "j-1052", "name": "開開心心" },
{ "key": "j-1054", "parentKey": "p-344", "name": "嘻嘻" },
{ "key": "t-2274", "parentKey": "j-1052", "name": "查詢" },
{ "key": "j-1051", "parentKey": "p-444", "name": "hello" },
{ "key": "j-1052", "parentKey": "p-444", "name": "編輯" },
{ "key": "t-2281", "parentKey": "j-1054", "name": "嘻嘻" },
{ "key": "p-444", "parentKey": "g--1586357624", "name": "test" },
{ "key": "g--1586357624", "name": "數據組1" },
{ "key": "g--1586357764", "name": "數據組2" },
{ "key": "t-2273", "parentKey": "j-1051", "name": "新建" }
],
"dependencies": [
{ "sourceKey": "t-2272", "targetKey": "t-2274" },
{ "sourceKey": "t-2274", "targetKey": "t-2275" },
{ "sourceKey": "t-2273", "targetKey": "t-2272" },
{ "sourceKey": "t-2271", "targetKey": "t-2272" },
{ "sourceKey": "t-2272", "targetKey": "t-2281" }
]
}
- 參考 gojs demo:grouping、 navigation
3. 實現步驟1:數據組建
-
gojs 圖表實例所需數據結構如下:
diagram.model = new go.GraphLinksModel( [ // node data { key: "A"}, { key: "F", group: "Omega"}, { key: "G"}, { key: "Chi", isGroup: true }, ], [ // link data { from: "A", to: "A" }, { from: "F", to: "G" }, { from: "G", to: "Chi"} ] );
-
根據接口數據構建出的最終數據如下:
node
[
{ "key": "g--1586357624", "text": "數據組1", "type": "g", "isGroup": true },
{ "key": "p-444", "text": "test", "type": "p", "isGroup": true, "group": "g--1586357624" },
{ "key": "j-1051", "text": "hello", "type": "j", "isGroup": true, "group": "p-444" },
{ "key": "t-2272", "text": "哈哈", "type": "t", "group": "j-1051" },
{ "key": "t-2271", "text": "查詢", "type": "t", "group": "j-1051" },
{ "key": "t-2273", "text": "新建", "type": "t", "group": "j-1051" },
{ "key": "j-1052", "text": "編輯", "type": "j", "isGroup": true, "group": "p-444" },
{ "key": "t-2275", "text": "開開心心", "type": "t", "group": "j-1052" },
{ "key": "t-2274", "text": "查詢", "type": "t", "group": "j-1052" },
{ "key": "g--1586357764", "text": "數據組2", "type": "g", "isGroup": true },
{ "key": "p-344", "text": "test", "type": "p", "isGroup": true, "group": "g--1586357764" },
{ "key": "j-1054", "text": "嘻嘻", "type": "j", "isGroup": true, "group": "p-344" },
{ "key": "t-2281", "text": "嘻嘻", "type": "t", "group": "j-1054" }
]
link
[
{ "from": "t-2272", "to": "t-2274", "nextLinks": [ "t-2274", "t-2275" ] },
{ "from": "t-2274", "to": "t-2275", "nextLinks": [ "t-2275" ] },
{ "from": "t-2273", "to": "t-2272", "nextLinks": [ "t-2272", "t-2274", "t-2275" ] },
{ "from": "t-2271", "to": "t-2272", "nextLinks": [ "t-2272", "t-2274", "t-2275" ] },
{ "from": "t-2272", "to": "t-2281", "nextLinks": [ "t-2281" ] }
]
如何根據接口數據組裝出所需數據就不介紹了。text字段用於顯示組及節點的標題,nextLinks是為后面做選中當前節點,能選中節點連線下的所有節點做數據准備。
大家如果感興趣,可以先不讀后面的,自己根據組裝出的數據自己實現下后面的交互。
4. 實現步驟2:構建圖表容器、實例,自定義布局、節點、連線、組的樣式等屬性。
- 容器
<div class="diagram" id="diagram"></div>
- 去除水印、畫布藍色邊框,參考前篇
- 構建圖表實例
import * as go from './go-module.js';
const $ = go.GraphObject.make;
const diagram = $(go.Diagram,
'diagram', // diagram 繪圖容器的 id
{
layout: $(go.TreeLayout, // 布局方式
{
angle: 90, // 自上而下,0 從左到右
arrangement: go.TreeLayout.ArrangementHorizontal
}
)
}
);
- 自定義節點、連線、組
const config = {
borderColor: '#d1d9e2',
groupTextColor: '#444',
nodeTextColor: '#585858',
linkColor: '#666',
selectedLinkColor: '#2090ff',
}
圖表顏色值統一管理。
定義節點
diagram.nodeTemplate = $(go.Node,
"Auto",
$(go.Shape, "Rectangle", // 節點形狀:矩形
{ stroke: config.borderColor, // 邊框顏色
strokeWidth: 1, // 邊框寬度
fill: "white", // 形狀填充顏色
},
),
$(go.TextBlock, // 節點文本
{ margin: 4,
stroke: config.nodeTextColor // 文本顏色
},
new go.Binding("text", "text"), // 將 model 中的 text 屬性進行綁定,用於節點顯示文本
),
{ doubleClick: nodeDblClick, // 節點雙擊事件,選中節點下的所有節點
},
);
定義邊
diagram.linkTemplate = $(go.Link,
{
curve: go.Link.Bezier // 貝塞爾曲線
},
// 連線
$(go.Shape, { name: 'link', strokeWidth: 1, stroke: config.linkColor }),
// 連線的箭頭
$(go.Shape, { name: 'linkArrow', toArrow: "OpenTriangle", stroke: config.linkColor })
);
定義組
diagram.groupTemplate = $(go.Group,
"Auto",
{ // 定義分組的內部布局
layout: $(go.TreeLayout,
{ angle: 90, arrangement: go.TreeLayout.ArrangementHorizontal }),
isSubGraphExpanded: false, // 默認展開true、折疊false
// 分組單擊事件
click: (e, group) => {
// todo 實現組選中,選中組中所有節點
}
},
$(go.Shape, // 定義分組形狀及描述
"Rectangle",
{
parameter1: 14,
fill: "rgba(2, 153, 255, .2)", // 填充色
stroke: config.borderColor, // 邊框色
strokeWidth: 1,
},
),
$(go.Panel, "Vertical",
{ defaultAlignment: go.Spot.Left, margin: 4 },
$(go.Panel, "Horizontal",
{ defaultAlignment: go.Spot.Top, margin: 4 },
$("SubGraphExpanderButton"), // 設置收縮按鈕,用於展開折疊子圖
$(go.TextBlock, // 定義文本
{
alignment: go.Spot.TopLeft,
font: "Bold 12px Sans-Serif",
stroke: config.groupTextColor,
},
new go.Binding("text"), // 將 model 中的 text 屬性進行綁定,用於節點顯示文本
)
),
// 創建占位符來表示組內容所在的區域
$(go.Placeholder, { padding: new go.Margin(5, 10) })
)
)
- 綁定數據
diagram.model = new go.GraphLinksModel(
[], // nodes
[] // links
)
5. 實現步驟3:交互處理
-
選中分組交互相對簡單,就不附上代碼了。
-
選中節點,選中節點連線下的所有節點
function nodeDblClick (e, node) {
// 遍歷每一條邊進行設置
let goneNodes = []; // 記錄遍歷過的,避免再次遍歷它
const forEdges = (edges, isSelected) => {
edges.forEach(edge => {
if (edge && edge.nextLinks) { // 當前節點下面有多個節點
edge.nextLinks.forEach((id, i) => {
if (!goneNodes.includes(id)) { // 避免遍歷過的
goneNodes.push(id);
const node = diagram.findNodeForKey(id);
node.isSelected = isSelected;
highlightLink(node, node.isSelected);
// 遞歸設置節點連線上下游的每一個節點選中及連線高亮,linkArr 為前面組裝出的圖的邊數據
forEdges(linkArr.filter(e => e.from === id), isSelected)
}
})
}
})
}
const {key: nodeId} = node.data;
// 存在多條邊,linkArr 為前面組裝出的圖的邊數據
const edges = linkArr.filter(e => e.from === nodeId);
// 先清除上次高亮的連線
clearHightLink();
// 高亮當前節點的連線
highlightLink(node, node.isSelected);
// 循環設置當前節點連線上下游的每一個節
點選中及連線高亮
forEdges(edges, node.isSelected);
}
優化:思路:先統計數據,再對統計數據進行UI處理。職責分明,增強可讀性。
function nodeDblClick (e, node) {
// 遍歷每一條邊
let allNodes = []; // 統計連線上的所有節點
const forEdges = (edges) => {
edges.forEach(edge => {
if (edge && edge.nextLinks) { // 當前節點下面有多個節點
edge.nextLinks.forEach((id, i) => {
allNodes.push(id);
// 遞歸節點連線上下游的每一個節點,linkArr 為前面組裝出的圖的邊數據
forEdges(linkArr.filter(e => e.from === id))
})
}
})
}
const {key: nodeId} = node.data;
// 存在多條邊,linkArr 為前面組裝出的圖的邊數據
const edges = linkArr.filter(e => e.from === nodeId);
// 循環統計當前節點連線上下游的每一個節點
forEdges(edges);
// 先清除上次高亮的連線
clearHightLink();
// 先統計出所有的,再去重,再對節點進行處理
// 設置統計出的連線上的所有節點及邊高亮
allNodes.push(nodeId);
allNodes = [...new Set(allNodes)]; // 去重
allNodes.forEach(id => {
const node = diagram.findNodeForKey(id);
node.isSelected = true;
highlightLink(node, true);
})
}
clearHightLink 方法:清除連線高亮
highlightLink 方法:根據node獲取到對應的id找出node的出去的線設置顏色等高亮。
最后
完成這個效果難點在哪,我自己的感受是:
- 組裝數據:如何組裝出圖表所需的數據,特別是選中節點要選中節點連線下的所有節點,怎么組裝數據才方便后面的處理。
- 在自定義布局、節點、連線、組屬性及樣式上,特別是細節處理,需要大量翻看文檔指南或api,查看案例是等,確定哪個屬性的哪個值改了才是需要的。
- 在做交互時,需要理清思路,看文檔事件相關的部分。特別難的是,節點信息打印出來查看時,看不到具體的,只能看出來是 迭代器,可以遍歷,但看不出具體的數據,只能通過相應 api 才能得到。
關於本文的代碼,只放了核心部分的。
最后的最后,有不到位的地方或者錯誤的地方,亦或是更好的意見,歡迎指出。
非常感謝!!!