本篇大纲:开放资料下载、绘制长条图
今天的一天一图表,我们来画图表世界中最常见的 长条图
吧!长条图系列总共分成三部曲,分别是「基础长条图、复数长条图、堆积长条图」,今天先来看看基础的第一部曲~
为了让我们画的图表更接近真实社会的资料,我这次选用台湾电力公司提供的开放资料 — 各县市售电资讯。台电目前只提供 xls 档案,但由於D3无法接受这种档案格式,因此我们要把载下来的 xls 档另存成 csv 档。
我们先把2021/07跟2021/08的售电资讯载下来并打开,档案里面第一行跟最後一行使用了合并储存格的方式去记录资料,但这样的方式另存成csv档时会出错,所以我们先将第一行跟最後一行都删掉
接着再把这两份资料另存成csv档。其实这个不是很好的做法,如果你要替公司的专案制作图表的话,记得跟後端沟通好你需要的资料格式,而不是用这种直接修改资料的方式。这边因为只是示范,所以就先用偷吃步的方式来处理~
确定了要使用哪些资料後,我们先来看看这次要做的图表以及互动效果吧!我们这次要做的图表如下:
画面与互动效果包含:
现在我们开始画图吧!首先,一样先建立图表区块与svg,并且将刚刚下载的资料用 d3.csv的方式取得
// css
.chart{
width: 100%;
min-width: 300px;
margin: auto;
}
//html
<h4 class="mt-3">台湾各县市每月住宅售电</h4>
<div class="chart"></div>
<div class="btnWrap">
<button class="btn btn-primary July">2021 7月</button>
<button class="btn btn-primary August">2021 8月</button>
</div>
// js
let data = []
async function getData() {
// 取资料
dataGet = await d3.csv('./data/202107_Electric.csv')
console.log(dataGet)
data = dataGet
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*0.8,
margin = 40;
const svg = d3.select('.chart')
.append('svg')
.attr('width', rwdSvgWidth)
.attr('height', rwdSvgHeight);
// 接下来的程序码放这边...
// 接下来的程序码放这边...
// 接下来的程序码放这边...
}
我们先把目前得到的资料印出来看一下
这是我们拿到的资料~A 代表的是「家用住宅的售电度数」,我们的图表就先来比较各县市家用售电度数吧!要画图表前要先确定XY轴分别使用哪些资料,我决定X轴放县市,Y轴就放家用售电度数,因此我要把XY轴需要的资料都分别拉出来整理成阵列,这样才能带给比例尺去做运算
。
// map 资料集
const xData = data.map((i) => i['县市']);
//由於台电提供的A售电数度是带有千分号的字串,我们要先把它处理成数字
const yData = data.map((i) => parseInt(i['A.售电量(度)'].split(',').join('')));
整理出XY轴的资料集後,接着来画XY轴的比例尺与轴线吧
// 设定要给 X 轴用的 scale 跟 axis
const xScale = d3.scaleBand()
.domain(xData)
.range([margin*2, rwdSvgWidth - margin/2]) // 宽度
.padding(0.2)
const xAxis = d3.axisBottom(xScale)
// 呼叫绘制x轴、调整x轴位置
const xAxisGroup = svg.append("g")
.call(xAxis)
.attr("transform", `translate(0,${rwdSvgHeight - margin})`)
.selectAll("text") // 调整刻度文字标签倾斜
.attr("transform", "translate(-10,0)rotate(-45)")
.style("text-anchor", "end");
// 设定要给 Y 轴用的 scale 跟 axis
const yScale = d3.scaleLinear()
.domain([0, d3.max(yData)])
.range([rwdSvgHeight - margin, 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)`)
再来就是画长条图啦!长条图我们会使用< rect >来建立,要特别注意的是 height 的部分。由於svg都是由原点从上而下绘制,如果我们想绘制正确高度的长条图,就要把高度设定成从「图表高度 — 直条图高度」的地方开始绘制
,才能画出正确高度的图表
// 开始建立长条图
const bar = svg.selectAll("rect")
.data(data)
.join("rect")
.attr("x", d => xScale(d['县市'])) // 让长条图在刻度线中间
.attr("y", d => yScale(parseInt(d['A.售电量(度)'].split(',').join(''))))
.attr("width", xScale.bandwidth())
.attr("height", d => {
return (rwdSvgHeight - margin) - yScale(parseInt(d['A.售电量(度)'].split(',').join('')))
})
.attr("fill", "#69b3a2")
.attr('cursor', 'pointer')
这样就建立完长条图啦!长条图真的算是最基本间单的图表~
为了增加点难度,我们来加上 mousehover 跟 mouseleave 的效果。 < rect > 绑定的资料藏在 d.target.__data__
中,找到之後就能把文字标签设定成每个DOM元素各别绑定的资料
bar.on("mouseover", handleMouseOver)
.on("mouseleave", handleMouseLeave)
function handleMouseOver(d, i){
// console.log(d)
// console.log(d.target.__data__)
d3.select(this)
.attr('fill', 'red')
// 加上文字标签
svg.append('text')
.attr('class', 'infoText')
.attr('y', yScale(parseInt(d.target.__data__['A.售电量(度)'].split(',').join('')))-20)
.attr("x", xScale(d.target.__data__['县市']) + 50)
.style('fill', '#000')
.style('font-size', '18px')
.style('font-weight', 'bold')
.style("text-anchor", 'middle')
.text(d.target.__data__['A.售电量(度)'] + '度');
}
function handleMouseLeave(){
d3.select(this)
.attr('fill', '#69b3a2')
// 移除文字标签
svg.select('.infoText').remove()
}
这样就完成啦!基础的长条图范例就到这边~明天再来看看长条图二部曲:复数长条图吧!
最後附上本章的程序码:想看完整程序码的请上 Github,想直接操作图表的则去 Github Page 吧!请自行取用~
<<: 【Day 21】卷积神经网路(Convolutional Neural Network, CNN)(上)
昨天我们以setInterval的方式,将指针放到时钟上并设定间隔,每秒钟执行1次setTime函式...
突然发现日历事件被误删?我们曾丢失/误删过 iPhone 的行事历档案。那麽我们如何才能从 iPho...
本篇同步发布於个人Blog: [PoEAA] Data Source Architectural P...
前情提要 一般在开发的时候,有些功能可能自己写不出来,但是网路上已经有别人写好的,那我们只需要将其引...
上一次我们完成了 ContextMenu 的部分,ContextMenu 也有了属於自己的 View...