Thursday, June 7, 2012

Rendering static progress bars using HTML5 canvas

This project is up on GitHub.

Recently our stakeholders were in need of an analytics page for some common tabular data generated from an in-house application we’ve built.  Without going into too much detail, they essentially needed to track progress.

In all honestly they would have been happy seeing a table of integers with some format like “progress / total”.  I thought that we could do better than that though, since it would just look like a wall of text.  The solution I wanted to build would allow them to easily glance at the data and know immediately what kind of progress was being made.

I’ve had some experience using fusion charts before but I needed something a little simpler for static progress bars.  Initially I was thinking to use SVG, but I would be rendering hundreds of progress bars on one page (even if I’m paging the data, which I am using a custom paging class I built – that’s for a future post though).

So, I decided on using canvas.  Everyone using our app has the latest browsers, and I knew it would be a good learning experience.


File->New Project… Lets model our data

We use MVC for the analytics site but I won’t go into too much detail here.  I’m using the Visual Studio 2012 preview.

For my demo data, I’ll create a simple model that allows me to display a set of progress values for one total value.


Index action

My index action is really simple.  Using the model above, I create a viewmodel and populate it with a whole bunch of random data.  For the demonstration, each set of progress data has 5 progress values for a given total.

First I randomize the total, and I then I use that value to randomize 5 progress values.



Create the index view

Next up I create my view which is strongly typed to a list of my ProgressionData model.  Most of the work will be done using the tooling template, which will allow me to easily display the data in a table.  Nothing too impressive to see yet.


I make some modifications to the templated view so that I have ‘appropriate’ headers, and utilize my viewmodels list of progress values so I can render progress bars. 

For each progress value, I have a cell that will contain the canvas.  Notice I use custom ‘data’ attributes so I have the values to work with in javascript.  The final markup is below:



Rendering borders

For each progress bar in my markup I want to render using the same method, so I select all the canvas elements by class using Jquery and iterate through them using an anonymous function.

First I need to get those custom data values.  I calculate the percentage complete here too, which you’ll see I use in rendering.


I’m finally ready to begin rendering, and I’m going to start with the borders for each progress bar.  It’s a nice starting point since it puts something on the screen and all I’m dealing with is width and height.

To render to a canvas you need to first get the rendering context, “2d” in this case.

Drawing with canvas is similar to openGL and DirectX in that it’s a state machine.  You setup some state, and then put all your rendering calls together in “batches” (or at least that is what you should be doing to maintain optimal rendering).

Here I don’t really need to set anything up.  Black is the default stroke and fill colors, so I just draw the border using the strokeRect function.


The first two parameters are the starting x and y points for the rectangle; the last two are the width and height.

Canvas’ coordinate system is setup such that 0,0 is the upper left point of the rendering area with positive x values moving right, and positive y values moving down.

I set the context to null at the end to encourage FireFox to collect garbage as quickly as possible.  It’s a performance trick I’ve read about somewhere… It did seem to help when looking under the hood at memory consumption.

Here’s what we have so far:



Visualizing the data

I’m well on my way to displaying a nice representation of the progress data that I have.  Next I’ll fill in those bars.

In order to draw the progress I multiply the width of the progress bar by the percentageComplete which gives me a value equal to or less than the width.

I also need to add a pixel of padding when I fill, otherwise I’ll draw over the border (which isn’t necessarily horrible depending on your needs).

I’ll set the fillStyle to green before I add an additional fill draw call.  I start just 1 pixel in for both x and y, and subtract 2 from the progress width and height (1 for the border pixel on the left and right, and 1 for the top and bottom – 2 total for the width and height).


Here’s what it looks like (keeping in mind that the values are randomized every refresh).



Going further

In all honesty I could have stopped here.  After all, this is a much better solution than just showing the values in plain text.  I can clearly see how much progress we’ve made for each set of data.

But, that’s just not my style.  I’m in it for the long haul; I don’t ever like to settle.

The first adjustment I want to make is the color.  It’s a bit flat, and it’s a whole lot of green.

To render a linear gradient you call createLinearGradient from the context, and pass in the start and end x and y coordinates.  I wanted a gradient that would travel down the height of the progress bar, so I only passed in the height value.

You make a call to addColorStop for every color you want to draw in the gradient.  The function takes a float value from 0.0 to 1.0, indicating the start position of that color.  The second parameter is the color of that color stop.

With this in mind, I decided to put a little life into the “staticness” of my progress bars. 

I start off with red and pink colors.  If our progress is equal to or above 75%, I color them green indicating that we’re close to being done!  If were 50% or above, yellow and orange.


Here’s what we have now, and I think you’ll agree this looks a ton better:


Different colors can be used if you so desire.  You could for example, color the bar a flat faded green if it’s 100% to further help separate “completion” from “in progress”.


Overlaying text

One last finishing touch I wanted to make was to also display the values themselves on the progress bar.

To do this, I make add some state changes for text and then call fillText.  It takes three parameters:  The text, the x location and the y location.

Canvas has built-in support for rendering shadows so I took advantage of this.  Normally I would just render the shadow text first a few pixels off, so it’s a welcomed feature.

My shadow color is set to white and offset by a few pixels.  I also added a 2 pixel blur.


To render the text, I needed to do some more math.  When text gets rendered, it appears at the x and y values you specify.  So if you were to render text at 0,0 you wouldn’t even see it because it would be just outside the canvas.

I decided to render the textual value of the progress as well as the percentage complete.  The text is rendered on the left and right side of the progress bar, centered in middle.

Rendering on the left was easy.  I used my previous padding variable and added it to 0.  To render in the middle of the bar, I divide the height by two and add a few pixels to compensate for the height of the text itself (you’ll have to play with this value to get it right).

Rendering on the right side is a bit more tricky.  You need to know how many pixels the text itself takes up to ensure that it doesn’t get rendered outside the canvas.

To solve this, you could guess and add padding, or just use the function measureText!  It takes one parameter, the text you want to measure.

To get the measurement I want, I needed to convert a float value with a very long trail of precision to a percentage.  So, I just multiply the float by 100 to move the decimal, and then call the toFixed function with 0 precision (I only wanted a whole number).  Then I added the percent symbol.


All that hard work has paid off.  Here’s how the final rendering looks:



Let me know what you think in the comments.  It was a really fun task that only took a few days of work, mostly to learn to use the canvas library and work with the data I had already been returning from our database (which took much longer).

The entire project is up on GitHub.  Feel free to download it and check it out.