原文地址:canvas图表(3) - 饼图
这几天把canvas图表都优化了下,动画效果更加出色了,可以说很逼近Echart了。刚刚写完的饼图,非常好的实现了既定的功能,交互的动画效果也是很棒的。

效果请看:饼图https://edwardzhong.github.io/sites/demo/dist/chartpie.html

功能点包括:

  1. 组织数据;
  2. 画面绘制;
    3. 数据动画的实现;
    4. 鼠标事件的处理。

使用方式

饼图的数据方面要简单很多,因为不用多个分组的数据。把所有的数据相加得出总数,然后每个数据分别求出百分比,有了百分比再相乘360度的弧度得出每个数据在圆盘中对应的要显示的角度。

    var con=document.getElementById('container');
    var pie=new Pie(con);
    pie.init({
        title:'网站用户访问来源',
        toolTip:'访问来源',
        data:[
            {value:435, name:'直接访问'},
            {value:310, name:'邮件营销'},
            {value:234, name:'联盟广告'},
            {value:135, name:'视频广告'},
            {value:1548, name:'搜索引擎'}
        ]
    });

代码结构

因为为了同时实现新增动画和更新动画,这次的代码结构经过了重构和优化,跟之前的有比较大的区别。

    class Line extends Chart{
        constructor(container){
            super(container);
        }
        // 初始化
        init(opt){

        }
        // 绑定事件
        bindEvent(){

        }
        // 显示信息
        showInfo(pos,arr){

        }
        // 清除内容再绘制
        clearGrid(index){

        }
        // 执行数据动画
        animate(){

        }
        // 执行
        create(){

        }
        // 组织数据
        initData(){

        }
        // 绘制
        draw(){

        }
    }

组织数据

这次把组织数据的功能单独拎了出来,这样方便重用和修改。然后还要给动画对象增加是否创建的属性create和上次最后更新的度数last,为什么呢?因为我们要同时实现创建和更新图形的动画效果。

    initData(){
        var that=this,
            item,
            total=0;
        if(!this.data||!this.data.length){return;}
        this.legend.length=0;
        for(var i=0;i<this.data.length;i++){
            item=this.data[i];
            // 赋予没有颜色的项
            if(!item.color){
                var hsl=i%2?180+20*i/2:20*(i-1);
                item.color='hsla('+hsl+',70%,60%,1)';
            }
            item.name=item.name||'unnamed';

            this.legend.push({
                hide:!!item.hide,
                name:item.name,
                color:item.color,
                x:50,
                y:that.paddingTop+40+i*50,
                w:80,
                h:30,
                r:5
            });

            if(item.hide)continue;
            total+=item.value;
        }

        for(var i=0;i<this.data.length;i++){
            item=this.data[i];
            if(!this.animateArr[i]){//创建
                this.animateArr.push({
                    i:i,
                    create:true,
                    hide:!!item.hide,
                    name:item.name,
                    color:item.color,
                    num:item.value,
                    percent:Math.round(item.value/total*10000)/100,
                    ang:Math.round(item.value/total*Math.PI*2*100)/100,
                    last:0,
                    cur:0
                });
            } else {//更新                
                if(that.animateArr[i].hide&&!item.hide){
                    that.animateArr[i].create=true;
                    that.animateArr[i].cur=0;
                } else {
                    that.animateArr[i].create=false;
                }
                that.animateArr[i].hide=item.hide;
                that.animateArr[i].percent=Math.round(item.value/total*10000)/100;
                that.animateArr[i].ang=Math.round(item.value/total*Math.PI*2*100)/100;
            }
        }
    }

绘制

饼图的绘制功能很简单,因为不用坐标系,只需要绘制标题和标签列表。

    draw(){
        var item,ctx=this.ctx;
        ctx.fillStyle='hsla(0,0%,30%,1)';
        ctx.strokeStyle='hsla(0,0%,20%,1)';
        ctx.textBaseLine='middle';
        ctx.font='24px arial';
        
        ctx.clearRect(0,0,this.W,this.H);
        if(this.title){
            ctx.save();
            ctx.textAlign='center';
            ctx.font='bold 40px arial';
            ctx.fillText(this.title,this.W/2,70);
            ctx.restore();
        }
        ctx.save();
        for(var i=0;i<this.legend.length;i++){
            item=this.legend[i];
            // 画分类标签
            ctx.textAlign='left';
            ctx.fillStyle=item.color;
            ctx.strokeStyle=item.color;
            roundRect(ctx,item.x,item.y,item.w,item.h,item.r);
            ctx.globalAlpha=item.hide?0.3:1;
            ctx.fill();
            ctx.fillText(item.name,item.x+item.w+20,item.y+item.h-5);
        }
        ctx.restore();
    }

执行绘制饼图动画

动画区分了创建和更新,这样用户很容易就能看出数据的比例关系变化,也就更加的直观。创建就是从0弧度到指定的弧度,只有数值的增加;而更新动画就要区分增加和减少的情况,因为当用户点击某个标签的时候,会隐藏显示某个分类的数据,于是需要重新计算每个分类的比例,那么相应的分类百分比就会增加或减少。我们根据当前最新要达到的比例ang和已经执行完的当前比例last的进行对比,相应执行增加和减少比例,动画原理就是这样。

canvas绘制圆形context.arc(x,y,r,sAngle,eAngle,counterclockwise);只要我们指定开始角度和结束角度就会画出披萨饼一样的效果,所有的披萨饼加起来就是一个圆。

    animate(){
        var that=this,
            ctx=that.ctx,
            canvas=that.canvas,
            item,startAng,ang,
            isStop=true;

        (function run(){
            isStop=true;
            ctx.save();
            ctx.translate(that.W/2,that.H/2);
            ctx.fillStyle='#fff';
            ctx.beginPath();
            ctx.arc(0,0,that.H/3+30,0,Math.PI*2,false);
            ctx.fill();
            for(var i=0,l=that.animateArr.length;i<l;i++){
                item=that.animateArr[i];
                if(item.hide)continue;
                startAng=-Math.PI/2;
                that.animateArr.forEach((obj,j)=>{
                    if(j<i&&!obj.hide){startAng+=obj.cur;}
                });

                ctx.fillStyle=item.color;
                if(item.create){//创建动画
                    if(item.cur>=item.ang){
                        item.cur=item.last=item.ang;
                    } else {
                        item.cur+=0.05;
                        isStop=false;
                    }
                } else {//更新动画
                    if(item.last>item.ang){
                        ang=item.cur-0.05;
                        if(ang<item.ang){
                            item.cur=item.last=item.ang;
                        }
                    } else {
                        ang=item.cur+0.05;
                        if(ang>item.ang){
                            item.cur=item.last=item.ang;
                        }
                    }
                    if(item.cur!=item.ang){
                        item.cur=ang;
                        isStop=false;
                    }
                }

                ctx.beginPath();
                ctx.moveTo(0,0);
                ctx.arc(0,0,that.H/3,startAng,startAng+item.cur,false);
                ctx.closePath();
                ctx.fill();
            }
            ctx.restore();
            if(isStop) {
                that.clearGrid();
                return;
            }
            requestAnimationFrame(run);
        }());
    }

交互处理

执行完动画后,我这里再执行了一遍清除绘制,这个也是鼠标触摸标签和饼图时的对应动画方法,会绘制每个分类的名称描述,更方便用户查看。

    clearGrid(index){
        var that=this,
            ctx=that.ctx,
            canvas=that.canvas,
            item,startAng=-Math.PI/2,
            len=that.animateArr.filter(item=>!item.hide).length,
            j=0,angle=0,
            r=that.H/3;
        ctx.clearRect(0,0,that.W,that.H);
        that.draw();
        ctx.save();
        ctx.translate(that.W/2,that.H/2);

        for(var i=0,l=that.animateArr.length;i<l;i++){
            item=that.animateArr[i];
            if(item.hide)continue;
            ctx.strokeStyle=item.color;
            ctx.fillStyle=item.color;
            angle=j>=len-1?Math.PI*2-Math.PI/2:startAng+item.ang;
            ctx.beginPath();
            ctx.moveTo(0,0);
            if(index===i){
                ctx.save();
                // ctx.shadowColor='hsla(0,0%,50%,1)';
                ctx.shadowColor=item.color;
                ctx.shadowBlur=5;
                ctx.arc(0,0,r+20,startAng,angle,false);
                ctx.closePath();
                ctx.fill();
                ctx.stroke();
                ctx.restore();
            } else {
                ctx.arc(0,0,r,startAng,angle,false);
                ctx.closePath();
                ctx.fill();
            }
            //画分类描述
            var tr=r+40,tw=0,
                tAng=startAng+item.ang/2,
                x=tr*Math.cos(tAng),
                y=tr*Math.sin(tAng);

            ctx.lineWidth=2;
            ctx.lineCap='round';
            ctx.beginPath();
            ctx.moveTo(0,0);
            ctx.lineTo(x,y);
            if(tAng>=-Math.PI/2&&tAng<=Math.PI/2){
                ctx.lineTo(x+30,y);
                ctx.fillText(item.name,x+40,y+10);
            } else {
                tw=ctx.measureText(item.name).width;//计算字符长度
                ctx.lineTo(x-30,y);
                ctx.fillText(item.name,x-40-tw,y+10);
            }
            
            ctx.stroke();
            startAng+=item.ang;
            j++;
        }
        ctx.restore();
    }

事件处理

mousemove的时候,触摸标签和触摸饼图都是基本相同的效果,选中的分类扩大半径,同时增加阴影,以达到凸出来的动画效果,具体实现请看上面的clearGrid方法。判断是否点中都是使用isPointInPath这个api,之前已经介绍过,不再细讲。

mousedown某个击标签就会显示隐藏对应分类,每次触发就会看到饼图的比例变化的动画效果,这个和之前的柱状图和折线图的功能一致。

    bindEvent(){
        var that=this,
            canvas=that.canvas,
            ctx=that.ctx;
        if(!this.data.length) return;
        this.canvas.addEventListener('mousemove',function(e){
            var isLegend=false;
            var box=canvas.getBoundingClientRect(),
                pos = {
                x:e.clientX-box.left,
                y:e.clientY-box.top
            };
            // 标签
            for(var i=0,item,len=that.legend.length;i<len;i++){
                item=that.legend[i];
                roundRect(ctx,item.x,item.y,item.w,item.h,item.r);
                // 因为缩小了一倍,所以坐标要*2
                if(ctx.isPointInPath(pos.x*2,pos.y*2)){
                    canvas.style.cursor='pointer';
                    if(!item.hide){
                        that.clearGrid(i);
                    }
                    isLegend=true;
                    break;
                }
                canvas.style.cursor='default';
                that.tip.style.display='none';
            }

            if(isLegend) return;
            // 图表
            var startAng=-Math.PI/2;
            for(var i=0,l=that.animateArr.length;i<l;i++){
                item=that.animateArr[i];
                if(item.hide)continue;
                ctx.beginPath();
                ctx.moveTo(that.W/2,that.H/2);
                ctx.arc(that.W/2,that.H/2,that.H/3,startAng,startAng+item.ang,false);
                ctx.closePath();
                startAng+=item.ang;
                if(ctx.isPointInPath(pos.x*2,pos.y*2)){
                    canvas.style.cursor='pointer';
                    that.clearGrid(i);
                    that.showInfo(pos,that.toolTip,[{name:item.name,num:item.num+' ('+item.percent+'%)'}]);
                    break;
                }
                canvas.style.cursor='default';
                that.clearGrid();
            }

        },false);
        this.canvas.addEventListener('mousedown',function(e){
            e.preventDefault();
            var box=that.canvas.getBoundingClientRect();
            var pos = {
                x:e.clientX-box.left,
                y:e.clientY-box.top
            };
            for(var i=0,item,len=that.legend.length;i<len;i++){
                item=that.legend[i];
                roundRect(ctx,item.x,item.y,item.w,item.h,item.r);
                if(ctx.isPointInPath(pos.x*2,pos.y*2)){
                    that.data[i].hide=!that.data[i].hide;
                    that.create();
                    break;
                }
            }
        },false);

    }

最后

所有图表代码请看chart.js

内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!