D3.js 力導向圖的顯示優化(二)- 自定義功能

NebulaGraph發表於2020-07-09
摘要: 在本文中,我們將藉助 D3.js 的靈活性這一優勢,去新增一些 D3.js 本身並不支援但我們想要的一些常見的功能:Nebula Graph 圖探索的刪除節點和縮放功能。

文章首發於 Nebula Graph 官博:https://nebula-graph.com.cn/p...

D3顯示優化

前言

在上篇文章中(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

這裡我們簡單介紹下上圖,上圖為圖資料庫 Nebula Graph 視覺化工具 Studio 的圖探索功能截圖,在業務上,圖探索支援使用者任意選中某個點進行擴充,找尋、顯示同它存在某種關係的點,例如上圖點 100 和 點 200 存在單向 follow 關係。

上圖資料量並不大,如果我們在擴充時返回的資料量較大或多步擴充出來的資料逐步累加顯示,則會導致當前檢視頁節點和邊極多,頁面需呈現的資料資訊量大,且也不好找到想要的某個節點。好的,一個新場景上線了:使用者只想分析圖中的部分節點資料,不想看到全部的節點資訊。刪除任意選中這個新功能就可以很好地應對上面場景,刪除不需要的節點資訊,只留下想探索的部分節點資料。

支援刪除任意選中功能

在實現這個功能之前,我先開始介紹下 D3.js 自帶 API。沒錯,還是上篇提及的 D3.js 的 enter() 及沒提到的 exit()

摘自文件的描述:

資料繫結的時候可能出現 DOM 元素與資料元素個數不匹配的問題, enterexit 就是用來處理這個問題的。enter 操作用來新增新的 DOM 元素,exit 操作用來移除多餘的 DOM 元素。
如果資料元素多於 DOM 個數時用 enter,如果資料元素少於 DOM元素,則用 exit
在資料繫結時候存在三種情形:

  • 資料元素個數多於 DOM 元素個數
  • 資料元素與 DOM 元素個數一樣
  • 資料元素個數少於 DOM 元素個數

根據文件描述,想實現刪除任意選中功能還是很簡單的,樂觀的筆者想當然地認為直接在資料層面進行操作就行。於是筆者直接在 nodes 資料裡刪除選中的節點資料 node,然後根據官方用法 d3.select(this.nodeRef).exit().remove() 移除多餘的元素,好的,我們現在來看看這樣做會帶來了什麼?

D3移除元素

不想選中的節點是刪除了,但其他節點的顯示也亂了,節點顏色和屬性同當前 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 表鼓勵啦~~ ?‍♂️?‍♀️ [手動跪謝]

交流圖資料庫技術?交個朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你進交流群~~

作者有話說:Hi,我是 Nico,是 Nebula Graph 的前端工程師,對資料視覺化比較感興趣,分享一些自己的實踐心得,希望這次分享能給大家帶來幫助,如有不當之處,歡迎幫忙糾正,謝謝~

相關文章