1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/**
 * @file defines echarts Heatmap Chart
 * @author Ovilia (me@zhangwenli.com)
 * Inspired by https://github.com/mourner/simpleheat
 *
 * @module
 */
define(function (require) {
    var defaultOptions = {
        blurSize: 30,
 
        // gradientColors is either shaped of ['blue', 'cyan', 'lime', 'yellow', 'red']
        // or [{
        //    offset: 0.2,
        //    color: 'blue'
        // }, {
        //    offset 0.8,
        //    color: 'cyan'
        // }]
        gradientColors: ['blue', 'cyan', 'lime', 'yellow', 'red'],
        minAlpha: 0.05,
        valueScale: 1,
        opacity: 1
    };
 
    var BRUSH_SIZE = 20;
    var GRADIENT_LEVELS = 256;
 
    /**
     * Heatmap Chart
     *
     * @class
     * @param {Object} opt options
     */
    function Heatmap(opt) {
        this.option = opt;
        if (opt) {
            for (var i in defaultOptions) {
                if (opt[i] !== undefined) {
                    this.option[i] = opt[i];
                } else {
                    this.option[i] = defaultOptions[i];
                }
            }
        } else {
            this.option = defaultOptions;
        }
    }
 
    Heatmap.prototype = {
        /**
         * Renders Heatmap and returns the rendered canvas
         * @param {Array} [x, y, value] array of data
         * @param {number} canvas width
         * @param {number} canvas height
         * @return {Object} rendered canvas
         */
        getCanvas: function(data, width, height) {
            var brush = this._getBrush();
            var gradient = this._getGradient();
            var r = BRUSH_SIZE + this.option.blurSize;
 
            var canvas = document.createElement('canvas');
            canvas.width = width;
            canvas.height = height;
            var ctx = canvas.getContext('2d');
 
            var len = data.length;
            for (var i = 0; i < len; ++i) {
                var p = data[i];
                var x = p[0];
                var y = p[1];
                var value = p[2];
 
                // calculate alpha using value
                var alpha = Math.min(1, Math.max(value * this.option.valueScale
                    || this.option.minAlpha, this.option.minAlpha));
 
                // draw with the circle brush with alpha
                ctx.globalAlpha = alpha;
                ctx.drawImage(brush, x - r, y - r);
            }
 
            // colorize the canvas using alpha value and set with gradient
            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;
                var colorOffset = Math.floor(alpha * (GRADIENT_LEVELS - 1));
                pixels[id - 3] = gradient[colorOffset * 4];     // red
                pixels[id - 2] = gradient[colorOffset * 4 + 1]; // green
                pixels[id - 1] = gradient[colorOffset * 4 + 2]; // blue
                pixels[id] *= this.option.opacity;              // alpha
            }
            ctx.putImageData(imageData, 0, 0);
 
            return canvas;
        },
 
        /**
         * get canvas of a black circle brush used for canvas to draw later
         * @private
         * @returns {Object} circle brush canvas
         */
        _getBrush: function() {
            if (!this._brushCanvas) {
                this._brushCanvas = document.createElement('canvas');
 
                // set brush size
                var r = BRUSH_SIZE + this.option.blurSize;
                var d = r * 2;
                this._brushCanvas.width = d;
                this._brushCanvas.height = d;
 
                var ctx = this._brushCanvas.getContext('2d');
 
                // in order to render shadow without the distinct circle,
                // draw the distinct circle in an invisible place,
                // and use shadowOffset to draw shadow in the center of the canvas
                ctx.shadowOffsetX = d;
                ctx.shadowBlur = this.option.blurSize;
                // draw the shadow in black, and use alpha and shadow blur to generate
                // color in color map
                ctx.shadowColor = 'black';
 
                // draw circle in the left to the canvas
                ctx.beginPath();
                ctx.arc(-r, r, BRUSH_SIZE, 0, Math.PI * 2, true);
                ctx.closePath();
                ctx.fill();
            }
            return this._brushCanvas;
        },
 
        /**
         * get gradient color map
         * @private
         * @returns {array} gradient color pixels
         */
        _getGradient: function() {
            if (!this._gradientPixels) {
                var levels = GRADIENT_LEVELS;
                var canvas = document.createElement('canvas');
                canvas.width = 1;
                canvas.height = levels;
                var ctx = canvas.getContext('2d');
                
                // add color to gradient stops
                var gradient = ctx.createLinearGradient(0, 0, 0, levels);
                var len = this.option.gradientColors.length;
                for (var i = 0; i < len; ++i) {
                    if (typeof this.option.gradientColors[i] === 'string') {
                        gradient.addColorStop((i + 1) / len,
                            this.option.gradientColors[i]);
                    } else {
                        gradient.addColorStop(this.option.gradientColors[i].offset,
                            this.option.gradientColors[i].color);
                    }
                }
 
                ctx.fillStyle = gradient;
                ctx.fillRect(0, 0, 1, levels);
                this._gradientPixels = ctx.getImageData(0, 0, 1, levels).data;
            }
            return this._gradientPixels;
        }
    };
 
    return Heatmap;
});