Winse Blog

走走停停, 熙熙攘攘, 忙忙碌碌, 不知何畏.

解读百度的Heatmap

前面通过Map的学习,了解到了瓦片的一些知识点。地图里面热图是一个比较典型的功能。通过对聚集数据不同颜色显示,直观形象的洞察数据的规律,比如说高危区等的热点分析,有点类似于arcgis的核密度。接下来结合百度里面的热图分析下它的实现。

1
2
3
4
5
6
7
8
9
var points =[
{"lng":116.418261,"lat":39.921984,"count":50},
...
]

//详细的参数,可以查看heatmap.js的文档 https://github.com/pa7/heatmap.js/blob/master/README.md
heatmapOverlay = new BMapLib.HeatmapOverlay({"radius":20});
map.addOverlay(heatmapOverlay);
heatmapOverlay.setDataSet({data:points,max:100});

setDataSet

把经纬度数据先转成界面的坐标(不在界面bounds内的点会被忽略掉),然后调用setData

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
HeatmapOverlay.prototype.setDataSet = function(data) {
    this.data = data;
    ...
    var currentBounds = this._map.getBounds();
    var mapdata = {
        max: data.max,
        data: []
    };
    var d = data.data,
        dlen = d.length;
        
    while (dlen--) {
        ...
        if (!currentBounds.containsPoint(latlng)) {
            continue;
        }            
        ...
        mapdata.data.push({
            x: point.x,
            y: point.y,
            count: d[dlen].count
        });
    }
    this.heatmap.setData(mapdata);
}

setData

计算最大最小,合并(对同一坐标的对应的count值求和),其中 _organiseData 根据坐标构建一个稀疏矩阵,最后emit给renderall

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
setData: function(data) {
  var dataPoints = data.data;
  var pointsLen = dataPoints.length;


  // reset data arrays
  this._data = [];
  this._radi = [];

  for(var i = 0; i < pointsLen; i++) {
    this._organiseData(dataPoints[i], false);
  }
  this._max = data.max;
  this._min = data.min || 0;
  
  this._onExtremaChange();
  this._coordinator.emit('renderall', this._getInternalData());
  return this;
},

_organiseData: function(dataPoint, forceRender) {
    var x = dataPoint[this._xField];
    var y = dataPoint[this._yField];
    var radi = this._radi;
    var store = this._data;
    var max = this._max;
    var min = this._min;
    var value = dataPoint[this._valueField] || 1;
    var radius = dataPoint.radius || this._cfgRadius || defaultRadius;
    
    ...
    
    if (!store[x][y]) {
      store[x][y] = value;
      radi[x][y] = radius;
    } else {
      store[x][y] += value;
    }
...

_getInternalData: function() {
  return { 
    max: this._max,
    min: this._min, 
    data: this._data,
    radi: this._radi 
  };
},

renderall 渲染

这个是重点,下面一个步骤一个步骤的讲。

1
2
3
4
5
6
renderAll: function(data) {
  // reset render boundaries
  this._clear();
  this._drawAlpha(_prepareData(data));
  this._colorize();
},

_prepareData

把上面合并数据创建的稀疏矩阵,再转回成对象 { x: ,y: ,value: , radius: } ,然后交给 _drawAlpha 进行画图。

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
  var _prepareData = function(data) {
    var renderData = [];
    var min = data.min;
    var max = data.max;
    var radi = data.radi;
    var data = data.data;
    
    var xValues = Object.keys(data);
    var xValuesLen = xValues.length;

    while(xValuesLen--) {
      var xValue = xValues[xValuesLen];
      var yValues = Object.keys(data[xValue]);
      var yValuesLen = yValues.length;
      while(yValuesLen--) {
        var yValue = yValues[yValuesLen];
        var value = data[xValue][yValue];
        var radius = radi[xValue][yValue];
        renderData.push({
          x: xValue,
          y: yValue,
          value: value,
          radius: radius
        });
      }
    }

    return {
      min: min,
      max: max,
      data: renderData
    };
  };

_drawAlpha

然后根据处理整合后的数据画alpha的圆(由于透明度可以进行叠加处理,shadowCtx.globalAlpha = (value-min)/(max-min); ),同时统计会有数据的最大边界rect。

特定半径的密度衰减圆通过 _getPointTemplate 获得,每个数据以其x,y的坐标为圆心,根据count的百分比叠加模板密度圆的透明度进行绘制。由于透明度的叠加,起到 被影响的点 密度相加的效果。

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
_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]
    shadowCtx.globalAlpha = (value-min)/(max-min);

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

  }
},

_colorize

最后根据rect的边界范围,然后结合palette的颜色条进行染色(palette 是一个 256 * 4(rgba) 的数组)。

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

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

},

最终绘制到canvas上,呈现热图效果。

–END

Comments