Hello,大家好。在当今数字化时代,理解用户行为成为了企业成功的关键之一。随着互联网的发展,用户与网站、应用和产品的互动变得愈发复杂而多样化。在这样的背景下,埋点系统成为了洞察用户行为的重要工具之一。而其中的热力图分析,则更加直观的帮助我们分析用户的喜好。
之前我们介绍了什么是热力图,以及它如何成为理解用户行为的有力工具。今天我们从技术的角度来看,如何实现热力图效果呢?
热力图是埋点系统必不可少的一项能力,可以来看看Webfunny一体化埋点系统的效果
一、有哪些免费工具可以实现热力图效果
热力图主要的实现方式,还是利用目前现有的开源工具,如:百度的echarts、阿里系的G2、还有就是我们今天要说的heatmap.js。
当然,你如果有兴趣和精力,可以自己手搓一个,原理也不是特别复杂,关于热力图的实现原理:
一般可大致归纳为以下几个步骤:
-
为每个数据点设置一个从中心向外灰度渐变的圆;
-
利用灰度可以叠加的原理,计算每个像素点数据交叉叠加得到的灰度值;
-
根据每个像素计算得到的灰度值,在一条彩色色带中进行颜色映射,最后对图像进行着色,得到热力图。
百度echarts的热力图效果
阿里系的G2热力图效果
heatmap.js热力图效果
根据效果来看,其中G2和heatmap.js的热力图效果都比较符合我们的使用场景,heatmap.js已经处理好颜色效果了,所以最后选择了heatmap.js。
二、如何采集网页上的热力数据
热力图在我们的印象中,主要的使用场景是在地图上,比如哪个地方温度,就会呈现红色,哪个地方的温度低就会呈现蓝色。而我们今天要做的是,采集网页上的热力图,主要包含三个指标的数据。
网页上又没有温度,哪来的热力值呢,他们分别是:鼠标点击量、鼠标停留时长、页面元素曝光时长
1. 点击热力数据
点击量热力图和好理解,点击的越多,热力值则越高。采集方式是通过监听全局点击事件,需要采集的几个主要的指标有:
页面地址:这个是用来确定是哪个页面的;
页面宽度/高度:这个是用来确定页面尺寸的;
鼠标点击位置(x坐标,y坐标):这个是用来锁定页面坐标,计算热力值的;
bash
/**
* 启动点击事件监听
*/
export function startClickRecord() {
window.addEventListener('click',function(e){
console.log('触发点击事件!', e)
if (!e) return
/** 检查定时器是否开启 */
if (store.timerStatus === 'off') {
console.log('定时器已结束,触发鼠标点击,则重新开启')
store.timerStatus = 'on';
startGlobalTimer()
}
try {
const scrollWidth = (document.body ? document.body.scrollWidth : 0) || window.innerWidth
const weTitle = document.title
const wePath = Utils.b64Code(Utils.getPath())
const weFullPath = Utils.b64Code(Utils.getPath('full'))
const weScrollWidth = scrollWidth - scrollWidth % 20
const weScrollHeigh = (document.body ? document.body.scrollHeight : 0) || window.innerHeight
const weXPath = Utils.getXPath(e)
const wePageX = e.pageX
const wePageY = e.pageY
// const weScrollX = window.scrollX
// const weScrollY = window.scrollY
const weRatio = parseInt(window.devicePixelRatio)
// 上报点击数据
const data = {weTitle, wePath, weScrollWidth, weScrollHeigh, weXPath, weFullPath, wePageX, wePageY, weRatio}
webfunnyGlobal.webfunnyEvent('Webfunny-Replace-HeatMapClickPointId').trackEvent(data);
} catch(e) {
console.error('click error', e)
}
}, true);
}
2. 鼠标停留时长热力图
点击量是直接反映用户达成的目标,鼠标停留时间则是反应了用户的兴趣之所在,也很重要。停留和点击的热力图很相似,只是停留时长的热力图数据会更密集一些。
鼠标停留时间的采集方式跟点击类似,通过监听mousemove事件进行采集,需要采集的几个主要的指标有:
页面地址:这个是用来确定是哪个页面的;
页面宽度/高度:这个是用来确定页面尺寸的;
鼠标点击位置(x坐标,y坐标):这个是用来锁定页面坐标,计算热力值的;
停留时间:这是一个关键性指标,这个值的上限是一个不确定的,但是它的上限对热力值的影响很大。
例如:如何设置很高,就会影响热力的准确性,用户鼠标放在哪里不动,那个点的热力值就会很高,其实他只有一个用户,并不能反馈出有价值的数据;如果设置很低,又无法反馈用户真是的停留时长了,所以这里的做成动态配置的最好。
建议:用户量小的应用,这个上限值设置低一些, 如:1000ms,因为个别用户会造成较大影响;用户量大的应用,设置稍微偏高些,2000ms,大量的用户会让拉平整体的数据,让数据趋于准确。
bash
/**
* 启动鼠标停留事件监听
*/
export function startMousemoveRecord() {
console.log('全埋点,鼠标停留时长,启动')
let timer
window.addEventListener('mousemove',function(e){
if (!e) return
if (timer) {
clearTimeout(timer)
}
/** 检查定时器是否开启 */
if (store.timerStatus === 'off') {
console.log('定时器已结束,触发鼠标移动,则重新开启')
store.timerStatus = 'on';
startGlobalTimer()
}
timer = setTimeout(() => {
try {
const weTitle = document.title
const scrollWidth = (document.body ? document.body.scrollWidth : 0) || window.innerWidth
const wePath = Utils.b64Code(Utils.getPath())
const weFullPath = Utils.b64Code(Utils.getPath('full'))
const weScrollWidth = scrollWidth - scrollWidth % 20
const weScrollHeigh = (document.body ? document.body.scrollHeight : 0) || window.innerHeight
const weXPath = Utils.getXPath(e)
const wePageX = e.pageX
const wePageY = e.pageY
// const weScrollX = window.scrollX
// const weScrollY = window.scrollY
const weRatio = parseInt(window.devicePixelRatio)
const mousemoveInfo = {weTitle, wePath, weScrollWidth, weScrollHeigh, weXPath, weFullPath, wePageX, wePageY, weRatio}
// 鼠标移动停留生效
console.log('鼠标移动停留生效:', store.mouseStayInfo)
if (!store.mouseStayInfo) {
// 如果没有记录信息,直接存入内存中
store.mouseStayInfo = { ...mousemoveInfo, startTime: new Date().getTime() }
} else {
// 如果有记录信息,就需要将之前的停留信息放进任务队列,并重新记录当前有效的停留
const nowTime = new Date().getTime()
const { startTime = 0 } = store.mouseStayInfo || {}
let timeDiff = nowTime - startTime
console.log('鼠标距离上次时间差:', timeDiff, wePageX, wePageY)
if (timeDiff > 150) {
// 如果停留时间超过上限, 则默认为上限时间
timeDiff = timeDiff > COMMON_FIELD.MOUSE_STAY_LIMIT ? COMMON_FIELD.MOUSE_STAY_LIMIT : timeDiff
const tempMousemoveInfo = { ...mousemoveInfo, stayTime: timeDiff }
// TaskQueue.addTask(config.trackUrl, tempMousemoveInfo)
// 上报鼠标移动数据
console.log('即将执行上报', tempMousemoveInfo)
webfunnyGlobal.webfunnyEvent('Webfunny-Replace-HeatMapStopPointId').trackEvent(tempMousemoveInfo);
store.mouseStayInfo = { ...mousemoveInfo, startTime: nowTime }
}
}
} catch(e) {
console.error('mousemove error: ', e)
}
}, 200)
}, true);
// 鼠标离开浏览器后,需要清理历史数据,延迟1s,防止
window.addEventListener('mouseout', function(e){
var tagName = e.target.tagName ? e.target.tagName.toLowerCase() : ''
if (tagName === 'html') {
setTimeout(function() {
store.mouseStayInfo = ''
console.log('鼠标移出了浏览器, 清理鼠标停留数据', store.mouseStayInfo)
}, 1000)
}
}, true)
}
三、如何将热力图覆盖到网页上呢
热力数据采集到了,怎么才能将它们正确的放到网页上呢。
其实也简单,上层是heatmap.js生成的热力效果图;下层是iframe,显示的是网页内容,这样热力图效果就呈现出来了。
需要注意的是:网页会滚动,宽度也不同,鼠标停留和点击的位置需要取相对位置,而不是绝对位置
四、heatmap.js生成热力图代码
生成heatmap对象,并将热力值数据一个个填充进去就可以了。
heatmap的配置项有很多,下方是我试验出来比较简单的配置项。
安装依赖:npm install heatmap.js --save
import h337 from "heatmap.js"
...
// 查找元素
const heatEle = document.getElementById(this.state.heatId)
heatmapInstance = h337.create({
container: heatEle,
// radius: 30,
// maxOpacity: 0.7
radius: 20, // 点的半径
// maxOpacity: 0.8, // 最大不透明度
// minOpacity: 0.2, // 最小不透明度
blur: 0.75, // 模糊半径
useLocalExtrema: true, // 是否使用局部极值
});
this.props.dataList.forEach((item) => {
heatmapInstance.addData({
x: item.x,
y: item.y,
value: item.value * 100
});
})
五、热力图细节优化
heatmap的热力效果虽然不错,但是我并未在文档中找到能提示热力值的API,这就有点尴尬了,因为热力图效果虽然好,但是没有热力值提示,总会让人觉得缺点什么
没办法,只有手动加一个了;虽然heatmap没有提供tip提示,但是却提供了获取热力值的API,这下就简单多了,添加鼠标移动事件,在鼠标位置上方添加div显示热力值,不要忘记延时显示和添加防抖哦
代码如下:
bash
let mousemoveTimer = 0
heatEle.addEventListener("mousemove", (e) => {
const { offsetX, offsetY } = e
const heatTipCon = heatmapInstance.getValueAt({ x: offsetX, y: offsetY })
if (mousemoveTimer) {
clearTimeout(mousemoveTimer)
mousemoveTimer = setTimeout(() => {
this.setState({heatTipCon: heatTipCon / 100, heatTipX: offsetX, heatTipY: offsetY - 20})
}, 300)
} else {
mousemoveTimer = setTimeout(() => {
this.setState({heatTipCon: heatTipCon / 100, heatTipX: offsetX, heatTipY: offsetY - 20})
}, 300)
}
})