Core JavaScript

SVG - Zoom a Graphed Line with a Mouse Wheel

The acronym SVG stands for Scalable Vector Graphics and refers to graphics that are generated by a geometric specification rather than by pixels, like an image (called rasterized graphics as opposed to the vectorized graphics described here). This example demonstartes how to zoom in on the graph of a line with the mouse wheel.

GraphOfLine.html

<!DOCTYPE html>
<html>
  <head>
    <title>XoaX.net's Javascript</title>
    <script type="text/javascript" src="GraphOfLine.js"></script>
  </head>
  <body onload="fnInitialize()">
  </body>
</html>

GraphOfLine.js

var gqRegion = {mdX0:-10, mdDX:20, mdY0:-10, mdDY:20};

function fnZoom(e) {
  e.preventDefault();
  var dFactor = (e.deltaY > 0.0) ? .1*gqRegion.mdDX : -.1*gqRegion.mdDX;
  if ((gqRegion.mdDX + dFactor) < 4.0) {
    gqRegion.mdDX = 4.0;
    gqRegion.mdDY = 4.0;
    gqRegion.mdX0 = -2.0;
    gqRegion.mdY0 = -2.0;
  } else if ((gqRegion.mdDX + dFactor) > 32.0){
    gqRegion.mdDX = 32.0;
    gqRegion.mdDY = 32.0;
    gqRegion.mdX0 = -16.0;
    gqRegion.mdY0 = -16.0;
  } else {
    gqRegion.mdDX += dFactor;
    gqRegion.mdDY += dFactor;
    gqRegion.mdX0 -= (dFactor/2);
    gqRegion.mdY0 -= (dFactor/2);
  }
  document.getElementById("idFlip").innerHTML = '';
  SetVisibleRange(gqRegion.mdX0, gqRegion.mdDX, gqRegion.mdY0, gqRegion.mdDY);
  CreateLine(2, -4, 1, 'red');
}

function fnInitialize() {
  // Get the body element
  var qBody = document.getElementsByTagName("body")[0];
  // Create a div to catch the mouse events (I will need to do the coordinate conversions)
  var qMainDiv = document.createElement("div");
  qMainDiv.style.backgroundColor = "#ffffff";
  qMainDiv.style.width = "600px";
  qMainDiv.style.height = "600px";
  qMainDiv.addEventListener("wheel", fnZoom, { passive: false });
  qBody.appendChild(qMainDiv);
  // Create the svg element inside the body
  var qSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  qSvg.setAttribute('width', '600');
  qSvg.setAttribute('height', '600');
  qMainDiv.appendChild(qSvg);

  // Add in an element for general definitions
  var qDefs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
  qDefs.setAttribute('id', 'idDefs');
  qSvg.appendChild(qDefs);
  // Add in the markers for the arrowheads
  var qArrowEnd = document.createElementNS("http://www.w3.org/2000/svg", "marker");
  qArrowEnd.setAttribute('id', 'idArrowEnd');
  qArrowEnd.setAttribute('markerWidth', '10');
  qArrowEnd.setAttribute('markerHeight', '10');
  qArrowEnd.setAttribute('refX', '9');
  qArrowEnd.setAttribute('refY', '5');
  qArrowEnd.setAttribute('orient', 'auto');
  qDefs.appendChild(qArrowEnd);
  var qArrowEndPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  qArrowEndPath.setAttribute('d', 'M 0,0 L10,5 L0,10 Z');
  qArrowEndPath.setAttribute('fill', 'context-fill');
  qArrowEndPath.setAttribute('stroke', 'context-stroke');
  qArrowEnd.appendChild(qArrowEndPath);
  // The arrow start
  var qArrowStart = document.createElementNS("http://www.w3.org/2000/svg", "marker");
  qArrowStart.setAttribute('id', 'idArrowStart');
  qArrowStart.setAttribute('markerWidth', '10');
  qArrowStart.setAttribute('markerHeight', '10');
  qArrowStart.setAttribute('refX', '0');
  qArrowStart.setAttribute('refY', '5');
  qArrowStart.setAttribute('orient', 'auto');
  qDefs.appendChild(qArrowStart);
  var qArrowStartPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  qArrowStartPath.setAttribute('d', 'M 10,10 L0,5 L10,0 Z');
  qArrowStartPath.setAttribute('fill', 'context-fill');
  qArrowStartPath.setAttribute('stroke', 'context-stroke');
  qArrowStart.appendChild(qArrowStartPath);

  // Add in a transformation node
  var qFlip = document.createElementNS("http://www.w3.org/2000/svg", "g");
  qFlip.setAttribute('id', 'idFlip');
  // Flip the y values over the x-axis
  qFlip.setAttribute('transform', 'scale(1,-1)');
  qSvg.appendChild(qFlip);

  // Set the visible range. This will be called again, if scaling is allowed
  SetVisibleRange(-10, 20, -10, 20);
  CreateLine(2, -4, 1, 'red');
}

function SetVisibleRange(dX0, dDX, dY0, dDY) {
  gqRegion.mdX0 = dX0;
  gqRegion.mdDX = dDX;
  gqRegion.mdY0 = dY0;
  gqRegion.mdDY = dDY;
  var qSvg = document.getElementsByTagName("svg")[0];
  qSvg.setAttribute('viewBox', dX0+' '+dY0+' '+dDX+' '+dDY);
  // If the coordinates are set to (x0, dx) and (y0, dy), then after we flip the y-axis the y starts at -(y0 + dy). So, I need to translate by (0, 2y0 + dy).
  var qFlip = document.getElementById("idFlip");
  qFlip.setAttribute('transform', 'translate(0,'+(2*dY0+dDY)+') scale(1,-1)');

  // Draw the horizontal grid lines (y = k)
  // Get the lowest and highest y integers
  var dLowY = Math.ceil(dY0);
  // Make sure that the first line isn't at zero
  var dLeastHoriz = (dLowY == 0.0) ? (dLowY + 1) : dLowY;
  var dHighY = Math.floor(dY0 + dDY);
  // Set the horizontal paths, but not y = 0 because that is the x-axis
  if (dLeastHoriz <= dHighY) {
    // Add in the horizontal line template for the grid
    var qHorizPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
	qHorizPath.setAttribute('id', 'idHorizPath');
	qHorizPath.setAttribute('d', 'M'+dX0+','+dLeastHoriz+' L'+(dX0+dDX)+','+dLeastHoriz);
	qHorizPath.setAttribute('stroke', 'black');
    qHorizPath.setAttribute('stroke-width', '.002');
    qFlip.appendChild(qHorizPath);
    for (var i = dLeastHoriz + 1; i <= dHighY; ++i) {
	  if (i != 0) {
	    var qHorizLine = document.createElementNS("http://www.w3.org/2000/svg", "use");
	    qHorizLine.setAttribute('href', '#idHorizPath');
	    qHorizLine.setAttribute('transform', 'translate(0, '+(i - dLeastHoriz)+')');
	    qFlip.appendChild(qHorizLine);
      }
    }
  }
  // Add in the x-axis, if needed
  if (dLowY <= 0.0 && dHighY >= 0.0) {
	var qAxisX = document.createElementNS("http://www.w3.org/2000/svg", "path");
	qAxisX.setAttribute('d', 'M'+dX0+', 0 L'+(dX0+dDX)+', 0');
	qAxisX.setAttribute('stroke', 'black');
    qAxisX.setAttribute('stroke-width', '.02');
    qAxisX.setAttribute('marker-end', 'url(#idArrowEnd)');
    qFlip.appendChild(qAxisX);
  }

  // Draw the vertical lines (x = h)
  // Get the lowest and highest x integers
  var dLowX = Math.ceil(dX0);
  // Make sure that the first line isn't at zero, the y-axis
  var dLeastVert = (dLowX == 0.0) ? (dLowX + 1) : dLowX;
  var dHighX = Math.floor(dX0 + dDX);
  // Set the horizontal paths, but not y = 0 because that is the x-axis
  if (dLeastVert <= dHighX) {
    // Add in the horizontal line template for the grid
    var qVertPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
	qVertPath.setAttribute('id', 'idVertPath');
	qVertPath.setAttribute('d', 'M'+dLeastVert+','+dY0+' L'+dLeastVert+','+(dY0+dDY));
	qVertPath.setAttribute('stroke', 'black');
    qVertPath.setAttribute('stroke-width', '.002');
    qFlip.appendChild(qVertPath);
    for (var i = dLeastVert + 1; i <= dHighX; ++i) {
	  if (i != 0) {
	    var qVertLine = document.createElementNS("http://www.w3.org/2000/svg", "use");
	    qVertLine.setAttribute('href', '#idVertPath');
	    qVertLine.setAttribute('transform', 'translate('+(i - dLeastVert)+',0)');
	    qFlip.appendChild(qVertLine);
      }
    }
  }
  // Add in the y-axis, if needed
  if (dLowX <= 0.0 && dHighX >= 0.0) {
	var qAxisY = document.createElementNS("http://www.w3.org/2000/svg", "path");
	qAxisY.setAttribute('d', 'M0,'+dY0+' L0,'+(dY0+dDY));
	qAxisY.setAttribute('stroke', 'black');
    qAxisY.setAttribute('stroke-width', '.02');
    qAxisY.setAttribute('marker-end', 'url(#idArrowEnd)');
    qFlip.appendChild(qAxisY);
  }
}

function CreateLine(dA, dB, dC, sColor) {
  // Find the two intersections with the edges, if any: gqRegion = {mdX0:-10, mdDX:20, mdY0:-10, mdDY:20};
  // Find the low and high x intersections: y = (C - Ax)/B
  var daPoints = [];
  if (dB != 0.0) {
	var dY = (dC - dA*gqRegion.mdX0)/dB;
	if ((dY >= gqRegion.mdY0) && (dY <= (gqRegion.mdY0 + gqRegion.mdDY))) {
	  daPoints.push([gqRegion.mdX0, dY]);
	}
	dY = (dC - dA*(gqRegion.mdX0 + gqRegion.mdDX))/dB;
	if (dY >= gqRegion.mdY0 && dY <= (gqRegion.mdY0 + gqRegion.mdDY)) {
	  daPoints.push([gqRegion.mdX0 + gqRegion.mdDX, dY]);
	}
  }
  // Find the low and high y intersections: x = (C - By)/A
  if (dA != 0.0) {
	var dX = (dC - dB*gqRegion.mdY0)/dA;
	if (dX >= gqRegion.mdX0 && dX <= (gqRegion.mdX0 + gqRegion.mdDX)) {
	  daPoints.push([dX, gqRegion.mdY0]);
	}
	dX = (dC - dB*(gqRegion.mdY0 + gqRegion.mdDY))/dA;
	if (dX >= gqRegion.mdX0 && dX <= (gqRegion.mdX0 + gqRegion.mdDX)) {
	  daPoints.push([dX, gqRegion.mdY0 + gqRegion.mdDY]);
	}
  }
  var qFlip = document.getElementById("idFlip");
  var qLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
  qLine.setAttribute('x1', daPoints[0][0]);
  qLine.setAttribute('y1', daPoints[0][1]);
  qLine.setAttribute('x2', daPoints[1][0]);
  qLine.setAttribute('y2', daPoints[1][1]);
  qLine.setAttribute('stroke', sColor);
  qLine.setAttribute('stroke-width', '.02');
  qLine.setAttribute('marker-start', 'url(#idArrowStart)');
  qLine.setAttribute('marker-end', 'url(#idArrowEnd)');
  qFlip.appendChild(qLine);
  // Add an equation label to the line
  //<text transform="translate(-2, -2.2) rotate(45) scale(1,-1)" x="0" y="0" font-size=".5" fill="red">x - y = 1</text>
  var qLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
  // We need three transformations: flip the label, rotate by the slope and translate by the perpendicular and center
  var sTransformation = 'scale(1,-1)';
  // The rotation will be in the range (-90, 90]
  var dRotation = ((dB == 0.0) ? 90.0 : (Math.atan(-dA/dB)*(180.0/Math.PI)));
  sTransformation = 'rotate('+dRotation+') '+sTransformation;
  // The 90 degree counter clockwise perpendicular is (A, B), slope vector is (B, -A)
  var dL = 1.5*Math.sqrt(dA*dA+dB*dB);
  sTransformation = 'translate('+((daPoints[0][0] + daPoints[1][0])/2.0 + (dA/dL))+', '+((daPoints[0][1] + daPoints[1][1])/2.0 + (dB/dL))+') '+sTransformation;
  // Set the label at the midpoint
  qLabel.setAttribute('transform', sTransformation);
  qLabel.setAttribute('x', '0');
  qLabel.setAttribute('y', '0');
  qLabel.setAttribute('text-anchor', 'middle');
  qLabel.setAttribute('font-size', '.5');
  qLabel.setAttribute('fill', sColor);
  var sA = (Math.abs(dA) == 1) ? ((dA == 1) ? '' : '-') : dA;
  var sB = (Math.abs(dB) == 1) ? ((dB == 1) ? ' + ' : ' - ') : ((dB == 1) ? ' + '+ Math.abs(dB) : ' - '+  Math.abs(dB));
  qLabel.textContent = sA+'x'+sB+'y = '+dC;
  qFlip.appendChild(qLabel);
}
 

Output

 
 

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