透過一段時間的使用和學習,對G6有了更一步的經驗,這篇博文主要從以下幾個小功能著手介紹,文章最後會給出完整的demo程式碼。
- 1. 樹圖的基本佈局和使用
- 2. 根據返回資料的屬性不同,定製不一樣的節點樣式
- 3. 節點 label 文案顯示過長時,透過截斷的方式,顯示...
- 4. 當一個父節點包含children葉子節點時,label後顯示children的長度,格式為:node.label(children.length)
- 5. 截斷後的label ,透過滑鼠懸浮,完全顯示在 tooltip 中,定義並改寫 tooltip 樣式
- 6. 根據節點展開收起狀態 動態變更 label 顯示位置,展開時在上,收起時在右
- 7. 設定節點的icon樣式和背景色(隨機色可自行定製)
- 8. 葉子節點超過 (xxx)條, 摺疊葉子節點,顯示展開更多
- 9. 點選展開更多節點,渲染被摺疊的葉子節點
- 10. 定義滑鼠點選事件,聚焦當前點選節點至畫布中心點
- 11. 定義滑鼠移入移出事件
- 12. 根據返回資料的屬性不同,定製不一樣的 邊 樣式
- 13. 設定連線上關係文案樣式
- 14. 解決畫布拖拽,出現黑色殘影問題
- 15. Demo 動圖演示
- 16. 完整Demo案例
1. 樹圖的基本佈局和使用
樹圖的佈局,使用的模板是官網所提供的 緊湊樹
模板,在此基礎上,進行一些定製化的改造,官網緊湊樹案例
屬性說明:
graph = new G6.TreeGraph({
container,
width: document.documentElement.clientWidth,
height:document.documentElement.clientHeight,
// .....
layout: {
type: 'compactBox', // 佈局型別
direction: 'LR', // 樹圖佈局方向, 從左向右
getHeight: function getHeight() { // 高度
return 16
},
getWidth: function getWidth() { // 寬度
return 16
},
getVGap: function getVGap() { // 節點之間 垂直間距
return 25
},
getHGap: function getHGap() { // 節點之間 水平間距
return 150
}
}
})
2. 根據返回資料的屬性不同,定製不一樣的節點樣式
大部分在實際專案中,資料都是由後端返回,可能會存在多種型別的資料,需要進行不同的處理和展示,那麼此時只在 graph 初始化時,定義defaultNode顯然是不夠用的,G6支援動態改變節點樣式。需要在 graph 例項化之後:
// 以下函式均在下方有實現程式碼:
graph.node((node)=> {
return {
label: node.label || formatLabel(node),
icon: formatIcon(node),
size: node.size || 40,
labelCfg: { position: setLabelPos(node) }, // label 顯示位置
style: {
fill: getNodeColor(),
stroke: getNodeColor()
}
}
})
3. 節點 label 文案顯示過長時,透過截斷的方式,顯示...
首先判斷當前節點有無子節點,後續會進行label的拼接,顯示葉子節點的數量,在進行結束,此書由於資料的原因,案例中資料label
和id
使用同一個欄位,後續同學們可以自己按實際情況進行調整:我擷取的長度是 15
// label 過長截斷 顯示...
const formatLabel = (node) => {
const hasChildren = node.childrenBak?.length || node.children?.length
const ellipsis = node.id.length > 15 ? '...' : ''
return `${node.id.slice(0, 15)}${ellipsis}${hasChildren ? ' (' + hasChildren + ')' : ''}`
}
4. 當一個父節點包含children葉子節點時,label後顯示children的長度,格式為:node.label(children.length)
這個小功能,與上一個label 截斷,統一都是處理label的,因此放在同一個函式中返回,其主要實現程式碼為 這一行:
return `${node.id.slice(0, 15)}${ellipsis}${hasChildren ? ' (' + hasChildren + ')' : ''}`
5. 截斷後的label ,透過滑鼠懸浮,完全顯示在 tooltip 中,定義並改寫 tooltip 樣式
這個小功能設計的改動比較多,同學們在看的時候,不要看錯了哈
第一步:首先定義一個函式,返回tooltip,官網的案例中並不是返回的函式,而是直接返回了一個物件,這個在實際使用過程中會存在問題,就是新增的資料無法使用到這個外掛,因此透過函式呼叫的方式 ,可以解決該現象:
// label 顯示... 時,顯示提示 tip
const treeTooltip = ()=> {
return new G6.Tooltip({
offsetX: 10, // 滑鼠偏移量
offsetY: 20,
shouldBegin(e: any) {
return e.item?.get('model')?.label?.includes('...') // label中有...才顯示,表示被截斷
},
getContent(e: any) {
let outDiv = document.createElement('div') // 設定tip容器和樣式
outDiv.innerHTML = `
<p style="max-width:600px;word-wrap:break-word;border-radius:5px;font-size:15px;color:#fff;background:#333;padding:10px">
${e.item.getModel().id}
</p>`
return outDiv
},
itemTypes: ['node'] // 表示觸發的元素型別
})
}
第二步:在new 例項化的options中新增 外掛使用
graph = new G6.TreeGraph({
container,
width: document.documentElement.clientWidth,
height:document.documentElement.clientHeight,
plugins: [treeTooltip()],
// .....
})
第三步:完整以上程式碼後,基本可以看出tip
的提示框,但是由於是改寫原有的樣式,因此還需要 改一下 style, 由於畫布操作較多,因此canvas畫布,修改滑鼠樣式,全文只有這裡提到了style修改。就寫在一起了,實際並不影響 tooltip 功能
<style scoped>
#container >>> .g6-component-tooltip {
background:#333;
color:#fff;
padding: 0 8px;
}
canvas {
cursor: pointer !important;
}
</style>
6. 根據節點展開收起狀態 動態變更 label 顯示位置,展開時在上,收起時在右
此功能的設計是為了最佳化,當節點label過長並展開時,父子之間水平間距不夠時會出現文案互相重疊等問題,做了一個小最佳化,
首先判斷節點是否是展開狀態,以及是否有葉子節點,節點有children並展開時在上,其餘情況都顯示在右邊,這是初始化時的程式碼:
// 根據節點展開收起狀態 動態變更 label 顯示位置,展開時在上,收起時在右
const setLabelPos = (node) => {
return !node.collapsed && node.children?.length ? 'top' : 'right'
}
因為樹圖可以監聽節點的展開收起狀態,因此在切換的時候,也需要進行 label定位的問題:在new 例項化的options中新增 modes
graph = new G6.TreeGraph({
container,
width: document.documentElement.clientWidth,
height:document.documentElement.clientHeight,
plugins: [treeTooltip()],
// .....
modes: {
default: [
{
type: 'collapse-expand',
onChange: function onChange(item: any, collapsed) {
const data = item?.get('model')
data.collapsed = collapsed
const model = {
id: data.id,
labelCfg: { position: !collapsed ? 'top' : 'right' }
}
item.update(model)
item.refresh()
return true
}
},
'drag-canvas',
'zoom-canvas'
]
}
})
7. 設定節點的icon樣式和背景色(隨機色可自行定製)
廢話不多話,處理節點的icon圖表,圖表可以使用圖片,也可以使用文字,此處就用文字了,擷取的label前兩個字串,並設定背景顏色(隨機色可自行定製)
// 葉子節點 圖示處理 擷取ID 前兩個字串
const formatIcon = (node) => {
node.icon = {
text: node.id.slice(0,2),
fill: '#fff',
stroke: '#fff',
textBaseline: 'middle',
fontSize: 20,
width: 25,
height: 25,
show: true
}
}
// 葉子節點 背景顏色隨機填充
const getNodeColor = () => {
const colors = ["#8470FF", "#A020F0", "#C0FF3E", "#FF4500", "#66d6d1"];
return colors[Math.floor(Math.random() * colors.length)];
}
8. 葉子節點超過 (xxx)條, 摺疊葉子節點,顯示展開更多
當葉子節點很多時,並不想全部展開,而是先只展示一部分,其他節點摺疊在 展開按鈕中,實現思路,定義一個屬性,接受原有全部children, 然後進行擷取,在push一個 展開按鈕,實現:
第一步:定義一個childrenBak
屬性接受 children
資料
// 子還有子,因此需要遞迴
const splitChild = (node) => {
node.childrenBak = node.children ? [...node.children] : []
let result: any = []
if(node.children){
result = node.children.slice(0, 5)
if (node.children.length > 5) {
result.push({ id: `Expand-${node.id}`, label: ' 展開更多...' })
}
node.children = result
node.children.forEach(child =>{
splitChild(child)
})
}
}
9. 點選展開更多節點,渲染被摺疊的葉子節點
第二步:(接功能小8繼續)點選展開更多時,顯示被摺疊的剩餘節點,,定義node:click
事件
思路:找到展開更多 , 找到展開更多節點的 父節點, 更新父節點的 children
graph.on('node:click', (evt) => {
const { item } = evt
const node = item?.get('model')
if (node.id.includes('Expand')) { // id 中包含Expand 表示是展開更多
const parentNode = graph.getNeighbors(item, 'source')[0].get('model') // 找到展開更多節點的 父節點
graph.updateChildren(parentNode.childrenBak, parentNode.id) // 使用上一步宣告的childrenBak 更新父節點的 children
}
})
10. 定義滑鼠點選事件,聚焦當前點選節點至畫布中心點
小9 說了點選事件,那麼點選事件中,還有一個小的最佳化點,就是將當前點選的節點,移動至畫布中心,並賦予高亮選中樣式
const animateCfg = { duration: 200, easing: 'easeCubic' }
graph.on('node:click', (evt) => {
const { item } = evt
const node = item?.get('model')
// if (node.id.includes('Expand')) { // 功能點 9 程式碼
// const parentNode = graph.getNeighbors(item, 'source')[0].get('model')
// console.log(parentNode,parentNode.childrenBak);
// graph.updateChildren(parentNode.childrenBak, parentNode.id)
// }
setTimeout(() => {
if (!node.id.includes('Expand')) {
graph.focusItem(item, true, animateCfg)
graph.getNodes().forEach((node) => {
graph.clearItemStates(node) // 先清空其他節點的 高亮樣式
})
graph.setItemState(item, 'selected', true) // selected 需要在例項化處進行定義
}
}, 500)
})
selected
表示的是 nodeStateStyles
,也就是節點狀態樣式
graph = new G6.TreeGraph({
container,
width: document.documentElement.clientWidth,
height:document.documentElement.clientHeight,
plugins: [treeTooltip()],
// .....
nodeStateStyles: {
active: { // 這個用在了 滑鼠懸浮,可以自行定義
fill: 'l(0) 0:#FF4500 1:#32CD32',
stroke: 'l(0) 0:#FF4500 1:#32CD32',
lineWidth: 5
},
selected: { // 這個用在了 滑鼠選中,可以自行定義
fill: 'l(0) 0:#FF4500 1:#32CD32',
stroke: 'l(0) 0:#FF4500 1:#32CD32',
lineWidth: 5
}
}
})
11. 定義滑鼠移入移出事件
graph.on('node:mouseenter', (evt) => {
const { item } = evt
graph.setItemState(item, 'active', true) // active 與 selected 都是節點狀態樣式
})
graph.on('node:mouseleave', (evt) => {
const { item } = evt
graph.setItemState(item, 'active', false)
})
12. 根據返回資料的屬性不同,定製不一樣的 邊 樣式
關於節點的差不多介紹完了,關於連線,內容就比較少了,動態定義連線樣式,及連線上的文字樣式:
可以根據 link的不同屬性自定義 連線顏色和label顏色,因為是測試資料,因此就用一個自增長的數判斷奇偶性來進行區分,以便明白其中定製化的方法
let selfGrowthNum = 0
graph.edge((edge)=> {
// let {source, target } = edge // 解構連線的 起始節點
selfGrowthNum++
return {
style: {
opacity: 0.5,
stroke: selfGrowthNum % 2 ? '#ADD8E6' : "#FFDEAD",
lineWidth: 2
},
labelCfg: {
position: 'end',
style: {
fontSize: 16,
fill: selfGrowthNum % 2 ? '#ADD8E6' : "#FFDEAD",
}
},
label: selfGrowthNum % 2 ? 'even' : "odd"
}
})
13. 設定連線上關係文案樣式
上述程式碼基本完成了 連線的樣式和文案的樣式,但此時,線是貫穿文字的,看著比較亂,因此還需要修改連線樣式 defaultEdge
graph = new G6.TreeGraph({
container,
width: document.documentElement.clientWidth,
height:document.documentElement.clientHeight,
// .....
defaultEdge: {
type: 'cubic-horizontal',
style: { // 如果不定製化,這個就是預設樣式
opacity: 0.5,
stroke: '#ccc',
lineWidth: 2
},
labelCfg: {
position: 'end', // 文字顯示線上段的哪個位置,
refX: -15,
style: {
fontSize: 16,
background: {
fill: '#ffffff', // 給文字新增背景色,解決文字被橫穿的問題
padding: [2, 2, 2, 2]
}
}
}
}
})
14. 解決畫布拖拽,出現黑色殘影問題
G6 4.x 依賴的渲染引擎 @antv/g@4.x 版本支援了區域性渲染,帶了效能提升的同時,也帶來了圖形更新時可能存在渲染殘影的問題。比如拖拽節點時,節點的文字會留下軌跡。由於目前 @antv/g 正在進行大版本的升級(到 5.x),可能不考慮在 4.x 徹底修復這個問題。當我們遇到這個問題的時候,可以透過關閉區域性渲染
的方法解決,但是這樣可能導致效能有所降低。
graph.get('canvas').set('localRefresh', false)。
15. Demo 動圖演示
16. 完整Demo案例
<template>
<div id="container"></div>
</template>
<script lang="ts" setup>
import G6 from "@antv/g6";
import { onMounted } from "vue";
let graph: any = null;
// 樹圖初始資料
const treeData = {
id: "Modeling Methods",
color: "",
children: [
{
id: "Classification",
children: [
{ id: "Logistic regression" },
{ id: "Linear discriminant analysis" },
{ id: "Rules" },
{ id: "Decision trees" },
{ id: "Naive Bayes" },
{ id: "Knearest neighbor" },
{ id: "Probabilistic neural network" },
{ id: "Support vector machine" },
],
},
{
id: "Methods",
children: [
{ id: "Classifier selection" },
{ id: "Models diversity" },
{ id: "Classifier fusion" },
],
},
],
};
onMounted(() => {
splitChild(treeData);
drawTreeGraph();
});
function drawTreeGraph() {
if (graph) graph.destroy();
const container = document.getElementById("container") as HTMLElement;
graph = new G6.TreeGraph({
container,
width: document.documentElement.clientWidth - 300,
height: document.documentElement.clientHeight,
fitView: false,
fitViewPadding: [10, 50, 10, 50],
animate: true,
plugins: [treeTooltip()],
defaultNode: {
type: "circle",
size: 40,
collapsed: false,
style: {
fill: "#fff",
lineWidth: 2,
cursor: "pointer",
},
labelCfg: {
position: "right",
offset: 10,
style: {
fill: "#333",
fontSize: 20,
stroke: "#fff",
background: {
fill: "#ffffff",
padding: [2, 2, 2, 2],
},
},
},
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
icon: {
show: true,
width: 25,
height: 25,
},
},
defaultEdge: {
type: "cubic-horizontal",
labelCfg: {
position: "end",
refX: -15,
style: {
fontSize: 16,
background: {
fill: "#ffffff",
padding: [2, 2, 2, 2],
},
},
},
},
modes: {
default: [
{
type: "collapse-expand",
onChange: function onChange(item: any, collapsed) {
const data = item?.get("model");
data.collapsed = collapsed;
const model = {
id: data.id,
labelCfg: { position: !collapsed ? "top" : "right" },
};
item.update(model);
item.refresh();
return true;
},
},
"drag-canvas",
"zoom-canvas",
],
},
layout: {
type: "compactBox",
direction: "LR",
getHeight: function getHeight() {
return 30;
},
getWidth: function getWidth() {
return 16;
},
getVGap: function getVGap() {
return 30;
},
getHGap: function getHGap() {
return 150;
},
},
nodeStateStyles: {
active: {
fill: "l(0) 0:#FF4500 1:#32CD32",
stroke: "l(0) 0:#FF4500 1:#32CD32",
lineWidth: 5,
},
selected: {
fill: "l(0) 0:#FF4500 1:#32CD32",
stroke: "l(0) 0:#FF4500 1:#32CD32",
lineWidth: 5,
},
},
});
graph.node((node: { label: any; size: any }) => {
return {
label: node.label || formatLabel(node),
icon: formatIcon(node),
size: node.size || 40,
labelCfg: { position: setLabelPos(node) },
style: {
fill: getNodeColor(),
stroke: getNodeColor(),
},
};
});
let selfGrowthNum = 0;
graph.edge((edge: any) => {
// let {source, target } = edge // 也可以根據 link的屬性不同自定義 連線顏色和label顏色,因為是測試資料,因此就用一個自增長的數判斷奇偶性來進行區分,以便明白其中定製化的方法
selfGrowthNum++;
return {
style: {
opacity: 0.5,
stroke: selfGrowthNum % 2 ? "#ADD8E6" : "#FFDEAD",
lineWidth: 2,
},
labelCfg: {
position: "end",
style: {
fontSize: 16,
fill: selfGrowthNum % 2 ? "#ADD8E6" : "#FFDEAD",
},
},
label: selfGrowthNum % 2 ? "even" : "odd",
};
});
graph.on("node:mouseenter", (evt: { item: any }) => {
const { item } = evt;
graph.setItemState(item, "active", true);
});
graph.on("node:mouseleave", (evt: { item: any }) => {
const { item } = evt;
graph.setItemState(item, "active", false);
});
const animateCfg = { duration: 200, easing: "easeCubic" };
graph.on("node:click", (evt: { item: any }) => {
const { item } = evt;
const node = item?.get("model");
if (node.id.includes("expand")) {
const parentNode = graph.getNeighbors(item, "source")[0].get("model");
console.log(parentNode, parentNode.childrenBak);
graph.updateChildren(parentNode.childrenBak, parentNode.id);
}
setTimeout(() => {
if (!node.id.includes("expand")) {
graph.focusItem(item, true, animateCfg);
graph.getNodes().forEach((node: any) => {
graph.clearItemStates(node);
});
graph.setItemState(item, "selected", true);
}
}, 500);
});
graph.on("canvas:click", () => {
graph.getNodes().forEach((node: any) => {
graph.clearItemStates(node);
});
});
graph.data(treeData);
graph.render();
graph.zoom(0.9);
graph.fitCenter();
graph.get("canvas").set("localRefresh", false);
}
// label 過長截斷 顯示...
const formatLabel = (node) => {
const hasChildren = node.childrenBak?.length || node.children?.length;
const ellipsis = node.id.length > 15 ? "..." : "";
return `${node.id.slice(0, 15)}${ellipsis}${
hasChildren ? " (" + hasChildren + ")" : ""
}`;
};
// 葉子節點 圖示處理 擷取ID 前兩個字串
const formatIcon = (node) => {
node.icon = {
text: node.id.slice(0, 2),
fill: "#fff",
stroke: "#fff",
textBaseline: "middle",
fontSize: 20,
width: 25,
height: 25,
show: true,
};
};
// 葉子節點 背景顏色隨機填充
const getNodeColor = () => {
const colors = ["#8470FF", "#A020F0", "#C0FF3E", "#FF4500", "#66d6d1"];
return colors[Math.floor(Math.random() * colors.length)];
};
// 根據節點展開收起狀態 動態變更 label 顯示位置,展開時在上,收起時在右
const setLabelPos = (node: { collapsed: any; children: string | any[] }) => {
return !node.collapsed && node.children?.length ? "top" : "right";
};
// label 顯示... 時,顯示提示 tip
const treeTooltip = () => {
return new G6.Tooltip({
offsetX: 10,
offsetY: 20,
shouldBegin(e: any) {
return e.item?.get("model")?.label?.includes("...");
},
getContent(e: any) {
let outDiv = document.createElement("p");
outDiv.innerHTML = ` ${e.item.getModel().id} `;
return outDiv;
},
itemTypes: ["node"],
});
};
// 葉子節點超過 5(xxx)條, 摺疊葉子節點,顯示展開更多
const splitChild = (node: any) => {
node.childrenBak = node.children ? [...node.children] : [];
let result: any = [];
if (node.children) {
result = node.children.slice(0, 5);
if (node.children.length > 5) {
result.push({ id: `expand-${node.id}`, label: " 展開更多..." });
}
node.children = result;
node.children.forEach((child: any) => {
splitChild(child);
});
}
};
</script>
<style scoped>
#container >>> .g6-component-tooltip {
background:#333;
color:#fff;
padding: 0 8px;
}
canvas {
cursor: pointer !important;
}
</style>