Search

Matthew Knott

Previous: CSS3 Support in mobile safari
Next: Descendant Selectors Explained

Advanced Canvas Based Pie Chart

Posted on Tuesday, 01 April 2008 22:41

I'm in the process of developing a project management web app for the iPhone / iPod Touch, and I wanted to build a canvas based charting component to reflect time spent, resource use, etc.

I knew I could code bar graphs quite easily, but pie charts were a different matter, it'd been a few years since I'd needed PI for anything, so I had a look around for some direction.

There are a couple of ready made solutions out there, notably plotKit, which I really liked the look of, but it is dependent on another framework. I wanted some bare bones code so that I could build upon it easily, and most importantly, understand what was happening at each stage.

I found this example, which was perfect, a well written article with clearly written code. I quickly incorporated it into my application, but wanted it to be more like the plotKit example in appearance, with some additional visuals that was absent from both examples. Only a small thing, but I wanted a line from just inside each segment of the pie, leading to the label. Below is the result of my labours.

 

Screenshot of my pie chart solution

It was quite a job to achieve but I'm happy to share how I got there.

Firstly though, a disclaimer, I have not gone through tidying up the code yet this is a rough and ready block of javascript. Now that's out the way, on to the code. You should insert this into the for loop where the segments are built.

 

//Attempt to get label position

var sliceStart = Math.PI * (2 * sofar);

var sliceEnd = Math.PI * (2 * (sofar + thisvalue));

var angle = (sliceStart + sliceEnd)/2;

// normalize the angle

var normalisedAngle = angle;

 

if (normalisedAngle > Math.PI * 2){

normalisedAngle = normalisedAngle - Math.PI * 2;

}else if (normalisedAngle < 0){

normalisedAngle = normalisedAngle + Math.PI * 2;

}

 

What I've done is define the start and end position of the slice, please note I've removed the -0.5 from the calculation for the labels, otherwise they appear at the wrong positions. I then add the two values and devide by two to find the mid point of the arc.

 

var labelx = center[0] + Math.sin(normalisedAngle) * (radius + 10);

var labely = center[1] - Math.cos(normalisedAngle) * (radius + 10);

var labelBack = '';

var labelAlign = '';

 

Now that I have the correct angle, I use sine and cosine to determine the x,y pixel reference for the labels.

 

if (normalisedAngle <= Math.PI * 0.5) { //Top Left

labelx = (labelx - 10) + "px";

labely = labely + "px";

labelBack = 'none';

labelAlign = 'right';

}

else if ((normalisedAngle > Math.PI * 0.5) && (normalisedAngle <= Math.PI)) { //Bottom Right Quarter

labelx = (labelx + 15) + "px";

labely = labely + 10 + "px";

labelBack = 'none';

labelAlign = 'left';

}

else if ((normalisedAngle > Math.PI) && (normalisedAngle <= Math.PI*1.5)) { //Bottom Left Quarter

labelx = (labelx  - 30) + "px";

labely = (labely + 10 )+ "px";

labelBack = 'none';

labelAlign = 'right';

}

else { //Top Right

// text on top and align right

labelx = (labelx  - 20) + "px";

labely = (labely) + "px";

labelBack = 'none';

labelAlign = 'left';

}

 

Depending on which quarter of the pie chart the label falls into the is some adjusting to be done, see comments to understand which quarter is being addressed. I also included a variable for background colour and another one for text direction.

 

//Create Label

var percentage = thisvalue * 100;

var labelContainer = document.getElementById('label_container');

var oSpan = document.createElement('span');

var oLabelText = document.createTextNode(percentage.toFixed(2)+'%');

oSpan.setAttribute('style','left: '+labelx+'; top: '+labely+'; position: absolute; font-size: 60%; z-index: 100; background:'+labelBack+'; width: 45px; display: block; text-align: '+labelAlign+';');

oSpan.appendChild(oLabelText);

labelContainer.insertBefore(oSpan, labelContainer.lastChild);

//End of create label

 

Finally I output the labels to a div that surrounds my canvas tag, and hey presto, the tags are on the form. Note that I've positioned the elements absolute, therefore the enclosing div tag must be set to position: relative. So that's the labels out of the way, now I render the pie segment. Now, after that the relatively simple task of adding the indicator lines.

 

// Draw label line

var labelLineFromX = center[0] + Math.sin(normalisedAngle) * (radius * 0.9);

var labelLineFromY = center[1] - Math.cos(normalisedAngle) * (radius * 0.9);

var labelLineToX = center[0] + Math.sin(normalisedAngle) * (radius + 10);

var labelLineToY = center[1] - Math.cos(normalisedAngle) * (radius + 10);

 

ctx.beginPath();

ctx.shadowBlur       = 0;

ctx.moveTo(labelLineFromX,labelLineFromY);

ctx.lineTo(labelLineToX,labelLineToY);

ctx.lineWidth = 1;

ctx.strokeStyle = '#000';

ctx.stroke();

 

To get the start point, I go one tenth into the segment, and go 10 pixels outside of it, adjust the values to suit your needs.

 

That's it, simple looking back at it but took me a couple of days tinkering at it getting it the way I wanted it. Feel free to use this code as you will, and my sincerest thanks to the guy who came up with the original code, you saved me a lot of work, and this isn't meant as disrespect, merely a desire to customise and take your work further.

 

Comments

  • I love this code, posted by Matt

    I've gotta say, I really love the quality of the charts that can be achieved using the canvas tag, especially for the iPhone apps I'm developing. Please let me know if you've found this useful.

  • Example Required, posted by Muhammad Faizal

    could you give me an example file. I got counfused on reading your post... Thank you

Add a comment