话说这天气一冷啊, 就患懒癌, 就不想码代码, 就想着在床上舒舒服服看视频. 那顺便就看blender视频, 学习下3D建模, 如果学会了建3D模型, 那我的webGL技术就大有用处啊,可以独立开发小游戏了😂, 当然是玩笑了。但首先还是把canvas图表系列先弄完吧, 今天就弄折线图。
效果请看:折线图http://ufcjd3.coding-pages.com/dist/chartline.html
主要功能点包括:
组织数据;
绘制;
数据动画的实现;
清屏并重绘画面;
鼠标事件的处理。
大部分的技术在上一节的*canvas图表(1) - 柱状图 *实现了, 所以这节内容其实是比较简单的。比较麻烦一点的就是折线图的动画了,所以重点就看一下这部分的代码。
使用方式 使用方式,和柱状图基本是一样的,我们这里表示的是气温变化图。
var con=document .getElementById('container' );var line = new Line(con);line.init({ title:'未来一周气温变化' , xAxis:{ data:['周一' ,'周二' ,'周三' ,'周四' ,'周五' ,'周六' ,'周日' ] }, yAxis:{ name:'温度' , formatter:'{value} °C' }, series:[ { name:'最高气温' , data:[11 , 11 , 15 , 13 , 12 , 13 , 10 ] }, { name:'最低气温' , data:[1 , -2 , 2 , 5 , 3 , 2 , 0 ] } ] })
代码结构 折线图对象大体和柱状图一致,只是部分方法经过重构。
class Line extends Chart { constructor (container){ super (container); } init(opt){ } bindEvent(){ } showInfo(pos,arr){ } clearGrid(index){ } animate(){ } create(){ } initData(){ } draw(){ } }
数据动画 折线图动画实现的是路径绘制特效,懂canvas的基本都知道原理,就是用lineTo绘制路径,最后stroke出来。但这个折线图是分段的,所以要分情况处理,主要难点就是获取两个点之间的坐标。
仔细思考下如何实现绘制路径动画,因为我们知道x轴总长度,所以可以让x依次递增,再求出x对应的y坐标即可。既然知道了两个点的坐标,还知道了x坐标,根据同角度等比例三角形原理,很容易求出y坐标。
还有就是更新状态的位移动画了,这个就更加简单了,根据当前位置和要将要移动到的位置对比,进行对应的增减即可。
animate(){ var that=this , ctx=this .ctx, obj,h=0 , isStop=true ; (function run ( ) { ctx.clearRect(0 ,that.padding+that.paddingTop-5 ,that.W,that.H-2 *that.padding-that.paddingTop+4 ); that.drawY(); ctx.save(); ctx.translate(that.padding,that.H-that.padding); isStop=true ; for (var i=0 ,item;i<that.animateArr.length;i++){ item=that.animateArr[i]; if (item.hide) continue ; ctx.strokeStyle=item.color; ctx.lineWidth=item.data[0 ].w; item.isStop=true ; if (item.create){ for (var j=0 ,jl=item.data.length;j<jl;j++){ obj=item.data[j]; if (obj.y>=obj.h){ obj.y=obj.p=obj.h; } else { obj.y+=obj.vy; item.isStop=false ; } ctx.beginPath(); ctx.moveTo(obj.x+obj.w/2 ,-obj.y); ctx.lineTo(obj.x+obj.w/2 ,-1 ); ctx.stroke(); } } else { for (var j=0 ,jl=item.data.length;j<jl;j++){ obj=item.data[j]; if (obj.p>obj.h){ h=obj.y-4 ; if (h<obj.h){ obj.y=obj.p=obj.h; } } else { h=obj.y+4 ; if (h>obj.h){ obj.y=obj.p=obj.h; } } if (obj.p!=obj.h){ obj.y=h; item.isStop=false ; } ctx.beginPath(); ctx.moveTo(obj.x+obj.w/2 ,-obj.y); ctx.lineTo(obj.x+obj.w/2 ,-1 ); ctx.stroke(); } } if (!item.isStop){isStop=false ; } } ctx.restore(); if (isStop)return ; requestAnimationFrame(run); }()) }
清屏并重绘画面 在画面上要实现动态效果的时候,需要清屏,重新绘制画面,如果指定了某个区间,就在该区间上画标志线,同时该区间的圆心放大。
clearGrid(index){ var that=this , obj, r=5 , ctx=this .ctx; ctx.clearRect(0 ,0 ,that.W,that.H); this .drawAxis(); this .drawTag(); this .drawY(); ctx.save(); ctx.translate(that.padding,that.H-that.padding); if (typeof index== 'number' ){ obj=that.animateArr[0 ].data[index]; ctx.lineWidth=1 ; ctx.strokeStyle='hsla(0,0%,70%,1)' ; ctx.moveTo(obj.x,-that.H+that.paddingTop+2 *that.padding); ctx.lineTo(obj.x,0 ); ctx.stroke(); } for (var i=0 ,item,il=that.animateArr.length;i<il;i++){ item=that.animateArr[i]; if (item.hide)continue ; ctx.lineWidth=4 ; ctx.strokeStyle=item.color; ctx.fillStyle='#fff' ; ctx.beginPath(); for (var j=0 ,obj,jl=item.data.length;j<jl;j++){ obj=item.data[j]; if (j==0 ){ ctx.moveTo(obj.x,-obj.h); } else { ctx.lineTo(obj.x,-obj.h); } } ctx.stroke(); for (var j=0 ,jl=item.data.length;j<jl;j++){ obj=item.data[j]; ctx.strokeStyle=item.color; ctx.lineWidth=index===j?6 :4 ; r=index===j?10 :5 ; ctx.beginPath(); ctx.arc(obj.x,-obj.h,r,0 ,Math .PI*2 ,false ); ctx.stroke(); ctx.fill(); } } ctx.restore(); }
事件处理 mousemove 一是触摸标签显示手形,二是滑过画面区域的时候擦除并重绘画面,选中的折线的圆形扩大,同时绘制指示线,具体看clearGrid方法。
mousedown某个击标签就会显示隐藏对应分组,创建状态执行路径绘制动画,而更新状态这是执行位移动画。
bindEvent(){ var that=this , ctx=that.ctx, canvas=that.canvas, xl=this .xAxis.data.length, xs=(that.W-2 *that.padding)/(xl-1 ), index=0 ; this .canvas.addEventListener('mousemove' ,function (e ) { var isLegend=false ; if (isLegend) return ; if (pos.y*2 >that.padding+that.paddingTop && pos.y*2 <that.H-that.padding && pos.x*2 >that.padding && pos.x*2 <that.W-that.padding){ canvas.style.cursor='pointer' ; for (var i=0 ;i<xl;i++){ if (pos.x*2 >i*xs){ index=i; } } that.clearGrid(index); var arr=[]; for (var j=0 ,item,l=that.animateArr.length;j<l;j++){ item=that.animateArr[j]; if (item.hide)continue ; arr.push({name :item.name, num :item.data[index].num}) } that.showInfo(pos,arr); ctx.restore(); } else { that.tip.style.display='none' ; 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.series[i].hide=!that.series[i].hide; that.create(); break ; } } },false ); }
最后 所有图表代码请看chart.js