1. 程式人生 > >高手才知道!七個你所不知道的 D3.js 祕技

高手才知道!七個你所不知道的 D3.js 祕技

D3.js ,當前最火紅的視覺化套件,你用過了嗎?越來越多人使用 D3.js 來開發視覺化專題,但… 你對 D3.js 的瞭解又到哪裡呢?這次我們就帶大家一起來看看一些 D3.js 很重要、大家卻又普遍不清楚的祕技與背後的設計邏輯。

製作 D3.js 動畫

transition 的名字

D3.js 提供 transition() 函式供我們作動畫,相當的方便,但是當我們需要多個動畫一起執行時,該怎麼辦?若我們對同一個物件呼叫的 transition 時間有重疊,後者會將前者完全取代掉,如下例我們根本不會看到紅色出現,只會看到邊框變色:

  d3.select("rect").transition().attr({ fill: "red" });
  d3.select("rect").transition().attr({ stroke: "green" });

如果我們改用串連呼叫的方式,兩者則會依序呈現,而非同時進行:

  d3.select("rect")
    .transition().attr({ fill: "red" })
    .transition().attr({ stroke: "green" });

事實上, transition 是可以命名的,而且只要將各個 transition 命名,他們就可以同時執行,不會互相取代。如下例,在呼叫 transition() 時以名稱為引數即可:

  d3.select("rect").transition("my-fill").attr({ fill: "red" });
  d3.select("rect").transition("my-stroke").attr({ stroke: "green" });

以下圖為例,左右兩個矩形各自以五個互相重疊的 transition 製作相同動畫,包含寬、高、填滿、邊框與邊線寬,左邊使用未命名的 transition 而右邊的 transition 各自有不同的名字。可以看到左方的動畫互相重疊導致大部份都無法完成,但右方的動畫則順利做完。

transition

這招在需要獨立對不同屬性做動畫時特別有用。

客製動畫

我們依靠 d3.transition 做動畫時,其底層是基於 d3.interpolate 內插函式在運作的,也因此我們所能做的動畫型別受到了他的限制。當我們想做更復雜的動畫時,我們當然可以自行利用 JavaScript 與瀏覽器所提供的 setTimeout 或 requestAnimationFrame 等函式來做,但這不僅繁瑣,動畫一多程式邏輯也複雜了起來。

d3.transition 其實提供了客製動畫的選項,我們可以利用 d3.transition.tween 函式來做!tween 函式接受一個 animation factory, 我們在裡面產生動畫處理器,並利用傳入的動畫進度引數來計算動畫值:

  d3.transition().tween("動畫名", function() {
    return function(progress)  {
      return progress; 
    };
  });

在上列程式碼中,紅色的函式會不斷的被呼叫,其引數 progress 則會由 0 至 1 不斷的被帶入, 0 代表動畫開始, 1 則代表動畫結束。在這裡面,我們可以應用各式各樣的視覺效果,例如根據 progress 來更新圓餅的比例,做出變大圓餅的效果:

  var arc = d3.svg.arc().innerRadius(0).outerRadius(50);
  d3.select("path").transition().tween("growth-pie", function() {
    return function(progress)  {
      d3.select(this).attr({
        d: function(progress) {
          return arc.startAngle(0).endAngle(progress * Math.PI * 2);
        }
      });
    };
  });

下圖即為利用 d3.transition.tween 做出的圓餅圖動畫,三個圓餅分別使用不同的 easing 函式:

tween

資料繫結與元素集合

繫結規則

用過 D3.js 的人都知道 D3.js 最核心的邏輯在於資料與元素的繫結,比方說下例我們將 1 ~ 5 的數字與 path 結合:

  d3.selectAll("path").data([1,2,3,4,5]);

這邊結合是照資料在陣列中的順序,也就是說若我們之後更新資料時、順序有變化,資料就不會綁到原先的元素上,而是綁到其它的元素上了。這有時會造成視覺呈現上的問題。該怎麼解決呢?

我們只要指定繫結的規則即可,在 data() 中再加上一個規則函式:

  d3.selectAll("path").data([1,2,3,4,5], function(it) { return it; });

該函式的傳回值即是繫結規則;當資料與元素有著相同規則時,兩者就會被結合。這類似資料庫系統 Table Join 時指定特定欄位做線索的概念。

階層元素

資料繫結後我們可以在設定樣式時使用:

  d3.selectAll("g").data([1,2,3,4,5]);
  d3.selectAll("g").attr({
    width: function(d,i) { return d * 100; }
  });

上例中,每個 <g> 得到紅色的 d 的數值都不一樣,分別會是 1 ~ 5 。那請問大家,如果 <g> 下面還有元素,該元素要怎麼利用 <g> 繫結的這個數字呢?比方說檔案的結構像是下面這樣:

  <g><rect/></g>
  <g><rect/></g>
  <g><rect/></g>
  <g><rect/></g>
  <g><rect/></g>

若我們利用 d3.selectAll(“g rect") 想要來設定 rect 的寬度 …. 事實上, D3.js 在讀取資料時,會用向上搜尋的方式尋找。在此例中,<rect> 並沒有與任何資料繫結,因此 D3.js 會向各個 <rect> 的父元素 <g> 詢問,這時因為 <g> 有繫結資料,於是這些資料就傳回給 <rect>  供作使用了。

屬性設定

Style v.s. Attr

因為 D3.js 與 SVG 的緊密結合,我們對 SVG 不夠了解的話有時會造成很大的困擾。其中一個問題是這樣的:SVG 元素可以用  CSS 設定樣式,例如:

  <rect style="fill:red"/>

將矩形用紅色填滿,看起來很棒是吧?但 fill 其實是 rect 的屬性之一,所以我們也可以這樣寫:

  <rect fill="green" style="fill:red"/>

請問此時這個矩形會是紅色還是綠色呢?此外,矩形的引數 rx 與 ry 可以設定矩形圓角,我們可以把他寫到 style 裡去嗎?如下:

  <rect fill="green" style="fill:red;rx:10;ry:10"/>

事實上,SVG 有所謂的「Presentational Attributes」,例如像是 fill 、 stroke 、 stroke-dasharray 等等的屬性;這一類的屬性可以放在 style 中,因此可以利用 CSS Selector 、 CSS Animation 來控制。然而,其它屬性像是 rx 、 ry 、 圓的 cx 、 cy 、 r 都是不能通過 CSS 控制的屬性。

接著,設定 Presentational Attributes 時, CSS 設定是優先於屬性設定的,因此若我們利用 CSS 對 <rect> 設定了紅色填滿,再用屬性設定綠色填滿,結果還是會得到紅色的矩形。若對這點不瞭解,有時利用 D3.js 操作屬性時就會碰到怎麼設定都不會動作的窘境:

  d3.selectAll("rect")
    .style({ fill: "red" })
    .attr({  stroke: "green" });

這點千萬要注意阿!

利用函式取值

在設定元素樣式時,我們可以利用函式來指定其值,例如下例中紅色的函式在要設定 <rect> fill 值時會被呼叫,其傳回值「red」則會被填入:

  d3.selectAll("rect").attr({ fill: function() { return "red"; });

事實上不光是設定屬性,D3.js 裡面很多放參數的地方都可以用函式替換,例如 d3.selectAll :

  d3.selectAll(function() { return "rect"; });

Encore: d3.scale 的神奇祕技

d3.scale 用來做座標轉換相當方便,他也可以將數值轉換成顏色,像是利用 d3.scale.category20 或是直接做線性內插,相當的方便:

  d3.scale.linear().domain([0,1]).range(["#f00","#0f0"]);

上例可以幫我們把 0 ~ 1 之間的值用內插的方式對換到紅色與綠色之間的值。好用吧!不過,這個大家都知道,不是我們要講的祕技。事實上, d3.scale 底層是使用 d3.interpolate 實現,所以 d3.interpolate 所能內插的東西, d3.scale 都支援,例如陣列:

  d3.scale.linear().domain([0,1]).range([[0,1],[1,0]]);

或者歌詞:

  d3.scale.linar().domain([0,1]).range([
    "5 little ducks, went out one day ....",
    "1 little ducks, went out one day ...."
  ]);

希望這沒有讓你的腦袋炸掉。

結語

D3.js 是個強大的視覺化函式庫,但顯然一般人只開發了他 10% 的可能性。做為作者 Mike Bostock 的畢業論文,這個工具顯然灌注了超乎我們想像的、更多的精力與設計在裡面,做為視覺化愛好者的我們若不好好了解 D3.js 的可能性、甚至把他當成一般的圖表工具的話,那就太對不起視覺化之神了 ( 誰? ) 。

讓我們一起往(視覺化)神乎其技的路上邁進吧!

D3JS相關係列教程