JavaScript Canvas 实现自定义多折线图

JavaScript Canvas 实现自定义多折线图

大家好,我是梦辛工作室的灵,今天给大家讲一讲如何实现一个好看的自定义多折线图,按惯例来,先看实现效果如下:
在这里插入图片描述
画面效果还是可以吧,颜色和内容都可以自定义哦,给大家讲一下我的绘制逻辑,我们需要将绘图封装成一个对象,并先给定一些默认值,比如画布的宽和高,我们将画布分成上下两部分,定义上面30%用于绘制标题和留出一些空白,图表占用60%,剩下的10%留白,用于显示横坐标的数值,且这样好看点:
在这里插入图片描述
还要 提供一个 装数据的对象,我这样定义的该对象:
在这里插入图片描述
title会在中间标题那里显示,并按照对应的的颜色值color,data里面放的就是图表的数据了,并对外提供set 方法便于外部修改数据内容,再设置时需要对所有数据进行排序并获取到数据中的 横纵坐标的最大值和最小值,等下我们计算坐标的时候会用到
在这里插入图片描述
接下来就是开始绘图了,将绘图操作放至 同一个绘图函数:
在这里插入图片描述
画图前先清空画布,防止多次画图,然后设置当前的样式信息,开始填充左侧标题的文本,顶部标题的文本,图表上方的单位 文本,顶部的划线信息,接下来就是绘制图表内容
在这里插入图片描述
设置线的颜色和宽度,将画笔移动到 图表的左上角,即x是 图表的左边距 y 是图表的上边距,然后绘制到
x是 图表的左边距 y 是 图表的上边距 加上图表的高度,继续话横坐标线,先计算每个横坐线的间隔高度,
然后按照每个高度进行画横线; 然后绘制最大值和最小值的显示;

接下来就是对每个点开始绘制了,这里得先提供几个方法,就是 根据 x 坐标 计算出当前 横坐标应对应的数据,根据数据 计算出 当前的 x 坐标, 根据 y 坐标 计算出 当前纵坐标对应的 数据,根据数据计算出 当前的 y 坐标;大致的 计算 方法 就是 用 最大值 和 最小值之差 除以 最大坐标值 和 最小坐标值 之差 ,获得一个对应的比例,然后用当前的坐标值乘以这个比例 在加上 最小值 ,就可以得到当前坐标值对应的数据值,具体如下:
在这里插入图片描述
这样的话 ,等下我们数据的折线就简单了,直接 提交 数值 就可以得到对应的 坐标值,画图就行
在这里插入图片描述
在这里插入图片描述
最后还有一个 小弹窗的绘制,这个就需要监听鼠标移动事件,并判断是否在 图表里,若在允许绘制弹窗,不在则不允许绘制图表:
在这里插入图片描述
判断鼠标是否在图表内在这里插入图片描述
然后在draw方法的最后面执行一个 绘制弹窗的操作
在这里插入图片描述
在这里插入图片描述
这里我增加了一个判读,当弹窗要超出界线时,将弹窗修改绘制方向,
最后我放上所有的源代码,供大家参考,如有不对之处,还请大家指出
先是html文件:

<!doctype html>
<html> 
<!DOCTYPE >
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html,charset=utf-8"> 
	<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0"> 
</head> 
<body> 
	<div id="Contents" >
		<div style="width:100%;height:auto;margin-top:40px;text-align: center;">
			 <canvas id="myCanvas" width="900" height="600" style="background:#fff;margin-top:10px;border:1px solid #b0b0b0;">您的浏览器不支持 HTML5 canvas 标签。</canvas>
		</div>   
	</div> 

 <script type="text/javascript" src="chart.js"></script>
 <script type="text/javascript" src="index.js"></script>
</body>
</html>

然后是 index.js:

 
window.onload =  (argument) => { 
	  
	var chartutil = new drawChart({
		 	width:900,
		 	height:600,
		 	canvasid:"myCanvas"
		 }); 
		
	chartutil.setDataInfo([]); 
	chartutil.draw(); 

}
  

然后是chart.js:


/**
  * 梦辛工作室 
  *	灵 v1.0.1
  */

function drawChart(options) {

	var canvasid = options.canvasid;	//画布id
	var width = options.width || 800;	//画布宽 默认值 800
	var height = options.height || 400;	//画布高 默认值 400
	var chartLeft = width * 0.1;	//图表的左边距
	var chartTop = height * 0.3;	//图表的上边距
	var widthChart = options.width * 0.8;	//图表宽度
	var heightChart = options.height * 0.6; 	//图表高度
	var maxY = 100;				//默认众坐标最大值
	var minY = 0;				//默认众坐标最小值
	var maxX = 1000;			//默认横坐标最大值
	var minX = 0;				//默认横坐标最小值
	var lineNunber = 7;	//分成几条线
	var unitstr = options.unit || "单位(°)";	//单位显示 
	var unitFontSize = 12;					//单位显示 字体大小
	var lineShortWidth = 5;					//横线的宽度
	var leftTitle = options.leftTitle || "左边标题";	//图标表左边的标题
	var title = options.title || "中间标题";	//图表中间标题的内容
	var leftTitleFontSize = 15;			//图表左边标题的文字大小
	var numberFontSize = 10;			//图表数字的文字大小
	var titleFontSize = 16;				//中间标题的文字代销
	var datatitleFontSize = 13;			//每条线的标题大小
	var dataTitleCirclePath = 10;		//每条线的标题小圆的直径
	var startLineCirclePath = 8;		//画点的时候 大圆的直径
	var endLineCirclePath = 4;			//画点的时候 小圆的直径
	var isShowDialog = false;			//是否显示 小弹窗
	var curMouseX = 0;					//当前鼠标的X坐标  相对于画布
	var curMouseY = 0;  				//当前鼠标的Y坐标  相对于画布
	var c = document.getElementById(canvasid);	//获取到canvas 对象
	c.onmousemove = canvsMouseMoveLister;		//监听鼠标移动事件
	c.onmouseout = canvsMouseLeave;				//监听鼠标的移出事件
	var ctx = c.getContext("2d"); 
	var date = new Date();

	var dataInfo = [
		{
			title:"X(°)",
			color:"#FF0000",
			data:[{key:20,value:20},{key:30,value:2},{key:50,value:10},{key:70,value:15},{key:80,value:18},{key:100,value:20},{key:300,value:20},{key:500,value:0},{key:600,value:80}]
		},
		{
			title:"Y(°)",
			color:"#00FF00",
			data:[{key:20,value:10},{key:30,value:5},{key:50,value:100},{key:70,value:14},{key:80,value:19},{key:100,value:30},{key:300,value:50}]
		},
		{
			title:"Z(°)",
			color:"#0000FF",
			data:[{key:20,value:5},{key:30,value:50},{key:50,value:10},{key:70,value:90},{key:80,value:80},{key:100,value:60},{key:300,value:50}]
		},
	];

	var _self = this;

	this.init = function(options){
		canvasid = options.canvasid;
		width = options.width || 800;
		height = options.height || 400;
		chartLeft = width * 0.1;
		chartTop = height * 0.3;
		widthChart = options.width * 0.8;
		heightChart = options.height * 0.6; 
		maxY = 100;
		minY = 0;
		maxX = 1000;
		minX = 0;
		lineNunber = 7;	//分成几条线
		unitstr = options.unit || "单位(°)";
		unitFontSize = 12;
		lineShortWidth = 5;
		leftTitle = options.leftTitle || "倾角计-1";
		title = options.title || "采样频率:暂未记录  上报频率:暂未记录  加报频率:暂未记录";
		leftTitleFontSize = 15;
		numberFontSize = 10;
		titleFontSize = 16;
		datatitleFontSize = 13;
		dataTitleCirclePath = 10;
		startLineCirclePath = 8;
		endLineCirclePath = 4;  
		c = document.getElementById(canvasid);
		c.onmousemove = canvsMouseMoveLister;
		c.onmouseout = canvsMouseLeave;
		ctx = c.getContext("2d");
	}



	this.setDataInfo = function(dataInfos){
		
		var isSet = {};
		for (var x in dataInfos) {
			var len = dataInfos[x].data.length;
			dataInfos[x].data.sort(sortByValue);
			if (len == 0) {
				continue;
			}
			var curMaxValue = dataInfos[x].data[len - 1].value;
			var curMinValue = dataInfos[x].data[0].value;

			if (!isSet.maxY || maxY < curMaxValue) {
				maxY = curMaxValue; 
				isSet.maxY = true;
			}
			if (!isSet.minY || minY > curMinValue) { 
				minY = curMinValue;
				isSet.minY = true;
			}
			dataInfos[x].data.sort(sortByKey);
			var curMaxKey = dataInfos[x].data[len - 1].key;
			var curMinKey = dataInfos[x].data[0].key;
			if (!isSet.maxX || maxX < curMaxKey) {
				maxX = curMaxKey; 
				isSet.maxX = true;
			}
			if (!isSet.minX || minX > curMinKey) { 
				minX = curMinKey;
				isSet.minX = true;
			} 
		}
		
		if (maxY == minY) {
			maxY += 20;
			minY -= 20;
		}

		if (minX == maxX) {
			maxX += 20;
			minX -= 20;
		}

		date.setTime(maxX * 1000);
		date.setHours(23,59,59);
		maxX = parseInt(date.getTime() / 1000);

		date.setTime(minX * 1000);
		date.setHours(0,0,0);
		minX = parseInt(date.getTime() / 1000);

		dataInfo = dataInfos; 
	}

	this.setMaxX = function(value){
		maxX = value;
	}

	this.setMaxY = function(value){
		maxY = value;
	}

	this.setMinX = function(value){
		minX = value;
	}

	this.setMinY = function(value){
		minY = value;
	}

	this.addMaxX = function(value){
		maxX += value;
	}

	this.addMaxY = function(value){
		maxY += value;
	}

	this.addMinX = function(value){
		minX += value;
	}

	this.addMinY = function(value){
		minY += value;
	}

	
	function canvsMouseMoveLister(event){
		curMouseX = event.offsetX;
		curMouseY = event.offsetY; 
		if (isInChart(curMouseX,curMouseY)) {
			isShowDialog = true;
			_self.draw();
		} else if (isShowDialog){
			isShowDialog = false;
			_self.draw();
		} else {
			isShowDialog = false;
		}
	}

	function canvsMouseLeave(){
		isShowDialog = false;
		_self.draw();
	}

	this.draw = function(){
		if (!canvasid) {
			return;
		}
		ctx.clearRect(0,0,c.width,c.height);
		ctx.fillStyle = "#000000"; 
		ctx.strokeStyle="#000"; 
		//画左侧标题
		ctx.font = "normal normal bold " + leftTitleFontSize + "px arial"; 
		ctx.fillText(leftTitle,chartLeft / 6,chartTop - unitFontSize - leftTitleFontSize * 2);

		//画最顶部标题
		ctx.font = "normal normal normal " + leftTitleFontSize + "px arial"; 
		ctx.fillText(title, (width - leftTitleFontSize * title.length) / 2, leftTitleFontSize * 1.5);

		//画单位
		ctx.font =  "normal normal normal " + unitFontSize + "px arial"; 
		ctx.fillText(unitstr,chartLeft - (unitFontSize * unitstr.length) / 2,chartTop - unitFontSize);

		


		//画竖线	
		ctx.strokeStyle="#AAAAAA"; 
		ctx.lineWidth = 0.5;
		ctx.moveTo(chartLeft,chartTop); 
		ctx.lineTo(chartLeft,chartTop + heightChart); 
		ctx.stroke();
		ctx.strokeStyle="#D1D1D1"; 
		var lineHeight = heightChart / lineNunber;
		//画横线
		ctx.beginPath();
		for (var i = 0;i <= lineNunber;i++) {   
			ctx.moveTo(chartLeft,chartTop + i * lineHeight);  
			ctx.lineTo(chartLeft + widthChart,chartTop + i * lineHeight);   
		} 
		ctx.stroke();
		ctx.strokeStyle="#000000"; 
		ctx.lineWidth = 1;
		ctx.beginPath(); 

		ctx.font = "normal normal normal " + numberFontSize + "px arial";
		//画最大最小值
		date.setTime(maxX * 1000);
		ctx.fillText(formatTime(date),chartLeft + widthChart,chartTop + heightChart + numberFontSize * 2); 
		date.setTime(minX * 1000);
		ctx.fillText(formatTime(date),chartLeft,chartTop + heightChart + numberFontSize * 2); 

		//画左边标尺
		for (var i = 0;i <= lineNunber;i++) {
			var curvalue  = (maxY - (i * lineHeight) / heightChart * (maxY - minY)).toFixed(0);
			ctx.fillText(curvalue,chartLeft - (numberFontSize * curvalue.length) / 1.5 - lineShortWidth - 2,chartTop + i * lineHeight + numberFontSize / 3); 
			ctx.moveTo(chartLeft - lineShortWidth,chartTop + i * lineHeight);
			ctx.lineTo(chartLeft,chartTop + i * lineHeight); 
		} 
		ctx.stroke(); 

		//画 数据标题
		ctx.font = "normal normal normal " + datatitleFontSize + "px arial"; 
		//计算 总长度
		var dataTitleLength = 0; 

		if (dataInfo.length > 0) {

			for (let i in dataInfo) {
			 	dataTitleLength += dataInfo[i].title.length * datatitleFontSize;
			} 

			var datatitleStartPos = (width - (dataTitleLength + (dataTitleCirclePath + dataInfo.length * lineShortWidth) + (dataInfo.length - 1) * 5)) / 2;

			var dataInfoHeight = leftTitleFontSize * 1.5 + datatitleFontSize * 1.5;
			for (let i in dataInfo) {

				var lineStartPos = datatitleStartPos + i *  (dataTitleCirclePath + 2 * lineShortWidth + dataInfo[i].title.length * datatitleFontSize + 5);
				var lineEndPos = lineStartPos + lineShortWidth;
				var lineRightStartPos = lineEndPos + dataTitleCirclePath;
				var lineRightEndPos = lineRightStartPos + lineShortWidth;
				 
				ctx.fillText(dataInfo[i].title,lineRightEndPos + 5,dataInfoHeight + datatitleFontSize / 3);
				ctx.strokeStyle = dataInfo[i].color; 	
				ctx.beginPath(); 
				ctx.moveTo(lineStartPos,dataInfoHeight); 
				ctx.lineTo(lineEndPos,dataInfoHeight); 
				ctx.moveTo(lineRightStartPos,dataInfoHeight);  
				ctx.lineTo(lineRightEndPos,dataInfoHeight); 
				ctx.stroke();
				ctx.beginPath();
				ctx.arc(lineEndPos + dataTitleCirclePath / 2,dataInfoHeight,dataTitleCirclePath / 2,0,2*Math.PI); 
				ctx.stroke();

				//画数据
				
				var data = dataInfo[i].data;
				var dataLen = data.length;
				if (dataLen <= 0) {
					continue;
				}  
				ctx.beginPath();
				for (let x in data) {
					posX = valueXToPos(data[x].key);
					posY = valueYToPos(data[x].value);
					if (x == 0) { 
						ctx.moveTo(posX,posY);
					} else if(x == dataLen - 1){ 
						ctx.lineTo(posX,posY);
					} else {
						ctx.lineTo(posX,posY);
					} 
				}
				ctx.stroke();
				ctx.beginPath();
				var posX = valueXToPos(data[0].key);
				var posY = valueYToPos(data[0].value);
				ctx.arc(posX,posY,startLineCirclePath / 2,0,2*Math.PI);
				ctx.stroke(); 
				ctx.fillStyle = "#FFFFFF";
				ctx.beginPath();
				ctx.arc(posX,posY,startLineCirclePath / 2 - 1,0,2*Math.PI);  
				ctx.fill(); 
				if (dataLen > 1) {
					posX = valueXToPos(data[dataLen - 1].key);
					posY = valueYToPos(data[dataLen - 1].value);
					ctx.beginPath();
					ctx.arc(posX,posY,endLineCirclePath / 2,0,2*Math.PI);
					ctx.stroke();
					ctx.beginPath();
					ctx.arc(posX,posY,endLineCirclePath / 2 - 1,0,2*Math.PI); 
					ctx.fill();  
				}
				ctx.fillStyle = "#000000";
			}	

		}  

		if (!isShowDialog) {
			return;
		} 

		//画当前坐标图
		var dialogWidth = 80;
		var dialogHeight = 30;
		var dialogFontSize = 10;

		//修正偏移值

		//curMouseY -= 10;
		//curMouseX -= 10;

		var curValueY = posYToValue(curMouseY).toFixed(2) + "°";
		var curValueX = posXToValue(curMouseX).toFixed(2);
		date.setTime(curValueX * 1000);
		var showStr = "X:"+ formatTime(date) +  ",Y:" + curValueY;
		dialogWidth = showStr.length * dialogFontSize * 0.7 + 10;
		dialogHeight = dialogFontSize + 6;

		ctx.fillStyle="#000000A0";        
		var dialogStartX = curMouseX - 10 - dialogWidth;
		var dialogStartY = curMouseY - 10 - dialogHeight;

		if (dialogStartX < chartLeft) {
			dialogStartX =  curMouseX + 10;
		}
		if (dialogStartX > chartLeft + widthChart) {
			dialogStartX =   curMouseX - 10 - dialogWidth;
		}

		if (dialogStartY < chartTop) { dialogStartY = curMouseY + 10; } 


		ctx.fillRect(dialogStartX,dialogStartY,dialogWidth,dialogHeight);
		ctx.fillStyle = "#FFFFFF";

		
		ctx.font = "normal normal normal " + dialogFontSize + "px arial"; 
		ctx.fillText(showStr,dialogStartX + 5, dialogStartY + dialogFontSize);

		ctx.strokeStyle="#FF0000";
		ctx.beginPath();

		ctx.moveTo(curMouseX,chartTop);
		ctx.lineTo(curMouseX,chartTop + heightChart);
		ctx.stroke();

		ctx.beginPath();
		ctx.moveTo(chartLeft,curMouseY);
		ctx.lineTo(chartLeft + widthChart,curMouseY);

		ctx.stroke();

	}
	const formatNumber = n => {
	  n = n.toString()
	  return n[1] ? n : '0' + n
	}
	function formatTime(date){
	  const year = date.getFullYear()
	  const month = date.getMonth() + 1
	  const day = date.getDate()
	  const hour = date.getHours()
	  const minute = date.getMinutes()
	  const second = date.getSeconds()

	  return [year, month, day].map(formatNumber).join('-') + ' ' + [hour, minute, second].map(formatNumber).join(':')
	}

	function formatTimeYMD(date){
	  const year = date.getFullYear()
	  const month = date.getMonth() + 1
	  const day = date.getDate() 

	  return [year, month, day].map(formatNumber).join('-')
	}

	function posXToValue(posx){
		var valuex =  ((posx - chartLeft) / widthChart) * (maxX - minX) + minX;
		return valuex;
	}

	function valueXToPos(valuex){
		var posx = ((valuex - minX) / (maxX - minX)) * widthChart + chartLeft; 
		return posx;
	}

	function posYToValue(posy){
		var valuey = maxY - ((posy - chartTop) / heightChart) * (maxY - minY);
		return valuey;
	}

	function valueYToPos(valuey){
		var posy = heightChart - ((valuey - minY) / (maxY - minY)) * heightChart + chartTop; 
		return posy;
	}

	function isInChart(x,y){
		return ((x > chartLeft - 10 && x < chartLeft + widthChart + 10)) && ((y > chartTop - 10 && y < chartTop + heightChart + 10));
	}
}

function sortByKey(a,b){ 
	return a.key - b.key;
}

function sortByValue(a,b){ 
	return a.value - b.value;
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灵神翁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值