上一篇: 气泡图 https://blog.csdn.net/zjw_python/article/details/98485368
下一篇: 矩形树状图 https://blog.csdn.net/zjw_python/article/details/98489369
代码结构和初始化画布的Chart对象介绍,请先看 https://blog.csdn.net/zjw_python/article/details/98182540
本图完整的源码地址: https://github.com/zjw666/D3_demo/tree/master/src/radarChart/basicRadarChart
1 图表效果
2 数据
subject,person1,person2
语文,80,70
数学,95,85
英语,62,80
物理,50,60
化学,70,65
生物,85,40
3 关键代码
导入数据
d3.csv('./data.csv', function(d){
return {
subject: d.subject,
person1: +d.person1,
person2: +d.person2
};
}).then(function(data){
....
一些样式参数配置,例如雷达图背景网格层数,填充颜色等
const config = {
margins: {top: 80, left: 80, bottom: 50, right: 80},
textColor: 'black',
title: '基本雷达图',
radius: 110,
animateDuration: 1000,
tickNum: 5,
axisfillColor: ['white','#ddd'],
axisStrokeColor: 'gray',
pointsColor: 'white',
pointsSize: 3
}
尺度转换,雷达图是正多边形,我们可以以其外接圆的半径作为尺度进行映射
/* ----------------------------尺度转换------------------------ */
chart.scaleRadius = d3.scaleLinear()
.domain([0, 100])
.range([0, config.radius])
首先渲染雷达图的坐标轴,也就是多个嵌套的正多边形背景,这里运用三角函数计算多边形的顶点,并使用polygon
元素进行渲染
/* ----------------------------渲染坐标轴------------------------ */
chart.renderAxes = function(){
// ----渲染背景多边形-----
const points = getPolygonPoints(data.length, config.radius, config.tickNum);
const axes = chart.body().append('g')
.attr('class', 'axes')
.attr('transform', 'translate(' + chart.getBodyWidth()/2 + ',' + chart.getBodyHeight()/2 + ')')
.selectAll('axis')
.data(points);
axes.enter()
.append('polygon')
.attr('class', 'axis')
.merge(axes)
.attr('points', (d) => d)
.attr('fill', (d,i) => i%2 === 0?config.axisfillColor[0]:config.axisfillColor[1])
.attr('stroke', config.axisStrokeColor);
axes.exit()
.remove();
// ----渲染对角线-----
const line = d3.line();
const outerPoints = getOuterPoints(points[0]);
const lines = d3.select('.axes')
.selectAll('.line')
.data(outerPoints);
lines.enter()
.append('path')
.attr('class', 'line')
.merge(lines)
.attr('d', (d) => {
return line([
[0, 0],
[d[0], d[1]]
]);
})
.attr('stroke', config.axisStrokeColor);
lines.exit()
.remove();
//生成背景多边形的顶点
function getPolygonPoints(vertexNum, outerRadius, tickNum){
const points = [];
let polygon;
if (vertexNum < 3) return points;
const anglePiece = Math.PI * 2 / vertexNum;
const radiusReduce = outerRadius / tickNum;
for (let r=outerRadius; r>0; r-=radiusReduce){
polygon = [];
for (let i=0; i<vertexNum; i++){
polygon.push(
Math.sin(i * anglePiece) * r + ',' +Math.cos(i * anglePiece) * r
);
}
points.push(polygon.join(' '));
}
return points;
}
//得到最外层多边形的顶点
function getOuterPoints(outerPoints){
const points = outerPoints.split(' ').map((d) => d.split(','));
return points;
}
}
接着渲染文本标签,要根据文本标签所在的角度值设置不同的text-anchor
样式
/* ----------------------------渲染文本标签------------------------ */
chart.renderText = function(){
const texts = d3.select('.axes')
.selectAll('.label')
.data(data);
texts.enter()
.append('text')
.attr('class', 'label')
.merge(texts)
.attr('x', (d,i) => Math.sin(i * Math.PI * 2 / data.length) * (config.radius + 20))
.attr('y', (d,i) => Math.cos(i * Math.PI * 2 / data.length) * (config.radius + 20))
.attr('text-anchor', (d,i) => computeTextAnchor(data,i))
.attr('dy', 6.5) //由于text-anchor属性在垂向上对齐文字底部,故需要使其对齐文字中部
.text((d) => d.subject);
function computeTextAnchor(data, i){
if (data.length < 3) return;
const angle = i * 360 / data.length;
if ( angle === 0 || Math.abs(angle - 180) < 0.01 ){
return 'middle';
}else if (angle > 180){
return 'end'
}else{
return 'start'
}
}
}
坐标轴和文本标签已经渲染完毕,接下来就渲染数据了,首先转换一下数据结构,然后计算多边形顶点,最后绘制多边形
/* ----------------------------渲染数据多边形------------------------ */
chart.renderPolygons = function(){
const newData = handleData(data);
const polygons = chart.body().selectAll('.polygons')
.data(newData);
polygons.enter()
.append('g')
.attr('class', (d) => 'g-' + d.person)
.attr('transform', 'translate(' + chart.getBodyWidth()/2 + ',' + chart.getBodyHeight()/2 + ')')
.append('polygon')
.attr('class', 'polygon')
.merge(polygons)
.attr('fill', 'none')
.attr('stroke', (d,i) => chart._colors(i))
.attr('stroke-width', '2')
.attr('points', (d,i) => {
const miniPolygon = [];
d.forEach(() => {
miniPolygon.push("0,0")
});
return miniPolygon.join(' ');
})
.transition().duration(config.animateDuration)
.attr('points', generatePolygons);
polygons.exit()
.remove();
//处理数据,转化数据结构,方便渲染
function handleData(data){
const newData = [];
Object.keys(data[0]).forEach((key) => {
if (key !== 'subject'){
const item = [];
item.person = key;
newData.push(item);
}
});
data.forEach((d) => {
newData.forEach((item,i) => {
item.push([d.subject, d['person' + (i+1)]]);
});
});
return newData;
}
//计算多边形的顶点并生成顶点圆圈
function generatePolygons(d,index){
const points = [];
const anglePiece = Math.PI * 2 / d.length;
d.forEach((item,i) => {
const x = Math.sin(i * anglePiece ) * chart.scaleRadius(item[1]);
const y = Math.cos(i * anglePiece) * chart.scaleRadius(item[1]);
//添加交点圆圈
d3.select('.g-' + d.person)
.append('circle')
.attr('class', 'point-' + d.person)
.attr('fill', config.pointsColor)
.attr('stroke', chart._colors(index))
.attr('cx', 0)
.attr('cy', 0)
.attr('r', config.pointsSize)
.transition().duration(config.animateDuration)
.attr('cx', x)
.attr('cy', y)
points.push(x + ',' + y);
});
return points.join(' ');
}
}
最后对多边形绑定交互鼠标交互事件
/* ----------------------------绑定鼠标交互事件------------------------ */
chart.addMouseOn = function(){
d3.selectAll('.polygon')
.on('mouseover', function(d){
const e = d3.event;
const position = d3.mouse(chart.svg().node());
d3.select(e.target)
.attr('stroke-width', '4');
chart.svg()
.append('text')
.classed('tip', true)
.attr('x', position[0]+5)
.attr('y', position[1])
.attr('fill', config.textColor)
.text(d.person);
})
.on('mouseleave', function(){
const e = d3.event;
d3.select(e.target)
.attr('stroke-width', '2');
d3.select('.tip').remove();
})
}