整體分成三部分:
前置設定SVG和XY軸、導入data資料、將資料丟到SVG進行update
(transition就先跳過QQQQQ)
Svg 前置設定(不需要用到Data)
Html 設定先設定一個名為 canvas 放 svg 的 div,並載入 firebase 與D3.js。
<div class=”canvas”> </div>
簡易步驟:(1)整個容器和群組 → (2)X & Y軸群組 → (3)X & Y軸刻度range → (4) 設定X & Y軸並把刻度的資料丟進去
- 製作容器和群組
d3.select('.canvas') 選取DOM,append(‘svg’) 插入 <svg>,attr() 設定 svg 長寬屬性。
const svg = d3.select(‘.canvas’)
.append(‘svg’)
.attr(‘width’, 600)
.attr(‘height’, 600);
svg.append(‘g’),利用 <g> 來進行整個長條圖的群組化。
// 設定 svg 內的圖形相關的長寬
const margin = { top: 20, right: 20, bottom: 100, left: 100}
const graphWidth = 600 — margin.left — margin.right;
const graphHeight = 600 — margin.top — margin.bottom;// 在 svg 中插入 <g> 容器來包裹整個長條圖並利用上面設定的長寬設定屬性
const graph = svg.append(‘g’)
.attr(‘width’, graphWidth)
.attr(‘height’, graphHeight)
2. 群組內再設置 X軸 & Y軸 的群組
// 設置X軸群組,並進行相關位移(往下移不然會在最上0的位置)、文字樣式設定等
const xAxisGroup = graph.append(‘g’)
.attr(‘transform’, `translate(0,${graphHeight})`);xAxisGroup.selectAll(‘text’)
.attr(‘transform’, ‘rotate(-40)’)
.attr(‘text-anchor’, ‘end’)
.attr(‘fill’, ‘orange’)// 設置Y軸群組
const yAxisGroup = graph.append(‘g’)
3. 設置X & Y軸range
設定 X軸 & Y軸 內的刻度,例如呈現的範圍、多少切分一格等,資料的地方先別管之後再匯入就好了。
d3.scaleLinear() 簡單而言就是幫我們進行比例尺的縮放, domain 是原始的範圍(需要資料),range 則是我們希望呈現的比例(可先設定)。
*因為y軸會由上往下跑,所以range要對調位置確保y軸從下至上
d3.scaleBand() 可以幫我們把不同筆數的資料domain塞到設定的range中,簡單來說就是平均設定每個bar的區段。(之後可以使用bandwidth來自動設定bar 的寬度)
paddingInner() 設定bar間的間距
paddingOuter() 設定bar外側兩邊的間距
*不同於 y軸 的domain和range都是([0,max])長度二的形式,x軸的domain會是([n1, n2, n3, n4..........])長度不固定的map回返陣列。
覺得這個解釋的超好懂得:
// Y刻度(點)的縮放比例range
const y = d3.scaleLinear()
.range([graphHeight, 0]) //因為會由上往下呈現所以要顛倒設定// X刻度(點)的縮放比例range
const x = d3.scaleBand()
.range([0, graphWidth])
.paddingInner(.2)
.paddingOuter(.2)
4. 設定X & Y軸並把刻度的資料丟進去
const xAxis = d3.axisBottom(x) //把x刻度丟到下軸生出x軸
const yAxis = d3.axisLeft(y) //把y刻度丟到下軸生出y軸//ticks 設定會顯示多少個刻度間距
.ticks(3)
.tickFormat(d => d + ‘ orders’)
獲取 firebase realtime 資料
使用 onSnapshot 來獲取 firebase 即時資料,只要資料變動就會即時更新。
// 先建立要用來渲染畫面用的data
let data = [];db.collection(‘dishes’).onSnapshot(res => {
res.docChanges().forEach(change => {// 設定要推入 data 中的每筆 doc 資料,包含資料 & id
const doc = { …change.doc.data(),
id: change.doc.id }// 依照 change type 來決定該 doc 的處置方式
switch (change.type) {// 若該筆 doc 情況是新增資料,就將 doc 推入 data 中
case ‘added’: data.push(doc);
break;// 若該筆 doc 情況是被修改,利用 findeIndex 從 data 找出同 id 的資料位置,並進行取代
case ‘modified’:
const index = data.findIndex(item => item.id == doc.id);
data[index] = doc;
break;// 若該筆 doc 情況是被刪除,利用 filter 從 data 中刪除
case ‘removed’:data = data.filter(item => item.id !== doc.id);
break; default: break;}});// 執行 update 來更新畫面
update(data);})
將資料丟到SVG進行update
課程的簡易流程,個人喜歡 1 & 2 步驟對調,4 & 5對調方便理解
- 選取所有rects並讀取資料 selectAll(‘rect’).data(data)
const rects = graph.selectAll(‘rect’)
.data(data)
2. 把實際的 domain 資料丟到 xy 刻度裡
x軸 的 domain 用 map 來輸出要抓的名稱,y軸 的 domian 則利用 d3.max(data, d => d.orders) 取
y.domain([0, d3.max(data, d => d.orders)])
x.domain(data.map(item => item.name))
3. 處理刪除資料, 資料筆數<DOM,利用 exits().remove() 刪掉多rect
rects.exit().remove();
4. 處理修改資料,資料筆數=DOM,也就是改變既存的實體rect
rects.attr(‘width’, x.bandwidth())
.attr(‘fill’, ‘orange’)
.attr(‘x’, d => x(d.name)) //把.data(data)得資料丟到設定的x刻度方法中
.attr(‘y’, d => y(d.orders)) //同上
.attr(‘height’, d => graphHeight — y(d.orders))
- bar 的寬度:觸發 x.bandwidth() 設定寬度
- bar 的 x 位置:上述是讀取 data 的簡略寫法,完整如下,也就是把 d 丟進讀取資料的函式並且執行之前設定的 x 方法中,讓每個 x 經過 scaleBrand處理投射到正確的新維度上。
.attr(‘x’, function(d,i,n){ })
d 代表當前的data陣列中的物件
i 代表在data陣列中的index,也就是第幾個rect
n 代表我們selectAll的整個data陣列
- bar 的 y 位置:同上。
- bar 的高度:原本的圖形 y 會是刻度上下顛倒的(在設定range時已調整),而利用全高度扣掉上下顛倒的高度就會是正確的高度。
5. 處理新增資料,資料筆數>DOM,利用 enter().append() 增加新的rect
rects.enter().append(‘rect’)
.attr(‘width’, 0) //在widthTween已經指定過了
.attr(‘height’, d => graphHeight — y(d.orders))
.attr(‘fill’, ‘orange’)
.attr(‘x’, d => x(d.name))
.attr(‘y’, d => y(d.orders))
}
除了選取的方式不同,4是選取本來就存在的react進行修改,而5是新增新的rect,以及 transition 所設定的前後狀態會不同外,基本上 4 & 5 的步驟是相同的。
(6.) 最後將設定好的 x 軸和 y 軸匯入一開始設定的 x 和 y 的群組中。
*這個和call apply bind的call是不同的,而是D3.js的語法,詳細可以參考這篇文章。
xAxisGroup.call(xAxis)
yAxisGroup.call(yAxis)