Heatmap.js热力图实现原理

Heatmap.js是基于canvas开源的热力图框架,使用该框架可以方便的实现热力图,其效果图如下所示:

热力图.png

最近正好在学习canvas,顺便研究了一下Heatmap的源码,Heatmap的代码还是比较好理解的,代码结构清晰,有很多值得学习的,比如代码的架构,热力图渲染原理、着色等。下面来说下具体的实现过程。讲的不是很清楚,可以看下精简版的代码实现。

架构

Heatmap其架构主要由两部分组成,分别是RendererStore
Renderer:渲染器,主要用于热力图画布的创建,绘制、着色等。
Store:数据管理器,主要用于管理热力图的数据,包括添加数据、更新数据、删除数据、组装渲染器需要的数据格式等。

Heatmap对象

Heatmap对象主要用于构造一个外部调用的对象,将公共方法写在原型对象中,在构造函数中,创建对象的时候,初始化了一个_renderer对象和_store对象,分别用于渲染热力和管理数据。

// 核心代码,创建heatmap对象
var heatmapFactory = {
  create: function(config) {
    return new Heatmap(config);
  },
  register: function(pluginKey, plugin) {
    HeatmapConfig.plugins[pluginKey] = plugin;
  }
};
return heatmapFactory;
//默认属性
var HeatmapConfig = {
  defaultRadius: 40,//半径
  defaultRenderer: 'canvas2d',//默认为2D画布
  defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)"}, //热度默认颜色值,从外到里的颜色
  defaultMaxOpacity: 1,//最大透明度
  defaultMinOpacity: 0,//最小透明度
  defaultBlur: .85,//模糊度
  defaultXField: 'x',//x坐标字段名
  defaultYField: 'y',//主坐标字段名
  defaultValueField: 'value', //热度值字段名
  plugins: {}//插件
};

数据管理器

Store是一个数据管理器,用于管理热力图所需要的数据。主要方法如下所示:
_organiseData用于构造渲染器所需要的数据格式,主要用于查找出热力值的最大值和最小值,通过这两个值来决定各热力图的颜色渲染比例。其构造的每个对象如下所示:

          { 
            x: x, 
            y: y,
            value: value, 
            radius: radius,
            min: min,
            max: max 
          }

_unOrganizeData:用于还原_organiseData组装的数据,方便外部获取。其对象格式如下所示:

          {
            x: x,
            y: y,
            radius: radi[x][y],
            value: data[x][y]
          }

_onExtremaChange:min或者max有变化时通知_coordinator执行extremachange方法进行重新渲染。
addData:动态添加数据进行渲染。
setDataMax:设置最大值。
setDataMin:设置最小值。
getData:获取原始数据。

渲染器

Renderer是整个框架中的核心代码,通过该渲染器可以完美的创建出热力图。通过Canvas2dRenderer来创建画布和创建调色板等。

调色板

调色板是通过使用defaultGradient设置的颜色来创建一个256*1的画布,使用createLinearGradient创建渐变的图像,最后将返回一个Uint8ClampedArray类型的图片数据,该是一个包含RGBA像素信息的Uint8ClampedArray,数组中所有的值都是整数,范围是0~255。

//获取颜色调色板
  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);//绘制一个宽为256,高为1的矩形
    //返回一个Uint8ClampedArray类型的图片数据
    return paletteCtx.getImageData(0, 0, 256, 1).data;
  };

绘制热力图

热力图的绘制是使用_drawAlpha方法来完成的,通过获取到数据的x坐标和y坐标、半径等值来生成一个canvas,然后再更新渲染边界。

 //绘制热点图
    _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;
        // 如果point的值大于最大值,选用max作为绘制的值
        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];
        }
        // 设置透明度
        var templateAlpha = (value-min)/(max-min);
        // 小于0.01的图片将无法显示
        shadowCtx.globalAlpha = templateAlpha < .01 ? .01 : templateAlpha;

        //绘制图片
        shadowCtx.drawImage(tpl, rectX, rectY);

        // 更新渲染边界
        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;
          }

      }
    },

着色器

着色器算是渲染器中最核心的代码,热力图显示不同的颜色都是通过_colorize来完成后,其核心原理是获取画布的RGBA像素信息,再使用调色板的颜色对画布上的RGBA像素信息进行修改,就可以将热力图渲染出不同的颜色了。

_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;
        }
          debugger

        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];

    },

以上只是对代码的初步了解,要想完全了解期思想和原理,还需要花更多的时间来进行研究,至少要自己能快速的模仿一个出来才算真正的吃透了。
个人博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容