hello 大家好,🙎🏻♀️🙋🏻♀️🙆🏻♀️
我是一个热爱知识传递,正在学习写作的作者,ClyingDeng
凳凳!
需求千千万,没遇到的依旧是千千万。
需求场景
产品:见过这样的热力图没?
我:见过!
产品:它是不是把那个文字遮住了
我:是的
产品:那让这个热力图不要遮住文字不然太影响美观了😁😁😁
我:emmm,需求很明确。。。就是觉得那里不太对劲🤔🤔🤔
产品一句话,那就开整!
技术方案
深入分析需求中🧐...
产品想要的是一个不被文字遮住的地图热力图展示功能。
在此案例中,选取的是百度地图作为demo,考虑可以适配其他腾讯、goole等其他地图,将热力图单独抽离,绘制成一个canvas,再通过地图的地面图层 API进行绘制添加。
Yes,这是完全可行的方案。
热力图原理
经调研热力图相关资料之后,发现热力图的实现主要有这四部曲:
-
绘制灰度点,使其径向渐变
-
针对不同点的数值,设置对应点的灰度值
-
准备一条标准的canvas彩带,根据不同灰度值的点,取色
-
将带有颜色的点画到最终的canvas上
热力图实现
相关功能划分
想要实现通用的热力图,我们就需要准备一个独立的 heatMap 对象。通过 heatMap 实例来调用其设置、移除、删除数据等方法。
这个 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
进行地图渲染,就可以看到其效果:
根据权重叠加灰度值
热力图值大的颜色深,小的浅。所以我们获取到需要渲染点的坐标跟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
,进行取色匹配。
比如:我们的第一个需要替换的灰度像素的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上下文中。
灰度点变成了有色点渲染到我们的地图上,且未遮挡标注名称哦~
看看多个点的热力图效果吧: