This JavaScript program demonstrates how to draw a linearly interpolated graph of a 2d function with adaptive scaling. The mouse can be used to pan the graph by pressing the left mouse button and moving the cursor or zoom the graph by using the mousewheel to zoom in and out at the cursor location.
<!DOCTYPE html>
<html>
<head>
<title>XoaX.net's Javascript</title>
<script type="text/javascript" src="LinearInterpolationWithAdaptiveScaling.js"></script>
</head>
<body onload="Initialize()">
<div style="width:708px;height:622px;">
<canvas id="idCanvas" width="600" height ="600" style="background-color:#F0F0F0;float:left;"></canvas>
<div style="width:108px;height:600px;float:right;">
<input id="idHighY" type="text" size="9" style="width:100px;height:16px;"/>
<div style="width:108px;height:556px;"></div>
<input id="idLowY" type="text" size="9" style="width:100px;height:16px;"/>
</div>
<div style="width:600px;height:22px;float:left;">
<input id="idLowX" type="text" size="9" style="width:100px;height:16px;float:left;"/>
<div style="width:384px;height:22px;float:left;">
<div style="width:fit-content;margin-left:auto;margin-right:auto;">Function:
<select style="height: 22px;" id="idFunction" onchange="SelectFunction()">
<option value="kHyperbola">y = 1/x</option>
<option value="kParabola">y = x^2</option>
<option value="kSin">y = sin(x)</option>
<option value="kTan">y = tan(x)</option>
</select>
</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>class GraphRange2D {
constructor(dLowX, dHighX, dLowY, dHighY, iCanvasW, iCanvasH) {
this.mdLowX = dLowX;
this.mdHighX = dHighX;
this.mdLowY = dLowY;
this.mdHighY = dHighY;
this.miCanvasW = iCanvasW;
this.miCanvasH = iCanvasH;
}
Scale(dScale, dZoomPixelX, dZoomPixelY) {
var dZoomX = gqGraphRange.PixelXToX(dZoomPixelX);
var dZoomY = gqGraphRange.PixelYToY(dZoomPixelY);
// Scale around the zoom center point, usually defined by the cursor
this.mdLowX = dZoomX - (dZoomX - this.mdLowX)*dScale;
this.mdHighX = dZoomX + (this.mdHighX - dZoomX)*dScale;
this.mdLowY = dZoomY - (dZoomY - this.mdLowY)*dScale;
this.mdHighY = dZoomY + (this.mdHighY - dZoomY)*dScale;
}
Pan(iDeltaPixelX, iDeltaPixelY) {
// Get the pixel sizes
var dPixelWidth = (this.mdHighX - this.mdLowX)/this.miCanvasW;
var dPixelHeight = (this.mdHighY - this.mdLowY)/this.miCanvasH;
this.mdLowX -= dPixelWidth*iDeltaPixelX;
this.mdHighX -= dPixelWidth*iDeltaPixelX;
this.mdLowY += dPixelHeight*iDeltaPixelY;
this.mdHighY += dPixelHeight*iDeltaPixelY;
}
PixelW() {
return (this.mdHighX - this.mdLowX)/this.miCanvasW;
}
PixelH() {
return (this.mdHighY - this.mdLowY)/this.miCanvasH;
}
// The pixel x range is [-.5, CanvasW - .5]
XtoPixelX(dX) {
var dT = (dX - this.mdLowX)/(this.mdHighX - this.mdLowX);
return (dT*this.miCanvasW - .5);
}
// The pixel y range is [-.5, CanvasH - .5], but the orientation is reversed
YtoPixelY(dY) {
var dT = (dY - this.mdLowY)/(this.mdHighY - this.mdLowY);
return (1.0 - dT)*(this.miCanvasH - .5);
}
PixelXToX(dPixelX) {
var dT = (dPixelX - .5)/(this.miCanvasW - 1.0);
return (dT*(this.mdHighX - this.mdLowX) + this.mdLowX);
}
PixelYToY(dPixelY) {
var dT = (dPixelY - .5)/(this.miCanvasH - 1.0);
return ((1.0 - dT)*(this.mdHighY - this.mdLowY) + this.mdLowY);
}
FindPixelXOfEntryExit(dLowPixelX, dHighPixelX, fnF) {
// The PixelX of the entry is between dLowPixelX and dHighPixelX
var dLowPixX = dLowPixelX;
var dHighPixX = dHighPixelX;
// Create a test x-value close to the inside or high pixel (We are entering from the outside)
var dLastPixX = dHighPixX;
var dLastPixY = this.YtoPixelY(fnF(this.PixelXToX(dLastPixX)));
var dT = 99.0/100.0;
// Linearly interpolate close to inside location
var dNextPixX = ((1.0 - dT)*dLowPixX + dT*dLastPixX);
var dNextPixY = this.YtoPixelY(fnF(this.PixelXToX(dNextPixX)));
// Is the graph increasing or decreasing?
var bIsPixelYIncreasing = ((dNextPixY < dLastPixY) ? true : false);
// If Pixel Y is increasing, find the Pixel X of Pixel Y = 0.0, since must be increasing from an outside Y value
// If Pixel Y is decreasing, find the Pixel X of Pixel Y = this.miCanvasH - 1.0, since must be decreasing from an outside Y value
var dTargetY = (bIsPixelYIncreasing ? 0.0 : (this.miCanvasH - 1.0));
do {
if (((dLowPixelX < dHighPixelX) && (dNextPixX < dLowPixelX || dNextPixX > dHighPixelX)) ||
((dHighPixelX < dLowPixelX) && (dNextPixX > dLowPixelX || dNextPixX < dHighPixelX))) {
return (dLowPixelX + dHighPixelX)/2.0;
}
// (y - y0)= m(x - x0) - solve for the crossing value by linear interpolation
var dM = (dNextPixY - dLastPixY)/(dNextPixX - dLastPixX);
dLastPixX = dNextPixX;
dLastPixY = dNextPixY;
// Linear interpolation x = (y - y0)/m + x0, where y = TargetY
dNextPixX = (dTargetY - dNextPixY)/dM + dNextPixX;
dNextPixY = this.YtoPixelY(fnF(this.PixelXToX(dNextPixX)));
} while (Math.abs(dNextPixX - dLastPixX) > .001);
return dNextPixX;
}
// Check whether the point is within the bounds of the graph
IsYInBounds(dY) {
if ((dY >= this.mdLowY) && (dY <= this.mdHighY)) {
return true;
}
return false;
}
IsPixelYInBounds(dPixelY) {
if ((dPixelY >= 0) && (dPixelY <= this.miCanvasH - 1)) {
return true;
}
return false;
}
DrawFunctionGraph(qContext, fnF, dLineWidth) {
// The x pixel intervals of the graph that are inside the bounds
var qaaBoundsIntervals = new Array();
var qaaPointCurves = new Array();
var qaLargeYDiff = new Array();
var dPixelX = 0;
var bIsInBounds = false;
var daCurrInterval = null;
// Start at 0 pixel value
while (dPixelX < this.miCanvasW) {
var dX = this.PixelXToX(dPixelX);
var dY = fnF(dX);
var dPixelY = this.YtoPixelY(dY);
// Was the last point within the visible graph bounds?
if (bIsInBounds) {
// Did we exit the bounds?
if (!this.IsPixelYInBounds(dPixelY)) {
// Find the actual endpoint of the exit.
daCurrInterval[1] = this.FindPixelXOfEntryExit(dPixelX, dPixelX - 1, fnF);
bIsInBounds = false;
} else { // Otherwise, we are still inside the bounds
// Extend the interval to the next pixel, possibly the last.
daCurrInterval[1] = dPixelX;
}
} else { // The last point was not inside the graph bounds
if (this.IsPixelYInBounds(dPixelY)) { // Entering visible interval
// This is the first position within the current interval
// If it isn't the first pixel, find the start position where the graph crosses into the visible region.
if (dPixelX >= 1) {
var dPixelXOfExit = this.FindPixelXOfEntryExit(dPixelX - 1, dPixelX, fnF);
// We are starting a new inbounds inteval. Initialize it as one point.
daCurrInterval = [dPixelXOfExit, dPixelXOfExit];
} else {
// Put the values into both positions, just in case we are at the end.
daCurrInterval = [dPixelX, dPixelX];
}
qaaBoundsIntervals.push(daCurrInterval);
bIsInBounds = true;
}
}
// Move to the next pixel location
dPixelX += 1.0;
}
// Draw the graph
qContext.strokeStyle = "pink";
qContext.lineWidth = dLineWidth;
qContext.fillStyle = "pink";
// Draw the graph for each continuous portion that lies inside the graph bounds.
for (var iInterval = 0; iInterval < qaaBoundsIntervals.length; ++iInterval) {
dPixelX = qaaBoundsIntervals[iInterval][0];
var dX = this.PixelXToX(dPixelX);
var dY = fnF(dX);
var dPixelY = this.YtoPixelY(dY);
qContext.beginPath();
qContext.moveTo(dPixelX, dPixelY);
// Graph the function within the bounds of the current interval
for (var j = qaaBoundsIntervals[iInterval][0] + 1; j <= qaaBoundsIntervals[iInterval][1]; ++j) {
dPixelX = j;
dX = this.PixelXToX(dPixelX);
dY = fnF(dX);
dPixelY = this.YtoPixelY(dY);
qContext.lineTo(dPixelX, dPixelY);
// Add in the extra partial pixel
if (j + 1 > qaaBoundsIntervals[iInterval][1]) {
dPixelX = qaaBoundsIntervals[iInterval][1];
dX = this.PixelXToX(dPixelX);
dY = fnF(dX);
dPixelY = this.YtoPixelY(dY);
qContext.lineTo(dPixelX, dPixelY);
}
}
qContext.stroke();
// Draw the two arrow heads at the end. Make sure that the interval is more that 2 units in length
if (qaaBoundsIntervals[iInterval][1] > (qaaBoundsIntervals[iInterval][0] + 2.0)) {
var dX1 = qaaBoundsIntervals[iInterval][0] + 1;
var dY1 = this.YtoPixelY(fnF(this.PixelXToX(dX1)));
var dX2 = qaaBoundsIntervals[iInterval][0];
var dY2 = this.YtoPixelY(fnF(this.PixelXToX(dX2)));
DrawArrowhead(qContext, dX1, dY1, dX2, dY2);
dX1 = qaaBoundsIntervals[iInterval][1] - 1;
dY1 = this.YtoPixelY(fnF(this.PixelXToX(dX1)));
dX2 = qaaBoundsIntervals[iInterval][1];
dY2 = this.YtoPixelY(fnF(this.PixelXToX(dX2)));
DrawArrowhead(qContext, dX1, dY1, dX2, dY2);
}
}
}
// Find the smallest power greater than x
FindSmallestGreaterPowerOfTwo(dX) {
var dPow = 1.0;
while (dPow < dX) {
dPow *= 2;
}
if (dPow == 1.0) {
while (dPow >= dX) {
dPow /= 2;
}
}
return dPow;
}
FindLeastMultipleBelow(dX, dLowX) {
Math.floor(dLowX/dX)*dX;
}
DrawGridLinesAndAxes(qContext) {
// Draw grid lines of two types
// Get an estimate for a value that will be drawn about every 100 pixels and every 20
var dThickFreqX = this.FindSmallestGreaterPowerOfTwo(this.PixelXToX(100) - this.PixelXToX(0));
var dThinFreqX = dThickFreqX/4;
var dThickFreqY = this.FindSmallestGreaterPowerOfTwo(this.PixelYToY(0) - this.PixelYToY(100));
var dThinFreqY = dThickFreqY/4;
// Draw the thin lines first, then the thick ones, and the axes.
// Draw the thin lines first
// Draw vertical grid lines
// Find the least frequency value below the low x (greatest lower multiple GLM)
var dGLM = Math.floor(this.mdLowX/dThickFreqX)*dThickFreqX;
var dGridValue = dGLM + dThinFreqX;
qContext.strokeStyle = "#E8E8E8";
var i = 1;
while (dGridValue < this.mdHighX) {
if ((i % 4) != 0) {
var dGridX = this.XtoPixelX(dGridValue);
qContext.beginPath();
qContext.moveTo(dGridX, 0);
qContext.lineTo(dGridX, this.miCanvasH);
qContext.stroke();
}
dGridValue += dThinFreqX;
++i;
}
// Draw horizontal grid lines
dGLM = Math.floor(this.mdLowY/dThickFreqY)*dThickFreqY;
dGridValue = dGLM + dThinFreqY;
i = 1;
while (dGridValue < this.mdHighY) {
if ((i % 4) != 0) {
var dGridY = this.YtoPixelY(dGridValue);
qContext.beginPath();
qContext.moveTo(0, dGridY);
qContext.lineTo(this.miCanvasW, dGridY);
qContext.stroke();
}
dGridValue += dThinFreqY;
++i;
}
// Draw the thick lines
// Draw vertical grid lines
qContext.strokeStyle = "#DDDDDD";
dGLM = Math.floor(this.mdLowX/dThickFreqX)*dThickFreqX;
dGridValue = dGLM + dThickFreqX;
while (dGridValue < this.mdHighX) {
var dGridX = this.XtoPixelX(dGridValue);
qContext.beginPath();
qContext.moveTo(dGridX, 0);
qContext.lineTo(dGridX, this.miCanvasH);
qContext.stroke();
dGridValue += dThickFreqX;
}
// Draw horizontal grid lines
dGLM = Math.floor(this.mdLowY/dThickFreqY)*dThickFreqY;
dGridValue = dGLM + dThickFreqY;
while (dGridValue < this.mdHighY) {
var dGridY = this.YtoPixelY(dGridValue);
qContext.beginPath();
qContext.moveTo(0, dGridY);
qContext.lineTo(this.miCanvasW, dGridY);
qContext.stroke();
dGridValue += dThickFreqY;
}
// Lastly, check whether zero is within each range in order to draw the axes.
// Draw the Y-Axis
qContext.strokeStyle = "gray";
qContext.fillStyle = "gray";
if (this.mdLowX <= 0 && this.mdHighX >= 0) {
var dPixelX = this.XtoPixelX(0);
DrawArrow(qContext, dPixelX, this.miCanvasH, dPixelX, 0);
}
// Draw the X-Axis
if (this.mdLowY <= 0 && this.mdHighY >= 0) {
var dPixelY = this.YtoPixelY(0);
DrawArrow(qContext, 0, dPixelY, this.miCanvasW, dPixelY);
}
}
FillInRanges(qLowX, qHighX, qLowY, qHighY) {
// Remove any trailing zeroes and decimal points
qLowX.value = this.mdLowX.toFixed(4).replace(/\.?0+$/, "");
qHighX.value = this.mdHighX.toFixed(4).replace(/\.?0+$/, "");
qLowY.value = this.mdLowY.toFixed(4).replace(/\.?0+$/, "");
qHighY.value = this.mdHighY.toFixed(4).replace(/\.?0+$/, "");
}
}
var gqGraphRange = null;
var gqContext = null;
var gbIsDragging = false;
var giCursorX = 0;
var giCursorY = 0;
var gfnFunction = Hyperbola;
function Initialize() {
var qCanvas = document.getElementById("idCanvas");
qCanvas.addEventListener("wheel", fnZoom, { passive: false });
qCanvas.addEventListener("mousedown", MouseDown);
qCanvas.addEventListener("mouseup", MouseUp);
qCanvas.addEventListener("mousemove", MouseMove);
gqContext = qCanvas.getContext("2d");
// Get the canvas size
gqGraphRange = new GraphRange2D(-4.0, 4.0, -4.0, 4.0, qCanvas.width, qCanvas.height);
gqGraphRange.DrawGridLinesAndAxes(gqContext);
gqGraphRange.DrawFunctionGraph(gqContext, gfnFunction, 1.0);
var qLowX = document.getElementById("idLowX");
var qHighX = document.getElementById("idHighX");
var qLowY = document.getElementById("idLowY");
var qHighY = document.getElementById("idHighY");
gqGraphRange.FillInRanges(qLowX, qHighX, qLowY, qHighY);
qCanvas.addEventListener("wheel", fnZoom, { passive: false });
}
function DrawArrow(qContext, dX1, dY1, dX2, dY2) {
// Draw the line portion
qContext.beginPath();
qContext.moveTo(dX1, dY1);
qContext.lineTo(dX2, dY2);
qContext.stroke();
// Draw the head portion
DrawArrowhead(qContext, dX1, dY1, dX2, dY2);
}
function DrawArrowhead(qContext, dX1, dY1, dX2, dY2) {
var dTheta = Math.atan2(dY2-dY1,dX2-dX1);
var dThicknessAngle = Math.PI/6;
var dHeadLength = 10;
qContext.beginPath();
qContext.moveTo(dX2, dY2);
qContext.lineTo(dX2 - dHeadLength*Math.cos(dTheta-dThicknessAngle), dY2 - dHeadLength*Math.sin(dTheta-dThicknessAngle));
qContext.lineTo(dX2 - dHeadLength*Math.cos(dTheta+dThicknessAngle), dY2 - dHeadLength*Math.sin(dTheta+dThicknessAngle));
qContext.closePath();
qContext.fill();
}
function fnZoom(e) {
e.preventDefault();
var dFactor = (e.deltaY > 0.0) ? 1.1: .9090909090909;
SetCursorCoordinates(e);
// Zoom centered on the pixel location
gqGraphRange.Scale(dFactor, giCursorX, giCursorY);
// Draw the graph
gqContext.clearRect(0, 0, 600, 600);
gqGraphRange.DrawGridLinesAndAxes(gqContext);
gqGraphRange.DrawFunctionGraph(gqContext, gfnFunction, 1.0);
var qLowX = document.getElementById("idLowX");
var qHighX = document.getElementById("idHighX");
var qLowY = document.getElementById("idLowY");
var qHighY = document.getElementById("idHighY");
gqGraphRange.FillInRanges(qLowX, qHighX, qLowY, qHighY);
}
function MouseMove(e) {
if (gbIsDragging) {
var iLastCursorX = giCursorX;
var iLastCursorY = giCursorY;
SetCursorCoordinates(e);
var iCurrCursorX = giCursorX;
var iCurrCursorY = giCursorY;
gqGraphRange.Pan(iCurrCursorX - iLastCursorX, iCurrCursorY - iLastCursorY);
// Draw the graph
gqContext.clearRect(0, 0, 600, 600);
gqGraphRange.DrawGridLinesAndAxes(gqContext);
gqGraphRange.DrawFunctionGraph(gqContext, gfnFunction, 1.0);
var qLowX = document.getElementById("idLowX");
var qHighX = document.getElementById("idHighX");
var qLowY = document.getElementById("idLowY");
var qHighY = document.getElementById("idHighY");
gqGraphRange.FillInRanges(qLowX, qHighX, qLowY, qHighY);
}
}
function MouseDown(e) {
SetCursorCoordinates(e);
gbIsDragging = true;
}
function MouseUp(e) {
gbIsDragging = false;
}
function SetCursorCoordinates(e) {
var dElementOffsetX = 0;
var dElementOffsetY = 0;
// Get the element that triggered the event
var qElement = e.target;
do{
dElementOffsetX += qElement.offsetLeft - qElement.scrollLeft;
dElementOffsetY += qElement.offsetTop - qElement.scrollTop;
qElement = qElement.offsetParent;
} while(qElement != document.body)
giCursorX = e.pageX - dElementOffsetX;
giCursorY = e.pageY - dElementOffsetY;
}
function Parabola(dX) {
return (dX*dX);
}
function Hyperbola(dX) {
return 1.0/dX;
}
function Sin(dX) {
return Math.sin(dX);
}
function Tan(dX) {
return Math.tan(dX);
}
function SelectFunction() {
var qFunction = document.getElementById("idFunction").value;
switch (qFunction){
case "kHyperbola":
gfnFunction = Hyperbola;
break;
case "kParabola":
gfnFunction = Parabola;
break;
case "kSin":
gfnFunction = Sin;
break;
case "kTan":
gfnFunction = Tan;
break;
}
// Draw the graph
gqContext.clearRect(0, 0, 600, 600);
gqGraphRange.DrawGridLinesAndAxes(gqContext);
gqGraphRange.DrawFunctionGraph(gqContext, gfnFunction, 1.0);
}© 20072025 XoaX.net LLC. All rights reserved.