拖更了好久,最近畢業的手續辦的差不多了,應該可以回來了...
系列傳送門:
知識點:
- d3資料繫結
- 柱狀圖畫法
- 座標軸
- 比例尺
資料讀入
資料視覺化的第一步還是資料讀取,在d3中可以使用d3.csv
非常方便的讀取資料,它會返回一個Promise
物件。
csv檔案是以逗號,
分隔的資料內容,本地使用的csv資料如下,檔名為data.csv
:
country,population
China,1415046
India,1354052
United States,326767
Indonesia,266795
Brazil,210868
Pakistan,200814
Nigeria,195875
Bangladesh,166368
Russia,143965
Mexico,130759
複製程式碼
首先將資料讀入,程式碼如下:
const data = d3.csv('data.csv').then(data => {
console.log(data))
})
複製程式碼
可以看到,控制檯輸出了一個陣列,陣列中的每條資料均是一個物件,型別為{country:xx, population:xx}
當然,人數自然應該是Number
型別的,同時,為了將人數轉換為單位個
,將所有資料都擴大一千倍,即:
const data = d3.csv('data.csv').then(data => {
data.forEach(element => {
element.population = +element.population * 1000
}); //處理完資料,就可以開始畫圖了
render(data)
})
複製程式碼
畫柱狀圖
HTML檔案與上一節相同,都是僅包含了一個<svg></svg>
標籤,首先選擇svg:
const svg = d3.select('svg');
const height = +svg.attr('height');
const width = +svg.attr('width');
const render = data => {
} //根據已有資料,畫圖渲染的函式
複製程式碼
資料繫結
將資料繫結到DOM上,是D3最大的特色。d3.select
與d3.selectAll
返回選擇集,但其本身是沒有資料的,通過data()函式,可以將資料與之繫結。相關函式有兩個:
selection.datum([value])
選擇集上的每個元素都繫結相同的元素valueselection.data(values[,key])
選擇集上每一個元素分別繫結陣列values的每一項,key是一個鍵函式,用於指定繫結陣列時的規則。
datum
用比較少,這裡主要用到的是data()
,將已處理好的資料繫結在dom上。
update、enter和exit
在進行資料繫結的時候,不一定資料和元素個數就是相同的,這個時候就需要一個動態的處理,這就需要用到updata
、enter
、和exit
了。
- update() 當對應的元素正好滿足時 ( 繫結資料數量 = 對應元素 ) 實際上並不存在這樣一個函式,只是為了要與之後的 enter 和 exit 一起說明才想象有這樣一個函式。但對應元素正好滿足時,直接操作即可,後面直接跟 text ,style 等操作即可。
- enter() 當對應的元素不足時 ( 繫結資料數量 > 對應元素 ) 當對應的元素不足時,通常要新增元素,使之與繫結資料的數量相等。後面通常先跟 append 操作。
- exit() 當對應的元素過多時 ( 繫結資料數量 < 對應元素 ) 當對應的元素過多時,通常要刪除元素,使之與繫結資料的數量相等。後面通常要跟 remove 操作。
本專案主要用到了enter
,因為頁面中只有svg
標籤,我們需要做的是根據資料內容,在svg
中畫<rect>
來表示柱狀圖。
理解 Update、Enter、Exit
render函式
有了以上的概念,就可以開始畫圖了
const render = data => {
svg.selectAll('rect').data(data) //選擇`rect`並繫結資料data,但這個時候沒有元素,因此使用enter
.enter().append('rect')
.attr('width',width)
.attr('height','30px')
}
複製程式碼
這樣更新檢視,就可以看到已經有影象出來了。但是隻能看到一個黑色的長方形。因為目前圖形並不能反映任何資料,只是單純的固定'width'的長方形。為此,需要用上資料,但是因為資料可能很大或者很小,為了讓其能夠正好顯示的檢視中,需要使用到比例尺。
比例尺
D3中有很多比例尺,本例中主要使用到了線性比例尺(scaleLinear)和序數比例尺(scaleBand)
線性比例尺可以將domain
的內容線性對映到range
的一個範圍內,這樣,就可以保證無論初始數值多大或多小,都能夠很好的適應畫當前檢視。
對映關係:
序數比例尺不是一個連續的比例尺,domain()
中使用一個陣列,range()
是一個連續域。
對映關係:
因此,加上兩個方向的比例尺,讓柱狀圖的雛形開始慢慢出現吧:
const render = data => {
const xScale = d3.scaleLinear()
.domain([0,d3.max(data, d => d.population)])
.range([0,width]) //最大值將檢視空間充滿
const yScale = d3.scaleBand()
.domain(data.map(d => d.country))
.range([0,height])
svg.selectAll('rect').data(data) //選擇`rect`並繫結資料data,但這個時候沒有元素,因此使用enter
.enter().append('rect')
.attr('y',d => yScale(d.country))
.attr('width',d => xScale(d.population)) //寬度根據資料
.attr('height',yScale.bandwidth()) //高度由比例尺自動生成
}
複製程式碼
這樣子,就有一個雛形了,效果如下:
程式碼優化
首先,重新審視下程式碼,發現其中d => d.population
以及d => d.country
在比例尺設定以及使用時出現了多次,如果需要修改,又是程式碼中多處的重複修改。為此,對其進行一個處理,如下:
const render = data => {
const xValue = d => d.population; //優化
const yValue = d => d.country; //優化
const xScale = d3.scaleLinear()
.domain([0,d3.max(data,xValue)]) //優化
.range([0,width])
const yScale = d3.scaleBand()
.domain(data.map(yValue)) //優化
.range([0,height])
svg.selectAll('rect').data(data)
.enter().append('rect')
.attr('y',d => yScale(yValue(d))) //優化
.attr('width',d => xScale(xValue(d))) //優化
.attr('height',yScale.bandwidth())
複製程式碼
座標軸
使用margin來優化佈局
下圖所示是margin
的佈局示意圖,因為直接按照svg
的height
和width
撐滿畫布將導致沒有多餘的位置放置座標軸等,所以這裡使用一個margin
來對佈局重新規劃。
程式碼如下:
const margin = {left:50,top:10,right:20,bottom:30};
const innerHeight = height - margin.top - margin.bottom;
const innerWidth = width - margin.left - margin.right;
複製程式碼
其中innerHeight和innerWidth是柱狀圖的實際佔有高度,因此,柱狀圖的程式碼可以修改為:
const xScale = d3.scaleLinear()
.domain([0, d3.max(data,xValue)])
.range([0, innerWidth]) //將 width 改為 innerWidth
const yScale = d3.scaleBand()
.domain(data.map(yValue))
.range([0, innerHeight]) //將height 改為 innerHeight
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`) //加入新元素g,整體移動maring.left和margin.top
g.selectAll('rect').data(data)
.enter().append('rect')
.attr('y',d => yScale(yValue(d)))
.attr('width', d => xScale(xValue(d)))
.attr('height',yScale.bandwidth())
複製程式碼
增加座標軸
座標軸的繪製,是d3通過<svg>
中的<path>
<text>
<line>
實現的,用到的函式如axisLeft
axisBottom
等,繪製一般分為一下幾個步驟:
- 建立座標軸
var axisX = d3.axisLeft(xScale)
根據比例尺建立座標軸 - 建立新的
<g>
組var gAxis = svg.append('g')
- 插入座標軸
axisX(gAxis)
或者 在上一步直接svg.append('g').call(axisX)
因此,本例中座標軸新增可以這樣:
g.append('g').call(d3.axisLeft(yScale)); //左邊顯示country
g.append('g').call(d3.axisBottom(xScale))
.attr('transform',`translate(0,${innerHeight})`) //雖然是bottom,但是預設位置並不在下,需要移動至下方
複製程式碼
增加間隙
現在柱狀圖還是很醜的狀態,應該增加一點間隙,讓它看起來更加美觀,這就非常簡單了,在yScale
上使用padding
屬性。
const yScale = d3.scaleBand()
.domain(data.map(yValue))
.range([0, innerHeight])
.padding(0.15) //增加了這個屬性
複製程式碼
修改樣式
為了讓柱狀圖看起來更美觀,增加一些css樣式,樣式如下:
body html{
margin:0;
overflow: hidden;
}
rect {
fill:steelblue;
}
text {
font-size: 1.1em;
}
複製程式碼
最終效果
注意
由於chrome的安全原因限制,在本地使用d3.csv
讀取本地檔案時是會遇到問題的,並不支援file//:
讀取內容。 因此,程式碼在github中是可以正常運轉,但是本地可能無法正常運作, 可以開一個本地伺服器,將程式碼放置上。
完整程式碼
完整程式碼詳見:d3系列教程原始碼