前端阅读室
heatmap.js (https://github.com/pa7/heatma...官网首页https://www.patrick-wied.at/s... 给出的代码示例:

var heatmap = h337.create({
  container: domElement
});

heatmap.setData({
  max: 5,
  data: [{ x: 10, y: 15, value: 5}, ...]
});

那么它是如何实现热力图绘制的呢?本文将为你全面解读heatmap.js源码。
<!-- more -->

热力图原理

点模板

点模板对应热力图数据点。它是一个圆点,根据可配置的模糊因子(blurFactor,默认.85),可使圆点带有模糊效果(借助createRadialGradient)。

var _getPointTemplate = function(radius, blurFactor) {
    var tplCanvas = document.createElement('canvas');
    var tplCtx = tplCanvas.getContext('2d');
    var x = radius;
    var y = radius;
    tplCanvas.width = tplCanvas.height = radius*2;

    if (blurFactor == 1) {
        tplCtx.beginPath();
        tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
        tplCtx.fillStyle = 'rgba(0,0,0,1)';
        tplCtx.fill();
    } else {
        var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius);
        gradient.addColorStop(0, 'rgba(0,0,0,1)');
        gradient.addColorStop(1, 'rgba(0,0,0,0)');
        tplCtx.fillStyle = gradient;
        tplCtx.fillRect(0, 0, 2*radius, 2*radius);
    }



    return tplCanvas;
};

灰度(透明度)叠加

这个热力图的"灵魂"。rgb通道是无法线性叠加呈现效果的,但是透明度是近似线性的。var templateAlpha = (value-min)/(max-min);,根据数据点的比率,对应于透明度的值alpha,我们在canvas上(shadowCtx)绘制一个数据点。它们的透明度是可以叠加的,值越大,越"不透明"。

_drawAlpha: function(data) {
    var min = this._min = data.min;
    var max = this._max = data.max;
    var data = data.data || [];
    var dataLen = data.length;
    // on a point basis?
    var blur = 1 - this._blur;

    while(dataLen--) {

        var point = data[dataLen];

        var x = point.x;
        var y = point.y;
        var radius = point.radius;
        // if value is bigger than max
        // use max as value
        var value = Math.min(point.value, max);
        var rectX = x - radius;
        var rectY = y - radius;
        var shadowCtx = this.shadowCtx;




        var tpl;
        if (!this._templates[radius]) {
            this._templates[radius] = tpl = _getPointTemplate(radius, blur);
        } else {
            tpl = this._templates[radius];
        }
        // value from minimum / value range
        // => [0, 1]
        var templateAlpha = (value-min)/(max-min);
        // this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
        shadowCtx.globalAlpha = templateAlpha < .01 ? .01 : templateAlpha;

        shadowCtx.drawImage(tpl, rectX, rectY);

        // update renderBoundaries
        if (rectX < this._renderBoundaries[0]) {
                this._renderBoundaries[0] = rectX;
            }
            if (rectY < this._renderBoundaries[1]) {
                this._renderBoundaries[1] = rectY;
            }
            if (rectX + 2*radius > this._renderBoundaries[2]) {
                this._renderBoundaries[2] = rectX + 2*radius;
            }
            if (rectY + 2*radius > this._renderBoundaries[3]) {
                this._renderBoundaries[3] = rectY + 2*radius;
            }

    }
},

线性色谱

通过createLinearGradient你可以自主定制自己的热力图色谱(config.gradient)。

var _getColorPalette = function(config) {
    var gradientConfig = config.gradient || config.defaultGradient;
    var paletteCanvas = document.createElement('canvas');
    var paletteCtx = paletteCanvas.getContext('2d');

    paletteCanvas.width = 256;
    paletteCanvas.height = 1;

    var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
    for (var key in gradientConfig) {
        gradient.addColorStop(key, gradientConfig[key]);
    }

    paletteCtx.fillStyle = gradient;
    paletteCtx.fillRect(0, 0, 256, 1);

    return paletteCtx.getImageData(0, 0, 256, 1).data;
};

着色

最后,透明度的叠加值(this.shadowCtx.getImageData)映射到线性色谱(palette),取线性色谱中的颜色为canvas上色(putImageData)就得到最终的热力图了。

_colorize: function() {
    var x = this._renderBoundaries[0];
    var y = this._renderBoundaries[1];
    var width = this._renderBoundaries[2] - x;
    var height = this._renderBoundaries[3] - y;
    var maxWidth = this._width;
    var maxHeight = this._height;
    var opacity = this._opacity;
    var maxOpacity = this._maxOpacity;
    var minOpacity = this._minOpacity;
    var useGradientOpacity = this._useGradientOpacity;

    if (x < 0) {
        x = 0;
    }
    if (y < 0) {
        y = 0;
    }
    if (x + width > maxWidth) {
        width = maxWidth - x;
    }
    if (y + height > maxHeight) {
        height = maxHeight - y;
    }

    var img = this.shadowCtx.getImageData(x, y, width, height);
    var imgData = img.data;
    var len = imgData.length;
    var palette = this._palette;


    for (var i = 3; i < len; i+= 4) {
        var alpha = imgData[i];
        var offset = alpha * 4;


        if (!offset) {
            continue;
        }

        var finalAlpha;
        if (opacity > 0) {
            finalAlpha = opacity;
        } else {
            if (alpha < maxOpacity) {
                if (alpha < minOpacity) {
                    finalAlpha = minOpacity;
                } else {
                    finalAlpha = alpha;
                }
            } else {
                finalAlpha = maxOpacity;
            }
        }

        imgData[i-3] = palette[offset];
        imgData[i-2] = palette[offset + 1];
        imgData[i-1] = palette[offset + 2];
        imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha;

    }

    img.data = imgData;
    this.ctx.putImageData(img, x, y);

    this._renderBoundaries = [1000, 1000, 0, 0];

},

其他

以上就是heatmap.js库最精华的部分了。当然,为了让库的设计更完备,它还做了很多其他工作。

Coordinator

自己实现了一个发布订阅模式来作为整个类库功能的调度者。

var Coordinator = (function CoordinatorClosure() {

function Coordinator() {
    this.cStore = {};
};

Coordinator.prototype = {
    on: function(evtName, callback, scope) {
    var cStore = this.cStore;

    if (!cStore[evtName]) {
        cStore[evtName] = [];
    }
    cStore[evtName].push((function(data) {
        return callback.call(scope, data);
    }));
    },
    emit: function(evtName, data) {
    var cStore = this.cStore;
    if (cStore[evtName]) {
        var len = cStore[evtName].length;
        for (var i=0; i<len; i++) {
        var callback = cStore[evtName][i];
        callback(data);
        }
    }
    }
};

return Coordinator;
})();

如你需要renderpartial、renderall,只需要emit就可以了。

coordinator.on('renderpartial', renderer.renderPartial, renderer);
coordinator.on('renderall', renderer.renderAll, renderer);

plugin

提供了插件接口,你可以使用heatmap.js提供的如gmaps-heatmap等各种插件。

if (config['plugin']) {
    var pluginToLoad = config['plugin'];
    if (!HeatmapConfig.plugins[pluginToLoad]) {
    throw new Error('Plugin \''+ pluginToLoad + '\' not found. Maybe it was not registered.');
    } else {
    var plugin = HeatmapConfig.plugins[pluginToLoad];
    // set plugin renderer and store
    this._renderer = new plugin.renderer(config);
    this._store = new plugin.store(config);
    }
}

Heatmap

实现了Heatmap构造方法,使用户可以通过heatmap实例调用各种功能。

Heatmap.prototype = {
    addData: function() {
    },
    removeData: function() {
    },
    setData: function() {
    },
    setDataMax: function() {
    },
    setDataMin: function() {
    },
    configure: function(config) {
    },
    repaint: function() {
    },
    getData: function() {
    },
    getDataURL: function() {
    },
    getValueAt: function(point) {
    }
};

return Heatmap;

Store

实现了自己的Store来统一管理热力图数据。

  Store.prototype = {
    // when forceRender = false -> called from setData, omits renderall event
    _organiseData: function(dataPoint, forceRender) {
    },
    _unOrganizeData: function() {
    },
    _onExtremaChange: function() {
    },
    addData: function() {
    },
    setData: function(data) {
    },
    removeData: function() {
      // TODO: implement
    },
    setDataMax: function(max) {
    },
    setDataMin: function(min) {
    },
    setCoordinator: function(coordinator) {
      this._coordinator = coordinator;
    },
    _getInternalData: function() {
      return { 
        max: this._max,
        min: this._min, 
        data: this._data,
        radi: this._radi 
      };
    },
    getData: function() {
      return this._unOrganizeData();
    }
  };

总结

以上,就是heatmap全部源码的内容(除各种plugins不一一介绍了),总体实现上其实并不复杂,但确实可以很方便地绘制热力图。基于它的原理,我们可以进行二次开发,实现定制的热力图。比如,我们一般都是通过xpath来保存无埋点数据的,而一般xpath元素并不是圆点,大部分时候它都是长方形元素。这时用圆点拟合就不太合适了,我们改造为椭圆点可以更好地拟合实际点击情况。
前端阅读室


小番茄
67 声望5 粉丝