canvas图表(3) - 饼图

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

效果请看:饼图

it

功能点包括:

  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);
                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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,928评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,077评论 4 62
  • 一:canvas简介 1.1什么是canvas? ①:canvas是HTML5提供的一种新标签 ②:HTML5 ...
    GreenHand1阅读 4,676评论 2 32
  • 有些时候,感觉谁都可以,又感觉谁都不可以,果然还是喜欢一个人的生活,单身久了,会上瘾的。
    蛇精谭小美阅读 119评论 0 2
  • 星星在月光中沉浮,端量我熟睡的悲伤,潮湿在夜蔼中低垂,弥漫我泣血的怀念。爸爸,我敬爱的公公,你离开我们已经十年...
    戎缱儿阅读 321评论 5 2