本篇大纲:范例图表一、范例图表二
昨天看完了单线折线图怎麽绘制,今天我们就来看看多线折线图
吧!
有时候我们拿到的资料会分成很多组,需要去比较不同组别的数据,这时候就可以使用这种多条线的折线图去绘制,不但能看出单项目的数据,还能比较不同项目的数据差异。因此我们今天就用两种不同互动效果的范例来练习画多线折线图吧!
为了跟真实世界接轨,我们这次也使用政府的开放资料来绘制折线图~这次要使用的是各观测站的降雨量资料,这里有提供 json档跟 csv 档,之前我们已经用 csv 档绘制过图表了,这次就换成用 json 档吧!我们先找到资料 json档的网址,打开来後资料结构是这样
接着我们先来看看第一张图表的结构与互动效果
现在开始来绘制图表吧!一样先取资料并建立 svg 元素
// css
.chart {
width: 100%;
min-width: 300px;
margin: auto;
position:relative;
}
// html
<div class="chart"></div>
由於我只想抓2017年的资料,所以就先用 filter 的方法把不是2017年的资料都过滤掉,再用得到的资料去绘制图表
// js
// 先取资料
let data = []
async function getData() {
dataGet = await d3.json('https://data.coa.gov.tw/Service/OpenData/TransService.aspx?UnitId=5n9c3AlEJ2DH')
data = dataGet.filter(d=>d.observeDate.substr(0,4)==='2017') // 只取2017的资料
drawChart()
};
getData()
// RWD
function drawChart(){
// 删除原本的svg.charts,重新渲染改变宽度的svg
d3.select('.chart svg').remove();
// RWD 的svg 宽高
const rwdSvgWidth = parseInt(d3.select('.chart').style('width')),
rwdSvgHeight = rwdSvgWidth*0.8,
margin = 60
const svg = d3.select('.chart')
.append('svg')
.attr('width', rwdSvgWidth)
.attr('height', rwdSvgHeight);
// 接下来的程序码放这边...
// 接下来的程序码放这边...
// 接下来的程序码放这边...
}
d3.select(window).on('resize', drawChart);
然後我们要把 XY轴需要的资料分别抓出来。这边要注意的是有些月份没有降雨量资料,我们要先把这些没有资料的部分转成0,否则建立轴线时会出错
// map 资料集
const xData = data.map((i) => i.observeDate.substr(5,6));
const yData = data.map((i)=>{
let rainfall = parseFloat(i.rainfall)
return rainfall = rainfall || 0 // 没有的资料换成0
})
接下来用整理好的资料去建立比例尺跟XY轴
// 设定要给 X 轴用的 scale 跟 axis
const xScale = d3.scaleLinear()
.domain(d3.extent(xData))
.range([margin, rwdSvgWidth - margin]) // 宽度
.nice()
// rwd X轴的刻度
const xAxis = d3.axisBottom(xScale)
.ticks(8)
.tickFormat(d => d + '月')
// 呼叫绘制x轴、调整x轴位置
const xAxisGroup = svg.append("g")
.call(xAxis)
.attr("transform", `translate(0,${rwdSvgHeight - margin})`)
// 设定要给 Y 轴用的 scale 跟 axis
const yScale = d3.scaleLinear()
.domain(d3.extent(yData))
.range([rwdSvgHeight - margin, margin]) // 数值要颠倒,才会从低往高排
.nice()
const yAxis = d3.axisLeft(yScale)
// 呼叫绘制y轴、调整y轴位置
const yAxisGroup = svg.append("g")
.call(yAxis)
.attr("transform", `translate(${margin},0)`)
再来是关键!我们要建立分组的组别,把哪些资料是同一组(同一观测站)的抓出来,这个就是用来建立不同条折线
// 把资料按照 name 分组
const sumName = d3.group(data, d => d.observatory);
之後用 d3 提供的 d3.schemeCategory10
方法设定折线的颜色,如果不知道这是什麽以及要怎麽设定,欢迎去看我的 D3 Scale篇章
const color = d3.scaleOrdinal()
.domain(data.map(d=>d.item))
.range(d3.schemeCategory10);
都设定好後就是建立折线图的阶段啦!
// 开始建立折线图
const line = svg.selectAll('.line')
.data(sumName)
.join('path')
.attr("fill", "none")
.attr("stroke", d => color(d))
.attr("stroke-width", 1.5)
.attr("d", d => {
return d3.line()
.x((d) => xScale(d.observeDate.substr(5,6)))
.y((d) => {
let rainfall = parseFloat(d.rainfall)
rainfall = rainfall || 0
return yScale(rainfall)
})
(d[1]) // 只取资料的部分带入
})
这样最基本的多线折线图就完成罗
再来我们要在线段上绑定滑鼠事件来显示 tooltips。其实绑在线段上不是很明智的选择,应该要另外建立颜色标签去标明每条线代表哪个观测站,因为线段太细了,使用者很难准确的滑到线段上。不过这边只是想示范折线图的滑鼠事件,所以就先绑吧!
我们要先建立 tooltips标签,如果不知道什麽是 tooltips,欢迎去看我的另一篇 tooltips 文章
// 建立浮动的资料标签
const nameTag = d3.select('.chart')
.append('div')
.attr('class', 'nameTag')
.style('position', 'absolute')
.style("opacity", 0)
.style("background-color", "black")
.style("border-radius", "5px")
.style("padding", "10px")
.style("color", "white")
最後把线段绑定上滑鼠事件,并且滑过时显示tooltips。tooltips 内容是绑定在 _data_
内的观测站名称资料,位置则是用 d3.pointer( )去设定。
line.style('cursor', 'pointer')
.on('mouseover', handleMouseover)
// 滑鼠事件
function handleMouseover(d){
const pt = d3.pointer(event, this)
// console.log(pt)
// console.log(d.target.__data__[0])
nameTag.style("opacity",1)
.html(d.target.__data__[0])
.style('left', (pt[0]+10) + 'px')
.style('top', (pt[1]+ 10) + 'px')
}
完成~最终结果就是这样
除了滑鼠事件之外,多线折线图更常搭配的互动效果是放大缩小。由於多线折线图的折线通常很密集,为了方便使用者查找某个时期的资料,我们往往会搭配刷子选取+放大的效果去绘制图表
这个范例比较难一点,会运用到 d3.brush 的功能,如果不清楚该怎麽使用 d3.brush的话,欢迎去看我的 d3.brush 篇章。现在我们开始吧!
这次因为有缩放功能,因此我决定把两年的所有资料都纳入,不像范例一只取2017年的资料。
// css
.chart2{
width: 100%;
min-width: 300px;
margin: auto;
position:relative;
}
// html
<div class="chart2"></div>
let dataTime = []
async function getDataTime() {
dataGet = await d3.json('https://data.coa.gov.tw/Service/OpenData/TransService.aspx?UnitId=5n9c3AlEJ2DH')
data = dataGet
drawTimeChart()
};
getDataTime()
// RWD
function drawTimeChart(){
// 删除原本的svg.charts,重新渲染改变宽度的svg
d3.select('.chart2 svg').remove();
// RWD 的svg 宽高
const rwdSvgWidth = parseInt(d3.select('.chart2').style('width')),
rwdSvgHeight = rwdSvgWidth*0.8,
margin = 60
const svg = d3.select('.chart2')
.append('svg')
.attr('width', rwdSvgWidth)
.attr('height', rwdSvgHeight);
// 接下来的程序码放这边...
// 接下来的程序码放这边...
// 接下来的程序码放这边...
}
d3.select(window).on('resize', drawTimeChart);
这次的X轴绘制方式跟上一个范例有点不同,我们改成使用 scaleTime 的方法来建立。如果不清楚不同 scale 的设定,请见我这一篇关於 scale的文章。因为要使用 scaleTime,我们要先设定一个方法把日期用 d3.timeParse( ) 转成d3能够读懂的数据,如果不知道该怎麽用 d3.timeParse( ),请看这篇文章
// 设定 format 时间的方法
function parseTime(d){
return d3.timeParse("%Y-%m")(d)
}
接着我们先把XY轴需要的资料分别整理出来,同时整理要分组的资料、设定折线颜色
// map 资料集
const xData = data.map((i) => i.observeDate);
const yData = data.map((i)=>{
let rainfall = parseFloat(i.rainfall)
return rainfall = rainfall || 0 // 没有的资料换成0
})
// 把资料按照 name 分组
const sumName = d3.group(data, d => d.observatory);
// 设定轴线颜色
const color = d3.scaleOrdinal()
.domain(data.map(d=>d.item))
.range(d3.schemeCategory10);
再来就是关键!X轴的部分要用刚刚设定好的 parseTime
方法,把 xData 资料带入 d3.scaleTime( )
中建立比例尺,再依此绘制XY轴
// 设定要给 X 轴用的 scale 跟 axis
const xScale = d3.scaleTime()
.domain(d3.extent(data, d => parseTime(d.observeDate)))
.range([margin, rwdSvgWidth - margin]) // 宽度
// rwd X轴
const xAxis = d3.axisBottom(xScale)
// .tickFormat(d3.timeFormat('%b')) // 只显示缩写月份
const xAxisGroup = svg.append("g")
.call(xAxis)
.attr("transform", `translate(0,${rwdSvgHeight - margin})`)
// 设定要给 Y 轴用的 scale 跟 axis
const yScale = d3.scaleLinear()
.domain(d3.extent(yData))
.range([rwdSvgHeight - margin, margin]) // 数值要颠倒,才会从低往高排
.nice()
const yAxis = d3.axisLeft(yScale)
// 呼叫绘制y轴、调整y轴位置
const yAxisGroup = svg.append("g")
.call(yAxis)
.attr("transform", `translate(${margin},0)`)
再来也是关键!因为我们要让使用者用 brush 选定范围後缩放图表,所以要建立一个在XY轴内的范围,确保图表缩放後不会超过XY轴,然後把折线图跟 brush 都绑定在这个范围内
// 建立一个画布范围,超过此画布的画面都不会被渲染,这样才能控制缩放的大小
const clip = svg.append("defs")
.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", margin)
.attr("y", margin)
.attr("width", rwdSvgWidth-margin*2)
.attr("height", rwdSvgHeight-margin*2)
// 加上brush
const brush = d3.brushX()
.extent([[margin, margin], [rwdSvgWidth-margin, rwdSvgHeight-margin]])
.on("end", updateChart)
// 开始建立折线图
const line = svg.append('g')
.attr("clip-path", "url(#clip)")
line.selectAll('.line')
.data(sumName)
.join('path')
.attr('class', 'line')
.attr("fill", "none")
.attr("stroke", d => color(d))
.attr("stroke-width", 1.5)
.attr("d", d => {
return d3.line()
.x((d) => xScale(parseTime(d.observeDate)))
.y((d) => {
let rainfall = parseFloat(d.rainfall)
rainfall = rainfall || 0
return yScale(rainfall)
})
(d[1]) // 只取资料的部分带入
})
// add brush
line.append('g')
.attr('class', 'brush')
.call(brush)
接着要设定放开刷子後要进行的 updateChart 方法。这边很重要,也是缩放的关键!
// brush end function
function updateChart(event, d){
// brush 的范围,会返还一个[x0, x1]的阵列
extent = event.selection
if(extent){
// xScale.invert 是把返还的x0跟x1变成xscale接受的数值
xScale.domain([xScale.invert(extent[0]), xScale.invert(extent[1]) ])
// 移除brush的灰色区块
line.select(".brush").call(brush.move, null)
}
// 按照更新的domain范围值重新渲染图表
xAxisGroup.transition().duration(1000).call(d3.axisBottom(xScale))
line
.selectAll('.line')
.transition()
.duration(1000)
.attr("d", d => {
return d3.line()
.x((d) => xScale(parseTime(d.observeDate)))
.y((d) => {
let rainfall = parseFloat(d.rainfall)
rainfall = rainfall || 0
return yScale(rainfall)
})
(d[1]) // 只取资料的部分带入
})
}
最後,图表一直不停放大也不是办法,我们希望双击svg时可以缩回原本的比例,因此要设定svg被双击时,xScale会回到原本设定的比例尺,并且一样要重新渲染一次X轴跟折线图
//双击 svg 缩回原本大小
svg.on('dblclick', function(){
// 回到原本的大小
xScale.domain(d3.extent(data, d => parseTime(d.observeDate)))
// 重新呼叫渲染轴线跟折线
xAxisGroup.transition().duration(1000).call(d3.axisBottom(xScale))
line
.selectAll('.line')
.transition()
.duration(1000)
.attr("d", d => {
return d3.line()
.x((d) => xScale(parseTime(d.observeDate)))
.y((d) => {
let rainfall = parseFloat(d.rainfall)
rainfall = rainfall || 0
return yScale(rainfall)
})
(d[1]) // 只取资料的部分带入
})
})
这样就能顺利完成啦!
这几篇写完後,基础的图表就讲得差不多了,而且还包含了不少进阶的互动功能~明天开始就要进入进阶一点的图表啦!希望可以平安顺利的完结铁人赛!!
最後附上本章的程序码:想看完整程序码的请上 Github,想直接操作图表的则去 Github Page 吧!请自行取用~
<<: 【day23】存local端 帐号 (SharedPreferences)
>>: [Day 24] -『 GO语言学习笔记』- 复合型别 - 阵列(Array) (II)
PostgreSQL Event Trigger 初探 什麽是Event Trigger? 这里的e...
上一篇中介绍了如何使用 FormControl 建立单个表单控制元件,也介绍了如何使用 FormGr...
FB登入 以上功能都完成後就要开始环境的建置了 第一步:下载android studio sdk(如...
本周专题发表成功! 先附上我本人制作的精美海报 分享一下我跟partner 忙了一个月的成果~ 去年...
前言 天啊今天整个非常赶,23:00到家打开发现今天是hard的题目(446. Arithmetic...