本篇大綱:開放資料下載、本次範例的畫面與互動效果、複數長條圖的繪製關鍵、繪製複數長條圖
今天的一天一圖表,我們要來畫圖表長條圖第二部曲 — 複數長條圖!
複數長條圖主要是用來比較 某項目在不同時間的變化
,或是 同一時間不同項目的差異
。由於有多項資料要比較,因此除了XY軸之外,最好還要有資料的標籤,才能讓看圖表的人更了解這張圖想比較什麼東西。
為了更貼近真實,我們這次一樣使用政府提供的公開資料:臺南市勞動人口-依年齡別區分 來當作此次圖表的資料!這次的資料一樣是csv檔案,打開檔案的結構如下
由於是 csv 檔,所以我們不用進行其他處理,直接拿來用就可以了 (耶比~)
這次我們要做的範例畫面與互動效果有:
繪製複數長條圖的關鍵是 多加一條X軸的比例尺
。一般的長條圖只有一條X軸跟一個X軸的比例尺,所有的資料會根據這條X軸去排列;但複數長條圖的關鍵是另外整理一個新的資料陣列放要比較的資料,接著用這個新的資料陣列再多設定一個新的X軸比例尺。這樣講可能很模糊,我們直接來看程式碼比較清楚。
首先,我們先將資料取回來,並且建立svg畫面
// css
.chart{
width: 100%;
min-width: 300px;
margin: auto;
}
// html
<div class="chart"></div>
let data = []
async function getData() {
// 取資料
dataGet = await d3.csv('./data/tainan_labor_force_population.csv')
data = dataGet
console.log(data)
drawBarChart()
};
getData()
// RWD
function drawBarChart(){
// 刪除原本的svg.charts,重新渲染改變寬度的svg
d3.select('.chart svg').remove();
// RWD 的svg 寬高
const rwdSvgWidth = parseInt(d3.select('.chart').style('width')),
rwdSvgHeight = rwdSvgWidth,
margin = 20,
marginBottom = 100
const svg = d3.select('.chart')
.append('svg')
.attr('width', rwdSvgWidth)
.attr('height', rwdSvgHeight);
// 接下來的程式碼放這邊...
// 接下來的程式碼放這邊...
// 接下來的程式碼放這邊...
}
d3.select(window).on('resize', drawBarChart);
接著,我們先把基本的XY軸、以及XY軸的比例尺建立出來
// map 資料集
const xData = data.map((i) => i['年度']);
// 設定要給 X 軸用的 scale 跟 axis
const xScale = d3.scaleBand()
.domain(xData)
.range([margin*2, rwdSvgWidth - margin]) // 寬度
.padding(0.2)
const xAxis = d3.axisBottom(xScale)
// 呼叫繪製x軸、調整x軸位置
const xAxisGroup = svg.append("g")
.call(xAxis)
.attr("transform", `translate(0,${rwdSvgHeight - marginBottom})`)
// 設定要給 Y 軸用的 scale 跟 axis
const yScale = d3.scaleLinear()
.domain([0, 600])
.range([rwdSvgHeight - marginBottom, margin]) // 數值要顛倒,才會從低往高排
.nice() // 補上終點值
const yAxis = d3.axisLeft(yScale)
.ticks(5)
.tickSize(3)
// 呼叫繪製y軸、調整y軸位置
const yAxisGroup = svg.append("g")
.call(yAxis)
.attr("transform", `translate(${margin*2},0)`)
再來!最重要的部分來了!我們把要比較的資料抓出來建立新的陣列,並且建立新的X軸比例尺
const subgroups = Object.keys(data[0]).slice(1)
// 第二條X軸的比例尺,用來設定多條bar的位置
const xSubgroup = d3.scaleBand()
.domain(subgroups)
.range([0, xScale.bandwidth()])
.padding([0.05])
我們可以把新的資料陣列 console 出來看看
這個就是我們要分組的陣列啦~~接著,我們來設定一下每個組別的顏色
// 設定不同 subgorup bar的顏色
const color = d3.scaleOrdinal()
.domain(subgroups)
.range(['#ff2d85','#4a4ae0','#4daf4a', '#f29909'])
然後就是建立長條圖啦!
// 開始建立長條圖
const bar = svg.append('g')
.selectAll('g')
.data(data)
.join('g')
.attr('transform', d => `translate(${xScale(d['年度'])}, 0)`)
.selectAll('rect')
.data(d => {
return subgroups.map(key=>{
return {key:key, value:d[key]};})
})
.join('rect')
.attr('x', d => xSubgroup(d.key))
.attr("y", d => yScale(d.value))
.attr("width", xSubgroup.bandwidth())
.attr("height", d =>{
return (rwdSvgHeight-marginBottom) - yScale(d.value)})
.attr("fill", d => color(d.key))
.style('cursor', 'pointer')
這樣就建立好長條圖囉~剛剛有說過,複數長條圖的標籤很重要,因此我們還要加上最下方的標籤
// 加上下方分類標籤
const tagsWrap = svg.append('g')
.selectAll('g')
.attr('class', 'tags')
.data(subgroups)
.enter()
.append('g')
.attr('transform', "translate(-70,0)")
tagsWrap.append('rect')
.attr('x', (d,i)=> (i+1)*marginBottom*1.3)
.attr('y', rwdSvgHeight-marginBottom/2)
.attr('width', 20)
.attr('height', 20)
.attr('fill', d => color(d))
tagsWrap.append('text')
.attr('x', (d,i)=> (i+1)*marginBottom*1.3)
.attr('y', rwdSvgHeight-10)
.style('fill', '#000')
.style('font-size', '12px')
.style('font-weight', 'bold')
.style("text-anchor", 'middle')
.text(d=>d)
這樣基本的圖表就好囉
再來我們來綁定滑鼠的事件,同時使用 d3.pointer( )
的方法來建立水平軸線跟資料的標示
bar.on("mouseover", handleMouseOver)
.on("mouseleave", handleMouseLeave)
function handleMouseOver(d, i){
// console.log(d.target.__data__)
const pt = d3.pointer(event, svg.node())
// 加上文字標籤
svg.append('text')
.attr('class', 'infoText')
.attr('y', yScale(d.target.__data__['value']))
.attr("x", margin*2)
.style('fill', '#000')
.style('font-size', '18px')
.style('font-weight', 'bold')
.style("text-anchor", 'middle')
.text(d.target.__data__['value'] + '千人')
// 加上軸線
svg.append('line')
.attr('class', 'dashed-Y')
.attr('x1', margin*2)
.attr('y1', yScale(d.target.__data__['value']))
.attr('x2', pt[0])
.attr('y2', yScale(d.target.__data__['value']))
.style('stroke', 'black')
.style('stroke-dasharray', '3' )
}
function handleMouseLeave(){
// 移除文字、軸線標籤
svg.select('.infoText').remove()
svg.select('.dashed-Y').remove()
}
大功告成~
最後附上本章的程式碼:想看完整程式碼的請上 Github,想直接操作圖表的則去 Github Page 吧!請自行取用~