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

摘要: 在本文中,咱們將藉助 D3.js 的靈活性這一優點,去新增一些 D3.js 自己並不支持但咱們想要的一些常見的功能:Nebula Graph 圖探索的刪除節點和縮放功能。

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

D3顯示優化

前言

在上篇文章中(D3.js 力導向圖的顯示優化),咱們說過 D3.js 在自定義圖形上相較於其餘開源可視化庫的優點,以及如何對文檔對象模型(DOM)進行靈活操做。既然 D3.js 辣麼靈活,那是否是實現不少咱們想作的事情呢?在本文中,咱們將藉助 D3.js 的靈活性這一優點,去新增一些 D3.js 自己並不支持但咱們想要的一些常見的功能。前端

構建 D3.js 力導向圖

在這裏咱們就再也不細說 d3-force 粒子物理運動模塊原理,感興趣同窗能夠看看咱們的上篇的簡單描述, 本次實踐咱們側重於可視化操做的功能實現。java

好的,進入咱們的實踐時間,咱們仍是以 D3.js 力導向圖對圖數據庫的數據關係進行分析爲目的,增長一些咱們想要功能。node

首先,咱們用 d3-force 力導向圖來構建一個簡單的關聯網git

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

經過上述代碼,咱們能夠獲得下面這樣一個可視化的節點和關係圖。github

Nebula Graph Studio

這裏咱們簡單介紹下上圖,上圖爲圖數據庫 Nebula Graph 可視化工具 Studio 的圖探索功能截圖,在業務上,圖探索支持用戶任意選中某個點進行拓展,找尋、顯示同它存在某種關係的點,例如上圖點 100 和 點 200 存在單向 follow 關係。web

上圖數據量並不大,若是咱們在拓展時返回的數據量較大或多步拓展出來的數據逐步累加顯示,則會致使當前視圖頁節點和邊極多,頁面需呈現的數據信息量大,且也很差找到想要的某個節點。好的,一個新場景上線了:用戶只想分析圖中的部分節點數據,不想看到所有的節點信息。刪除任意選中這個新功能就能夠很好地應對上面場景,刪除不須要的節點信息,只留下想探索的部分節點數據。docker

支持刪除任意選中功能

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

摘自文檔的描述:api

數據綁定的時候可能出現 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 的前端工程師,對數據可視化比較感興趣,分享一些本身的實踐心得,但願此次分享能給你們帶來幫助,若有不當之處,歡迎幫忙糾正,謝謝~