在Dashboard的應用裡,總是少不了精緻好看的互動式圖形,諸如長條圖、折線圖等,雖然現今已有許多免費而且內建多種圖形的Library可以直接採用,但是需要客製化樣式的時候,就知道哪個Library沒穿褲子了。因此,不如打從一開始就學好低階的繪圖工具,不管想刻什麼圖都能搞定,而其中最廣為人知的就是D3這套Library了。
步驟一:使用React維護DOM Tree
步驟二:使用D3映射Datapoint至DOM元素屬性
步驟三:使用CSS處理靜態樣式細節
官方正式全名為,乍看之下完全無法理解D3到底是什麼,所以我個人認為解讀成較容易理解。
直接舉個例子來看,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元素來做為整個圖形的容器,但是D3並不侷限於用在SVG上,你也可以使用或等HTML元素來畫出圖形,只是慣例上使用SVG會比使用其他HTML元素更方便也更有效率,因此D3的使用者一般都是搭配SVG來繪圖。此外,D3也針對SVG繪圖提供了一些好用的helper,像是axis、tooltip等常見的圖形元素。
常見的SVG元素包括:
多數元素都能直覺地顧名思義,而其中可能較難判斷,它表示群組的概念,也可以想成Photoshop或是Illustrator中的。如果想更加深入了解SVG,非常推薦閱讀參考資料中的SVG 完整教學 31 天open_in_new。
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 } = React4
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.props24 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 Tree34 const { data } = this.props35 return (36 <Graph id="my-svg">37 <g className="container">38 {data.map((d, i) => (39 <circle40 key={i}41 className="my-circle"42 />43 ))}44 </g>45 </Graph>46 )47 }48}49
50const App = () => (51 <SomeAwesomeGraph52 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'));
實際整合的步驟可以分為三個:
(Line 38-43)
透過JS array的將datapoint轉換成DOM元素,這是非常典型的React語法。由於DOM元素的建立及刪除寫在裡,所以完全利用了React的vDOM達到最佳性能,非常乾淨俐落。
(Line 23-29)
DOM Tree既已建立,便不需要理解D3複雜的、和機制,更能專注在裡面繪製圖形。的實作應著重在將datapoint映射至DOM元素的屬性,所以會呼叫大量的、、等調整屬性的API,若圖形中有使用到繪製X軸Y軸或Tooltip之類的Helper,也須在內一起使用。如果圖形過大,也建議將拆分成更細小的模組增加可讀性。
(Line 7-9)
一個完整圖形裡肯定會有不隨資料變動的樣式,像是本例中圓圈的背景色,只需要視情況套用適合的CSS即可(inline、className、styled-component、CSS-in-JS、...)