实现一个地面热力图

564 阅读5分钟

hello 大家好,🙎🏻‍♀️🙋🏻‍♀️🙆🏻‍♀️

我是一个热爱知识传递,正在学习写作的作者,ClyingDeng 凳凳!

需求千千万,没遇到的依旧是千千万。

需求场景

产品:见过这样的热力图没? image.png

我:见过!

产品:它是不是把那个文字遮住了 我:是的

产品:那让这个热力图不要遮住文字不然太影响美观了😁😁😁

我:emmm,需求很明确。。。就是觉得那里不太对劲🤔🤔🤔

产品一句话,那就开整!

技术方案

深入分析需求中🧐...

产品想要的是一个不被文字遮住的地图热力图展示功能。

在此案例中,选取的是百度地图作为demo,考虑可以适配其他腾讯、goole等其他地图,将热力图单独抽离,绘制成一个canvas,再通过地图的地面图层 API进行绘制添加。

Yes,这是完全可行的方案。

热力图原理

经调研热力图相关资料之后,发现热力图的实现主要有这四部曲:

  • 绘制灰度点,使其径向渐变

  • 针对不同点的数值,设置对应点的灰度值

  • 准备一条标准的canvas彩带,根据不同灰度值的点,取色

  • 将带有颜色的点画到最终的canvas上

热力图实现

相关功能划分

想要实现通用的热力图,我们就需要准备一个独立的 heatMap 对象。通过 heatMap 实例来调用其设置、移除、删除数据等方法。

image.png

这个 heatMap 对象,我们需要有一个可以设置数据的的方法,在提供数据后,还需要去渲染绘制canvas。这样内部实现我们可以通过发布订阅模式,在new heatMap时,对渲染对象进行订阅。在调用setData设置数据时,触发对应的render功能。

heatMap

let HeatMapCanvas = (() => {
  function HeatMapCanvas(config) {
    this._coordinator = new EventEmitter() // 发布订阅
    this.container = config.container // 容器
    this.config = mergeConfig(defaultConfig, config) // 配置整合

    this._render = new canvasRender(this.config)
    // 订阅renderAll
    this._coordinator.on('renderAll', this._render.renderAll, this._render) // 使用时可以获取到当前scope指向的具体的this
  }
  HeatMapCanvas.prototype = {
    setData(data) {
      // 触发
      this._coordinator.emit('renderAll', data)
    }
  }
  return HeatMapCanvas
})()
;(() => {
  window.HeatMap = HeatMapCanvas
})()

canvasRender

渲染绘制对象:

let canvasRender = (function Canvas2dRendererClosure(config) {
  function canvasRender(config) { // 接受相关宽高、透明度、radius等
  this.config = config
  // 最后生成的canvas内容
  this.canvas = document.createElement('canvas')
  this.ctx = this.canvas.getContext('2d')
  this.canvas.className = 'heatmap-canvas'
  // 初始点状 canvas 灰度点
  this.shadowCanvas = document.createElement('canvas')
  this.shadowCtx = this.shadowCanvas.getContext('2d')
  this.shadowCanvas.className = 'shadow-canvas'
   ...
  },
  canvasRender.prototype = {
  // 绘制相关方法,画点,设置模糊度...
    ...
  },
  return canvasRender
})()

heatMap使用

this.heatmapCanvas = new window.HeatMapCanvas({
     container: document.getElementById('container'),
     radius: 20,
     blur: 1,
     // 数据点中 x 坐标的属性名称
     xField: 'lng',
     // 数据点中 y 坐标的属性名称
     yField: 'lat',
     // 数据点中 y 坐标的属性名称
     valueField: 'count'
   })
 // 设置热力图数据
 this.heatmapCanvas.setData({
  min: 0,
  max: 100,
  data: this.points
})
this.canvas = this.heatmapCanvas._render.canvas // 带颜色的canvas

画点

接收到数据后,需要渲染遍历每个数据,将每个数据进行绘制。

对于每个点我们可以通过创建一个canvas,对其进行点的绘制。设置绘制的大小、渐变点。

  let _getPointTemplate = (radius, blur, pointVal, min, max) => {
    let tplCanvas = document.createElement('canvas')
    let tplCtx = tplCanvas.getContext('2d')
    tplCanvas.width = tplCanvas.height = radius * 2
    let x = radius
    let y = radius
    // 径向渐变的点
    let gradient = tplCtx.createRadialGradient(x, y, radius * blur, x, y, radius)
    gradient.addColorStop(0, 'rgba(0,0,0,1)')
    gradient.addColorStop(1, 'rgba(0,0,0,0)')
    tplCtx.fillRect(0, 0, 2 * radius, 2 * radius)
    return tplCanvas
  }

绘制完成后,通过 canvas 的drawImage方法将每个点都设置到shadowCtx中。

shadowCanvas进行地图渲染,就可以看到其效果:

image.png

根据权重叠加灰度值

热力图值大的颜色深,小的浅。所以我们获取到需要渲染点的坐标跟count值后,对点数值进行不同程度的模糊,来区分点的多少。

 tplCtx.globalAlpha = (pointVal - min) / (max - min) // 给每个点设置灰度值

标准取色彩带

生成一个标准的彩带,给灰度点进行取色使用。

颜色的RGBA 每种都有0-255色阶。所以我们生成一个256px * 1px 的来存储对应的像素颜色。通过getImageData可以获取到这条彩带对应的像素集合palette。 一个像素由四位数值组成,例如生成的像素集合palette的前四个表示一个像素,以此类推。所以palette集合的长度为 256 * 4。

// 生成一个256px的彩带 获取像素点集合
_getColorPalette() {
  let gradientConfig = this.config.defaultGradient
  let paletteCanvas = document.createElement('canvas')
  let paletteCtx = paletteCanvas.getContext('2d')
  paletteCanvas.width = 256
  paletteCanvas.height = 1
  let gradient = paletteCtx.createLinearGradient(0, 0, 256, 1)
  for (let 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
}

根据不同权重值取色

通过getImageData获取到阴影canvas的上下文shadowCtx的像素集合imgData。与获取到的彩带像素集合palette,进行取色匹配。

image.png

比如:我们的第一个需要替换的灰度像素的rgba为(2,3,4,1),透明度为1,我们就应该对应去找彩带上的第一个像素点,进行匹配的话就需要将我们的透明度 1 * 4 来获取对应有色像素值。此时,找到的rgba为(0,0,255,255),替换rgb值,将灰度点变成有色点。

_colorResize() {
  let width = this._width
  let height = this._height
  let opacity = this._opacity
  let maxOpacity = this._maxOpacity
  let minOpacity = this._minOpacity
  let img = this.shadowCtx.getImageData(0, 0, width, height)
  let imgData = img.data // 我们画的点的像素集合
  let palette = this._getColorPalette() // 彩带的像素集合
  let len = imgData.length
  for (var i = 3; i < len; i += 4) {
        let alpha = imgData[i] // i=3 取第一个像素的透明度
        let offset = alpha * 4
        if (!offset) {
          continue
        }
        let 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] = finalAlpha
  }
  img.data = imgData
  this.ctx.putImageData(img, 0, 0)
}

设置完成后,将我们的有色点像素集合putImageData添加到最终的canvas上下文中。

image.png

灰度点变成了有色点渲染到我们的地图上,且未遮挡标注名称哦~

看看多个点的热力图效果吧:

image.png