起因
热力图原本的样式不太美观,热力图是由各种颜色色块按照不同的比例渲染出一定的范围区域的,但是从颜色到其它颜色之间是通过渐变的方式过渡的。而百度热力图是通过锯齿线进行过渡,相对来说美观很多。
具体实现
动态热力图,涉及heatmap.js的函数或者canvas函数
- globalAlpha透明度设置,通过透明度的相间覆盖来实现不同的渐变区域之间的层叠效果,主要目前就在于解决这个问题
// 每个点的值 与最大值之间的计算, 点的值-最小值/最大值-最小值来做透明度?
var value = Math.min(point.value, max);
var templateAlpha = (value - min) / (max - min);
// this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
shadowCtx.globalAlpha = templateAlpha < 0.01 ? 0.01 : templateAlpha;
createRadialGradient主要函数,用于生成径向/圆渐变- 还有一种image值的处理比较重点,但是目前尚未理解透彻
_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;
}
console.log('imgData', imgData)
img.data = imgData;
this.ctx.putImageData(img, x, y);
this._renderBoundaries = [1000, 1000, 0, 0];
},
静态热力图,涉及克里金统计方式生成图【百度地图目前的方案】
涉及主要的克里金js,什么是克里金插值
克里金插值又称空间局部插值法,是以半变异函数理论和结构分析为基础,在有限区域内对区域化变量进行无偏最优估计的一种方法,是地统计学的主要内容之一。南非矿产工程师D.R.Krige在寻找金矿时首次运用这种方法,法国著名统计学家G.Matheron随后将该方法理论化、系统化,并命名为Kriging,即克里金方法。——引自《地理信息系统空间分析实验教程》
克里金的适用条件
区域化变量要存在空间相关性,即半变异函数和结构分析的结果表明区域化变量存在空间相关性。
kriging.js github地址
克里金插值需要注意的点?
数据应符合前提假设。例如,普通克里金要求数据变化呈正态分布。数据应尽量充分,样本数尽量大于80,每一种距离间隔分类中的样本对数尽量多余10对。在具体建模过程中,很多参数是可调的,且每个参数对结果的影响不同。当数据足够多时,各种插值方法的效果基本相同。(我理解的数据足够多,是指数据量在500以上)
克里金更详细的说明
还有查看arcGIS的官方手册
百度热力图的相关信息
11年推出的大数据可视化项目,基于手机的热力图应用,按照位置聚类,计算人群密度,很强的时效性15分钟更新一次。
颜色层级
代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>leaflet克里金空间插值</title>
<style>
html,
body,
#map {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
cursor: default;
}
</style>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" />
<script src="https://cdn.bootcss.com/jquery/3.0.0/jquery.min.js"></script>
<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/esri-leaflet@2.2.4/dist/esri-leaflet.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script src="kriging.js"></script>
<script src="point.js"></script>
<script src="world.js"></script>
<script src="tifData.js"></script>
</head>
<body>
<canvas id="canvasMap" style="display: none;"></canvas>
<div id="map"></div>
<script>
function getRandomArrayElements(arr, count) {
var shuffled = arr.slice(0), i = arr.length, min = i - count, temp, index;
while (i-- > min) {
index = Math.floor((i + 1) * Math.random());
temp = shuffled[index];
shuffled[index] = shuffled[i];
shuffled[i] = temp;
}
return shuffled.slice(min);
}
// 找出一个圈子范围的点 1公里
tempEndData = _.orderBy(endData, ['y', 'x'], ['asc', 'asc']);
endData = [];
tempEndData.forEach(e => {
let one = L.latLng(e.y, e.x);
let two = L.latLng(tempEndData[0].y, tempEndData[0].x);
let tempDistance = one.distanceTo(two);
if (tempDistance < 3000) {
endData.push(e);
}
// if (tempDistance < 3000) {
// endData.push(e);
// }
});
// .slice(0, 170);
// endData = endData.slice(0, 2000);
// endData = getRandomArrayElements(endData, 500);
console.info(points);
console.info(points.map(e => e.attributes.TN_))
console.info(endData);
points = []
world = [[]];
// 抽稀,过滤点的函数
function chouXi(data) {
let newData = [];
newData.push(data[0]);
for (let index = 0; index < data.length; index++) {
const e = data[index];
const e2 = data[index + 1];
if ((e2 && e2.y) && (e2.y === e.y) && (Number(e2.ColorValue) !== Number(e.ColorValue))) {
newData.push(e2);
}
}
return newData;
}
// endData = chouXi(endData);
// debugger;
endDataIndex = 0;
endData.forEach(e => {
points.push({
"attributes": {
"FID": endDataIndex,
"NAME": "",
"TN_": Number(e.ColorValue),
},
"geometry": {
"x": Number(e.x),
"y": Number(e.y)
}
});
endDataIndex++;
})
// 找出四个角的点
maxY = endData.map(e => Number(e.y)).reduce((xx, yy) => {
return xx > yy ? xx : yy;
})
maxX = endData.map(e => Number(e.x)).reduce((xx, yy) => {
return xx > yy ? xx : yy;
})
minY = endData.map(e => Number(e.y)).reduce((xx, yy) => {
return xx < yy ? xx : yy;
})
minX = endData.map(e => Number(e.x)).reduce((xx, yy) => {
return xx < yy ? xx : yy;
})
console.log(maxY, minY, maxX, minX)
world[0].push([minX, maxY]);
world[0].push([maxX, maxY]);
world[0].push([maxX, minY]);
world[0].push([minX, minY]);
// 回归
world[0].push([minX, maxY]);
console.info(points);
var map = L.map('map', {
// center: [38.65953686, 120.8696333],
// center: [23.04972597, 113.3160423],
center: [23.08139498, 113.3289766],
zoom: 14
});
L.esri.tiledMapLayer({
url: 'http://map.geoq.cn/ArcGIS/rest/services/ChinaOnlineCommunity/MapServer/tile/{z}/{y}/{x}'
}).addTo(map);
//world.js,是插值之后需要裁切的图形边界信息【之前的案例,目前没用到】
//point.js,是要插值的离散的点【之前的案例,目前没用到】
//tifData.js 最终要处理的值
//遍历world边界数据,生成scope边界线
var positions = [];
world[0].forEach(function (point) {
positions.push([point[1], point[0]]);
});
var scope = L.polyline(positions, { color: 'red' }).addTo(map);
map.fitBounds(scope.getBounds());
//根据scope边界线的范围,计算范围变量
var xlim = [scope.getBounds()._southWest.lng, scope.getBounds()._northEast.lng];
var ylim = [scope.getBounds()._southWest.lat, scope.getBounds()._northEast.lat];
console.info(xlim, ylim);
//进行克里金插值
function loadkriging() {
var canvas = document.getElementById("canvasMap");
canvas.width = 1000;
canvas.height = 1000;
var n = points.length;
var t = [];//数值
var x = [];//经度
var y = [];//纬度
for (var i = 0; i < n; i++) {
t.push(points[i].attributes.TN_);
x.push(points[i].geometry.x);
y.push(points[i].geometry.y);
// 打上点
L.circle([y[i], x[i]], { radius: 1 }).addTo(map);
}
//对数据集进行训练
var variogram = kriging.train(t, x, y, "exponential", 0, 100);
//使用variogram对象使polygons描述的地理位置内的格网元素具备不一样的预测值,最后一个参数,是插值格点精度大小
// var grid = kriging.grid(world, variogram, (ylim[1] - ylim[0]) / 150);
var grid = kriging.grid(world, variogram, (ylim[1] - ylim[0]) / 500);
var colors = [
// "#006837",
// "#1a9850",
// "#66bd63",
// "#a6d96a",
// "#d9ef8b",
// "#ffffbf",
// "#fee08b",
// "#fdae61",
// "#f46d43",
// "#d73027",
// "#a50026"
'rgb(142,142,232)',
'rgb(131,161,261)',
'rgb(141,219,223)',
'rgb(132,222,118)',
'rgb(221,223,110)',
'rgb(209, 127, 65)',
'rgb(208, 33, 0)'
// 'rgb(115,118,221)',
// 'rgb(138,226,231)',
// 'rgb(123,225,126)',
// 'rgb(222,222,121)',
// 'rgb(217,127,74)',
// 'rgb(213,60,62)',
];
//将得到的格网grid渲染至canvas上
kriging.plot(canvas, grid, [xlim[0], xlim[1]], [ylim[0], ylim[1]], colors);
}
//将canvas对象转换成image的URL
function returnImgae() {
var mycanvas = document.getElementById("canvasMap");
return mycanvas.toDataURL("image/png");
}
loadkriging();
var imageBounds = [[ylim[0], xlim[0]], [ylim[1], xlim[1]]];
L.imageOverlay(returnImgae(), imageBounds, { opacity: 0.8 }).addTo(map);
</script>
</body>
</html>
涉及知识点
- 目前市面上的热力图基本都是用heatmap.js来实现,或者是第三方地图自己写的一套热力图方案,不过第三方地图也有可能是引用heatmap.js来实现的,基本原理是一致的。
- 要实现这种自动绘制的方式,需要用到地图的自定义canvas绘图
- 喷漆效果的实现,通过调研知道实际上使用的克里金生成地图的方式
- 传统热力图主函数用两种渐变方式去处理的。两种渐变的理解,一种是线性渐变,另一种是径向/圆渐变。
- 传统的动态热力图的不同,百度用得是静态热力图
- 什么是geojson格式,基于json的地理空间信息数据交换格式
GeoJSON是一种对各种地理数据结构进行编码的格式,基于Javascript对象表示法(JavaScript Object Notation, 简称JSON)的地理空间信息数据交换格式。GeoJSON对象可以表示几何、特征或者特征集合。GeoJSON支持下面几何类型:点、线、面、多点、多线、多面和几何集合。GeoJSON里的特征包含一个几何对象和其他属性,特征集合表示一系列特征。