Stretching the (HTML 5) Canvas: Fixing Aspect Ratio Problems

The Problem

I’m working in a web part that uses the HTML 5 <canvas> element, and I ran into a little problem. The web part is a “microsurvey” that asks a single question and, when the user clicks an answer, displays the results of the survey so far. For the results, I wanted to show a bar chart, and I thought it would be a fun opportunity to use the new HTML 5 canvas. The problem is that my chart was distorted; as you can see the text is too wide and looks like it came off an old dot matrix printer. Somebody stretched the canvas!

3-9-2015 6-28-46 PM


The problem was caused by the fact that my web part uses css to size the canvas, and that’s important since I want it to conform to the user-selected width of the web part. It turns out that the canvas has two sets of dimensions: the dimensions of the inner drawing surface (height and width) and the dimensions of the rendered element (clientHeight and clientWidth). When my css set the canvas to 100% of its container, it made the rendered size a different aspect ratio than the canvas’ inner drawing surface. My code scaled the bars correctly, but the text was stretched badly.

The Solution

I love it when there’s a one-statement fix, and here it is:

canvas.width = canvas.height * 
    (canvas.clientWidth / canvas.clientHeight);

All this does is change the width of the canvas’ inner drawing surface so it’s the same aspect ratio as the canvas element on the page. Here’s the result:

3-9-2015 6-29-49 PM

While I’m no artist, this is what I intended, and the text looks crisp and well proportioned. Note that this wouldn’t be necessary if I simply used the height and width attributes on the canvas element, but as I said I didn’t want to do that in this case.

In case you’re interested, here is the code to draw the little graph. This is implemented as an Angular directive, but the same technique could work without Angular.


(function () {

    'use strict';

    angular
      .module('microSurvey', [])
      .directive('bgShowData', function bgShowDataDirective() {
          return {

              restrict: 'A',

              link: function link(scope, element, attrs) {

                  // Get the canvas DOM element
                  var canvas = element[0];
                  var context = canvas.getContext('2d');

                  // Scale the inner drawling surface to the same
                  // aspect ratio as the canvas element
                  canvas.width = canvas.height * 
                      (canvas.clientWidth / canvas.clientHeight);

                  // We're drawing a horizontal bar chart
                  // Calculate the height of each bar 
                  var barHeight = 
                      Math.ceil(canvas.height / scope.answerChoices.length);
                  var barTop = 0;

                  // For each bar in the chart
                  for (var a in scope.answerChoices) {

                      // Draw a rectangle for the bar
                      context.beginPath();
                      context.rect(0, barTop,
                          (scope.answerChoices[a].percent / 100) *
                           canvas.width, barHeight);
                      context.strokeStyle = 'lightGray';
                      context.fillStyle = 'white';
                      context.fill();

                      // Draw a rectangle the width of the canvas as a border
                      // for each line
                      context.beginPath();
                      context.rect(0, barTop, canvas.width, barHeight);
                      context.strokeStyle = 'darkGray';
                      barTop = barTop + barHeight;
                      context.stroke();

                      // Add text with the answer and percentage
                      context.beginPath();
                      context.font = '14pt arial';
                      context.fillStyle = 'black';
                      context.textAlign = 'left';
                      context.fillText(scope.answerChoices[a].text + ' - ' +
                          Math.round(scope.answerChoices[a].percent) + '%',
                          10, barTop - 10);

                  } // for loop

              } // returned object

          } // function link()

      }) // End bg-show-data directive

})();

The canvas element is a lot of fun; I hope you enjoy using it as much as I do!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s