zoukankan      html  css  js  c++  java
  • canvas图表(3)

    原文地址: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);
    				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

  • 相关阅读:
    AI听说读写
    C#文件夹检测与创建
    全球唯一标识符 System.Guid.NewGuid().ToString();
    mssql不允许删表方法
    SQLServer(MSSQL)、MySQL、SQLite、Access相互迁移转换工具
    sql语句精剪数据长度函数substring(string,int,int)
    代码生成工具
    [Java123] POI CellType 数据类型
    FRM二级备考感想
    【Java123】XML与JSON互相转化
  • 原文地址:https://www.cnblogs.com/edwardloveyou/p/7884050.html
Copyright © 2011-2022 走看看