D3 二维图表的绘制系列(十五)雷达图

上一篇: 气泡图 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();
            })
    }

大功告成!!!

发布了250 篇原创文章 · 获赞 88 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/zjw_python/article/details/98486655