iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 27
1
Modern Web

React + D3 的正確姿勢系列 第 27 篇

Day27-bubble chart(using D3.js)

  • 分享至 

  • xImage
  •  

前言

今天要來介紹本系列文最後一個圖表了,由於鐵人賽時間真的很有限沒辦法介紹全部的圖表,但筆者已經盡量把最常用的幾種圖表都介紹給大家了,相信大家學會這幾種圖表一定就可以應付日常需求了,今天要介紹的是氣泡圖這個圖表。

氣泡圖

氣泡圖是由各種大小不一的圓圈組成,由於各式各樣的圓圈看起來很像泡泡所以就把這種圖表稱之為氣泡圖,氣泡圖適用在描述同種類型的關聯性,這麼說可能會有點抽象所以筆者用個例子來說明。

不曉得大家有沒有用過 Apple Music ,在 Apple Music 的初始設定中,系統會詢問使用者喜歡什麼類型的音樂,音樂其實涵蓋的類型很大,有古典樂、流行樂、重低音樂等等,所以這時候 Apple Music 會請使用者點擊喜歡的音樂類型氣泡,氣泡越大就代表該使用者最喜歡那種類型的歌,像下圖這樣:

即便音樂涵蓋的類型非常多,透過氣泡圖的方式我們就可以很快的了解該使用者最喜歡什麼類型了,所以當今天圖表希望可以呈現的現象是想表達同一種物品但不同類型的關聯性,這時候就很適合使用氣泡圖,用更白話的說法就是今天想要比較資料的關聯性,使用氣泡圖就對了。

pack

在開始進入繪製流程之前,筆者要先跟大家介紹兩個在繪製氣泡圖中非常重要的觀念,首先是 d3.pack() , d3.pack() 是 D3 用來設計 階層性的 Layout ,透過 d3.pack() 我們就可以很順利的把資料包裝成同一個階層,各位讀者可以這樣想像,氣泡圖要求的是關聯性,今天如果沒有把資料都打包在一起,這個氣泡圖就很難畫出這些資料的關聯性了,而 d3.pack() 的寫法也很簡單就像底下這樣:

let pack = d3
  .pack()
  .size([width, height])

上面可以看到多設定了 pack.size() ,這個 pack.size() 就是用來表示該階層要佔多大的範圍,為了讓讀者更能理解這個範圍,筆者用 width 以及 height 的方式讓大家知道這邊是要填入 pack 的寬高。

hierarchy

講完如何打包階層之後再來就是要提到階層最重要的觀念: hierarchy 了, D3 針對 hierarchy 的部分有設計了一套 API 叫 d3.hierarchy() ,我們都知道 hierarchy 非常注重 父節點(Parent Node) 與 子結點(Child Node) 之間的關係,但氣泡圖其實非常單純,我們只需要釐清最高的 Root Node 與底下的 Child Node 即可。

講完氣泡圖 hierarchy 的觀念後接下來就進入實作啦!其實 D3 都幫我們寫好 hierarchy 的實作了,我們只需要呼叫 d3.hierarchy() 並且把資料傳進去,這個 API 就會自動幫我們 parse 整個陣列中的資料,之後就可以透過 hierarchy.data 的方式取得 hierarchy 後的物件。

const data = [
  {
    title: '日本流行樂',
    likes: 40,
  },
  {
    title: '古典音樂',
    likes: 10,
  },
]

let rootNode = d3.hierarchy(dataObj)
const hierarchical_data = rootNode.data

但這種寫法其實不太好,因為原始物件全部的資料都會被塞進去 data 裡,這樣就很難取得真正要傳入圖表的資料,所以 D3 有特別準備一個 key 叫 children ,只要把真正需要傳入圖表的資料加到 children 這個 key 內,之後就可以透過 hierarchy.children 的方式取得縮小範圍後的資料了,整體寫法就像下面這樣:

const data = [
  {
    title: '日本流行樂',
    likes: 40,
  },
  {
    title: '古典音樂',
    likes: 10,
  },
]

const dataObj = {
  children: data
}

let rootNode = d3.hierarchy(dataObj)
rootNode = rootNode.children

繪製流程

今天的氣泡圖就來試著按照 Apple Music 的介面設計看看吧!由於筆者真的沒有設計美感所以最終結果不可能會百分百一樣但應該也有 87% 像了XD

第一步相信大家都知道要做什麼了,一樣也是設定好 svg 容器。

const width = document.querySelector(`#${root}`).clientWidth,
    height = width

const svg = d3
  .select(document.querySelector(`#${root}`))
  .append('div')
  .attr('class', 'bubble')
  .style('display', 'block')
  .append('svg')
  .attr('width', width)
  .attr('height', height)

再來就是設定 pack 的部分了,寫法就跟上面提到的差不多。

let pack = d3
  .pack()
  .size([width, height])

設定完 pack 再來就是要處理 hierarchy 了,這邊要記住多設定一個 children 這個 key 喔!

let dataobj = {
  children: bubbleDatas,
}

let rootnode = d3.hierarchy(dataobj)

解決了 hierarchy 接下來就是要進行打包的動作啦!其實在變成 Pack Layout 之前可以再針對剛剛設定好的 hierarchical data 進行操作,這邊筆者為了讓氣泡由大到小排序以及讓 D3 算出每個氣泡需要多大的大小,所以會將 hierarchical data 進行 sum 以及 sort 的動作,最後再把 children 的資料來出來就可以正式傳入圖表中了。

let nodes = pack(
  // use song likes as sum value and sort likes
  rootnode
    .sum(d => d.likes)
    .sort((a, b) => b.data.likes - a.data.likes)
)

nodes = nodes.children

所有設定都寫好後再來就是開始畫圓啦!畢竟氣泡圖就是一堆圓圈嘛XD

筆者習慣用一個 group 把全部的氣泡包裝起來,這樣日後想要針對泡泡做樣式的修改也比較好改,這個也是在設計 D3 圖表中常用到的一個小技巧,分享給各位讀者知道~

// all circles
const circles = svg
  .selectAll('.node')
  .data(nodes)
  .enter()
  .append('g')
  .attr('class', 'node')

circles
  .append('circle')
  .attr('cx', d => d.x)
  .attr('cy', d => d.y)
  .attr('r', d => d.r)
  // use title name to generate random color
  .attr('fill', '#fb3131')
  // circle border
  .style('stroke', '#d43d5e')
  // circle border width
  .style('stroke-width', 3)

為了讓最終的呈現跟 Apple Music 有 87% 像,所以筆者在這邊多加一個 text 把資料的標題傳進去泡泡內,並且把 text 的位置設定在中間。

circles
  .append('text')
  .attr('x', d => d.x)
  .attr('y', d => d.y)
  .attr('dy', '.5em')
  .attr('text-anchor', 'middle')
  .attr('fill', '#ffffff')
  .text(d => d.data.title)

組合起來

最後筆者有把上面流程所講的程式碼都丟到 GitHub 上,有興趣想要參考的讀者歡迎上去參考這些範例碼。

bubble chart

總結

今天介紹了氣泡圖,相信大家對於 D3 對資料進行階層化打包有了初步的印象了,也知道未來只要遇到跟關聯性有關的圖表都可以利用氣泡圖進行視覺化的呈現。

如果對於文章有任何問題歡迎在下面留言給我,沒問題的話明天就要正式結合 React 以及 D3 了。


上一篇
Day26-donut chart(using D3.js)
下一篇
Day28-React + D3(一)
系列文
React + D3 的正確姿勢 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
effytseng
iT邦新手 5 級 ‧ 2022-09-01 00:37:43

您好:
我想請問,您文章內的範例程式:

// use title name to generate random color
  .attr('fill', '#fb3131')

從註解看起來是可以利用 title name 去隨機生成顏色,
但是 code 看起來是統一填成 #fb3131

不知道如果想利用 title name 去隨機生成顏色是否有範例 code 可以參考呢?
謝謝您~
文章獲益良多,感謝

我要留言

立即登入留言