This JavaScript program demonstrates how to draw a grid for a 2D graph 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. This can be used as the basis for a 2D graph control.
<!DOCTYPE html> <html> <head> <title>XoaX.net's Javascript</title> <script type="text/javascript" src="CAffineTransformation2D.js"></script> <script type="text/javascript" src="CGraphOnImage2D.js"></script> <script type="text/javascript" src="CCanvasGraph2D.js"></script> <script type="text/javascript" src="Main.js"></script> </head> <body onload="Initialize()"> <!-- This is created in the code with a dynamic size <div style="width:708px;height:622px;"> <div style="width:108px;height:600px;float:left;"> <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> <canvas id="idCanvas" width="600" height ="600" style="background-color:#F0F0F0;float:left;"></canvas> <div style="width:600px;height:22px;float:right;"> <input id="idLowX" type="text" size="9" style="width:100px;height:16px;float:left;"/> <div style="width:384px;height:22px;float:left;"></div> <input id="idHighX" type="text" size="9" style="width:100px;height:16px;float:left;"/> </div> </div> --> </body> </html>
// Scaling followed by a Translation // [ Sx 0 Tx ] [ 1 0 Tx ][ Sx 0 0 ] // [ 0 Sy Ty ] = [ 0 1 Ty ][ 0 Sy 0 ] // [ 0 0 1 ] [ 0 0 1 ][ 0 0 1 ] // Inverse // [ 1/Sx 0 -Tx/Sx ] [1/Sx 0 0 ][ 1 0 -Tx ] // [ 0 1/Sy -Ty/Sy ] = [ 0 1/Sy 0 ][ 0 1 -Ty ] // [ 0 0 1 ] [ 0 0 1 ][ 0 0 1 ] class CAffineTransformation2D { constructor(dSx, dSy, dTx, dTy) { this.mdaM = [dSx, 0.0, dTx, 0.0, dSy, dTy]; } Transform(daInP) { let mdaOutP = [0.0, 0.0]; mdaOutP[0] = this.mdaM[0]*daInP[0] + this.mdaM[2]; mdaOutP[1] = this.mdaM[4]*daInP[1] + this.mdaM[5]; return mdaOutP; } Invert(daInP) { let mdaOutP = [0.0, 0.0]; mdaOutP[0] = daInP[0]/this.mdaM[0] - this.mdaM[2]/this.mdaM[0]; mdaOutP[1] = daInP[1]/this.mdaM[4] - this.mdaM[5]/this.mdaM[4]; return mdaOutP; } // Scale the transformation and scale around the center point. This means that the translation must be adjusted ScaleAround(dScaleFactor, daCenter) { this.mdaM[0] *= dScaleFactor; this.mdaM[4] *= dScaleFactor; // This is LowX = CX - (CX - LowX)*ScalingFactor = CX*(1 - dScaleFactor) + LowX*dScaleFactor this.mdaM[2] = daCenter[0] - (daCenter[0] - this.mdaM[2])*dScaleFactor; // This is LowY = CY - (CY - LowY)*ScalingFactor = CY*(1 - dScaleFactor) + LowY*dScaleFactor this.mdaM[5] = daCenter[1] - (daCenter[1] - this.mdaM[5])*dScaleFactor; } TranslateBy(daDelta) { this.mdaM[2] += daDelta[0]; this.mdaM[5] += daDelta[1]; } }
// Use the affine transformation to transform from spatial coordinates to pixels class CGraphOnImage2D { constructor(dLowX, dHighX, dLowY, dHighY, qCanvas) { this.mqCanvas = qCanvas; let iCanvasW = qCanvas.width; let iCanvasH = qCanvas.height; let dScaleX = (dHighX - dLowX)/iCanvasW; // The y-axis is flipped let dScaleY = -(dHighY - dLowY)/iCanvasH; // Add .5 for the pixels to get the locations correct for half pixel offsets // [ Sx 0 (Tx + .5Sx) ] [ 1 0 Tx ][ Sx 0 0 ][ 1 0 .5 ] // [ 0 Sy (Ty + .5Sy) ] = [ 0 1 Ty ][ 0 Sy 0 ][ 0 1 .5 ] // [ 0 0 1 ] [ 0 0 1 ][ 0 0 1 ][ 0 0 1 ] this.mqPixelToPoint = new CAffineTransformation2D(dScaleX, dScaleY, dLowX + .5*dScaleX, dHighY + .5*dScaleY); } // Use this to scale the space for mouse events Zoom(dScaleFactor, daPixel) { // Transform the pixel to spatial coordinates let daCenter = this.mqPixelToPoint.Transform(daPixel); this.mqPixelToPoint.ScaleAround(dScaleFactor, daCenter); } Pan(daDeltaPixels) { // Get the x and y sizes of a single pixel in spatial coordiantes let daOriginImage = this.mqPixelToPoint.Transform([0.0, 0.0]); let daOneOneImage = this.mqPixelToPoint.Transform([1.0, 1.0]); let dPixelWidth = (daOneOneImage[0] - daOriginImage[0]); let dPixelHeight = (daOneOneImage[1] - daOriginImage[1]); // Adjust the transformation this.mqPixelToPoint.TranslateBy([daDeltaPixels[0]*dPixelWidth, daDeltaPixels[1]*dPixelHeight]); } LowerLeft() { return this.mqPixelToPoint.Transform([-.5, this.mqCanvas.height -.5]); } UpperRight() { return this.mqPixelToPoint.Transform([this.mqCanvas.width - .5, -.5]); } // 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() { let daLowerLeft = this.LowerLeft(); let daUpperRight = this.UpperRight(); let qContext = this.mqCanvas.getContext("2d"); // This for calculating a range for grid spacing let daZero = this.mqPixelToPoint.Transform([0.0, 0.0]); let daHundred = this.mqPixelToPoint.Transform([100.0, 100.0]); // Draw grid lines of two types // Get an estimate for a value that will be drawn about every 100 pixels and every 25 var dThickFreqX = this.FindSmallestGreaterPowerOfTwo(Math.abs(daHundred[0] - daZero[0])); var dThinFreqX = dThickFreqX/4; // Reverse the order for the y-coordinates var dThickFreqY = this.FindSmallestGreaterPowerOfTwo(Math.abs(daZero[1] - daHundred[1])); 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(daLowerLeft[0]/dThickFreqX)*dThickFreqX; var dGridValue = dGLM + dThinFreqX; qContext.strokeStyle = "#E8E8E8"; var i = 1; while (dGridValue < daUpperRight[0]) { if ((i % 4) != 0) { let daGridPt = this.mqPixelToPoint.Invert([dGridValue, 0]); qContext.beginPath(); qContext.moveTo(daGridPt[0], 0); qContext.lineTo(daGridPt[0], this.mqCanvas.width); qContext.stroke(); } dGridValue += dThinFreqX; ++i; } //------------------------------------ // Draw horizontal grid lines dGLM = Math.floor(daLowerLeft[1]/dThickFreqY)*dThickFreqY; dGridValue = dGLM + dThinFreqY; i = 1; while (dGridValue < daUpperRight[1]) { if ((i % 4) != 0) { let daGridPt = this.mqPixelToPoint.Invert([0, dGridValue]); qContext.beginPath(); qContext.moveTo(0, daGridPt[1]); qContext.lineTo(this.mqCanvas.width, daGridPt[1]); qContext.stroke(); } dGridValue += dThinFreqY; ++i; } // Draw the thick lines // Draw vertical grid lines qContext.strokeStyle = "#DDDDDD"; dGLM = Math.floor(daLowerLeft[0]/dThickFreqX)*dThickFreqX; dGridValue = dGLM + dThickFreqX; while (dGridValue < daUpperRight[0]) { let daGridPt = this.mqPixelToPoint.Invert([dGridValue, 0]); qContext.beginPath(); qContext.moveTo(daGridPt[0], 0); qContext.lineTo(daGridPt[0], this.mqCanvas.height); qContext.stroke(); dGridValue += dThickFreqX; } // Draw horizontal grid lines dGLM = Math.floor(daLowerLeft[1]/dThickFreqY)*dThickFreqY; dGridValue = dGLM + dThickFreqY; while (dGridValue < daUpperRight[1]) { let daGridPt = this.mqPixelToPoint.Invert([0, dGridValue]); qContext.beginPath(); qContext.moveTo(0, daGridPt[1]); qContext.lineTo(this.mqCanvas.width, daGridPt[1]); 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"; // Find the pixel location of the origin var daOriginPixel = this.mqPixelToPoint.Invert([0, 0]); if (-.5 <= daOriginPixel[0] && (this.mqCanvas.width - .5) >= daOriginPixel[0]) { this.DrawArrow(qContext, daOriginPixel[0], this.mqCanvas.height, daOriginPixel[0], 0); } // Draw the X-Axis if (-.5 <= daOriginPixel[1] && (this.mqCanvas.height - .5) >= daOriginPixel[1]) { this.DrawArrow(qContext, 0, daOriginPixel[1], this.mqCanvas.width, daOriginPixel[1]); } } // Arrow drawing function to draw the axes 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 this.DrawArrowhead(qContext, dX1, dY1, dX2, dY2); } DrawArrowhead(qContext, dX1, dY1, dX2, dY2) { // The angle direction of the arrow 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(); } }
// This is the external container for the HTML elements class CCanvasGraph2D { constructor(iCanvasWidth, iCanvasHeight) { // Get the body element and create the other elements let qBodyElement = document.getElementsByTagName("body")[0]; let qInstructions = document.createElement("p"); qInstructions.innerHTML = "Hold the left mouse button and move the mouse to pan. Use the mouse wheel to zoom in and out at the cursor."; qBodyElement.appendChild(qInstructions); // Create the external div with width = (iCanvasWidth + 108) and height = (iCanvasHeight + 22) and add it to the body let qContainerDiv = document.createElement("div"); qContainerDiv.style.width = (iCanvasWidth + 108) + "px"; qContainerDiv.style.height = (iCanvasHeight + 22) + "px"; qBodyElement.appendChild(qContainerDiv); // Add a div for the y-range labels let qLabelYDiv = document.createElement("div"); qLabelYDiv.style.width = "108px"; qLabelYDiv.style.height = iCanvasHeight + "px"; qLabelYDiv.style.float = "left"; qContainerDiv.appendChild(qLabelYDiv); this.mqHighY = document.createElement("input"); this.mqHighY.style.width = "100px"; this.mqHighY.style.height = "16px"; this.mqHighY.id = "idHighY"; this.mqHighY.type = "text"; this.mqHighY.size = "9"; qLabelYDiv.appendChild(this.mqHighY); let qSpaceYDiv = document.createElement("div"); qSpaceYDiv.style.width = "108px"; qSpaceYDiv.style.height = (iCanvasHeight - 44) + "px"; qSpaceYDiv.style.float = "left"; qLabelYDiv.appendChild(qSpaceYDiv); this.mqLowY = document.createElement("input"); this.mqLowY.style.width = "100px"; this.mqLowY.style.height = "16px"; this.mqLowY.id = "idLowY"; this.mqLowY.type = "text"; this.mqLowY.size = "9"; qLabelYDiv.appendChild(this.mqLowY); // Create the canvas and add it to that div let qCanvas = document.createElement("canvas"); qCanvas.id = "idCanvas"; qCanvas.width = iCanvasWidth + ""; qCanvas.height = iCanvasHeight + ""; qCanvas.style.backgroundColor = "#F0F0F0"; qCanvas.style.float = "left"; qContainerDiv.appendChild(qCanvas); // Add a div for the x-range labels let qLabelXDiv = document.createElement("div"); qLabelXDiv.style.width = iCanvasWidth + "px"; qLabelXDiv.style.height = "22px"; qLabelXDiv.style.float = "right"; qContainerDiv.appendChild(qLabelXDiv); this.mqLowX = document.createElement("input"); this.mqLowX.style.width = "100px"; this.mqLowX.style.height = "16px"; this.mqLowX.style.float = "left"; this.mqLowX.id = "idLowX"; this.mqLowX.type = "text"; this.mqLowX.size = "9"; qLabelXDiv.appendChild(this.mqLowX); let qSpaceXDiv = document.createElement("div"); qSpaceXDiv.style.width = (iCanvasWidth - 216) + "px"; qSpaceXDiv.style.height = "22px"; qSpaceXDiv.style.float = "left"; qLabelXDiv.appendChild(qSpaceXDiv); this.mqHighX = document.createElement("input"); this.mqHighX.style.width = "100px"; this.mqHighX.style.height = "16px"; this.mqHighX.id = "idHighX"; this.mqHighX.type = "text"; this.mqHighX.size = "9"; qLabelXDiv.appendChild(this.mqHighX); // Create the graphing object this.mqGraph2D = new CGraphOnImage2D(-4.0, 4.0, -4.0, 4.0, qCanvas); } DrawTheGraphAndBounds() { let qCanvas = document.getElementById("idCanvas"); let qContext = qCanvas.getContext("2d"); qContext.clearRect(0, 0, qCanvas.width, qCanvas.height); this.mqGraph2D.DrawGridLinesAndAxes(); // Fill in the range bounds let daLowerLeft = this.mqGraph2D.LowerLeft(); let daUpperRight = this.mqGraph2D.UpperRight(); // Remove any trailing zeroes and decimal points this.mqLowX.value = daLowerLeft[0].toFixed(4).replace(/\.?0+$/, ""); this.mqHighX.value = daUpperRight[0].toFixed(4).replace(/\.?0+$/, ""); this.mqLowY.value = daLowerLeft[1].toFixed(4).replace(/\.?0+$/, ""); this.mqHighY.value = daUpperRight[1].toFixed(4).replace(/\.?0+$/, ""); } GetGraph() { return this.mqGraph2D; } }
// Global variables var gqCanvasGraph = null; var gbIsDragging = false; var giCursorX = 0; var giCursorY = 0; function Initialize() { gqCanvasGraph = new CCanvasGraph2D(600, 600); let qCanvas = document.getElementById("idCanvas"); // With the elements created, add the event listeners to the canvas qCanvas.addEventListener("wheel", fnZoom, { passive: false }); qCanvas.addEventListener("mousedown", MouseDown); qCanvas.addEventListener("mouseup", MouseUp); qCanvas.addEventListener("mousemove", MouseMove); gqCanvasGraph.DrawTheGraphAndBounds(); } // Global mouse event handler functions function fnZoom(e) { e.preventDefault(); var dFactor = (e.deltaY > 0.0) ? 1.1: .9090909090909; SetCursorCoordinates(e); // Zoom centered on the pixel location gqCanvasGraph.GetGraph().Zoom(dFactor, [giCursorX, giCursorY]); gqCanvasGraph.DrawTheGraphAndBounds(); } function MouseMove(e) { if (gbIsDragging) { var iLastCursorX = giCursorX; var iLastCursorY = giCursorY; SetCursorCoordinates(e); var iCurrCursorX = giCursorX; var iCurrCursorY = giCursorY; gqCanvasGraph.GetGraph().Pan([iLastCursorX - iCurrCursorX, iLastCursorY - iCurrCursorY]); gqCanvasGraph.DrawTheGraphAndBounds(); } } 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; }
© 20072025 XoaX.net LLC. All rights reserved.