A Snazzy Animated Pie Chart with HTML5 and jQuery

Learn how to use the HTML5 canvas element, CSS3 and jQuery to create a gorgeous, interactive animated pie chart. Full code included for your own use.

How to Create Snazzy Animated Pie Charts with HTML5 and jQuery
View Demo » Download Code

In this tutorial I'm going to show you how to build a lovely, interactive pie chart using the latest HTML5 technologies. Not that long ago, this kind of thing was only practical to do with Flash. Now, thanks to advances such as the HTML5 canvas element, we can create pretty nifty animated effects using nothing but JavaScript, CSS, and a small sprinkling of maths!

Step 1. Create the markup

Here's the markup for our chart page:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>

<title>Elated.com | Snazzy Animated Pie Chart with HTML5 and jQuery - Demo</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" >

</head>
<body>

<div id="container">

  <div class="wideBox">
    <h1>Widgets Sold in 2010</h1>
    <p>Click a colour in the chart, or an item in the table, to pull out a slice! <a href="#">Back to Tutorial</a></p>
  </div>

  <canvas id="chart" width="600" height="500"></canvas>

  <table id="chartData">

    <tr>
      <th>Widget</th><th>Sales ($)</th>
     </tr>

    <tr style="color: #0DA068">
      <td>SuperWidget</td><td>1862.12</td>
    </tr>

    <tr style="color: #194E9C">
      <td>MegaWidget</td><td>1316.00</td>
    </tr>

    <tr style="color: #ED9C13">
      <td>HyperWidget</td><td>712.49</td>
    </tr>

    <tr style="color: #ED5713">
      <td>WonderWidget</td><td>3236.27</td>
    </tr>

    <tr style="color: #057249">
      <td>MicroWidget</td><td>6122.06</td>
    </tr>

    <tr style="color: #5F91DC">
      <td>NanoWidget</td><td>128.11</td>
    </tr>

    <tr style="color: #F88E5D">
      <td>LovelyWidget</td><td>245.55</td>
    </tr>
  </table>

  <div class="wideBox">
    <p>© Elated.com | <a href="#">Back to Tutorial</a></p>
    <p style="font-size: .8em"><a rel="license" href="http://creativecommons.org/licenses/by/3.0/"><img alt="Creative Commons License" style="border-width:0" src="http://i.creativecommons.org/l/by/3.0/88x31.png" /></a><br />This <span xmlns:dc="http://purl.org/dc/elements/1.1/" href="http://purl.org/dc/dcmitype/Text" rel="dc:type">work</span> by <a xmlns:cc="http://creativecommons.org/ns#" href="http://www.elated.com/" property="cc:attributionName" rel="cc:attributionURL">http://www.elated.com/</a> is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/3.0/">Creative Commons Attribution 3.0 Unported License</a>.</p>
  </div>

</div>

</body>
</html>

The markup is pretty simple. It contains:

  • A "container" div to wrap and centre the content
  • An HTML5 canvas element for the pie chart
  • A table element containing the chart data
  • Header and footer boxes containing the chart title, copyright and so on

Notice that the tr (table row) elements in the table are all given their own colours. Later, we'll use our JavaScript to read these colour values and use them to colour the corresponding pie chart slices.

Step 2. Create the CSS

Now we've created our basic HTML page, let's add some CSS to style the various elements in the page:


<style>

body {
  background: #fff;
  color: #333;
  font-family: "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
  font-size: 0.9em;
  padding: 40px;
}

.wideBox {
  clear: both;
  text-align: center;
  margin-bottom: 50px;
  padding: 10px;
  background: #ebedf2;
  border: 1px solid #333;
  line-height: 80%;
}

#container {
  width: 900px;
  margin: 0 auto;
}

#chart, #chartData {
  border: 1px solid #333;
  background: #ebedf2 url("images/gradient.png") repeat-x 0 0;
}

#chart {
  display: block;
  margin: 0 0 50px 0;
  float: left;
  cursor: pointer;
}

#chartData {
  width: 200px;
  margin: 0 40px 0 0;
  float: right;
  border-collapse: collapse;
  box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
  -moz-box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
  -webkit-box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
  background-position: 0 -100px;
}

#chartData th, #chartData td {
  padding: 0.5em;
  border: 1px dotted #666;
  text-align: left;
}

#chartData th {
  border-bottom: 2px solid #333;
  text-transform: uppercase;
}

#chartData td {
  cursor: pointer;
}

#chartData td.highlight {
  background: #e8e8e8;
}

#chartData tr:hover td {
  background: #f0f0f0;
}

</style>

Again, no big surprises here. The CSS contains rules for the page body, the wide header/footer boxes, the container, the #chart canvas element, and the #chartData table element.

A couple of points to note:

  • The #chart and #chartData elements are given a subtle gradient background, created using the image gradient.png (included in the code download). Yes, you can even have background images on canvas elements!
  • We've used the CSS3 box-shadow property (and its vendor-specific equivalents) to add a drop shadow to the data table. (While it's also possible to add a drop shadow to the canvas element, I found that this really slowed down the pie chart animation on WebKit browsers.)
Styled table
The styled table containing the data. Now doesn't it look lovely!

Step 3. Include the jQuery and ExplorerCanvas libraries

Now we're ready to start writing our JavaScript code. First of all, include 2 libraries:


<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<!--[if IE]>
<script src="http://explorercanvas.googlecode.com/svn/trunk/excanvas.js"></script>
<![endif]-->
<script>
  • jQuery
    This should be fairly self-explanatory. If not, you might like to read our other jQuery tutorials!
  • ExplorerCanvas
    Internet Explorer — bless it — doesn't support canvas. (IE 9 might or might not, at the time of writing.) Fortunately some very clever coders created ExplorerCanvas, a library that emulates most of the canvas methods and properties using IE's SVG capabilities. It's not perfect, but it'll do the job for us. Since only IE needs this library, we use conditional comments to load this library just for IE.

We've linked directly to both these libraries on Google's CDN, but you could just as easily download the 2 .js files and link to them on your own site.

Step 4. Create the main function, config settings, and useful variables

We'll wrap our code in a main function called pieChart(). That way, we can keep all functions and variables related to the chart in one place, and not pollute the global scope. We'll then use jQuery to call pieChart() when the page's DOM is ready.

Let's start by putting various useful configuration settings inside pieChart():


// Run the code when the DOM is ready
$( pieChart );

function pieChart() {

  // Config settings
  var chartSizePercent = 55;                        // The chart radius relative to the canvas width/height (in percent)
  var sliceBorderWidth = 1;                         // Width (in pixels) of the border around each slice
  var sliceBorderStyle = "#fff";                    // Colour of the border around each slice
  var sliceGradientColour = "#ddd";                 // Colour to use for one end of the chart gradient
  var maxPullOutDistance = 25;                      // How far, in pixels, to pull slices out when clicked
  var pullOutFrameStep = 4;                         // How many pixels to move a slice with each animation frame
  var pullOutFrameInterval = 40;                    // How long (in ms) between each animation frame
  var pullOutLabelPadding = 65;                     // Padding between pulled-out slice and its label  
  var pullOutLabelFont = "bold 16px 'Trebuchet MS', Verdana, sans-serif";  // Pull-out slice label font
  var pullOutValueFont = "bold 12px 'Trebuchet MS', Verdana, sans-serif";  // Pull-out slice value font
  var pullOutValuePrefix = "$";                     // Pull-out slice value prefix
  var pullOutShadowColour = "rgba( 0, 0, 0, .5 )";  // Colour to use for the pull-out slice shadow
  var pullOutShadowOffsetX = 5;                     // X-offset (in pixels) of the pull-out slice shadow
  var pullOutShadowOffsetY = 5;                     // Y-offset (in pixels) of the pull-out slice shadow
  var pullOutShadowBlur = 5;                        // How much to blur the pull-out slice shadow
  var pullOutBorderWidth = 2;                       // Width (in pixels) of the pull-out slice border
  var pullOutBorderStyle = "#333";                  // Colour of the pull-out slice border
  var chartStartAngle = -.5 * Math.PI;              // Start the chart at 12 o'clock instead of 3 o'clock

  // Declare some variables for the chart
  var canvas;                       // The canvas element in the page
  var currentPullOutSlice = -1;     // The slice currently pulled out (-1 = no slice)
  var currentPullOutDistance = 0;   // How many pixels the pulled-out slice is currently pulled out in the animation
  var animationId = 0;              // Tracks the interval ID for the animation created by setInterval()
  var chartData = [];               // Chart data (labels, values, and angles)
  var chartColours = [];            // Chart colours (pulled from the HTML table)
  var totalValue = 0;               // Total of all the values in the chart
  var canvasWidth;                  // Width of the canvas, in pixels
  var canvasHeight;                 // Height of the canvas, in pixels
  var centreX;                      // X-coordinate of centre of the canvas/chart
  var centreY;                      // Y-coordinate of centre of the canvas/chart
  var chartRadius;                  // Radius of the pie chart, in pixels

  // Set things up and draw the chart
  init();

Most of these lines should be easy to understand by looking at the comments in the code. A few warrant closer inspection:

chartSizePercent
To allow room for pulling out slices and displaying labels, we want the actual pie chart to be a fair bit smaller than the canvas. In this case, 55% is a good setting.
chartStartAngle
By default, angles in JavaScript — as in most other languages — are specified in radians, where 0 radians is the 3 o'clock position. Since we want to start our chart from 12 o'clock, we'll use this setting to subtract π/2 radians — that is, a quarter turn — from various angles in the code. I've explained this visually in the figure below.
currentPullOutSlice and currentPullOutDistance
Since we'll be animating a slice as it pulls out from the pie, we need these variables to track the animation. currentPullOutSlice tracks which slice is being pulled out, or has been pulled out (a value of -1 means that no slices are pulled out), and currentPullOutDistance tracks how much the slice has been pulled out so far.
animationId
This will hold the value returned by setInterval() when we create the animation. It's a numeric ID that we can then pass to clearInterval() when we want to end the animation.
chartData
We'll use this array to store data about each slice in the chart, including its label and value (pulled from the HTML table), and its start and end angles.
chartColours
This array will hold the colour of each slice in the chart, again pulled from the HTML table.
init()
The last line of code calls an init() function, which sets up the chart and gets things going. We'll write this function next!
The chartStartAngle variable explained
When drawing on a canvas, 0 radians is 3 o'clock. Since we want our pie chart to start from 12 o'clock, we use chartStartAngle to subtract π/2 radians (a quarter turn) from the slice angles when drawing the chart.

Step 5. Initialise the chart

We're now ready to set up our chart. Let's write an init() function to do all the setup for us:


  /**
   * Set up the chart data and colours, as well as the chart and table click handlers,
   * and draw the initial pie chart
   */

  function init() {

    // Get the canvas element in the page
    canvas = document.getElementById('chart');

    // Exit if the browser isn't canvas-capable
    if ( typeof canvas.getContext === 'undefined' ) return;

    // Initialise some properties of the canvas and chart
    canvasWidth = canvas.width;
    canvasHeight = canvas.height;
    centreX = canvasWidth / 2;
    centreY = canvasHeight / 2;
    chartRadius = Math.min( canvasWidth, canvasHeight ) / 2 * ( chartSizePercent / 100 );

    // Grab the data from the table,
    // and assign click handlers to the table data cells
    
    var currentRow = -1;
    var currentCell = 0;

    $('#chartData td').each( function() {
      currentCell++;
      if ( currentCell % 2 != 0 ) {
        currentRow++;
        chartData[currentRow] = [];
        chartData[currentRow]['label'] = $(this).text();
      } else {
       var value = parseFloat($(this).text());
       totalValue += value;
       value = value.toFixed(2);
       chartData[currentRow]['value'] = value;
      }

      // Store the slice index in this cell, and attach a click handler to it
      $(this).data( 'slice', currentRow );
      $(this).click( handleTableClick );

      // Extract and store the cell colour
      if ( rgb = $(this).css('color').match( /rgb\((\d+), (\d+), (\d+)/) ) {
        chartColours[currentRow] = [ rgb[1], rgb[2], rgb[3] ];
      } else if ( hex = $(this).css('color').match(/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/) ) {
        chartColours[currentRow] = [ parseInt(hex[1],16) ,parseInt(hex[2],16), parseInt(hex[3], 16) ];
      } else {
        alert( "Error: Colour could not be determined! Please specify table colours using the format '#xxxxxx'" );
        return;
      }

    } );

    // Now compute and store the start and end angles of each slice in the chart data

    var currentPos = 0; // The current position of the slice in the pie (from 0 to 1)

    for ( var slice in chartData ) {
      chartData[slice]['startAngle'] = 2 * Math.PI * currentPos;
      chartData[slice]['endAngle'] = 2 * Math.PI * ( currentPos + ( chartData[slice]['value'] / totalValue ) );
      currentPos += chartData[slice]['value'] / totalValue;
    }

    // All ready! Now draw the pie chart, and add the click handler to it
    drawChart();
    $('#chart').click ( handleChartClick );
  }

Note that init(), as well as all the other functions we'll create from now on, should go inside the outer pieChart() function. Functions within functions, where the inner function accesses variables declared in the outer function, are known as closures.

The init() function is fairly lengthy, so let's break it down:

  1. Get the canvas element
    First we get the "#chart" canvas element from the page and store it in an object called canvas. We'll do all of our drawing on the canvas through this object.
  2. Check for canvas support in the browser
    Before we do anything else, we should check that the browser actually supports the HTML5 canvas element. We do this by seeing if the canvas object contains the getContext() method — a frequently-used method that we'll use later on. If it doesn't, then the browser presumably doesn't support canvas, so we exit the function.
  3. Compute and store the canvas and chart dimensions
    Since we'll frequently use values like the canvas width, height, and centre, as well as the chart radius, we compute these values now and store them in variables for use later on.
  4. Grab the data from the table
    We use a jQuery selector — $('#chartData td') — to select all the data cells in the table. We can then iterate through these cells with the jQuery each() method. For each cell, we determine if it's a label (e.g. "SuperWidget") or a value (e.g. "1862.12") cell, based on whether it's in the left or right column. We then store the cell contents under the 'label' or 'value' key in an associative array, which we then place inside the chartData array.
  5. Store the slice index with each table cell, and assign a click handler to the cell
    While we're looping through the table cells, we store the current row index (and therefore slice index) in a key called 'slice' inside the jQuery object holding the table cell. We do this using the handy jQuery data() method. That way, we can easily find out which slice a cell refers to whenever the cell is clicked on. We also assign a click event handler function, handleTableClick(), to the cell, so that when the cell is clicked on we can animate the chart appropriately. We'll create this function in a moment.
  6. Extract the cell colour and store it in the chartColours array
    In the last part of the loop, we use jQuery to extract the colour of the cell by looking at its color CSS property. We then store its colour in the chartColors array, as a 3-element nested array containing the red, green, and blue values (in decimal).

    Most browsers return an element's colour in the format "rgb(r, g, b)". However, some browsers (*cough* IE *cough*) simply return the colour in whatever format it was specified in the CSS (e.g. "#RRGGBB"). So our code uses regular expressions to check for both scenarios.

  7. Compute and store the start and end angles of each slice
    We'll need to know the angles at which each pie slice starts and ends fairly often throughout the code, so we'll pre-compute them here and store them in 'startAngle' and 'endAngle' elements of the associative array inside chartData. We do this by looping through the slices, using currentPos to keep a record of the running total as a ratio of the grand total (between 0 and 1). We can then multiply this running total by 2π radians (a whole turn) to get the start and end angles for each slice.

    The slice angles stored in chartData run from 0 to 2π (3 o'clock to 3 o'clock). We'll need to offset these angles using the chartStartAngle variable when we actually draw the slices, so that the pie starts from 12 o'clock instead.

  8. Draw the chart and attach a click handler to the canvas
    Finally, our init() function calls a drawChart() function to draw the initial pie chart. (We'll create this function later.) It also assigns a click event handler function, handleChartClick(), to the canvas element, so that when the chart is clicked we can pull out or push in a slice as appropriate. We'll write this function next.

Step 6. Write a click handler for the pie chart

Clicking on the chart

We now need to write our handleChartClick() event handler function. This is called automatically whenever the user clicks inside the canvas element.

Here's the code for the function:


  /**
   * Process mouse clicks in the chart area.
   *
   * If a slice was clicked, toggle it in or out.
   * If the user clicked outside the pie, push any slices back in.
   *
   * @param Event The click event
   */

  function handleChartClick ( clickEvent ) {

    // Get the mouse cursor position at the time of the click, relative to the canvas
    var mouseX = clickEvent.pageX - this.offsetLeft;
    var mouseY = clickEvent.pageY - this.offsetTop;

    // Was the click inside the pie chart?
    var xFromCentre = mouseX - centreX;
    var yFromCentre = mouseY - centreY;
    var distanceFromCentre = Math.sqrt( Math.pow( Math.abs( xFromCentre ), 2 ) + Math.pow( Math.abs( yFromCentre ), 2 ) );

    if ( distanceFromCentre <= chartRadius ) {

      // Yes, the click was inside the chart.
      // Find the slice that was clicked by comparing angles relative to the chart centre.

      var clickAngle = Math.atan2( yFromCentre, xFromCentre ) - chartStartAngle;
      if ( clickAngle < 0 ) clickAngle = 2 * Math.PI + clickAngle;
                  
      for ( var slice in chartData ) {
        if ( clickAngle >= chartData[slice]['startAngle'] && clickAngle <= chartData[slice]['endAngle'] ) {

          // Slice found. Pull it out or push it in, as required.
          toggleSlice ( slice );
          return;
        }
      }
    }

    // User must have clicked outside the pie. Push any pulled-out slice back in.
    pushIn();
  }

As with all jQuery event handlers, handleChartClick() should accept a jQuery Event object as an argument. This Event object contains useful information about the click event, including the coordinates of the point that was clicked in the page.

Let's break this function down:

  1. Get the coordinates of the mouse click
    We can read the coordinates of the clicked point via the pageX and pageY properties of the Event object. However, these coordinates are relative to the page, so we need to subtract the coordinates of the top left corner of the canvas element to get the position relative to the canvas. Fortunately this is easy, since the this keyword in a click event handler refers to the clicked element (the canvas in this case). We can then read the element's top left corner position using offsetLeft and offsetTop.
  2. Determine if the click was inside the pie chart
    Now we know what part of the canvas was clicked, we need to find out if it was inside the radius of the pie chart. We use Pythagoras' theorem to work out the distance of the click from the centre of the pie chart. This distance is the hypotenuse of the right triangle formed between the x-axis position of the clicked point relative to the centre, the y-axis position of the clicked point relative to the centre, and the centre itself. If this distance is less than the chart radius then the click was inside the chart. Easy!
  3. Find the slice that was clicked
    Assuming the click was inside the pie chart, we then need to work out which slice was clicked. To do this, we first calculate the angle of the click relative to the chart's x axis using the atan2() function. We subtract chartStartAngle from this value to allow for the fact that the pie chart is shifted a quarter turn. Also, since atan2() can return negative angles, we add the value to 2π if it's negative, so that we're always working with positive angles between 0 and 2π. Now we can loop through the slices until we find the slice whose start and end angles are either side of our adjusted click angle. That's our clicked slice.
  4. Toggle the clicked slice
    Now that we know which slice was clicked, we call a function called toggleSlice(), passing in the slice index. This function starts pulling the slice out if it's currently in, or pushes it back in if it's currently out. (We'll write the toggleSlice() function in a moment.)
  5. If the user clicked outside the chart, push any pulled-out slice back in
    If the user clicked outside the chart then it would be nice if any pulled-out slice went back in, so at the end of our function — which is only reached if the user clicked outside the chart — we call a pushIn() function that pushes any pulled-out slice back in. (Again, we'll write the pushIn() function in a moment.)

Step 7. Write a click handler for the table

Our table click handler is actually called whenever the user clicks on a data cell in the table. Its job is to toggle the corresponding pie slice in or out. Here's the code:


  /**
   * Process mouse clicks in the table area.
   *
   * Retrieve the slice number from the jQuery data stored in the
   * clicked table cell, then toggle the slice
   *
   * @param Event The click event
   */

  function handleTableClick ( clickEvent ) {
    var slice = $(this).data('slice');
    toggleSlice ( slice );
  }

The table click handler is, thankfully, a lot simpler than the chart click handler! Remember how, in the init() function, we stored the slice index with each table cell using the jQuery data() method? This makes it really easy for us to determine which slice to toggle based on the clicked table cell, by simply calling $(this).data('slice').

Step 8. Write a function to toggle a slice in or out

This utility function is called from the 2 click handler functions we just made. Its job is to push in the given slice if it's pulled out, or pull it out if it's not:


  /**
   * Push a slice in or out.
   *
   * If it's already pulled out, push it in. Otherwise, pull it out.
   *
   * @param Number The slice index (between 0 and the number of slices - 1)
   */

  function toggleSlice ( slice ) {
    if ( slice == currentPullOutSlice ) {
      pushIn();
    } else {
      startPullOut ( slice );
    }
  }

As you can see, the function doesn't do the actual pushing or pulling; it defers these operations to the pushIn() and startPullOut() functions, which we'll write next.

Step 9. Create a function to pull a slice out

Pulling a slice out

When a slice is pulled out, we want to animate the pull-out effect. To do this, we'll need to use JavaScript's setInterval() function to draw each animation frame every few milliseconds.

This function, startPullOut(), sets up the animation. It uses setInterval() to call the animation function, animatePullOut(), and also highlights the corresponding data row in the table:


  /**
   * Start pulling a slice out from the pie.
   *
   * @param Number The slice index (between 0 and the number of slices - 1)
   */

  function startPullOut ( slice ) {

    // Exit if we're already pulling out this slice
    if ( currentPullOutSlice == slice ) return;

    // Record the slice that we're pulling out, clear any previous animation, then start the animation
    currentPullOutSlice = slice;
    currentPullOutDistance = 0;
    clearInterval( animationId );
    animationId = setInterval( function() { animatePullOut( slice ); }, pullOutFrameInterval );

    // Highlight the corresponding row in the key table
    $('#chartData td').removeClass('highlight');
    var labelCell = $('#chartData td:eq(' + (slice*2) + ')');
    var valueCell = $('#chartData td:eq(' + (slice*2+1) + ')');
    labelCell.addClass('highlight');
    valueCell.addClass('highlight');
  }

A few notes on this function:

  • The call to setInterval() passes in an anonymous function, which in turn calls animatePullOut( slice ). This is another example of a closure, since the anonymous function accesses a variable (slice) in the scope of the enclosing function. This ensures that setInterval() has access to the animatePullOut() function, as well as the value of slice.
  • We also pass the interval, in milliseconds, to setInterval(), using the variable pullOutFrameInterval. This causes setInterval() to call animatePullOut() every pullOutFrameInterval milliseconds.
  • setInterval() returns an in interval ID, which we store in the variable animationId. We can then call clearInterval() with this ID whenever we need to stop the animation.
  • To highlight the table row, we first remove the 'highlight' class from all cells in the table, then we use the jQuery :eq() selector to find the 2 cells in the target row, and add the 'highlight' class to these cells.

Step 10. Create a function to animate the pull-out effect

Now we need to write animatePullOut(), the function that animates each frame of the pull-out effect. This function is pretty simple, since it defers the actual drawing of the frame to the drawChart() function (which we'll create shortly):


  /**
   * Draw a frame of the pull-out animation.
   *
   * @param Number The index of the slice being pulled out
   */

  function animatePullOut ( slice ) {

    // Pull the slice out some more
    currentPullOutDistance += pullOutFrameStep;

    // If we've pulled it right out, stop animating
    if ( currentPullOutDistance >= maxPullOutDistance ) {
      clearInterval( animationId );
      return;
    }

    // Draw the frame
    drawChart();
  }

As you can see, this function simply adds the value of pullOutFrameStep to currentPullOutDistance to pull out the slice a few more pixels, then calls drawChart() to draw the frame. It also checks if the slice has been pulled out to its maximum distance (maxPullOutDistance). If it has, then the animation is stopped by calling clearInterval().

Step 11. Create a function to push slices back in

This function, pushIn(), is called by the handleChartClick() and toggleSlice() functions whenever we need to push any currently pulled-out slice back into the pie:


  /**
   * Push any pulled-out slice back in.
   *
   * Resets the animation variables and redraws the chart.
   * Also un-highlights all rows in the table.
   */

  function pushIn() {
    currentPullOutSlice = -1;
    currentPullOutDistance = 0;
    clearInterval( animationId );
    drawChart();
    $('#chartData td').removeClass('highlight');
  }

This function resets the currentPullOutSlice and currentPullOutDistance variables, clears any current animation by calling clearInterval(), redraws the chart to reflect the new conditions, and removes highlights from any cells in the table.

Step 12. Write a function to draw the chart

Drawing a slice

Now we need the function to actually draw the chart! This function, drawChart(), is actually pretty straightforward, since it leaves the drawing work to another function called drawSlice(), which we'll create next:


  /**
   * Draw the chart.
   *
   * Loop through each slice of the pie, and draw it.
   */

  function drawChart() {

    // Get a drawing context
    var context = canvas.getContext('2d');
        
    // Clear the canvas, ready for the new frame
    context.clearRect ( 0, 0, canvasWidth, canvasHeight );

    // Draw each slice of the chart, skipping the pull-out slice (if any)
    for ( var slice in chartData ) {
      if ( slice != currentPullOutSlice ) drawSlice( context, slice );
    }

    // If there's a pull-out slice in effect, draw it.
    // (We draw the pull-out slice last so its drop shadow doesn't get painted over.)
    if ( currentPullOutSlice != -1 ) drawSlice( context, currentPullOutSlice );
  }

Let's look at each step of the function:

  1. Get a drawing context
    In order to draw anything on a canvas element, you first need to get a drawing context. This is an object that exposes the various canvas drawing methods. To get the context, we call canvas.getContext(), passing in '2d' to indicate we want a 2D context. (3D canvases are on their way, but still future tech at the time of writing!)
  2. Clear the canvas
    Since we're likely to be drawing an animation frame, we first need to clear the canvas of any previous frame. There are a few different ways to wipe a canvas, but I've found that the clearRect() method produces the smoothest animation across a range of browsers. As you'd imagine, this clears a rectangular area of pixels specified by the top-left x and y coordinates of the rectangle, the width, and the height.
  3. Draw all slices except the pull-out slice
    Now we can loop through each slice in the chartData array and, skipping any pull-out slice for now, draw the slice by calling drawSlice(). We pass in the drawing context and the index of the slice to draw.
  4. Draw the pull-out slice
    Finally, we can draw the slice that's currently being pulled out (or has been pulled out). We check if there is a pull-out slice by looking at the value of currentPullOutSlice. If there is, then we call drawSlice() once again, this time passing in the index of the pull-out slice.

Why do we draw the pull-out slice last? Because the pull-out slice has a drop shadow. If we drew all slices in order then the slice after the pull-out slice would sometimes paint over part of the drop shadow. Which would look a bit uncool.

Step 13. Build a function to draw each slice in the chart

Now, at last, we come to the function that is, in many ways, the heart of the script. drawSlice() takes the drawing context that we retreived in drawChart(), as well as the index of the slice to draw, then draws the slice on the canvas:


  /**
   * Draw an individual slice in the chart.
   *
   * @param Context A canvas context to draw on  
   * @param Number The index of the slice to draw
   */

  function drawSlice ( context, slice ) {

    // Compute the adjusted start and end angles for the slice
    var startAngle = chartData[slice]['startAngle']  + chartStartAngle;
    var endAngle = chartData[slice]['endAngle']  + chartStartAngle;
      
    if ( slice == currentPullOutSlice ) {

      // We're pulling (or have pulled) this slice out.
      // Offset it from the pie centre, draw the text label,
      // and add a drop shadow.

      var midAngle = (startAngle + endAngle) / 2;
      var actualPullOutDistance = currentPullOutDistance * easeOut( currentPullOutDistance/maxPullOutDistance, .8 );
      startX = centreX + Math.cos(midAngle) * actualPullOutDistance;
      startY = centreY + Math.sin(midAngle) * actualPullOutDistance;
      context.fillStyle = 'rgb(' + chartColours[slice].join(',') + ')';
      context.textAlign = "center";
      context.font = pullOutLabelFont;
      context.fillText( chartData[slice]['label'], centreX + Math.cos(midAngle) * ( chartRadius + maxPullOutDistance + pullOutLabelPadding ), centreY + Math.sin(midAngle) * ( chartRadius + maxPullOutDistance + pullOutLabelPadding ) );
      context.font = pullOutValueFont;
      context.fillText( pullOutValuePrefix + chartData[slice]['value'] + " (" + ( parseInt( chartData[slice]['value'] / totalValue * 100 + .5 ) ) +  "%)", centreX + Math.cos(midAngle) * ( chartRadius + maxPullOutDistance + pullOutLabelPadding ), centreY + Math.sin(midAngle) * ( chartRadius + maxPullOutDistance + pullOutLabelPadding ) + 20 );
      context.shadowOffsetX = pullOutShadowOffsetX;
      context.shadowOffsetY = pullOutShadowOffsetY;
      context.shadowBlur = pullOutShadowBlur;

    } else {

      // This slice isn't pulled out, so draw it from the pie centre
      startX = centreX;
      startY = centreY;
    }

    // Set up the gradient fill for the slice
    var sliceGradient = context.createLinearGradient( 0, 0, canvasWidth*.75, canvasHeight*.75 );
    sliceGradient.addColorStop( 0, sliceGradientColour );
    sliceGradient.addColorStop( 1, 'rgb(' + chartColours[slice].join(',') + ')' );

    // Draw the slice
    context.beginPath();
    context.moveTo( startX, startY );
    context.arc( startX, startY, chartRadius, startAngle, endAngle, false );
    context.lineTo( startX, startY );
    context.closePath();
    context.fillStyle = sliceGradient;
    context.shadowColor = ( slice == currentPullOutSlice ) ? pullOutShadowColour : "rgba( 0, 0, 0, 0 )";
    context.fill();
    context.shadowColor = "rgba( 0, 0, 0, 0 )";

    // Style the slice border appropriately
    if ( slice == currentPullOutSlice ) {
      context.lineWidth = pullOutBorderWidth;
      context.strokeStyle = pullOutBorderStyle;
    } else {
      context.lineWidth = sliceBorderWidth;
      context.strokeStyle = sliceBorderStyle;
    }

    // Draw the slice border
    context.stroke();
  }

Let's take a deeeep breath, and work through each step of this function. (Actually, it's not really that bad, just a bit long!)

  1. Compute the adjusted slice angles
    Remember that the slice angles in the chartData array start from 0 radians (3 o'clock), but we want our chart to start from 12 o'clock. So we need to subtract a quarter turn from the slice angles before we use them to draw the slice. We do this using the chartStartAngle variable, storing the results in startAngle and endAngle.
  2. For a pull-out slice, offset the slice from the centre
    The next block of code checks to see if we're drawing a pull-out slice. If we are, then we need to offset the slice from the centre. To do this, we compute the angle of the middle of the slice (halfway between the start and end angle), and work out the number of pixels to pull the slice out by multiplying currentPullOutDistance by the result of the easeOut() function (more on this function in a moment). We can then use simple trigonometry — cosine and sine — to compute the new start position for the slice.
  3. For a pull-out slice, draw the label and set the drop shadow
    After computing the start position for the pull-out slice, we then draw the text label, which consists of the label from the table (e.g. "SuperWidget"), along with the value (e.g. "$1862.12") and the percentage of the total value. Most of this code should be self-explanatory. context.fillText() expects the text to draw, followed by the X and Y coordinates of the point to start the text. Again, we compute these X and Y values using cosine and sine, adding a bit of padding (pullOutLabelPadding) to the distance from the centre to allow plenty of room for the label.
  4. For a regular slice, draw the slice from the centre
    If the slice isn't a pull-out slice then we just set the slice's start coordinates to be the centre of the pie.
  5. Set up the gradient
    Our gradient fill adds a bit of extra pizazz to the chart. We use context.createLinearGradient() to create a gradient running from the top left of the canvas to three-quarters of the way across and down the canvas. Then we call addColorStop() on the gradient to add 2 colour stops: the gradient start colour at the start of the gradient ("#ddd", or light grey, in our settings), and the slice colour at the end of the gradient.
  6. Draw the slice
    To draw the segment, we first start a path by calling beginPath(). Then we move to the start point using moveTo(), then draw an arc using arc(). This function draws a straight line from the current position out to the start angle of the arc, then draws the arc to the end angle. We then complete the segment shape with another lineTo() back to the start, and finish the path with closePath(). Then we specify the gradient for the fill style, add the drop shadow if it's a pull-out slice, and call fill() to fill the slice.

    The last argument to arc()false — tells the function to draw the arc clockwise instead of anticlockwise.

    The shadow colours are specified using rgba format, where the last value is the opacity. So "rgba( 0, 0, 0, 0 )" draws the shadow with zero opacity, effectively turning off the shadow when it's not needed.

  7. Draw the slice border
    Finally, we draw a thin white border around the slice if it's a regular slice, or a thicker dark border if it's a pulled-out slice. We get the widths and colours from the variables we created in the init() function, and set the context.lineWidth and context.strokeStyle properties accordingly. We then draw the border by calling context.stroke().

Step 14. Create an easing function

Our very last function is called an easing function. It's called by drawSlice(), and its job is to adjust the position of the slice as it animates so that it appears to slow down towards the end of the animation:


  /**
   * Easing function.
   *
   * A bit hacky but it seems to work! (Note to self: Re-read my school maths books sometime)
   *
   * @param Number The ratio of the current distance travelled to the maximum distance
   * @param Number The power (higher numbers = more gradual easing)
   * @return Number The new ratio
   */

  function easeOut( ratio, power ) {
    return ( Math.pow ( 1 - ratio, power ) + 1 );
  }

This is a bit of a mishmash of various easing functions I found on the web, and frankly I'm not entirely sure how it works, but it does. :) The basic idea is that you pass it the ratio (how far along the path the slice currently is), and the power (how abruptly it should slow down). The function then returns the new ratio, adjusted so that the slice takes bigger steps at the start of the animation, and smaller steps towards the end.

There's some great info on easing functions over at ActionScript.org.

The end result

The end result
The finished pie chart with a slice pulled out. Nice!

Here's the demo again. I'm pretty pleased with the result: a pie chart that is both snazzy and useful! But does it work well across all browsers? Well, nearly. Here's how it runs on my somewhat long-in-the-tooth Core Duo iMac:

Firefox 4 Beta 2
Lovely and smooth. No problems here!
Opera 10.6
Ditto.
Safari 5 and Chrome 5 (and presumably other WebKit browsers)
Fairly smooth, but no drop shadow on the pulled-out slice. It seems that, with the current WebKit, you can have a gradient or a drop shadow on a canvas path, but not both, since the drop shadow appears if you turn off the gradient.
Mobile Safari, iPhone 3G, iOS 4.0.1
Pretty damn slow, but then so's the rest of my phone since I upgraded to iOS 4. (Thanks Apple!) I'm guessing it'd be acceptable on an iPhone 3GS or 4 - comments anyone?
Internet Explorer 8
A bit jittery as you'd expect, since the whole thing's being emulated through SVG. Slower than other browsers, and the labels jump around a bit as the slices are animated. The gradients are per-slice rather than global, and there are no drop shadows anywhere. Still, amazing it works at all really! And I expect running it on an old Mac through VMware doesn't help much either...
Internet Explorer 7
Same as IE8, except that it gets the point you click wrong, so you have to click to the left of the chart to pull out the segments. Bad IE7, no biscuit for you! It's probably caused by the margin: auto; on the container, or it may be a jQuery bug. I may look into this (or I may not — it is a dying browser, after all.)
Internet Explorer 6
I can't bring myself to look. :)

I hope you enjoyed this journey through the world of the HTML5 canvas element, and found it useful! I certainly learned a lot while writing the tutorial.

The code is CC-licensed so feel free to use it in your own projects. Have fun — and as always, please let me know your thoughts and suggestions in the comments below! :)

Follow Elated

Related articles

Responses to this article

20 most recent responses (oldest first):

07-Jul-11 17:36
Silly question here: We have a pie chart where the pie pieces are 2000, 60, and 3. The 3 pie piece shows up as "0%", and it's piece on the whole pie does not show any color because of this, making it appear to not be there. Like I said, silly question, but can we somehow get that pie piece's color to appear even as a sliver on the chart (we can see the "3" in the key on the side, we just want to see that bit of color in the pie itself)? Thanks!
12-Jul-11 02:29
@cross01: Here's a reworked chunk of the init() function. It's a bit of a hack but should give you a starting point!


// Now compute and store the start and end angles of each slice in the chart data

var currentPos = 0; // The current position of the slice in the pie (from 0 to 1)

for ( var slice in chartData ) {
var value = chartData[slice]['value'];

if ( value < (totalValue / 300) ) {
totalValue += ( totalValue / 300 );
value = totalValue / 300;
}

chartData[slice]['startAngle'] = 2 * Math.PI * currentPos;
chartData[slice]['endAngle'] = 2 * Math.PI * ( currentPos + ( value / totalValue ) );
currentPos += value / totalValue;
}
12-Jul-11 07:57
That seems to be exactly what we're looking for. Thank you again for your help!
04-Aug-11 12:59
Hi there,

The pie chart is not showing up in IE 8 for me. Just to make sure it wasn't my modified code, I uploaded the source file and still not showing up in IE 8.
Has the explorercanvas api changed?
09-Aug-11 04:42
@rexonms: Strange! It worked fine at the time I wrote the tutorial. Could be a problem with the version of excanvas and/or jQuery that the tutorial links to.

I can sometimes get it to work by reloading the page. It's intermittent.

You could try downloading the latest version of excanvas (r3) from http://code.google.com/p/explorercanvas/ and installing it locally. That might fix the problem. Also try linking to the latest version of jQuery (1.6.2).

Here are some other tips that might help:

http://stackoverflow.com/questions/941170/does-anyone-could-make-excanvas-work-in-ie-8-with-jquery
29-Aug-11 16:02
Matt
I love your pie charts and the tutorial(s). I've made some modification to your pie charts and in the end, I've got a funny thing happening that I thought maybe you could help me with. I've put your pie chart in an ASP.NET page, it works fine (using function pageLoad(sender, args) in the scripts so that it reloads after an AsyncPostBack update). Then I replaced your table with a <asp:Gridview> inside an UpdatePanel. What I want to do is change the table data, and thereby the pie chart on the page. And it works. Here's the problem, I've got ghosting. More than that, sometimes, when I click, I get the old pie chart back. But more often, which I click on a piece of the pie, I sometimes get a quick flicker of the previous pie chart, and the slice of pie that is moving is the old slice (in the flicker) and the current slice moves as well. And like I said, sometimes I get the swapping of the pie charts altogether. In fact, every click will toggle me between pie charts when it's in that state (might have to do with where the mouse is). It's all very cool if that was what I wanted, but it's not. Do you know what needs to happen to clear the other pie chart out of memory so the new pie chart will function without the ghost haunting us? I think that will fix it but I'm not sure where that should be done. Thank You.
S.
01-Sep-11 02:31
@seddleman: I'm afraid I know very little about ASP.NET so I'm not sure I can help much. I can put forward a couple of theories though:

1) Your table is somehow changing all the time, so that the data that my pie chart code reads is constantly changing as it draws the chart.

2) You've somehow ended up with 2 canvas elements on top of each other, and they're fighting over which one should be on top! (Check for this using your browser's DOM inspector.)

Also take a look at your JavaScript console for errors, and see if the problem is the same across all browsers.
01-Sep-11 08:01
Thanks Matt, for your reply. It's been an interesting working with your pie charts. The solution to my problem was to unbind all the #chart 'click's prior to defining them, that way each new pie chart gets a fresh start. It works very well, thanks for your post. I've learned a lot by manipulating and changing the charts.

seddleman
02-Sep-11 02:28
@seddleman: Glad you got it working
30-Nov-11 21:24
great tutorial! thanks for sharing.
05-Dec-11 08:45
What a useful tutorial! Thanks for posting...

I have one simple question:
I want to update the chart without refreshing the whole page. I am done with writing a jQuery code for updating the HTML table. [done it with the jQuery function: .load]
OK now I would like to know the next step to achieve my goal.
Could you help? Thanks Matt!
06-Dec-11 00:32
@jshamley: Thanks

@akos: Can you not just call init() to redraw the chart?
06-Dec-11 07:28
Hi Matt

Great piece of work and tutorial I'm looking to adapt the graph slightly. I want each piece of pie to have its own background-image attached to it. Is this easily done?

Ive been trying for a while with no luck

Any ideas?

Cheers

Skeddy
09-Dec-11 03:34
@skeddy: Tricky since the chart is done with canvas. You'd have to load the image into a JavaScript object, then (I think) place the image in the right position on the canvas using drawImage(), and clip the image to the shape of the slice somehow using clip().

drawImage():

https://developer.mozilla.org/en/Canvas_tutorial/Using_images

clip():

https://developer.mozilla.org/samples/canvas-tutorial/6_2_canvas_clipping.html

No idea if this would work but it's all I can think of right now!
09-Dec-11 06:24
Thanks Matt

Great article.

I need one pie chart after another on the same web page. The first table i gave an id of chartData as in the example but what would you call the second ? Is there any simple way to do this so they both work or does the code need a some re writing/enhancement.

Thanks

David
11-Dec-11 17:34
@dklugmann: The easiest, if hacky, approach would be to put 2 iframes in your page, and load a separate pie chart page into each iframe.

The more elegant, but long-winded approach would be to rewrite all the pie chart code to hold an array of charts, each with their own arrays of data and their own canvas elements.
12-Dec-11 08:24
Matt

Thanks. I appreciate the response and thanks for writing some great code in the first place.

David
17-Dec-11 03:23
You're welcome, David! I hope you manage to get it working.
22-Dec-11 10:53
Hi Matt

Did you ever find the answer why this doesnt should an image under ie8 ? Looks great in frefox but nothing in ie8.

Thanks

David
23-Dec-11 06:47
Matt

I think the ie7 and ie8 issues are a timing issue.

The line in the init function

if ( typeof canvas.getContext === 'undefined' ) return;

was giving undefined as the canvas element had not yet loaded.

I changed the call to pieChart() to be called via a body onload.

<body onload="pieChart();">

it then works in IE.

Hope that is of use to someone.

David

View all 38 responses »

Post a response

Want to add a comment, or ask a question about this article? Post a response.

To post responses you need to be a member. Not a member yet? Signing up is free, easy and only takes a minute. Sign up now.

Top of Page