canvas圖表(2) - 折線圖


原文地址:canvas圖表(2) - 折線圖
話說這天氣一冷啊, 就患懶癌, 就不想碼代碼, 就想着在床上舒舒服服看視頻. 那順便就看blender視頻, 學習下3D建模, 如果學會了建3D模型, 那我的webGL技術就大有用處啊,可以獨立開發小游戲了😂, 當然是玩笑了。但首先還是把canvas圖表系列先弄完吧, 今天就弄折線圖。

效果請看:折線圖https://edwardzhong.github.io/sites/demo/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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM