D3 二维图表的绘制系列(二十一)仪表盘图

上一篇: 河流图 https://blog.csdn.net/zjw_python/article/details/98592543

下一篇: 桑基图 https://blog.csdn.net/zjw_python/article/details/98611559

代码结构和初始化画布的Chart对象介绍,请先看 https://blog.csdn.net/zjw_python/article/details/98182540

本图完整的源码地址: https://github.com/zjw666/D3_demo/tree/master/src/gauge/basicGauge

1 图表效果

在这里插入图片描述

2 数据

仪表盘图的数据很简单,就是一个值,其被包含到config配置中了,没有单独的数据文件

3 关键代码

一些样式配置、数值的范围、仪表盘的弧度分段等

const config = {
    margins: {top: 80, left: 80, bottom: 50, right: 80},
    textColor: 'black',
    title: '仪表盘',
    totalAngle: 270,
    totalValue: 100,
    showValue: 90,
    width: 25,
    domain: [0, 20, 80, 100],
    lineColor: 'white',
    animateDuration: 500
}

尺度转换,根据画布大小计算仪表盘的宽度

/* ----------------------------计算半径-----------------------------  */

const radius = d3.min([chart.getBodyWidth()*0.95, chart.getBodyHeight()*0.95])/2;

渲染仪表盘轮廓,总体上就是一个具有内半径的大角度扇形,计算一下角度,用arc渲染即可

/* ----------------------------渲染弧形仪表盘轮廓------------------------  */
chart.renderSlices = function(){
    const drawAngles = config.domain.map((value, i, domain) => {
        const angle = value / config.totalValue * config.totalAngle;
        if (i !== domain.length-1){
            return {
                startAngle: (angle - config.totalAngle/2) * Math.PI/180,
                endAngle: (domain[i+1] / config.totalValue * config.totalAngle - config.totalAngle/2) * Math.PI/180
            }
        }
    });

    drawAngles.pop();

    const arc = d3.arc()
                    .outerRadius(radius)
                    .innerRadius(radius - config.width > 0 ? radius - config.width : 10);

    const slices = chart.body()
                        .append('g')
                        .attr('transform', 'translate(' + chart.getBodyWidth()/2 + ',' + chart.getBodyHeight()/2 + ')')
                        .selectAll('.arc')
                        .data(drawAngles);

          slices.enter()
                  .append('path')
                  .attr('class', (d,i) => 'arc arc-' + i)
               .merge(slices)
                  .attr('fill', (d,i) => chart._colors(i % 10))
                  .attr('d', arc);

          slices.exit()
                  .remove();
}

轮廓渲染好了,下一步该渲染坐标轴和文本标签,这里通过三角函数计算角度,然后直接渲染,比较简单,注意坐标轴的长度变化即可

/* -----------------------渲染环形坐标轴和标签------------------------  */
chart.renderTicks = function(){
    const drawAngles = [];
    for (let i=-config.totalAngle/2; i<=config.totalAngle/2+0.01; i+=config.totalAngle/50){
        drawAngles.push(i * Math.PI/180);
    }

    const ticks = chart.body()
                        .append('g')
                        .attr('transform', 'translate(' + chart.getBodyWidth()/2 + ',' + chart.getBodyHeight()/2 + ')')
                        .selectAll('.ticks')
                        .data(drawAngles);

          ticks.enter()
                 .append('g')
                 .attr('class', 'ticks')
               .merge(ticks)
                 .each(drawTicks)
                 .each(drawLabels);

    function drawTicks(d, i){
        if (i === 0 || i === 50) return;
        const innerRadius = (i % 5===0 ? radius-config.width : radius-config.width/3)
        d3.select(this)
            .append('line')
            .attr('stroke', config.lineColor)
            .attr('x1', Math.sin(d) * radius)
            .attr('y1', -Math.cos(d) * radius)
            .attr('x2', Math.sin(d) * innerRadius)
            .attr('y2', -Math.cos(d) * innerRadius)
    }

    function drawLabels(d, i){
        let textAnchor = 'end';
        if (i === 25) textAnchor = 'middle';
        if (i % 5 === 0){
            const textRadius = radius - config.width - 10;
            d3.select(this)
                .append('text')
                .attr('class', 'label')
                .attr('x', Math.sin(d) * textRadius)
                .attr('y', -Math.cos(d) * textRadius)
                .attr('dy', 5.5)
                .attr('stroke', config.textColor)
                .attr('text-anchor', d<0?'start':textAnchor)
                .text(i/50 * config.totalValue);
        }
    }
}

然后最后一步渲染指针,这里的指针是一个左右对称四边形,用polygon渲染即可,通过控制其旋转角度显示指定的值,注意实现动画过渡效果,要保证指针的旋转方向正确

/* --------------------------渲染指针--------------------------  */
chart.renderPointer = function(){
    const verticalLongOffset = Math.floor((radius-config.width-10) * 0.8);
    const verticalShortOffset = Math.floor(verticalLongOffset * 0.12);
    const horizontalOffset = Math.floor(verticalShortOffset * 0.6);

    const points = [
        "0," + verticalShortOffset,
        horizontalOffset + ",0",
        "0," + (-verticalLongOffset),
        -horizontalOffset + ",0"
    ].join(" ");

    const pointer = chart.body()
                            .selectAll(".pointer")
                            .data([config.showValue]);

          pointer.enter()
                    .append('polygon')
                    .attr('class', 'pointer')
                    .attr('points', points)
                    .attr('shape-rendering', 'geometricPrecision')
                    .attr('stroke', 'none')
                    .attr('transform', 'translate(' + chart.getBodyWidth()/2 + ',' + chart.getBodyHeight()/2 + ') ' + 'rotate(' + (-0.5)*config.totalAngle +')')
                 .merge(pointer)
                    .attr('fill', (d) => {
                        let i = 0;
                        while (i<config.domain.length-1 && config.domain[i] < d){i++}
                        return chart._colors((i-1) % 10);
                    })
                    .transition().duration(config.animateDuration)
                    .attrTween('transform', rotateTween);

          pointer.exit()
                    .remove();

    function rotateTween(d){
        let lastAngle = this.last || 0;
        let angleDiff = d - lastAngle;
        this.last = d;
        return function(t){
            return 'translate(' + chart.getBodyWidth()/2 + ',' + chart.getBodyHeight()/2 + ') ' + 'rotate(' + ((lastAngle + angleDiff*t)/config.totalValue-0.5)*config.totalAngle +')';
        }
    }

}

大功告成!!!

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

猜你喜欢

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