摘要: 在本文中,我們将借助 D3.js 的靈活性這一優勢,去新增一些 D3.js 本身并不支援但我們想要的一些常見的功能:Nebula Graph 圖探索的删除節點和縮放功能

前言
在上篇文章中(D3.js 力導向圖的顯示優化),我們說過 D3.js 在自定義圖形上相較于其他開源可視化庫的優勢,以及如何對文檔對象模型(DOM)進行靈活操作。既然 D3.js 辣麼靈活,那是不是實作很多我們想做的事情呢?在本文中,我們将借助 D3.js 的靈活性這一優勢,去新增一些 D3.js 本身并不支援但我們想要的一些常見的功能。
建構 D3.js 力導向圖
在這裡我們就不再細說 d3-force 粒子實體運動子產品原理,感興趣同學可以看看我們的上篇的簡單描述, 本次實踐我們側重于可視化操作的功能實作。
好的,進入我們的實踐時間,我們還是以 D3.js 力導向圖對圖資料庫的資料關系進行分析為目的,增加一些我們想要功能。
首先,我們用 d3-force 力導向圖來建構一個簡單的關聯網
this.force = d3
.forceSimulation()
// 為節點配置設定坐标
.nodes(data.vertexes)
// 連接配接線
.force('link', linkForce)
// 整個執行個體中心
.force('center', d3.forceCenter(width / 2, height / 2))
// 引力
.force('charge', d3.forceManyBody().strength(-20))
// 碰撞力 防止節點重疊
.force('collide',d3.forceCollide().radius(60).iterations(2));
通過上述代碼,我們可以得到下面這樣一個可視化的節點和關系圖。
這裡我們簡單介紹下上圖,上圖為圖資料庫 Nebula Graph 可視化工具 Studio 的圖探索功能截圖,在業務上,圖探索支援使用者任意選中某個點進行拓展,找尋、顯示同它存在某種關系的點,例如上圖點 100 和 點 200 存在單向 follow 關系。
上圖資料量并不大,如果我們在拓展時傳回的資料量較大或多步拓展出來的資料逐漸累加顯示,則會導緻目前視圖頁節點和邊極多,頁面需呈現的資料資訊量大,且也不好找到想要的某個節點。好的,一個新場景上線了:使用者隻想分析圖中的部分節點資料,不想看到全部的節點資訊。删除任意選中這個新功能就可以很好地應對上面場景,删除不需要的節點資訊,隻留下想探索的部分節點資料。
支援删除任意選中功能
在實作這個功能之前,我先開始介紹下 D3.js 自帶 API。沒錯,還是上篇提及的 D3.js 的 enter() 及沒提到的 exit()
摘自文檔的描述:
資料綁定的時候可能出現元素與資料元素個數不比對的問題,
DOM
和
enter
就是用來處理這個問題的。
exit
操作用來添加新的
enter
元素,
DOM
操作用來移除多餘的
exit
元素。 如果資料元素多于
DOM
個數時用
DOM
,如果資料元素少于
enter
元素,則用
DOM
exit
。 在資料綁定時候存在三種情形:
1. 資料元素個數多于
DOM
元素個數
2. 資料元素與
DOM
元素個數一樣
3. 資料元素個數少于
元素個數
DOM
根據文檔描述,想實作删除任意選中功能還是很簡單的,樂觀的筆者想當然地認為直接在資料層面進行操作就行。于是筆者直接在 nodes 資料裡删除選中的節點資料 node,然後根據官方用法
d3.select(this.nodeRef).exit().remove()
移除多餘的元素,好的,我們現在來看看這樣做會帶來了什麼?
不想選中的節點是删除了,但其他節點的顯示也亂了,節點顔色和屬性同目前 DOM 節點對不上,為什麼會這樣呢?筆者又仔仔細細地看了一遍上面的文檔描述,靈光一閃,來,先列印下 exit().remove() 的節點,看看到底它 remove 哪些節點?
果然是它,D3.js enter().exit() 的觸發其實是在監聽元素的個數的變化,也就是說,如果總個數缺少了兩個,它确實會觸發 exit() 方法,但是它處理的資料不是真正需删除的資料,而是目前 nodes 資料最後兩個節點。說白了 enter()、exit() 的觸發原理,是 D3.js 監聽目前資料的長度變化來觸發的。然而 D3.js 在擷取資料長度變化之後,以 exit() 為例,對單個資料的處理方法是根據長度的減量 N 截取資料數組位置中最後 N 位到最後一位區間的所有元素,enter() 則相反,會在數組位置中最後一個元素後面增加 N 個資料。
是以,如果選中删除的是之前拓展探索出來的節點(它不是目前資料數組位置的最後一個元素),進行删除操作時,雖然從我們的 nodes 資料裡面删除了這個資料,但是在已經存在的視圖中,d3.select(this.nodeRef).exit() 方法定位到的操作元素卻是最後一個,這樣顯示就亂套了,那麼,我們該如何處理這個問題呢?
這裡就直接分享下我的方法,簡單粗暴但有效——顯然這個 exit() 并不能滿足删除選中節點的業務需求,那我們單獨地處理需删除的節點。我們定位到真實删除的節點 DOM 進行操作,為此我們需要在渲染時給每個節點綁定一個 ID,然後再進行周遊,根據已删除的節點資料找到這些需要删除的節點對應的 DOM,以下為我們的處理代碼:
componentDidUpdate(prevProps) {
const { nodes } = this.props;
if (nodes.length < prevProps.nodes.length) {
const removeNodes = _.differenceBy(
prevProps.nodes,
nodes,
(v: any) => v.name,
);
removeNodes.forEach(removeNode => {
d3.select('#name_' + removeNode.name).remove();
});
} else {
this.labelRender(this.props.nodes);
}
}
其實在這裡需要處理的不僅僅定位到目前真實删除節點的 DOM,還需要将它所關聯的邊、顯示文案一并删除。因為沒有起點/終點的邊,是沒有任何意義的,邊、文案的處理方法同點删除的邏輯類似,這裡不做贅述,如果你有任何疑問,歡迎前往我們的項目位址:https://github.com/vesoft-inc/nebula-web-docker 進行交流。
支援按鈕縮放功能
說完删除選中點,在可視化視圖中縮放操作也是比較常見的功能,D3.js 中的 d3.zoom() 就是用來實作縮放功能的,且該方法經過其他廠的業務考驗相對來說成熟穩定,那我們還有什麼理由要自己做呢?(要啥自行車 )。
其實縮放功能純粹是互動改動層面上的一個功能。采用滾輪控制縮放的方案的話,不了解 Nebula Graph Studio 的使用者很難發現這種隐藏操作,而且滾動控制縮放無法控制縮放的明确比例,舉個例子,使用者想縮放 30% / 50%,對于這種限定的比例,滾動控制縮放就無能為力了。除此之外,筆者在實施滾輪縮放的過程中發現滾動縮放會影響節點和邊的位置偏移,這又是什麼原因造成的呢?
通過檢視 d3.zoom() 代碼,我們發現 D3.js 本質是擷取事件中 d3.event 的縮放值再針對整個畫布修改 transform 屬性值,但這樣處理 svg 中的節點和邊元素 x、y 坐标不發生變化,是以導緻 d3.zoom() 實作縮放功能時,放大畫布,視圖會往坐左上方偏移(因為對畫布來說,相較視圖中的邊元素 x、y 坐标,自己變小了),縮小畫布,視圖會往右下方偏移。
發現問題形成的原因是解決問題的第一步,下面來解決下問題,在進行縮放時添加一個節點和邊相對畫布大小偏移量的變化處理邏輯,好的,那開始操作吧。
我們先弄一個滑動條控件提供給使用者進行手動控制縮放畫布的比例,直接用 antd 的滑動條,根據它滑動的的值來控制整個畫布縮放比例,下面直接貼代碼了
<svg
width={width}
viewBox={`0 0 ${width * (1 + scale)} ${height * (1 + scale)}`}
height={height}
>
{/*****/}
</svg>
上面代碼中的 scale 參數是我們根據控件滾動條中縮放值來生成的,我們需要記錄這個值來放大畫布(svg 元素),從來造成視圖縮小的效果的。
此外,我們處理下上面提到的節點和邊偏移問題時也需要 scale 值,因為我們需要給節點和邊設定一個反偏移量。簡單的說,畫布放大 scale 倍,節點和邊的 x、y 位置也要相對畫布偏移目前的 scale 倍,這樣就能保持在縮放過程中,節點和邊位置相對畫布大小變化而保持不變。下面就是處理節點縮放過程中偏移的關鍵代碼
const { width, height } = this.props;
const scale = (100 - zoomSize) / 100;
const offsetX = width * (scale / 2);
const offsetY = height * (scale / 2);
// 操作節點邊父元素 DOM <g/> 的偏移
d3.select(this.circleRef).attr(
'transform',
`translate(${offsetX} ${offsetY})`,
);
結語
好了,以上便是筆者使用 D3.js 力導向圖實作關系網的在自定義功能過程中思路和方法。不得不說,D3.js 的自由度真的高,我們可以盡情地開腦洞實作我們想要的功能。
在這次分享中,筆者分享了圖資料庫可視化業務中 2 個實用且使用者高頻使用的功能:任意選中删除節點、自定義縮放并優化視圖偏移功能。說到可視化展示一個複雜的關系網,需要考慮的問題還很多,需要優化的互動和顯示的地方也很多,我們會持續優化,後續我們會更新 D3.js 優化系列文,歡迎訂閱 Nebula Graph 部落格。
喜歡這篇文章?來來來,給我們的 GitHub 點個 star 表鼓勵啦~~ ♂️ ♀️ [手動跪謝]
GitHubgithub.com
交流圖資料庫技術?交個朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你進交流群~~
作者有話說:Hi,我是 Nico,是 Nebula Graph 的前端工程師,對資料可視化比較感興趣,分享一些自己的實踐心得,希望這次分享能給大家帶來幫助,如有不當之處,歡迎幫忙糾正,謝謝~