Vue + D3 动态可视化图实现之一:折线图
GTD数据分析及可视化项目的第一张图表,项目总体介绍见这篇文章。最终效果实现数据集目标是做出世界、地区、国家三级所有选项的图表。原数据集中有地区(region)和国家(country)的编码,增加编码“1”,代表全世界。在数据表中使用这一编码,按年份统计袭击次数,死亡人数,受伤人数。在项目中用json文件记录层级关系及编码和文本的对应关系。二级联动菜单这个不细说了,网上一查教程很多。思路是通过v-
GTD数据分析及可视化项目的第一张图表,项目总体介绍见这篇文章。
最终效果
实现
数据集
目标是做出世界、地区、国家三级所有选项的图表。原数据集中有地区(region)和国家(country)的编码,增加编码“1”,代表全世界。在数据表中使用这一编码,按年份统计袭击次数,死亡人数,受伤人数。在项目中用json文件记录层级关系及编码和文本的对应关系。
二级联动菜单
这个不细说了,网上一查教程很多。思路是通过v-model分别将一级、二级select框绑定到form.type和form.detailedType数据上。一级select框的@change事件绑定到一个函数getType上,函数根据一级select框的值判定类型,然后更新detailedType内容,v-model自动实现二级select框的视图更新。二级select框绑定到函数onGenerate上,根据选择的值(即世界、地区或国家的编码)+指标值(袭击次数,死亡或受伤人数)生成折线图。另外,指标值的@change也要绑定到onGenerate函数。
html部分:
<div id="selectSection">
<select name="type" id="type" @change="getType($event)" v-model="form.type" >
<option v-for="(item,index) in types" :key="index" :value="item.value" :label="item.name"></option>
</select>
<select name="detailedType" id="detailedType" @change="onGenerate()" v-model="form.detailedType" >
<option v-for="(item,index) in detailedTypes" :key="index" :value="item.value" :label="item.name"></option>
</select>
<select name="category" id="category" @change="onGenerate()" >
<option value="attacks">攻击次数</option>
<option value="killed">死亡人数</option>
<option value="wounded">受伤人数</option>
</select>
</div>
js部分:
// 声明及初始化
data() {
return {
// type代表一级地区选择,detailedType代表二级,category代表指标
types: '',
detailedTypes: '',
form: {
type: '',
detailedType: '',
category: ''
}
};
},
created: function() {
this.types = typeSelect.body
this.form.type = this.types[0].value
this.detailedTypes = this.types[0].children
this.form.detailedType = this.detailedTypes[0].value
},
// 省略...
// 更新二级菜单
getType: function(event) {
let type = event.target.value
if (type == 'world') {
this.detailedTypes = this.types[0].children
} else if (type == 'region') {
this.detailedTypes = this.types[1].children
} else if (type == 'country') {
this.detailedTypes = this.types[2].children
}
},
// 生成图表,type为世界、地区或国家的编码,category为袭击次数,死亡,受伤人数指标
onGenerate: function() {
let type = d3.select(this.$el)
.select('#detailedType').node().value
category = d3.select(this.$el)
.select('#category').node().value
this.update(procData(type, category))
}
数据处理
拿到种类编码type和指标值category就可以从数据集中筛选出需要的子集了。完整数据集保存在data数组中,需要做的就是筛选出data中type值与我们的参数type相等的行,再从列中选出年份和我们的参数category。实现如下:
function procData(type, category) {
var filteredData = data.filter(d => d.type == type)
result = filteredData.map(d => ({
year: d.year,
value: d. [category] // 中括号代表使用category变量
}))
return result
}
折线图绘制
终于到重点了。首先,在html中留一个供注入的div。
<div id="line-chart-graph"></div>
在初始化函数中,用d3.select找到该节点,添加一个svg节点作为图形根节点,通过attr为该节点添加属性,再添加一个g节点。
// 变量已在前面声明
// ...
// 该函数需要用户负责在网页加载时调用
init: function() {
data = this.getLineChartData() // 获取数据到data
margin = {
top: 100,
right: 150,
bottom: 30,
left: 50
},
width = totalWidth - margin.left - margin.right,
height = totalHeight - margin.top - margin.bottom;
svg = d3.select("#line-chart-graph")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// ...
然后画坐标轴。
x = d3.scaleLinear().range([0, width]);
xAxis = d3.axisBottom().scale(x).ticks((2018 - 1970) / 5).tickFormat(d3.format("d"));
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "myXaxis")
y = d3.scaleLinear().range([height, 0]);
yAxis = d3.axisLeft().scale(y);
svg.append("g")
.attr("class", "myYaxis")
画(更新)折线在update函数完成。初始状态,统计类型为世界(编码为1),指标为袭击次数。
this.update(procData(1, 'attacks'))
重点来了,update函数的写法。 x轴域固定为1970-2018,y轴域为0到data中最大值。坐标轴的动态更新:
update: function(data) {
x.domain([1970, 2018]);
svg.selectAll(".myXaxis").transition()
.duration(2000)
.call(xAxis);
y.domain([0, d3.max(data, d => d.value)]);
svg.selectAll(".myYaxis")
.transition()
.duration(2000)
.call(yAxis);
// ...
折线的更新,可以感受到d3数据绑定的思路和简便:
var u = svg.selectAll(".lineTest")
.data([data], d => d.year);
u
.enter()
.append("path")
.attr("class", "lineTest")
.merge(u)
.transition()
.duration(2000)
.attr("d", d3.line()
.x(d => x(d.year))
.y(d => y(d.value)))
.attr("fill", "none")
.attr("stroke", "url(#line-gradient)")
.attr("stroke-width", 3.5)
折线根据y值进行梯度染色:
// 注意这段放在上面那段之前
const max = d3.max(data, d => +d.value);
var color = d3.scaleSequential(y.domain(), d3.interpolateTurbo)
svg.append("linearGradient")
.attr("id", "line-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0)
.attr("y1", y(0))
.attr("x2", 0)
.attr("y2", y(max))
.selectAll("stop")
.data(d3.ticks(0, 1, 10))
.join("stop")
.attr("offset", d => d)
.attr("stop-color", color.interpolator());
十字线绘制
鼠标悬浮显示数据的效果会明显增强图表的可交互性。下面来实现这一效果。为了捕获图表区域内的鼠标事件,我们在图表上覆盖一个同样大小的rect,并将鼠标事件绑定到自定义的函数。在init函数中:
svg
.append('rect')
.style("fill", "none")
.style("pointer-events", "all")
.attr('width', width)
.attr('height', height)
.on('mouseover', mouseover)
.on('mousemove', mousemove)
.on('mouseout', mouseout);
完成这三个函数之前,先来想想我们要画哪些东西:一个距离鼠标位置最近的数据点(圆),聚焦在数据点的一条横线+一条竖线,年份文本+指标文本。把它们的定义和属性填好:
focus = svg
.append('g')
.append('circle')
.attr("stroke", "black")
.attr("fill", "black")
.attr('r', 4)
.style("opacity", 0)
line1 = svg
.append('line')
.attr("stroke", "black")
.attr("stroke-width", 1)
.style("opacity", 0)
line2 = svg
.append('line')
.attr("stroke", "black")
.attr("stroke-width", 1)
.style("opacity", 0)
focusText = svg
.append('g')
.append('text')
text1 = focusText
.append('tspan')
.attr('id', 't1')
.style("opacity", 0)
.attr("text-anchor", "left")
.attr("alignment-baseline", "middle")
text2 = focusText
.append('tspan')
.attr('id', 't2')
.style("opacity", 0)
.attr("text-anchor", "left")
.attr("alignment-baseline", "middle")
那么mouseover和mouseout函数就很好写了,就是让这些元素出现和隐藏。
let mouseover = function() {
focus.style("opacity", 0.8)
line1.style("opacity", 0.8)
line2.style("opacity", 0.8)
focusText.style("opacity", 0.9)
text1.style("opacity", 0.9)
text2.style("opacity", 0.9)
}
let mouseout = function() {
focus.style("opacity", 0)
line1.style("opacity", 0)
line2.style("opacity", 0)
focusText.style("opacity", 0)
}
mousemove函数中,用bisect找到鼠标悬浮位置最近的数据点。有了数据点后,就可以将其映射到x,y坐标,将点、线和文字画在正确的位置,同时文本显示数据点的值。
let mousemove = function(e) {
var x0 = x.invert(d3.pointer(e)[0]);
var i = bisect(result, x0, 1); // bisect定义:var bisect = d3.bisector(d => d.year).left;
let selectedData = result[i]
focus
.attr("cx", x(selectedData.year))
.attr("cy", y(selectedData.value))
line1
.attr("x1", 0)
.attr("x2", svgrect.width)
.attr("y1", y(selectedData.value))
.attr("y2", y(selectedData.value))
line2
.attr("x1", x(selectedData.year))
.attr("x2", x(selectedData.year))
.attr("y1", 0)
.attr("y2", svgrect.height)
focusText
.attr("cx", x(selectedData.year) + 20)
.attr("cy", y(selectedData.value) - 50)
text1.text('年份: ' + selectedData.year)
.attr("x", x(selectedData.year) + 20)
.attr("dy", y(selectedData.value) - 50)
text2.text(categoryMap(category) + ': ' + selectedData.value)
.attr("x", x(selectedData.year) + 15)
.attr("dy", "1.5em")
}
源码
见项目总体介绍底部项目链接。本图源码为src/components/LineChart.vue文件。
更多推荐
所有评论(0)