HomeBlog
Drawing Heatmap with HTML Canvas - 羡辙杂俎
2015.06.12 21:45:37
Drawing Heatmap with HTML Canvas

This post introduces how to draw a heatmap chart using Canvas. By the end of the post, you can get the result as shown in the following image.

A heat map is a graphical representation of data where the individual values contained in a matrix are represented as colors. Fractal maps and tree maps both often use a similar system of color-coding to represent the values taken by a variable in a hierarchy. The term is also used to mean its thematic application as a choropleth map.

From Wikipedia

There are plenty of Heatmap JavaScript libraries. So…

Why Bother Making One?

Well, maybe you would just get curious about it one day, as I did.

Step 1. Preparing a Blurring Brush

The brush will be later used to generate the blending effect.

Create a Canvas

We create a new small Canvas of the brush size. Here, we set the brushSize to be 10, which is the radius of the distict brush size, while brushBlurSize is the extra radius of blurring area.

var brushCanvas = document.createElement('canvas');
var brushSize = 10;
var brushBlurSize = 10;

// set brush size

var r = brushSize + brushBlurSize;
var d = r * 2;
brushCanvas.width = d;
brushCanvas.height = d;

In order to render shadow without the distinct circle, we draw the distinct circle in an invisible place, which is to the left of the canvas in our case. And then, use shadowOffset to draw shadow in the center of the canvas. The circle is drawn in black, and is blended with other brushes using alpha value and shadow blur.

var ctx = brushCanvas.getContext('2d');
ctx.shadowOffsetX = d;
ctx.shadowBlur = brushBlurSize;
ctx.shadowColor = 'black';

// draw circle in the left to the canvas

ctx.beginPath();
ctx.arc(-r, r, ecConfig.heatmap.brushSize, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();

The brush is shaped as the image in the right shows.

Step 2. Drawing with The Brush

We can generate some random data to see how well our brush works, and may further changing the size of the brush to get a better effect.

var data = [];
for (var i = 0; i < 1000; ++i) {
    data.push([Math.random() * 400, Math.random() * 300, Math.random()]);
}

And use data to draw brushCanvas to a new canvas, we can get the following gray image.

var len = data.length;
for (var i = 0; i < len; ++i) {
    var p = data[i];
    var x = p[0];
    var y = p[1];
    var alpha = p[2]; // using value as alpha


    // draw with the circle brush with alpha

    ctx.globalAlpha = alpha;
    ctx.drawImage(brush, x - r, y - r);
}

Where is my color?!

Hold on…

Step 3. Preparing a Gradient Map

In order to may different darkness into different color in our heatmap, we need a gradient map as a dictionary. Then, we use the alpha value to determine which color to use in the dictionary.

We create a Canvas of 1 x 256 size, in which 256 is the gradient level.

var levels = 256;
var canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = levels;
var ctx = canvas.getContext('2d');

var gradientColors = {
    0.4: 'blue',
    0.5: 'cyan',
    0.6: 'lime',
    0.8: 'yellow',
    1.0: 'red'
};

// add color to gradient stops

var gradient = ctx.createLinearGradient(0, 0, 0, levels);
for (var pos in ecConfig.heatmap.gradientColors) {
    gradient.addColorStop(pos, gradientColors[pos]);
}

ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1, levels);
var gradientPixels = ctx.getImageData(0, 0, 1, levels).data;

gradientPixels is the pixel array of the gradient canvas. Since each pixel contains red, green, blue, and alpha channel, the length of gradientPixels is 256 * 4, which is 1024.

In order to visualize the gradient map, we set the width a bit wider, as shown in the right image. But remember in gradient, it’s only of 1 pixel wide.

Step 4. Colorize using Gradient Map

Finally!! :joy:

The canvas is colorized by mapping alpha value to the gradient map.

var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var pixels = imageData.data;
var len = pixels.length / 4;
while(len--) {
    var id = len * 4 + 3;
    var alpha = pixels[id] / 256; // why not `gradientLevels`?

    var colorOffset = Math.floor(alpha * (gradientLevels - 1));
    pixels[id - 3] = gradient[colorOffset * 4];     // red

    pixels[id - 2] = gradient[colorOffset * 4 + 1]; // green

    pixels[id - 1] = gradient[colorOffset * 4 + 2]; // blue

}
ctx.putImageData(imageData, 0, 0);

To make the result nicer, we modify our random data generating method.

var data = [];
for (var i = 0; i < 1000; ++i) {
    data.push([Math.random() * 400, Math.random() * 300, Math.random()]);
}
for (var i = 0; i < 100; ++i) {
    data.push([Math.random() * 20 + i / 2 + 100, 
        Math.random() * 20 + 200, Math.random()]);
}
for (var i = 0; i < 100; ++i) {
    data.push([Math.random() * 20 + i / 2 + 300, 
        Math.random() * 20 - i / 3 + 200, Math.random()]);
}

And now, the heat map is drawn to your Canvas!

Reference

In fact, I read about 3 to 4 Heatmap Chart libraries and most of them are rendered in this way. simpleheat is a small but powerful implementation among them.

博文对你有帮助吗?如果有的话,要不要送我一本书呢?:heart_eyes:

微信公众号“羡辙部落格”(xianzheblog)提供本站优质非技术类博文推送。

Creative Commons License
Every blog post in this site by Wenli Zhang is licensed under a CC 3.0 License, based on a work at Github. You can see the source code of this blog site at github.com/Ovilia/blog.

Comments

comments powered by Disqus