热力图将一团坐标点转换为即时可读的密度表面。炎热的颜色标记事件聚集的位置;冷色标记事件稀少的位置。用户甚至在读一个标签之前就能掌握这个模式。这就是为什么热力图是人流量分析、配送需求预测、犯罪报告工具和房产价格研究的默认选择。
本教程使用 MapAtlas SDK 从头开始构建交互式热力图。你将把点数据结构化为加权 GeoJSON,使用自定义颜色渐变呈现密度图层,连接强度滑块,最后完成一个真实的配送需求示例。在教程结束时,你将拥有一个可复用的模式,可以放入任何 JavaScript 或 React 项目中。
如果你是 MapAtlas SDK 的新手,请首先阅读 如何向网站添加交互式地图。它涵盖了安装、地图初始化和标记。本教程从那个教程的结束处继续。
何时使用热力图
当你有大量的单个点位置并想表达密度而非身份时,热力图是正确的工具。超过几百个点后,单个标记就失去了意义;热力图揭示了无法通过固定标记实现的结构。
常见用例包括:
- 人流量分析:零售店址选址、城市规划、活动人群建模。为每一个进过大门的顾客投放一个 GPS ping,热力图就会显示城市、购物中心或场馆的哪些区域吸引最多人流。
- 配送需求:按邮编或原始坐标聚合订单来源,向调度员展示需求集中的位置。这直接用于区域规划和司机分配。
- 犯罪和事件数据:警务分析仪表板、保险风险地图和公共安全报告工具都使用密度热力图来传达空间风险,而不会用单个标记压倒用户。
- 房产价格梯度:与加权值结合时,热力图可以显示城市各处价格最高的位置。详见 房地产地图教程 了解更多有关构建房产地图工具的信息。
如果你的问题是「事情最常发生在哪里」,热力图就是你的答案。
数据格式:加权 GeoJSON 点
MapAtlas 热力图图层读取标准 GeoJSON 「Point」特征的「FeatureCollection」。每个特征都可以携带一个「weight」属性,它可以缩放其对密度表面的贡献。包含 10 件物品的配送订单贡献的热量比单件订单多;主要交通枢纽产生的人流量比侧街多。
const demandData = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.8952, 52.3702] },
properties: { weight: 8, zone: "centrum" }
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9123, 52.3601] },
properties: { weight: 3, zone: "oost" }
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.8801, 52.3780] },
properties: { weight: 12, zone: "west" }
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9041, 52.3540] },
properties: { weight: 1, zone: "south" }
}
]
};
「weight」值是无量纲的。你需要相对于数据集范围对其进行标准化。如果需求最高的区域产生 500 个订单,最安静的区域产生 10 个,则在传入前将它们映射到 1 到 10 的范围。这使热力图在绝对数量差异很大的数据集中保持视觉上的有意义。
如果没有「weight」属性,每个点的贡献相等,热力图反映纯计数密度。
Prerequisites
Before you start:
- A MapAtlas API key (sign up free, no credit card required)
- Node.js 18+ for npm-based projects, or a plain HTML page if you prefer the CDN
第 1 步:安装和初始化地图
安装 SDK:
npm install @mapmetrics/mapmetrics-gl
或通过 CDN 在纯 HTML 文件中加载:
<link
rel="stylesheet"
href="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css"
/>
<script src="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.js"></script>
添加具有定义高度的容器:
<div id="map" style="width: 100%; height: 600px;"></div>
初始化地图。深色样式使热力图颜色渐变在背景中突出,这就是为什么它是密度可视化的首选基础:
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'map',
style: 'https://tiles.mapatlas.eu/styles/dark/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676], // Amsterdam
zoom: 12,
});
第 2 步:添加热力图图层
将你的 GeoJSON 注册为源,然后添加从其读取的热力图图层。「map.on('load', ...)」回调确保样式在你修改之前已完成加载。
map.on('load', () => {
map.addSource('demand', {
type: 'geojson',
data: demandData,
});
map.addLayer({
id: 'demand-heatmap',
type: 'heatmap',
source: 'demand',
paint: {
// Weight each point by its 'weight' property (defaults to 1 if absent)
'heatmap-weight': [
'interpolate', ['linear'],
['get', 'weight'],
0, 0,
12, 1
],
// Radius in pixels; larger = smoother but less precise
'heatmap-radius': 30,
// Overall opacity of the heatmap layer
'heatmap-opacity': 0.85,
},
});
});
此时你已有一个有效的热力图。蓝绿色区域表示低密度;默认渐变在高密度处向黄色和红色转移。下一步用刻意的颜色方案替换默认调色板。
第 3 步:自定义颜色渐变
「heatmap-color」属性使用与 MapAtlas 绘制系统其他部分相同的「interpolate」表达式将密度值(0 到 1)映射到颜色。密度 0 是透明的,所以地图底层在空区域显示。
map.addLayer({
id: 'demand-heatmap',
type: 'heatmap',
source: 'demand',
paint: {
'heatmap-weight': [
'interpolate', ['linear'],
['get', 'weight'],
0, 0,
12, 1
],
'heatmap-radius': 30,
'heatmap-opacity': 0.85,
'heatmap-color': [
'interpolate', ['linear'],
['heatmap-density'],
0, 'rgba(0, 0, 255, 0)', // transparent at zero density
0.2, 'rgba(0, 128, 255, 0.6)',
0.4, 'rgba(0, 230, 200, 0.7)',
0.6, 'rgba(100, 230, 0, 0.8)',
0.8, 'rgba(255, 200, 0, 0.9)',
1.0, 'rgba(255, 50, 0, 1)' // bright red at peak density
],
},
});
这个渐变从蓝色(稀疏)通过绿色和黄色到红色(密集),符合大多数用户从天气雷达图和热成像带来的心智模型。如果你的应用有品牌颜色调色板,用你自己的颜色值替换 RGB 值;插值自动处理平滑过渡。
设计提示: 对于专业仪表板和分析工具,保持低密度端接近透明。这在安静区域保持基础地图的可读性,并自然地吸引眼睛到热点区域。
第 4 步:添加半径滑块进行交互控制
热力图半径控制每个点「辐射」影响的距离。小半径(10 到 15px)显示细粒度簇;大半径(50 到 80px)产生更宽、更平滑的表面。不同的用例需要不同的默认值:密集城市中心的人流量需要小半径,全国范围的配送需求需要大半径。
为用户提供一个滑块,使他们可以动态调整:
<div id="controls" style="position: absolute; top: 16px; left: 16px; z-index: 1;
background: white; padding: 12px 16px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.2);">
<label style="font-size: 14px; font-weight: 600;">
Radius: <span id="radius-value">30</span>px
</label>
<br />
<input id="radius-slider" type="range" min="5" max="80" value="30" style="width: 180px; margin-top: 6px;" />
</div>
将滑块连接到「setPaintProperty」,它更新图层而不重新添加它:
const slider = document.getElementById('radius-slider');
const radiusLabel = document.getElementById('radius-value');
slider.addEventListener('input', () => {
const radius = Number(slider.value);
radiusLabel.textContent = radius;
map.setPaintProperty('demand-heatmap', 'heatmap-radius', radius);
});
「setPaintProperty」是无闪烁的实时更新。热力图在 GPU 上的同一帧中重新渲染。此模式适用于任何绘制属性:不透明度、强度、颜色停止点。
第 5 步:按缩放级别调整强度
在低缩放级别(城市级视图)下,附近的点大量重叠,热力图看起来均匀饱和。在高缩放(街道级)下,相同的点分散开,密度表面看起来稀疏。缩放关联的强度补偿使可视化在每个缩放级别都保持可读性。
paint: {
// ...other paint properties...
'heatmap-intensity': [
'interpolate', ['linear'],
['zoom'],
8, 1, // low zoom: normal intensity
14, 3 // high zoom: boost intensity to compensate for point spread
],
'heatmap-radius': [
'interpolate', ['linear'],
['zoom'],
8, 20, // small radius at city scale
14, 50 // larger radius at street scale
],
},
两个属性都使用相同的「zoom」上的「interpolate」表达式。停止点之间的值是线性插值的,所以用户缩放时过渡是平滑的。
真实示例:配送需求热力图
以下是配送需求仪表板的完整独立实现。订单从 API 以 GeoJSON「FeatureCollection」的形式到达。每当用户更改时间过滤器时,热力图就会更新。
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const API_KEY = 'YOUR_API_KEY';
const map = new mapmetricsgl.Map({
container: 'map',
style: `https://tiles.mapatlas.eu/styles/dark/style.json?key=${API_KEY}`,
center: [4.9041, 52.3676],
zoom: 11,
});
async function loadOrders(hourFrom, hourTo) {
const res = await fetch(
`/api/orders/geojson?hour_from=${hourFrom}&hour_to=${hourTo}`
);
return res.json(); // returns a GeoJSON FeatureCollection
}
map.on('load', async () => {
const orders = await loadOrders(8, 12); // morning peak
map.addSource('orders', { type: 'geojson', data: orders });
map.addLayer({
id: 'order-heatmap',
type: 'heatmap',
source: 'orders',
paint: {
'heatmap-weight': [
'interpolate', ['linear'], ['get', 'items'],
0, 0, 20, 1
],
'heatmap-color': [
'interpolate', ['linear'], ['heatmap-density'],
0, 'rgba(0, 0, 255, 0)',
0.2, 'rgba(0, 128, 255, 0.5)',
0.5, 'rgba(0, 230, 150, 0.8)',
0.8, 'rgba(255, 200, 0, 0.9)',
1.0, 'rgba(255, 50, 0, 1)'
],
'heatmap-radius': [
'interpolate', ['linear'], ['zoom'],
8, 20, 14, 50
],
'heatmap-intensity': [
'interpolate', ['linear'], ['zoom'],
8, 1, 14, 3
],
'heatmap-opacity': 0.9,
},
});
// Time-of-day filter buttons
document.querySelectorAll('[data-hour-range]').forEach((btn) => {
btn.addEventListener('click', async () => {
const [from, to] = btn.dataset.hourRange.split('-').map(Number);
const newOrders = await loadOrders(from, to);
map.getSource('orders').setData(newOrders);
});
});
});
现有源上的「setData」调用替换 GeoJSON 而不重新添加图层。热力图自动重新渲染。此模式可扩展到任何基于时间的过滤器:一天中的小时、星期几、天气条件。
对于通常伴随配送需求热力图的路线规划图层,见 路线优化 API 教程。在需求热力图上叠加优化的配送路线,为调度员提供单一视图中的完整操作图景。
大型数据集性能提示
MapAtlas 热力图图层使用 WebGL 并快速渲染,但供应它的数据管道在扩展时可能成为瓶颈。
对非常大的数据集在服务器上预聚合。 如果你有数百万个 GPS ping,不要将它们全部发送到浏览器。运行服务器端空间聚合(H3 六边形网格、四叉树或简单网格舍入),将你的 100 万个原始点减少到 10,000 个网格单元,每个都有「count」和「weight」字段。热力图对用户来说看起来相同,加载时间会大大缩短。
增量流式传输更新。 对于实时数据(实时人流量、实时订单放置),使用「setData」和最近点的滚动窗口,而不是累积不断增长的 GeoJSON 对象。保持源以固定的最大点数,并淘汰旧记录。
在源上使用「maxzoom」。 将「maxzoom: 14」添加到你的「addSource」调用告诉 SDK 停止在缩放 14 以上请求切片数据。对于热力图,这很少重要,因为热力图图层读取单个平面 GeoJSON 源而不是切片数据,但它防止在高缩放级别上进行不必要的重新处理。
降低绘制属性复杂性。 绘制表达式中的每个额外「interpolate」停止点都会增加每帧的 GPU 评估成本。对于移动设备应用,将颜色渐变简化为三或四个停止点,并在低优先级视图上放弃缩放关联的半径/强度缩放。
懒初始化地图。 将整个地图初始化包装在「IntersectionObserver」回调中,以便它仅在地图容器滚入视图时运行。这会将 SDK 包从初始页面加载推迟,在地图位于页面下方的营销页面上特别有价值。
有关地图性能模式的更深入探讨,包括延迟加载和聚类,如何向网站添加交互式地图 中的生产检查清单涵盖完整列表。
React Integration
Wrapping the heatmap in a React component follows the same pattern as any MapAtlas map: initialise in useEffect, expose state for the slider and filter via useState, and clean up on unmount.
import { useEffect, useRef, useState } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
export function DeliveryHeatmap({ geojson, apiKey }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
const [radius, setRadius] = useState(30);
useEffect(() => {
const map = new mapmetricsgl.Map({
container: containerRef.current,
style: `https://tiles.mapatlas.eu/styles/dark/style.json?key=${apiKey}`,
center: [4.9041, 52.3676],
zoom: 11,
});
mapRef.current = map;
map.on('load', () => {
map.addSource('orders', { type: 'geojson', data: geojson });
map.addLayer({
id: 'order-heatmap',
type: 'heatmap',
source: 'orders',
paint: {
'heatmap-radius': radius,
'heatmap-opacity': 0.9,
},
});
});
return () => map.remove();
}, [apiKey]);
// Update radius without remounting the map
useEffect(() => {
const map = mapRef.current;
if (!map || !map.getLayer('order-heatmap')) return;
map.setPaintProperty('order-heatmap', 'heatmap-radius', radius);
}, [radius]);
return (
<div>
<div style={{ padding: '8px 0' }}>
<label>
Radius: {radius}px
<input
type="range" min={5} max={80} value={radius}
onChange={e => setRadius(Number(e.target.value))}
style={{ marginLeft: 8, width: 160 }}
/>
</label>
</div>
<div ref={containerRef} style={{ width: '100%', height: '600px' }} />
</div>
);
}
In Next.js, import this component with dynamic(() => import('./DeliveryHeatmap'), { ssr: false }) to avoid server-side rendering errors from the browser-only SDK.
What to Build Next
You now have a working interactive heatmap with weighted data, a custom colour gradient, a radius slider, and zoom-linked intensity. Here is where to take it:
- Overlay a routing layer on top of the heatmap to show planned delivery routes against the demand surface. The Route Optimization API tutorial walks through the full implementation.
- Add a time animation: cycle through hourly snapshots with a
setIntervalloop callingsetDataon the source. This turns a static density map into a time-lapse of how demand moves through the day. - Combine with property price data to show price gradients by neighbourhood. The Real Estate Property Map tutorial covers weighted data patterns for property platforms.
- Check the MapAtlas pricing page to find the right plan for your production traffic.
Full SDK reference and additional examples are available at docs.mapatlas.xyz.
Frequently Asked Questions
What is the difference between a heatmap and a choropleth map?
A heatmap visualises point density by blending nearby points into a continuous colour gradient, ideal for raw coordinate data like GPS pings or event locations. A choropleth map colours pre-defined geographic areas (countries, postcodes, census tracts) by a statistical value. Use a heatmap when you have many individual points; use a choropleth when your data is already aggregated by region.
How many data points can a JavaScript heatmap handle?
The MapAtlas heatmap layer renders on the GPU via WebGL, so it handles tens of thousands of points without frame drops at normal zoom levels. Above roughly 500,000 points, pre-aggregating your data server-side into a lower-resolution grid and switching to a GeoJSON fill-extrusion or circle layer gives better performance on low-end devices.
Can I use MapAtlas heatmaps for free?
Yes. MapAtlas has a free tier that includes map tile rendering, GeoJSON layer support, and heatmap layers. The free plan covers development and low-volume production use. See mapatlas.eu/pricing for full plan details.
Do heatmaps work on mobile browsers?
Yes. The MapAtlas SDK uses WebGL for rendering, which is supported in all modern mobile browsers including Safari on iOS and Chrome on Android. For very large datasets on low-end mobile hardware, reducing the point count or increasing the heatmap radius keeps frame rates smooth.
Related reading:

