高级指引——自定义节点
title: 自定義節(jié)點(diǎn)
order: 1
G6 提供了一系列內(nèi)置節(jié)點(diǎn),包括 circle、rect、diamond、triangle、star、image、modelRect。若內(nèi)置節(jié)點(diǎn)無(wú)法滿足需求,用戶還可以通過(guò) G6.registerNode('nodeName', options) 進(jìn)行自定義節(jié)點(diǎn),方便用戶開(kāi)發(fā)更加定制化的節(jié)點(diǎn),包括含有復(fù)雜圖形的節(jié)點(diǎn)、復(fù)雜交互的節(jié)點(diǎn)、帶有動(dòng)畫的節(jié)點(diǎn)等。
在本章中我們會(huì)通過(guò)五個(gè)案例,從簡(jiǎn)單到復(fù)雜講解節(jié)點(diǎn)的自定義。這五個(gè)案例是:
1. 從無(wú)到有的定義節(jié)點(diǎn):繪制圖形;優(yōu)化性能。
2. 擴(kuò)展現(xiàn)有的節(jié)點(diǎn):附加圖形;增加動(dòng)畫。
3. 調(diào)整節(jié)點(diǎn)的錨點(diǎn);
4. 調(diào)整節(jié)點(diǎn)的鼠標(biāo)選中/懸浮樣式:樣式變化響應(yīng);動(dòng)畫響應(yīng);
5. 使用 DOM 自定義節(jié)點(diǎn)。
通過(guò) 圖形 Shape 章節(jié)的學(xué)習(xí),我們應(yīng)該已經(jīng)知道了自定義節(jié)點(diǎn)時(shí)需要滿足以下兩點(diǎn):
- 控制節(jié)點(diǎn)的生命周期;
- 解析用戶輸入的數(shù)據(jù),在圖形上展示。
G6 中自定義節(jié)點(diǎn)的 API 如下:
G6.registerNode('nodeName',{options: {style: {},stateStyles: {hover: {},selected: {},},},/*** 繪制節(jié)點(diǎn),包含文本* @param {Object} cfg 節(jié)點(diǎn)的配置項(xiàng)* @param {G.Group} group 圖形分組,節(jié)點(diǎn)中圖形對(duì)象的容器* @return {G.Shape} 返回一個(gè)繪制的圖形作為 keyShape,通過(guò) node.get('keyShape') 可以獲取。* 關(guān)于 keyShape 可參考文檔 核心概念-節(jié)點(diǎn)/邊/Combo-圖形 Shape 與 keyShape*/draw(cfg, group) {},/*** 繪制后的附加操作,默認(rèn)沒(méi)有任何操作* @param {Object} cfg 節(jié)點(diǎn)的配置項(xiàng)* @param {G.Group} group 圖形分組,節(jié)點(diǎn)中圖形對(duì)象的容器*/afterDraw(cfg, group) {},/*** 更新節(jié)點(diǎn),包含文本* @override* @param {Object} cfg 節(jié)點(diǎn)的配置項(xiàng)* @param {Node} node 節(jié)點(diǎn)*/update(cfg, node) {},/*** 更新節(jié)點(diǎn)后的操作,一般同 afterDraw 配合使用* @override* @param {Object} cfg 節(jié)點(diǎn)的配置項(xiàng)* @param {Node} node 節(jié)點(diǎn)*/afterUpdate(cfg, node) {},/*** 響應(yīng)節(jié)點(diǎn)的狀態(tài)變化。* 在需要使用動(dòng)畫來(lái)響應(yīng)狀態(tài)變化時(shí)需要被復(fù)寫,其他樣式的響應(yīng)參見(jiàn)下文提及的 [配置狀態(tài)樣式] 文檔* @param {String} name 狀態(tài)名稱* @param {Object} value 狀態(tài)值* @param {Node} node 節(jié)點(diǎn)*/setState(name, value, node) {},/*** 獲取錨點(diǎn)(相關(guān)邊的連入點(diǎn))* @param {Object} cfg 節(jié)點(diǎn)的配置項(xiàng)* @return {Array|null} 錨點(diǎn)(相關(guān)邊的連入點(diǎn))的數(shù)組,如果為 null,則沒(méi)有控制點(diǎn)*/getAnchorPoints(cfg) {},},// 繼承內(nèi)置節(jié)點(diǎn)類型的名字,例如基類 'single-node',或 'circle', 'rect' 等// 當(dāng)不指定該參數(shù)則代表不繼承任何內(nèi)置節(jié)點(diǎn)類型extendedNodeName, );???? 注意:
- 如果不從任何現(xiàn)有的節(jié)點(diǎn)或從 'single-node' 擴(kuò)展新節(jié)點(diǎn)時(shí),draw 方法是必須的;
- 節(jié)點(diǎn)內(nèi)部所有圖形使用相對(duì)于節(jié)點(diǎn)自身的坐標(biāo)系,即 (0, 0) 是該節(jié)點(diǎn)的中心。而節(jié)點(diǎn)的坐標(biāo)是相對(duì)于畫布的,由該節(jié)點(diǎn) group 上的矩陣控制,自定義節(jié)點(diǎn)中不需要用戶感知。若在自定義節(jié)點(diǎn)內(nèi)增加 rect 圖形,要注意讓它的 x 與 y 各減去其長(zhǎng)與寬的一半。詳見(jiàn)例子 從無(wú)到有定義節(jié)點(diǎn);
- update 方法可以不定義:
- 當(dāng) update 未定義:若指定了 registerNode 的第三個(gè)參數(shù) extendedNodeName(即代表繼承指定的內(nèi)置節(jié)點(diǎn)類型),則節(jié)點(diǎn)更新時(shí)將執(zhí)行被繼承的內(nèi)置節(jié)點(diǎn)類型的 update 邏輯;若未指定 registerNode 的第三個(gè)參數(shù),則節(jié)點(diǎn)更新時(shí)會(huì)執(zhí)行 draw 方法,所有圖形清除重繪;
- 當(dāng)定義了 update 方法,則不論是否指定 registerNode 的第三個(gè)參數(shù),在節(jié)點(diǎn)更新時(shí)都會(huì)執(zhí)行復(fù)寫的 update 函數(shù)邏輯。
- afterDraw,afterUpdate 方法一般用于擴(kuò)展已有的節(jié)點(diǎn),例如:在矩形節(jié)點(diǎn)上附加圖片,圓節(jié)點(diǎn)增加動(dòng)畫等;
- setState 只有在需要使用動(dòng)畫的方式來(lái)響應(yīng)狀態(tài)變化時(shí)需要復(fù)寫,一般的樣式響應(yīng)狀態(tài)變化可以通過(guò) 配置狀態(tài)樣式 實(shí)現(xiàn);
- getAnchorPoints 方法僅在需要限制與邊的連接點(diǎn)時(shí)才需要復(fù)寫,也可以在數(shù)據(jù)中直接指定。
1. 從無(wú)到有定義節(jié)點(diǎn)
繪制圖形
我們自己來(lái)實(shí)現(xiàn)一個(gè)菱形的節(jié)點(diǎn),如下圖所示。
G6 有內(nèi)置的菱形節(jié)點(diǎn) diamond。為了演示,這里實(shí)現(xiàn)了一個(gè)自定義的菱形,相當(dāng)于復(fù)寫了內(nèi)置的 diamond。
???? 注意: 從下面代碼可以看出,自定義節(jié)點(diǎn)中所有通過(guò) addShape 增加的圖形的坐標(biāo)都是相對(duì)于節(jié)點(diǎn)自身的子坐標(biāo)系,即 (0, 0) 是該節(jié)點(diǎn)的中心。如 'text' 圖形的 x 和 y 均為 0,代表該圖形相對(duì)于該節(jié)點(diǎn)居中;'path' 圖形 path 屬性中的坐標(biāo)也是以 (0, 0) 為原點(diǎn)計(jì)算的。換句話說(shuō),在自定義節(jié)點(diǎn)時(shí)不需要感知相對(duì)于畫布的節(jié)點(diǎn)坐標(biāo),節(jié)點(diǎn)坐標(biāo)由該節(jié)點(diǎn)所在 group 的矩陣控制。
G6.registerNode('diamond', {draw(cfg, group) {// 如果 cfg 中定義了 style 需要同這里的屬性進(jìn)行融合const keyShape = group.addShape('path', {attrs: {path: this.getPath(cfg), // 根據(jù)配置獲取路徑stroke: cfg.color, // 顏色應(yīng)用到描邊上,如果應(yīng)用到填充,則使用 fill: cfg.color},// must be assigned in G6 3.3 and later versions. it can be any value you wantname: 'path-shape',// 設(shè)置 draggable 以允許響應(yīng)鼠標(biāo)的圖拽事件draggable: true});if (cfg.label) {// 如果有文本// 如果需要復(fù)雜的文本配置項(xiàng),可以通過(guò) labeCfg 傳入// const style = (cfg.labelCfg && cfg.labelCfg.style) || {};// style.text = cfg.label;const label = group.addShape('text', {// attrs: styleattrs: {x: 0, // 居中y: 0,textAlign: 'center',textBaseline: 'middle',text: cfg.label,fill: '#666',},// must be assigned in G6 3.3 and later versions. it can be any value you wantname: 'text-shape',// 設(shè)置 draggable 以允許響應(yīng)鼠標(biāo)的圖拽事件draggable: true});}return keyShape;},// 返回菱形的路徑getPath(cfg) {const size = cfg.size || [40, 40]; // 如果沒(méi)有 size 時(shí)的默認(rèn)大小const width = size[0];const height = size[1];// / 1 \// 4 2// \ 3 /const path = [['M', 0, 0 - height / 2], // 上部頂點(diǎn)['L', width / 2, 0], // 右側(cè)頂點(diǎn)['L', 0, height / 2], // 下部頂點(diǎn)['L', -width / 2, 0], // 左側(cè)頂點(diǎn)['Z'], // 封閉];return path;}, });上面的代碼自定義了一個(gè)菱形節(jié)點(diǎn)。值得注意的是,G6 3.3 需要用戶為自定義節(jié)點(diǎn)中的圖形設(shè)置 name 和 draggable。其中,name 可以是不唯一的任意值。draggable 為 true 是表示允許該圖形響應(yīng)鼠標(biāo)的拖拽事件,只有 draggable: true 時(shí),圖上的交互行為 'drag-node' 才能在該圖形上生效。若上面代碼僅在 keyShape 上設(shè)置了 draggable: true,而 label 圖形上沒(méi)有設(shè)置,則鼠標(biāo)拖拽只能在 keyShape 上響應(yīng)。
現(xiàn)在,我們使用下面的數(shù)據(jù)輸入就會(huì)繪制出 diamond 這個(gè)節(jié)點(diǎn)。
const data = {nodes: [{ id: 'node1', x: 50, y: 100, type: 'diamond' }, // 最簡(jiǎn)單的{ id: 'node2', x: 150, y: 100, type: 'diamond', size: [50, 100] }, // 添加寬高{ id: 'node3', x: 250, y: 100, color: 'red', type: 'diamond' }, // 添加顏色{ id: 'node4', x: 350, y: 100, label: '菱形', type: 'diamond' }, // 附加文本], }; const graph = new G6.Graph({container: 'mountNode',width: 500,height: 500, }); graph.data(data); graph.render();優(yōu)化性能
當(dāng)圖中節(jié)點(diǎn)或邊通過(guò) ?graph.update(item, cfg) 重繪時(shí),默認(rèn)情況下會(huì)調(diào)用節(jié)點(diǎn)的 draw 方法進(jìn)行重新繪制。在數(shù)據(jù)量大或節(jié)點(diǎn)上圖形數(shù)量非常多(特別是文本多)的情況下,draw 方法中對(duì)所有圖形、賦予樣式將會(huì)非常消耗性能。
在自定義節(jié)點(diǎn)時(shí),重寫 ?update 方法,在更新時(shí)將會(huì)調(diào)用該方法替代 draw。我們可以在該方法中指定需要更新的圖形,從而避免頻繁調(diào)用 ?draw?、全量更新節(jié)點(diǎn)上的所有圖形。當(dāng)然,update 方法是可選的,如果沒(méi)有性能優(yōu)化的需求可以不重寫該方法。
在實(shí)現(xiàn) diamond 的過(guò)程中,重寫 ?update 方法,找到需要更新的 shape 進(jìn)行更新,從而優(yōu)化性能。尋找需要更新的圖形可以通過(guò):
- group.get('children')[0] 找到 關(guān)鍵圖形 ?keyShape,也就是 draw 方法返回的 shape;
- group.get('children')[1] 找到 label 圖形。
下面代碼僅更新了 diamond 的關(guān)鍵圖形的路徑和顏色。
G6.registerNode('diamond', {draw(cfg, group) {// ... // 見(jiàn)前面代碼},getPath(cfg) {// ... // 見(jiàn)前面代碼},update(cfg, node) {const group = node.getContainer(); // 獲取容器const shape = group.get('children')[0]; // 按照添加的順序const style = {path: this.getPath(cfg),stroke: cfg.color,};shape.attr(style); // 更新屬性// 更新文本的邏輯類似,但是需要考慮 cfg.label 是否存在的問(wèn)題// 通過(guò) label.attr() 更新文本屬性即可}, });2. 擴(kuò)展現(xiàn)有節(jié)點(diǎn)
擴(kuò)展 Shape
G6 中已經(jīng)內(nèi)置了一些節(jié)點(diǎn),如果用戶僅僅想對(duì)現(xiàn)有節(jié)點(diǎn)進(jìn)行調(diào)整,復(fù)用原有的代碼,則可以基于現(xiàn)有的節(jié)點(diǎn)進(jìn)行擴(kuò)展。同樣實(shí)現(xiàn) diamond ,可以基于 ?circle、ellipse、rect 等內(nèi)置節(jié)點(diǎn)的進(jìn)行擴(kuò)展。single-node 是這些內(nèi)置節(jié)點(diǎn)類型的基類,也可以基于它進(jìn)行擴(kuò)展。(single-edge 是所有內(nèi)置邊類型的基類。)
下面以基于 single-node 為例進(jìn)行擴(kuò)展。update,setState 方法在 ?single-node 中都有實(shí)現(xiàn),這里僅需要復(fù)寫 draw 方法即可。返回的對(duì)象中包含自定義圖形的路徑和其他樣式。
G6.registerNode('diamond',{draw(cfg, group) {const size = this.getSize(cfg); // 轉(zhuǎn)換成 [width, height] 的模式const color = cfg.color;const width = size[0];const height = size[1];// / 1 \// 4 2// \ 3 /const path = [['M', 0, 0 - height / 2], // 上部頂點(diǎn)['L', width / 2, 0], // 右側(cè)頂點(diǎn)['L', 0, height / 2], // 下部頂點(diǎn)['L', -width / 2, 0], // 左側(cè)頂點(diǎn)['Z'], // 封閉];const style = G6.Util.mix({},{path: path,stroke: color,},cfg.style,);// 增加一個(gè) path 圖形作為 keyShapeconst keyShape = group.addShape('path', {attrs: {...style},draggable: true,name: 'diamond-keyShape'});// 返回 keyShapereturn keyShape;}},// 注意這里繼承了 'single-node''single-node', );添加動(dòng)畫
通過(guò) afterDraw 同樣可以實(shí)現(xiàn)擴(kuò)展,下面我們來(lái)看一個(gè)節(jié)點(diǎn)的動(dòng)畫場(chǎng)景,如下圖所示。
上面的動(dòng)畫效果,可以通過(guò)以下方式實(shí)現(xiàn):
- 擴(kuò)展內(nèi)置的 rect,在 rect 中添加一個(gè)圖形;
- 反復(fù)執(zhí)行新添加圖形的旋轉(zhuǎn)動(dòng)畫。
更多關(guān)于動(dòng)畫的實(shí)現(xiàn),請(qǐng)參考基礎(chǔ)動(dòng)畫章節(jié)。
3. 調(diào)整錨點(diǎn) anchorPoint
節(jié)點(diǎn)上的錨點(diǎn) anchorPoint 作用是確定節(jié)點(diǎn)與邊的相交的位置,看下面的場(chǎng)景:
(左)沒(méi)有設(shè)置錨點(diǎn)時(shí)。(右)diamond 設(shè)置了錨點(diǎn)后。
有兩種方式來(lái)調(diào)整節(jié)點(diǎn)上的錨點(diǎn):
- 在數(shù)據(jù)里面指定 anchorPoints。
**適用場(chǎng)景:**可以為不同節(jié)點(diǎn)配置不同的錨點(diǎn),更定制化。
- 自定義節(jié)點(diǎn)中通過(guò) getAnchorPoints 方法指定錨點(diǎn)。
**適用場(chǎng)景:**全局配置錨點(diǎn),所有該自定義節(jié)點(diǎn)類型的節(jié)點(diǎn)都相同。
數(shù)據(jù)中指定錨點(diǎn)
const data = {nodes: [{id: 'node1',x: 100,y: 100,anchorPoints: [[0, 0.5], // 左側(cè)中間[1, 0.5], // 右側(cè)中間],},//... // 其他節(jié)點(diǎn)],edges: [//... // 邊], };自定義時(shí)指定錨點(diǎn)
G6.registerNode('diamond',{//... // 其他方法getAnchorPoints() {return [[0, 0.5], // 左側(cè)中間[1, 0.5], // 右側(cè)中間];},},'rect', );4. 調(diào)整狀態(tài)樣式
常見(jiàn)的交互都需要節(jié)點(diǎn)和邊通過(guò)樣式變化做出反饋,例如鼠標(biāo)移動(dòng)到節(jié)點(diǎn)上、點(diǎn)擊選中節(jié)點(diǎn)/邊、通過(guò)交互激活邊上的交互等,都需要改變節(jié)點(diǎn)和邊的樣式,有兩種方式來(lái)實(shí)現(xiàn)這種效果:
我們推薦用戶使用第二種方式來(lái)實(shí)現(xiàn)節(jié)點(diǎn)的狀態(tài)調(diào)整,可以通過(guò)以下方式來(lái)實(shí)現(xiàn):
- 在 G6 中自定義節(jié)點(diǎn)/邊時(shí)在 setState 方法中進(jìn)行節(jié)點(diǎn)狀態(tài)變化的響應(yīng);
- 通過(guò) graph.setItemState() 方法來(lái)設(shè)置狀態(tài)。
基于 rect 擴(kuò)展出一個(gè) custom 圖形,默認(rèn)填充色為白色,當(dāng)鼠標(biāo)點(diǎn)擊時(shí)變成紅色,實(shí)現(xiàn)這一效果的示例代碼如下:
// 基于 rect 擴(kuò)展出新的圖形 G6.registerNode('custom',{// 響應(yīng)狀態(tài)變化setState(name, value, item) {const group = item.getContainer();const shape = group.get('children')[0]; // 順序根據(jù) draw 時(shí)確定if (name === 'selected') {if (value) {shape.attr('fill', 'red');} else {shape.attr('fill', 'white');}}},},'rect', );// 點(diǎn)擊時(shí)選中,再點(diǎn)擊時(shí)取消 graph.on('node:click', ev => {const node = ev.item;graph.setItemState(node, 'selected', !node.hasState('selected')); // 切換選中 });G6 并未限定節(jié)點(diǎn)的狀態(tài),只要你在 setState 方法中進(jìn)行處理你可以實(shí)現(xiàn)任何交互,如實(shí)現(xiàn)鼠標(biāo)放到節(jié)點(diǎn)上后節(jié)點(diǎn)逐漸變大的效果。
5. 使用 DOM 自定義節(jié)點(diǎn)
SVG 與 DOM 圖形在 V3.3.x 中不支持。
這里,我們演示使用 DOM 自定義一個(gè)名為 'dom-node' 的節(jié)點(diǎn)。在 draw 方法中使用 group.addShape 增加一個(gè) 'dom' 類型的圖形,并設(shè)置其 html 為 DOM 的 html 值。
G6.registerNode('dom-node', {draw: (cfg: ModelConfig, group: Group) => {return group.addShape('dom', {attrs: {width: cfg.size[0],height: cfg.size[1],// 傳入 DOM 的 htmlhtml: `<div style="background-color: #fff; border: 2px solid #5B8FF9; border-radius: 5px; width: ${cfg.size[0]-5}px; height: ${cfg.size[1]-5}px; display: flex;"><div style="height: 100%; width: 33%; background-color: #CDDDFD"><img alt="img" style="line-height: 100%; padding-top: 6px; padding-left: 8px;" src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Q_FQT6nwEC8AAAAAAAAAAABkARQnAQ" width="20" height="20" /> </div><span style="margin:auto; padding:auto; color: #5B8FF9">${cfg.label}</span></div>`},draggable: true});}, }, 'single-node');上面的代碼自定義了一個(gè)名為 'dom-node' 的帶有 DOM 的節(jié)點(diǎn)。值得注意的是,G6 3.3 需要用戶為自定義節(jié)點(diǎn)中的圖形設(shè)置 name 和 draggable。其中,name 可以是不唯一的任意值。draggable 為 true 是表示允許該圖形響應(yīng)鼠標(biāo)的拖拽事件,只有 draggable: true 時(shí),圖上的交互行為 'drag-node' 才能在該圖形上生效。
現(xiàn)在,我們使用下面的數(shù)據(jù)輸入就會(huì)繪制出帶有 'dom-node' 節(jié)點(diǎn)的圖。
const data = {nodes: [{ id: 'node1', x: 50, y: 100 },{ id: 'node2', x: 150, y: 100 },],edges: [source: 'node1',target: 'node2'] }; const graph = new G6.Graph({container: 'mountNode',width: 500,height: 500,defaultNode: {type: 'dom-node',size: [120, 40]} }); graph.data(data); graph.render();?? 注意: G6 的節(jié)點(diǎn)/邊事件不支持 DOM 類型的圖形。如果需要為 DOM 節(jié)點(diǎn)綁定事件,請(qǐng)使用原生 DOM 事件。例如:
G6.registerNode('dom-node', {draw: (cfg: ModelConfig, group: Group) => {return group.addShape('dom', {attrs: {width: cfg.size[0],height: cfg.size[1],// 傳入 DOM 的 html,帶有原生 onclick 事件html: `<div οnclick="alert('Hi')" style="background-color: #fff; border: 2px solid #5B8FF9; border-radius: 5px; width: ${cfg.size[0]-5}px; height: ${cfg.size[1]-5}px; display: flex;"><div style="height: 100%; width: 33%; background-color: #CDDDFD"><img alt="img" style="line-height: 100%; padding-top: 6px; padding-left: 8px;" src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Q_FQT6nwEC8AAAAAAAAAAABkARQnAQ" width="20" height="20" /> </div><span style="margin:auto; padding:auto; color: #5B8FF9">${cfg.label}</span></div>`},draggable: true});}, }, 'single-node');總結(jié)
以上是生活随笔為你收集整理的高级指引——自定义节点的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 315. Count of Smalle
- 下一篇: 拓展阅读 —— G6 坐标系深度解析