D3.js画图:3D动态饼图(齿轮图)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了D3.js画图:3D动态饼图(齿轮图)相关的知识,希望对你有一定的参考价值。

参考技术A 通常画可视化图的工具很多,除了d3.js,还有echarts.js等。

通过比较,看起来ECharts.js更容易上手,但是因为我需要更灵活更符合个性定制化的工具,所以选了d3.js。

经过一段时间的磨炼,从折线图、闭合路径图、蜂窝图、直角坐标、极坐标都玩了个遍。
那这次就来个3D的吧,其实d3.js做3D的图不是很容易的,有更好的选择,但我认准了d3.js,一条道走到黑吧(想起高中数学老师说的话,当你解题解到一半时发现有更好的办法,不,赶紧忘掉,接着当前的方法,只要方法没错,总能解出来,也许会傻一点,但是一定会有正确的结果;如果中途放弃,也许另一个方法更快更聪明,但也许更慢或者错误,不算到最后,谁都不知道谁最准确。我选择相信他的话,于是。。。我成了程序员O(∩_∩)O哈哈~)。

有人鄙视拿来主义,要我说,你能拿来那是你的本事,如果还能在此基础上做出更好的东西,何乐而不为呢?
每个人时间有限,每个项目也有deadline,不可能从每一个螺丝钉怎么拧开始学起,不然怎么会有那么多五花八门的框架,会有封装好的组件和接口,正因为有人已经做了前期工作,所以时间才能省下来做更有意义的事情,这就是站在巨人肩上的道理所在吧。
但是我们得明白拿来的东西的原理,以及出了问题该怎么解决的能力。然后才能做出更厉害的东西。

首选当然是官网的例子咯,目测搜了一圈,终于找到一个3D Donut。就是你了,我的巨人。
把该地址的donut3d.js拷贝下来作为画3D饼图的基础js,待会会在此基础上修改,以满足我的要求(长的像齿轮的要求)。

那我们就一睹她的芳容吧。
如果这张图符合你的要求,那就打住,不用往下看了,直接看官网例子即可。
注意d3版本的问题,如果你用d3.v3.js,恭喜你,啥也不用改,直接拿来用;如果你用d3.v5.js,那稍微改下方法,比如d3.v5.js没有d3.layout,所以d3.layout.pie改成d3.pie。我就是那个不幸的人,用的d3.v5.js。没关系,改起来很快,运行下,看哪里有错,就改哪里,O(∩_∩)O哈哈~so easy!

还是先上个我已经改好之后的3D饼图(齿轮图)吧,方便说明。
其实显示的时候是个动态的,一节一节显示出齿轮的。
背景是黑乎乎的,据说现在流行黑乎乎的背景,显得有科技感,技术也要赶时髦啊,我这么fashion的人,做出来的东西也要fashion啊O(∩_∩)O~

从以上分析可以看出,难啃的骨头在第4点。这个图断断续续花了3天时间才搞定,为啥是断断续续呢,因为还有其他工作要做嘛,你懂得。
那就按顺序一条一条实现,总有一天我们的愿望都能实现!

首先新建svg及设置宽高。

我是切分成了32个小齿轮(包含透明的),如果你想分的更细,可以分成40或50个,只要你觉得好看就行。
既然要分成32个小立体快,那数据也要切分成32个。

通过以上处理,把数据整合成可以生成齿轮的完整数据dataset。

如果不增加左侧面和右侧面,那调用donut3d.js的draw方法后,会生成什么样的图形呢?

请各位仔细看。

是不是有种被掏空的感觉?如果你觉得这样挺好看,那也行,打住吧,后面就不用再看了;如果你想补齐其他面,请耐心往下看。
经过观察和比较,增加左侧面和右侧面就能填满空虚的心啦啦啦~
这次要在donut3d.js这个巨人身上添砖加瓦咯。

然后再用新增加的两个方法画出左右侧面。

终于填满需要的每一面,看上去像个立体齿轮图了。
这个图是很久之前做的,当时花了很长时间调试,每一个面有4条边,定位2个点,再加上高度和内半径,就可以计算出4个点,然后就可以画出4条边,最后填充颜色,一个面就完成了。
最近整理文档时觉得有必要写出来,方便以后查阅和探讨,也告诉自己积累是一个长期过程,不急不躁,慢慢来,一步一步完成既定目标,总有一天你会走遍技术的每个角落。
现在我整理成vue组件,传一个百分比的参数,就可以显示3D齿轮图了,我的3D齿轮图也成巨人啦。

canvas+js实现动态饼图效果

canvas+js实现动态饼图效果

参考了一些网上的例子,利用canvas+js实现动态饼图效果demo,包括鼠标移入图块颜色变化以及带有tooltip提示。实现逻辑可分为三个步骤:

1、首先利用canvas画布原理画饼图。
2、添加鼠标移入监听事件,获取鼠标当前坐标。
3、判断当前鼠标坐标是否在所画饼图扇形区域,改变饼图颜色和显示tooltip提示。

1、画饼图

利用canvas实现一个饼图效果,我们首先需要知道canvas画布画图原理以及相关需要的API。
W3school这样介绍canvas,还不太了解的朋友可以先去熟悉熟悉。

什么是 Canvas?
HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像。
画布是一个矩形区域,您可以控制其每一像素。
canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。

创建 Canvas 元素
向 HTML5 页面添加 canvas 元素。
规定元素的 id、宽度和高度:

  <canvas id="myCanvas" width="200" height="100"></canvas>

通过上面的介绍,即画图的html部分我们已经了解,只需要定义一个带有宽高及id的canvas即可,接下来便是通过js来绘制图形,首先我们需要利用id来寻找canvas元素。

let el = document.getElementById("myCanvas");

然后创建画布的绘制对象:

let cxt = el.getContext("2d"); 

接下来,即可利用canvas的相关API来绘制我们需要的图像,canvas的API可参考《HTML Canvas 参考手册》。本次画饼图设计需要的API整理如下:

ctx.beginPath(); // 起始一条路径,或重置当前路径
ctx.fillStyle; // 设置或返回用于填充绘画的颜色、渐变或模式 ctx.moveTo(x, y); // 把路径移动到画布中的指定点,不创建线条
ctx.arc(x, y, radius, sAngle, eAngle, false); // 创建弧/曲线(用于创建圆形或部分圆)
ctx.fill(); // 填充当前绘图(路径)

以上只列出了几个化饼图的关键API,其他包括饼图具体实现、小图标以及文字的相关API详细参见下面的代码。

2、添加鼠标监听

利用js的addEventListener,添加所绘画布鼠标监听,获取当前鼠标移入的坐标。

// 鼠标位置监听
getPosition(element) {
    let mouseTimer = null;
    element.addEventListener('mousemove', (e) => {
        e = e || window.event;
        if ( e.ofSfsetX || e.offsetX==0 ) {
            this.mousePosition.x = e.offsetX;
            this.mousePosition.y = e.offsetY;
        } else if ( e.layerX || e.layerX==0 ){
            this.mousePosition.x = e.layerX;
            this.mousePosition.y = e.layerY;
        } 

        // 监听tooltip显示
        let el =  document.getElementById('self-tooltip');
        el.style.visibility = 'hidden';
        
        clearTimeout(mouseTimer);
        mouseTimer = setTimeout(() => {
            this.drawPie(element, this.pieData);
        }, 50)
    });
}, 

3、鼠标移入,改变饼图样式

在绘制饼图方法中调用getPosition(element)方法,利用ctx.isPointInPath()API判断当前鼠标坐标是否在所绘制扇形中,以改变相关样式和显示tooltip提示。

// 监听鼠标是否移动到绘制扇形处
if (ctx.isPointInPath(this.mousePosition.x, this.mousePosition.y)) {
    // 鼠标移上改变颜色
    ctx.fillStyle = '#F56C6C';
    // tooltip显示
    let el =  document.getElementById('self-tooltip');
    el.innerHTML = data.labels[index] + ":" + (percent * 100).toFixed(2) + "%";
    el.style.left = `${this.mousePosition.x + 15}px`;
    el.style.top = `${this.mousePosition.y + 15}px`;
    el.style.visibility = 'visible';
}

4、详细代码如下

<template>
    <div class="box-style">
        <canvas :id="canvasId" :width="width"  :height="height" style="border: 1px solid #909399"></canvas>
        <span id="self-tooltip" class="tooltip-text"></span>
    </div>
</template>

<script>
export default {
    props: {
        pieData: {
            type: Object,
            default: function() {
                return {
                    colors: ['#AFB4DB', '#91CC75', '#FFC333', '#FFC0CB', '#73C0DE'], // 颜色
                    labels: ['周一', '周二', '周三', '周四', '周五'], //标签
                    values: [10, 20, 30 , 40, 50], //值
                    radius: 100 //圆半径
                };
            }
        },
        width: {
            type: Number,
            default: function() {
                return 400;
            }
        },
        height: {
            type: Number,
            default: function() {
                return 400;
            }
        },
        canvasId: {
            type: String,
            default: function() {
                return 'pie';
            }
        }
    },
    data() {
        return {
            // 鼠标移动是坐标
            mousePosition: {}
        }
    },
    mounted() {
        let pieElement = document.getElementById(this.canvasId);
        this.drawPie(pieElement, this.pieData);
        this.getPosition(pieElement);
    },
    methods: {
        // 画饼状图
        drawPie(element, data) {
            // 在画布上初始化绘图环境
            let ctx = element.getContext('2d');
            let drawData = data.values; // 画图数据
            let sum = this.getSum(drawData); //获取绘制数据的总和
            let sAngle = 0; // 扇形开始的角度
            let eAngle; // 结束的角度
            let x = element.width / 2;
            let y = element.height / 2; //圆心坐标
            let radius = data.radius;  // 圆半径
            let xMarker = 20; // 标记坐标
            let yMarker = 20; // 标记坐标
            drawData.forEach((value, index) => {
                // 绘制饼图
                let percent = value / sum; // 计算每个数据的占比,根据占比求扇形的弧度,即每个扇形结束的角度
                eAngle = sAngle + Math.PI * 2 * (percent);
                ctx.beginPath(); //新路径
                ctx.fillStyle = data.colors[index];
                ctx.moveTo(x, y);
                ctx.arc(x, y, radius, sAngle, eAngle, false);
                // 监听鼠标是否移动到绘制扇形处
                if (ctx.isPointInPath(this.mousePosition.x, this.mousePosition.y)) {
                    // 鼠标移上改变颜色
                    ctx.fillStyle = '#F56C6C';
                    // tooltip显示
                    let el =  document.getElementById('self-tooltip');
                    el.innerHTML = data.labels[index] + ":" + (percent * 100).toFixed(2) + "%";
                    el.style.left = `${this.mousePosition.x + 15}px`;
                    el.style.top = `${this.mousePosition.y + 15}px`;
                    el.style.visibility = 'visible';
                }

                // 绘制左侧标记
                ctx.moveTo(xMarker, yMarker);
                // 绘制矩形
                ctx.fillRect(xMarker, yMarker, 30, 10);
                // 绘制文字
                ctx.font = 'blod 14px';
                ctx.txtalgin = 'center';
                ctx.textBaseline = 'top';
                
                ctx.fillText(data.labels[index] + ":" + (percent * 100).toFixed(2) + "%", xMarker + 35, yMarker);

                // 填充以上绘制
                ctx.fill(); 
                // 绘制下一个扇形时初始值改变
                sAngle = eAngle;
                yMarker += 20;
            });
        },
        // 求和
        getSum(data) {
            let sum = 0;
            data.map(value => {
                sum += value;
            });
            return sum;
        },
        // 鼠标位置监听
        getPosition(element) {
            let mouseTimer = null;
            element.addEventListener('mousemove', (e) => {
                e = e || window.event;
                if ( e.ofSfsetX || e.offsetX==0 ) {
                    this.mousePosition.x = e.offsetX;
                    this.mousePosition.y = e.offsetY;
                } else if ( e.layerX || e.layerX==0 ){
                    this.mousePosition.x = e.layerX;
                    this.mousePosition.y = e.layerY;
                } 

                // 监听tooltip显示
                let el =  document.getElementById('self-tooltip');
                el.style.visibility = 'hidden';
                
                clearTimeout(mouseTimer);
                mouseTimer = setTimeout(() => {
                    this.drawPie(element, this.pieData);
                }, 50)
            });
        }, 
    }
}
</script>

<style lang="less" scoped>
.box-style {
    position: relative;
    display: inline-block;
    width: 100%;
    height: 100%;
    // 画布
    canvas {
        cursor: pointer;
    }
} 
// 提示工具
.tooltip-text {
    position: absolute;
    width: 150px;
    z-index: 10;
    background-color: #909399;
    color: #fff;
    border-radius: 5px;
    padding: 5px;
    left: 0;
    top: 0;
    visibility: hidden;
}
</style>>

5、效果

以上是关于D3.js画图:3D动态饼图(齿轮图)的主要内容,如果未能解决你的问题,请参考以下文章

d3.js:饼图布局 - 调整角度以创建快门效果

使用 d3.js 和 TypeScript 绘制饼图时出现编译错误

使用 d3.js 向饼图添加工具提示

如何在d3.js中修复饼图标签

标签外弧(饼图)d3.js

使用 D3.js 创建频率计数饼图