1. 背景
業務背景:CRM 系統隨著各業務條線對線索精細化分配的訴求逐漸增加,各個條線的流向規則會越來越復雜,各個條線甚至整個 CRM 的線索流轉規則急需一種樹形的可視化的圖來表達。
技術背景:在開發之前考慮了三種方案,原生 canvas、fabric 以及 G6,三種方案各有優劣勢
原生 canvas | fabric | G6 | |
---|---|---|---|
優點 | 靈活、自由、可定制化非常強 | 封裝了 canvas 的 api,使用簡單靈活 | 提供了復雜樹、圖等 api,只需要按照文檔配置即可 |
缺點 | 開發復雜、耗時 | 對于構建大型樹、圖等復雜、耗時 | 在開發前需要認真閱讀 api 文檔,上手慢 |
通過上述表格對比就可以看出來,對于構建更為復雜的樹、圖等,G6 具備明顯的優勢,而且又有活躍的開源社區,為后續的維護升級提供了保證。
2. 基礎知識
下面是關于本次雙樹流轉繪制的幾個核心概念,簡單介紹一下,如有興趣,還是建議閱讀 G6 官方 API 文檔
圖 Graph
通過 new G6.Graph (config) 進行圖的實例化。其中參數 config 是 Object 類型的圖的配置項,圖的大部分功能可以通過該配置項進行全局配置。
如果是樹圖,需要使用 new G6.TreeGraph 方法進行實例化
const graph = new G6.Graph({ container: 'mountNode', // 指定圖畫布的容器 id,與第 9 行的容器對應 // 畫布寬高 width: 800, height: 500, }); // 讀取數據 graph.data(data); // 渲染圖 graph.render();
圖元素 - 節點
G6 的內置節點包括 circle,rect,ellipse,diamond,triangle,star,image,modelRect,donut(v4.2.5 起支持)。這些內置節點的默認樣式分別如下圖所示。
內置節點配置
const graph = new G6.Graph({ container: 'mountNode', width: 800, height: 600, defaultNode: { type: 'circle', // 節點類型 // ... 其他配置 }, });
自定義節點配置
// 在節點配置中配置自定義節點名稱以及尺寸 defaultNode: { type: 'card-node', size: [338, 70], } // 使用G6.registerNode自定義節點,在自定義節點中可以自定義各種形狀的圖形,包括text等 G6.registerNode('card-node', { draw: function drawShape(cfg, group) { const w = cfg.size[0]; const h = cfg.size[1]; group.addShape('rect', { attrs: { x: -w / 2 - 2, y: -(h - 30) / 2, width: 6, //200, height: h - 30, // 60 fill: '#3c6ef0', radius: [0, 4, 4, 0] }, name: 'mark-box', draggable: true, });
圖元素 - 邊
G6 提供了 9 種內置邊: line:直線,不支持控制點; polyline:折線,支持多個控制點; arc:圓弧線; quadratic:二階貝塞爾曲線; cubic:三階貝塞爾曲線; cubic-vertical:垂直方向的三階貝塞爾曲線,不考慮用戶從外部傳入的控制點; cubic-horizontal:水平方向的三階貝塞爾曲線,不考慮用戶從外部傳入的控制點; loop:自環。
內置邊配置
const graph = new G6.Graph({ container: 'mountNode', width: 800, height: 600, defaultEdge: { type: 'cubic', // 其他配置 }, });
自定義邊配置
在配置中引用自定義邊 defaultEdge: { type: 'hvh', // 其他配置 } // 使用G6.registerEdge方法配置自定義邊 G6.registerEdge('hvh', { draw(cfg, group) { const startPoint = cfg.startPoint; const endPoint = cfg.endPoint; const shape = group.addShape('path', { attrs: { stroke: '#333', path: [ ['M', startPoint.x, startPoint.y], ['L', endPoint.x / 3 + (2 / 3) * startPoint.x, startPoint.y], // 三分之一處 ['L', endPoint.x / 3 + (2 / 3) * startPoint.x, endPoint.y], // 三分之二處 ['L', endPoint.x, endPoint.y], ], }, // 在 G6 3.3 及之后的版本中,必須指定 name,可以是任意字符串,但需要在同一個自定義元素類型中保持唯一性 name: 'path-shape', }); return shape; }, });
圖布局 - 樹圖布局
樹圖 TreeGraph 布局方法總覽 CompactBox Layout:緊湊樹布局; Dendrogram Layout:樹狀布局(葉子節點布局對齊到同一層); Indented Layout:縮進布局; Mindmap Layout:腦圖布局
const graph = new G6.TreeGraph({ container: 'mountNode', modes: { default: [ { // 定義展開/收縮行為 type: 'collapse-expand', }, 'drag-canvas', ], }, // 定義布局 layout: { type: 'dendrogram', // 布局類型 direction: 'LR', // 自左至右布局,可選的有 H / V / LR / RL / TB / BT nodeSep: 50, // 節點之間間距 rankSep: 100, // 每個層級之間的間距 excludeInvisibles: true, // 布局計算是否排除掉隱藏的節點,v4.8.8 起支持 }, });
交互與事件
全局事件 只要在畫布上范圍內發生均會被觸發,如 mousedown,mouseup,click,mouseenter,mouseleave 等。
graph.on('click', (ev) => { const shape = ev.target; const item = ev.item; if (item) { const type = item.getType(); } });
canvas 事件
只在 canvas 空白處被觸發,如 canvas:mousedown,canvas:click 等,以 canvas:eventName 為事件名稱。
graph.on('canvas:click', (ev) => { const shape = ev.target; const item = ev.item; if (item) { const type = item.getType(); } });
節點 / 邊 上的事件
例如 node:mousedown,edge:click,combo:click 等,以 type:eventName 為事件名稱。
graph.on('node:click', (ev) => { const node = ev.item; // 被點擊的節點元素 const shape = ev.target; // 被點擊的圖形,可根據該信息作出不同響應,以達到局部響應效果 // ... do sth }); graph.on('edge:click', (ev) => { const edge = ev.item; // 被點擊的邊元素 const shape = ev.target; // 被點擊的圖形,可根據該信息作出不同響應,以達到局部響應效果 // ... do sth }); graph.on('combo:click', (ev) => { const combo = ev.item; // 被點擊 combo 元素 const shape = ev.target; // 被點擊的圖形,可根據該信息作出不同響應,以達到局部響應效果 // ... do sth });
3. 技術方案 & 實施
數據準備
要求數據中每一個節點必須有 id,且 id 為字符串類型。module 字段 root 表示根節點,right 表示子節點,left 表示父節點。flowCountList 表示邊,從某個節點到某個節點
data: { id: '1', name: '根節點', flowInCount: 9999, flowOutCount: 9999, currentCount: 9999, module: 'root', children: [ { id: '2', name: '右一節點', flowInCount: 9999, flowOutCount: 9999, currentCount: 9999, module: 'son', }, { id: '3', name: '左一節點', flowInCount: 9999, flowOutCount: 9999, currentCount: 9999, module: 'father', } ] } flowCountList: [ { fromPoolId: '1', toPoolId: '2', clueCount: 111 }, { fromPoolId: '1', toPoolId: '3', clueCount: 222 } ]
初始化 Minimap 實例
如果需要加 Minimap 可以在畫布外層 div 中增加一個 div#minimap,將小地圖放入其中。下面配置中 delegate 表示小地圖只渲染畫布中元素的框架,以此來降低性能損耗。
const miniMap = document.getElementById('minimap'); const minimap = new G6.Minimap({ container: miniMap, type: 'delegate', size: [178, 138] });
初始化樹圖
modes 中配置的 drag-canvas 表示支持畫布拖拽,zoom-canvas 表示支持畫布放大縮小,tooltip 表示給節點增加 tooltip 提示。 layout 中 getSide 方法會根據數據類型判斷當前節點屬于父節點還是子節點,本方法只針對根節點有效。
this.graph = new G6.TreeGraph({ container: 'clueCanvas', width:1000, // width和height可以根據自己畫布大小進行賦值 height:500, modes: { default: ['drag-canvas', 'zoom-canvas',{ type: 'tooltip', formatText: function formatText(model) { return model.name; }, shouldBegin: (e) => { const target = e.target; if (target.get('name') === 'title') return true; return false; }, }], }, defaultNode: { type: 'card-node', size: [338, 70], }, defaultEdge: { type: 'custom-edge', style: { lineWidth: 4, lineAppendWidth: 8, stroke: '#BABEC7', } }, layout: { type: 'mindmap', direction: 'H', getHeight: () => {return 70;}, //節點高度 getWidth: () => {return 338;}, // 節點寬度 getVGap: () => {return 8;}, // 節點之間的垂直間距 getHGap: () => {return 100;}, // 節點之間的水平間距 getSide: (d) => { if (d.data.module === 'father') { return 'left'; } return 'right'; }, }, plugins: [minimap] }); this.graph.data(data); this.graph.render(); // 渲染 this.graph.fitView(); // 全屏展示
自定義節點
自定義節點是在默認節點不能滿足我們的需求時,自己定義一些圖形要素,可以單個的也可以是多個進行組合,如下圖所示:
由于篇幅有限,以下代碼只展示了外邊框和眼睛圖標的繪制,其他元素的繪制基本類似。其中 setState 回調方法是監聽狀態變化的,比如本例中監聽樹節點展開 / 收起,進行眼睛圖標的切換以及節點底色的變更邏輯。
G6.registerNode('card-node', { draw: function drawShape(cfg, group) { const w = cfg.size[0]; const h = cfg.size[1]; const shape = group.addShape('rect', { attrs: { x: -w / 2, y: -h / 2, width: w, //200, height: h, // 60 radius: 8, fill: 'l(0) 0:rgba(197,213,255,1) 0.3:rgba(226,233,253,1) 1:rgba(255,255,255,1)', shadowOffsetX: -2, shadowOffsetY: 0, shadowColor: '#82A2F5', shadowBlur: 0, stroke: 'l(0) 0.1:rgba(138,169,249,1) 1:rgba(202,216,254,1)' }, // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type name: 'main-box', draggable: true, }); cfg.children && group.addShape('image', { attrs: { x: w / 2 - 35, y: -h / 2 + 10, cursor: 'pointer', img: flowEyeOpen,//cfg.collapsed ? flowEyeOpen : flowEyeClose, width: 16, height: 16 }, name: 'collapse-icon', }); return shape; }, setState(name, value, item) { if (name === 'collapsed') { // 替換眼睛圖標 const marker = item.get('group').find((ele) => ele.get('name') === 'collapse-icon'); const icon = value ? flowEyeClose : flowEyeOpen; marker.attr('img', icon); // 替換卡片背景 const mainBox = item.get('group').find((ele) => ele.get('name') === 'main-box'); const fill = value ? '#fff' : 'l(0) 0:rgba(197,213,255,1) 0.3:rgba(226,233,253,1) 1:rgba(255,255,255,1)' const shadowOffsetX = value ? 0 : -2 mainBox.attr('fill', fill) mainBox.attr('shadowOffsetX', shadowOffsetX) } }, });
節點事件監聽
當點擊節點中的眼睛圖標則執行展開 / 收起,并更新其狀態,進行重繪。狀態更新之后,上文中的 setState 回調函數就被出發,隨后根據新的展開 / 收起狀態對節點樣式進行變更
this.graph.on('node:click', (e) => { // 點擊眼睛圖標展開子節點 if (e.target.get('name') === 'collapse-icon') { e.item.getModel().collapsed = !e.item.getModel().collapsed; this.graph.setItemState(e.item, 'collapsed', e.item.getModel().collapsed); this.graph.layout(); } });
自定義邊
邊的默認樣式使用內置邊 cubic-horizontal 類型,當鼠標移到邊上時會出現一個數量數據如下圖所示:
當狀態變為激活狀態 active 時,將邊上的元素透明度置為 1 可見,否則置為 0 不可見。默認為 0
G6.registerEdge( 'custom-edge', { afterDraw(cfg, group) { const source = cfg.sourceNode._cfg.model.id const target = cfg.targetNode._cfg.model.id let current = self.flowCountList.find(item=>{ return source === item.fromPoolId && target === item.toPoolId }) // 如果未找到,在進行反向查一次 if(!current) { current = self.flowCountList.find(item=>{ return source === item.toPoolId && target === item.fromPoolId }) } // 獲取圖形組中的第一個圖形,在這里就是邊的路徑圖形 const shape = group.get('children')[0]; // 獲取路徑圖形的中點坐標 const midPoint = shape.getPoint(0.5); // 在中點增加一個矩形,注意矩形的原點在其左上角 group.addShape('rect', { attrs: { width: 92, height: 20, fill: '#BABEC7', stroke: '#868D9F', lineWidth: 1, radius: 10, opacity: 0, // x 和 y 分別減去 width / 2 與 height / 2,使矩形中心在 midPoint 上 x: midPoint.x - 92/2, y: midPoint.y - 20/2, }, name: 'edge-count', // 在 G6 3.3 及之后的版本中,必須指定 name,可以是任意字符串,但需要在同一個自定義元素類型中保持唯一性 }); group.addShape('text', { attrs: { textBaseline: 'top', x: midPoint.x - 92/2 + 20, y: midPoint.y - 12/2 + 1, lineHeight: 12, text: `流出 ${current ? current.clueCount : 0}`, fontSize: 12, fill: '#ffffff', opacity: 0, }, name: 'edge-text', }); }, // // 響應狀態變化 setState(name, value, item) { if (name === 'active') { const edgeCount = item.get('group').find((ele) => ele.get('name') === 'edge-count'); const edgeText = item.get('group').find((ele) => ele.get('name') === 'edge-text'); edgeCount.attr('opacity', value ? 1 : 0) edgeText.attr('opacity', value ? 1 : 0) } }, }, 'cubic-horizontal', );
邊事件監聽
監聽邊的 mouseenter 和 mouseleave 事件,更新其激活狀態
this.graph.on('edge:mouseenter', (ev) => { const edge = ev.item; this.graph.setItemState(edge, 'active', true); }); this.graph.on('edge:mouseleave', (ev) => { const edge = ev.item; this.graph.setItemState(edge, 'active', false); });到此雙數渲染邏輯介紹完畢,如果有類似案例,歡迎參考
4. 踩坑記錄
縮進樹 - 頂部對齊,左側樹不會對齊
配置如下 indented 表示縮進樹,dropCap 置為 false 表示關閉下垂樹,只有右側樹時,顯示正常,如果雙樹渲染,左側樹不會縮進,屬于 g6 本身的 bug
layout: { type: 'indented', dropCap: false, },
初次加載樹結構,限制只展開兩級只需要在二級節點增加屬性 collapsed : true 數據結構如下:
{ id: '1', children: [{ id: '1-1', children: [{ id: '1-1-1', collapsed: true }] }] }
一個節點有兩個局部區域增加 tooltip
const tooltip = new G6.Tooltip({ className: 'g6-tooltip', offsetX: -5, offsetY: -165, getContent(e) { return '111' }, shouldBegin(e){ return true }, itemTypes: ['node'] }); // 增加tooltip插件來實現一個節點多個局部區域增加tooltip plugins: [tooltip,tooltip1]
關于編譯報錯的問題
由于我本地 ts 版本太低,導致 ts 在對 g6 進行代碼檢查時報錯,具體報錯信息如下截圖:
原因分析: g6 代碼庫用到了 ts 的高級語法,導致當前低版本 ts 在對其進行檢查時未通過 解決方法:
(1). 在 tsconfig.json 中增加 "skipLibCheck": true 跳過 ts 的檢查,但是發現測試環境和預發環境編譯通過,上線時使用 npm run build 編譯不成功
(2). 升級 ts 版本,將當前使用的 3.5.3 升級至最新版,發現雖然上述問題解決了,但是系統中其他地方有報錯,為了降低上線的風險,放棄了這種方式
(3). 降級 @antv/g6 的版本,使其從最新版本降到了 4.3.4,最后發現可行,編譯沒有任何問題,而且功能運行正常,體驗較差的是 @antv/g6 關于每一個版本的升級都沒有提及這個問題,所以需要按版本試驗,整整折騰了一天才找到這個版本,后續如果有人遇到這個問題可以進行借鑒
5. 成果展示
全局展示
局部展示
審核編輯:劉清
-
CRM
+關注
關注
1文章
145瀏覽量
21136
原文標題:如何基于G6進行雙樹流轉繪制?
文章出處:【微信號:OSC開源社區,微信公眾號:OSC開源社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論