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):

16-Jul-12 06:12
Hi there, I would like to display the chart with data that are extract from ms access. Is it possible to do it in here?
24-Jul-12 13:21
I love your chart, thank you very much for putting it together. I am wondering if there is a way to change the spacing between the label and the value?

Thank you,
April
27-Jul-12 02:29
@april_r: In the table, or the chart?
27-Jul-12 09:43
@matt

sorry, in the chart.
27-Jul-12 23:42
@april_r: I believe the line you're looking for is in the drawSlice method:


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 );


The "+ 20" at the end of the line is what you'll want to change to increase or decrease the spacing between the label and value texts. Alternatively, I've put a derivative of Matt's work up on my website if you want a JavaScript class with getter/setter methods for this value. If you want to give that a shot just PM me and I can send the link to you.

@matt: I just looked over the compatibility chart again, and your note about needing to click to the left of the chart in IE7 sounded similar to or possibly the same as a problem I'd had with it as well. To fix it, I just used jQuery's .offset() method to get the position of the canvas instead of calculating it myself. That gave me the correct offset in all the browsers I tried. Happy days.
30-Jul-12 10:53
@grapoza
Thank you, that is what I needed.
28-Sep-12 00:36
This chart is amazing.. i need to add another columns to the table , however when i added another 2 columns , the chart does not work. Appreciate your help on this..
07-Dec-12 08:04
The chart is very nice. But, i have some issues and need suggestion from you. Actually in my page, the click event on chart does not work. I think the problem is with div element of the chart. Because if i click outside of chart in the canvas, the toggleslice is called but when i click on chart it does not work. Any suggestions? From the table chart handler works perfectly fine. If you want i can give you my page. Also, I need to display 3 charts on a same page. So do i have to write 3 different init functions or is there any one function that works?
18-Dec-12 19:02
@sampada: Can you post the URL of your chart page so we can see the problem?

I answered your second question here: http://www.elated.com/forums/topic/5000/#post21904
19-Dec-12 04:11
var mouseX = clickEvent.pageX - this.offsetLeft;
var mouseY = clickEvent.pageY - this.offsetTop;

The code above will not work if the canvas is inside a <td> for example.
So i suggest the following modification :

var p = $('#' + name);
var offset = p.offset();
var mouseX = clickEvent.pageX - offset.left
var mouseY = clickEvent.pageY - offset.top
23-Jan-13 07:48
Hey Matt,

Thanks so much for the article and great sample code. I want to make sure I give credit where credit is do. Can you provide the copyright attribution text you'd like me to use in my about page?

Thanks,

Missing
27-Jan-13 16:53
Matt!
Amazing job, thank you very much!

I managed to understand most of the code and to implement it.
A question please, if i want to replace the text about each slice with a picture, how should i do it?

Again, Great job! thank you!

Meschiany
10-Feb-13 21:47
@missing: Just something like "pie chart code courtesy of Matt Doyle" (with a link to the article) is fine!

@meschiany: In drawSlice() you'd need to replace the calls to fillText() with a call to drawImage(): http://www.html5canvastutorials.com/tutorials/html5-canvas-images/
12-Mar-13 16:23
Thank you for this tutorial. I have a question regarding the colors. My chart is created dynamically from my DB. The problem i am having is I can not dynamically change the <tr style="color: #>?
I tried doing it via javascript but it didn't work. Is there something I can put into the var chartColours = []; instead of it creating from the HTML
10-Apr-13 16:54
Matt,

Thank you for sharing! This chart is fantastic, and exactly what I was looking for in my current project.

I just have one question... is there anyway to currently control the opacity of the slices (pulled out and inactive) ? What I need to do is essentially add opacity to the un-selected layers so that the selected slice is focused on by its opacity in comparison to the others. (my product manager would rather do that than toggle each slice).

jffgrdnr
30-Apr-13 00:01
@vinsite54: I'd need to see more context to answer your question. How are you pulling the data from the DB? If you're using Ajax then it should be fairly easy to use JS to change the tr elements as well as the chartColours array.

@jffgrdnr: Glad it helped! You should be able to just replace the rgb() notation in drawSlice() with rgba(), where 'a' is the alpha (opacity) value you want to use. 'a' ranges from 0 (completely transparent) to 1 (completely opaque).
30-Apr-13 03:46
The pie chart works awesomely, however it clash with Shareaholic scripts.
When Shareaholic script is installed, the piechart will have problem drawing the pullout slice, it will just show blank for that slice.
Using Chrome Developer tools, the error was
[i]"Uncaught TypeError: Object function (){return i} has no method 'join' "
[/i]

After diving into the script, I made a one line change in function drawChart().

I change from

for ( var slice in chartData ) {

to

for ( var slice=0; slice < chartData.length; slice++ ) {


Hopefully this will help other who face the same problem.

[Edited by shanai on 30-Apr-13 08:52]
21-May-13 11:01
HELLO thanks for this lovely article, my question is i want to display the info (in this case widget ...) into one place for example in a little zone just under the pie .

any help please would much appreciated
19-Feb-14 01:03
Hi Matt,

Great Work!!! This is what I am looking for my current project.

I wonder how to change the dollar sign into other currency symbol when I pull up the slice by clicking one of the rows in the table?
02-Mar-14 11:36
Hello

For obtuse909 :
- ligne 99


var pullOutValuePrefix = "€ "; // Pull-out slice value prefix



For my part, to make your interactive graph:
- I wish I could feed the digital values of those four variables calculated following the seizure of the user.

But I do not know
- Or the syntax to be used
- Or the place where he must place the mini script

The only thing I understood was that we must attribute "id" to <td>

Thank you in advance and bravos



PS : Traduction Google

View all 80 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