Visual Graphing in JavaScript

While on vacation, I took some time to have fun writing a JavaScript tool that I have never seen before. While on the plane, I decided to explore graphing in JavaScript, using the old-school TI-85 as a visual model. Even though, I never seen this done before, JavaScript is a fast language and the only reason it may have not already been written, is a lack of purpose. Nevertheless, I felt writing this application was fun exercise and an opportunity to explore writing a graphing program.

To start, we have to consider the how to draw the graph that we will be drawing lines on. We need to draw the x and y axises, and the tick marks on those axises. By default, I assumed that most people would be use a square DOM element, but it can be configured to use rectangular shaped elements as well. When instantiating a new instance of the Grapher, use the following:

Example 1: Instantiating Grapher

var graphNode = $(graph);

var config = {
    equation: -5 + Math.pow(x, 2), // default is 
    tickFreq: 10, // default
    xRange: 10, // default
    yRange: 10 // default
};

var graph = Core.Widget.Grapher(graphNode, config);
graph.redraw();

In this example, the graph will range from -10 to 10 x and y, showing a tick my every other even value in those ranges, which happens to be the defaults assumed by the Object. To support this, the graphNode element need only be square; the Object will manage scale. We are also passing in a default equation so a line is initially drawn with the graph.

Inside the Object, we have several internal objects: node (the graph DOM), config (configuration of graph), points (array of line points), and that (the closure safe scope); 5 constants for the axis styles, point styles, and tick styles; and two helper Functions: one to clear the graph complete (clearGraph) and the second to only clear the points of the current line (clearPoints). The clearGraph method removes everything from the graph DOM, and the clearPoints method iterates through each point and removes them, while leaving the graph intact.

Example 2: Private Helper Functions

/**
 * Clears all graph related DOM nodes.
 * @method clearGraph
 * @private
 */
var clearGraph = function() {
    while (node.firstChild) {
        node.removeChild(node.firstChild);
    };
};

/**
 * Clears all graph point DOM nodes.
 * @method clearPoints 
 * @private
 */
var clearPoints = function() {
    for (var i = points.length - 1; 0 <= i; i -= 1) {
        points[i].parentNode.removeChild(points[i]);
    }

    points = [];
};

When the page first loads, or the graph DOM first becomes available, we need to make it look more like a graph. For this reason, there is a private method drawGraph, that calls clearGraph, then rebuilds the nodes that causes the DOM to look like a graph:

Example 3: Drawing the Graph

/**
 * Draws the actual graph: axis and ticks.
 * @method drawGraph
 * @private
 */
var drawGraph = function() {
    clearGraph();

    var xAxis = node.appendChild($D.createTag(div, {style: XAXIS}));
    var yAxis = node.appendChild($D.createTag(div, {style: YAXIS}));

    Dom.setStyle(xAxis, top, config.region.top + config.yMid + px);
    Dom.setStyle(xAxis, left, config.region.left + px);
    Dom.setStyle(xAxis, width, config.width + px);
    
    Dom.setStyle(yAxis, top, config.region.top + px);
    Dom.setStyle(yAxis, left, config.region.left + config.xMid + px);
    Dom.setStyle(yAxis, height, config.height + px);

    for (var i = 0; i < config.tickFreq; i += 1) {
        var xTick = node.appendChild($D.createTag(div, {style: XTICK}));
        var yTick = node.appendChild($D.createTag(div, {style: YTICK}));

        Dom.setStyle(xTick, left, config.region.left + config.xTickStep * (i * 2) + px);
        Dom.setStyle(xTick, top, config.region.top + config.yMid - (config.tickFreq / 2) + px);

        Dom.setStyle(yTick, left, config.region.left + config.xMid - (config.tickFreq / 2) + px);
        Dom.setStyle(yTick, top, config.region.top + config.yTickStep * (i * 2) + px);
    }
};

Once the DOM is clear, we create the x and y axises using the internal config Object where we store many cached values about the DOM node and other values computed when the graph Object is instantiated. Both axises are lines that are positioned in the center of the node according to their respective dimension, and the height/width of the graph DOM node. Then we iterated through the tick frequency count and add the tick marks along the axises.

Finally, we need a method to draw lines according to a provided equation containing 1 or more x. For now, we simply eval the equation, but I intend to validate the equation to only support +, -, /, *, %, and all methods on the Math Object. Here is the method to draw the line:

Example 4: Drawing the Line

/**
 * Draws the line for the equation in a given color.
 * @method drawLine
 * @param eq {String} Required. The equation to draw.
 * @param color {String} Optional. The color of the line.
 * @private
 */
var drawLine = function(eq, color) {
    // iterate on the posible x points, stop when limit is reached or y exceeds grid
    for (var i = 0; i < config.width; i += 1) {
        var x = config.xStep * (config.xMid + (config.width > config.xMid ? -i : i));
        var y = eval(eq.replace(/x/gi, -x).replace(--, ));
        var y2 = config.yMid - (y * config.yTickStep);

        // stop when exceeding graph range
        if (y > config.yRange || y < -config.yRange || isNaN(y)) {continue;};

        var point = node.appendChild($D.createTag(div, {style: POINT}));

        Dom.setStyle(point, left, config.region.left + i + px);
        Dom.setStyle(point, top, config.region.top + y2 + px);

        if (color) {
            Dom.setStyle(point, background-color, color);
        }

        points[points.length] = point;
    }

    var k = points.length;

    // iterate on each point, determine if and fill all the y positions
    for (var j = 0; j < k - 1; j += 1) {
        var p1 = Dom.getRegion(points[j]);
        var p2 = Dom.getRegion(points[j + 1]);
        var useP2 = p2.top > p1.top;
        var diff = Math.abs(p2.top - p1.top);

        if (1 < diff) {
            for (var m = 1; m < diff; m += 1) {
                var left = m < diff / 2 && useP2 ? p1.left : p2.left;
                var top = (useP2 ? p1.top : p2.top) + m;

                var p = node.appendChild($D.createTag(div, {style: POINT}));

                Dom.setStyle(p, left, left + px);
                Dom.setStyle(p, top, top + px);

                if (color) {
                    Dom.setStyle(p, background-color, color);
                }

                points[points.length] = p;
            }
        }
    }
};

The method requires an equation and also excepts a color, if you wish to not use black to draw a line. This is forward thinking, so that, once I figure out the best way to do it, each graph will be able to support multiple lines. We iterate through each 1px in the width of the graph DOM node. So we first compute x (value of x at pixel i), y (value produced by the equation), and y2, the actual top position of the point. These values are rather tricky, because computing x changes depending on whether we are on the left or right of the y-axis, and computing y is done using eval and regex replaces. I learned that the equation -x would throw an error computing y, because it had double negatives, so I fixed that using a simple regex.

When iterating, we continue when the value of y does not fit inside the configured range, then we create the point DOM node and position it (we would also color it, if a color was provided), and cache the point. Then we iterate through the points and fill in each pixel between point n and n - 1 when they are more than 1-pixel apart in the y-axis, creating a smooth line. Whichever point is higher determines which point to use as the top offset, and since each point is only 1-pixel separated on the x-axis, we also use the higher point to determine which pixel should be used for the left style, depending on how close the fill point is to the point used for the top&rsquot; style.

When you bring it all together, you get something like this example: Graph Test. I plan on making the graph colors more customizable, cleaning all equation strings to ensure no tomfoolery, additional graph stuff (like titles, legends, labels, etc.), and supporting multiple lines on a single graph.