Experiments with ExCanvas: Pie Charts

November 2008

Mmmmm. Pie.

ExCanvas is Google's answer to the problem of the <canvas> tag not being natively understood by the market hogging Internet Explorer browser. This element allows us to "draw" directly within it, creating vector graphics (including animations) on the fly.

If you can see two pie charts below then so far, so good. If they look like pie charts, even better! This means the <canvas> tag has done it's job, albeit assisted by ExCanvas if you're viewing this in IE. Sadly, on my laptop, IE7 rendered the "donut" chart (the one on the right side of the two) sans donut until I coded around the problem with some hackery. I had to apply even uglier hackery to make it render nicely.

While I could solve the above problem quickly, this required nastiness that really annoyed me; why can nothing work across the board as it is designed to? I'm sure I'll find more of these shenanigans as I play further, but regardless, I think ExCanvas is going to be the way to go in future when it comes to drawing graphs and charts for my employers by day, and particularly - in the spirit of open source - for my JAWStats web statistics project I tinker with by night.

P.S. Everytime I think of the word donut, I laugh about this skit. Thank you, Ammon & Anne, for introducing it to me. :)

For completeness, here is my pie chart function:

function PieChart(iMiddleX, iMiddleY, iRadius,
                  iExplodeDistance, bShowShadow, iDonutRadius, aData) {
    // draw shadow
    if (bShowShadow == true) {
        var iStartAngle = 0;
        var iShadowOffset = (iRadius / 45);
        for (var i = 0; i < aData.length; i++) {
            iStartAngle = DrawSlice(oColors.shadow,
                                    iMiddleX + iShadowOffset,
                                    iMiddleY + iShadowOffset,
                                    (iRadius * 1), iExplodeDistance,
                                    iStartAngle, aData[i]);
        }
    }

    // draw slices
    var iStartAngle = 0;
    for (var i = 0; i < aData.length; i++) {
        iStartAngle = DrawSlice(oColors.colors[i],
                                iMiddleX, iMiddleY, iRadius,
                                iExplodeDistance, iStartAngle, aData[i]);
    }

    // draw donut
    if (iDonutRadius > 0) {
        // 4 half circles? IE hackery :(
        oCTX.globalCompositeOperation = "destination-out";
        iStartAngle = DrawSlice(oColors.shadow,
                                iMiddleX, iMiddleY,
                                iDonutRadius, 0, 0, 50);
        iStartAngle = DrawSlice(oColors.shadow,
                                iMiddleX, iMiddleY,
                                iDonutRadius, 0, iStartAngle, 50);
        iStartAngle = DrawSlice(oColors.shadow,
                                iMiddleX, iMiddleY,
                                iDonutRadius, 0, (iStartAngle + 5), 50);
        iStartAngle = DrawSlice(oColors.shadow,
                                iMiddleX, iMiddleY,
                                iDonutRadius, 0, (iStartAngle + 5), 50);
    }

    // slice drawing
    function DrawSlice(sColor, iMiddleX, iMiddleY, iRadius,
                       iExplodeDistance, iStartAngle, iPercentage) {
        // calculate angles
        var iEndAngle = ((iPercentage * 3.6) + iStartAngle);
        var iRadiansFrom = DegreesToRadians(iStartAngle);
        var iRadiansTo   = DegreesToRadians(iEndAngle);
        var iRadiansMid  = (((iRadiansTo - iRadiansFrom) / 2) + iRadiansFrom);

        // calculate explode
        var iExplodeModify = (iExplodeDistance * (1 - (iPercentage / 100)));
        iMiddleX += (Math.cos(iRadiansMid) * iExplodeModify);
        iMiddleY += (Math.sin(iRadiansMid) * iExplodeModify);

        // draw
        oCTX.fillStyle = sColor;
        oCTX.beginPath();
        oCTX.moveTo(iMiddleX, iMiddleY);
        oCTX.arc(iMiddleX, iMiddleY, iRadius, iRadiansFrom, iRadiansTo, false);
        oCTX.fill();

        // return end point angle
        return iEndAngle;
    }
}

function DegreesToRadians(iDegrees) {
    return (Math.PI / 180) * (iDegrees - 90);
}

Above is a cobbled together proof of concept function, written quickly. View Source to get the overall gist, because if you want to use it yourself, you'll need to break out the oCTX reference if you have multiple graphs.

Jon Combe