初識 D3.js :打造專屬視覺化

vivo網際網路技術發表於2021-01-12

一、前言

隨著現在自定義視覺化的需求日益增長,Highcharts、echarts等高度封裝的視覺化框架已經無法滿足使用者各種強定製性的視覺化需求了,這個時候D3的無限定製的能力就脫穎而出。

如果想要通過D3完成視覺化,除了對於D3本身API的學習, 關於web標準的HTML, SVG, CSS, Javascript 和 資料視覺化的概念以及標準都是需要學習的。這無疑帶來了較高的學習門檻,但這也是值得的,因為掌握 D3 後,我們幾乎可以實現任何 2d 的視覺化需求。

本文通過對D3核心模組分析以及進行具體案例實踐的方式,來幫助初學者學習瞭解D3的繪圖思路。

二、D3是什麼

D3的全稱是 Data-Driven Documents(資料驅動文件),是基於資料來操作文件的 JavaScript 庫,其核心在於使用繪圖指令對資料進行轉換,在源資料的基礎上建立新的可繪製資料, 生成SVG路徑以及通過資料和方法在DOM中建立資料視覺化元素(如軸)。

相對於Echats等開箱即用的視覺化框架來說,D3更接近底層,它可以直接控制原生的SVG元素,並且不直接提供任何一種現成的視覺化圖表,所有的圖表都需我們在它的庫裡挑選合適的方法構建而成,這也大大提高了它的視覺化定製能力。而且D3 沒有引入新的圖形元素,它遵循了web標準(HTML, CSS, SVG 以及 Canvas )來展示資料 ,所以它可以不需要依賴其他框架獨立執行在現代瀏覽器中。

三、D3 核心模組

在V4版本後,D3的 API 現在已經被拆分成一個個模組,我們可以根據自己的視覺化需求進行按需載入。根據泛義可以將D3 API模組分為以下的幾大類:DOM操作、資料處理,資料分析轉換、地理路徑,行為等

這裡我們主要對 D3-selection 和 D3-scale 模組進行解析:

3.1  D3-selection

D3-selection(選擇集) 是 D3js的核心模組,主要是用來進行選擇元素,設定屬性、資料繫結,事件繫結等操作。

選擇元素:D3-selection 提供了兩種方法來獲取目標元素,d3.select():返回目標元素的第一個節點,d3.selectAll():返回目標元素的集合,乍一看有點類似原生API 的 querySelector 和 querySelectorAll,但是 d3.select 返回的是一個 selection 物件,querySelector 返回的是一個 NodeList 陣列。通過控制檯列印的資訊,可以看到 selection 下的 groups 存放了所有選擇的元素集合,parents 存放了所有選中元素的父節點。

設定屬性或者繫結事件:我們不需要關心 groups 的結構是怎麼樣的。當呼叫 selection.attr  或者 selection.style 的時候, selection 中的所有 group 的所有子元素都會被呼叫,group 存在的唯一影響是: 當我們傳參是一個function 的時候,例如 selection.attr('attrName', function(data, i)) 或 selection.on('click', function(data, i)) 時, 傳遞的 function(data, i) 中, 第二個引數 i 是元素在 group 中的索引而不是在整個 selection 中的索引。

資料繫結:實際上是給選擇的DOM元素的 \_\_data\_\_ 屬性賦值,這裡提供了3種方式進行資料繫結:

(1)給每一個單獨的 DOM 元素呼叫 selection.datum:d3.select('body').datum(20) 等價於 document.body.\_\_data\_\_ = 20

(2)從父節點中繼承來資料, 比如: append , insert , select,子節點會主動繼承父節點的資料:

(3) 呼叫 selection.data() 方法,支援傳入裝有基礎資料型別的資料,也支援傳入一個function(parentNode, groupIndex)根據節點索引與資料做對映,data()方法引入了 d3 中非常重要的 join 思想: 

繫結 data 到 DOM 元素, 在D3中是通過比較 data 和 DOM 的 key 值來找到對應關係的。 如果我們沒有單獨設定 key 值,那麼預設根據 data 的下標索引來設定,但是當資料順序發生改變,這個預設下標 key 值 就變得不可靠了,這時我們可以使用 selection.data(data, keyFunction) 中的第二個引數 keyFunction,根據當前的資料返回一個對應的 key 值。通過下面的圖例可以看出,不管是有一個還是多個 group(每個group 都是獨立的),只要我們保證在任意一個 group 中的 key 值是唯一的,資料一旦發生變化都會反映給對應的 DOM 元素( update 的過程):

3.2 Join 思想

上面提到的都是data資料和DOM元素數量相同的情況下的資料繫結,那如果data資料和DOM元素數量不相同時,我們來看看 D3 又是如何進行資料繫結的:現在終於可以來介紹 D3-selecion 模組的核心 Join 思想了,這個思想簡單來說就是 “不應該告訴D3去怎麼建立元素, 而是告訴D3,.selectAll() 得到的 selecion 集合應該和  .data(data) 繫結的資料要怎麼一一對應”。  

從上圖可以看出,在進行 d3.data(data) 資料繫結的時候,會產生三種狀態的選擇集:

  • Update:  已經和data資料繫結的DOM元素集合
  • Enter:data資料沒有找到與之對應的DOM元素集合(就是缺失的DOM元素)
  • Exit: 沒有被資料繫結的DOM元素集合(多餘的DOM元素)

用 Join 的方式來理解意味著,我們要做的事情僅僅是宣告 DOM集合和資料集合之間的關係, 並且通過處理三個不同狀態的集合 enter、update 、 exit 來描述這種關係。這種方式可以大大簡化我們對DOM元素的操作,我們不需要再用 if 和 for 迴圈的方式來進行復雜的邏輯判斷,來得到我們需要得到的元素集合。並且在處理動態資料的時候,可以通過處理這三種狀態,輕鬆的展示實時資料和新增平滑的動態互動效果。

3. 3 D3-scale

D3-scale(比列尺) 提供多種不同型別的比例尺。經常和 D3-axis 座標軸模組一起使用。

D3-scale 提供了多種連續性和非連續性的比例尺,總體可以將他們分為三大類:

  • 連續性輸入(domain)和連續性輸出(range)
  • 連續性輸入(domain)和離散性輸出(range)
  • 離散性輸入(domain)和離散性輸出(range)

常用的一些比例尺:

(1)d3-scaleLinear 線性比例尺(連續性輸入和連續性輸出)

可以看出,呼叫d3.scaleLinear()可以生成線性比例尺,domain()是輸入域,range()是輸出域,相當於將domain中的資料集對映到range的資料集中。

使用示例:

對映關係:

(2)d3-scaleTime 時間比例尺(連續性輸入和連續性輸出)

時間比例尺與線性比例尺類似,只不過輸入域變成了一個時間軸。正常我們使用比例尺都是個正序的過程,但是D3也提供了invert()以及invertExtent()方法,我們可以通過輸出域中的具體值得出對應輸入域的值。

使用示例:

(3)d3.scaleQuantize  量化比例尺(連續性輸入和離散性輸出)

量化比例尺是將連續的輸入域根據輸出域被分割為均勻的片段,所以它的輸出域是離散的。

使用示例:

對映關係:

(4)d3. scaleThreshold 閾值比例尺(連續性輸入和離散性輸出)

閾值比例尺可以為一組連續資料指定分割閾值,閾值比例尺預設的 domain:[0.5] 以及預設的 range:[0, 1] ,因此預設的 d3.scaleThreshold() 等價於 Math.round 函式。 閾值比例尺輸入域為 N 的話,輸出域必須為 N + 1,否則比例尺對某些值可能會返回 undefined,或者輸出域多餘的值會被忽略。

使用示例:

存在三種對映關係:

a. 當domain和range的資料是 N : N+1   

b. 當domain和range的資料是 N : N + 大於1         

c. 當domain和range的資料是 N + 大於0 :  N 

(5)d3.scaleOrdinal 序數比例尺(離散性輸入和離散性輸出)

與scaleLinear等連續性比例尺不同,序數比例尺的輸出域和輸入域都是離散的。

使用示例:

存在三種對映關係:

a.當domain和range的資料是一一對應     

b.當domain少於range的資料

c.當domain多於range的資料

四、實戰

通過以上的學習,應該對d3是如何操作DOM以及座標軸的資料對映為相應的視覺化表現有了一定的瞭解,下面我們來實際運用這兩個模組,來實現我們常見的視覺化圖表:柱狀圖。

(1)首先新增一個SVG元素。

(2)根據我們上面說到 d3.scale 模組以及 d3.axis 模組繪製座標軸,d3.scaleBand() 叫做序數分段比例尺,類似我們說的 d3.scaleOrdinal() 序數比例尺,但是它支援連續的數值型別的輸出域,離散的輸入域可以將連續的範圍劃分為均勻的分段。這裡再講一個細節,在繪製網格的時候,我們並沒有額外新增 line 元素來實現,而是通過 d3.axis 座標軸模組的 axis.ticks() 方法對座標軸刻度進行了設定,通過 tickSIze() 設定了刻度線長度,來模擬和圖表寬度相等的網格線,並且還可以通過 tickFormat() 對Y軸刻度值進行格式化轉換。

(3)座標軸繪製好了後,我們通過資料繫結來繪製與之對應的矩形(rect)元素了。

(4)這個時候柱狀圖已經基本繪製好了,我們再豐富內容展示,新增標籤、標題等提示資訊。

(5)最後我們通過給柱子繫結監聽事件,實現tooltips的資訊浮層互動。

五、總結

通過對 d3.selection 、d3.scale 以及 d3.axis等模組的學習,我們已經可以繪製出常用的柱狀圖等圖表,我們也可以通過d3提供的其他模組繪製出更加複雜的視覺化效果,例如通過  d3-hierarchy(層級模組) 實現層級樹圖視覺化,d3-geo(地理投影) 實現地圖資料視覺化等,本文講解的內容還只是D3庫的冰山一角。所以等我們掌握了D3後,限制我們實現視覺化的不再是技術而是想象力。

作者:Ray

相關文章