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文件。

Logo

前往低代码交流专区

更多推荐