HTML5 Dynamic Charting

Nick Bennett

26/01/2011

  • HTML5
  • JavaScript
  • Scripts

HTML5 logo

I recently attended an HTML5 workshop held by Mr Remy Sharp. I was the only honest schmo in the room to admit to having already purchased the HTML5 book on offer. I was waiting for the free T-shirt that never transpired :( The workshop itself was really interesting and I left with the usual 'I'm going to change the world with this new knowledge' excitement. A few months have passed and I've finally got around to writing this blog (the changing the world part will have to wait).

The part of the day that really got my juices flowing was the Canvas tag stuff. It's perhaps embarrassing to admit but I'd not had any dealings with it beforehand. The bit that really blew my top was the traversing through each pixel of a transposed image on the canvas and altering each one. Simply amazing. During my lunch hours at work, I wanted to investigate the Canvas tag in more depth. I wanted to create a simple table of data, that using JavaScript, could be used to generate a simple line graph. I wasn't even sure if it was possible.

Just a quick mention of Zen-Coding which I was also introduced to at the Workshop. Imagine being able to write entire blocks of HTML shorthand and then convert them at the press of a button. So

div.class-name becomes <div class="class-name"></div>

and

ul>li*3 becomes <ul><li></li><li></li><li></li></ul> (Tabbed in reality)

Back to the point...

Generating a line graph on  the fly

The final example can be seen here. I started off with a basic table of data within my HTML5 document.

<table id="data">
    <tbody>
        <tr>
            <th></th>
            <th>Jokes Learnt</th>
            <th>Girls Numbers</th>
        </tr>
        <tr>
            <td>Day 1</td>
            <td>5</td>
            <td>1</td>
        </tr>
        <tr>
            <td>Day 2</td>
            <td>7</td>
            <td>1</td>
        </tr>
        <tr>
            <td>Day 3</td>
            <td>10</td>
            <td>3</td>
        </tr>
        <tr>
            <td>Day 4</td>
            <td>20</td>
            <td>4</td>
        </tr>
        <tr>
            <td>Day 5</td>
            <td>40</td>
            <td>7</td>
       </tr>
    </tbody>
</table>

Here we are studying the highly scientific effect of comedy on the fairer sex. The next thing I added was the container for my X and Y labels and a key holder which explained what the line represented.

<div id="y-axis">

    <ul id="y-axis">

     <li id="label-1"></li>

     <li id="label-2"></li>

     <li id="label-3"></li>

     <li id="label-4"></li>

  </ul>
</div>

<div id="canvas-holder"></div>

<div id="key-holder">

<table id="key">
    <tbody>
        <tr>
            <th>Key</th>
            <th>Value</th>
        </tr>
    </tbody>
</table>

</div>

<div id="x-axis">

</div>

I learnt at the workshop about document.querySelector and document.querySelectorAll. Both return the corresponding DOM elements of the queried parameters. I wanted this HTML file to be self-sufficient and not require any 3rd party JavaScript libraries. So using these DOM selectors I did the following.

var can     = document.querySelector('canvas');

// Traverse each header to work out the keys required
var thList    = document.querySelector('#data').querySelectorAll('th');
var dataKey    = new Array();

// Start count at 1 to avoid our blank cell
for (i=1;i<thList.length;i++) {
    dataKey[i] = thList[i].innerHTML;
}

The first line assigns our canvas element to the variable can. Next up we use the headers to define the number of lines we need on our graph and what they represent. Notice that the count starts at 1 to ignore the first th cell which is empty. We assign these variables to an array (dataKey).

// Use the td content to derive the data array
var tdList            = document.querySelector('#data').querySelectorAll('td');
var dataArray        = new Object();
var dataCount        = 0;
var currentKey        = '';

// Variables for our X and Y labels
var smallestFigure    =    0;
var largestFigure    =     0;
var xLabels            =    new Array();
var xlableCnt        =    0;

// Traverse the td to derive our data array
for (i=0;i<tdList.length;i++) {

    // Set smallest largest
    if (!isNaN(tdList[i].innerHTML)) {
        testNumber            =    parseFloat(tdList[i].innerHTML);

        if (!smallestFigure && !largestFigure) {
            smallestFigure        =    testNumber;
            largestFigure        =    testNumber;
        } else {
            if (testNumber < smallestFigure) {
                smallestFigure     = testNumber;
            }

            if (testNumber > largestFigure) {
                largestFigure    =    testNumber;
            }
        }
    }

    // Col 1 is the key for this row
    if (dataCount == 0) {
        currentKey                 = tdList[i].innerHTML;
        dataArray[currentKey]    = new Array();
        xLabels[xlableCnt]        = tdList[i].innerHTML;
        xlableCnt++;
    } else {
       dataArray[currentKey][dataKey[dataCount]] = tdList[i].innerHTML;
    }

    // Count starts at zero
    if (dataCount == (dataKey.length-1)) {
        dataCount = 0;
    } else {
        dataCount++;
    }

}

OK, this may look slightly overwhelming. Just take some deep breaths and I'll talk you through it. What this code is trying to do is to use the content of the table to form a data array. Firstly the tdList becomes a holder for all of the content within the td tags. Next, we define all of the variables we are going to require to get our array. We loop through all of the td DOM elements. For each td element, we check whether the content is a valid number. If it is we check the value against previous values to determine the largest and smallest numbers. This will be used to determine our Y axis range. Our X axis labels use the first (vertical) column in our table. Each label will also be used as our array key to determine the values plotted on the canvas.

// Each value will be worth a units worth of pixel
var unit     = can.height / largestFigure;

// Lets now get our side label
document.querySelector('#label-1').innerHTML = largestFigure;
document.querySelector('#label-2').innerHTML = (largestFigure/4) * 3;
document.querySelector('#label-3').innerHTML = (largestFigure/4) * 2;
document.querySelector('#label-4').innerHTML = (largestFigure/4) * 1;

// Our bottom label
var xWidth    =    can.width/xLabels.length;

// Lets fill our x labels
for (i=0;i<xLabels.length;i++) {

    xSpan                 = document.createElement('span');
    xSpan.innerHTML     = xLabels[i];
    xSpan.className        = 'spanBlock';
    xSpan.style.width    = xWidth + 'px';
    document.querySelector('#ul-x-axis').appendChild(xSpan);

}

Next up we use our assigned variables to determine the X and Y axis labels. We find our unit (what each value is in pixels) by taking the length of the canvas and dividing it by the largest figure we have. The Y axis is split into 4 so is not very dynamic, I'm sure you can do better. Also, you may have noted I've picked some nice round numbers in this example, barely the case in reality! Our X-axis uses the xLabels variable we assigned to early. We create a new span DOM element and set the inner HTML to match the value of the label. Now for the fun part...

// Check whether we can use the canvas
if (can.getContext){ 

    // Get our object
    var ctx = can.getContext('2d');  

    // Black out back ground
    ctx.fillStyle     = '#000';
    ctx.fillRect(0,0, ctx.canvas.width, ctx.canvas.height);
    ctx.fillStyle     = '#fff';
    ctx.strokeStyle = '#fff';

    // default drawing style
    ctx.lineWidth     = 5;
    ctx.lineCap     = 'round';
    ctx.save();

    // Define array of fill colours first element is
    // blank so that our keys can be used
    strokeArr    =    new Array('','#fff','#0000FF');

The first line checks that we can actually use the getContext method of the object. We set up our ctx object which allows us to draw on our canvas tag. We create our black rectangle which covers the canvas, this is the base for our chart. The lineWidth and lineCap set up how our line graph will look on the canvas. Our strokeArr sets the colours for each of our lines. This isn't at all dynamic. You may ask 'why is the first element blank' and 'what if someone adds another column'. To which my response will be 'Shut the hell up!'. Anyway, we are now ready to start drawing our lines.

for (i=0;i<dataKey.length;i++) {

    if (dataKey[i] != undefined) {

        // Set Starting point
        ctx.strokeStyle = strokeArr[i];
        ctx.beginPath();
        ctx.moveTo(0, 500);

        // Set defualt points
        xPoint    =    0;
        yPoint    =    500;

        // Item in object
        xCount    =    0;

        // Loop through items
        for (o in dataArray) {

            // X point moves along with the count
            xPoint     = xCount * xWidth;

            // Use the unit calculated earlier to calc the y point
            if (dataArray[o][dataKey[i]]) {
                yPoint    = 500 - (dataArray[o][dataKey[i]] * unit);
            } else {
                yPoint    = 500;
            }

            // Start new line or join existing
            if (xCount == 0) {
                ctx.moveTo(xPoint, yPoint);
            } else {
                ctx.lineTo(xPoint, yPoint);
            }

            xCount++;
        }

        // Add stroke
        ctx.stroke();

        // Add this row to key
        keyTr = document.createElement('tr');

        keyTd = document.createElement('td');
        keyTd.style.backgroundColor  = strokeArr[i];

        keyTr.appendChild(keyTd);

        keyTd2    =    document.createElement('td');
        keyTd2.innerHTML = dataKey[i];

        keyTr.appendChild(keyTd2);

        document.querySelector('#key tbody').appendChild(keyTr);
    }
}

We are now ready to use our data array which we generated earlier. We loop through each item (line on the chart) which contains an object of points on the map. The strokeArr value is the colour of the line. The beginPath method is the equivalent of taking your pen off of the canvas. The moveTo method puts the pen back on the canvas at the correct point, in this case where the X and Y axis meet. We start looping the points on the canvas we need to draw.

The xPoint (where on the X axis we need to plot our point) is calculated by using the xCount (number of iterations through our X-axis) times the xWidth (number of pixels between each X label). The Y axis is a little more complicated as the lowest Y point isn't 0 it's 500 (because the canvas start at the top left not the bottom left). We use our data value and times it by the unit which gives us the number of pixels up we need to be. We have our X and Y points so we now just need to set them. We check whether this is a new line or an existing line and plot the point accordingly. The ctx.stroke() fills the line for us. Now we have our line we need to create a record in our key table. The first td is filled with the same colour of the line while the second td holds the label for that line. We append the new table row to the key table.

And that's that. I was very impressed with the ease with which we can do this funky stuff and look forward to more of the same!