iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 24
0
Modern Web

React + D3 的正確姿勢系列 第 24

Day24-line chart(using D3.js)

前言

在講完長條圖之後接下來要介紹的是折線圖,折線圖也是視覺化中非常重要且基礎的一種圖表,廢話不多說馬上開始今天的文章吧!

折線圖

折線圖是用直線段將各數據點連接起來而組成的圖形,以折線方式顯示數據的變化趨勢,所以像股票、房價等等這種會有成長趨勢的現象都會用折線圖來表示,在折線圖中可以很明確的瞭解整體數據隨著時間的變化,不論是遞增還是遞減、增減的速率、增減的規律、峰值等等都可以很明確地表現出來,所以只要是遇到這種需要表現出時間走向的圖表,首選折線圖就對了!

line

在正式進行折線圖繪製前,筆者要先跟大家說明一下 line 這個 svg 內才能使用的元素, line 顧名思義就是要畫一條直線,而折線圖最主要呈現的方式就是以直線為主,所以接下來的繪製流程都會圍繞在 line 上。

至於要怎麼設定這個 line 也很簡單, D3 都幫你做好好了只要呼叫 API 即可,正常來說寫法會像這樣:

const line = d3
  .line()
  .x(d => x(d.name))
  .y(d => y(d.value))

上面的寫法就是利用 d3.line() 畫線,並把線上的點傳進去,點座標都會是由 x 以及 y 兩個方位組成,筆者建議先傳 x 再傳 y 進去,至於 x 以及 y 的座標點要如何設定就是利用我們一開始設定好的 x 軸以及 y 軸藉由傳入的資料得到相對應的點即可。

path

有了 line 之後當然也要告訴 D3 這條直線要往哪個方向走,這時候就需要 path 這個元素了,在 path 這個元素內有一個特別的 attribute 叫 d ,我們會在 d attribute 內設定這個 path 的走向,至於怎麼寫這個 d 讀者也不用擔心, D3 都幫你寫好好了只要去呼叫 API 就好了,寫法會長這樣:

d3.attr('d', d => line(d.series))

筆者這邊故意把 data 稱之為 series 代表直線上一系列的資料,由於 d 就是要把線條的走向全部都繪製出來,所以這邊在設定 d 的時候要記住一定要把完整的資料陣列傳進去喔!最後再經由上面寫好的 line 把資料都傳進去直線內即可完成線條的繪製了。

繪製流程

老樣子一開始一樣要先初始化 svg 容器。

const margin = { top: 10, right: 35, bottom: 20, left: 40 },
  width = document.querySelector(`#${root}`).clientWidth,
  height = width
    
const svg = d3
  .select(document.querySelector(`#${root}`))
  .append('svg')
  .attr('width', width)
  .attr('height', height + margin.top + margin.bottom)
  .append('g')
  .attr('transform', `translate(${margin.left}, ${margin.top})`)

再來就是設定 x 軸以及 y 軸的比例尺以及輸出區域,正常來說折線圖都是為了表現一個隨時間發展的趨勢,所以 x 軸都會是以時間為主,也因為都是以時間為主所以這邊的比例尺都需要設定成 d3.scaleLinear() 這種連續比例尺喔!對於 D3 如何設定時間格式有任何不懂的讀者歡迎閱讀筆者之前的文章。

不過今天筆者不打算寫 d3.scaleLinear() 今天筆者想跟各位介紹一個專門處理時間的連續比例尺,雖然 d3.scaleLinear() 一樣可以做到我們最終想要的結果,但多學一種比例尺也等於多增加一種不一樣的寫法,所以今天筆者會用 d3.scaleTime() 這個負責處理時間的連續比例尺來進行 x 軸的繪製,寫法的部分其實就跟 d3.scaleLinear() 差不多,只差在比例尺內部的運作而已。

Day13-D3基本介紹(scale、range)
Day15-D3基本介紹(time format)

// Format Data
const parseDate = d3.timeParse('%m-%d')
const newData = lineDatas.map(data => ({
  series: data.series.map(el => ({
    label: data.name,
    name: parseDate(el.name),
    value: parseFloat(el.value),
  })),
  name: data.name,
}))

// 讓最後平移圖表時 x 軸可以完整顯示出來,所以輸出區域會扣掉左右兩邊的 margin
const x = d3.scaleTime().range([0, width - margin.left - margin.right])

const y = d3.scaleLinear().range([height, 0])

設定完比例尺以及輸出區域後接下來就是要設定輸入區域啦!由於這次 x 軸也是設定成連續比例尺,所以輸入區域就不用傳入全部的資料只需傳入範圍就可以了。

Day14-D3基本介紹(domain、endpoint)

// val 為 d3.max() 得到的資料最大值
function getSmartEndpoint(val) {
  // 先取得最大值的位數,並算出這個位數的最小值
  let count = Math.floor(val).toString().length - 1
  let step = Math.pow(10, count)

  // 以 5 的倍數為基準,假如最大值除以此位數的最小值小於 5
  if (val / step < 5) {
  // 將這個位數最小值砍半,這樣之後就會是以 5 為基準了
    step = step / 2
  } 
  
  count = Math.ceil(val / step)
  
  return count * step
}

x.domain(d3.extent(newData[0].series, d => d.name))
y.domain([0, endPoint])

解決了最基本設定之後接下來要設定折線圖的直線了,這邊就會用 d3.line() 來產生直線,這邊筆者習慣再用一個 group 進行包裝,以確保這條直線不會受到其他的樣式影響,最終寫法長得像這樣:

const line = d3
  .line()
  .x(d => x(d.name))
  .y(d => y(d.value))
  
const lines = svg.append('g').attr('class', 'lines')

解決了折線圖最基本的設定後終於要傳資料進去啦,由於我們在上面已經設定好 group 了,所以底下的資料都是要傳進去剛剛設定好的 group 內,也為了要讓資料可以順利的被胃進去剛剛設定好的 group 內,所以這邊筆者有寫了一個 class 來進行選取的動作,姑且就把這個 class 稱為 line-group 吧XD

也為了讓圖表的直線有不一樣的顏色,所以這邊設定了 strokestroke 是負責幫線條上色的 attribute ,想要設定線條的顏色要記得設定這個 attribute 喔!

Day18-D3基本介紹(data)

// add lines into svg
lines
  .selectAll('.line-group')
  .data(newData)
  .enter()
  .append('g')
  .attr('class', 'line-group')
  .append('path')
  .attr('class', 'line')
  .attr('d', d => line(d.series))
  .style('stroke', d => chartColor[d.name].color)
  .style('fill', 'none')

接下來這個段落就比較看個人需不需要設定了,還記得之前介紹過的 tooltip 嗎?由於前面的設定都是針對整張圖表進行繪製,可是 tooltip 是根據圖表上的點進行繪製,這時候就必須要用不一樣的方式來設定 tooltip 了,通常筆者是習慣在折線圖上用圓點的方式來代表直線上的點,透過設定這些圓點就可以順利的設定 tooltip 了,但筆者有點潔癖不太喜歡線上還有圓圓的點XD,所以這時候就會將原點大小縮小讓肉眼幾乎感受不到他的存在,接下來就提供設定的寫法吧!

Day20-D3基本介紹(chart event、tooltip)

// add tooltip
lines
  .selectAll('circle-group')
  .data(newData)
  .enter()
  .selectAll('circle')
  .data(d => d.series)
  .enter()
  // 用 group 打包線條上的圓點
  .append('g')
  .attr('class', 'tooltip')
  .on('touchstart', tooltip.show)
  .on('touchend', tooltip.hide)
  // 新增圓點,在 svg 中的圓點是 circle
  .append('circle')
  // 設定圓點中心點的 x 以及 y 座標
  .attr('cx', d => x(d.name))
  .attr('cy', d => y(d.value))
  // r 代表的是半徑
  .attr('r', 3)
  // 這邊 fill 顏色設定為繼承就可以讓線條上的點與線條是同一種顏色
  .style('fill', 'transparent')

最後就是設定 Axis 啦!終於快把折線圖繪製完了,大家再加把勁就可以完成了,由於我們一開始在設定 x 軸的輸入區域時有利用 d3.timeParse() 的方式先改變時間格式,為了讓座標軸可以顯示成一般人看得懂的格式這時候就必須要用 d3.timeFormat() 來完成最後的顯示模樣。

Day19-D3基本介紹(Axis)

// add the x Axis
const xAxis = d3
  .axisBottom(x)
  .ticks(5)
  .tickFormat(d3.timeFormat('%m/%d'))
  
svg
  .append('g')
  .attr('transform', `translate(0, ${height})`)
  .attr('class', 'xaxis')
  .call(xAxis)
  
// add the y Axis
svg
  .append('g')
  .attr('class', 'yaxis')
  .call(d3.axisLeft(y).ticks(5))

組合起來

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

line chart

總結

今天介紹了折線圖,相信讀者應該會猜到明天筆者要來介紹多重折線圖了,但筆者今天的教學其實已經混入多重折線圖進去了,以後遇到折線圖只要用筆者這個方法就可以輕易的畫出單條以及多條的折線圖了,是不是相當方便呢XD

如果有任何問題歡迎在下面留言給我,沒問題的話明天要來介紹圓餅圖了。


上一篇
Day23-multi bar chart(using D3.js)
下一篇
Day25-pie chart(using D3.js)
系列文
React + D3 的正確姿勢30

尚未有邦友留言

立即登入留言