Canvas JavaScript

Graphing an Ordinary Least Squares Line

This JavaScript program demonstrates how to generate and graph an ordinary least squares line. An ordinary least squares fit involves calculating a regression line for a set of random points. Given a set of points that are assumed to have a linear relationship, a line is fitted that minimizes the least squares error of the vertical distance to the line.

In ordinary least squares, data are collected at regular interval, typically it is time. These intervals are regular and assumed to be without error in the x-coordinate. The y-value is the value that we are concerned with approximating and it is assumed to be a linear function of x with a gaussian error.

Below, we generate a random line in black. Then Gaussian error terms are added to the y-values of that line to generate a theoretical set of points, in red, that we would expect to get from some measurements. The red line is the line that we would calculate as the best least squares fit. The vertical red lines show the error for each y-value relative to the least squares line.

OrdinaryLeastSquares.html

<!DOCTYPE html>
<html>
	<head>
		<title>XoaX.net's Javascript</title>
		<script type="text/javascript" src="OrdinaryLeastSquares.js"></script>
	</head>
	<body onload="Initialize()">
		<div style="width:848px;height:630px;">
			<div style="width:48px;height:630px;float:left;">
				<input id="idHighY" type="text" size="3" style="width:40px;height:16px;"/>
				<div style="width:48px;height:556px;"></div>
				<input id="idLowY" type="text" size="3" style="width:40px;height:16px;"/>
			</div>
			<canvas id="idCanvas" width="800" height ="600" style="background-color: #F0F0F0;float:left;"></canvas>
			<div style="width:800px;height:22px;float:left;">
				<input id="idLowX" type="text" size="3" style="width:40px;height:16px;float:left;"/>
				<div style="width:704px;height:22px;float:left;">
					<label style="color:black;float:left;height:22px;margin-left:5px;">Source Line: <span id="idSourceLine">y = mx + b</span></label>
					<button style="color:black;height:22px;float:left;margin-left:100px;" onclick="Initialize()">Reset</button>
					<label style="color:red;float:right;height:22px;margin-right:5px;">Least Squares Line: <span id="idLeastSquaresLine">y = mx + b</span></label>
				</div>
				<input id="idHighX" type="text" size="3" style="width:40px;height:16px;float:left;"/> 
			</div>
			<div style="width:108px;height:22px;float:right;"></div>
		</div>
	</body>
</html>

OrdinaryLeastSquares.js

class CLine {
	#mdaSlope;
	#mdIntercept;
	// The graph width should be calculated automatically from the data or set by the distribution
	constructor(daY) {
		let dSumOfX = 0.0;
		let dSumOfX2 = 0.0;
		let dSumOfY = 0.0;
		let dSumOfXY = 0.0;
		for (let iX = 0; iX < daY.length; ++iX) {
			dSumOfX += iX;
			dSumOfX2 += (iX*iX);
			dSumOfY += daY[iX];
			dSumOfXY += iX*daY[iX];
		}
		// Denominator = (1.1)(x.x) - (1.x)(1.x)
		let dDenominator = (daY.length*dSumOfX2 - dSumOfX*dSumOfX);
		// Intercept = (x.x)(1.y) - (1.x)(x.y)/Denominator
		this.#mdIntercept = (dSumOfX2*dSumOfY - dSumOfX*dSumOfXY)/dDenominator;
		// Slope = (1.1)(x.y) - (1.x)(1.y)/Denominator
		this.#mdaSlope =(daY.length*dSumOfXY - dSumOfX*dSumOfY)/dDenominator;
	}
	
	CalculateY(dX) {
		return dX*this.#mdaSlope + this.#mdIntercept;
	}
	
	ToString() {
		return "y = "+this.#mdaSlope.toFixed(2)+"x + "+this.#mdIntercept.toFixed(2);
	}
}

function GenerateRandomPointFromNormalDistribution(dMean = 0.0, dStdDev = 1.0) {
	return dStdDev*InversePhi(Math.random()) + dMean;
}

function InversePhi(p) {

	// Constants for the approximation
	const p_low = 0.02425;
	const p_high = 1 - p_low;
		
	// Coefficients for the rational approximation
	const a1 = -3.969683028665376e+01;
	const a2 = 2.209460984245205e+02;
	const a3 = -2.759285104469687e+02;
	const a4 = 1.383577518672690e+02;
	const a5 = -3.066479806614716e+01;
	const a6 = 2.506628277459239e+00;

	const b1 = -5.447609879822406e+01;
	const b2 = 1.615858368580409e+02;
	const b3 = -1.556989798598866e+02;
	const b4 = 6.680131188771972e+01;
	const b5 = -1.328068155288572e+01;

	const c1 = -7.784894002430293e-03;
	const c2 = -3.223964580411365e-01;
	const c3 = -2.400758277161838e+00;
	const c4 = -2.549732539343734e+00;
	const c5 = 4.374664141464968e+00;
	const c6 = 2.938163982698783e+00;

	const d1 = 7.784695709041462e-03;
	const d2 = 3.224671290700398e-01;
	const d3 = 2.445134137142996e+00;
	const d4 = 3.754408661907416e+00;

	let x = 0.0;
	if (0.0 < p && p < 1) {
		if (p < p_low) {
			let q = Math.sqrt(-2*Math.log(p));
			x = (((((c1*q+c2)*q+c3)*q+c4)*q+c5)*q+c6) / ((((d1*q+d2)*q+d3)*q+d4)*q+1);
		} else if (p_high < p) {
			let q = Math.sqrt(-2*Math.log(1-p));
			x = -(((((c1*q+c2)*q+c3)*q+c4)*q+c5)*q+c6) / ((((d1*q+d2)*q+d3)*q+d4)*q+1);
		} else {
			let q = p - 0.5;
			let r = q*q;
			x = (((((a1*r+a2)*r+a3)*r+a4)*r+a5)*r+a6)*q / (((((b1*r+b2)*r+b3)*r+b4)*r+b5)*r+1);
		}
	}
	return x;
}

function Initialize() {
	let qCanvas = document.getElementById("idCanvas");
	let qContext2D = qCanvas.getContext("2d");
	qContext2D.clearRect(0, 0, 800, 600);
	
	// Transform the coordinates
	const kiCanvasW = qContext2D.canvas.width;
	const kiCanvasH = qContext2D.canvas.height;
	// Reset the transformation so that the flip transformations do not build up an cancel each other.
	qContext2D.resetTransform();
	// Flip the y-axis and translate by the height
	qContext2D.transform(1, 0, 0, -1, 0, kiCanvasH);
	const kiGraphW = 16.0;
	const kiGraphH = 12.0;
	// I will make this transformation myself because this scaling changes the thickness of the lines.
	// The range will be 0 to 8 and 0 to 6: [0, 8)x[0 ,6)
	//qContext2D.transform(kiCanvasW/kiGraphW, 0, 0, kiCanvasH/kiGraphH, 0, 0);

	// This a simple scaling that puts the points in canvas coordinates
	const kdScaleW = kiCanvasW/kiGraphW;
	const kdScaleH = kiCanvasH/kiGraphH;
	// Set the bounds in the graph
	let qLowYElement = document.getElementById("idLowY");
	qLowYElement.value = 0;
	let qHighYElement = document.getElementById("idHighY");
	qHighYElement.value = kiGraphH;
	let qLowXElement = document.getElementById("idLowX");
	qLowXElement.value = 0;
	let qHighXElement = document.getElementById("idHighX");
	qHighXElement.value = kiGraphW;
	
	// Generate a random line as the source model
	// Make the intercept b in [2,3] and the slope m in [0, .5]
	const kdIntercept = Math.random() + 2.0;
	const kdSlope = .5*Math.random();
	let qSourceLineElement = document.getElementById("idSourceLine");
	qSourceLineElement.innerHTML = "y = "+kdSlope.toFixed(2)+"x + "+kdIntercept.toFixed(2);
	
	// Graph the source model line
	qContext2D.strokeStyle = "black";
	qContext2D.lineWidth = 1;
	qContext2D.beginPath();
	qContext2D.moveTo(0*kdScaleW, kdIntercept*kdScaleH);
	qContext2D.lineTo(kiGraphW*kdScaleW, (kdIntercept + kiGraphW*kdSlope)*kdScaleH);
	qContext2D.stroke();
	
	// Generate a set of points with a normal distribution to represent the error
	let daY = new Array(kiGraphW + 1);
	for (let i = 0; i <= kiGraphW; ++i) {
		// Graph the position lines
		qContext2D.strokeStyle = "gray";
		qContext2D.lineWidth = .25;
		qContext2D.beginPath();
		qContext2D.moveTo(i*kdScaleW, 0*kdScaleH);
		qContext2D.lineTo(i*kdScaleW, kiGraphH*kdScaleH);
		qContext2D.stroke();
		// Calculate the random points
		daY[i] = kdIntercept + i*kdSlope + GenerateRandomPointFromNormalDistribution();
	}
	for (let i = 0; i <= kiGraphH; ++i) {
		qContext2D.beginPath();
		qContext2D.moveTo(0*kdScaleW, i*kdScaleH);
		qContext2D.lineTo(kiGraphW*kdScaleW, i*kdScaleH);
		qContext2D.stroke();
	}
	
	// Calculate the least squares
	let qOLS = new CLine(daY);
	let qLeastSquaresLineElement = document.getElementById("idLeastSquaresLine");
	qLeastSquaresLineElement.innerHTML = qOLS.ToString();
	// Graph the position lines
	qContext2D.strokeStyle = "red";
	qContext2D.lineWidth = 2;
	const kdStartY = qOLS.CalculateY(0);
	const kdEndY = qOLS.CalculateY(kiGraphW);
	qContext2D.beginPath();
	qContext2D.moveTo(0*kdScaleW, kdStartY*kdScaleH);
	qContext2D.lineTo(kiGraphW*kdScaleW, kdEndY*kdScaleH);
	qContext2D.stroke();

	for (let i = 0; i <= kiGraphW; ++i) {
		// Draw the error bars
		const kdLineY = qOLS.CalculateY(i);
		qContext2D.strokeStyle = "red";
		qContext2D.lineWidth = 1;
		qContext2D.beginPath();
		qContext2D.moveTo(i*kdScaleW, kdLineY*kdScaleH);
		qContext2D.lineTo(i*kdScaleW, daY[i]*kdScaleH);
		qContext2D.stroke();
		
		// Graph the points
		qContext2D.fillStyle = "red";
		qContext2D.beginPath();
		const kdRadius = 5;
		qContext2D.ellipse(i*kdScaleW, daY[i]*kdScaleH, kdRadius, kdRadius, 0, 0, 2*Math.PI);
		qContext2D.fill();
	}
}


 

Output

 
 

© 2007–2025 XoaX.net LLC. All rights reserved.