话说这天气一冷啊, 就患懒癌, 就不想码代码, 就想着在床上舒舒服服看视频. 那顺便就看blender视频, 学习下3D建模, 如果学会了建3D模型, 那我的webGL技术就大有用处啊,可以独立开发小游戏了😂, 当然是玩笑了。但首先还是把canvas图表系列先弄完吧, 今天就弄折线图。

效果请看:折线图http://ufcjd3.coding-pages.com/dist/chartline.html

主要功能点包括:

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

大部分的技术在上一节的*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();
// 画y轴刻度
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;
// todo ...

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