d3.js pack layout - cover

常看到網上精美的泡泡圖,想不想自己做做看?透過 D3.js ,不光是一般的泡泡圖,還可以做出各種不同的變化喔!這次就讓我們來看看如何利用 D3.js 製作基本的泡泡圖。

 

在將資料轉換為視覺化的過程中,「確定資料的排版與位置」這件事往往讓人感到很棘手。圖形是否重疊,點擊時的反應等等,往往讓視覺化變得很困難。

先前我們在「網頁視覺畫利器 - D3.js 入門簡介」一文中介紹了 D3.js 的基礎,其中提到 D3.js 可以分成兩個部份,包括 D3.js 核心 - 文件物件模型,以及強大的函式庫 - 各式各樣與視覺化有關的輔助函式。其中輔助函式庫的部份有一組很強大的 排版函式 (Layout Functions ),可以幫助我們快速的根據我們選定的排版策略計算出物件相對的擺放位置。

泡泡圖所使用的「Pack Layout」即是其中的一個。Pack Layout 利用階層性的結構把資料分類,並且在排版上使用資料數據中的「大小」來決定代表各個元素的圓圈大小,在下圖中大圓包小圓的結構圖解了這樣的階層性關係:

circle-packing

截取自 “Circle Packing“, D3.js example by mike bostock

若過於抽象,我們可以用公司架構圖這樣的結構來想像,最底層的每個點是員工,我們可以想像用員工薪水來表示圈圈的大小;上一層是各部門, 又是另 個更大的圈圈,以部門內員工薪水的總合來決定圈圈大小;再上一層是事業部、最上層則是整個公司,圈圈代表整個公司的人事成本。

製作泡泡圖

好吧,抽象的部份我們趕快把他帶過,馬上就先來進入實例吧!

準備資料

首先我們要準備好想呈現的資料。比方說,我們想呈現世界各國的人口數,從 Wikipedia List of Country Population 提供的訊息我們可以建出一張如下的 CSV 檔 ( 部份顯示,存檔為 population.csv ):

  country,value
  "China",1369550000
  "India",1270440000
  "United States",320892000
  "Indonesia",255461700
  "Brazil",204234000

為了讀取 CSV 檔,我們使用 d3.csv 函式,只要提供檔名跟資料處理器給他即可:

  d3.csv("population.csv", function(data) { ... });

讀取後的資料會放在 callback 函式的 data 陣列中,每個元素都代表  CSV 其中一行的值,我們可以透過 data[ n ].country 與 data[ n ].value 存取第 n 個物件的國名與人口數。

為了讓 D3.js  Pack Layout 能夠處理我們的資料,我們必須使用他所規範的資料結構;但只有一層的簡單泡泡圖並不需要用到 Pack Layout 所支援的多層結構,我們只要在 data 陣列外多包一層即可:

  var dataobj = { children: data };

計算排版

接著我們要利用 Pack Layout 算出各國圓圈的大小與座標。Pack Layout 的概念很簡單,我們把一個個物件倒進去,Pack Layout 便會幫我們在每個物件上標上「x」、「y」、「r」等變數,分別代表泡泡中心的 ( x, y ) 座標,以及泡泡的半徑 r 。

首先我們建立一個新的 Layout 物件:

  var pack = d3.layout.pack();

然後根據我們的需求做設定泡泡圖的長寬以及泡泡間的距離:

  pack = pack.padding(2).size([800,600]);

上例將泡泡之間的距離 ( padding ) 設定為 2,並且設定整張圖的大小為「寬 800 x 高 600 」;除了間距與畫布尺寸外,我們也可以設定泡泡半徑的計算方式或物件的取值方法,詳情可以參考 Pack Layout 的 API Reference

接著終於要來既計算座標 (x, y) 與 半徑 r 了!利用 dataobj 呼叫 pack.nodes 就可以:

  var nodes = pack.nodes(dataobj);

傳回值 nodes 是一個陣列,包含所有 D3.js 為我們算出來的圓圈。此例來說便包含了所有國家、國家對應泡泡的座標與半徑囉。例如,下面是印度的物件內容:

  {
    country: "India"
    depth: 1
    parent: Object
    r: 86.04375520326641    // 圓半徑
    value: 1270440000
    x: 412.45332064106776   // X 座標
    y: 107.01334969105733   // Y 座標
  }

由於 nodes 會包含一個所有國家總和的大圈,在使用之前我們必須將他過濾掉:

  nodes = nodes.filter(function(it) { return it.parent; });

做到這邊,泡泡圖也呼之欲出了。還記得「網頁視覺畫利器 - D3.js 入門簡介」中教大家的、如何連接資料與繪圖物件嗎?沒錯,使用 「data」 與「enter」函式:

d3.select("svg")
  .selectAll("circle")                 // 建立 circle 的 Selection
  .data(nodes)                         // 綁定 selection 與資料
  .enter()                             // 對於任何沒被對應而落單的資料 ...
  .append("circle")                    // 新增一個 circle 標籤
  .attr({
    cx: function(it) { return it.x; }, // 用 x,y 當圓心
    cy: function(it) { return it.y; },
    r : function(it) { return it.r; }, // 用 r 當半徑
    fill: "#ccc",                      // 填滿亮灰色
    stroke: "#444",                    // 邊框畫深灰色
  });

大功告成!結果會類似下圖:

Pack Layout - Bubble Chart Example1

目前得到的是一個單色的泡泡圖,我們可以再利用 d3.scale.category20 函式來為他上色。首先建立顏色轉換函式:

  var color = d3.scale.category20();

color 為一個很簡單的函式,隨意給他不同的值他便會給幫你挑個不同的顏色,最多 20 種。利用 color 取代前面設定顏色的地方:

   r : function(it) { return it.r; },
   fill: function(it) { return color(it.country); },
   stroke: "#444",

也可以另外為圓圈填上對應的國名:

  d3.select("svg").selectAll("text").data(nodes).enter()
    .append("text")
    .attr({
      x: function(it) { return it.x; },
      y: function(it) { return it.y; },
      "text-anchor": "middle",                    // 文字水平置中
    }).text(function(it) { return it.country; }); // 設定文字為國名

由於中間的點太小,很多國家的字都擠成一團了:

Pack Layout - Label Overlapped

我們將圓圈逆向排序 ( 這樣小圈才不會擠在一塊 ) ,然後只在人口數大的國家顯示國名:

  // 設定 Pack Layout 時追加排序設定
  pack = pack.padding(2).size([800,600]).sort(function(a,b) { b.value - a.value; });
  ...
  // 設定文字時判斷人口數
  }).text(function(it) { return (it.value>60000000?it.country:""); });

結果如下,是不是好多了呢?

Pack Layout - Reorder Circle

完整的程式碼已經放置在 github 上,可以在這個連結中找到。


結語

利用 D3.js 製作泡泡圖並不困難,不到 30 行的程式碼便能做出一個體面的結果;不過若光是一個簡單的泡泡圖,不見得非得用 D3.js 來做才行。我們之後會從這裡的例子出發,在後續的文章中深入研究使用 D3.js 做泡泡圖還可以做出什麼樣有趣的變化。


Written by infographics.tw

6 Comments

iron0336

你好,想請問一下,要怎麼取得population.csv檔案呢?

我在文中沒找到連結,在wiki也沒找到 ^ ^”

Reply
infographics.tw

您好, 我們當時沒有將 population.csv 留下來, 不過沒關係, 你可以將該段下方的資料存檔成 population.csv 做為範例來使用 ( 如下 ):
country,value
“China”,1369550000
“India”,1270440000
“United States”,320892000
“Indonesia”,255461700
“Brazil”,204234000

Reply
vvi

您好,我想請問一下,怎樣根據已有的數據設置圓的半徑大小?比如,您上面的例子,怎樣設置可以使得半徑的大小就是國家的人口數呢?

Reply
infographics.tw

泡泡圖應該要讓人口數與圓的面積成正比, 所以我們可以把人口數的根號值對應到半徑. 利用 d3js 的 scale 函式可以做到 ( d3.scale.sqrt ) . 比方說, 下例的 scale 函式可以將 0 ~ 100 的數值對應到 0 ~ 2 的半徑:

scale = d3.scale.sqrt().domain([0,100]).range([0,2]);

Reply
gratia

r : function(it) { return it.r; }, // 用 r 當半徑←請問這個r是怎麼產生的呢?看到index.js中沒這樣寫,但人口數仍然影響圓的面積

Reply

發表迴響

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