这几天把canvas图表都优化了下,动画效果更加出色了,可以说很逼近Echart了。刚刚写完的饼图,非常好的实现了既定的功能,交互的动画效果也是很棒的。
效果请看:饼图http://ufcjd3.coding-pages.com/dist/chartpie.html
功能点包括:
- 组织数据;
- 画面绘制;
- 数据动画的实现;
- 鼠标事件的处理。
使用方式
饼图的数据方面要简单很多,因为不用多个分组的数据。把所有的数据相加得出总数,然后每个数据分别求出百分比,有了百分比再相乘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=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