A very short introduction to HTML5's canvas tag element: Graphing mathematical functions in JavaScript

The canvas tag in JavaScript gives the programmer low-level graphics control. If you wanted to write an in-browser game with 2D graphics, it's the right element to use. Firefox, Safari and Chrome support the tag natively, and Internet Explorer supports canvas with a translation layer. This article serves as a very short introduction to the canvas tag by describing the code necessary to create a system for rendering graphs of univariate mathematical functions (like y(x) = sin(x) + C and f(x) = x2 + 2).

More resources

Why?

I've wanted to play with the canvas tag for a while now, and I finally have the perfect confluence of excuses to do so: I want to build an HTML-based rendering system for the output of my static analyzers; I'm introducing three engineering scholars to programming languages with graphics in JavaScript; and I'm building a super-optimizing compilation engine for dynamic languages like Scheme and JavaScript. Building a system for rendering univariate functions seems like a fun way to tip-toe into the canvas tag; below, you'll find an explanation of the system with a working demo.

Setting up the canvas

Setting up a canvas is straightforward; just drop in a canvas tag:

 <canvas id="xy-graph" width="535" height="300">
  CANVAS NOT SUPPORTED IN THIS BROWSER!
 </canvas>

The body of the canvas tag is what will be shown if the browser does not support the canvas tag.

Accessing the canvas

To draw on the canvas, grab ahold of the element itself:

 var Canvas = document.getElementById('xy-graph');

And then grab its context:

 if (Canvas.getContext) {
  Ctx = Canvas.getContext('2d');
  // Rendering code goes here.
 } else {
  // Canvas not supported code goes here.
 }

To draw on the canvas, call methods on the Ctx object.

Preparing the canvas

To prepare the canvas for drawing, it's best to clear what's already on the canvas with the clearRect(startX,startY,finishX,finishY) method:

 Ctx.clearRect(0,0,Canvas.width,Canvas.height) ;

Drawing lines

For graphing functions, we only need to draw lines, and we can do that with only four canvas context functions:

Demo application: Graphing a function

Type JavaScript code in below that defines a function named F, and the click Draw! button further down. The full, well-commented source code to this app is about 200 lines, and it's available down below.




CANVAS NOT SUPPORTED IN THIS BROWSER!

Source code

[canvas-graph.js]

/* Initialization */

// To be called when the page finishes loading:
function init() {
 Draw() ;
}


/* Canvas and context objects */

var Canvas = document.getElementById('xy-graph');  
var Ctx = null ;

var Width = Canvas.width ;
var Height = Canvas.height ;


/*
  The origin (0,0) of the canvas is the upper left:

  (0,0)
    --------- +X
   |
   |
   |
   |
   +Y

  Positive x coordinates go to the right, and positive y coordinates go down.

  The origin in mathematics is the "center," and positive y goes *up*.

  We'll refer to the mathematics coordinate system as the "logical"
  coordinate system, and the coordinate system for the canvas as the
  "physical" coordinate system.

  The functions just below set up a mapping between the two coordinate
  systems.

  They're defined as functions, so that one wanted to, they could read
  ther values from a from instead of having them hard-coded.
 
 */


// Returns the right boundary of the logical viewport:
function MaxX() {
  return 10 ;
}

// Returns the left boundary of the logical viewport:
function MinX() {
  return -10 ;
}

// Returns the top boundary of the logical viewport:
function MaxY() {
  return MaxX() * Height / Width;
}

// Returns the bottom boundary of the logical viewport:
function MinY() {
   return MinX() * Height / Width;
}

// Returns the physical x-coordinate of a logical x-coordinate:
function XC(x) {
  return (x - MinX()) / (MaxX() - MinX()) * Width ;
}

// Returns the physical y-coordinate of a logical y-coordinate:
function YC(y) {
  return Height - (y - MinY()) / (MaxY() - MinY()) * Height ;
}


/* Rendering functions */

// Clears the canvas, draws the axes and graphs the function F.
function Draw() {

 // Evaluate the user-supplied code, which must bind a value to F.
 eval(document.getElementById('function-code').value) ;
 
 if (Canvas.getContext) {
  
   // Set up the canvas:
   Ctx = Canvas.getContext('2d');
   Ctx.clearRect(0,0,Width,Height) ;

   // Draw:
   DrawAxes() ;
   RenderFunction(F) ;
  
  } else {
    // Do nothing.
  }
}


// Returns the distance between ticks on the X axis:
function XTickDelta() {
  return 1 ;
}

// Returns the distance between ticks on the Y axis:
function YTickDelta() {
  return 1 ;
}

  
// DrawAxes draws the X ad Y axes, with tick marks.
function DrawAxes() {
 Ctx.save() ;
 Ctx.lineWidth = 2 ;
 // +Y axis
 Ctx.beginPath() ;
 Ctx.moveTo(XC(0),YC(0)) ;
 Ctx.lineTo(XC(0),YC(MaxY())) ;
 Ctx.stroke() ;

 // -Y axis
 Ctx.beginPath() ;
 Ctx.moveTo(XC(0),YC(0)) ;
 Ctx.lineTo(XC(0),YC(MinY())) ;
 Ctx.stroke() ;

 // Y axis tick marks
 var delta = YTickDelta() ;
 for (var i = 1; (i * delta) < MaxY() ; ++i) {
  Ctx.beginPath() ;
  Ctx.moveTo(XC(0) - 5,YC(i * delta)) ;
  Ctx.lineTo(XC(0) + 5,YC(i * delta)) ;
  Ctx.stroke() ;  
 }

 var delta = YTickDelta() ;
 for (var i = 1; (i * delta) > MinY() ; --i) {
  Ctx.beginPath() ;
  Ctx.moveTo(XC(0) - 5,YC(i * delta)) ;
  Ctx.lineTo(XC(0) + 5,YC(i * delta)) ;
  Ctx.stroke() ;  
 }  

 // +X axis
 Ctx.beginPath() ;
 Ctx.moveTo(XC(0),YC(0)) ;
 Ctx.lineTo(XC(MaxX()),YC(0)) ;
 Ctx.stroke() ;

 // -X axis
 Ctx.beginPath() ;
 Ctx.moveTo(XC(0),YC(0)) ;
 Ctx.lineTo(XC(MinX()),YC(0)) ;
 Ctx.stroke() ;

 // X tick marks
 var delta = XTickDelta() ;
 for (var i = 1; (i * delta) < MaxX() ; ++i) {
  Ctx.beginPath() ;
  Ctx.moveTo(XC(i * delta),YC(0)-5) ;
  Ctx.lineTo(XC(i * delta),YC(0)+5) ;
  Ctx.stroke() ;  
 }

 var delta = XTickDelta() ;
 for (var i = 1; (i * delta) > MinX() ; --i) {
  Ctx.beginPath() ;
  Ctx.moveTo(XC(i * delta),YC(0)-5) ;
  Ctx.lineTo(XC(i * delta),YC(0)+5) ;
  Ctx.stroke() ;  
 }
 Ctx.restore() ;
}


// When rendering, XSTEP determines the horizontal distance between points:
var XSTEP = (MaxX()-MinX())/Width ;


// RenderFunction(f) renders the input funtion f on the canvas.
function RenderFunction(f) {
  var first = true;

  Ctx.beginPath() ;
  for (var x = MinX(); x <= MaxX(); x += XSTEP) {
   var y = f(x) ;
   if (first) {
    Ctx.moveTo(XC(x),YC(y)) ;
    first = false ;
   } else {
    Ctx.lineTo(XC(x),YC(y)) ;
   }
  }
  Ctx.stroke() ;
}