JavaScript Canvas implementation of custom polyline graph

Keywords: Javascript REST html5

JavaScript Canvas implementation of custom polyline graph

Hello everyone, I'm the spirit of Mengxin studio. Today, I'll tell you how to realize a good-looking custom polyline chart. According to the Convention, the implementation effect is as follows:

The picture effect is still OK. The color and content can be customized. Let me tell you my drawing logic. We need to encapsulate the drawing into an object, and give some default values first, such as the width and height of the canvas. We divide the canvas into two parts: the top 30% is used to draw the title and leave some blank. The chart takes up 60%, and the rest 10% is left blank To display the value of abscissa, and look like this:

Also provide an object for loading data, which I define as follows:

The title will be displayed in the middle title, and according to the corresponding color value, the data in the data is the data of the chart, and the external set method is provided to facilitate the external modification of the data content. When setting again, we need to sort all the data and obtain the maximum and minimum values of the horizontal and vertical coordinates in the data, which will be used when we calculate the coordinates

The next step is to start drawing. Put the drawing operation into the same drawing function:

Clear the canvas before drawing to prevent drawing multiple times, then set the current style information, start filling in the text of the left title, the text of the top title, the unit text above the chart, and the dash information at the top, and then draw the chart content

Set the color and width of the line, move the brush to the top left corner of the chart, that is, x is the left margin of the chart, y is the top margin of the chart, and then draw to
x is the left margin of the chart y is the top margin of the chart plus the height of the chart. Continue with the abscissa line, first calculate the interval height of each abscissa line,
Then draw horizontal lines according to each height, and then draw the display of maximum and minimum values;

The next step is to start drawing each point. Here we have to provide several methods first, that is, calculate the data corresponding to the current abscissa according to the x coordinate, calculate the current x coordinate according to the data, calculate the data corresponding to the current ordinate according to the y coordinate, and calculate the current y coordinate according to the data. The approximate calculation method is to use the maximum value and the maximum value Divide the difference of small value by the difference between the maximum coordinate value and the minimum coordinate value to obtain a corresponding proportion, then multiply the current coordinate value by this proportion and add the minimum value to obtain the data value corresponding to the current coordinate value, as follows:

In this way, the polyline of our data will be simple. You can directly submit the value to get the corresponding coordinate value and draw a picture


Finally, there is a small pop-up window to draw, which needs to monitor the mouse movement event and determine whether it is in the chart. If it is allowed to draw pop-up window, it is not allowed to draw chart if it is not:

Determine whether the mouse is in the chart
Then a pop-up window drawing operation is performed on the last face of the draw method


Here I add an interpretation. When the pop-up window is going beyond the boundary line, I will change the drawing direction of the pop-up window,
Finally, I put all the source code for your reference. If there are any mistakes, please point out
First, html file:

<!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;">Your browser does not support HTML5 canvas Label.</canvas>
		</div>   
	</div> 

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

Then index.js:

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

}
  

Then chart.js:

/**
  * Mengxin studio 
  *	Ling v1.0.1
  */

function drawChart(options) {

	var canvasid = options.canvasid;	//Canvas id
	var width = options.width || 800;	//Canvas width default 800
	var height = options.height || 400;	//Canvas height default value 400
	var chartLeft = width * 0.1;	//Left margin of chart
	var chartTop = height * 0.3;	//Top margin of chart
	var widthChart = options.width * 0.8;	//Chart width
	var heightChart = options.height * 0.6; 	//Chart height
	var maxY = 100;				//Default multi coordinate maximum
	var minY = 0;				//Default multi coordinate minimum
	var maxX = 1000;			//Default abscissa Max
	var minX = 0;				//Default abscissa minimum
	var lineNunber = 7;	//Divided into several lines
	var unitstr = options.unit || "Unit (°)";	//Unit display 
	var unitFontSize = 12;					//Unit display font size
	var lineShortWidth = 5;					//Width of horizontal line
	var leftTitle = options.leftTitle || "Left heading";	//Title to the left of the icon table
	var title = options.title || "Middle title";	//Contents of the middle title of the chart
	var leftTitleFontSize = 15;			//Text size of the title on the left side of the chart
	var numberFontSize = 10;			//Text size of chart numbers
	var titleFontSize = 16;				//Text selling of middle title
	var datatitleFontSize = 13;			//Title size of each line
	var dataTitleCirclePath = 10;		//Diameter of the title circle of each line
	var startLineCirclePath = 8;		//The diameter of the big circle when drawing points
	var endLineCirclePath = 4;			//The diameter of the small circle when drawing points
	var isShowDialog = false;			//Show pop ups or not
	var curMouseX = 0;					//The X coordinate of the current mouse is relative to the canvas
	var curMouseY = 0;  				//Y coordinate of current mouse relative to canvas
	var c = document.getElementById(canvasid);	//Get to canvas object
	c.onmousemove = canvsMouseMoveLister;		//Listening for mouse movement events
	c.onmouseout = canvsMouseLeave;				//Listen for mouse removal events
	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;	//Divided into several lines
		unitstr = options.unit || "Unit (°)";
		unitFontSize = 12;
		lineShortWidth = 5;
		leftTitle = options.leftTitle || "Inclinometer-1";
		title = options.title || "Sampling frequency: not recorded and reported frequency: not recorded and reported frequency: not recorded yet";
		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"; 
		//Draw the title on the left
		ctx.font = "normal normal bold " + leftTitleFontSize + "px arial"; 
		ctx.fillText(leftTitle,chartLeft / 6,chartTop - unitFontSize - leftTitleFontSize * 2);

		//Draw top title
		ctx.font = "normal normal normal " + leftTitleFontSize + "px arial"; 
		ctx.fillText(title, (width - leftTitleFontSize * title.length) / 2, leftTitleFontSize * 1.5);

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

		


		//Draw vertical lines	
		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;
		//Draw horizontal line
		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";
		//Draw max min
		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); 

		//Draw the left ruler
		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(); 

		//Draw data title
		ctx.font = "normal normal normal " + datatitleFontSize + "px arial"; 
		//Calculate total length
		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();

				//Drawing data
				
				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;
		} 

		//Draw the current coordinate chart
		var dialogWidth = 80;
		var dialogHeight = 30;
		var dialogFontSize = 10;

		//Correction of offset value

		//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;
}
Published 33 original articles, won praise 11, visited 10000+
Private letter follow

Posted by tbales on Sat, 18 Jan 2020 21:20:46 -0800