Force Layout - Cover看過人物關係圖嗎?說到 D3.js 與互動視覺化,你一定不能錯過的就是 Force Layout 了,這是一個利用引力與斥力計算物體位置的版面配置,經典的例子像是零時政府與歐巴馬年度的總預算視覺化、或是人物關係圖等等;而今天我們就來一探 Force Layout 的究竟。

在之前的「網頁視覺化利器 - D3.js 簡介」 一文中我們介紹了 D3.js 的基本概念,也提到 D3.js 提供了一個強大函式庫。裡面包含了數值轉換、地理操作、向量輔助等函式,而其中一個函式系列「 d3.layout 」更是任何使用 D3.js 的人都愛不釋手的一組函式,專門替我們計算視覺化的版面配置,如之前「D3.js 入門系列 - 泡泡圖製作教學」即提到了其中一種「Pack Layout」的配置方式,而 「Force Layout」也是其中的一個成員。

Force Layout 將視覺元素視為網路中一個個的節點,彼此有斥力、全體也有一個引力,隨著程式的運行,節點在各種力的交互作用下會逐漸收斂到一個穩定位置,很適合利用來呈現網路圖等需要動態配置顯示位置的圖表類型。

就最簡單的例子來說,我們先建立 100 個物件準備餵給 Force Layout:

  var nodes = [];
  for(var i = 0; i < 100 ; i ++ ) nodes.push({idx: i});

利用這 100 個物件建立對應的 SVG 標籤:

  var circles = d3.select("svg").selectAll("circle").data(nodes).enter().append("circle");

接著建立 Force Layout ,除了設定 Layout 範圍外,由於圖表是動態的,我們還要設置一個 tick 函式來對座標改變做圖表的更新:

  function tick() { // tick 會不斷的被呼叫
    circles.attr({
      cx: function(it) { return it.x; },  // it.x 由 Force Layout 提供
      cy: function(it) { return it.y; },  // it.y 由 Force Layout 提供
      r: 5,
    });
  }
  var force = d3.layout.force() // 建立 Layout
    .nodes(nodes)               // 綁定資料
    .size([800,600])            // 設定範圍
    .on("tick", tick)           // 設定 tick 函式
    .start();                   // 啟動!

這樣便好了。 Force Layout 會把計算出的 (x, y) 座標寫到我們提供的物件中,我們只要使用即可 ( 如上段程式碼中 tick 函式所做的事 ) 。這段程式碼的成果大約如下:

基本變化

光是畫一些黑點出來當然並不是很有趣,我們先來嘗試一些簡單的變化。首先為圓圈加入不同的半徑:

  var nodes = [];
  for(var i = 0; i < 100 ; i ++ ) nodes.push({
    idx: i, 
    r: parseInt(Math.random()*10 + 2) // 半徑範圍: 2 ~ 12
  });

繪圖時使用簡單的紅到綠對應來上色:

  var color = d3.scale.linear().domain([2,12]).range(["#090","#f00"]);
   function tick() {
    circles.attr({
      r: function(it) { return it.r; },
      fill: function(it) { return color(it.r); }
      ....
    });
  }

結果看起來不錯!

除了 Force Layout 本身的座標計算以外,我們也可以自行更改各個節點的座標來干涉節點配置的位置。下例試著在每次 tick 中對所有節點的 Y 軸加入一個加速度,並設定 Y 軸 (下方) 的邊界:

function tick() {
  for(var i = 0; i< nodes.length; i++) {
    nodes[i].vy += 0.3; // 加速度 - 向下 0.3
    nodes[i].y += nodes[i].vy;
    if(n.y + n.r >= 300) {  // 不超過 y 軸 300 的位置
      n.y = 300 - n.r;
      n.vy = -1 * Math.abs(n.vy);
    }
    n.vy = n.vy * 0.9; // 速度因空氣阻力(!?)而不斷減少
}}

結果如同下例,我們可以看到許多點下墜彈跳的同時,圓圈也因為 Force Layout 的效果而往兩側散開。

亦或者是限制節點門在一個圓環上:

或是送讀者一個愛心:

這些例子的程式碼都可以在 infographics.tw github 找到,有興趣可以去研究一下。

最後一個例子, Force Layout 除了處理節點間的斥力外,我們也可以指定哪些節點應該要連接在一起。我們將圓圈隨意排放後任意連接並使用 Spline 接上,可以看到尚未執行前真是亂成一團,但開始執行以後由於連接的節點彼此靠攏,漸漸的變得整齊了起來:

連線部份與 Force Layout 相關的程式碼範例如下,繪圖的部份若不使用 Spline 則大同小異,這邊先略過:

  var links = [];
  for(var i = 0; i < 59 ; i++) links.push({source:nodes[i], target:nodes[i+1]});
  force.nodes(nodes).links(links).on("tick",tick).start();
  d3.select("svg").selectAll("path").data(links) .... (後略)

結語

這次為大家介紹了如何使用 Force Layout ,但並沒有一個很具體的實用範例;大家可以參考一些線上已經有的例子,例如捷運各站出站人數圖或是歐巴馬的 2013 總預算視覺化。捷運出站人數圖利用 Force Layout 將捷運圖以保持大致相對位置的方式顯示於圖上,而預算視覺化則利用 Force Layout 在預算泡泡圖中做更有效率的資料分類,都是很不錯的應用範例,將來有機會再為讀者們寫專文介紹。

順道一提, Force Layout 為了提升運算效能,使用了 d3.geom.quadtree 以及 Verlet Integration Algorithm ,這裡牽扯到比較深的數學與演算法概念,有興趣的讀者不妨深入的了解這些東西的細節,都相當有意思喔!


Written by infographics.tw

2 Comments

TonyQ

Good post.

只是這個範例好像有點小小的 typo ? tick 中 cx 出現了兩次,我猜應該是要寫 cy ? 不過瑕不掩瑜。

function tick() { // tick 會不斷的被呼叫
circles.attr({
cx: function(it) { return it.x; }, // it.x 由 Force Layout 提供
cx: function(it) { return it.y; }, // it.y 由 Force Layout 提供
r: 5,
});
}
var force = d3.layout.force() // 建立 Layout
.nodes(nodes) // 綁定資料
.size([800,600]) // 設定範圍
.on(“tick", tick) // 設定 tick 函式
.start(); // 啟動!

Reply

發表迴響

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