如何整合React與D3

Failed

在Dashboard的應用裡,總是少不了精緻好看的互動式圖形,諸如長條圖、折線圖等,雖然現今已有許多免費而且內建多種圖形的Library可以直接採用,但是需要客製化樣式的時候,就知道哪個Library沒穿褲子了。因此,不如打從一開始就學好低階的繪圖工具,不管想刻什麼圖都能搞定,而其中最廣為人知的就是D3這套Library了。

TL;DR

#

步驟一:使用React維護DOM Tree
步驟二:使用D3映射Datapoint至DOM元素屬性
步驟三:使用CSS處理靜態樣式細節

D3

#

官方正式全名為,乍看之下完全無法理解D3到底是什麼,所以我個人認為解讀成較容易理解。

與jQuery相似之處

#

直接舉個例子來看,D3的API支援Method chain,而且是直接操作DOM tree(、...)

1/* https://codepen.io/gocreating/pen/PrRbZo */
2
3d3.select('body')
4 .append('div')
5 .attr('class', 'red')
6 .style('width', '20px')
7 .style('height', '10px')
8 .style('border', '10px solid red')

向量化之處

#

D3最大的特色就是能夠將資料綁定至DOM元素,並以向量化的方式來操作DOM tree。如下方範例,之後的method都是批次套用至data內的每一筆entry,因此我們目前有三筆entry,就會三次。

而向量化呼叫D3 API時,可以透過傳入的callback function來映射datapoint至對應的DOM元素屬性,以下例而言,我們將datapoint的映射至不同rect元素的寬度及背景色,而座標與datapoint本身無關,但與索引順序相關,因此只取index值來進行映射。

1/* https://codepen.io/gocreating/pen/PrRbyz */
2
3const data = [
4 { value: 100, color: '#f00'},
5 { value: 150, color: '#0f0' },
6 { value: 50, color: '#00f' },
7]
8d3.select('#my-svg')
9 .selectAll('.my-bar')
10 .data(data)
11 .enter()
12 .append('rect')
13 .attr('class', 'my-bar')
14 .attr('y', (d, i) => i * 30)
15 .attr('width', (d) => d.value)
16 .attr('height', 20)
17 .attr('fill', (d, i) => d.color)

使用Vanilla D3必須熟悉三個重要觀念,但是本文著重在與React的整合,以結論而言,最終並不會使用到這三個method。

SVG

#

眼尖的讀者應該已經發現,前面的例子其實是使用SVG元素來做為整個圖形的容器,但是D3並不侷限於用在SVG上,你也可以使用等HTML元素來畫出圖形,只是慣例上使用SVG會比使用其他HTML元素更方便也更有效率,因此D3的使用者一般都是搭配SVG來繪圖。此外,D3也針對SVG繪圖提供了一些好用的helper,像是axis、tooltip等常見的圖形元素。

常見的SVG元素包括:

  • (群組)
  • (不規則形)
  • etc.

多數元素都能直覺地顧名思義,而其中可能較難判斷,它表示群組的概念,也可以想成Photoshop或是Illustrator中的。如果想更加深入了解SVG,非常推薦閱讀參考資料中的SVG 完整教學 31 天open_in_new

在React中使用D3

#

D3會操作到DOM,React也會操作到DOM,如果兩套Library一起使用,顯然會互相衝突而導致程式碼難以維護。「3 ways to integrate React and D3open_in_new」一文中提供了三種整合React與D3的策略:,筆者認為混用才是上策,經過自家產品實驗至目前為止,還沒遇過實作不出來的設計,Production環境下也都正常運作。

範例

#

讓我們直接從一個例子來說明,假設我們想將3筆資料轉化成Scatter plot,並依據資料值來設定每個圓的座標及半徑:

在React中,我們當然會將這張圖封裝成一個Component,姑且稱做,而資料源由外部傳入做為prop。從生命週期來看,分別在時呼叫instance method ,也就是使用D3進行繪圖操作的核心。

1/* https://codepen.io/gocreating/pen/QXmpaL */
2
3const { Component } = React
4
5// 步驟三:使用CSS處理靜態樣式細節
6const Graph = styled.svg`
7 .my-circle {
8 fill: red;
9 }
10`
11
12class SomeAwesomeGraph extends Component {
13 componentDidMount() {
14 this.draw()
15 }
16
17 componentDidUpdate() {
18 this.draw()
19 }
20
21 draw = () => {
22 // 步驟二:使用D3映射Datapoint至DOM元素屬性
23 const { data } = this.props
24 d3.select('#my-svg g.container')
25 .selectAll('.my-circle')
26 .data(data)
27 .attr('cx', d => d.cx)
28 .attr('cy', d => d.cy)
29 .attr('r', d => d.value * 2)
30 }
31
32 render() {
33 // 步驟一:使用React維護DOM Tree
34 const { data } = this.props
35 return (
36 <Graph id="my-svg">
37 <g className="container">
38 {data.map((d, i) => (
39 <circle
40 key={i}
41 className="my-circle"
42 />
43 ))}
44 </g>
45 </Graph>
46 )
47 }
48}
49
50const App = () => (
51 <SomeAwesomeGraph
52 data={[
53 { cx: 100, cy: 100, value: 7 },
54 { cx: 150, cy: 70, value: 15 },
55 { cx: 50, cy: 30, value: 4 },
56 ]}
57 />
58)
59
60ReactDOM.render(<App />, document.getElementById('app'));

實際整合的步驟可以分為三個:

步驟一:使用React維護DOM Tree

#

(Line 38-43)
透過JS array的將datapoint轉換成DOM元素,這是非常典型的React語法。由於DOM元素的建立及刪除寫在裡,所以完全利用了React的vDOM達到最佳性能,非常乾淨俐落。

步驟二:使用D3映射Datapoint至DOM元素屬性

#

(Line 23-29)
DOM Tree既已建立,便不需要理解D3複雜的機制,更能專注在裡面繪製圖形。的實作應著重在將datapoint映射至DOM元素的屬性,所以會呼叫大量的等調整屬性的API,若圖形中有使用到繪製X軸Y軸或Tooltip之類的Helper,也須在內一起使用。如果圖形過大,也建議將拆分成更細小的模組增加可讀性。

步驟三:使用CSS處理靜態樣式細節

#

(Line 7-9)
一個完整圖形裡肯定會有不隨資料變動的樣式,像是本例中圓圈的背景色,只需要視情況套用適合的CSS即可(inline、className、styled-component、CSS-in-JS、...)

Reference

#