Vue + D3 动态可视化图实现之四:甜甜圈(饼状,环形)图

GTD数据分析及可视化项目的第四张图表,项目总体介绍见这篇文章

最终效果

在这里插入图片描述
这张图的分析内容是影响比较严重的10个恐怖组织的详细信息统计,包括攻击手段,攻击目标,攻击武器和攻击地点,指标为攻击次数,以甜甜圈图形式呈现。左上和右下两张图从多到少染色为红到栏,右上和左下则为蓝到红,视觉效果比较好。每个指标的前两到三名会以文字标注内容,指标值和百分比。

实现

因为只统计10个组织,而且指标值仅为攻击次数,数据量较小,故数据表中直接用文字表示。
在这里插入图片描述
数据处理很简单,根据gname筛选即可。主要来看怎么画图。

画图主函数,四张图分别对应数据表中type为1,2,3,4的数据,中心位置为宽高的四分之一,四分之三点,画图模式两张为红到蓝,两张为蓝到红。

			draw: function() {
				svg.selectAll('*').remove()
				this.drawSingle(1, [width / 4, height / 4], 'Red2Blue')
				this.drawSingle(2, [width * 3 / 4, height / 4], 'Blue2Red')
				this.drawSingle(3, [width / 4, height * 3 / 4], 'Blue2Red')
				this.drawSingle(4, [width * 3 / 4, height * 3 / 4], 'Red2Blue')
			},

drawSingle函数,先筛选type,然后transform到对应位置,用d3.pie,d3.arc实现数据转换和环形图绘制,并染上红到蓝或蓝到红的颜色。在0.5到0.8半径的环形外圈,再做一个0.9半径的弧,用于定位提示线的转折点。

			drawSingle: function(type, offset, colorType) {

				let singleData = data.filter(d => d.type == type)

				let g = svg.append("g")
					.attr("transform", "translate(" + offset[0] + "," + offset[1] + ")");

				var pie = d3.pie()
					.padAngle(0.005)
					.sort(null)
					.value(d => d.attacks)

				var data_ready = pie(singleData)

				color = d3.scaleOrdinal()
					.domain(singleData.map(d => d.category))

				if (colorType == 'Red2Blue') {
					color = color.range(d3.quantize(t => d3.interpolateSpectral(t * 0.8 + 0.1), singleData.length))
				} else if (colorType == 'Blue2Red') {
					color = color.range(d3.quantize(t => d3.interpolateSpectral(t * 0.8 + 0.1), singleData.length).reverse())
				}

				var arc = d3.arc()
					.innerRadius(radius * 0.5) // This is the size of the donut hole
					.outerRadius(radius * 0.8)

				// Another arc that won't be drawn. Just for labels positioning
				var outerArc = d3.arc()
					.innerRadius(radius * 0.9)
					.outerRadius(radius * 0.9)

				g
					.selectAll('allSlices')
					.data(data_ready)
					.enter()
					.append('path')
					.attr('d', arc)
					.attr('fill', function(d) {
						return (color(d.data.category))
					})
					.attr("stroke", "white")
					.style("stroke-width", "1px")
					.style("opacity", 0.7)
					.transition().duration(1000) // 环形图生成动画1000ms
					.attrTween('d', function(d) {
						var i = d3.interpolate({
							startAngle: 1.1 * Math.PI,
							endAngle: 1.1 * Math.PI
						}, d);
						return function(t) {
							return arc(i(t));
						};
					})

				// Add the polylines between chart and labels:
				g
					.selectAll('allPolylines')
					.data(data_ready.filter((d, i) => type == 4 ? i < 2 : i < 3)) // 攻击地点通常比例差距较大,取前两名提示即可
																				  // 其它提示前三名
					.enter()
					.append('polyline')
					.attr("stroke", "black")
					.style("fill", "none")
					.attr("stroke-width", 1)
					.attr('points', function(d) {
						var posA = arc.centroid(d) // line insertion in the slice
						var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
						var posC = outerArc.centroid(d); // Label position = almost the same as posB
						var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
						posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
						return [posA, posB, posC]
					})
					.style("opacity", 0)
					.transition().delay(1000).duration(800) // 等待环形图1000ms动画结束后播放提示信息的动画
					.style("opacity", 1)

				let text = g
					.selectAll('allLabels')
					.data(data_ready.filter((d, i) => type == 4 ? i < 2 : i < 3))
					.enter()
					.append('text')
					.attr('transform', function(d) {
						var pos = outerArc.centroid(d);
						var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
						pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
						return 'translate(' + pos + ')';
					})
				text
					.append('tspan')
					.text(d => d.data.category)
					.style('text-anchor', function(d) {
						var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
						return (midangle < Math.PI ? 'start' : 'end')
					})
				text
					.append('tspan')
					.text(d => '次数: ' + d.data.attacks + '     百分比: ' + (100 * d.data.attacks / attackCount.find(item => item.name == d.data.name).attacks).toFixed(2) +'%')
					.attr('x', 0)
					.attr('dy', 20)
					.style('text-anchor', function(d) {
						var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
						return (midangle < Math.PI ? 'start' : 'end')
					})
					.style('white-space', 'pre')
				text
					.style("opacity", 0)
					.transition().delay(1000).duration(800)
					.style("opacity", 1)
			},

源码

项目总体介绍底部项目链接。本图源码为src/components/DonutChart.vue文件。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值