iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Modern Web

三十天成為D3.js v7 好手系列 第 9

Day9-D3繪圖:繪製形狀的Helper Functions

  • 分享至 

  • twitterImage
  •  

本篇大綱:Generator、Component、Layout

截至目前,我們已經學會 D3 如何將資料與DOM 元素綁定來呈現資料視覺化,也知道要怎麼將資料整理成想要的內容,接下來我們就要看看 D3 如何建構圖形囉!

看到這邊大家心裡可能會有個疑問:不是用 SVG 去建構圖形就好嗎?為何 D3 還要設計其它建構圖形的API 呢?

那是因為我們之前提到 SVG 提供的內建幾何圖形(圓形、矩形、線條、路徑等)其實只是小小的集合,一張完整圖表則是由幾百個這些小元件組成的複雜集合,如果只使用 SVG 提供的圖形,就需要很辛苦地一個一個建立圖形,才能完成一張圖表。為了省去這個麻煩,D3 創建了很多不同的 API 來協助建構複雜圖形/圖表,這些 API 們也因此便被稱為 helper functions。

這些用來協助繪製圖型的 helper functions 可以依照使用的資料複雜度產生出來的結果來劃分為三大類:

  • Generator:產生 < path > 的d標籤路徑
  • Component:產生 DOM 元素
  • Layout:產生整張圖表

我們先看到下面這張圖表
https://ithelp.ithome.com.tw/upload/images/20210921/20134930umP8TEOFl9.jpg

看完不太懂也沒關係,下面我們會分別講講這三大類 helper functions 的特性~

Generators

這一大類的 API 們是 D3 裡面最基本的方法~主要是透過使用基礎的資料集(像是array、number等等),來產生繪製 svg <path>需要的命令列字串(d)

我們在 Day3-SVG 那篇有提到,如果我想在 svg 使用 < path > 去繪製一條線的話,需要透過 d 屬性與屬性值去設定這條線的位置

  <path
    d="M50 20 C80 90,40 200,250,100"  // <==就是這個傢伙
    stroke="black"
    fill="none"
    stroke-width="2"
  />

但 d 屬性值的這一大串英文+數字基本上很難用人力去自行換算,因此我們就要借助 d3 的API去計算。

d3.line( )

我們先以 d3.line( ) 來示範:一樣先到官網看看 d3.line( ) 有哪些API可以使用
https://ithelp.ithome.com.tw/upload/images/20210921/20134930pilhKAzWc5.jpg

接著來看看 d3.line( )官方的解說,瞭解可以帶入那些參數、哪些方法必須搭配使用
https://ithelp.ithome.com.tw/upload/images/20210921/20134930OpMpqDX0sB.jpg

從官網的解說得知,d3.line() 可以帶入兩個參數來進行運算:分別是 x 跟 y 值,而這兩個參數可以是數字或是方法
https://ithelp.ithome.com.tw/upload/images/20210921/20134930CACJaaHJuW.jpg

https://ithelp.ithome.com.tw/upload/images/20210921/201349308jBB77rj2k.jpg

舉例來說,假設我們手上有一筆資料,想把它換算成 需要的 d 屬性值

// Line Generator
const data1 = [{x:10,y:10},{x:20,y:10},{x:30,y:10},{x:40,y:10},{x:50,y:10}]

一開始先用 line( ) 來設定方法

const line = d3.line()
               .x(d=> d.x) // 設定x值要抓哪些資料
               .y(d=> d.y) // 設定y值要抓哪些資料

設定好方法後,我們再將手上的這筆資料帶進去,就能得到想要的值了

line(data1) // 帶入要換算的資料,得到"M10,10L20,10L30,10L40,10L50,10"

取得可以用在d屬性上的值後,最後就是把這些資料綁訂到指定的 DOM 元素上面啦~

// html
<svg class="line"></svg>

// js
d3.select('.line')
  .append('path')
  .attr('d', line(data1))
  .attr('stroke', 'black')
  .attr('stroke-width', '2')
  .attr('fill', 'none')

成功產出線條!
https://ithelp.ithome.com.tw/upload/images/20210921/20134930nIoVJLaTHY.jpg

是不是很簡單呢? Generator 這類的方法主要就是在做這些事: 將資料換算成繪製 svg 需要的 code。這一類常見的API 包含:line ( )、arc ( )、area( )、symbol ( ) 等等,都是將資料換算並產出 < path > 的d屬性值,再將值套到 DOM 元素上去繪製圖型。我們再來多看幾個 Generator 的範例吧!

d3.area( )

一樣上[官網](https://github.com/d3/d3-shape/blob/v3.0.1/README.md#area)去看解說,得知d3.area( ) 有三個必須要帶的參數,分別是
API 解釋
area.x( ) x的座標
area.y1( ) y軸做邊
area.y0( ) 開始繪製區域的y軸範圍

知道應該要帶那些參數後,我們就開始繪製區域圖案吧!我們手上有的資料是 data1,一樣使用 d3.area( ) 先設定 area 這個方法

// html
<svg class="area"></svg>

// js
const data1 = [{x:10,y:100},{x:20,y:100},{x:30,y:100},{x:90,y:20},{x:220,y:10}]
const area = d3.area()
                   .x(d=>d.x)
                   .y1(d=>d.y)
                   .y0(10)

area(data1) // 呼叫方法並帶入資料,得到 M10,100L20,100L30,100L90,20L220,10L220,10L90,10L30,10L20,10L10,10Z

得到 d 的屬性值後,我們就可以將這個資料帶進選定的 DOM 元素啦

d3.select('.area')
  .append('path')
  .attr('d', area(data1))
  .attr('stroke', 'blue')
  .attr('fill', 'blue')

登登登~成功得到一個填滿區域的圖型!
https://ithelp.ithome.com.tw/upload/images/20210921/20134930hRuPMFs5f0.jpg

這邊只是基礎的介紹跟使用,等之後與 scale( )、axis( ) 等其他方法結合後,就可以使用 area( ) 去繪製出類似下方的圖表,是不是很好看呀~
https://ithelp.ithome.com.tw/upload/images/20210921/20134930Y26tPdcoJS.jpg

d3.arc( )

最後再來看到另一個 Generator 中很用到的API — d3.arc( ),這個 API 主要是用來畫弧線,它通常會跟 pie( ) 這個 API 結合繪製圓餅圖。但它還能搭配其他API 畫另外一種很酷炫的圖,猜得到是什麼圖表嗎?

就是「車子的儀錶板」!
https://ithelp.ithome.com.tw/upload/images/20210921/201349300cRuctYx78.jpg

是不是很酷呀?很想知道要怎麼畫嗎?先別急,我們先來看看要怎麼使用 d3.arc( )。一樣先看到官方文件,得知要使用 d3.arc( ) 需要搭配另外四個 API

參數 解釋
arc.innerRadius( ) 內圈範圍
arc.outerRadius( ) 外圈範圍
arc.startAngle( ) 起始角度
arc.endAngle( ) 終點角度

官方文件上對於 arc( ) 跟它的定義也寫得很清楚

  • arc ( ) 這個API 是用來繪製弧形
  • 弧形的中心點永遠 [0 , 0],亦即 畫面左上角,如果想要移動這個弧形就用 transform 去處理
  • 弧形角度由 .startAngle( ) 跟.endAngle( ) 去控制,當start 跟 end 的角度加起來大於或等於 τ (也就是2π) 的時候,就會形成一個完整的圓型
  • .startAngle( ) 起始角度從指針12點鐘方向開始,順時針畫圓弧
  • pie ( ) 這個API 會將資料陣列換算成 arc( ) 需要的 start 跟 end 角度,所以 arc( ) 通常跟 pie( ) 搭配使用

https://ithelp.ithome.com.tw/upload/images/20210921/20134930P9wYEocqIc.jpg

了解這些後,我們就可以直接來使用 arc( ) 啦!

<svg class="arc"></svg>

// js
const arc = d3.arc()
              .innerRadius(40)  // 內圈範圍40
              .outerRadius(50)  // 內圈範圍50
              .startAngle(0)
              .endAngle(Math.PI*0.5) // 畫一個 1/4 的圓形

d3.select('.arc')
      .append("g")
      .attr("transform", "translate(100,100)")  // 把整個圓弧移動到 100,100 的位置
      .append('path')
      .attr('d', arc())
      .attr('stroke', 'blue')
      .attr('fill', 'blue')

這樣就能得到 1/4 個半圓弧啦~
https://ithelp.ithome.com.tw/upload/images/20210921/2013493092sr4JDij6.jpg

因此,如果想要畫出方向盤的圓弧,只要調整一下 start angle 跟 end angle;想改變圓的大小則是調整 innerRadius 跟 outerRadius

// arc
    const arc = d3.arc()
                  .innerRadius(60)
                  .outerRadius(65)
                  .startAngle(Math.PI*1.2)
                  .endAngle(Math.PI*2.8)

    d3.select('.arc')
      .append("g")
      .attr("transform", "translate(150,80)")
      .append('path')
      .attr('d', arc())
      .attr('stroke', 'blue')
      .attr('fill', 'blue')

https://ithelp.ithome.com.tw/upload/images/20210921/20134930ZBvP6fCjZz.jpg


Components

接下來我們來講講第二類 helper functions — Components。上面提到 Generators 只建立給 < path > 用的d 的命令指令,而 Components 則完全相反。這一大類的 API 們會使用回傳的方法去建立一整組圖形物件,供給特定的圖表使用。就拿這一大類中最被使用的 d3.axis( ) 來舉例好了,.axis( ) 會接收 scale( ) 回傳的方法,接著繪製出 < line >, < path >, < g > 與 < text > 等等的一堆元素,一起組成一組軸線。我們實際來看看例子會更清楚:

axis ( )

一樣先開官方文件來看看有哪些 API 可以用!
https://ithelp.ithome.com.tw/upload/images/20210921/20134930hicktTgwHa.jpg

知道有這些API 可以使用之後,我們來看看 axis( ) 的解說~官方文件上其實就是簡單短短的一行

axis( ) 用來將 scale( )的資料轉換成人類能看得懂的文字,讓這個最無趣的任務變得輕鬆簡單

沒錯,就是這麼簡單!因為當我們想使用 d3 建構圖表時,要先用 scale( ) 把數據資料換算成符合比例、能夠讓d3讀懂的數值,才能把這些數值傳換成圖表;而axis( ) 唯一的任務就是把 scale( ) 換算好的值,再轉成人類看得懂的文字,最後繪製成軸線。

這邊我們就來實際繪製一條X軸線看看吧:

// 我目前的資料集
const data1 = [{x:10,y:100},{x:20,y:100},{x:30,y:100},{x:90,y:20},{x:220,y:10}]

// 抓出 x 軸要使用的值
const xData = data1.map((i) => i.x);

// 設定X軸的比例尺與繪製範圍
const xScale = d3.scaleLinear()
                 .domain([0, d3.max(xData)])
                 .range([10, 290]);

//使用xScale的設定,繪製刻度(ticks)朝下的軸線
const xAxis = d3
    .axisBottom(xScale)

// 呼叫軸線
d3.select('.axis').append("g").call(xAxis);

透過這幾個步驟,我們就能畫出一條寫有刻度的軸線啦!
https://ithelp.ithome.com.tw/upload/images/20210921/201349303NzYaXfL5j.jpg

看到這邊你可能會大喊:寫錯了!X軸應該在下面才對!賠錢、還我時間!!

先別激動,這是因為我們之前說過,svg 的原點都在左上角,依序由上至下、由左至右建構圖形,所以這邊的X軸會飄在上方也是合情合理、完全沒錯。那要怎麼讓它導邪歸正,做回一條正常的X軸線呢?一樣就要派 transform 上場啦~

d3.select('.axis')
  .append("g")
  .call(xAxis)
  .attr("transform", "translate(0,130)")  // 調整X軸位置

透過修改 DOM 元素的 transform 值,我們就能將 X 軸移動到任何想要的位置上~
https://ithelp.ithome.com.tw/upload/images/20210921/20134930rheMbJ4peg.jpg

這邊由於篇幅的關係,axis( ) 就先介紹到這邊,等到後面專門講軸線的章節時,會有更詳細的解說。

看完 axis( ) 的例子後,有清楚 Components 這類的 API 在做什麼了嗎? 除了 axis( ) 之外,brush( )、zoom( ) 也都歸納在 Components 這一類 Helper Functions 內。後面也會有專門的章節來講這兩個 API,有興趣的人可以訂閱按讚開啟小鈴鐺


Layouts

看完前面兩類 Helper Functions 之後,我們來看看最後一大類 Helper Functions 吧!Layouts 比起 Generator、Components 又更進階,這類的 API 是直接拿 一整個完整的資料集去繪製完整的圖表,這類的 API 可以很直覺簡單,例如:pie( ) 繪製的圓餅圖;也可以很複雜,例如:force( ) 繪製的原力關聯圖。

Layouts 需要的完整資料集有可能是多個陣列,也有可能是 Generators 類的 API 產生的資料,它會使用資料集去計算像素座標與角度,常見的API 有 stack( )、pie( )、force( ),下面我們就用 stack( ) 來實際演練看看吧!

stack( )

stack( ) 這個 API 主要用來畫長條堆集圖,但還得要搭配 scale( )、axis( ) 等API 才能畫出下面的圖表。等到後面章節講完其他必要的API後,會再帶大家實際繪製圖表,今天就先稍微了解 stack( ) 在幹嘛就好
https://ithelp.ithome.com.tw/upload/images/20210921/20134930mky479MMk3.jpg

我們先看到 官網 上列出 stacks 有哪些 API 可以使用,以及它們的功能是什麼
https://ithelp.ithome.com.tw/upload/images/20210921/20134930Ou8vS3cTk0.jpg

接著我們來看看 d3.stack( )的解釋
https://ithelp.ithome.com.tw/upload/images/20210921/201349301KqgcXvM9U.jpg

文件上寫著當我們使用 d3.stack( ) 帶入一個資料陣列時,這個 API 會返還另一個代表每筆資料集的陣列,而這些資料集是用 keys 去決定的。看完後是不是覺得有看沒有懂呢?沒關係很正常(看得懂的是神人),我們來一一解說吧!

由於d3.stack( ) 大多被用來繪製長條堆積圖,因此很重要的一點就是:要把哪些資料歸為同一集合?假設我目前有一系列的資料,紀錄2021年1~4月,中國、美國、台灣的每月肺炎確診人數

const dataStack = [
      {month: new Date(2021, 0, 1), China: 32, America: 20, Taiwan: 30},
      {month: new Date(2021, 1, 1), China: 7, America: 27, Taiwan: 18},
      {month: new Date(2021, 2, 1), China: 13, America: 33, Taiwan: 18},
      {month: new Date(2021, 3, 1), China: 6, America: 18, Taiwan: 20}
    ];

目前的資料是:一個陣列內含四個物件,這四個物件內分別有 month、China、America、Taiwan 四個 key 值,而這些正是 d3.stack() 所需要的 keys。

當我們使用d3.stack() 時,它會根據資料的 key 值把資料分類,同樣 key 值的數據會被視為同一個集合,因此這邊就有四個集合

  1. month [new Date(2021, 0, 1)、new Date(2021, 1, 1)、new Date(2021, 2, 1)、new Date(2021, 3, 1)]
  2. China [32、7、13、6]
  3. America [20、27、33、18]
  4. Taiwan [30、10、18、20]

以這個例子來說明,我們先使用 d3.stack( ) 定義一個叫做 stack 的方法,設定要建立堆疊圖的資料 keys 分別是"China"、 "America"、 "Taiwan",接著再設定一個變數stackedSeries,這個變數是把 stack 方法帶 dataStack 的資料後得到的數值

const stack = d3.stack()
                .keys(["China", "America", "Taiwan"]) // 設定資料的keys

const stackedSeries = stack(dataStack); // 把資料帶入stack方法

因為我們設定的keys有三項,因此 d3.stack() 會把同一個 key 的數值視為同一集合(series),就像這樣
https://ithelp.ithome.com.tw/upload/images/20210921/20134930gJOCIXC945.jpg

接著使用這些集合去計算並各自返還一個陣列
https://ithelp.ithome.com.tw/upload/images/20210921/20134930UWlJM0mPoz.jpg

每個集合中有幾筆資料,就會一一返還對應的陣列
https://ithelp.ithome.com.tw/upload/images/20210921/20134930teW4TFMyGo.jpg

我們把返還的陣列展開,看看裡面到底是放什麼資料~展開後我們會看到每個陣列都包含三筆資料,分別代表

  • 起始值
  • 終點值
  • 隸屬哪個物件
    https://ithelp.ithome.com.tw/upload/images/20210921/20134930CnaZZQMlMg.jpg

這樣一來我們就得到需要的資料了,可以運用這些起始值終點值來繪製堆集圖啦!

// stack
    const dataStack = [
      {month: new Date(2021, 0, 1), China: 132, America: 80, Taiwan: 30},
      {month: new Date(2021, 1, 1), China: 67, America: 27, Taiwan: 188},
      {month: new Date(2021, 2, 1), China: 123, America: 153, Taiwan: 18},
      {month: new Date(2021, 3, 1), China: 27, America: 112, Taiwan: 20}
    ];

    const stack = d3.stack()
                    .keys(['China', 'America', 'Taiwan'])

    const stackedSeries = stack(dataStack);
    console.log(stackedSeries)

    // 顏色
    const colorScale = d3.scaleOrdinal()
                         .domain(['China', 'America', 'Taiwan'])
                         .range(["red", "blue", "orange"])

    // 建立集合元素g、設定顏色
    const g = d3.select('.stack')
                .attr('width', 300)
                .selectAll('g')
                .data(stackedSeries)
                .enter()
                .append('g')
                .attr('fill', d => colorScale(d.key));
    
    // 繪製長條圖
    g.selectAll('rect')
      .data(d=>d)
      .join('rect')
      .attr('width', d => d[1] - d[0]) // 長度為終點值減掉起始值
      .attr('x', d => d[0]) // x 座標設定為起始值
      .attr('y', (d, i) => i *30) // y 座標用 index 來處理,乘上每條bar想拉開的距離
      .attr('height', 20);

我們先把不同的 keys 所組合的資料集設定不同顏色,接著使用 d3.stack( ) 返還的起始值跟終點值去設定每條bar中,不同資料集的起迄位置,最後用 < rect > 來繪製橫向長條圖,就完成啦!!!
https://ithelp.ithome.com.tw/upload/images/20210921/20134930xIPCQqoGdJ.jpg

其實了解 d3 API 的運作原理、需要的資料以及返還什麼東西之後,是不是覺得圖表也沒有很難畫了呀~雖然 Layout 這一類的 API 相比 Generators 跟 Components 更難懂一些,但只要搞懂這些 API 返還什麼值、要怎麼運用,其實就能輕鬆畫出想要的圖表了。

今天的 Helper Functions 就講到這邊~看完之後相信大家對於 d3 用來繪製形狀的 API 都有一定的認識啦,明天開始要進入 d3 動畫跟互動的部分囉!敬請期待!


Github Page 圖表與 Github 程式碼

最後的最後,一樣附上本章的程式碼與圖表 GithubGithub Page,需要的人請自行取用~


上一篇
Day8-D3 資料整理的API們:Array、Time Formats、Number Formats、Random
下一篇
Day10-D3 Transition 動畫
系列文
三十天成為D3.js v7 好手30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Global Rachel
iT邦新手 3 級 ‧ 2021-09-24 19:24:10

看完後是不是覺得有看沒有懂呢?沒關係很正常(看得懂的是神人)
所以金金是神人
/images/emoticon/emoticon12.gif

金金 iT邦新手 1 級 ‧ 2021-09-25 22:25:10 檢舉

我也是看了各種神人的解說跟實際做一次才懂的XD 光靠文件的描述真的是無法~~

我要留言

立即登入留言