地址:canvas圖表(2) - 折線圖
canvas圖表系列第2節(jié)吹散,我們接著實(shí)現(xiàn)折線圖
效果請(qǐng)看:折線圖
主要功能點(diǎn)包括:
- 組織數(shù)據(jù)施敢;
- 繪制缘挑;
- 數(shù)據(jù)動(dòng)畫的實(shí)現(xiàn)胶滋;
- 清屏并重繪畫面;
- 鼠標(biāo)事件的處理惭婿。
大部分的技術(shù)在上一節(jié)的canvas圖表(1) - 柱狀圖實(shí)現(xiàn)了, 所以這節(jié)內(nèi)容其實(shí)是比較簡(jiǎn)單的箕昭。比較麻煩一點(diǎn)的就是折線圖的動(dòng)畫了,所以重點(diǎn)就看一下這部分的代碼衣陶。
使用方式
使用方式柄瑰,和柱狀圖基本是一樣的,我們這里表示的是氣溫變化圖剪况。
var con=document.getElementById('container');
var line = new Line(con);
line.init({
title:'未來(lái)一周氣溫變化',
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]
}
]
})
代碼結(jié)構(gòu)
折線圖對(duì)象大體和柱狀圖一致教沾,只是部分方法經(jīng)過(guò)重構(gòu)。
class Line extends Chart{
constructor(container){
super(container);
}
// 初始化
init(opt){
}
// 綁定事件
bindEvent(){
}
// 顯示信息
showInfo(pos,arr){
}
// 清除內(nèi)容再繪制
clearGrid(index){
}
// 執(zhí)行數(shù)據(jù)動(dòng)畫
animate(){
}
// 執(zhí)行
create(){
}
// 組織數(shù)據(jù)
initData(){
}
// 繪制
draw(){
}
}
數(shù)據(jù)動(dòng)畫
折線圖動(dòng)畫實(shí)現(xiàn)的是路徑繪制特效译断,懂canvas的基本都知道原理授翻,就是用lineTo繪制路徑,最后stroke出來(lái)孙咪。但這個(gè)折線圖是分段的堪唐,所以要分情況處理,主要難點(diǎn)就是獲取兩個(gè)點(diǎn)之間的坐標(biāo)翎蹈。
仔細(xì)思考下如何實(shí)現(xiàn)繪制路徑動(dòng)畫淮菠,因?yàn)槲覀冎纗軸總長(zhǎng)度,所以可以讓x依次遞增杨蛋,再求出x對(duì)應(yīng)的y坐標(biāo)即可兜材。既然知道了兩個(gè)點(diǎn)的坐標(biāo),還知道了x坐標(biāo)逞力,根據(jù)同角度等比例三角形原理曙寡,很容易求出y坐標(biāo)。
還有就是更新狀態(tài)的位移動(dòng)畫了寇荧,這個(gè)就更加簡(jiǎn)單了举庶,根據(jù)當(dāng)前位置和要將要移動(dòng)到的位置對(duì)比,進(jìn)行對(duì)應(yīng)的增減即可揩抡。
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){//新增繪制路徑動(dòng)畫
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 { //更新位移動(dòng)畫
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);
}())
}
清屏并重繪畫面
在畫面上要實(shí)現(xiàn)動(dòng)態(tài)效果的時(shí)候户侥,需要清屏镀琉,重新繪制畫面,如果指定了某個(gè)區(qū)間蕊唐,就在該區(qū)間上畫標(biāo)志線屋摔,同時(shí)該區(qū)間的圓心放大。
clearGrid(index){
var that=this,
obj, r=5,
ctx=this.ctx;
ctx.clearRect(0,0,that.W,that.H);
// 畫坐標(biāo)系
this.drawAxis();
// 畫標(biāo)簽
this.drawTag();
// 畫y軸刻度
this.drawY();
ctx.save();
ctx.translate(that.padding,that.H-that.padding);
// 畫標(biāo)志線
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 一是觸摸標(biāo)簽顯示手形替梨,二是滑過(guò)畫面區(qū)域的時(shí)候擦除并重繪畫面钓试,選中的折線的圓形擴(kuò)大,同時(shí)繪制指示線副瀑,具體看clearGrid方法弓熏。
mousedown某個(gè)擊標(biāo)簽就會(huì)顯示隱藏對(duì)應(yīng)分組,創(chuàng)建狀態(tài)執(zhí)行路徑繪制動(dòng)畫糠睡,而更新狀態(tài)這是執(zhí)行位移動(dòng)畫挽鞠。
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;
// 鼠標(biāo)位置在圖表中時(shí)
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;
}
}
// 重繪并標(biāo)志選中信息
that.clearGrid(index);
// 獲取處于當(dāng)前位置的信息
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);
}
最后
所有圖表代碼請(qǐng)看chart.js