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