- 1. 設定分組模板,預設樣式,統一最小寬度,展開收起狀態監聽
- 2. 分組名稱顯示成員個數: 分組名稱+成員個數: name(children)
- 3. 分組成員為空時,不顯示 placeholder 佔位留白
- 4. 分組第一次展開請求獲取成員介面,監聽展開收起狀態 subGraphExpandedChanged:fn
- 5. 設定節點模板
- 6. 設定節點的 icon,此處以文字為例
- 7. 文案太長擷取顯示...,滑鼠懸浮顯示全部
- 8. 設定滑鼠移入、移出狀態,點選節點高亮相關聯節點
- 9. 動態追加節點
- 10. 節點眾多需要不同表現樣式時,可定義不同的節點模板
- 11. 每個節點前後追加 input/output spot 的兩種方式
- 12. 設定連線模板,箭頭樣式,連線上新增關係文字
- 13. 動態追加連線 addLinkData()
- 14. 點選節點使其相關聯節點與連線及文字高亮顯示
- 15. 點選畫布清空當前高亮狀態元素
- 16. 刪除連線的方法
- 17. 動態根據需求 展開和收起某一個或全部的分組
- 18. 關閉畫布重新渲染時的動畫
- 19. 畫布無限拖拽功能
- 20. 圖禁止複製,禁止刪除,開啟滑鼠滾輪縮放,toolTip 觸發 hoverDelay 初始定位
- 21. 消除水印
- 更多分享 詳見下一篇博文
視覺化的庫非常多,如:echarts、highcharts、antv 系列、d3、gojs.....
按照可自定義繪圖的程度排序: gojs、d3js > antv > echarts 、highcharts
如果需求簡單,不需要自定義圖元素,那麼 echarts 、highcharts 看中哪個 demo 效果就選用哪個庫。
如果有一定程度需要自定義圖元素,那麼可以看 antv g2/g6 demo 是否能滿足需求,可自定義大部分圖元素。
如果上面的都不能解決你的需求,那麼就是高可定製的,可以考慮 d3js、gojs,還是先去看 demo,看哪個更接近你的需求就採用哪個。
gojs 是一個用於構建互動式視覺化圖的 js 庫,使用可自定義的模板和佈局構建複雜節點、連結和組,從而構建出簡單到複雜的各類圖,如流程圖、腦圖、組織圖、甘特圖等。而且提供了許多用於使用者互動的高階功能,例如拖放、複製和貼上、就地文字編輯......
本文是關於如何使用視覺化庫 gojs 的介紹及使用時的小技巧。gojs 的高可自定義性,非常適合需求複雜的圖互動。
繪製基本流程簡單介紹,
- 引入庫,可以下載,也可以引入 cdn 地址
- html 檔案建立畫布容器,並設定寬高
- 建立例項,定義佈局,樣式,互動,屬性,事件等
- 繫結節點和連線資料,渲染圖表
先繪製出基本的例項,讓後續的學習,有個大致的輪廓
<template
>>
<div>
<div id="myDiagramDiv" style="height: 1000px;"></div>
</div>
</template>
<script lang="ts" setup>
import go from "@/assets/js/go";
import { onMounted } from "vue";
let diagram: any = null;
const $ = go.GraphObject.make;
onMounted(() => {
init();
});
function init() {
// 建立diagram例項,
diagram = $(go.Diagram, "myDiagramDiv");
// 分組模板
diagram.groupTemplate = $(go.Group, "Auto", {
/* options 後期主要學習部分 */
});
// 連線模板
diagram.linkTemplate = $(go.Link, {
/* options 後期主要學習部分 */
});
// 節點模板
diagram.nodeTemplate = $(go.Node, "Auto", {
/* options 後期主要學習部分 */
});
// 繪製節點模板 追加新的 自定義的模板型別
diagram.nodeTemplateMap.add();
diagram.layout = $(go.LayeredDigraphLayout, {
direction: 0, // 佈局方向,0 水平 90 垂直
layerSpacing: 120, // 節點間隔
isOngoing: false,
});
var nodeDataArray = []; // 節點集合
var linkDataArray = []; // 分組集合
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
}
</script>
1. 設定分組模板,預設樣式,統一最小寬度,展開收起狀態監聽
// 分組模板
diagram.groupTemplate = $(
go.Group,
"Auto",
{
layout: $(go.LayeredDigraphLayout, { direction: 0, columnSpacing: 5 }),
isSubGraphExpanded: false, // 預設展開 true 、摺疊false.
subGraphExpandedChanged: function (group) {}, // 功能 小 ## 4
},
$(
go.Shape,
"RoundedRectangle", // 分組形狀,圓角矩形
{ parameter1: 5, opacity: 0.7, minSize: new go.Size(120, NaN) }, // 圓角 透明度 統一最小寬度
new go.Binding("fill", "color"), // 繫結填充色,如果是固定顏色,可以直接在上述物件中,填寫對應的屬性,如 fill:"#ccc"
new go.Binding("stroke", "color") // 繫結描邊色,同上
),
$(
go.Panel,
"Vertical",
{ defaultAlignment: go.Spot.Left, margin: 4 },
$(
go.Panel,
"Horizontal",
{ defaultAlignment: go.Spot.Top, margin: 4, padding: new go.Margin(5, 5, 5, 2) },
// 設定收縮按鈕,用於展開摺疊子圖 +/-
$("SubGraphExpanderButton", { padding: new go.Margin(0, 5, 0, 0) }),
$(
go.TextBlock,
{ font: "bold 12px Sans-Serif", stroke: "white" },
new go.Binding("text", "", (node) => node.key + `(${node.children})`) // 分組名稱+成員個數: name(children)
)
),
// 分組展開後的 皮膚佔位
$(
go.Placeholder,
{ background: "white" },
new go.Binding("padding", "", function (node) {
// 每組背景色和邊距
return node.children ? new go.Margin(10, 10) : new go.Margin(0, 10);
})
)
)
);
2. 分組名稱顯示成員個數: 分組名稱+成員個數: name(children)
$(
go.TextBlock,
{ font: "bold 12px Sans-Serif", stroke: "white" },
new go.Binding("text", "", (node) => node.key + `(${node.children})`) // 分組名稱+成員個數: name(children)
);
3. 分組成員為空時,不顯示 placeholder 佔位留白
// 分組展開後的 皮膚佔位
$(
go.Placeholder,
{ background: "white" },
new go.Binding("padding", "", function (node) {
// 每組背景色和邊距
return node.children ? new go.Margin(10, 10) : new go.Margin(0, 10);
})
);
4. 分組第一次展開請求獲取成員介面,監聽展開收起狀態 subGraphExpandedChanged:fn
subGraphExpandedChanged: function (group) {
// 子圖展開或收起的狀態 group.isSubGraphExpanded
var groupData = group.part.data; // 獲取分組 資料
if (!groupData.isRequested) {
// 設定一個標識位,標明該分組資料是否 有請求過介面,
// 未請求過,可以編寫請求介面程式碼,或者新增已知節點程式碼,
groupData.isRequested = true;
diagram.model.addNodeData({
key: "任意要顯示的node節點名稱",
group: groupData.key,
color: groupData.color,
icon: groupData.icon,
});
// diagram.model.addLinkData({ from: "", to: "" }) // 新增節點 有連線關係則新增連線物件
diagram.animationManager.stopAnimation(); // 取消動畫
}
// 請求過 就直接 展開或收起分組 isOngoing 屬性true會自動佈局,但是會影響使用者拖拽效果,因此分組自行佈局後,需要在改為false
diagram.layout.isOngoing = true;
setTimeout(() => {
diagram.layout.isOngoing = false;
});
}
5. 設定節點模板
// 節點模板
diagram.nodeTemplate = $(
go.Node,
"Auto",
{
mouseEnter: mouseEnter,
mouseLeave: mouseLeave,
click: nodeclick,
},
$(
go.Shape,
"Rectangle",
{ strokeWidth: 1, stroke: "white", fill: "white" },
new go.Binding("stroke", "isHighlighted", (sel) => (sel ? "#1E90FF" : "white")).ofObject(), // 滑鼠選中高亮樣式
new go.Binding("strokeWidth", "isHighlighted", (sel) => (sel ? 3 : 1)).ofObject()
),
$(
go.Panel,
"Horizontal",
{ width: 280, padding: new go.Margin(5, 5, 5, 2) },
$(
go.TextBlock, // 設定 icon
{ font: "bold 12px Sans-Serif", stroke: "white", width: 24, textAlign: "center" },
new go.Binding("text", "icon"), // 繫結icon 圖表文案
new go.Binding("background", "color") // 繫結 背景色
),
$(
go.TextBlock,
{
margin: 5,
width: 240,
font: "15px Verdana",
stroke: "#444",
maxSize: new go.Size(260, NaN),
maxLines: 1,
overflow: go.TextBlock.OverflowEllipsis, // maxSize maxLines overflow 屬性聯合使用,用於文案截斷 顯示...
name: "TEXT", // 命名隨意,用於後期 滑鼠狀態事件,節點成員的獲取
},
new go.Binding("text", "key")
),
{
toolTip: $(
// 滑鼠懸浮顯示全部文案 ,預設觸發時間比較長,可以透過屬性來修改
"ToolTip",
$(go.TextBlock, { margin: 4 }, new go.Binding("text", "key"))
),
}
)
);
6. 設定節點的 icon,此處以文字為例
$(
go.TextBlock, // 設定 icon
{ font: "bold 12px Sans-Serif", stroke: "white", width: 24, textAlign: "center" },
new go.Binding("text", "icon"), // 繫結icon 圖表文案
new go.Binding("background", "color") // 繫結 背景色
),
7. 文案太長擷取顯示...,滑鼠懸浮顯示全部
{
toolTip: $(
// 滑鼠懸浮顯示全部文案 ,預設觸發時間比較長,可以透過屬性來修改
"ToolTip",
$(go.TextBlock, { margin: 4 }, new go.Binding("text", "key"))
);
}
8. 設定滑鼠移入、移出狀態,點選節點高亮相關聯節點
function mouseEnter(e, obj) {
var text = obj.findObject("TEXT");
text.stroke = "#1E90FF";
}
function mouseLeave(e, obj) {
var text = obj.findObject("TEXT");
text.stroke = "#444";
}
function nodeclick(e, node) {
var diagram = node.diagram;
diagram.clearHighlighteds();
node.findNodesConnected().each(function (l) {
l.isHighlighted = true;
});
node.linksConnected.each(function (n) {
n.isHighlighted = true;
});
}
9. 動態追加節點
diagram.model.addNodeData({
key: "任意要顯示的node節點名稱",
group: "分組名",
color: "節點背景顏色",
icon: "icon文字",
});
10. 節點眾多需要不同表現樣式時,可定義不同的節點模板
- 僅有一種不同形式時,可使用
diagram.nodeTemplateMap.add(node)
呼叫不同的節點模板 - 多種不同模板時,是封裝一個方法,生成模板,在呼叫
diagram.nodeTemplateMap.set(typename, node)
11. 每個節點前後追加 input/output spot 的兩種方式
方法一: 該方法需要重點關注的方法是 makePort
,函式呼叫位置及返回值
var nodeDataArray = [{ key: "A", category: "Start", text: "節點設定左右連線點" }];
var linkDataArray = [
{ from: "A", to: "B", frompid: "1", topid: "1" }, // createPort方法 portId
{ from: "B", to: "C", frompid: "2", topid: "2" },
];
diagram.model.linkFromPortIdProperty = "frompid"; // 連線點對應名稱
diagram.model.linkToPortIdProperty = "topid";
diagram.nodeTemplateMap.add(
"Start", // nodeDataArray 中的 category
$(
go.Node,
"Auto",
{ width: 260, height: 80 },
$(go.Shape, "Rectangle", { fill: "white", stroke: "white", strokeWidth: 1 }),
$(
go.Panel,
"Vertical",
{ padding: new go.Margin(5, 5, 5, 2) },
$(
go.TextBlock,
"節點設定左右連線點", // 1. 節點文字也可以直接寫在這
{ font: "18px Sans-Serif", stroke: "#444", textAlign: "center" },
new go.Binding("text") // 2.文字也可以透過繫結 nodeDataArray 中的 text, 或者其他任意欄位
)
),
$(
go.Panel,
"Vertical",
{
alignment: go.Spot.Left,
alignmentFocus: new go.Spot(0, 0.5, 8, 0),
},
makePort(2, 3).inSpotList // 需要返回一個陣列,表示 2個 入邊連線點
),
$(
go.Panel,
"Vertical",
{
alignment: go.Spot.Right,
alignmentFocus: new go.Spot(1, 0.5, -8, 0),
},
makePort(2, 3).outSpotList // 需要返回一個陣列,表示 3個 出邊連線點
)
)
);
function makePort(inCount, outCount) {
let inSpot = inCount;
let outSpot = outCount;
let inSpotList: any = [];
let outSpotList: any = [];
for (let i = 1; i <= inSpot; i++) {
inSpotList.push(createPort(i, "Left"));
}
for (let i = 1; i <= outSpot; i++) {
outSpotList.push(createPort(i, "Right"));
}
function createPort(portId, pos) {
var port = $(go.Shape, "Rectangle", {
fill: "gray",
stroke: null,
desiredSize: new go.Size(8, 8),
portId: String(portId), // 該屬性比較重要,用於給每一個連線點 命名,
toMaxLinks: 3,
cursor: "pointer",
});
var panel = $(go.Panel, "Horizontal", { margin: new go.Margin(2, 0) });
port.fromSpot = go.Spot[pos];
port.fromLinkable = true;
panel.alignment = go.Spot["Top" + pos];
panel.add(port);
return panel;
}
return { inSpotList, outSpotList };
}
方法二: 該方法需要重點關注的方法itemArray
,在資料中分別定義了 leftArray
和rightArray
,用於迴圈顯示子元素
diagram.nodeTemplate = $(
go.Node,
"Table",
$(
go.Panel,
"Horizontal",
{ row: 1, column: 2 },
$(
go.TextBlock, // 資產名稱
{
margin: 5,
width: 240,
font: "15px Verdana",
stroke: "#444",
}
)
),
$(go.Panel, "Vertical", new go.Binding("itemArray", "leftArray"), {
// 節點 左側 入邊連線點 迴圈顯示
row: 1,
column: 0,
itemTemplate: $(
go.Panel,
{
_side: "left",
fromSpot: go.Spot.Left,
toSpot: go.Spot.Left,
cursor: "pointer",
},
new go.Binding("portId", "portId"),
$(
go.Shape,
"Rectangle",
{
stroke: null,
strokeWidth: 1,
desiredSize: new go.Size(8, 8),
margin: new go.Margin(1, 5, 1, 0),
},
new go.Binding("fill", "portColor")
)
),
}),
$(go.Panel, "Vertical", new go.Binding("itemArray", "rightArray"), {
// 節點 右側 出邊連線點 迴圈顯示
row: 1,
column: 3,
itemTemplate: $(
go.Panel,
{
_side: "right",
fromSpot: go.Spot.Right,
toSpot: go.Spot.Right,
cursor: "pointer",
},
new go.Binding("portId", "portId"),
$(
go.Shape,
"Rectangle",
{
stroke: null,
strokeWidth: 0,
desiredSize: new go.Size(8, 8),
margin: new go.Margin(1, 0),
},
new go.Binding("fill", "portColor")
)
),
})
);
var nodeDataArray = [
{ key: "A", rightArray: [{ portColor: "#33B12C", portId: "left0" }], rightArray: [{ portColor: "#33B12C", portId: "right0" }] },
{ key: "B", rightArray: [{ portColor: "#F29941", portId: "left0" }], rightArray: [{ portColor: "#F29941", portId: "right0" }] },
{ key: "C", rightArray: [{ portColor: "#11C67B", portId: "left0" }], rightArray: [{ portColor: "#11C67B", portId: "right0" }] },
];
var linkDataArray = [
{ from: "A", to: "B", frompid: "right0", topid: "left0" },
{ from: "B", to: "C", frompid: "right0", topid: "left0" },
];
diagram.model.linkFromPortIdProperty = "frompid"; // 連線點對應名稱
diagram.model.linkToPortIdProperty = "topid";
12. 設定連線模板,箭頭樣式,連線上新增關係文字
// 連線模板
diagram.linkTemplate = $(
go.Link,
{
routing: go.Link.Orthogonal,
corner: 25,
relinkableFrom: true,
relinkableTo: true,
},
$(go.Shape, { isPanelMain: true, stroke: "transparent" }),
$(go.Shape, { isPanelMain: true, stroke: "#ccc", strokeWidth: 2 }, new go.Binding("stroke", "color"), new go.Binding("strokeWidth", "strokeWidth")),
$(
go.Shape,
{ toArrow: "standard", strokeWidth: 1, fill: "#ccc" }, // 箭頭
new go.Binding("stroke", "color"),
new go.Binding("fill", "color")
),
$(
go.Panel,
"Auto", // 連線上的文字
$(go.Shape, { fill: "white", stroke: "white" }),
$(go.TextBlock, { stroke: "#ff6600", visible: false }, new go.Binding("text", "linkText"), new go.Binding("visible", "linkText", (a) => (a ? true : false)), new go.Binding("stroke", "isHighlighted", (sel) => (sel ? "#1E90FF" : "#ff6600")).ofObject())
)
);
13. 動態追加連線 addLinkData()
diagram.model.addLinkData({ from: "節點key", to: "節點key", color: "線的顏色", linkText: "連線上的文字" }); // 不指定連線點,直接連
diagram.model.addLinkData({ from: "節點key", to: "節點key", color: "線的顏色", linkText: "連線上的文字", frompid: "right0", topid: "left0" }); // 設定入邊和出邊的連線點
var nodeDataArray = [
{ key: "A", rightArray: [{ portColor: "#33B12C", portId: "left0" }], rightArray: [{ portColor: "#33B12C", portId: "right0" }] },
{ key: "B", rightArray: [{ portColor: "#F29941", portId: "left0" }], rightArray: [{ portColor: "#F29941", portId: "right0" }] },
{ key: "C", rightArray: [{ portColor: "#11C67B", portId: "left0" }], rightArray: [{ portColor: "#11C67B", portId: "right0" }] },
];
var linkDataArray = [
{ from: "A", to: "B", frompid: "right0", topid: "left0" },
{ from: "B", to: "C", frompid: "right0", topid: "left0" },
];
diagram.model.linkFromPortIdProperty = "frompid"; // 連線點對應名稱
diagram.model.linkToPortIdProperty = "topid";
14. 點選節點使其相關聯節點與連線及文字高亮顯示
function nodeclick(e, node) {
var diagram = node.diagram;
diagram.clearHighlighteds();
node.findNodesConnected().each(function (l) {
l.isHighlighted = true;
});
node.linksConnected.each(function (n) {
n.isHighlighted = true;
});
}
15. 點選畫布清空當前高亮狀態元素
diagram.click = function (e) {
e.diagram.commit(function (d) {
d.clearHighlighteds();
}, "no highlighteds");
};
16. 刪除連線的方法
刪除一條: diagram.model.removeLinkData(linkData);
,這個方法,我試了幾個都沒有成功,可能是linkData
獲取的不對,又由於我是要全部刪除,因此使用了 diagram.model.removeLinkDataCollection
方法,進行批次刪除,但是在實際過程中發現,呼叫這個方法只能刪除一半,(也不知道是什麼原因,如果有耐心的有緣人,讀到此處並解決了問題,歡迎留言幫我解惑),但是呢辦法總比困難多,寫一個 while 迴圈就可以搞定了
while (diagram.model.linkDataArray.length) {
diagram.model.removeLinkDataCollection(diagram.model.linkDataArray);
}
17. 動態根據需求 展開和收起某一個或全部的分組
展開或收起某一個分組:
// groupKey 在 nodeDataArray節點列表中的, 分組節點的 Key值
diagram.findNodeForKey(groupKey).isSubGraphExpanded = true; // 展開
diagram.findNodeForKey(groupKey).isSubGraphExpanded = false; // 收起
展開或收起全部分組: 這個功能我用在了,在每次展開某種特定條件的分組時,先關閉之前所有的分組,在進行新一輪的展開操作
function graphExpandCollaps() {
const { nodeDataArray } = diagram.model;
nodeDataArray.forEach((v) => {
if (v.isGroup) {
diagram.findNodeForKey(v.groupName).isSubGraphExpanded = false; // 分組全部收起
}
});
}
18. 關閉畫布重新渲染時的動畫
diagram.animationManager.stopAnimation(); // 取消動畫
19. 畫布無限拖拽功能
畫布固定寬度和高度之後,對拖拽功能很不友好,當內容比較多時,容易拖拽不動,再則在使用 mac 瀏覽器時,會觸發瀏覽器的前進後退事件。設定畫布可以無限滾動後,解決
diagram.scrollMode = go.Diagram.InfiniteScroll;
20. 圖禁止複製,禁止刪除,開啟滑鼠滾輪縮放,toolTip 觸發 hoverDelay 初始定位
diagram = $(go.Diagram, "myDiagramDiv", {
"toolManager.hoverDelay": 200, // toolTip觸發
"toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom, // 開啟滑鼠滾輪縮放
// initialContentAlignment: go.Spot.Center, // 居中:第一次 不好使
// contentAlignment: go.Spot.Center, // 可以居中,但是居中之後不可以拖動
initialPosition: new go.Point(-150, -300), // 初始座標
allowCopy: false, // 禁止複製
allowDelete: false, // 禁止刪除
scale: 1, // 縮放會恢復預設值 diagram.scale = 1
minScale: 0.4, // 縮小 diagram.scale -= 0.1
maxScale: 1.4, // 放大 diagram.scale += 0.1
});
21. 消除水印
在其他文章中看到要刪除一個函式的方法,但是由於 gojs 是壓縮過的,每個版本的變數都不一樣,因此查到另外一個方式,親測有效,
全文搜尋 String.fromCharCode(a.charCodeAt(g)^b[(b[c]+b[d])%256])
,大家再搜尋時,需要注意一個空格,不然可能會導致搜尋不到結果。
在 for 迴圈的後面 加上一個 判斷, 需要跟畫布上水印的文字進行比對一下,我覺得其實寫一個條件語句,應該就可以命中了。
if (f.indexOf("GoJS 2.2 evaluation") > -1 || f.indexOf("(c) 1998-2022 Northwoods Software") > -1 || f.indexOf("Not for distribution or production use") > -1 || f.indexOf("gojs.net") > -1) {
return "";
} else {
return f;
}