HTML5 Tutorial: Build a Chart with JavaScript and the HTML5 Canvas

Learn how to use HTML5 to build charting features such as lines, shapes, and gradients into your web applications

Dan Wahlin

August 26, 2011

10 Min Read
Green chalkboard with pictures drawn on

The HTML5 canvas is capable of rendering lines, shapes, images, text, and more without relying on a plug-in. Although the canvas element isn't supported by older browsers, the latest version of all major browsers (Internet Explorer, Safari, Chrome, Firefox, and Opera) now support the canvas, making it an option for rendering charts, graphs, and other types of visual data. In cases where a browser doesn't support the canvas, a fallback can be provided that renders data using Silverlight, Flash, or another type of plug-in.

In my article "Using the HTML5 Canvas Tag," I walked through the fundamentals of using the HTML5 canvas to render different types of shapes. In this article I'll discuss how the canvas can be used to render a line chart using JavaScript. An example of the chart that will be discussed is shown in Figure 1.

Figure 1: Building a chart using the HTML5 Canvas

To render the chart, a JavaScript object named CanvasChart was created that handles rendering all the lines, shapes, and text shown in Figure 1. Figure 2 shows an example of defining CanvasChart settings and calling the CanvasChart's render() function.

                    

The render() function accepts the canvas element ID as well as a JSON object that defines chart properties and data to be used in the rendering process.

The CanvasChart object demonstrates several key features of the canvas element that can be used in applications, including rendering lines, shapes, gradients, text, and even transformed text. Let's take a look at how the CanvasChart object was created.

Rendering Gradients and Text

The code for the CanvasChart object is located in a file named canvasChart.js that's available with this article's downloadable code. The code starts by defining a CanvasChart object that exposes two members named renderType and render. renderType is used to define what will be rendered on the chart (currently it supports rendering lines and points), while render() is used to render the data on the canvas as well as the associated labels for the x and y axes. The skeleton code for CanvasObject is shown in Figure 3.

var CanvasChart = function () {    var ctx,    margin = { top: 40, left: 75, right: 0, bottom: 75 },    chartHeight, chartWidth, yMax, xMax, data,    maxYValue = 0,    ratio = 0,    renderType = { lines: 'lines', points: 'points' };//functions go herereturn {    renderType: renderType,    render: render    };} (); 

The code follows a JavaScript pattern referred to as the "revealing module pattern," which provides a convenient way to write objects that expose specific members only to outside callers. This example exposes the renderType variable and render function.

The render() function shown in Figure 4 accepts the canvas ID defined within the page (see Figure 2) as well as a JSON object that defines details about labels, font sizes, data points, and more that are used for charting.

var render = function(canvasId, dataObj) {    data = dataObj;    getMaxDataYValue();    var canvas = document.getElementById(canvasId);    chartHeight = canvas.getAttribute('height');    chartWidth = canvas.getAttribute('width');    xMax = chartWidth - (margin.left + margin.right);    yMax = chartHeight - (margin.top + margin.bottom);    ratio = yMax / maxYValue;    ctx = canvas.getContext("2d");    renderChart();};

The render function starts by assigning the dataObj parameter to a variable within the CanvasChart object, then calls an internal function named getMaxDataYValue(). The getMaxDataYValue() function determines the maximum Y value for the data points. From there, the render() function locates the target canvas element within the page, calculates width and height values, and accesses the canvas's 2D context that will be used to draw. Finally, a call is made to renderChart() to start the rendering process.

The renderChart() function (see Figure 5) orchestrates different drawing functions and handles rendering the background, lines, labels, and data by calling the respective functions.

var renderChart = function () {    renderBackground();    renderText();    renderLinesAndLabels();    //render data based upon type of renderType(s) that client supplies    if (data.renderTypes == undefined || data.renderTypes == null)     data.renderTypes = [renderType.lines];    for (var i = 0; i < data.renderTypes.length; i++) {    renderData(data.renderTypes[i]);    }};

Different canvas features are used in the CanvasChart object, such as gradients and transforms. For example, the renderBackground() function shown in Figure 6 demonstrates how linear gradients can be created. The renderBackground() function uses the 2D context's createLinearGradient() function to define a gradient that has four gradient stops. Once the gradient is defined, it is assigned to the fillStyle property, then rendered to a rectangular area using the fillRect() function.

var renderBackground = function() {    var lingrad = ctx.createLinearGradient(margin.left, margin.top,   xMax - margin.right, yMax);    lingrad.addColorStop(0.0, '#D4D4D4');    lingrad.addColorStop(0.2, '#fff');    lingrad.addColorStop(0.8, '#fff');    lingrad.addColorStop(1, '#D4D4D4');    ctx.fillStyle = lingrad;    ctx.fillRect(margin.left, margin.top, xMax - margin.left,   yMax - margin.top);    ctx.fillStyle = 'black';};

CanvasChart also demonstrates how text can be manipulated using transforms. The text displayed on the Y axis is rotated so that it displays vertically, as shown in Figure 7.

Figure 7: Using the canvas to rotate text vertically

Text rotation is accomplished by using the canvas element's rotate transform functionality, which is found in the renderText() function shown in Figure 8. The key section of this code is the call to ctx.save() (toward the bottom of the function shown in Figure 8), which saves the current state of the canvas so that it can be restored. This is necessary so that the entire canvas isn't rotated. Once the current canvas state is saved, a call to the rotate() function is made to rotate the canvas. The text is then drawn for the vertical axis using the fillText() function. Once the rotated text is rendered, the canvas is restored back to its saved state—the state before the rotate transform was applied and the text was rendered.

var renderText = function() {    var labelFont = (data.labelFont != null) ? data.labelFont : '20pt Arial';    ctx.font = labelFont;    ctx.textAlign = "center";//Title    var txtSize = ctx.measureText(data.title);    ctx.fillText(data.title, (chartWidth / 2), (margin.top / 2));//X-axis text    txtSize = ctx.measureText(data.xLabel);    ctx.fillText(data.xLabel, margin.left + (xMax / 2) - (txtSize.width / 2),   yMax + (margin.bottom / 1.2));//Y-axis text    ctx.save();    ctx.rotate(-Math.PI / 2);    ctx.font = labelFont;    ctx.fillText(data.yLabel, (yMax / 2) * -1, margin.left / 4);    ctx.restore();};

After the x and y axis text is rendered, the CanvasChart object makes a call to renderLinesAndLabels() (see Figure 9) to handle rendering the horizontal and vertical lines.

var renderLinesAndLabels = function () {    //Vertical guide lines    var yInc = yMax / data.dataPoints.length;    var yPos = 0;    var yLabelInc = (maxYValue * ratio) / data.dataPoints.length;    var xInc = getXInc();    var xPos = margin.left;    for (var i = 0; i < data.dataPoints.length; i++) {    yPos += (i == 0) ? margin.top : yInc;    //Draw horizontal lines    drawLine(margin.left, yPos, xMax, yPos, '#E8E8E8');    //y axis labels    ctx.font = (data.dataPointFont != null) ? data.dataPointFont :       '10pt Calibri';    var txt = Math.round(maxYValue - ((i == 0) ? 0 : yPos / ratio));    var txtSize = ctx.measureText(txt);    ctx.fillText(txt, margin.left - ((txtSize.width >= 14) ?       txtSize.width : 10) - 7, yPos + 4);    //x axis labels    txt = data.dataPoints[i].x;    txtSize = ctx.measureText(txt);    ctx.fillText(txt, xPos, yMax + (margin.bottom / 3));    xPos += xInc;    }//Vertical line    drawLine(margin.left, margin.top, margin.left, yMax, 'black');//Horizontal Line    drawLine(margin.left, yMax, xMax, yMax, 'black');};

Lines are normally drawn using the 2D context's moveTo() and lineTo() functions, which are wrapped in a function named drawLine() to simplify the process. Figure 10 shows the drawLine function.

var drawLine = function(startX, startY, endX, endY, strokeStyle, lineWidth) {    if (strokeStyle != null) ctx.strokeStyle = strokeStyle;    if (lineWidth != null) ctx.lineWidth = lineWidth;    ctx.beginPath();    ctx.moveTo(startX, startY);    ctx.lineTo(endX, endY);    ctx.stroke();    ctx.closePath();};

At this point the canvas looks like the image shown in Figure 11.

Figure 11: Rendering the background, lines, text, and axes text components of a chart using the HTML5 canvas

Rendering Data

Once the labels and lines are rendered, CanvasChart handles rendering the data points by calling a function named renderData(). This function handles iterating through the JSON data points and drawing lines, points, or both depending upon the settings passed to CanvasChart's render() function. Lines are drawn to connect the different data points through calls to the drawLine() function shown earlier in Figure 10, while circles are drawn for specific data points by making calls to the 2D context's arc() function. The circles that are rendered have a radial gradient applied to them using the createRadialGradient() function. The complete renderData function is shown in Figure 12.

var renderData = function(type) {    var xInc = getXInc();    var prevX = 0,     prevY = 0;for (var i = 0; i < data.dataPoints.length; i++) {    var pt = data.dataPoints[i];    var ptY = (maxYValue - pt.y) * ratio;    if (ptY < margin.top) ptY = margin.top;    var ptX = (i * xInc) + margin.left;    if (i > 0 && type == renderType.lines) {        //Draw connecting lines        drawLine(ptX, ptY, prevX, prevY, 'black', 2);    }    if (type == renderType.points) {        var radgrad = ctx.createRadialGradient(ptX, ptY, 8, ptX - 5,       ptY - 5, 0);        radgrad.addColorStop(0, 'Green');        radgrad.addColorStop(0.9, 'White');        ctx.beginPath();        ctx.fillStyle = radgrad;        //Render circle        ctx.arc(ptX, ptY, 8, 0, 2 * Math.PI, false)        ctx.fill();        ctx.lineWidth = 1;        ctx.strokeStyle = '#000';        ctx.stroke();        ctx.closePath();    }    prevX = ptX;    prevY = ptY;    }};

The renderData function handles iterating through the JSON data points and calling the appropriate canvas function to render the data. Once the data points are rendered, the chart looks like the image shown in Figure 13.

Figure 13: The completed chart rendered using the HTML5 canvas

Canvas Insights

You can see that there's a fair amount of JavaScript code required to use the canvas object. However, once the different API functions are understood, it's simply a process of calling the appropriate functions to render lines, text, or shapes. Next, join us in "Using MVVM in JavaScript with Knockout," we'll switch gears and discuss how you this JavaScript framework can help streamline UI coding in your web apps.

Sign up for the ITPro Today newsletter
Stay on top of the IT universe with commentary, news analysis, how-to's, and tips delivered to your inbox daily.

You May Also Like