D3 + Canvas : Cover

「我的視覺化好慢!」使用 D3.js 作網頁視覺化最常與 SVG 一起運用,但向量化的本質讓 SVG 實用上有些許限制,最大的問題就出在效能上。因此,我們在這篇文章簡單的介紹如何運用 Canvas 來提升你的視覺化效率。

編按:這篇文章談到 D3.js 與 SVG,屬進階議題,尚未接觸過 D3.js 與 SVG 的讀者請參考下列兩篇文章:

非技術背景的讀者請斟酌閱讀。

 

D3.js 雖然是個相當強大的視覺化函式庫,但其實很大一部份我們看得到的元素都是靠 SVG 在表現。 SVG 做為一個向量圖形語言,與 D3.js 合作無間 - D3.js 負責計算、SVG 負責繪圖。一個常見的 SVG 圖形如下:

  <svg><circle cx="10" cy="10" r="10" fill="red"/></svg>

上例畫出了一個紅色的圓。D3.js 幫助我們將資料與 SVG 綁定,一筆資料綁定一個圓,十筆資料綁定十個圓,那麼問題就來了:一萬筆資料時,我們會有一萬個圓,若搭配 Force Layout 等版面繪製動畫時,你會明顯的感覺到速度很慢。

Canvas 簡介

接著談談 Canvas 吧, HTML5 的 Canvas 元素是一個 HTML 標籤:

  <canvas id="myCanvas" width="800" height="600"/>

他的特點在於一組附帶的 JavaScript API ,讓我們可以在上面利用程式繪圖,比方說這段程式碼可以畫個圓:

  var ctx = document.getElementById("myCanvas").getContext("2d"); // 取得 Canvas Context
  ctx.beginPath(); // 「我要開始畫囉!」
  ctx.arc(100,100,10,0,6.28);  // 畫一個圓弧,圓心在 (100,100) 且半徑是 10, 繞 360 度
  ctx.stroke(); // 替圓弧的邊著色!

相對於 SVG 「宣告式」的繪圖 ( 我宣告 (100,100) 的位置有個圓 … ) , Canvas 則是「指令式」的繪圖 ( 你給我以 (100,100) 為圓心畫個圓 ) 。指令式繪圖的速度跟繪圖的面積有關,但通常使用 Canvas 會比 SVG 快上相當多。考慮畫上一萬個圓, Canvas 只要加上一個迴圈:

  var ctx = document.getElementById("myCanvas").getContext("2d"); // 取得 Canvas Context
  for(var i=0; i<10000; i++) {
    ctx.beginPath(); // 「我要開始畫囉!」
    ctx.arc(100,100,10,0,6.28); // 畫一個圓弧,圓心在 (100,100) 且半徑是 10, 繞 360 度
    ctx.stroke(); // 替圓弧的邊著色!
  }

SVG 的一萬個 <circle>  顯然是比上例使用 Canvas 的繪製多出了許多負擔。

D3.js 與 Canvas

既然 D3.js 的本質是元素與資料的綁定,那要怎麼與沒有元素的 Canvas 結合呢?以下介紹三個可能的手法。

1. D3.js 原生的 Canvas 支援

有些 D3.js 函式提供直接將圖形畫到 Canvas 上的選項,通常這些函式處理的圖形也比較複雜。例如地圖的多邊形圖案:

Render Map with Canvas

from http://bl.ocks.org/mbostock/3783604

d3.geo.path 函式組提供了 context 函式,一但我們利用他設定了 Canvas ,接下來呼叫該 path 函式時他便會幫我們將圖形畫到 Canvas 上:

  var projection = d3.geo.mercator();  // 設定地圖繪製時使用的投影法
  var canvas = document.getElementById("myCanvas"); // 取得 canvas
  var path = d3.geo.path().projection(projection).context(canvas.getContext("2d"));
  path(topjson.feature(geodata, geodata.objects[...])); // 將 geodata 畫在 Canvas 上
  context.stroke(); // 替邊著色

上面那張圖 (來自 Mike Bostock 的 Gist ) 即是利用 d3.geo.path 與 canvas 繪製的,有興趣的朋友可以參考看看他的原始碼

2. 不使用資料綁定,單純使用 D3.js 的函式計算

舉例來說,我想要利用 Force Layout (可參考 D3.js 入門系列 - Force Layout 教學 一文) 來畫圓,我不一定要利用 SVG,也可以直接利用 Force Layout 的計算結果來畫到 Canvas 上:

  var data = {children: [{r: 10}, {r: 20}, {r: 30}, .... ]} // 這裡有若干個物件,其中的「r」代表圓的半徑
  var force = d3.layout.force().nodes(data).size([800,600])
      .on("tick", tick).start(); // 設定 Force Layout
  function tick() {
    for(i = 0; i < data.length ; i++) { // 幫 data 內每個物件畫個圓
      ctx.beginPath();
      ctx.arc( data[i].x, data[i].y, data[i].r, 0, 6.28 );
      ctx.stroke();
  }}

這樣一方面可以利用到強大的 D3.js 函式庫,也可以有 Canvas 的效能,不失為一種組合運用的好例子。缺點則在於無法利用 D3.js 核心的資料綁定功能,資料與繪製的程式邏輯沒辦法切得很開。

3. 建立虛擬的 DOM 元件,並實作元件繪製函式

我們也可以自己定義標籤,然後自己實作標籤的繪製函式。比方說,我們自行定義一個特殊標籤 <custom:circle> 來代表圓圈以及 <custom:root> 做為頂層容器,這些標籤瀏覽器不認得,我們也不打算讓瀏覽器看到。下面是將資料綁定到這組標籤的範例程式:

  var ctx = document.getElementById("myCanvas").getContext("2d");
  // 利用 infographics.tw 的網址做為 custom 的命名空間,並於其下建立 root 元素
  var root = document.createElementNS("http://infographics.tw/custom", "root")
  d3.ns.prefix.custom = "http://infographics.tw/custom" // 也把  custom 標籤的命名空間告訴 D3.js
  root.selectAll("circle").data(myData).enter().append("custom:circle") //綁定 custom:circle 與 myData
    .attr({ cx: ..., cy: ..., r: ... }); // 並設定圓心 (cx, cy) 與半徑 r 

  // 這裡負責把 custom:circle 畫出來
  d3.timer(function() {
    root.selectAll("circle").each(function() {
      ctx.beginPath();
      ctx.arc(this.getAttribute("cx"), this.getAttribute("cy"), this.getAttribute("r"), 0, 6.28);
      ctx.stroke();
    });
  });

這個方式雖然仍然建立了一個文件模型,實作上仍然比  SVG 快了近兩倍。下例是 SVG 與 custom:circle + Canvas 各別畫 2000 個圓的速度比較,左邊是 SVG,右邊是 Canvas,可以按下方的 Toggle 按鈕切換:

結語

D3.js 與 Canvas 的結合運用可說是相當進階的議題,而且比起視覺化來說更接近於程式設計或網頁開發的議題。然而這也是很現實無法逃避的問題:當你需要視覺呈現上萬個座標點的時候,你該怎麼辦?視覺化心法如果是內功,那視覺化技術就是外功。

SVG 與 Canvas 的議題其實還可以繼續延伸下去:作品需不需要考慮 RWD ? 是否要提供使用者圖檔下載?互動性的操作時 Canvas 要怎麼處理?這篇文章只是個引言,後續的議題將來若有機會,再逐一與大家討論。


Written by infographics.tw

發表迴響

你的電子郵件位址並不會被公開。 必要欄位標記為 *