視覺化—gojs 超多超實用經驗分享(一)

2023-05-04 18:01:11

視覺化的庫非常多,如: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,在資料中分別定義了 leftArrayrightArray,用於迴圈顯示子元素

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;
}

更多分享 詳見下一篇博文

gojs 超多超實用經驗分享(二)