Canvas JavaScript

Graphing a Normal Distribution

This JavaScript program demonstrates how to generate and graph a normal distribution. Use the selector to select the bar width on the histogram. The mean is displayed as a red line and three standard deviations are displayed as progressively lighter gray regions.

GraphingANormalDistribution.html

<!DOCTYPE html>
<html>
	<head>
		<title>XoaX.net's Javascript</title>
		<script type="text/javascript" src="GraphingANormalDistribution.js"></script>
	</head>
	<body onload="Initialize()">
		<div style="width:908px;height:430px;">
			<div style="width:108px;height:430px;float:left;">
				<input id="idHighY" type="text" size="9" style="width:100px;height:16px;"/>
				<div style="width:108px;height:356px;"></div>
				<input id="idLowY" type="text" size="9" style="width:100px;height:16px;"/>
			</div>
			<canvas id="idCanvas" width="800" height ="400" style="background-color: #F0F0F0;float:left;"></canvas>
			<div style="width:800px;height:22px;float:left;">
				<input id="idLowX" type="text" size="9" style="width:100px;height:16px;float:left;"/>
				<div style="width:584px;height:22px;float:left;">
					<div style="width:fit-content;margin-left:auto;margin-right:auto;">
						<div style="width:fit-content;float:left;">Distribution:
							<select style="height: 22px;" id="idDistribution" onchange="fnSelectFunction()">
								<option value="kGaussian" selected>Gaussian</option>
							</select>
						</div>
						<div style="width:fit-content;float:left;margin-left:5px;">Bar Width:
							<select style="height: 22px;" id="idBarWidth" onchange="fnSelectFunction()">
								<option value="2">2</option>
								<option value="3">3</option>
								<option value="5">5</option>
								<option value="8">8</option>
								<option value="13">13</option>
								<option value="21" selected>21</option>
							</select>
						</div>
					</div>
				</div>
				<input id="idHighX" type="text" size="9" style="width:100px;height:16px;float:left;"/>    		
			</div>
			<div style="width:108px;height:22px;float:right;"></div>
		</div>
	</body>
</html>

GraphingANormalDistribution.js

class CDistribution {
	mdaX;
	#mdGraphWidth;
	#mdMean = null;
	#mdStdDev = null;
	// The graph width should be calculated automatically from the data or set by the distribution
	constructor() {
	}
	GraphWidth() {
		if (this.#mdGraphWidth == null) {
			this.#mdGraphWidth = 8*this.StandardDeviation();
		}
		return this.#mdGraphWidth;
	}
	Mean() {
		if (this.#mdMean == null) {
			this.#mdMean = 0.0;
			for (let i = 0; i < this.mdaX.length; ++i) {
				this.#mdMean += this.mdaX[i]/this.mdaX.length;
			}
		}
		return this.#mdMean;
	}
	StandardDeviation() {
		if (this.#mdStdDev == null) {
			let dMean = this.Mean();
			let dVariance = 0;
			for (let i = 0; i < this.mdaX.length; ++i) {
				dVariance += Math.pow(this.mdaX[i] - dMean, 2)/this.mdaX.length;
			}
			this.#mdStdDev = Math.sqrt(dVariance);
		}
		return this.#mdStdDev;
	}
	PixelXToGraphX(dPixelX, iCanvasWidth) {
		// Half the size should map to the mean
		let dHalfWidth = this.GraphWidth()/2;
		let dMean = this.Mean();
		let dX = ((dPixelX + .5)/iCanvasWidth)*this.GraphWidth() - dHalfWidth + dMean;
		return dX;
	}
	GraphXToPixelX(dX, iCanvasWidth) {
		let dMean = this.Mean();
		let dHalfWidth = this.GraphWidth()/2;
		// X = ((PX + .5)/CW)GW - HW + M
		// PX = (CW(X - M + HW)/GW) - .5
		let dPX = iCanvasWidth*(dX - dMean + dHalfWidth)/this.GraphWidth() - .5;
		return dPX;
	}
	Graph(qContext, dBarWidthInPixels) {
		// Reset the transformation so that the flip transformations do not build up an cancel each other.
		qContext.resetTransform();
		// This is the basic transformation to flip the y-axis
		qContext.transform(1, 0, 0, -1, 0, qContext.canvas.height);
		// The canvas width and height in pixels or pixels per canvas
		let iW = qContext.canvas.width;
		let iH = qContext.canvas.height;
		
		// Graph the standard deviation regions
		qContext.fillStyle = "gray";
		qContext.globalAlpha = .25;
		for (let i = 0; i < 3; ++i) {
			let iStartStdDev = this.GraphXToPixelX(this.Mean() - (i + 1)*this.StandardDeviation(), iW);
			let iEndStdDev = this.GraphXToPixelX(this.Mean() + (i + 1)*this.StandardDeviation(), iW);
			qContext.fillRect(iStartStdDev, 0, iEndStdDev - iStartStdDev, 400);
		}
		// Reset the alpha value
		qContext.globalAlpha = 1.0;
		qContext.fillStyle = "#606060";
		
		// The canvas graph width
		let dGraphW = this.GraphWidth();
		// Pixel width in graph units = graph width / pixels per canvas width
		let dPixW = dGraphW/iW;
		// Bar width in pixels / pixels per canvas width
		let dBarsPerWidth = iW/dBarWidthInPixels;
		// The width of a bar in values
		let dBarWidth = dBarWidthInPixels*dPixW;
		// The middle bar is centered on the mean
		// Find the start of the first bar and continue from there until all of them are drawn
		let dStartOfFirstBarPix = iW/2 - dBarWidthInPixels/2 - dBarWidthInPixels*Math.ceil(dBarsPerWidth/2);
		let dStartOfCurrBarPix = dStartOfFirstBarPix;
		let dStartOfCurrBarValue = this.PixelXToGraphX(dStartOfCurrBarPix, iW);
		let iIndexOfNextPoint = 0;
		// Get to the first point that is in the range of the first bar.
		while (iIndexOfNextPoint < this.mdaX.length && dStartOfCurrBarValue > this.mdaX[iIndexOfNextPoint]) {
			++iIndexOfNextPoint;
		}
		let iaBins = new Array();
		while (iIndexOfNextPoint < this.mdaX.length && dStartOfCurrBarPix < iW) {
			let dEndOfCurrBarPix = dStartOfCurrBarPix + dBarWidthInPixels;
			let dEndOfCurrBarValue = this.PixelXToGraphX(dEndOfCurrBarPix, iW);
			let iIndexOfEnd = iIndexOfNextPoint;
			while (iIndexOfEnd < this.mdaX.length && dEndOfCurrBarValue > this.mdaX[iIndexOfEnd]) {
				++iIndexOfEnd;
			}
			// The number of points in the bin
			iaBins.push(iIndexOfEnd - iIndexOfNextPoint);
			// The increment for the bar
			dStartOfCurrBarPix = dEndOfCurrBarPix;
			// Increment for the points
			iIndexOfNextPoint = iIndexOfEnd;
		}
		// Get the size of the largest bin
		let iLargestBinSize = 0;
		for (let i = 0; i < iaBins.length; ++i) {
			if (iLargestBinSize < iaBins[i]) {
				iLargestBinSize = iaBins[i];
			}
		}
		// Make the height of the graph 20% higher than the largest bin size.
		let dMaxY = iLargestBinSize*1.1;
		let iBinIndex = 0;
		dStartOfCurrBarPix = dStartOfFirstBarPix;
		// Draw the bars of the graph
		while (iBinIndex < iaBins.length && dStartOfCurrBarPix < iW) {
			let dEndOfCurrBarPix = dStartOfCurrBarPix + dBarWidthInPixels;
			let dRectHeight = iaBins[iBinIndex];
			qContext.fillRect(dStartOfCurrBarPix, 0, dBarWidthInPixels, (dRectHeight/dMaxY)*iH);
			// The increment for the bar
			dStartOfCurrBarPix = dEndOfCurrBarPix;
			++iBinIndex;
		}
		// Draw the mean line
		let iMeanBarX = this.GraphXToPixelX(this.Mean(), iW);
		qContext.strokeStyle = "red";
		qContext.lineWidth = .5;
		qContext.beginPath();
		qContext.moveTo(iMeanBarX, 0);
		qContext.lineTo(iMeanBarX, 400);
		qContext.stroke();
		
		// Fill in the bounds
		let qLowYElement = document.getElementById("idLowY");
		qLowYElement.value = 0;
		let qHighYElement = document.getElementById("idHighY");
		qHighYElement.value = dMaxY.toFixed(1);
		let qLowXElement = document.getElementById("idLowX");
		qLowXElement.value = this.PixelXToGraphX(-.5, iW).toFixed(3);
		let qHighXElement = document.getElementById("idHighX");
		qHighXElement.value = this.PixelXToGraphX(iW-.5, iW).toFixed(3);
	}
}

class CGaussianDistribution extends CDistribution {
	constructor(iCount) {
		super();
		// This generates a sorted array of the sample values of the distribution
		// This is an approximately gaussian distribution from 0 to 6
		this.mdaX = new Array(iCount);
		for (let i = 0; i < iCount; ++i) {
			// Add the point to the end of the array
			this.mdaX[i] = CGaussianDistribution.InversePhi(Math.random());
			// Insertion sort each point after it is added
			let j = i;
			while (j > 0 && this.mdaX[j] < this.mdaX[j - 1]) {
				let dSwap = this.mdaX[j];
				this.mdaX[j] = this.mdaX[j - 1];
				this.mdaX[j - 1] = dSwap;
				--j;
			}
		}
	}
	// Turn a uniformly chosen random variable into a value chosen from a
	// normal distribution with mean 0 and standard deviation 1
	static 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() {
	var qCanvas = document.getElementById("idCanvas");
	var qContext = qCanvas.getContext("2d");
	var qDist = new CGaussianDistribution(50000);
	qDist.Graph(qContext, 21);
}

function fnSelectFunction() {
	var qCanvas = document.getElementById("idCanvas");
	var qContext = qCanvas.getContext("2d");
	qContext.clearRect(0, 0, qCanvas.width, qCanvas.height);

	// Get the distribution value
	var sDistributionValue = document.getElementById("idDistribution").value;
	var qDistribution = new CGaussianDistribution(50000);

	// Get the bar width
	var sBarWidth = document.getElementById("idBarWidth").value;
	let iBarWidth = 2;
	switch (sBarWidth) {
		case "2":
			iBarWidth = 2;
			break;
		case "3":
			iBarWidth = 3;
			break;
		case "5":
			iBarWidth = 5;
			break;
		case "8":
			iBarWidth = 8;
			break;
		case "13":
			iBarWidth = 13;
			break;
		case "21":
			iBarWidth = 21;
			break;
	}
	qDistribution.Graph(qContext, iBarWidth);
}
 

Output

 
 

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